feat: POW miner

This commit is contained in:
Kieran 2023-08-18 00:35:48 +01:00
parent 667518a2df
commit 2a851c442d
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
14 changed files with 209 additions and 40 deletions

View File

@ -1,12 +1,13 @@
import { ExternalStore, dedupe } from "@snort/shared"; import { ExternalStore, dedupe } from "@snort/shared";
import { import {
EventKind, EventKind,
SystemInterface,
NostrPrefix, NostrPrefix,
encodeTLVEntries, encodeTLVEntries,
TLVEntryType, TLVEntryType,
TLVEntry, TLVEntry,
decodeTLV, decodeTLV,
PowWorker,
NostrEvent,
} from "@snort/system"; } from "@snort/system";
import { GiftWrapCache } from "Cache/GiftWrapCache"; import { GiftWrapCache } from "Cache/GiftWrapCache";
import { UnwrappedGift } from "Db"; import { UnwrappedGift } from "Db";
@ -103,16 +104,23 @@ export class Nip24ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
} }
return eb; return eb;
}); });
const messages = []; const messages: Array<Promise<NostrEvent>> = [];
const powTarget = 4 * 4; // 4-char zero
for (const pt of participants) { for (const pt of participants) {
const recvSealedN = await pub.giftWrap(await pub.sealRumor(gossip, pt.id), pt.id); const recvSealedN = pub.giftWrap(
await pub.sealRumor(gossip, pt.id),
pt.id,
powTarget,
new PowWorker("/pow.js")
);
messages.push(recvSealedN); messages.push(recvSealedN);
} }
const sendSealed = await pub.giftWrap(await pub.sealRumor(gossip, pub.pubKey), pub.pubKey); messages.push(
return [...messages, sendSealed]; pub.giftWrap(await pub.sealRumor(gossip, pub.pubKey), pub.pubKey, powTarget, new PowWorker("/pow.js"))
);
return await Promise.all(messages);
}, },
sendMessage: (ev, system: SystemInterface) => { sendMessage: (ev, system) => {
console.debug(ev);
ev.forEach(a => system.BroadcastEvent(a)); ev.forEach(a => system.BroadcastEvent(a));
}, },
} as Chat; } as Chat;

View File

