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.
132 lines
4.4 KiB
TypeScript
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;
|
|
}
|