Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pow #196

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions ndk/src/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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<NDKEvent> {
if (!this.ndk) throw new Error("No NDK instance found");

return pow.call(this, target);
}

/**
* Checks whether the event is valid per underlying NIPs.
*
Expand Down
108 changes: 108 additions & 0 deletions ndk/src/events/nip13/Miner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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 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 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],
};
}

/**
* 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;
}

/**
* 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;
}

/**
* 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;
}
}

return count;
}
100 changes: 100 additions & 0 deletions ndk/src/events/nip13/miner.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
105 changes: 105 additions & 0 deletions ndk/src/events/nip13/nip13.ts
Original file line number Diff line number Diff line change
@@ -0,0 +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 { deserializeEvent, getNonceBounds } from "./Miner";

let worker: Worker;

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<NDKEvent> {
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) => {
// 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<NDKEvent>;
}
Loading