From d652bf7a943650467c8bcf27bdcbb49abd789e4f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 11 Apr 2023 16:01:09 -0500 Subject: [PATCH] Add NIP-13 proof-of-work policy --- deno.lock | 11 +++++ mod.ts | 1 + src/deps.ts | 1 + src/policies/pow-policy.test.ts | 20 +++++++++ src/policies/pow-policy.ts | 74 +++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+) create mode 100644 src/policies/pow-policy.test.ts create mode 100644 src/policies/pow-policy.ts diff --git a/deno.lock b/deno.lock index a6e2211..3de80ba 100644 --- a/deno.lock +++ b/deno.lock @@ -210,5 +210,16 @@ "https://esm.sh/v113/nostr-tools@1.8.1/lib/references.d.ts": "b2e39f5c439380a1dc8e578b471ba3992f33e3936aa0eecf625734c7ba669098", "https://esm.sh/v113/nostr-tools@1.8.1/lib/relay.d.ts": "9bc6f2897a95ec12d128e94b2e7d6161d30a6bfe8d4350e217fbf4b710988db3", "https://esm.sh/v113/nostr-tools@1.8.1/lib/utils.d.ts": "6f37b09db0dce09f17ff7b70b921b9bb809bcb18c3bc058cb914afdf3c0e774e" + }, + "npm": { + "specifiers": { + "@noble/secp256k1@^1.7.1": "@noble/secp256k1@1.7.1" + }, + "packages": { + "@noble/secp256k1@1.7.1": { + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "dependencies": {} + } + } } } diff --git a/mod.ts b/mod.ts index e3addcb..8016ff5 100644 --- a/mod.ts +++ b/mod.ts @@ -4,6 +4,7 @@ export { default as hellthreadPolicy, type Hellthread } from './src/policies/hel export { default as keywordPolicy } from './src/policies/keyword-policy.ts'; export { default as noopPolicy } from './src/policies/noop-policy.ts'; export { default as openaiPolicy, type OpenAI, type OpenAIHandler } from './src/policies/openai-policy.ts'; +export { default as powPolicy } from './src/policies/pow-policy.ts'; export { default as pubkeyBanPolicy } from './src/policies/pubkey-ban-policy.ts'; export { default as rateLimitPolicy, type RateLimit } from './src/policies/rate-limit-policy.ts'; export { default as readOnlyPolicy } from './src/policies/read-only-policy.ts'; diff --git a/src/deps.ts b/src/deps.ts index 756bf2a..e77a7c3 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,3 +1,4 @@ export { readLines } from 'https://deno.land/std@0.181.0/io/mod.ts'; export { assert, assertEquals } from 'https://deno.land/std@0.181.0/testing/asserts.ts'; export { Keydb } from 'https://deno.land/x/keydb@1.0.0/sqlite.ts'; +export * as secp from 'npm:@noble/secp256k1@^1.7.1'; diff --git a/src/policies/pow-policy.test.ts b/src/policies/pow-policy.test.ts new file mode 100644 index 0000000..3a3bcf8 --- /dev/null +++ b/src/policies/pow-policy.test.ts @@ -0,0 +1,20 @@ +import { assertEquals } from '../deps.ts'; +import { buildEvent, buildInputMessage } from '../test.ts'; + +import powPolicy from './pow-policy.ts'; + +Deno.test('blocks events without a nonce', async () => { + const msg = buildInputMessage(); + assertEquals((await powPolicy(msg)).action, 'reject'); +}); + +Deno.test('accepts event with sufficient POW', async () => { + const msg = buildInputMessage({ + event: buildEvent({ + id: '000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358', + tags: [['nonce', '776797', '20']], + }), + }); + + assertEquals((await powPolicy(msg, { difficulty: 20 })).action, 'accept'); +}); diff --git a/src/policies/pow-policy.ts b/src/policies/pow-policy.ts new file mode 100644 index 0000000..0a0bbb2 --- /dev/null +++ b/src/policies/pow-policy.ts @@ -0,0 +1,74 @@ +import { secp } from '../deps.ts'; + +import type { Policy } from '../types.ts'; + +/** Policy options for `powPolicy`. */ +interface POW { + /** Events will be rejected if their `id` does not contain at least this many leading `0`'s. Default: `1` */ + difficulty?: number; +} + +/** Reject events which don't meet Proof-of-Work ([NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md)) criteria. */ +const powPolicy: Policy = ({ event }, opts = {}) => { + const { difficulty = 1 } = opts; + + const pow = getPow(event.id); + const nonce = event.tags.find((t) => t[0] === 'nonce'); + + if (pow >= difficulty && nonce && Number(nonce[2]) >= difficulty) { + return { + id: event.id, + action: 'accept', + msg: '', + }; + } + + return { + id: event.id, + action: 'reject', + msg: `pow: insufficient proof-of-work (difficulty ${difficulty})`, + }; +}; + +/** Get POW difficulty from a Nostr hex ID. */ +function getPow(id: string): number { + return getLeadingZeroBits(secp.utils.hexToBytes(id)); +} + +/** + * Get number of leading 0 bits. Adapted from nostream. + * https://github.com/Cameri/nostream/blob/fb6948fd83ca87ce552f39f9b5eb780ea07e272e/src/utils/proof-of-work.ts + */ +function getLeadingZeroBits(hash: Uint8Array): number { + let total: number, i: number, bits: number; + + for (i = 0, total = 0; i < hash.length; i++) { + bits = msb(hash[i]); + total += bits; + if (bits !== 8) { + break; + } + } + return total; +} + +/** + * Adapted from nostream. + * https://github.com/Cameri/nostream/blob/fb6948fd83ca87ce552f39f9b5eb780ea07e272e/src/utils/proof-of-work.ts + */ +function msb(b: number) { + let n = 0; + + if (b === 0) { + return 8; + } + + // deno-lint-ignore no-cond-assign + while (b >>= 1) { + n++; + } + + return 7 - n; +} + +export default powPolicy;