Skip to content

Commit 6728f91

Browse files
Validator reads from the BlockStore (offline), p2p only for gaps
validate-testnet4.js now reads each block from FileBlockStore (disk) and only fetches missing blocks over p2p (storing them). With a complete store it runs fully offline; the peer connection is optional. Progress shows disk vs net. Smoke: 300 blocks validated entirely from disk at ~2,800 blk/s, 0 network. This is the real node shape: grab+store once (download-testnet4), then validate from disk (validate-testnet4) -- the slow signature work is decoupled from the network.
1 parent 37cb140 commit 6728f91

1 file changed

Lines changed: 26 additions & 11 deletions

File tree

‎src/validate-testnet4.js‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { HeaderEngine } from '@bitcoin-desktop/schema/codec/headers.js';
1919
import { BlockEngine } from '@bitcoin-desktop/schema/codec/blocks.js';
2020
import { Peer } from './peer.js';
2121
import { FileHeaderStore } from './store/header-store.js';
22+
import { FileBlockStore } from './store/block-store.js';
2223
import { HeaderSync } from './chain/header-sync.js';
2324

2425
const load = async (p) => JSON.parse(await readFile(new URL(import.meta.resolve('@bitcoin-desktop/schema/' + p)), 'utf8'));
@@ -49,10 +50,17 @@ async function candidates() { const c = []; for (const s of SEEDS) { try { for (
4950
async function firstPeer() { const hosts = await candidates(); while (hosts.length) { const peer = new Peer(p2p, codec); try { await peer.connect(hosts.shift(), params.port); return peer; } catch { peer.close(); } } throw new Error('no peer'); }
5051

5152
const store = new FileHeaderStore(codec, he, genesisHeader, HEADERS_FILE);
53+
const blockStore = new FileBlockStore(DATA, codec);
5254
await store.load();
53-
let peer = await firstPeer();
54-
console.log(`syncing headers (have ${fmt(store.height)})...`);
55-
await new HeaderSync(store, he, codec).sync(async (locator) => { peer.send('getheaders', { version: 70016, blockLocator: locator, hashStop: '0'.repeat(64) }); return (await peer.waitFor(['headers'])).payload?.entries?.map((e) => e.header) ?? []; });
55+
56+
// A peer is optional: needed only to top up headers and to fetch blocks the
57+
// BlockStore is missing. With a complete store, this validates fully offline.
58+
let peer = null;
59+
try {
60+
peer = await firstPeer();
61+
console.log(`syncing headers (have ${fmt(store.height)})...`);
62+
await new HeaderSync(store, he, codec).sync(async (locator) => { peer.send('getheaders', { version: 70016, blockLocator: locator, hashStop: '0'.repeat(64) }); return (await peer.waitFor(['headers'])).payload?.entries?.map((e) => e.header) ?? []; });
63+
} catch (e) { console.log(`offline: validating from the local store (no peer: ${e.message})`); }
5664
console.log(`header tip: ${fmt(store.height)}`);
5765

5866
// resume
@@ -74,16 +82,23 @@ function summary() {
7482
}
7583
process.on('SIGINT', async () => { summary(); await checkpoint(lastH); process.exit(0); });
7684

85+
let fromDisk = 0, fromNet = 0;
7786
async function fetchWindow(lo, hi) {
78-
const want = new Map(); for (let k = lo; k <= hi; k++) want.set(codec.blockHash(store.headerAt(k)), k);
7987
const win = new Map();
80-
for (let attempt = 0; win.size < hi - lo + 1 && attempt < 5; attempt++) {
81-
const need = [...want.entries()].filter(([, k]) => !win.has(k)).map(([hash]) => hash);
82-
if (!need.length) break;
83-
try { peer.send('getdata', { items: need.map((hash) => ({ type: WITNESS_BLOCK, hash })) }); await peer.collect('block', need.length, { timeoutMs: 120000, onItem: (m) => { const b = m.payload; const k = want.get(codec.blockHash(b.header)); if (k != null) win.set(k, b); } }); }
84-
catch { peer.close(); peer = await firstPeer(); }
88+
const want = new Map(); // hash -> height, for blocks not on disk
89+
for (let k = lo; k <= hi; k++) {
90+
const b = blockStore.get(k);
91+
if (b) { win.set(k, b); fromDisk++; } else want.set(codec.blockHash(store.headerAt(k)), k);
92+
}
93+
for (let attempt = 0; want.size && attempt < 5; attempt++) {
94+
if (!peer) peer = await firstPeer();
95+
const need = [...want.keys()];
96+
try {
97+
peer.send('getdata', { items: need.map((hash) => ({ type: WITNESS_BLOCK, hash })) });
98+
await peer.collect('block', need.length, { timeoutMs: 120000, onItem: (m) => { const b = m.payload; const hash = codec.blockHash(b.header); const k = want.get(hash); if (k != null) { blockStore.putSync(k, b); win.set(k, b); want.delete(hash); fromNet++; } } });
99+
} catch { try { peer?.close(); } catch {} peer = null; }
85100
}
86-
if (win.size < hi - lo + 1) throw new Error(`window ${lo}..${hi} incomplete (${win.size}/${hi - lo + 1})`);
101+
if (want.size) throw new Error(`window ${lo}..${hi}: ${want.size} block(s) not in store and no peer`);
87102
return win;
88103
}
89104

@@ -102,7 +117,7 @@ for (let h = start; h <= TIP; h += BATCH) {
102117
validated++; txs += block.transactions.length; lastH = k;
103118
}
104119
const secs = (Date.now() - t0) / 1000, rate = validated / secs;
105-
process.stdout.write(`\r validated ${fmt(hi)}/${fmt(TIP)} | ${rate.toFixed(0)} blk/s | ${fmt(txs)} tx | utxo ${fmt(utxo.size)} | warns ${warnings.size} | ${(secs / 60).toFixed(1)} min, ETA ${((TIP - hi) / rate / 60).toFixed(0)} min `);
120+
process.stdout.write(`\r validated ${fmt(hi)}/${fmt(TIP)} | ${rate.toFixed(0)} blk/s | disk ${fmt(fromDisk)} net ${fmt(fromNet)} | utxo ${fmt(utxo.size)} | warns ${warnings.size} | ETA ${((TIP - hi) / rate / 60).toFixed(0)} min `);
106121
if (hi % 2000 < BATCH) await checkpoint(hi);
107122
}
108123
await checkpoint(TIP);

0 commit comments

Comments
 (0)