@@ -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 ;
@@ -250,6 +250,7 @@ pub struct NodeBuilder {
250250 runtime_handle : Option < tokio:: runtime:: Handle > ,
251251 pathfinding_scores_sync_config : Option < PathfindingScoresSyncConfig > ,
252252 recovery_mode : bool ,
253+ wallet_birthday_height : Option < u32 > ,
253254}
254255
255256impl NodeBuilder {
@@ -268,6 +269,7 @@ impl NodeBuilder {
268269 let runtime_handle = None ;
269270 let pathfinding_scores_sync_config = None ;
270271 let recovery_mode = false ;
272+ let wallet_birthday_height = None ;
271273 Self {
272274 config,
273275 chain_data_source_config,
@@ -278,6 +280,7 @@ impl NodeBuilder {
278280 async_payments_role : None ,
279281 pathfinding_scores_sync_config,
280282 recovery_mode,
283+ wallet_birthday_height,
281284 }
282285 }
283286
@@ -579,6 +582,22 @@ impl NodeBuilder {
579582 self
580583 }
581584
585+ /// Sets the wallet birthday height for seed recovery on pruned nodes.
586+ ///
587+ /// When set, the on-chain wallet will start scanning from the given block height
588+ /// instead of the current chain tip. This allows recovery of historical funds
589+ /// without scanning from genesis, which is critical for pruned nodes where
590+ /// early blocks are unavailable.
591+ ///
592+ /// The birthday height should be set to a block height at or before the wallet's
593+ /// first transaction. If unknown, use a conservative estimate.
594+ ///
595+ /// This only takes effect when creating a new wallet (not when loading existing state).
596+ pub fn set_wallet_birthday_height ( & mut self , height : u32 ) -> & mut Self {
597+ self . wallet_birthday_height = Some ( height) ;
598+ self
599+ }
600+
582601 /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
583602 /// previously configured.
584603 pub fn build ( & self , node_entropy : NodeEntropy ) -> Result < Node , BuildError > {
@@ -752,6 +771,7 @@ impl NodeBuilder {
752771 self . pathfinding_scores_sync_config . as_ref ( ) ,
753772 self . async_payments_role ,
754773 self . recovery_mode ,
774+ self . wallet_birthday_height ,
755775 seed_bytes,
756776 runtime,
757777 logger,
@@ -1010,6 +1030,13 @@ impl ArcedNodeBuilder {
10101030 self . inner . write ( ) . unwrap ( ) . set_wallet_recovery_mode ( ) ;
10111031 }
10121032
1033+ /// Sets the wallet birthday height for seed recovery on pruned nodes.
1034+ ///
1035+ /// See [`NodeBuilder::set_wallet_birthday_height`] for details.
1036+ pub fn set_wallet_birthday_height ( & self , height : u32 ) {
1037+ self . inner . write ( ) . unwrap ( ) . set_wallet_birthday_height ( height) ;
1038+ }
1039+
10131040 /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
10141041 /// previously configured.
10151042 pub fn build ( & self , node_entropy : Arc < NodeEntropy > ) -> Result < Arc < Node > , BuildError > {
@@ -1153,7 +1180,8 @@ fn build_with_store_internal(
11531180 gossip_source_config : Option < & GossipSourceConfig > ,
11541181 liquidity_source_config : Option < & LiquiditySourceConfig > ,
11551182 pathfinding_scores_sync_config : Option < & PathfindingScoresSyncConfig > ,
1156- async_payments_role : Option < AsyncPaymentsRole > , recovery_mode : bool , seed_bytes : [ u8 ; 64 ] ,
1183+ async_payments_role : Option < AsyncPaymentsRole > , recovery_mode : bool ,
1184+ wallet_birthday_height : Option < u32 > , seed_bytes : [ u8 ; 64 ] ,
11571185 runtime : Arc < Runtime > , logger : Arc < Logger > , kv_store : Arc < DynStore > ,
11581186) -> Result < Node , BuildError > {
11591187 optionally_install_rustls_cryptoprovider ( ) ;
@@ -1359,10 +1387,65 @@ fn build_with_store_internal(
13591387 BuildError :: WalletSetupFailed
13601388 } ) ?;
13611389
1362- if !recovery_mode {
1390+ if let Some ( birthday_height) = wallet_birthday_height {
1391+ // Wallet birthday: checkpoint at the birthday block so the wallet
1392+ // syncs from there, allowing fund recovery on pruned nodes.
1393+ let birthday_hash_res = runtime. block_on ( async {
1394+ chain_source. get_block_hash_by_height ( birthday_height) . await
1395+ } ) ;
1396+ match birthday_hash_res {
1397+ Ok ( birthday_hash) => {
1398+ log_info ! (
1399+ logger,
1400+ "Setting wallet checkpoint at birthday height {} ({})" ,
1401+ birthday_height,
1402+ birthday_hash
1403+ ) ;
1404+ let mut latest_checkpoint = wallet. latest_checkpoint ( ) ;
1405+ let block_id = bdk_chain:: BlockId {
1406+ height : birthday_height,
1407+ hash : birthday_hash,
1408+ } ;
1409+ latest_checkpoint = latest_checkpoint. insert ( block_id) ;
1410+ let update = bdk_wallet:: Update {
1411+ chain : Some ( latest_checkpoint) ,
1412+ ..Default :: default ( )
1413+ } ;
1414+ wallet. apply_update ( update) . map_err ( |e| {
1415+ log_error ! ( logger, "Failed to apply birthday checkpoint: {}" , e) ;
1416+ BuildError :: WalletSetupFailed
1417+ } ) ?;
1418+ } ,
1419+ Err ( e) => {
1420+ log_error ! (
1421+ logger,
1422+ "Failed to fetch block hash at birthday height {}: {:?}. \
1423+ Falling back to current tip.",
1424+ birthday_height,
1425+ e
1426+ ) ;
1427+ // Fall back to current tip
1428+ if let Some ( best_block) = chain_tip_opt {
1429+ let mut latest_checkpoint = wallet. latest_checkpoint ( ) ;
1430+ let block_id = bdk_chain:: BlockId {
1431+ height : best_block. height ,
1432+ hash : best_block. block_hash ,
1433+ } ;
1434+ latest_checkpoint = latest_checkpoint. insert ( block_id) ;
1435+ let update = bdk_wallet:: Update {
1436+ chain : Some ( latest_checkpoint) ,
1437+ ..Default :: default ( )
1438+ } ;
1439+ wallet. apply_update ( update) . map_err ( |e| {
1440+ log_error ! ( logger, "Failed to apply fallback checkpoint: {}" , e) ;
1441+ BuildError :: WalletSetupFailed
1442+ } ) ?;
1443+ }
1444+ } ,
1445+ }
1446+ } else if !recovery_mode {
13631447 if let Some ( best_block) = chain_tip_opt {
1364- // Insert the first checkpoint if we have it, to avoid resyncing from genesis.
1365- // TODO: Use a proper wallet birthday once BDK supports it.
1448+ // No birthday: insert current tip to avoid resyncing from genesis.
13661449 let mut latest_checkpoint = wallet. latest_checkpoint ( ) ;
13671450 let block_id = bdk_chain:: BlockId {
13681451 height : best_block. height ,
@@ -1377,6 +1460,7 @@ fn build_with_store_internal(
13771460 } ) ?;
13781461 }
13791462 }
1463+ // else: recovery_mode without birthday syncs from genesis
13801464 wallet
13811465 } ,
13821466 } ;
0 commit comments