dgb-wallet/packages/p2p/scripts/block-fetch-demo.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

169 lines
6.6 KiB
TypeScript

// Milestone A5 demo — filter probe → block fetch → UTXO extraction.
// Given a set of watched scripts and a small block range:
// 1. Sync headers up to the range
// 2. Download filters for the range
// 3. For any filter that matches: fetch the full block
// 4. Extract our UTXOs from those blocks
// Also fetches one arbitrary block unconditionally to prove the
// block-fetch + decode pipeline works even when the wallet has no
// hits in the tested range (the common case for a throwaway seed).
import {
PeerPool,
HeaderChain,
encodeGetHeaders,
encodeGetCfilters,
encodeGetBlock,
filterMatchesAny,
extractFromBlock,
FILTER_TYPE_BASIC,
NODE_COMPACT_FILTERS,
type BlockHeader,
type CfilterMessage,
} from '../src/index.js';
import type { Block } from 'bitcoinjs-lib';
import {
seedFromPhrase,
rootFromSeed,
accountNode,
addressNode,
digibyte,
} from '@dgb-wallet/core';
import { payments } from 'bitcoinjs-lib';
const DGB_GENESIS_HASH = '7497ea1b465eb39f1c8f507bc877078fe016d6fcb6dfad3a64c98dcc6e1e8496';
const START = Number(process.argv[2]) || 145_000;
const COUNT = Number(process.argv[3]) || 100;
const RUN_MS = 120_000;
console.log('\n=== DigiByte BIP158 Filter → Block Fetch → UTXO Extract ===\n');
console.log(`Filter range: ${START.toLocaleString()} .. ${(START + COUNT).toLocaleString()}\n`);
const testSeed = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const seed = await seedFromPhrase(testSeed);
const root = rootFromSeed(seed);
const acct = accountNode(root, 84, 0);
const watchScripts = new Set<string>();
const scriptBuffers: Buffer[] = [];
for (let i = 0; i < 20; i++) {
const node = addressNode(acct, 0, i);
const spk = payments.p2wpkh({ pubkey: Buffer.from(node.publicKey), network: digibyte }).output!;
const buf = Buffer.from(spk);
scriptBuffers.push(buf);
watchScripts.add(buf.toString('hex'));
}
const ownedOutpoints = new Set<string>();
// Use a bigger pool so we're likely to hit a CF-serving peer quickly
// (only ~30% of DGB peers advertise NODE_COMPACT_FILTERS per A2 data).
const pool = new PeerPool({ poolSize: 12, peerOpts: { handshakeTimeoutMs: 10_000 } });
const chain = new HeaderChain();
chain.appendGenesis({
hash: DGB_GENESIS_HASH, prevBlock: '00'.repeat(32), merkleRoot: '',
version: 0, timestamp: 0, bits: 0, nonce: 0, raw: Buffer.alloc(80),
}, 0);
let peer: ReturnType<typeof pool.pickReady>[number] | null = null;
let phase: 'headers' | 'filters' | 'blocks' | 'done' = 'headers';
let filterCount = 0;
const matchedHashes: string[] = [];
let blocksFetched = 0;
let totalDiscovered = 0;
let didUnconditionalFetch = false;
const t0 = Date.now();
pool.on('connect', () => {
if (peer) return;
const cfPeers = pool.pickWithServices(NODE_COMPACT_FILTERS);
if (cfPeers.length === 0) return; // wait for a CF-serving peer to connect
peer = cfPeers[0];
console.log(`Peer: ${peer.host}:${peer.port} (advertises COMPACT_FILTERS)\n`);
peer.on('headers', (headers: BlockHeader[]) => {
if (phase !== 'headers') return;
chain.append(headers);
process.stdout.write(`\r headers: ${chain.tipHeight.toLocaleString()} `);
if (chain.tipHeight >= START + COUNT) {
const stop = chain.getByHeight(START + COUNT - 1);
if (!stop) return finish('no stop header');
phase = 'filters';
console.log(`\n header sync done in ${((Date.now() - t0) / 1000).toFixed(1)}s\n`);
console.log(`Requesting cfilters ${START}..${START + COUNT - 1}`);
peer!.send('getcfilters', encodeGetCfilters(FILTER_TYPE_BASIC, START, stop.hash));
return;
}
if (headers.length >= 2000) {
peer!.send('getheaders', encodeGetHeaders(70019, chain.buildLocator()));
}
});
peer.on('cfilter', (msg: CfilterMessage) => {
if (phase !== 'filters') return;
filterCount++;
if (filterMatchesAny(msg.filter, msg.blockHash, scriptBuffers)) {
console.log(` HIT block ${msg.blockHash}`);
matchedHashes.push(msg.blockHash);
}
if (filterCount % 25 === 0) {
process.stdout.write(`\r filters: ${filterCount}/${COUNT}, hits: ${matchedHashes.length} `);
}
if (filterCount >= COUNT) {
phase = 'blocks';
console.log(`\n\n filter sweep done in ${((Date.now() - t0) / 1000).toFixed(1)}s`);
console.log(` ${matchedHashes.length} filter hits, ${COUNT - matchedHashes.length} clean\n`);
// Always fetch one block unconditionally, so we prove the fetch/
// decode pipeline works even when there are no filter hits.
const proofBlock = chain.getByHeight(START)!;
console.log(`Unconditional proof-of-fetch: block ${proofBlock.hash}`);
peer!.send('getdata', encodeGetBlock(proofBlock.hash));
for (const h of matchedHashes) {
peer!.send('getdata', encodeGetBlock(h));
}
}
});
peer.on('block', (block: Block) => {
if (phase !== 'blocks' && phase !== 'done') return;
const blockHash = Buffer.from(block.getHash()).reverse().toString('hex');
blocksFetched++;
if (!didUnconditionalFetch) {
didUnconditionalFetch = true;
const txs = block.transactions ?? [];
console.log(` ✓ decoded block ${blockHash}: ${txs.length} txs, first tx has ${txs[0]?.outs?.length ?? 0} outputs`);
}
const { discovered, spent } = extractFromBlock(block, watchScripts, ownedOutpoints);
for (const u of discovered) {
totalDiscovered++;
console.log(` 💰 UTXO ${u.txid}:${u.vout} value=${u.value / 1e8} DGB (block ${blockHash})`);
}
for (const s of spent) {
console.log(` ⊘ spent ${s.spent.prevTxid}:${s.spent.prevVout} by ${s.txid}`);
}
if (blocksFetched >= matchedHashes.length + 1) finish('all blocks fetched');
});
peer.send('getheaders', encodeGetHeaders(70019, chain.buildLocator()));
});
const runTimer = setTimeout(() => finish('runtime cap reached'), RUN_MS);
function finish(reason: string): void {
clearTimeout(runTimer);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
console.log('\n--- summary ---');
console.log(` reason: ${reason}`);
console.log(` elapsed: ${elapsed}s`);
console.log(` headers synced: ${chain.tipHeight.toLocaleString()}`);
console.log(` filters received: ${filterCount}`);
console.log(` filter hits: ${matchedHashes.length}`);
console.log(` blocks fetched: ${blocksFetched}`);
console.log(` UTXOs discovered: ${totalDiscovered}`);
if (totalDiscovered === 0 && didUnconditionalFetch) {
console.log('\n 0 UTXOs is expected — test seed has no history here.');
console.log(' Block-fetch + decode pipeline verified via the unconditional proof-of-fetch above.');
}
pool.stop();
process.exit(0);
}