@@ -3197,6 +3197,77 @@ async fn onchain_wallet_recovery_cbf_advances_reveal_cursor() {
31973197 recovered_node. stop ( ) . unwrap ( ) ;
31983198}
31993199
3200+ /// Regression test: a fresh CBF recovery must discover funds at derivation
3201+ /// indices past the steady-state `BDK_CLIENT_STOP_GAP` window. Without a wider
3202+ /// initial scan on fresh wallets, the first sync only covers `0..stop_gap`
3203+ /// scripts, and once BDK's checkpoint advances past the funding block the
3204+ /// derived `skip_height` excludes it from any subsequent scans — so funds
3205+ /// beyond idx 20 are silently missed.
3206+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
3207+ async fn onchain_wallet_recovery_cbf_deep_stop_gap ( ) {
3208+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
3209+ let chain_source = TestChainSource :: Cbf ( & bitcoind) ;
3210+
3211+ let original_config = random_config ( true ) ;
3212+ let original_node_entropy = original_config. node_entropy . clone ( ) ;
3213+ let original_node = setup_node ( & chain_source, original_config) ;
3214+
3215+ // Reveal addresses past the steady-state stop-gap (20), then fund one
3216+ // inside the initial window and another well beyond it. A fresh recovery
3217+ // only finds the deeper one if its first sync uses a wider stop-gap.
3218+ let mut addrs = Vec :: with_capacity ( 40 ) ;
3219+ for _ in 0 ..40 {
3220+ addrs. push ( original_node. onchain_payment ( ) . new_address ( ) . unwrap ( ) ) ;
3221+ }
3222+ let funded_low = addrs[ 19 ] . clone ( ) ;
3223+ let funded_high = addrs[ 38 ] . clone ( ) ;
3224+
3225+ let premine_amount_sat = 100_000 ;
3226+ premine_and_distribute_funds (
3227+ & bitcoind. client ,
3228+ & electrsd. client ,
3229+ vec ! [ funded_low, funded_high] ,
3230+ Amount :: from_sat ( premine_amount_sat) ,
3231+ )
3232+ . await ;
3233+
3234+ // Mine extra blocks so the funding block sits more than `REORG_SAFETY_BLOCKS`
3235+ // behind the chain tip. Without this gap a broken recovery could still find
3236+ // the deeper funds on a follow-up sync because the second-sync `skip_height`
3237+ // would not yet exclude the funding block.
3238+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 20 ) . await ;
3239+
3240+ wait_for_cbf_sync ( & original_node, || {
3241+ original_node. list_balances ( ) . spendable_onchain_balance_sats == premine_amount_sat * 2
3242+ } )
3243+ . await ;
3244+ assert_eq ! (
3245+ original_node. list_balances( ) . spendable_onchain_balance_sats,
3246+ premine_amount_sat * 2
3247+ ) ;
3248+
3249+ original_node. stop ( ) . unwrap ( ) ;
3250+ drop ( original_node) ;
3251+
3252+ // Recover from a completely fresh wallet state, same seed.
3253+ let mut recovered_config = random_config ( true ) ;
3254+ recovered_config. node_entropy = original_node_entropy;
3255+ recovered_config. recovery_mode = true ;
3256+ let recovered_node = setup_node ( & chain_source, recovered_config) ;
3257+
3258+ wait_for_cbf_sync ( & recovered_node, || {
3259+ recovered_node. list_balances ( ) . spendable_onchain_balance_sats == premine_amount_sat * 2
3260+ } )
3261+ . await ;
3262+ assert_eq ! (
3263+ recovered_node. list_balances( ) . spendable_onchain_balance_sats,
3264+ premine_amount_sat * 2 ,
3265+ "recovery did not find funds beyond the initial CBF stop-gap window"
3266+ ) ;
3267+
3268+ recovered_node. stop ( ) . unwrap ( ) ;
3269+ }
3270+
32003271#[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
32013272async fn onchain_send_receive_cbf ( ) {
32023273 let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
0 commit comments