@@ -66,10 +66,38 @@ enum CbfRuntimeStatus {
6666
6767#[ derive( Clone , Copy ) ]
6868enum CbfSyncState {
69- Active { applied_tip : Option < u32 > } ,
69+ Active {
70+ /// Highest tip whose preceding chain updates have been applied to all listeners.
71+ applied_tip : Option < u32 > ,
72+ /// Whether kyoto has reported catching up to the network tip (via `FiltersSynced`) and
73+ /// the resulting blocks have been applied. `wait_until_synced` blocks until this is set.
74+ ///
75+ /// This must not be derived from a locally-sampled chain tip: kyoto does not persist, so a
76+ /// freshly (re)started node's local header chain sits at genesis until it syncs from peers.
77+ /// Comparing against that would make `wait_until_synced` return before any sync happens.
78+ synced_to_tip : bool ,
79+ } ,
7080 Failed ( Error ) ,
7181}
7282
83+ /// Marks that we are applying a block past the last `FiltersSynced` tip, so a `sync_wallets` call
84+ /// issued after new blocks are mined waits for the next `FiltersSynced` rather than returning on a
85+ /// stale `synced_to_tip`. Only flips (and notifies waiters) when currently set.
86+ ///
87+ /// Called both when a new block's filter is received (before it is fetched and applied) and after
88+ /// it is applied, so `synced_to_tip` reflects "behind by an unapplied block" as soon as we learn
89+ /// that block exists, not only once we've finished catching up to it.
90+ fn mark_syncing ( sync_state_tx : & watch:: Sender < CbfSyncState > ) {
91+ // Copy the current state out and drop the `watch` read guard before calling `send_replace`:
92+ // `borrow()` holds a read lock for the lifetime of its temporary, and `send_replace` takes
93+ // the write lock, so holding the borrow across it deadlocks. `CbfSyncState` is `Copy`, so the
94+ // deref copies and the guard is released at the end of this statement.
95+ let current = * sync_state_tx. borrow ( ) ;
96+ if let CbfSyncState :: Active { applied_tip, synced_to_tip : true } = current {
97+ sync_state_tx. send_replace ( CbfSyncState :: Active { applied_tip, synced_to_tip : false } ) ;
98+ }
99+ }
100+
73101/// Struct for holding cbf chain source
74102pub struct CbfChainSource {
75103 /// Trusted peer addresses for kyoto's `Builder::add_peers`.
@@ -91,24 +119,13 @@ pub struct CbfChainSource {
91119 logger : Arc < Logger > ,
92120}
93121
122+ #[ derive( Debug ) ]
94123enum ChainOp {
95- ConnectFull {
96- block : IndexedBlock ,
97- } ,
98- ConnectFiltered {
99- header : Header ,
100- height : u32 ,
101- } ,
102- Disconnect {
103- fork_point : BlockLocator ,
104- } ,
105- /// Marks reaching the chain tip.
106- Synced {
107- tip_height : u32 ,
108- } ,
109- Failed {
110- error : Error ,
111- } ,
124+ ConnectFull { block : IndexedBlock } ,
125+ ConnectFiltered { header : Header , height : u32 } ,
126+ Disconnect { fork_point : BlockLocator } ,
127+ Synced { tip_height : u32 } ,
128+ Failed { error : Error } ,
112129}
113130
114131struct BlockApplicator {
@@ -140,6 +157,7 @@ impl BlockApplicator {
140157 }
141158 self . chain_listener . block_connected ( & ib. block , ib. height ) ;
142159 self . next_height += 1 ;
160+ mark_syncing ( & self . sync_state_tx ) ;
143161 if let Some ( cache) = & self . block_fee_cache {
144162 let fee_rate = coinbase_fee_rate ( & ib. block , ib. height ) ;
145163 cache
@@ -160,12 +178,14 @@ impl BlockApplicator {
160178 }
161179 self . chain_listener . filtered_block_connected ( & header, & [ ] , height) ;
162180 self . next_height += 1 ;
181+ mark_syncing ( & self . sync_state_tx ) ;
163182 } ,
164183 ChainOp :: Disconnect { fork_point } => {
165184 self . chain_listener . blocks_disconnected ( fork_point) ;
166185 self . next_height = fork_point. height + 1 ;
167186 self . sync_state_tx . send_replace ( CbfSyncState :: Active {
168187 applied_tip : Some ( fork_point. height ) ,
188+ synced_to_tip : false ,
169189 } ) ;
170190 } ,
171191 ChainOp :: Synced { tip_height } => {
@@ -180,8 +200,10 @@ impl BlockApplicator {
180200 self . next_height
181201 ) ;
182202 }
203+ log_info ! ( self . logger, "we set new tip and published at {}" , tip_height) ;
183204 } ,
184205 ChainOp :: Failed { error } => {
206+ log_info ! ( self . logger, "we received error chain op {}" , error) ;
185207 self . sync_state_tx . send_replace ( CbfSyncState :: Failed ( error) ) ;
186208 } ,
187209 }
@@ -192,14 +214,23 @@ impl BlockApplicator {
192214 let already_published = {
193215 let sync_state = * self . sync_state_tx . borrow ( ) ;
194216 match sync_state {
195- CbfSyncState :: Active { applied_tip } => applied_tip,
217+ CbfSyncState :: Active { applied_tip, .. } => applied_tip,
196218 CbfSyncState :: Failed ( _) => None ,
197219 }
198220 } ;
199221 if already_published. map_or ( false , |published_height| published_height >= tip_height) {
222+ // Even if the applied tip is unchanged, we have now confirmed we are caught up to the
223+ // network tip, so ensure the synced flag is set for any `wait_until_synced` waiter.
224+ self . sync_state_tx . send_replace ( CbfSyncState :: Active {
225+ applied_tip : already_published,
226+ synced_to_tip : true ,
227+ } ) ;
200228 return ;
201229 }
202- self . sync_state_tx . send_replace ( CbfSyncState :: Active { applied_tip : Some ( tip_height) } ) ;
230+ self . sync_state_tx . send_replace ( CbfSyncState :: Active {
231+ applied_tip : Some ( tip_height) ,
232+ synced_to_tip : true ,
233+ } ) ;
203234 let unix_time_secs_opt =
204235 SystemTime :: now ( ) . duration_since ( UNIX_EPOCH ) . ok ( ) . map ( |d| d. as_secs ( ) ) ;
205236 if let Err ( e) = update_and_persist_node_metrics (
@@ -225,9 +256,12 @@ const FEE_WINDOW_BLOCKS: u32 = BLOCK_FEE_CACHE_CAPACITY as u32;
225256/// Electrum fee sources. Coinbase-derived rates are frequently zero on regtest/signet.
226257const CBF_MIN_FEERATE_SAT_PER_KWU : u64 = 250 ;
227258
228- /// Per-block timeout when downloading a block to derive its coinbase fee rate. Kept short so a
229- /// slow peer only delays a single sample rather than the whole fee update.
230- const CBF_FEE_BLOCK_FETCH_TIMEOUT_SECS : u64 = 10 ;
259+ /// Per-attempt timeout when downloading a block from a peer — used both for matched blocks we apply
260+ /// to the listeners and for the coinbase-fee-rate samples. Kyoto queues the request and awaits a
261+ /// peer response with no timeout of its own, so a slow or unresponsive peer would otherwise park the
262+ /// fetch forever. Kept short so a single request is bounded and can be retried (or, for fees, only
263+ /// delays one sample) rather than stalling.
264+ const CBF_BLOCK_FETCH_TIMEOUT_SECS : u64 = 10 ;
231265
232266/// Recent per-block coinbase-derived fee rates, keyed by height so we can window on the tip, evict
233267/// stale entries, and detect reorged-out blocks (a height whose cached hash no longer matches the
@@ -276,7 +310,8 @@ impl CbfChainSource {
276310 } ;
277311 let registered_scripts = Arc :: new ( Mutex :: new ( HashSet :: new ( ) ) ) ;
278312 let cbf_runtime_status = Arc :: new ( Mutex :: new ( CbfRuntimeStatus :: Stopped ) ) ;
279- let ( sync_state_tx, _) = watch:: channel ( CbfSyncState :: Active { applied_tip : None } ) ;
313+ let ( sync_state_tx, _) =
314+ watch:: channel ( CbfSyncState :: Active { applied_tip : None , synced_to_tip : false } ) ;
280315 Ok ( Self {
281316 trusted_peers,
282317 fee_source,
@@ -343,8 +378,10 @@ impl CbfChainSource {
343378 _ => None ,
344379 } ;
345380 let best_block_height = chain_listener. get_best_block ( ) . height ;
346- self . sync_state_tx
347- . send_replace ( CbfSyncState :: Active { applied_tip : Some ( best_block_height) } ) ;
381+ self . sync_state_tx . send_replace ( CbfSyncState :: Active {
382+ applied_tip : Some ( best_block_height) ,
383+ synced_to_tip : false ,
384+ } ) ;
348385 let block_applicator = BlockApplicator {
349386 next_height : best_block_height + 1 ,
350387 sync_state_tx : self . sync_state_tx . clone ( ) ,
@@ -393,6 +430,7 @@ impl CbfChainSource {
393430 Arc :: clone ( & restart_cbf_runtime_status) ,
394431 ops_tx. clone ( ) ,
395432 Arc :: clone ( & restart_listener. onchain_wallet ) ,
433+ restart_sync_state_tx. clone ( ) ,
396434 ) ) ;
397435
398436 match current_node. run ( ) . await {
@@ -460,6 +498,7 @@ impl CbfChainSource {
460498 * status = CbfRuntimeStatus :: Started { requester : new_requester } ;
461499 restart_sync_state_tx. send_replace ( CbfSyncState :: Active {
462500 applied_tip : Some ( restart_listener. get_best_block ( ) . height ) ,
501+ synced_to_tip : false ,
463502 } ) ;
464503 }
465504
@@ -495,21 +534,20 @@ impl CbfChainSource {
495534 }
496535
497536 pub ( crate ) async fn wait_until_synced ( & self ) -> Result < ( ) , Error > {
498- let requester = match & * self . cbf_runtime_status . lock ( ) . expect ( "lock" ) {
499- CbfRuntimeStatus :: Started { requester } => requester. clone ( ) ,
500- CbfRuntimeStatus :: Stopped => return Err ( Error :: NotRunning ) ,
501- } ;
502- let target_tip = requester. chain_tip ( ) . await . map_err ( |e| {
503- log_error ! ( self . logger, "Failed to fetch CBF chain tip before syncing: {:?}" , e) ;
504- Error :: TxSyncFailed
505- } ) ?;
506- let target_height = target_tip. height ;
537+ if matches ! ( & * self . cbf_runtime_status. lock( ) . expect( "lock" ) , CbfRuntimeStatus :: Stopped ) {
538+ return Err ( Error :: NotRunning ) ;
539+ }
507540 let mut sync_state_rx = self . sync_state_tx . subscribe ( ) ;
508541
542+ // Wait for kyoto to report catching up to the network tip (a `FiltersSynced`-driven
543+ // `synced_to_tip`) and for the resulting blocks to be applied. We must not target a
544+ // locally-sampled `chain_tip()`: kyoto does not persist, so a freshly (re)started node's
545+ // local header chain sits at genesis until it syncs from peers, which would let this return
546+ // before any sync happens.
509547 loop {
510548 match * sync_state_rx. borrow ( ) {
511- CbfSyncState :: Active { applied_tip } => {
512- if applied_tip . map_or ( false , |applied_height| applied_height >= target_height ) {
549+ CbfSyncState :: Active { synced_to_tip , .. } => {
550+ if synced_to_tip {
513551 return Ok ( ( ) ) ;
514552 }
515553 } ,
@@ -542,11 +580,17 @@ impl CbfChainSource {
542580 logger : Arc < Logger > , mut event_rx : mpsc:: UnboundedReceiver < Event > ,
543581 registered_scripts : Arc < Mutex < HashSet < ScriptBuf > > > ,
544582 cbf_runtime_status : Arc < Mutex < CbfRuntimeStatus > > , ops_tx : mpsc:: UnboundedSender < ChainOp > ,
545- onchain_wallet : Arc < Wallet > ,
583+ onchain_wallet : Arc < Wallet > , sync_state_tx : watch :: Sender < CbfSyncState > ,
546584 ) {
547585 while let Some ( event) = event_rx. recv ( ) . await {
548586 match event {
549587 Event :: IndexedFilter ( indexed_filter) => {
588+ // A new block's filter arrived, so we're behind by at least this block until it
589+ // is fetched (if matched) and applied. Flip this before the fetch, not after,
590+ // so a `sync_wallets` call issued in between doesn't return on a stale
591+ // `synced_to_tip` that predates this block.
592+ mark_syncing ( & sync_state_tx) ;
593+
550594 let requester = match & * cbf_runtime_status. lock ( ) . expect ( "lock" ) {
551595 CbfRuntimeStatus :: Started { requester } => requester. clone ( ) ,
552596 CbfRuntimeStatus :: Stopped => {
@@ -559,7 +603,7 @@ impl CbfChainSource {
559603 //each time we receive an IndexedFilter event, we ask bdk to give us all
560604 //revealed scripts. We create all_scripts starting from onchain wallet's
561605 //scripts and extend them with LDK's ones
562- let mut all_scripts = onchain_wallet. list_revealed_scripts ( ) ;
606+ let mut all_scripts = onchain_wallet. list_watched_scripts ( ) ;
563607 all_scripts. extend ( registered_scripts. lock ( ) . expect ( "lock" ) . iter ( ) . cloned ( ) ) ;
564608
565609 let block_hash = indexed_filter. block_hash ( ) ;
@@ -583,42 +627,37 @@ impl CbfChainSource {
583627 } ,
584628 } ;
585629
586- match handle. await {
587- Ok ( Ok ( block) ) => break block,
588- Ok ( Err ( e) ) if attempt < CBF_BLOCK_FETCH_RETRIES => {
630+ // Bound the download so an unresponsive peer can't park the fetch forever,
631+ // then flatten the three error layers (timeout / receiver dropped / fetch
632+ // error) into a single reason so the retry-or-fail decision is written once.
633+ let fetched = tokio:: time:: timeout (
634+ Duration :: from_secs ( CBF_BLOCK_FETCH_TIMEOUT_SECS ) ,
635+ handle,
636+ )
637+ . await
638+ . map_err ( |_| {
639+ format ! ( "timed out after {}s" , CBF_BLOCK_FETCH_TIMEOUT_SECS )
640+ } )
641+ . and_then ( |recv| recv. map_err ( |_| "receiver was dropped" . to_string ( ) ) )
642+ . and_then ( |fetch| fetch. map_err ( |e| format ! ( "failed: {:?}" , e) ) ) ;
643+
644+ match fetched {
645+ Ok ( block) => break block,
646+ Err ( reason) if attempt < CBF_BLOCK_FETCH_RETRIES => {
589647 log_debug ! (
590648 logger,
591- "CBF block fetch for {} failed on attempt {}: {:?}; retrying" ,
592- block_hash,
593- attempt,
594- e
595- ) ;
596- } ,
597- Ok ( Err ( e) ) => {
598- log_error ! (
599- logger,
600- "CBF block fetch for {} failed after {} attempts: {:?}" ,
601- block_hash,
602- CBF_BLOCK_FETCH_RETRIES ,
603- e
604- ) ;
605- let _ =
606- ops_tx. send ( ChainOp :: Failed { error : Error :: TxSyncFailed } ) ;
607- return ;
608- } ,
609- Err ( _) if attempt < CBF_BLOCK_FETCH_RETRIES => {
610- log_debug ! (
611- logger,
612- "CBF block receiver for {} dropped on attempt {}; retrying" ,
649+ "CBF block fetch for {} {} on attempt {}; retrying" ,
613650 block_hash,
651+ reason,
614652 attempt
615653 ) ;
616654 } ,
617- Err ( _ ) => {
655+ Err ( reason ) => {
618656 log_error ! (
619657 logger,
620- "CBF block receiver for {} dropped after {} attempts" ,
658+ "CBF block fetch for {} {} after {} attempts; giving up " ,
621659 block_hash,
660+ reason,
622661 CBF_BLOCK_FETCH_RETRIES
623662 ) ;
624663 let _ =
@@ -632,9 +671,7 @@ impl CbfChainSource {
632671 let height = indexed_filter. height ( ) ;
633672 //TODO we need to recheck that a particular height has not been
634673 //reorganized, and we retrieve indeed the same block header that we
635- //received `IndexedFilter` event of. right now this would block
636- //the further sync, as we cannot apply blocks in order.
637- //Future solution would use something like `get_header_by_hash`.
674+ //received `IndexedFilter` event of.
638675 match requester. get_header ( height) . await {
639676 Ok ( Some ( indexed_header) ) => {
640677 if indexed_header. block_hash ( ) != block_hash {
@@ -713,10 +750,6 @@ impl CbfChainSource {
713750 self . registered_scripts . lock ( ) . expect ( "lock" ) . insert ( output. script_pubkey ) ;
714751 }
715752
716- // pub(crate) fn register_script(&self, script: ScriptBuf) {
717- // self.registered_scripts.lock().expect("lock").insert(script);
718- // }
719-
720753 pub ( crate ) async fn continuously_update_fee_rate_estimates (
721754 & self , mut stop_sync_receiver : watch:: Receiver < ( ) > ,
722755 ) {
@@ -935,7 +968,7 @@ impl CbfChainSource {
935968 }
936969
937970 match tokio:: time:: timeout (
938- Duration :: from_secs ( CBF_FEE_BLOCK_FETCH_TIMEOUT_SECS ) ,
971+ Duration :: from_secs ( CBF_BLOCK_FETCH_TIMEOUT_SECS ) ,
939972 requester. average_fee_rate ( canonical_hash) ,
940973 )
941974 . await
0 commit comments