dgb-wallet/packages/p2p/src/gcs.ts
Digibyte.X d50286a9a5 DigiByte.X Wallet — recovery-first web wallet monorepo
Packages: core (BIP39/32/44/49/84/86 derivation, WIF), keystore (PBKDF2 +
AES-GCM), psbt, p2p (BIP157/158 SPV daemon, Dandelion++ broadcast with
relay-echo verification), blockbook, rpc, electrum, net, bns, web-ui
(React/Vite), mobile (Expo). ops/ holds the nginx vhost, systemd unit and
push.sh deploy script for digibyte.x/web.
2026-09-20 20:16:52 +02:00

132 lines
4.4 KiB
TypeScript

import { siphash24 } from './siphash.js';
import { decodeVarint } from './varint.js';
// BIP158 Golomb-Coded Set filter operations.
// Parameters for the "basic" filter type (0x00):
export const P = 19;
export const M = 784931n;
// Derive the SipHash key from a block hash: first 16 bytes of the
// block hash's LITTLE-endian byte order.
// (Our stored block hashes are hex big-endian display form, so we
// have to reverse them before slicing.)
export function keyFromBlockHash(blockHashHex: string): Buffer {
const be = Buffer.from(blockHashHex, 'hex');
const le = Buffer.from(be).reverse();
return Buffer.from(le.subarray(0, 16));
}
// Map an element to the filter's [0, N*M) range: fastReduce of the
// SipHash output over N*M. Matches BIP158's `hashToRange`.
function hashToRange(key: Buffer, element: Buffer, F: bigint): bigint {
const h = siphash24(key, element);
// (h * F) >> 64 — do it in bigint to avoid precision loss.
return (h * F) >> 64n;
}
// Read one Golomb-Rice-coded value: unary prefix (`q` ones, then 0)
// followed by P bits of remainder. Value = q*2^P + r.
class BitReader {
private byte = 0;
private bit = 0;
constructor(private buf: Buffer) {}
readBit(): number {
if (this.byte >= this.buf.length) throw new Error('BitReader: past end of buffer');
const b = (this.buf[this.byte] >> (7 - this.bit)) & 1;
if (++this.bit === 8) {
this.bit = 0;
this.byte++;
}
return b;
}
readBits(n: number): bigint {
let out = 0n;
for (let i = 0; i < n; i++) out = (out << 1n) | BigInt(this.readBit());
return out;
}
}
// Decode all values in a Golomb-coded set into a sorted array.
// The set body is what `cfilter.filter` returns AFTER the varint N
// prefix has been consumed.
function decodeGcsValues(body: Buffer, n: number): bigint[] {
const reader = new BitReader(body);
const values: bigint[] = new Array(n);
let last = 0n;
for (let i = 0; i < n; i++) {
// Unary quotient: count 1-bits until we see a 0.
let q = 0n;
while (reader.readBit() === 1) q++;
const r = reader.readBits(P);
const delta = (q << BigInt(P)) | r;
last += delta;
values[i] = last;
}
return values;
}
// Test whether ANY of `elements` are in the filter. Returns true on
// the first match; false if none matched. This is the primary
// wallet-side operation: pass every scriptPubKey (and any input
// script data for spend detection) we care about.
export function filterMatchesAny(
filterBody: Buffer,
blockHash: string,
elements: Buffer[],
): boolean {
if (elements.length === 0) return false;
// Parse N (element count) as a varint at the start of the body.
const { value: nBig, bytesRead } = decodeVarint(filterBody, 0);
const n = Number(nBig);
if (n === 0) return false;
const setBody = filterBody.subarray(bytesRead);
const F = BigInt(n) * M;
const key = keyFromBlockHash(blockHash);
const targets = elements
.map((el) => hashToRange(key, el, F))
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
// Two-pointer sweep over sorted values + sorted targets.
const setValues = decodeGcsValues(setBody, n);
let ti = 0;
for (const v of setValues) {
while (ti < targets.length && targets[ti] < v) ti++;
if (ti === targets.length) return false;
if (targets[ti] === v) return true;
}
return false;
}
// Return the subset of elements that matched the filter — useful when
// probing one filter against many address candidates and needing to
// know WHICH matched. Slower than matchesAny (can't short-circuit).
export function filterMatches(
filterBody: Buffer,
blockHash: string,
elements: Buffer[],
): Buffer[] {
if (elements.length === 0) return [];
const { value: nBig, bytesRead } = decodeVarint(filterBody, 0);
const n = Number(nBig);
if (n === 0) return [];
const setBody = filterBody.subarray(bytesRead);
const F = BigInt(n) * M;
const key = keyFromBlockHash(blockHash);
const withHash = elements.map((el) => ({ el, target: hashToRange(key, el, F) }));
withHash.sort((a, b) => (a.target < b.target ? -1 : a.target > b.target ? 1 : 0));
const setValues = decodeGcsValues(setBody, n);
const matched: Buffer[] = [];
let ti = 0;
for (const v of setValues) {
while (ti < withHash.length && withHash[ti].target < v) ti++;
if (ti === withHash.length) break;
if (withHash[ti].target === v) {
matched.push(withHash[ti].el);
ti++;
}
}
return matched;
}