forked from Kieran/snort
feat: POW miner
This commit is contained in:
parent
667518a2df
commit
2a851c442d
@ -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;
|
||||||
|
@ -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"],
|
||||||
|
@ -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");
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,4 +86,4 @@ export interface FullRelaySettings {
|
|||||||
settings: RelaySettings;
|
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),
|
nonce: base64.decode(iv),
|
||||||
v: MessageEncryptorVersion.Nip4,
|
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;
|
/((?: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);
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user