@@ -69,7 +69,7 @@ use crate::liquidity::{
6969 LSPS1ClientConfig , LSPS2ClientConfig , LSPS2ServiceConfig , LiquiditySourceBuilder ,
7070} ;
7171use crate :: lnurl_auth:: LnurlAuth ;
72- use crate :: logger:: { log_error, LdkLogger , LogLevel , LogWriter , Logger } ;
72+ use crate :: logger:: { log_error, log_info , LdkLogger , LogLevel , LogWriter , Logger } ;
7373use crate :: message_handler:: NodeCustomMessageHandler ;
7474use crate :: payment:: asynchronous:: om_mailbox:: OnionMessageMailbox ;
7575use crate :: peer_store:: PeerStore ;
@@ -247,6 +247,7 @@ pub struct NodeBuilder {
247247 runtime_handle : Option < tokio:: runtime:: Handle > ,
248248 pathfinding_scores_sync_config : Option < PathfindingScoresSyncConfig > ,
249249 recovery_mode : bool ,
250+ wallet_birthday_height : Option < u32 > ,
250251}
251252
252253impl NodeBuilder {
@@ -265,6 +266,7 @@ impl NodeBuilder {
265266 let runtime_handle = None ;
266267 let pathfinding_scores_sync_config = None ;
267268 let recovery_mode = false ;
269+ let wallet_birthday_height = None ;
268270 Self {
269271 config,
270272 chain_data_source_config,
@@ -275,6 +277,7 @@ impl NodeBuilder {
275277 async_payments_role : None ,
276278 pathfinding_scores_sync_config,
277279 recovery_mode,
280+ wallet_birthday_height,
278281 }
279282 }
280283
@@ -559,6 +562,22 @@ impl NodeBuilder {
559562 self
560563 }
561564
565+ /// Sets the wallet birthday height for seed recovery on pruned nodes.
566+ ///
567+ /// When set, the on-chain wallet will start scanning from the given block height
568+ /// instead of the current chain tip. This allows recovery of historical funds
569+ /// without scanning from genesis, which is critical for pruned nodes where
570+ /// early blocks are unavailable.
571+ ///
572+ /// The birthday height should be set to a block height at or before the wallet's
573+ /// first transaction. If unknown, use a conservative estimate.
574+ ///
575+ /// This only takes effect when creating a new wallet (not when loading existing state).
576+ pub fn set_wallet_birthday_height ( & mut self , height : u32 ) -> & mut Self {
577+ self . wallet_birthday_height = Some ( height) ;
578+ self
579+ }
580+
562581 /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
563582 /// previously configured.
564583 pub fn build ( & self , node_entropy : NodeEntropy ) -> Result < Node , BuildError > {
@@ -732,6 +751,7 @@ impl NodeBuilder {
732751 self . pathfinding_scores_sync_config . as_ref ( ) ,
733752 self . async_payments_role ,
734753 self . recovery_mode ,
754+ self . wallet_birthday_height ,
735755 seed_bytes,
736756 runtime,
737757 logger,
@@ -981,6 +1001,13 @@ impl ArcedNodeBuilder {
9811001 self . inner . write ( ) . unwrap ( ) . set_wallet_recovery_mode ( ) ;
9821002 }
9831003
1004+ /// Sets the wallet birthday height for seed recovery on pruned nodes.
1005+ ///
1006+ /// See [`NodeBuilder::set_wallet_birthday_height`] for details.
1007+ pub fn set_wallet_birthday_height ( & self , height : u32 ) {
1008+ self . inner . write ( ) . unwrap ( ) . set_wallet_birthday_height ( height) ;
1009+ }
1010+
9841011 /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
9851012 /// previously configured.
9861013 pub fn build ( & self , node_entropy : Arc < NodeEntropy > ) -> Result < Arc < Node > , BuildError > {
@@ -1124,7 +1151,8 @@ fn build_with_store_internal(
11241151 gossip_source_config : Option < & GossipSourceConfig > ,
11251152 liquidity_source_config : Option < & LiquiditySourceConfig > ,
11261153 pathfinding_scores_sync_config : Option < & PathfindingScoresSyncConfig > ,
1127- async_payments_role : Option < AsyncPaymentsRole > , recovery_mode : bool , seed_bytes : [ u8 ; 64 ] ,
1154+ async_payments_role : Option < AsyncPaymentsRole > , recovery_mode : bool ,
1155+ wallet_birthday_height : Option < u32 > , seed_bytes : [ u8 ; 64 ] ,
11281156 runtime : Arc < Runtime > , logger : Arc < Logger > , kv_store : Arc < DynStore > ,
11291157) -> Result < Node , BuildError > {
11301158 optionally_install_rustls_cryptoprovider ( ) ;
@@ -1321,10 +1349,65 @@ fn build_with_store_internal(
13211349 BuildError :: WalletSetupFailed
13221350 } ) ?;
13231351
1324- if !recovery_mode {
1352+ if let Some ( birthday_height) = wallet_birthday_height {
1353+ // Wallet birthday: checkpoint at the birthday block so the wallet
1354+ // syncs from there, allowing fund recovery on pruned nodes.
1355+ let birthday_hash_res = runtime. block_on ( async {
1356+ chain_source. get_block_hash_by_height ( birthday_height) . await
1357+ } ) ;
1358+ match birthday_hash_res {
1359+ Ok ( birthday_hash) => {
1360+ log_info ! (
1361+ logger,
1362+ "Setting wallet checkpoint at birthday height {} ({})" ,
1363+ birthday_height,
1364+ birthday_hash
1365+ ) ;
1366+ let mut latest_checkpoint = wallet. latest_checkpoint ( ) ;
1367+ let block_id = bdk_chain:: BlockId {
1368+ height : birthday_height,
1369+ hash : birthday_hash,
1370+ } ;
1371+ latest_checkpoint = latest_checkpoint. insert ( block_id) ;
1372+ let update = bdk_wallet:: Update {
1373+ chain : Some ( latest_checkpoint) ,
1374+ ..Default :: default ( )
1375+ } ;
1376+ wallet. apply_update ( update) . map_err ( |e| {
1377+ log_error ! ( logger, "Failed to apply birthday checkpoint: {}" , e) ;
1378+ BuildError :: WalletSetupFailed
1379+ } ) ?;
1380+ } ,
1381+ Err ( e) => {
1382+ log_error ! (
1383+ logger,
1384+ "Failed to fetch block hash at birthday height {}: {:?}. \
1385+ Falling back to current tip.",
1386+ birthday_height,
1387+ e
1388+ ) ;
1389+ // Fall back to current tip
1390+ if let Some ( best_block) = chain_tip_opt {
1391+ let mut latest_checkpoint = wallet. latest_checkpoint ( ) ;
1392+ let block_id = bdk_chain:: BlockId {
1393+ height : best_block. height ,
1394+ hash : best_block. block_hash ,
1395+ } ;
1396+ latest_checkpoint = latest_checkpoint. insert ( block_id) ;
1397+ let update = bdk_wallet:: Update {
1398+ chain : Some ( latest_checkpoint) ,
1399+ ..Default :: default ( )
1400+ } ;
1401+ wallet. apply_update ( update) . map_err ( |e| {
1402+ log_error ! ( logger, "Failed to apply fallback checkpoint: {}" , e) ;
1403+ BuildError :: WalletSetupFailed
1404+ } ) ?;
1405+ }
1406+ } ,
1407+ }
1408+ } else if !recovery_mode {
13251409 if let Some ( best_block) = chain_tip_opt {
1326- // Insert the first checkpoint if we have it, to avoid resyncing from genesis.
1327- // TODO: Use a proper wallet birthday once BDK supports it.
1410+ // No birthday: insert current tip to avoid resyncing from genesis.
13281411 let mut latest_checkpoint = wallet. latest_checkpoint ( ) ;
13291412 let block_id = bdk_chain:: BlockId {
13301413 height : best_block. height ,
@@ -1339,6 +1422,7 @@ fn build_with_store_internal(
13391422 } ) ?;
13401423 }
13411424 }
1425+ // else: recovery_mode without birthday syncs from genesis
13421426 wallet
13431427 } ,
13441428 } ;
0 commit comments