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

View File

@ -17,6 +17,10 @@ const config = {
import: "./src/service-worker.ts",
filename: "service-worker.js",
},
pow: {
import: require.resolve("@snort/system/dist/pow-worker.js"),
filename: "pow.js",
},
},
target: "browserslist",
mode: isProduction ? "production" : "development",
@ -24,12 +28,7 @@ const config = {
output: {
publicPath: "/",
path: path.resolve(__dirname, "build"),
filename: ({ runtime }) => {
if (runtime === "sw") {
return "[name].js";
}
return isProduction ? "[name].[chunkhash].js" : "[name].js";
},
filename: isProduction ? "[name].[chunkhash].js" : "[name].js",
clean: isProduction,
},
devServer: {
@ -50,7 +49,7 @@ const config = {
new HtmlWebpackPlugin({
template: "public/index.html",
favicon: "public/favicon.ico",
excludeChunks: ["sw"],
excludeChunks: ["sw", "pow"],
}),
new ESLintPlugin({
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 { getPublicKey, unixNow } from "@snort/shared";
import { EventExt } from "./event-ext";
@ -10,6 +10,8 @@ export class EventBuilder {
#createdAt?: number;
#pubkey?: string;
#tags: Array<Array<string>> = [];
#pow?: number;
#powMiner?: PowMiner;
kind(k: EventKind) {
this.#kind = k;
@ -38,6 +40,17 @@ export class EventBuilder {
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
*/
@ -74,14 +87,21 @@ export class EventBuilder {
async buildAndSign(pk: HexKey | EventSigner) {
if (typeof pk === "string") {
const ev = this.pubKey(getPublicKey(pk)).build();
EventExt.sign(ev, pk);
return ev;
return EventExt.sign(await this.#mine(ev), pk);
} else {
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() {
if (this.#kind === undefined) {
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 { EventKind, HexKey, NostrEvent, NotSignedNostrEvent } from ".";
import { Nip4WebCryptoEncryptor } from "./impl/nip4";
import { minePow } from "./pow-util";
export interface Tag {
key: string;
@ -44,6 +44,7 @@ export abstract class EventExt {
if (!secp.schnorr.verify(e.sig, e.id, e.pubkey)) {
throw new Error("Signing failed");
}
return e;
}
/**
@ -58,13 +59,14 @@ export abstract class EventExt {
static createId(e: NostrEvent | NotSignedNostrEvent) {
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) {
console.debug(payload);
throw new Error("ID doesnt match!");
}
return hash;
/**
* Mine POW for an event (NIP-13)
*/
static minePow(e: NostrEvent, target: number) {
return minePow(e, target);
}
/**

View File

@ -10,6 +10,7 @@ import {
Lists,
NostrEvent,
NotSignedNostrEvent,
PowMiner,
PrivateKeySigner,
RelaySettings,
TaggedNostrEvent,
@ -285,7 +286,7 @@ export class EventPublisher {
/**
* 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 signer = new PrivateKeySigner(secret);
@ -296,6 +297,9 @@ export class EventPublisher {
eb.pubKey(signer.getPubKey());
eb.kind(EventKind.GiftWrap);
eb.tag(["p", pTag]);
if (powTarget) {
eb.pow(powTarget, powMiner);
}
eb.content(await signer.nip44Encrypt(JSON.stringify(inner), pTag));
return await eb.buildAndSign(secret);

View File

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

View File

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

View File

@ -15,16 +15,17 @@ export function createNostrLink(prefix: NostrPrefix, id: string, relays?: string
type: prefix,
id,
relays,
kind, author,
kind,
author,
encode: () => {
if(prefix === NostrPrefix.Note || prefix === NostrPrefix.PublicKey) {
if (prefix === NostrPrefix.Note || prefix === NostrPrefix.PublicKey) {
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 "";
}
},
} as NostrLink;
}

View File

@ -86,4 +86,4 @@ export interface FullRelaySettings {
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),
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;
return str.split(urlRegex);
}
}