From cfae4fa5271191abc6ed2d06f46263e9d1eff6bb Mon Sep 17 00:00:00 2001 From: arkin0x Date: Fri, 22 Mar 2024 12:22:34 -0500 Subject: [PATCH 1/4] adding web worker-based POW to NDK --- ndk/src/events/index.ts | 11 +++++ ndk/src/events/nip13/Miner.ts | 72 ++++++++++++++++++++++++++++ ndk/src/events/nip13/nip13.ts | 88 +++++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 ndk/src/events/nip13/Miner.ts create mode 100644 ndk/src/events/nip13/nip13.ts diff --git a/ndk/src/events/index.ts b/ndk/src/events/index.ts index efa28fe1..64eeb05a 100644 --- a/ndk/src/events/index.ts +++ b/ndk/src/events/index.ts @@ -16,6 +16,7 @@ import { NDKKind } from "./kinds/index.js"; import { decrypt, encrypt } from "./nip04.js"; import { encode } from "./nip19.js"; import { repost } from "./repost.js"; +import { pow } from "./nip13/nip13"; export type NDKEventId = string; export type NDKTag = string[]; @@ -632,6 +633,16 @@ export class NDKEvent extends EventEmitter { return e; } + /** + * NIP-13 Proof of Work + * @param target The target to reach + */ + async pow(target: number): Promise { + if (!this.ndk) throw new Error("No NDK instance found"); + + return pow.call(this, target); + } + /** * Checks whether the event is valid per underlying NIPs. * diff --git a/ndk/src/events/nip13/Miner.ts b/ndk/src/events/nip13/Miner.ts new file mode 100644 index 00000000..9c2656f9 --- /dev/null +++ b/ndk/src/events/nip13/Miner.ts @@ -0,0 +1,72 @@ +import type { UnsignedEvent } from "nostr-tools"; + +/** + * Determine the beginning and ending index of the nonce in the serialized event + * @param serializedEvent string + * @returns beginning and end index of the nonce in the buffer + */ +export const getNonceBounds = (serializedEvent: string): [number, number] => { + const nonceTag = '"nonce","'; + const nonceStart = serializedEvent.indexOf(nonceTag) + nonceTag.length; + const nonceEnd = serializedEvent.indexOf('"', nonceStart); + return [nonceStart, nonceEnd]; +}; + +/** + * Deserialize a nostr event from a string + * @param serializedEvent string + * @returns UnsignedEvent + */ +export const deserializeEvent = (serializedEvent: string): UnsignedEvent => { + const eventArray = JSON.parse(serializedEvent); + return { + pubkey: eventArray[1], + created_at: eventArray[2], + kind: eventArray[3], + tags: eventArray[4], + content: eventArray[5], + }; +}; + +export const incrementNonceBuffer = (buffer: Uint8Array, startIndex: number, endIndex: number): Uint8Array => { + // go from right to left to update count, because the number is big-endian + for (let i = endIndex-1; i >= startIndex; i--) { + if (buffer[i] === 63) { + // we are using 16 UTF-8 symbols between decimal 48 and 63 (0-9, :, ;, <, =, >, ?) + // 16 nonce digits * 4 bits per digit = 64 bits of possible entropy, which is more than enough for a nonce, especially since the created_at will be incremented and serve as entropy too. + // wrap around if the symbol is 63 (?) and set to 48 (0) + buffer[i] = 48; + } else { + buffer[i]++; + break; + } + } + return buffer; +}; + +export const setNonceBuffer = (buffer: Uint8Array, startIndex: number, endIndex: number, nonce: number): Uint8Array => { + + // Convert the nonce back to a big-endian array of bytes + for (let i = endIndex - 1; i >= startIndex; i--) { + buffer[i] = (nonce & 0xF) + 48; + nonce = nonce >> 4; + } + + return buffer; +}; + +export function countLeadingZeroesBin(binary: Uint8Array) { + let count = 0; + + for (let i = 0; i < binary.length; i++) { + const byte = binary[i]; + if (byte === 0) { + count += 8; + } else { + count += Math.clz32(byte) - 24; + break; + } + } + + return count; +} \ No newline at end of file diff --git a/ndk/src/events/nip13/nip13.ts b/ndk/src/events/nip13/nip13.ts new file mode 100644 index 00000000..02c84a1b --- /dev/null +++ b/ndk/src/events/nip13/nip13.ts @@ -0,0 +1,88 @@ +import { serializeEvent } from "nostr-tools"; +import type { UnsignedEvent } from "nostr-tools"; +import type { NDKEvent } from "../index.js"; +import Worker from './pow.worker.js'; +import { deserializeEvent, getNonceBounds } from "./Miner"; + +let worker: Worker; + +export async function pow(this: NDKEvent, target: number) { + if (!this.ndk) throw new Error("No NDK instance found!"); + + const intTarget = Math.floor(target); + + // check if event has necessary props: + if (!this.kind) throw new Error("NIP-13: Event has no kind"); + if (!this.created_at) throw new Error("NIP-13: Event has no created_at"); + if (!this.pubkey) throw new Error("NIP-13: Event has no pubkey"); + if (!this.content) console.warn("NIP-13: Event has no content"); + if (intTarget !== target) console.warn("NIP-13: Target is not an integer") + + // can't do POW if these things are already present + if (this.id) throw new Error("NIP-13: Event already has an id"); + if (this.sig) throw new Error("NIP-13: Event already has a signature"); + + // spawn worker + if (!worker) { + worker = new Worker(); + } + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const _event = this; + + return new Promise((resolve, reject) => { + + // declare handler for worker messages in closure + function handleMinedPOW(msg: MessageEvent) { + if (msg.data.status === 'pow-target-found') { + worker.postMessage({command: 'stop'}); // not strictly necessary as the worker stops on its own when the target is found + const binary = msg.data.binary; + const eventSerialized = new TextDecoder().decode(binary); + const raw = deserializeEvent(eventSerialized); + // copy the successful nonce tag to the event + _event.tags[0][1] = raw.tags[0][1]; + + // resolve promise with the updated event + resolve(_event); + } + } + + // attach handler to worker + worker.onmessage = handleMinedPOW; + + // add nonce tag to event + const nonceTagIndex = _event.tags.findIndex((tag) => tag[0] === "nonce"); + if (nonceTagIndex) { + // remove old nonce tag + _event.tags.splice(nonceTagIndex, 1); + } + // we use unshift so that the nonce tag is first. This is important because we need to know the byte position of the nonce tag in the serialized event, and if any tags come before it which contain multibyte characters, the byte position will be off. + _event.tags.unshift(["nonce", '0000000000000000', intTarget.toString()]); + + // serialize event + const serialized = serializeEvent(_event.rawEvent() as UnsignedEvent); + + // get nonce bounds + const bounds: [number, number] = getNonceBounds(serialized); + + // binary event + const binary = new TextEncoder().encode(serialized); + + // send event to worker for mining + worker.postMessage({ + command: 'start', + data: { + thread: 1, + threadCount: 1, + binary, + nonceOffset: 2**20, + nonceBounds: bounds, + nonceStartValue: 0, + nonceEndValue: 2**20, // ~1 million + targetPOW: intTarget, + } + }); + + }) as Promise; + +} \ No newline at end of file From 30b0e6d775980618875af146a07d52130e1ca8b0 Mon Sep 17 00:00:00 2001 From: arkin0x <99223753+arkin0x@users.noreply.github.com> Date: Tue, 26 Mar 2024 08:59:41 -0500 Subject: [PATCH 2/4] changed pow.worker.ts from .js so it isn't .gitignored --- ndk/src/events/nip13/pow.worker.ts | 90 ++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 ndk/src/events/nip13/pow.worker.ts diff --git a/ndk/src/events/nip13/pow.worker.ts b/ndk/src/events/nip13/pow.worker.ts new file mode 100644 index 00000000..7a40b77e --- /dev/null +++ b/ndk/src/events/nip13/pow.worker.ts @@ -0,0 +1,90 @@ +import { sha256 } from '@noble/hashes/sha256'; +import { incrementNonceBuffer, setNonceBuffer, countLeadingZeroesBin } from "./Miner"; + +let threadID = undefined; +let threadCountNum = 0; +let NONCE_OFFSET = 0; +let active = false; +let currentNonce = 0; + +self.onmessage = function(message) { + const { command, data } = message.data; + if (data.thread) threadID = data.thread; + if (data.threadCount) threadCountNum = data.threadCount; + if (data.nonceOffset) NONCE_OFFSET = data.nonceOffset; + switch (command) { + case 'start': + safeInterrupt(data); + break; + case 'stop': + active = false; + break; + } +}; + +// if we call 'start' while mining, we need to stop mining, wait a tick, then start again to allow the previous mining loop to finish +function safeInterrupt(data) { + active = false; + setTimeout(() => { + active = true; + initiateMining(data); + }, 2); +} + +// called with the data needed to mine the next action in the chain +function initiateMining(data) { + // console.log('worker',threadID,'starting'); + let { + binary, + nonceBounds, + nonceStartValue, + nonceEndValue, + targetPOW, + } = data; + + currentNonce = nonceStartValue; + + // get binary nonce to start value + + binary = setNonceBuffer(binary, nonceBounds[0], nonceBounds[1], nonceStartValue); + + // start mining loop + function mine(){ + if (active && currentNonce <= nonceEndValue) { + + let digest = sha256(binary); + let POW = countLeadingZeroesBin(digest); + + if (POW === targetPOW) { + // success! end thread and return the result + postMessage({ thread: threadID, status: 'pow-target-found', binary, nonceBounds, digest, currentNonce, POW }); + active = false; + return; + } + + currentNonce++; + + binary = incrementNonceBuffer(binary, nonceBounds[0], nonceBounds[1]) + + // keep mining + setTimeout(mine, 0); + + return; + + } else if (!active) { + console.log('worker',threadID,'stopped'); + } + + if (currentNonce > nonceEndValue) { + console.log('worker',threadID,'finished'); + // figure out what the next nonce range will be for this worker based on our threadCount; use NONCE_OFFSET to change our starting point + data.nonceStartValue += threadCountNum * NONCE_OFFSET; + data.nonceEndValue = data.nonceStartValue + NONCE_OFFSET; + // keep mining at new nonce range + setTimeout(() => initiateMining(data), 1); + } + } + + mine(); + +} From 2a62d51c424b606d105609d040e960f4b0157859 Mon Sep 17 00:00:00 2001 From: arkin0x <99223753+arkin0x@users.noreply.github.com> Date: Tue, 26 Mar 2024 09:19:18 -0500 Subject: [PATCH 3/4] removed ONOSENDAI specific language --- ndk/src/events/nip13/pow.worker.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ndk/src/events/nip13/pow.worker.ts b/ndk/src/events/nip13/pow.worker.ts index 7a40b77e..cb5c599a 100644 --- a/ndk/src/events/nip13/pow.worker.ts +++ b/ndk/src/events/nip13/pow.worker.ts @@ -31,7 +31,6 @@ function safeInterrupt(data) { }, 2); } -// called with the data needed to mine the next action in the chain function initiateMining(data) { // console.log('worker',threadID,'starting'); let { From e29bb5e7b267521b6a1b9e6f7fe2cd5f13838be2 Mon Sep 17 00:00:00 2001 From: Jeff Gardner <202880+erskingardner@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:39:22 +0100 Subject: [PATCH 4/4] Add tests, reformat and clean up typescript types --- ndk/src/events/nip13/Miner.ts | 138 ++++++++++++++--------- ndk/src/events/nip13/miner.test.ts | 100 +++++++++++++++++ ndk/src/events/nip13/nip13.ts | 175 ++++++++++++++++------------- ndk/src/events/nip13/pow.worker.ts | 161 +++++++++++++------------- 4 files changed, 369 insertions(+), 205 deletions(-) create mode 100644 ndk/src/events/nip13/miner.test.ts diff --git a/ndk/src/events/nip13/Miner.ts b/ndk/src/events/nip13/Miner.ts index 9c2656f9..148b7c16 100644 --- a/ndk/src/events/nip13/Miner.ts +++ b/ndk/src/events/nip13/Miner.ts @@ -5,68 +5,104 @@ import type { UnsignedEvent } from "nostr-tools"; * @param serializedEvent string * @returns beginning and end index of the nonce in the buffer */ -export const getNonceBounds = (serializedEvent: string): [number, number] => { - const nonceTag = '"nonce","'; - const nonceStart = serializedEvent.indexOf(nonceTag) + nonceTag.length; - const nonceEnd = serializedEvent.indexOf('"', nonceStart); - return [nonceStart, nonceEnd]; -}; +export function getNonceBounds(serializedEvent: string): [number, number] { + const nonceTag = '"nonce","'; + const nonceStart = serializedEvent.indexOf(nonceTag) + nonceTag.length; + const nonceEnd = serializedEvent.indexOf('"', nonceStart); + return [nonceStart, nonceEnd]; +} /** * Deserialize a nostr event from a string * @param serializedEvent string * @returns UnsignedEvent */ -export const deserializeEvent = (serializedEvent: string): UnsignedEvent => { - const eventArray = JSON.parse(serializedEvent); - return { - pubkey: eventArray[1], - created_at: eventArray[2], - kind: eventArray[3], - tags: eventArray[4], - content: eventArray[5], - }; -}; +export function deserializeEvent(serializedEvent: string): UnsignedEvent { + const eventArray = JSON.parse(serializedEvent); + return { + pubkey: eventArray[1], + created_at: eventArray[2], + kind: eventArray[3], + tags: eventArray[4], + content: eventArray[5], + }; +} -export const incrementNonceBuffer = (buffer: Uint8Array, startIndex: number, endIndex: number): Uint8Array => { - // go from right to left to update count, because the number is big-endian - for (let i = endIndex-1; i >= startIndex; i--) { - if (buffer[i] === 63) { - // we are using 16 UTF-8 symbols between decimal 48 and 63 (0-9, :, ;, <, =, >, ?) - // 16 nonce digits * 4 bits per digit = 64 bits of possible entropy, which is more than enough for a nonce, especially since the created_at will be incremented and serve as entropy too. - // wrap around if the symbol is 63 (?) and set to 48 (0) - buffer[i] = 48; - } else { - buffer[i]++; - break; +/** + * Increments the values in a Uint8Array buffer from a specified start index to an end index. + * The buffer is treated as a big-endian number, and the increment is performed from right to left. + * We are using 16 UTF-8 symbols between decimal 48 and 63 (0-9, :, ;, <, =, >, ?) + * + * 16 nonce digits * 4 bits per digit = 64 bits of possible entropy, + * which is more than enough for a nonce, especially since the created_at will be incremented + * and serve as entropy too. + * + * If a value in the buffer reaches 63, it wraps around to 48 (0). + * + * @param buffer - The Uint8Array buffer to increment. + * @param startIndex - The index to start incrementing from. + * @param endIndex - The index to stop incrementing at (inclusive). + * @returns The modified Uint8Array buffer. + */ +export function incrementNonceBuffer( + buffer: Uint8Array, + startIndex: number, + endIndex: number +): Uint8Array { + // go from right to left to update count, because the number is big-endian + for (let i = endIndex - 1; i >= startIndex; i--) { + if (buffer[i] === 63) { + buffer[i] = 48; + } else { + buffer[i]++; + break; + } } - } - return buffer; -}; + return buffer; +} -export const setNonceBuffer = (buffer: Uint8Array, startIndex: number, endIndex: number, nonce: number): Uint8Array => { - - // Convert the nonce back to a big-endian array of bytes - for (let i = endIndex - 1; i >= startIndex; i--) { - buffer[i] = (nonce & 0xF) + 48; - nonce = nonce >> 4; - } +/** + * Sets the nonce value in the given buffer at the specified range. + * + * @param buffer - The buffer to set the nonce value in. + * @param startIndex - The starting index in the buffer to set the nonce value. + * @param endIndex - The ending index in the buffer to set the nonce value. + * @param nonce - The nonce value to set in the buffer. + * @returns The modified buffer with the nonce value set. + */ +export function setNonceBuffer( + buffer: Uint8Array, + startIndex: number, + endIndex: number, + nonce: number +): Uint8Array { + // Convert the nonce back to a big-endian array of bytes + for (let i = endIndex - 1; i >= startIndex; i--) { + buffer[i] = (nonce & 0xf) + 48; + nonce = nonce >> 4; + } - return buffer; -}; + return buffer; +} -export function countLeadingZeroesBin(binary: Uint8Array) { - let count = 0; +/** + * Counts the number of leading zeroes in a binary array. + * + * @param binary {Uint8Array} - The binary array to count leading zeroes from. + * @returns The number of leading zeroes in the binary array. + */ +export function countLeadingZeroesBin(binary: Uint8Array): number { + let count = 0; - for (let i = 0; i < binary.length; i++) { - const byte = binary[i]; - if (byte === 0) { - count += 8; - } else { - count += Math.clz32(byte) - 24; - break; + for (let i = 0; i < binary.length; i++) { + const byte = binary[i]; + if (byte === 0) { + count += 8; + } else { + count += Math.clz32(byte) - 24; + break; + } } - } - return count; -} \ No newline at end of file + return count; +} diff --git a/ndk/src/events/nip13/miner.test.ts b/ndk/src/events/nip13/miner.test.ts new file mode 100644 index 00000000..6a1e1581 --- /dev/null +++ b/ndk/src/events/nip13/miner.test.ts @@ -0,0 +1,100 @@ +import { generateSecretKey, getPublicKey, serializeEvent } from "nostr-tools"; +import type { NostrEvent } from "nostr-tools"; +import { + getNonceBounds, + deserializeEvent, + incrementNonceBuffer, + setNonceBuffer, + countLeadingZeroesBin, +} from "./Miner"; + +describe("miner.ts", () => { + let privKey: Uint8Array; + let pubkey: string; + let event: NostrEvent; + let serializedEvent: string; + let binaryEvent: Uint8Array; + + beforeAll(() => { + privKey = generateSecretKey(); + pubkey = getPublicKey(privKey); + + event = { + id: "", + sig: "", + pubkey: pubkey, + created_at: 1234567890, + kind: 1, + tags: [ + ["nonce", "0000000000000000", "20"], + ["exampleTagKey", "exampleTagValue"], + ], + content: "exampleContent", + } as NostrEvent; + + serializedEvent = serializeEvent(event); + binaryEvent = new TextEncoder().encode(serializedEvent); + }); + + describe("getNonceBounds", () => { + it("should return the beginning and ending index of the nonce in the serialized event", () => { + const [startIndex, endIndex] = getNonceBounds(serializedEvent); + expect(startIndex).toBeGreaterThan(0); + expect(endIndex).toBeGreaterThan(0); + expect(endIndex).toBeGreaterThan(startIndex); + // We're using a 16-bit nonce, so we expect the difference between the start and end index to be 16 + expect(endIndex - startIndex).toBe(16); + }); + }); + + describe("deserializeEvent", () => { + it("should deserialize a nostr event from a string", () => { + const deserializedEvent = deserializeEvent(serializedEvent); + + expect(deserializedEvent.content).toBe(event.content); + expect(deserializedEvent.created_at).toBe(event.created_at); + expect(deserializedEvent.kind).toBe(event.kind); + expect(deserializedEvent.pubkey).toBe(event.pubkey); + expect(deserializedEvent.tags).toStrictEqual(event.tags); + }); + }); + + describe("incrementNonceBuffer", () => { + it("should increment the values in a Uint8Array buffer from a specified start index to an end index", () => { + const startIndex = 94; + const endIndex = 110; + const modifiedBuffer = incrementNonceBuffer(binaryEvent, startIndex, endIndex); + // Assertions here + }); + }); + + describe("setNonceBuffer", () => { + it("should set the nonce value in the given buffer at the specified range", () => { + const buffer = new Uint8Array([1, 2, 3, 4, 5]); + const startIndex = 1; + const endIndex = 3; + const nonce = 10; + const modifiedBuffer = setNonceBuffer(buffer, startIndex, endIndex, nonce); + // Assertions here + }); + }); + + describe("countLeadingZeroesBin", () => { + it("should count the number of leading zeroes in a binary array", () => { + const hex1 = "000000000e9d97a1ab09fc381030b346cdd7a142ad57e6df0b46dc9bef6c7e2d"; + const binary1 = new Uint8Array(Buffer.from(hex1, "hex")); + const leadingZeroesCount1 = countLeadingZeroesBin(binary1); + expect(leadingZeroesCount1).toBe(36); + + const hex2 = "0000a7f81e9d97a1ab09fc381030b346cdd7a142ad57e6df0b46dc9bef6c7e2d"; + const binary2 = new Uint8Array(Buffer.from(hex2, "hex")); + const leadingZeroesCount2 = countLeadingZeroesBin(binary2); + expect(leadingZeroesCount2).toBe(16); + + const hex3 = "abc1a7f81e9d97a1ab09fc381030b346cdd7a142ad57e6df0b46dc9bef6c7e2d"; + const binary3 = new Uint8Array(Buffer.from(hex3, "hex")); + const leadingZeroesCount3 = countLeadingZeroesBin(binary3); + expect(leadingZeroesCount3).toBe(0); + }); + }); +}); diff --git a/ndk/src/events/nip13/nip13.ts b/ndk/src/events/nip13/nip13.ts index 02c84a1b..4f5771eb 100644 --- a/ndk/src/events/nip13/nip13.ts +++ b/ndk/src/events/nip13/nip13.ts @@ -1,88 +1,105 @@ import { serializeEvent } from "nostr-tools"; import type { UnsignedEvent } from "nostr-tools"; import type { NDKEvent } from "../index.js"; -import Worker from './pow.worker.js'; +import Worker from "./pow.worker.js"; import { deserializeEvent, getNonceBounds } from "./Miner"; let worker: Worker; -export async function pow(this: NDKEvent, target: number) { - if (!this.ndk) throw new Error("No NDK instance found!"); - - const intTarget = Math.floor(target); - - // check if event has necessary props: - if (!this.kind) throw new Error("NIP-13: Event has no kind"); - if (!this.created_at) throw new Error("NIP-13: Event has no created_at"); - if (!this.pubkey) throw new Error("NIP-13: Event has no pubkey"); - if (!this.content) console.warn("NIP-13: Event has no content"); - if (intTarget !== target) console.warn("NIP-13: Target is not an integer") - - // can't do POW if these things are already present - if (this.id) throw new Error("NIP-13: Event already has an id"); - if (this.sig) throw new Error("NIP-13: Event already has a signature"); - - // spawn worker - if (!worker) { - worker = new Worker(); - } - - // eslint-disable-next-line @typescript-eslint/no-this-alias - const _event = this; - - return new Promise((resolve, reject) => { - - // declare handler for worker messages in closure - function handleMinedPOW(msg: MessageEvent) { - if (msg.data.status === 'pow-target-found') { - worker.postMessage({command: 'stop'}); // not strictly necessary as the worker stops on its own when the target is found - const binary = msg.data.binary; - const eventSerialized = new TextDecoder().decode(binary); - const raw = deserializeEvent(eventSerialized); - // copy the successful nonce tag to the event - _event.tags[0][1] = raw.tags[0][1]; - - // resolve promise with the updated event - resolve(_event); - } +export type PowMessageData = { + thread: number; + threadCount: number; + status?: string; + binary: Uint8Array; + digest?: Uint8Array; + nonceOffset: number; + nonceBounds: [number, number]; + nonceStartValue: number; + nonceEndValue: number; + currentNonce?: number; + targetPOW: number; +}; + +/** + * Performs proof-of-work (POW) for the NDKEvent replacing the ID field. + * @param target - The target value for the POW. + * @returns A Promise that resolves to the updated NDKEvent with the successful nonce tag. + * @throws Error if the NDK instance is not found, or if the event is missing required properties, or if the event already has an id or a signature. + */ +export async function pow(this: NDKEvent, target: number): Promise { + if (!this.ndk) throw new Error("No NDK instance found!"); + + const intTarget = Math.floor(target); + + // check if event has necessary props: + if (!this.kind) throw new Error("NIP-13: Event has no kind"); + if (!this.created_at) throw new Error("NIP-13: Event has no created_at"); + if (!this.pubkey) throw new Error("NIP-13: Event has no pubkey"); + if (!this.content) console.warn("NIP-13: Event has no content"); + if (intTarget !== target) console.warn("NIP-13: Target is not an integer"); + + // can't do POW if these things are already present + if (this.id) throw new Error("NIP-13: Event already has an id"); + if (this.sig) throw new Error("NIP-13: Event already has a signature"); + + // spawn worker + if (!worker) { + worker = new Worker(); } - // attach handler to worker - worker.onmessage = handleMinedPOW; - - // add nonce tag to event - const nonceTagIndex = _event.tags.findIndex((tag) => tag[0] === "nonce"); - if (nonceTagIndex) { - // remove old nonce tag - _event.tags.splice(nonceTagIndex, 1); - } - // we use unshift so that the nonce tag is first. This is important because we need to know the byte position of the nonce tag in the serialized event, and if any tags come before it which contain multibyte characters, the byte position will be off. - _event.tags.unshift(["nonce", '0000000000000000', intTarget.toString()]); - - // serialize event - const serialized = serializeEvent(_event.rawEvent() as UnsignedEvent); - - // get nonce bounds - const bounds: [number, number] = getNonceBounds(serialized); - - // binary event - const binary = new TextEncoder().encode(serialized); - - // send event to worker for mining - worker.postMessage({ - command: 'start', - data: { - thread: 1, - threadCount: 1, - binary, - nonceOffset: 2**20, - nonceBounds: bounds, - nonceStartValue: 0, - nonceEndValue: 2**20, // ~1 million - targetPOW: intTarget, - } - }); - - }) as Promise; - -} \ No newline at end of file + // eslint-disable-next-line @typescript-eslint/no-this-alias + const _event = this; + + return new Promise((resolve) => { + // declare handler for worker messages in closure + function handleMinedPOW(msg: MessageEvent) { + if (msg.data.status === "pow-target-found") { + worker.postMessage({ command: "stop" }); // not strictly necessary as the worker stops on its own when the target is found + const binary = msg.data.binary; + const eventSerialized = new TextDecoder().decode(binary); + const raw = deserializeEvent(eventSerialized); + // copy the successful nonce tag to the event + _event.tags[0][1] = raw.tags[0][1]; + + // resolve promise with the updated event + resolve(_event); + } + } + + // attach handler to worker + worker.onmessage = handleMinedPOW; + + // add nonce tag to event + const nonceTagIndex = _event.tags.findIndex((tag) => tag[0] === "nonce"); + if (nonceTagIndex) { + // remove old nonce tag + _event.tags.splice(nonceTagIndex, 1); + } + // we use unshift so that the nonce tag is first. This is important because we need to know the byte position of the nonce tag in the serialized event, and if any tags come before it which contain multibyte characters, the byte position will be off. + _event.tags.unshift(["nonce", "0000000000000000", intTarget.toString()]); + + // serialize event + const serialized = serializeEvent(_event.rawEvent() as UnsignedEvent); + + // get nonce bounds + const bounds: [number, number] = getNonceBounds(serialized); + + // binary event + const binary = new TextEncoder().encode(serialized); + + // send event to worker for mining + worker.postMessage({ + command: "start", + data: { + thread: 1, + threadCount: 1, + binary, + nonceOffset: 2 ** 20, + nonceBounds: bounds, + nonceStartValue: 0, + nonceEndValue: 2 ** 20, // ~1 million + targetPOW: intTarget, + } as PowMessageData, + }); + }) as Promise; +} diff --git a/ndk/src/events/nip13/pow.worker.ts b/ndk/src/events/nip13/pow.worker.ts index cb5c599a..2c29861b 100644 --- a/ndk/src/events/nip13/pow.worker.ts +++ b/ndk/src/events/nip13/pow.worker.ts @@ -1,89 +1,100 @@ -import { sha256 } from '@noble/hashes/sha256'; +import { sha256 } from "@noble/hashes/sha256"; import { incrementNonceBuffer, setNonceBuffer, countLeadingZeroesBin } from "./Miner"; +import type { PowMessageData } from "./nip13"; -let threadID = undefined; +let threadID: number | undefined = undefined; let threadCountNum = 0; let NONCE_OFFSET = 0; let active = false; let currentNonce = 0; -self.onmessage = function(message) { - const { command, data } = message.data; - if (data.thread) threadID = data.thread; - if (data.threadCount) threadCountNum = data.threadCount; - if (data.nonceOffset) NONCE_OFFSET = data.nonceOffset; - switch (command) { - case 'start': - safeInterrupt(data); - break; - case 'stop': - active = false; - break; - } +self.onmessage = function (message) { + const command: string = message.data.command; + const data: PowMessageData = message.data.data; + if (data.thread) threadID = data.thread; + if (data.threadCount) threadCountNum = data.threadCount; + if (data.nonceOffset) NONCE_OFFSET = data.nonceOffset; + switch (command) { + case "start": + safeInterrupt(data); + break; + case "stop": + active = false; + break; + } }; -// if we call 'start' while mining, we need to stop mining, wait a tick, then start again to allow the previous mining loop to finish -function safeInterrupt(data) { - active = false; - setTimeout(() => { - active = true; - initiateMining(data); - }, 2); +/** + * Safely interrupts the mining process and initiates it again after a short delay. + * If we call 'start' while mining, we need to stop mining, wait a tick, then start again to + * allow the previous mining loop to finish. + * @param data - The PowMessageData object containing the necessary data for mining. + */ +function safeInterrupt(data: PowMessageData): void { + active = false; + setTimeout(() => { + active = true; + initiateMining(data); + }, 2); } -function initiateMining(data) { - // console.log('worker',threadID,'starting'); - let { - binary, - nonceBounds, - nonceStartValue, - nonceEndValue, - targetPOW, - } = data; - - currentNonce = nonceStartValue; - - // get binary nonce to start value - - binary = setNonceBuffer(binary, nonceBounds[0], nonceBounds[1], nonceStartValue); - - // start mining loop - function mine(){ - if (active && currentNonce <= nonceEndValue) { - - let digest = sha256(binary); - let POW = countLeadingZeroesBin(digest); - - if (POW === targetPOW) { - // success! end thread and return the result - postMessage({ thread: threadID, status: 'pow-target-found', binary, nonceBounds, digest, currentNonce, POW }); - active = false; - return; - } - - currentNonce++; - - binary = incrementNonceBuffer(binary, nonceBounds[0], nonceBounds[1]) - - // keep mining - setTimeout(mine, 0); - - return; - - } else if (!active) { - console.log('worker',threadID,'stopped'); +/** + * Initiates the mining process. + * + * @param data - The PowMessageData object containing the mining parameters. + */ +function initiateMining(data: PowMessageData): void { + // console.log('worker',threadID,'starting'); + let { binary } = data; + const { nonceBounds, nonceStartValue, nonceEndValue, targetPOW } = data; + + currentNonce = nonceStartValue; + + // get binary nonce to start value + binary = setNonceBuffer(binary, nonceBounds[0], nonceBounds[1], nonceStartValue); + + // start mining loop + function mine(): void { + if (active && currentNonce <= nonceEndValue) { + const digest: Uint8Array = sha256(binary); + const POW: number = countLeadingZeroesBin(digest); + + if (POW === targetPOW) { + // success! end thread and return the result + postMessage({ + thread: threadID, + status: "pow-target-found", + binary, + nonceBounds, + digest, + currentNonce, + POW, + }); + active = false; + return; + } + + currentNonce++; + + binary = incrementNonceBuffer(binary, nonceBounds[0], nonceBounds[1]); + + // keep mining + setTimeout(mine, 0); + + return; + } else if (!active) { + console.log("worker", threadID, "stopped"); + } + + if (currentNonce > nonceEndValue) { + console.log("worker", threadID, "finished"); + // figure out what the next nonce range will be for this worker based on our threadCount; use NONCE_OFFSET to change our starting point + data.nonceStartValue += threadCountNum * NONCE_OFFSET; + data.nonceEndValue = data.nonceStartValue + NONCE_OFFSET; + // keep mining at new nonce range + setTimeout(() => initiateMining(data), 1); + } } - if (currentNonce > nonceEndValue) { - console.log('worker',threadID,'finished'); - // figure out what the next nonce range will be for this worker based on our threadCount; use NONCE_OFFSET to change our starting point - data.nonceStartValue += threadCountNum * NONCE_OFFSET; - data.nonceEndValue = data.nonceStartValue + NONCE_OFFSET; - // keep mining at new nonce range - setTimeout(() => initiateMining(data), 1); - } - } - - mine(); - + mine(); }