feat: POW miner
This commit is contained in:
parent
667518a2df
commit
2a851c442d
@ -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;
|
||||
|
@ -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"],
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -86,4 +86,4 @@ export interface FullRelaySettings {
|
||||
settings: RelaySettings;
|
||||
}
|
||||
|
||||
export type NotSignedNostrEvent = Omit<NostrEvent, "sig">;
|
||||
export type NotSignedNostrEvent = Omit<NostrEvent, "sig">;
|
||||
|
57
packages/system/src/pow-util.ts
Normal file
57
packages/system/src/pow-util.ts
Normal 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;
|
||||
}
|
21
packages/system/src/pow-worker.ts
Normal file
21
packages/system/src/pow-worker.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
56
packages/system/src/pow.ts
Normal file
56
packages/system/src/pow.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
@ -59,7 +59,7 @@ export class PrivateKeySigner implements EventSigner {
|
||||
nonce: base64.decode(iv),
|
||||
v: MessageEncryptorVersion.Nip4,
|
||||
},
|
||||
secret
|
||||
secret,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user