@@ -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
@@ -196,6 +197,15 @@ pub enum BuildError {
196197 AsyncPaymentsConfigMismatch ,
197198 /// An attempt to setup a DNS Resolver failed.
198199 DNSResolverSetupFailed ,
200+ /// We failed to determine the current chain tip on first startup.
201+ ///
202+ /// Returned when a fresh node is built against a Bitcoin Core RPC or REST chain source that
203+ /// is unreachable or misconfigured, so we cannot learn the tip height/hash to use as the
204+ /// wallet birthday. Falling back to genesis would silently force a full-history rescan on
205+ /// the next successful startup, so we abort instead.
206+ ChainTipFetchFailed ,
207+ /// The configured wallet rescan height is above the current chain tip.
208+ WalletRescanHeightTooHigh ,
199209}
200210
201211impl fmt:: Display for BuildError {
@@ -233,6 +243,15 @@ impl fmt::Display for BuildError {
233243 Self :: DNSResolverSetupFailed => {
234244 write ! ( f, "An attempt to setup a DNS resolver has failed." )
235245 } ,
246+ Self :: ChainTipFetchFailed => {
247+ write ! (
248+ f,
249+ "Failed to determine the current chain tip on first startup. Verify the chain data source is reachable and correctly configured."
250+ )
251+ } ,
252+ Self :: WalletRescanHeightTooHigh => {
253+ write ! ( f, "Wallet rescan height is above the current chain tip." )
254+ } ,
236255 }
237256 }
238257}
@@ -287,7 +306,6 @@ pub struct NodeBuilder {
287306 async_payments_role : Option < AsyncPaymentsRole > ,
288307 runtime_handle : Option < tokio:: runtime:: Handle > ,
289308 pathfinding_scores_sync_config : Option < PathfindingScoresSyncConfig > ,
290- recovery_mode : bool ,
291309}
292310
293311impl NodeBuilder {
@@ -305,7 +323,6 @@ impl NodeBuilder {
305323 let log_writer_config = None ;
306324 let runtime_handle = None ;
307325 let pathfinding_scores_sync_config = None ;
308- let recovery_mode = false ;
309326 Self {
310327 config,
311328 chain_data_source_config,
@@ -315,7 +332,6 @@ impl NodeBuilder {
315332 runtime_handle,
316333 async_payments_role : None ,
317334 pathfinding_scores_sync_config,
318- recovery_mode,
319335 }
320336 }
321337
@@ -380,15 +396,21 @@ impl NodeBuilder {
380396 /// ## Parameters:
381397 /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC
382398 /// 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.
383403 pub fn set_chain_source_bitcoind_rpc (
384404 & mut self , rpc_host : String , rpc_port : u16 , rpc_user : String , rpc_password : String ,
405+ wallet_rescan_from_height : Option < u32 > ,
385406 ) -> & mut Self {
386407 self . chain_data_source_config = Some ( ChainDataSourceConfig :: Bitcoind {
387408 rpc_host,
388409 rpc_port,
389410 rpc_user,
390411 rpc_password,
391412 rest_client_config : None ,
413+ wallet_rescan_from_height,
392414 } ) ;
393415 self
394416 }
@@ -402,16 +424,21 @@ impl NodeBuilder {
402424 /// * `rest_host`, `rest_port` - Required parameters for the Bitcoin Core REST connection.
403425 /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC
404426 /// 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.
405431 pub fn set_chain_source_bitcoind_rest (
406432 & mut self , rest_host : String , rest_port : u16 , rpc_host : String , rpc_port : u16 ,
407- rpc_user : String , rpc_password : String ,
433+ rpc_user : String , rpc_password : String , wallet_rescan_from_height : Option < u32 > ,
408434 ) -> & mut Self {
409435 self . chain_data_source_config = Some ( ChainDataSourceConfig :: Bitcoind {
410436 rpc_host,
411437 rpc_port,
412438 rpc_user,
413439 rpc_password,
414440 rest_client_config : Some ( BitcoindRestClientConfig { rest_host, rest_port } ) ,
441+ wallet_rescan_from_height,
415442 } ) ;
416443
417444 self
@@ -602,16 +629,6 @@ impl NodeBuilder {
602629 Ok ( self )
603630 }
604631
605- /// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
606- /// historical wallet funds.
607- ///
608- /// This should only be set on first startup when importing an older wallet from a previously
609- /// used [`NodeEntropy`].
610- pub fn set_wallet_recovery_mode ( & mut self ) -> & mut Self {
611- self . recovery_mode = true ;
612- self
613- }
614-
615632 /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
616633 /// previously configured.
617634 pub fn build ( & self , node_entropy : NodeEntropy ) -> Result < Node , BuildError > {
@@ -852,7 +869,6 @@ impl NodeBuilder {
852869 self . liquidity_source_config . as_ref ( ) ,
853870 self . pathfinding_scores_sync_config . as_ref ( ) ,
854871 self . async_payments_role ,
855- self . recovery_mode ,
856872 seed_bytes,
857873 runtime,
858874 logger,
@@ -966,14 +982,20 @@ impl ArcedNodeBuilder {
966982 /// ## Parameters:
967983 /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC
968984 /// 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.
969989 pub fn set_chain_source_bitcoind_rpc (
970990 & self , rpc_host : String , rpc_port : u16 , rpc_user : String , rpc_password : String ,
991+ wallet_rescan_from_height : Option < u32 > ,
971992 ) {
972993 self . inner . write ( ) . expect ( "lock" ) . set_chain_source_bitcoind_rpc (
973994 rpc_host,
974995 rpc_port,
975996 rpc_user,
976997 rpc_password,
998+ wallet_rescan_from_height,
977999 ) ;
9781000 }
9791001
@@ -986,9 +1008,13 @@ impl ArcedNodeBuilder {
9861008 /// * `rest_host`, `rest_port` - Required parameters for the Bitcoin Core REST connection.
9871009 /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC
9881010 /// 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.
9891015 pub fn set_chain_source_bitcoind_rest (
9901016 & self , rest_host : String , rest_port : u16 , rpc_host : String , rpc_port : u16 ,
991- rpc_user : String , rpc_password : String ,
1017+ rpc_user : String , rpc_password : String , wallet_rescan_from_height : Option < u32 > ,
9921018 ) {
9931019 self . inner . write ( ) . expect ( "lock" ) . set_chain_source_bitcoind_rest (
9941020 rest_host,
@@ -997,6 +1023,7 @@ impl ArcedNodeBuilder {
9971023 rpc_port,
9981024 rpc_user,
9991025 rpc_password,
1026+ wallet_rescan_from_height,
10001027 ) ;
10011028 }
10021029
@@ -1139,15 +1166,6 @@ impl ArcedNodeBuilder {
11391166 self . inner . write ( ) . expect ( "lock" ) . set_async_payments_role ( role) . map ( |_| ( ) )
11401167 }
11411168
1142- /// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
1143- /// historical wallet funds.
1144- ///
1145- /// This should only be set on first startup when importing an older wallet from a previously
1146- /// used [`NodeEntropy`].
1147- pub fn set_wallet_recovery_mode ( & self ) {
1148- self . inner . write ( ) . expect ( "lock" ) . set_wallet_recovery_mode ( ) ;
1149- }
1150-
11511169 /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
11521170 /// previously configured.
11531171 pub fn build ( & self , node_entropy : Arc < NodeEntropy > ) -> Result < Arc < Node > , BuildError > {
@@ -1343,8 +1361,8 @@ fn build_with_store_internal(
13431361 gossip_source_config : Option < & GossipSourceConfig > ,
13441362 liquidity_source_config : Option < & LiquiditySourceConfig > ,
13451363 pathfinding_scores_sync_config : Option < & PathfindingScoresSyncConfig > ,
1346- async_payments_role : Option < AsyncPaymentsRole > , recovery_mode : bool , seed_bytes : [ u8 ; 64 ] ,
1347- 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 > ,
13481366) -> Result < Node , BuildError > {
13491367 optionally_install_rustls_cryptoprovider ( ) ;
13501368
@@ -1460,6 +1478,7 @@ fn build_with_store_internal(
14601478 rpc_user,
14611479 rpc_password,
14621480 rest_client_config,
1481+ ..
14631482 } ) => match rest_client_config {
14641483 Some ( rest_client_config) => runtime. block_on ( async {
14651484 ChainSource :: new_bitcoind_rest (
@@ -1513,6 +1532,12 @@ fn build_with_store_internal(
15131532 } ,
15141533 } ;
15151534 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+ } ;
15161541
15171542 // Initialize the on-chain wallet and chain access
15181543 let xprv = bitcoin:: bip32:: Xpriv :: new_master ( config. network , & seed_bytes) . map_err ( |e| {
@@ -1555,8 +1580,33 @@ fn build_with_store_internal(
15551580 } ,
15561581 } ) ?;
15571582 let bdk_wallet = match wallet_opt {
1558- 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+ } ,
15591591 None => {
1592+ // Guard against silently setting the wallet birthday to genesis on a fresh node:
1593+ // if we are creating a new wallet but failed to learn the current chain tip from
1594+ // a Bitcoin Core RPC/REST backend, we'd otherwise persist fresh wallet state
1595+ // pinned at height 0 and force a full-history rescan once the backend comes back.
1596+ // Abort cleanly instead so the misconfiguration surfaces on the first startup.
1597+ // Esplora/Electrum backends currently never return a tip at build time, so they
1598+ // retain their existing behavior.
1599+ if wallet_rescan_from_height. is_none ( )
1600+ && chain_tip_opt. is_none ( )
1601+ && matches ! ( chain_data_source_config, Some ( ChainDataSourceConfig :: Bitcoind { .. } ) )
1602+ {
1603+ log_error ! (
1604+ logger,
1605+ "Failed to determine chain tip on first startup. Aborting to avoid pinning the wallet birthday to genesis."
1606+ ) ;
1607+ return Err ( BuildError :: ChainTipFetchFailed ) ;
1608+ }
1609+
15601610 let mut wallet = runtime
15611611 . block_on ( async {
15621612 BdkWallet :: create ( descriptor, change_descriptor)
@@ -1569,23 +1619,67 @@ fn build_with_store_internal(
15691619 BuildError :: WalletSetupFailed
15701620 } ) ?;
15711621
1572- if !recovery_mode {
1573- if let Some ( best_block) = chain_tip_opt {
1574- // Insert the first checkpoint if we have it, to avoid resyncing from genesis.
1575- // TODO: Use a proper wallet birthday once BDK supports it.
1576- let mut latest_checkpoint = wallet. latest_checkpoint ( ) ;
1577- let block_id = bdk_chain:: BlockId {
1578- height : best_block. height ,
1579- hash : best_block. block_hash ,
1580- } ;
1581- latest_checkpoint = latest_checkpoint. insert ( block_id) ;
1582- let update =
1583- bdk_wallet:: Update { chain : Some ( latest_checkpoint) , ..Default :: default ( ) } ;
1584- wallet. apply_update ( update) . map_err ( |e| {
1585- 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+ ) ;
15861645 BuildError :: WalletSetupFailed
15871646 } ) ?;
1588- }
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+ } ) ?;
15891683 }
15901684 wallet
15911685 } ,
0 commit comments