@@ -19,6 +19,7 @@ import { HeaderEngine } from '@bitcoin-desktop/schema/codec/headers.js';
1919import { BlockEngine } from '@bitcoin-desktop/schema/codec/blocks.js' ;
2020import { Peer } from './peer.js' ;
2121import { FileHeaderStore } from './store/header-store.js' ;
22+ import { FileBlockStore } from './store/block-store.js' ;
2223import { HeaderSync } from './chain/header-sync.js' ;
2324
2425const 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 (
4950async 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
5152const store = new FileHeaderStore ( codec , he , genesisHeader , HEADERS_FILE ) ;
53+ const blockStore = new FileBlockStore ( DATA , codec ) ;
5254await 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 } )` ) ; }
5664console . log ( `header tip: ${ fmt ( store . height ) } ` ) ;
5765
5866// resume
@@ -74,16 +82,23 @@ function summary() {
7482}
7583process . on ( 'SIGINT' , async ( ) => { summary ( ) ; await checkpoint ( lastH ) ; process . exit ( 0 ) ; } ) ;
7684
85+ let fromDisk = 0 , fromNet = 0 ;
7786async 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}
108123await checkpoint ( TIP ) ;
0 commit comments