@ -17,6 +17,10 @@ const config = {
import: "./src/service-worker.ts", import: "./src/service-worker.ts",
filename: "service-worker.js", filename: "service-worker.js",
}, },
pow: {
import: require.resolve("@snort/system/dist/pow-worker.js"),
filename: "pow.js",
},
}, },
target: "browserslist", target: "browserslist",
mode: isProduction ? "production" : "development", mode: isProduction ? "production" : "development",
@ -24,12 +28,7 @@ const config = {
output: { output: {
publicPath: "/", publicPath: "/",
path: path.resolve(__dirname, "build"), path: path.resolve(__dirname, "build"),
filename: ({ runtime }) => { filename: isProduction ? "[name].[chunkhash].js" : "[name].js",
if (runtime === "sw") {
return "[name].js";
}
return isProduction ? "[name].[chunkhash].js" : "[name].js";
},
clean: isProduction, clean: isProduction,
}, },
devServer: { devServer: {
@ -50,7 +49,7 @@ const config = {
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: "public/index.html", template: "public/index.html",
favicon: "public/favicon.ico", favicon: "public/favicon.ico",
excludeChunks: ["sw"], excludeChunks: ["sw", "pow"],
}), }),
new ESLintPlugin({ new ESLintPlugin({
extensions: ["js", "mjs", "jsx", "ts", "tsx"], extensions: ["js", "mjs", "jsx", "ts", "tsx"],

View File

@ -1,4 +1,4 @@
import { EventKind, HexKey, NostrPrefix, NostrEvent, EventSigner } from "."; import { EventKind, HexKey, NostrPrefix, NostrEvent, EventSigner, PowMiner } from ".";
import { HashtagRegex, MentionNostrEntityRegex } from "./const"; import { HashtagRegex, MentionNostrEntityRegex } from "./const";
import { getPublicKey, unixNow } from "@snort/shared"; import { getPublicKey, unixNow } from "@snort/shared";
import { EventExt } from "./event-ext"; import { EventExt } from "./event-ext";
@ -10,6 +10,8 @@ export class EventBuilder {
#createdAt?: number; #createdAt?: number;
#pubkey?: string; #pubkey?: string;
#tags: Array<Array<string>> = []; #tags: Array<Array<string>> = [];
#pow?: number;
#powMiner?: PowMiner;
kind(k: EventKind) { kind(k: EventKind) {
this.#kind = k; this.#kind = k;
@ -38,6 +40,17 @@ export class EventBuilder {
return this; return this;
} }
pow(target: number, miner?: PowMiner) {
this.#pow = target;
this.#powMiner = miner ?? {
minePow: (ev, target) => {
EventExt.minePow(ev, target);
return Promise.resolve(ev);
},
};
return this;
}
/** /**
* Extract mentions * Extract mentions
*/ */
@ -74,14 +87,21 @@ export class EventBuilder {
async buildAndSign(pk: HexKey | EventSigner) { async buildAndSign(pk: HexKey | EventSigner) {
if (typeof pk === "string") { if (typeof pk === "string") {
const ev = this.pubKey(getPublicKey(pk)).build(); const ev = this.pubKey(getPublicKey(pk)).build();
EventExt.sign(ev, pk); return EventExt.sign(await this.#mine(ev), pk);
return ev;
} else { } else {
const ev = this.pubKey(await pk.getPubKey()).build(); const ev = this.pubKey(await pk.getPubKey()).build();
return await pk.sign(ev); return await pk.sign(await this.#mine(ev));
} }
} }
async #mine(ev: NostrEvent) {
if (this.#pow && this.#powMiner) {
const ret = await this.#powMiner.minePow(ev, this.#pow);
return ret;
}
return ev;
}
#validate() { #validate() {
if (this.#kind === undefined) { if (this.#kind === undefined) {
throw new Error("Kind must be set"); throw new Error("Kind must be set");

View File

@ -3,7 +3,7 @@ import * as utils from "@noble/curves/abstract/utils";
import { getPublicKey, sha256, unixNow } from "@snort/shared"; import { getPublicKey, sha256, unixNow } from "@snort/shared";
import { EventKind, HexKey, NostrEvent, NotSignedNostrEvent } from "."; import { EventKind, HexKey, NostrEvent, NotSignedNostrEvent } from ".";
import { Nip4WebCryptoEncryptor } from "./impl/nip4"; import { minePow } from "./pow-util";
export interface Tag { export interface Tag {
key: string; key: string;
@ -44,6 +44,7 @@ export abstract class EventExt {
if (!secp.schnorr.verify(e.sig, e.id, e.pubkey)) { if (!secp.schnorr.verify(e.sig, e.id, e.pubkey)) {
throw new Error("Signing failed"); throw new Error("Signing failed");
} }
return e;
} }
/** /**
@ -58,13 +59,14 @@ export abstract class EventExt {
static createId(e: NostrEvent | NotSignedNostrEvent) { static createId(e: NostrEvent | NotSignedNostrEvent) {
const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content]; const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content];
return sha256(JSON.stringify(payload));
}
const hash = sha256(JSON.stringify(payload)); /**
if (e.id !== "" && hash !== e.id) { * Mine POW for an event (NIP-13)
console.debug(payload); */
throw new Error("ID doesnt match!"); static minePow(e: NostrEvent, target: number) {
} return minePow(e, target);
return hash;
} }
/** /**

View File

@ -10,6 +10,7 @@ import {
Lists, Lists,
NostrEvent, NostrEvent,
NotSignedNostrEvent, NotSignedNostrEvent,
PowMiner,
PrivateKeySigner, PrivateKeySigner,
RelaySettings, RelaySettings,
TaggedNostrEvent, TaggedNostrEvent,
@ -285,7 +286,7 @@ export class EventPublisher {
/** /**
* NIP-59 Gift Wrap event with ephemeral key * NIP-59 Gift Wrap event with ephemeral key
*/ */
async giftWrap(inner: NostrEvent, explicitP?: string) { async giftWrap(inner: NostrEvent, explicitP?: string, powTarget?: number, powMiner?: PowMiner) {
const secret = utils.bytesToHex(secp.secp256k1.utils.randomPrivateKey()); const secret = utils.bytesToHex(secp.secp256k1.utils.randomPrivateKey());
const signer = new PrivateKeySigner(secret); const signer = new PrivateKeySigner(secret);
@ -296,6 +297,9 @@ export class EventPublisher {
eb.pubKey(signer.getPubKey()); eb.pubKey(signer.getPubKey());
eb.kind(EventKind.GiftWrap); eb.kind(EventKind.GiftWrap);
eb.tag(["p", pTag]); eb.tag(["p", pTag]);
if (powTarget) {
eb.pow(powTarget, powMiner);
}
eb.content(await signer.nip44Encrypt(JSON.stringify(inner), pTag)); eb.content(await signer.nip44Encrypt(JSON.stringify(inner), pTag));
return await eb.buildAndSign(secret); return await eb.buildAndSign(secret);

View File

@ -23,10 +23,10 @@ export class Nip4WebCryptoEncryptor implements MessageEncryptor {
return { return {
ciphertext: new Uint8Array(result), ciphertext: new Uint8Array(result),
nonce: iv, nonce: iv,
v: MessageEncryptorVersion.Nip4 v: MessageEncryptorVersion.Nip4,
} as MessageEncryptorPayload; } as MessageEncryptorPayload;
} }
async decryptData(payload: MessageEncryptorPayload, sharedSecet: Uint8Array) { async decryptData(payload: MessageEncryptorPayload, sharedSecet: Uint8Array) {
const key = await this.#importKey(sharedSecet); const key = await this.#importKey(sharedSecet);
const result = await window.crypto.subtle.decrypt( const result = await window.crypto.subtle.decrypt(
@ -35,7 +35,7 @@ export class Nip4WebCryptoEncryptor implements MessageEncryptor {
iv: payload.nonce, iv: payload.nonce,
}, },
key, key,
payload.ciphertext payload.ciphertext,
); );
return new TextDecoder().decode(result); return new TextDecoder().decode(result);
} }

View File

@ -21,6 +21,7 @@ export * from "./profile-cache";
export * from "./zaps"; export * from "./zaps";
export * from "./signer"; export * from "./signer";
export * from "./text"; export * from "./text";
export * from "./pow";
export * from "./impl/nip4"; export * from "./impl/nip4";
export * from "./impl/nip44"; export * from "./impl/nip44";
@ -60,9 +61,9 @@ export const enum MessageEncryptorVersion {
} }
export interface MessageEncryptorPayload { export interface MessageEncryptorPayload {
ciphertext: Uint8Array, ciphertext: Uint8Array;
nonce: Uint8Array, nonce: Uint8Array;
v: MessageEncryptorVersion v: MessageEncryptorVersion;
} }
export interface MessageEncryptor { export interface MessageEncryptor {

View File

@ -15,16 +15,17 @@ export function createNostrLink(prefix: NostrPrefix, id: string, relays?: string
type: prefix, type: prefix,
id, id,
relays, relays,
kind, author, kind,
author,
encode: () => { encode: () => {
if(prefix === NostrPrefix.Note || prefix === NostrPrefix.PublicKey) { if (prefix === NostrPrefix.Note || prefix === NostrPrefix.PublicKey) {
return hexToBech32(prefix, id); return hexToBech32(prefix, id);
} }
if(prefix === NostrPrefix.Address || prefix === NostrPrefix.Event || prefix === NostrPrefix.Profile) { if (prefix === NostrPrefix.Address || prefix === NostrPrefix.Event || prefix === NostrPrefix.Profile) {
return encodeTLV(prefix, id, relays, kind, author); return encodeTLV(prefix, id, relays, kind, author);
} }
return ""; return "";
} },
} as NostrLink; } as NostrLink;
} }

View File

@ -86,4 +86,4 @@ export interface FullRelaySettings {
settings: RelaySettings; settings: RelaySettings;
} }
export type NotSignedNostrEvent = Omit<NostrEvent, "sig">; export type NotSignedNostrEvent = Omit<NostrEvent, "sig">;

View File

@ -0,0 +1,57 @@
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex } from "@noble/hashes/utils";
export interface NostrPowEvent {
id: string;
pubkey: string;
created_at: number;
kind: number;
tags: Array<Array<string>>;
content: string;
sig: string;
}
export function minePow(e: NostrPowEvent, target: number) {
let ctr = 0;
let nonceTagIdx = e.tags.findIndex(a => a[0] === "nonce");
if (nonceTagIdx === -1) {
nonceTagIdx = e.tags.length;
e.tags.push(["nonce", ctr.toString(), target.toString()]);
}
do {
//roll ctr and compute id
const now = Math.floor(new Date().getTime() / 1000);
// reset ctr if timestamp changed, this is not really needed but makes the ctr value smaller
if (now !== e.created_at) {
ctr = 0;
e.created_at = now;
}
e.tags[nonceTagIdx][1] = (++ctr).toString();
e.id = createId(e);
} while (countLeadingZeroes(e.id) < target);
return e;
}
function createId(e: NostrPowEvent) {
const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content];
return bytesToHex(sha256(JSON.stringify(payload)));
}
function countLeadingZeroes(hex: string) {
let count = 0;
for (let i = 0; i < hex.length; i++) {
const nibble = parseInt(hex[i], 16);
if (nibble === 0) {
count += 4;
} else {
count += Math.clz32(nibble) - 28;
break;
}
}
return count;
}

View File

@ -0,0 +1,21 @@
/// <reference lib="webworker" />
import { minePow, NostrPowEvent } from "./pow-util";
export interface PowWorkerMessage {
id: string;
cmd: "req" | "rsp";
event: NostrPowEvent;
target: number;
}
globalThis.onmessage = ev => {
const data = ev.data as PowWorkerMessage;
if (data.cmd === "req") {
queueMicrotask(() => {
minePow(data.event, data.target);
data.cmd = "rsp";
globalThis.postMessage(data);
});
}
};

View File

@ -0,0 +1,56 @@
import { v4 as uuid } from "uuid";
import { NostrEvent } from "./nostr";
import { PowWorkerMessage } from "./pow-worker";
export interface PowMiner {
minePow(ev: NostrEvent, target: number): Promise<NostrEvent>;
}
interface PowQueue {
resolve: (ev: NostrEvent) => void;
reject: () => void;
timeout: ReturnType<typeof setTimeout>;
}
export class PowWorker implements PowMiner {
#worker: Worker;
#queue: Map<string, PowQueue> = new Map();
constructor(script: string) {
this.#worker = new Worker(script, {
name: "POW",
});
this.#worker.onerror = ev => {
console.error(ev);
};
this.#worker.onmessage = ev => {
const data = ev.data as PowWorkerMessage;
const job = this.#queue.get(data.id);
if (job) {
clearTimeout(job.timeout);
this.#queue.delete(data.id);
job.resolve(data.event);
}
};
}
minePow(ev: NostrEvent, target: number) {
return new Promise<NostrEvent>((resolve, reject) => {
const req = {
id: uuid(),
cmd: "req",
event: ev,
target,
} as PowWorkerMessage;
this.#queue.set(req.id, {
resolve: ex => resolve(ex),
reject,
timeout: setTimeout(() => {
this.#queue.delete(req.id);
reject();
}, 600_000),
});
this.#worker.postMessage(req);
});
}
}

View File

@ -59,7 +59,7 @@ export class PrivateKeySigner implements EventSigner {
nonce: base64.decode(iv), nonce: base64.decode(iv),
v: MessageEncryptorVersion.Nip4, v: MessageEncryptorVersion.Nip4,
}, },
secret secret,
); );
} }

View File

@ -49,4 +49,4 @@ export function splitByUrl(str: string) {
/((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i; /((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i;
return str.split(urlRegex); return str.split(urlRegex);
} }