nip13: let minePow keep a caller-chosen created_at (NIP-59 wraps) (#552)

* nip13: let minePow keep a caller-chosen created_at

minePow refreshes created_at to the current second while it mines, which is
right for a plain note but wrong for anything that has already chosen its
timestamp: NIP-59 seals and gift wraps carry a randomized created_at up to
two days in the past, and callers mining those lost it. This adds an opt-in
keepCreatedAt option (default behaviour unchanged) with a test.

* Delete bun.lock

---------

Co-authored-by: Ronish Bhatt <16808243+sunmoonron@users.noreply.github.com>
This commit is contained in:
BCR
2026-09-08 15:57:15 -03:00
committed by GitHub
co-authored by Ronish Bhatt
parent 0b4f7710bc
commit 336ff25710
2 changed files with 38 additions and 5 deletions
+20
View File
@@ -28,3 +28,23 @@ test('mines POW for an event', async () => {
expect(getPow(event.id)).toBeGreaterThanOrEqual(difficulty) expect(getPow(event.id)).toBeGreaterThanOrEqual(difficulty)
}) })
test('keeps a caller-chosen created_at when asked', async () => {
const difficulty = 10
const createdAt = 1700000000 // a fixed, past timestamp, as NIP-59 wraps use
const event = minePow(
{
kind: 1,
tags: [],
content: 'Hello, world!',
created_at: createdAt,
pubkey: '79c2cae114ea28a981e7559b4fe7854a473521a8d22a66bbab9fa248eb820ff6',
},
difficulty,
{ keepCreatedAt: true },
)
expect(event.created_at).toEqual(createdAt)
expect(getPow(event.id)).toBeGreaterThanOrEqual(difficulty)
})
+18 -5
View File
@@ -38,11 +38,22 @@ function getPowFromBytes(hash: Uint8Array): number {
return count return count
} }
export type MinePowOptions = {
/**
* By default the miner refreshes `created_at` to the current second while it works and restarts the
* nonce whenever the second changes, so a long mining run still yields a fresh timestamp. Set this
* to `true` to leave `created_at` exactly as given. NIP-59 gift wraps and seals must carry a
* randomized timestamp (up to two days in the past), and any other caller that has already chosen a
* `created_at` needs it to survive mining.
*/
keepCreatedAt?: boolean
}
/** /**
* Mine an event with the desired POW. This function mutates the event. * Mine an event with the desired POW. This function mutates the event.
* Note that this operation is synchronous and should be run in a worker context to avoid blocking the main thread. * Note that this operation is synchronous and should be run in a worker context to avoid blocking the main thread.
*/ */
export function minePow(unsigned: UnsignedEvent, difficulty: number): Omit<Event, 'sig'> { export function minePow(unsigned: UnsignedEvent, difficulty: number, options: MinePowOptions = {}): Omit<Event, 'sig'> {
let count = 0 let count = 0
const event = unsigned as Omit<Event, 'sig'> const event = unsigned as Omit<Event, 'sig'>
@@ -51,11 +62,13 @@ export function minePow(unsigned: UnsignedEvent, difficulty: number): Omit<Event
event.tags.push(tag) event.tags.push(tag)
while (true) { while (true) {
const now = Math.floor(new Date().getTime() / 1000) if (!options.keepCreatedAt) {
const now = Math.floor(new Date().getTime() / 1000)
if (now !== event.created_at) { if (now !== event.created_at) {
count = 0 count = 0
event.created_at = now event.created_at = now
}
} }
tag[1] = (++count).toString() tag[1] = (++count).toString()