Skip to content

Commit 172b44e

Browse files
Add wallet birthday height for seed recovery on pruned nodes
When set_wallet_birthday_height(height) is called, the BDK wallet checkpoint is set to the birthday block instead of the current chain tip. This allows the wallet to sync from the birthday forward, recovering historical UTXOs without scanning from genesis. This is critical for pruned nodes where blocks before the birthday are unavailable, making recovery_mode (which scans from genesis) unusable. Three-way logic: - Birthday set: checkpoint at birthday block - No birthday, no recovery mode: checkpoint at current tip (existing) - Recovery mode without birthday: sync from genesis (existing) Falls back to current tip if the birthday block hash cannot be fetched. Resolves the TODO: 'Use a proper wallet birthday once BDK supports it.' Closes #818
1 parent 804f00f commit 172b44e

File tree

2 files changed

+103
-5
lines changed

2 files changed

+103
-5
lines changed

src/builder.rs

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ use crate::liquidity::{
6969
LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder,
7070
};
7171
use 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};
7373
use crate::message_handler::NodeCustomMessageHandler;
7474
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
7575
use 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

255256
impl 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
};

src/chain/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use bitcoin::{Script, Txid};
1717
use lightning::chain::{BestBlock, Filter};
1818

1919
use crate::chain::bitcoind::{BitcoindChainSource, UtxoSourceClient};
20+
use lightning_block_sync::gossip::UtxoSource;
2021
use crate::chain::electrum::ElectrumChainSource;
2122
use crate::chain::esplora::EsploraChainSource;
2223
use crate::config::{
@@ -214,6 +215,19 @@ impl ChainSource {
214215
}
215216
}
216217

218+
/// Fetches the block hash at the given height from the chain source.
219+
pub(crate) async fn get_block_hash_by_height(
220+
&self, height: u32,
221+
) -> Result<bitcoin::BlockHash, ()> {
222+
match &self.kind {
223+
ChainSourceKind::Bitcoind(bitcoind_chain_source) => {
224+
let utxo_source = bitcoind_chain_source.as_utxo_source();
225+
utxo_source.get_block_hash_by_height(height).await.map_err(|_| ())
226+
},
227+
_ => Err(()),
228+
}
229+
}
230+
217231
pub(crate) fn registered_txids(&self) -> Vec<Txid> {
218232
self.registered_txids.lock().unwrap().clone()
219233
}

0 commit comments

Comments
 (0)