@@ -257,6 +257,34 @@ impl fmt::Display for BuildError {
257257
258258impl std:: error:: Error for BuildError { }
259259
260+ /// Describes how the wallet should be recovered on the next startup.
261+ ///
262+ /// Pass `Some(RecoveryMode { .. })` to [`NodeBuilder::set_wallet_recovery_mode`] to opt into
263+ /// recovery behavior; see [`RecoveryMode::rescan_from_height`] for the details of what each
264+ /// setting does on each chain source.
265+ #[ derive( Debug , Clone , Copy , Default , PartialEq , Eq ) ]
266+ #[ cfg_attr( feature = "uniffi" , derive( uniffi:: Record ) ) ]
267+ pub struct RecoveryMode {
268+ /// Where the wallet should start rescanning the chain from on the next startup.
269+ ///
270+ /// Behavior depends on the configured chain source:
271+ ///
272+ /// **`Bitcoind` (RPC or REST):**
273+ /// * `None` — no wallet checkpoint is inserted; BDK rescans from genesis. This matches the
274+ /// previous `recovery_mode = true` flag.
275+ /// * `Some(h)` — the block hash at height `h` is resolved via the chain source and inserted
276+ /// as the initial wallet checkpoint, so BDK rescans forward from `h`. Useful for
277+ /// restoring a wallet on a pruned node where the full history is unavailable but the
278+ /// wallet's birthday height is known.
279+ ///
280+ /// **`Esplora` / `Electrum`:** this field is ignored — the BDK client APIs for these
281+ /// backends do not currently expose a block-hash-by-height lookup. Instead, setting any
282+ /// `Some(RecoveryMode { .. })` forces a one-shot BDK `full_scan` on the next wallet sync
283+ /// after startup — even if the node has synced before — so funds sent to addresses the
284+ /// current instance does not yet know about are re-discovered.
285+ pub rescan_from_height : Option < u32 > ,
286+ }
287+
260288/// A builder for an [`Node`] instance, allowing to set some configuration and module choices from
261289/// the getgo.
262290///
@@ -305,7 +333,7 @@ pub struct NodeBuilder {
305333 async_payments_role : Option < AsyncPaymentsRole > ,
306334 runtime_handle : Option < tokio:: runtime:: Handle > ,
307335 pathfinding_scores_sync_config : Option < PathfindingScoresSyncConfig > ,
308- recovery_mode : bool ,
336+ recovery_mode : Option < RecoveryMode > ,
309337}
310338
311339impl NodeBuilder {
@@ -323,7 +351,7 @@ impl NodeBuilder {
323351 let log_writer_config = None ;
324352 let runtime_handle = None ;
325353 let pathfinding_scores_sync_config = None ;
326- let recovery_mode = false ;
354+ let recovery_mode = None ;
327355 Self {
328356 config,
329357 chain_data_source_config,
@@ -629,13 +657,17 @@ impl NodeBuilder {
629657 Ok ( self )
630658 }
631659
632- /// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
633- /// historical wallet funds.
660+ /// Configures the [`Node`] to perform wallet recovery on the next startup, optionally
661+ /// specifying the block height to rescan the chain from.
662+ ///
663+ /// Pass `Some(RecoveryMode { .. })` to enable recovery; pass `None` to clear any previously
664+ /// configured recovery mode and use the default "checkpoint at current tip" behavior. See
665+ /// [`RecoveryMode`] for the details of what each setting does on each chain source.
634666 ///
635667 /// This should only be set on first startup when importing an older wallet from a previously
636668 /// used [`NodeEntropy`].
637- pub fn set_wallet_recovery_mode ( & mut self ) -> & mut Self {
638- self . recovery_mode = true ;
669+ pub fn set_wallet_recovery_mode ( & mut self , mode : Option < RecoveryMode > ) -> & mut Self {
670+ self . recovery_mode = mode ;
639671 self
640672 }
641673
@@ -1101,13 +1133,17 @@ impl ArcedNodeBuilder {
11011133 self . inner . write ( ) . expect ( "lock" ) . set_async_payments_role ( role) . map ( |_| ( ) )
11021134 }
11031135
1104- /// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any
1105- /// historical wallet funds.
1136+ /// Configures the [`Node`] to perform wallet recovery on the next startup, optionally
1137+ /// specifying the block height to rescan the chain from.
1138+ ///
1139+ /// Pass `Some(RecoveryMode { .. })` to enable recovery; pass `None` to clear any previously
1140+ /// configured recovery mode and use the default "checkpoint at current tip" behavior. See
1141+ /// [`RecoveryMode`] for the details of what each setting does on each chain source.
11061142 ///
11071143 /// This should only be set on first startup when importing an older wallet from a previously
11081144 /// used [`NodeEntropy`].
1109- pub fn set_wallet_recovery_mode ( & self ) {
1110- self . inner . write ( ) . expect ( "lock" ) . set_wallet_recovery_mode ( ) ;
1145+ pub fn set_wallet_recovery_mode ( & self , mode : Option < RecoveryMode > ) {
1146+ self . inner . write ( ) . expect ( "lock" ) . set_wallet_recovery_mode ( mode ) ;
11111147 }
11121148
11131149 /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
@@ -1253,8 +1289,8 @@ fn build_with_store_internal(
12531289 gossip_source_config : Option < & GossipSourceConfig > ,
12541290 liquidity_source_config : Option < & LiquiditySourceConfig > ,
12551291 pathfinding_scores_sync_config : Option < & PathfindingScoresSyncConfig > ,
1256- async_payments_role : Option < AsyncPaymentsRole > , recovery_mode : bool , seed_bytes : [ u8 ; 64 ] ,
1257- runtime : Arc < Runtime > , logger : Arc < Logger > , kv_store : Arc < DynStore > ,
1292+ async_payments_role : Option < AsyncPaymentsRole > , recovery_mode : Option < RecoveryMode > ,
1293+ seed_bytes : [ u8 ; 64 ] , runtime : Arc < Runtime > , logger : Arc < Logger > , kv_store : Arc < DynStore > ,
12581294) -> Result < Node , BuildError > {
12591295 optionally_install_rustls_cryptoprovider ( ) ;
12601296
@@ -1462,7 +1498,7 @@ fn build_with_store_internal(
14621498 // retain their existing behavior.
14631499 let is_bitcoind_source =
14641500 matches ! ( chain_data_source_config, Some ( ChainDataSourceConfig :: Bitcoind { .. } ) ) ;
1465- if ! recovery_mode && chain_tip_opt. is_none ( ) && is_bitcoind_source {
1501+ if recovery_mode. is_none ( ) && chain_tip_opt. is_none ( ) && is_bitcoind_source {
14661502 log_error ! (
14671503 logger,
14681504 "Failed to determine chain tip on first startup. Aborting to avoid pinning the wallet birthday to genesis."
@@ -1478,23 +1514,62 @@ fn build_with_store_internal(
14781514 BuildError :: WalletSetupFailed
14791515 } ) ?;
14801516
1481- if !recovery_mode {
1482- if let Some ( best_block) = chain_tip_opt {
1483- // Insert the first checkpoint if we have it, to avoid resyncing from genesis.
1484- // TODO: Use a proper wallet birthday once BDK supports it.
1485- let mut latest_checkpoint = wallet. latest_checkpoint ( ) ;
1486- let block_id = bdk_chain:: BlockId {
1487- height : best_block. height ,
1488- hash : best_block. block_hash ,
1489- } ;
1490- latest_checkpoint = latest_checkpoint. insert ( block_id) ;
1491- let update =
1492- bdk_wallet:: Update { chain : Some ( latest_checkpoint) , ..Default :: default ( ) } ;
1493- wallet. apply_update ( update) . map_err ( |e| {
1494- log_error ! ( logger, "Failed to apply checkpoint during wallet setup: {}" , e) ;
1517+ // Decide which block (if any) to insert as the initial BDK checkpoint.
1518+ // - No recovery mode: use the current chain tip to avoid any rescan.
1519+ // - Recovery mode with an explicit `rescan_from_height` on a bitcoind backend:
1520+ // resolve the block hash at that height and use it as the checkpoint, so BDK
1521+ // rescans forward from there.
1522+ // - Recovery mode otherwise (no explicit height, or non-bitcoind backend): skip
1523+ // the checkpoint entirely. For bitcoind this falls back to a full rescan from
1524+ // genesis; for Esplora/Electrum the on-chain wallet syncer forces a one-shot
1525+ // `full_scan` (see `Wallet::take_force_full_scan`).
1526+ let checkpoint_block = match recovery_mode {
1527+ None => chain_tip_opt,
1528+ Some ( RecoveryMode { rescan_from_height : Some ( height) } ) if is_bitcoind_source => {
1529+ let utxo_source = chain_source. as_utxo_source ( ) . ok_or_else ( || {
1530+ log_error ! (
1531+ logger,
1532+ "Recovery mode requested a rescan height but the chain source does not support block-by-height lookups." ,
1533+ ) ;
14951534 BuildError :: WalletSetupFailed
14961535 } ) ?;
1497- }
1536+ let hash_res = runtime. block_on ( async {
1537+ lightning_block_sync:: gossip:: UtxoSource :: get_block_hash_by_height (
1538+ & utxo_source,
1539+ height,
1540+ )
1541+ . await
1542+ } ) ;
1543+ match hash_res {
1544+ Ok ( hash) => Some ( BestBlock { block_hash : hash, height } ) ,
1545+ Err ( e) => {
1546+ log_error ! (
1547+ logger,
1548+ "Failed to resolve block hash at height {} for wallet rescan: {:?}" ,
1549+ height,
1550+ e,
1551+ ) ;
1552+ return Err ( BuildError :: WalletSetupFailed ) ;
1553+ } ,
1554+ }
1555+ } ,
1556+ Some ( _) => None ,
1557+ } ;
1558+
1559+ if let Some ( best_block) = checkpoint_block {
1560+ // Insert the checkpoint so BDK starts scanning from there instead of from
1561+ // genesis.
1562+ // TODO: Use a proper wallet birthday once BDK supports it.
1563+ let mut latest_checkpoint = wallet. latest_checkpoint ( ) ;
1564+ let block_id =
1565+ bdk_chain:: BlockId { height : best_block. height , hash : best_block. block_hash } ;
1566+ latest_checkpoint = latest_checkpoint. insert ( block_id) ;
1567+ let update =
1568+ bdk_wallet:: Update { chain : Some ( latest_checkpoint) , ..Default :: default ( ) } ;
1569+ wallet. apply_update ( update) . map_err ( |e| {
1570+ log_error ! ( logger, "Failed to apply checkpoint during wallet setup: {}" , e) ;
1571+ BuildError :: WalletSetupFailed
1572+ } ) ?;
14981573 }
14991574 wallet
15001575 } ,
@@ -1514,6 +1589,12 @@ fn build_with_store_internal(
15141589 } ,
15151590 } ;
15161591
1592+ // On Esplora/Electrum the initial wallet-checkpoint logic above cannot honor a specific
1593+ // rescan height because the backends don't expose a block-hash-by-height lookup. When the
1594+ // user has explicitly opted into recovery mode we instead force the next on-chain sync to
1595+ // escalate to a BDK `full_scan` so funds sent to previously-unknown addresses are
1596+ // re-discovered.
1597+ let force_full_scan = recovery_mode. is_some ( ) ;
15171598 let wallet = Arc :: new ( Wallet :: new (
15181599 bdk_wallet,
15191600 wallet_persister,
@@ -1524,6 +1605,7 @@ fn build_with_store_internal(
15241605 Arc :: clone ( & config) ,
15251606 Arc :: clone ( & logger) ,
15261607 Arc :: clone ( & pending_payment_store) ,
1608+ force_full_scan,
15271609 ) ) ;
15281610
15291611 // Initialize the KeysManager
0 commit comments