// Cross-check the Number-based SipHash / GCS matcher against a straight // BigInt reference (the previous implementation), on random filters and // elements, and report throughput for a realistic watched-script set. // // npx tsx packages/p2p/scripts/gcs-check.ts import { randomBytes } from 'node:crypto'; import { filterMatchesAny, filterMatches, keyFromBlockHash, gcsBackend, gcsUseJs, P, M } from '../src/gcs.js'; import { siphash24 } from '../src/siphash.js'; // ---- reference (BigInt) implementation ---- const MASK64 = 0xffffffffffffffffn; const rot = (v: bigint, n: number): bigint => ((v << BigInt(n)) & MASK64) | (v >> BigInt(64 - n)); function sipRound(s: bigint[]): void { let [v0, v1, v2, v3] = s; v0 = (v0 + v1) & MASK64; v1 = rot(v1, 13); v1 ^= v0; v0 = rot(v0, 32); v2 = (v2 + v3) & MASK64; v3 = rot(v3, 16); v3 ^= v2; v0 = (v0 + v3) & MASK64; v3 = rot(v3, 21); v3 ^= v0; v2 = (v2 + v1) & MASK64; v1 = rot(v1, 17); v1 ^= v2; v2 = rot(v2, 32); s[0] = v0; s[1] = v1; s[2] = v2; s[3] = v3; } function refSiphash(key: Buffer, msg: Buffer): bigint { const k0 = key.readBigUInt64LE(0), k1 = key.readBigUInt64LE(8); const s = [k0 ^ 0x736f6d6570736575n, k1 ^ 0x646f72616e646f6dn, k0 ^ 0x6c7967656e657261n, k1 ^ 0x7465646279746573n]; const full = Math.floor(msg.length / 8); for (let i = 0; i < full; i++) { const m = msg.readBigUInt64LE(i * 8); s[3] ^= m; sipRound(s); sipRound(s); s[0] ^= m; } const tail = Buffer.alloc(8); msg.subarray(full * 8).copy(tail); tail[7] = msg.length & 0xff; const m = tail.readBigUInt64LE(0); s[3] ^= m; sipRound(s); sipRound(s); s[0] ^= m; s[2] ^= 0xffn; sipRound(s); sipRound(s); sipRound(s); sipRound(s); return (s[0] ^ s[1] ^ s[2] ^ s[3]) & MASK64; } const refRange = (key: Buffer, el: Buffer, F: bigint): bigint => (refSiphash(key, el) * F) >> 64n; // ---- BIP158 encoder (for building test filters) ---- function encodeVarint(n: number): Buffer { if (n < 0xfd) return Buffer.from([n]); if (n <= 0xffff) { const b = Buffer.alloc(3); b[0] = 0xfd; b.writeUInt16LE(n, 1); return b; } const b = Buffer.alloc(5); b[0] = 0xfe; b.writeUInt32LE(n, 1); return b; } function buildFilter(blockHash: string, elements: Buffer[]): Buffer { const key = keyFromBlockHash(blockHash); const F = BigInt(elements.length) * M; const vals = [...new Set(elements.map((e) => refRange(key, e, F)))].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); const bits: number[] = []; let last = 0n; for (const v of vals) { const d = v - last; last = v; const q = d >> BigInt(P), r = d & ((1n << BigInt(P)) - 1n); for (let i = 0n; i < q; i++) bits.push(1); bits.push(0); for (let i = P - 1; i >= 0; i--) bits.push(Number((r >> BigInt(i)) & 1n)); } const body = Buffer.alloc(Math.ceil(bits.length / 8)); bits.forEach((b, i) => { if (b) body[i >> 3] |= 0x80 >> (i & 7); }); // N is the element count the encoder hashed with (duplicates collapse // in the set but N·M stays the range), exactly as Core writes it. return Buffer.concat([encodeVarint(elements.length), body]); } // 1. SipHash agreement on random keys / lengths 0..40 (incl. the 7-byte tail cases). let fails = 0; for (let i = 0; i < 2000; i++) { const key = randomBytes(16), msg = randomBytes(i % 41); if (siphash24(key, msg) !== refSiphash(key, msg)) { fails++; if (fails < 5) console.log('siphash mismatch len', msg.length); } } console.log(`siphash: ${fails === 0 ? 'OK' : fails + ' MISMATCHES'} (2000 random messages)`); // 2. Matcher agreement: random filters (1..3000 elements), random // probes, plus probes that deliberately include members so the // positive path is exercised. Run for each backend available. let mfails = 0; const backends: Array<'wasm' | 'js'> = gcsBackend() === 'wasm' ? ['wasm', 'js'] : ['js']; if (backends[0] === 'js') console.log('note: dgbpow.wasm without gcs_match — only the JS path is checked'); for (const backend of backends) { gcsUseJs(backend === 'js'); let bf = 0, pos = 0, neg = 0; for (let round = 0; round < 60; round++) { const n = 1 + Math.floor(Math.random() * (round < 50 ? 60 : 3000)); const inSet = Array.from({ length: n }, () => randomBytes(20 + Math.floor(Math.random() * 15))); const hash = randomBytes(32).toString('hex'); const filter = buildFilter(hash, inSet); for (const el of inSet.slice(0, 30)) if (!filterMatchesAny(filter, hash, [el])) { bf++; console.log('member missed'); } const key = keyFromBlockHash(hash); const F = BigInt(n) * M; const setVals = new Set(inSet.map((e) => refRange(key, e, F))); const probes = Array.from({ length: 500 }, () => randomBytes(25)); if (round % 2) probes[Math.floor(Math.random() * probes.length)] = inSet[Math.floor(Math.random() * n)]; const expectAny = probes.some((p) => setVals.has(refRange(key, p, F))); const got = filterMatchesAny(filter, hash, probes); if (got !== expectAny) { bf++; console.log('verdict mismatch'); } got ? pos++ : neg++; const expectWhich = probes.filter((p) => setVals.has(refRange(key, p, F))).map((p) => p.toString('hex')).sort(); const gotWhich = filterMatches(filter, hash, probes).map((p) => p.toString('hex')).sort(); if (expectWhich.join() !== gotWhich.join()) { bf++; console.log('filterMatches mismatch'); } } mfails += bf; console.log(`matcher[${backend}]: ${bf === 0 ? 'OK' : bf + ' MISMATCHES'} (60 filters; ${pos} any-hit, ${neg} no-hit rounds)`); } // 3. Throughput: 3,300 watched scripts vs 200 filters of ~40 elements (a typical DGB block). const scripts = Array.from({ length: 3300 }, () => randomBytes(25)); const filters = Array.from({ length: 200 }, () => { const h = randomBytes(32).toString('hex'); return { h, f: buildFilter(h, Array.from({ length: 40 }, () => randomBytes(25))) }; }); for (const backend of backends) { gcsUseJs(backend === 'js'); const t0 = performance.now(); for (const { h, f } of filters) filterMatchesAny(f, h, scripts); const ms = (performance.now() - t0) / filters.length; console.log(`throughput[${backend}]: ${(1000 / ms).toFixed(0)} filters/s with 3,300 scripts (${ms.toFixed(2)} ms/filter)`); } let t0 = performance.now(); for (const { h } of filters.slice(0, 10)) { const key = keyFromBlockHash(h); const F = BigInt(40) * M; for (const s of scripts) refRange(key, s, F); } console.log(`throughput[bigint reference, hashing only]: ${((performance.now() - t0) / 10).toFixed(1)} ms/filter`); process.exit(fails + mfails === 0 ? 0 : 1);