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.
169 lines
6.6 KiB
TypeScript
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);
|
|
}
|