@@ -105,6 +105,7 @@ enum ChainDataSourceConfig {
105105 rpc_user : String ,
106106 rpc_password : String ,
107107 rest_client_config : Option < BitcoindRestClientConfig > ,
108+ wallet_rescan_from_height : Option < u32 > ,
108109 } ,
109110}
110111
@@ -203,6 +204,8 @@ pub enum BuildError {
203204 /// wallet birthday. Falling back to genesis would silently force a full-history rescan on
204205 /// the next successful startup, so we abort instead.
205206 ChainTipFetchFailed ,
207+ /// The configured wallet rescan height is above the current chain tip.
208+ WalletRescanHeightTooHigh ,
206209}
207210
208211impl fmt:: Display for BuildError {
@@ -246,6 +249,9 @@ impl fmt::Display for BuildError {
246249 "Failed to determine the current chain tip on first startup. Verify the chain data source is reachable and correctly configured."
247250 )
248251 } ,
252+ Self :: WalletRescanHeightTooHigh => {
253+ write ! ( f, "Wallet rescan height is above the current chain tip." )
254+ } ,
249255 }
250256 }
251257}
@@ -300,7 +306,6 @@ pub struct NodeBuilder {
300306 async_payments_role : Option < AsyncPaymentsRole > ,
301307 runtime_handle : Option < tokio:: runtime:: Handle > ,
302308 pathfinding_scores_sync_config : Option < PathfindingScoresSyncConfig > ,
303- recovery_mode : bool ,
304309}
305310
306311impl NodeBuilder {
@@ -318,7 +323,6 @@ impl NodeBuilder {
318323 let log_writer_config = None ;
319324 let runtime_handle = None ;
320325 let pathfinding_scores_sync_config = None ;
321- let recovery_mode = false ;
322326 Self {
323327 config,
324328 chain_data_source_config,
@@ -328,7 +332,6 @@ impl NodeBuilder {
328332 runtime_handle,
329333 async_payments_role : None ,
330334 pathfinding_scores_sync_config,
331- recovery_mode,
332335 }
333336 }
334337
@@ -393,15 +396,21 @@ impl NodeBuilder {
393396 /// ## Parameters:
394397 /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC
395398 /// connection.
399+ /// * `wallet_rescan_from_height` - Optional wallet birthday height to rescan from on first
400+ /// startup, before wallet state exists. Existing wallets are not rewound. The height must
401+ /// be at or below the current tip. Passing `Some(0)` rescans from genesis; passing `None`
402+ /// checkpoints at the current tip.
396403 pub fn set_chain_source_bitcoind_rpc (
397404 & mut self , rpc_host : String , rpc_port : u16 , rpc_user : String , rpc_password : String ,
405+ wallet_rescan_from_height : Option < u32 > ,
398406 ) -> & mut Self {
399407 self . chain_data_source_config = Some ( ChainDataSourceConfig :: Bitcoind {
400408 rpc_host,
401409 rpc_port,
402410 rpc_user,
403411 rpc_password,
404412 rest_client_config : None ,
413+ wallet_rescan_from_height,
405414 } ) ;
406415 self
407416 }
@@ -415,16 +424,21 @@ impl NodeBuilder {
415424 /// * `rest_host`, `rest_port` - Required parameters for the Bitcoin Core REST connection.
416425 /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC
417426 /// connection
427+ /// * `wallet_rescan_from_height` - Optional wallet birthday height to rescan from on first
428+ /// startup, before wallet state exists. Existing wallets are not rewound. The height must
429+ /// be at or below the current tip. Passing `Some(0)` rescans from genesis; passing `None`
430+ /// checkpoints at the current tip.
418431 pub fn set_chain_source_bitcoind_rest (
419432 & mut self , rest_host : String , rest_port : u16 , rpc_host : String , rpc_port : u16 ,
420- rpc_user : String , rpc_password : String ,
433+ rpc_user : String , rpc_password : String , wallet_rescan_from_height : Option < u32 > ,
421434 ) -> & mut Self {
422435 self . chain_data_source_config = Some ( ChainDataSourceConfig :: Bitcoind {
423436 rpc_host,
424437 rpc_port,
425438 rpc_user,
426439 rpc_password,
427440 rest_client_config : Some ( BitcoindRestClientConfig { rest_host, rest_port } ) ,
441+ wallet_rescan_from_height,
428442 } ) ;
429443
430444 self
@@ -615,16 +629,6 @@ impl NodeBuilder {
615629 Ok ( self )
616630 }
617631
618- /// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
619- /// historical wallet funds.
620- ///
621- /// This should only be set on first startup when importing an older wallet from a previously
622- /// used [`NodeEntropy`].
623- pub fn set_wallet_recovery_mode ( & mut self ) -> & mut Self {
624- self . recovery_mode = true ;
625- self
626- }
627-
628632 /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
629633 /// previously configured.
630634 pub fn build ( & self , node_entropy : NodeEntropy ) -> Result < Node , BuildError > {
@@ -865,7 +869,6 @@ impl NodeBuilder {
865869 self . liquidity_source_config . as_ref ( ) ,
866870 self . pathfinding_scores_sync_config . as_ref ( ) ,
867871 self . async_payments_role ,
868- self . recovery_mode ,
869872 seed_bytes,
870873 runtime,
871874 logger,
@@ -979,14 +982,20 @@ impl ArcedNodeBuilder {
979982 /// ## Parameters:
980983 /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC
981984 /// connection.
985+ /// * `wallet_rescan_from_height` - Optional wallet birthday height to rescan from on first
986+ /// startup, before wallet state exists. Existing wallets are not rewound. The height must
987+ /// be at or below the current tip. Passing `Some(0)` rescans from genesis; passing `None`
988+ /// checkpoints at the current tip.
982989 pub fn set_chain_source_bitcoind_rpc (
983990 & self , rpc_host : String , rpc_port : u16 , rpc_user : String , rpc_password : String ,
991+ wallet_rescan_from_height : Option < u32 > ,
984992 ) {
985993 self . inner . write ( ) . expect ( "lock" ) . set_chain_source_bitcoind_rpc (
986994 rpc_host,
987995 rpc_port,
988996 rpc_user,
989997 rpc_password,
998+ wallet_rescan_from_height,
990999 ) ;
9911000 }
9921001
@@ -999,9 +1008,13 @@ impl ArcedNodeBuilder {
9991008 /// * `rest_host`, `rest_port` - Required parameters for the Bitcoin Core REST connection.
10001009 /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC
10011010 /// connection
1011+ /// * `wallet_rescan_from_height` - Optional wallet birthday height to rescan from on first
1012+ /// startup, before wallet state exists. Existing wallets are not rewound. The height must
1013+ /// be at or below the current tip. Passing `Some(0)` rescans from genesis; passing `None`
1014+ /// checkpoints at the current tip.
10021015 pub fn set_chain_source_bitcoind_rest (
10031016 & self , rest_host : String , rest_port : u16 , rpc_host : String , rpc_port : u16 ,
1004- rpc_user : String , rpc_password : String ,
1017+ rpc_user : String , rpc_password : String , wallet_rescan_from_height : Option < u32 > ,
10051018 ) {
10061019 self . inner . write ( ) . expect ( "lock" ) . set_chain_source_bitcoind_rest (
10071020 rest_host,
@@ -1010,6 +1023,7 @@ impl ArcedNodeBuilder {
10101023 rpc_port,
10111024 rpc_user,
10121025 rpc_password,
1026+ wallet_rescan_from_height,
10131027 ) ;
10141028 }
10151029
@@ -1152,15 +1166,6 @@ impl ArcedNodeBuilder {
11521166 self . inner . write ( ) . expect ( "lock" ) . set_async_payments_role ( role) . map ( |_| ( ) )
11531167 }
11541168
1155- /// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
1156- /// historical wallet funds.
1157- ///
1158- /// This should only be set on first startup when importing an older wallet from a previously
1159- /// used [`NodeEntropy`].
1160- pub fn set_wallet_recovery_mode ( & self ) {
1161- self . inner . write ( ) . expect ( "lock" ) . set_wallet_recovery_mode ( ) ;
1162- }
1163-
11641169 /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
11651170 /// previously configured.
11661171 pub fn build ( & self , node_entropy : Arc < NodeEntropy > ) -> Result < Arc < Node > , BuildError > {
@@ -1356,8 +1361,8 @@ fn build_with_store_internal(
13561361 gossip_source_config : Option < & GossipSourceConfig > ,
13571362 liquidity_source_config : Option < & LiquiditySourceConfig > ,
13581363 pathfinding_scores_sync_config : Option < & PathfindingScoresSyncConfig > ,
1359- async_payments_role : Option < AsyncPaymentsRole > , recovery_mode : bool , seed_bytes : [ u8 ; 64 ] ,
1360- runtime : Arc < Runtime > , logger : Arc < Logger > , kv_store : Arc < DynStore > ,
1364+ async_payments_role : Option < AsyncPaymentsRole > , seed_bytes : [ u8 ; 64 ] , runtime : Arc < Runtime > ,
1365+ logger : Arc < Logger > , kv_store : Arc < DynStore > ,
13611366) -> Result < Node , BuildError > {
13621367 optionally_install_rustls_cryptoprovider ( ) ;
13631368
@@ -1473,6 +1478,7 @@ fn build_with_store_internal(
14731478 rpc_user,
14741479 rpc_password,
14751480 rest_client_config,
1481+ ..
14761482 } ) => match rest_client_config {
14771483 Some ( rest_client_config) => runtime. block_on ( async {
14781484 ChainSource :: new_bitcoind_rest (
@@ -1526,6 +1532,12 @@ fn build_with_store_internal(
15261532 } ,
15271533 } ;
15281534 let chain_source = Arc :: new ( chain_source) ;
1535+ let wallet_rescan_from_height = match chain_data_source_config {
1536+ Some ( ChainDataSourceConfig :: Bitcoind { wallet_rescan_from_height, .. } ) => {
1537+ * wallet_rescan_from_height
1538+ } ,
1539+ _ => None ,
1540+ } ;
15291541
15301542 // Initialize the on-chain wallet and chain access
15311543 let xprv = bitcoin:: bip32:: Xpriv :: new_master ( config. network , & seed_bytes) . map_err ( |e| {
@@ -1568,7 +1580,14 @@ fn build_with_store_internal(
15681580 } ,
15691581 } ) ?;
15701582 let bdk_wallet = match wallet_opt {
1571- Some ( wallet) => wallet,
1583+ Some ( wallet) => {
1584+ // `wallet_rescan_from_height`, when set, is fresh-wallet-only. Rewinding a
1585+ // persisted wallet is not just replacing BDK's best block: its local-chain and
1586+ // tx-graph changesets are already persisted, and LDK state may also have synced
1587+ // to a later tip. A safe rewind needs an explicit recovery flow that invalidates
1588+ // all dependent state before replaying blocks.
1589+ wallet
1590+ } ,
15721591 None => {
15731592 // Guard against silently setting the wallet birthday to genesis on a fresh node:
15741593 // if we are creating a new wallet but failed to learn the current chain tip from
@@ -1577,9 +1596,10 @@ fn build_with_store_internal(
15771596 // Abort cleanly instead so the misconfiguration surfaces on the first startup.
15781597 // Esplora/Electrum backends currently never return a tip at build time, so they
15791598 // retain their existing behavior.
1580- let is_bitcoind_source =
1581- matches ! ( chain_data_source_config, Some ( ChainDataSourceConfig :: Bitcoind { .. } ) ) ;
1582- if !recovery_mode && chain_tip_opt. is_none ( ) && is_bitcoind_source {
1599+ if wallet_rescan_from_height. is_none ( )
1600+ && chain_tip_opt. is_none ( )
1601+ && matches ! ( chain_data_source_config, Some ( ChainDataSourceConfig :: Bitcoind { .. } ) )
1602+ {
15831603 log_error ! (
15841604 logger,
15851605 "Failed to determine chain tip on first startup. Aborting to avoid pinning the wallet birthday to genesis."
@@ -1599,23 +1619,67 @@ fn build_with_store_internal(
15991619 BuildError :: WalletSetupFailed
16001620 } ) ?;
16011621
1602- if !recovery_mode {
1603- if let Some ( best_block) = chain_tip_opt {
1604- // Insert the first checkpoint if we have it, to avoid resyncing from genesis.
1605- // TODO: Use a proper wallet birthday once BDK supports it.
1606- let mut latest_checkpoint = wallet. latest_checkpoint ( ) ;
1607- let block_id = bdk_chain:: BlockId {
1608- height : best_block. height ,
1609- hash : best_block. block_hash ,
1610- } ;
1611- latest_checkpoint = latest_checkpoint. insert ( block_id) ;
1612- let update =
1613- bdk_wallet:: Update { chain : Some ( latest_checkpoint) , ..Default :: default ( ) } ;
1614- wallet. apply_update ( update) . map_err ( |e| {
1615- log_error ! ( logger, "Failed to apply checkpoint during wallet setup: {}" , e) ;
1622+ // Decide which block (if any) to insert as the initial BDK checkpoint. If the
1623+ // bitcoind config provides a wallet rescan height, resolve that block and use it as
1624+ // the checkpoint. Otherwise, use the current chain tip to avoid any rescan.
1625+ let checkpoint_block = match wallet_rescan_from_height {
1626+ None => chain_tip_opt,
1627+ Some ( height) => {
1628+ if let Some ( chain_tip) = chain_tip_opt {
1629+ if height > chain_tip. height {
1630+ log_error ! (
1631+ logger,
1632+ "Wallet rescan height {} is above current chain tip {}." ,
1633+ height,
1634+ chain_tip. height
1635+ ) ;
1636+ return Err ( BuildError :: WalletRescanHeightTooHigh ) ;
1637+ }
1638+ }
1639+
1640+ let utxo_source = chain_source. as_utxo_source ( ) . ok_or_else ( || {
1641+ log_error ! (
1642+ logger,
1643+ "Wallet rescan height requested but the chain source does not support block-by-height lookups." ,
1644+ ) ;
16161645 BuildError :: WalletSetupFailed
16171646 } ) ?;
1618- }
1647+ let hash_res = runtime. block_on ( async {
1648+ lightning_block_sync:: gossip:: UtxoSource :: get_block_hash_by_height (
1649+ & utxo_source,
1650+ height,
1651+ )
1652+ . await
1653+ } ) ;
1654+ match hash_res {
1655+ Ok ( hash) => Some ( BlockLocator :: new ( hash, height) ) ,
1656+ Err ( e) => {
1657+ log_error ! (
1658+ logger,
1659+ "Failed to resolve block hash at height {} for wallet rescan: {:?}" ,
1660+ height,
1661+ e,
1662+ ) ;
1663+ return Err ( BuildError :: WalletSetupFailed ) ;
1664+ } ,
1665+ }
1666+ } ,
1667+ } ;
1668+
1669+ if let Some ( best_block) = checkpoint_block {
1670+ // Insert the checkpoint so BDK starts scanning from there instead of from
1671+ // genesis.
1672+ // TODO: Use a proper wallet birthday once BDK supports it.
1673+ let mut latest_checkpoint = wallet. latest_checkpoint ( ) ;
1674+ let block_id =
1675+ bdk_chain:: BlockId { height : best_block. height , hash : best_block. block_hash } ;
1676+ latest_checkpoint = latest_checkpoint. insert ( block_id) ;
1677+ let update =
1678+ bdk_wallet:: Update { chain : Some ( latest_checkpoint) , ..Default :: default ( ) } ;
1679+ wallet. apply_update ( update) . map_err ( |e| {
1680+ log_error ! ( logger, "Failed to apply checkpoint during wallet setup: {}" , e) ;
1681+ BuildError :: WalletSetupFailed
1682+ } ) ?;
16191683 }
16201684 wallet
16211685 } ,
0 commit comments