Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 89 additions & 5 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ use crate::liquidity::{
LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder,
};
use crate::lnurl_auth::LnurlAuth;
use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger};
use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger};
use crate::message_handler::NodeCustomMessageHandler;
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
use crate::peer_store::PeerStore;
Expand Down Expand Up @@ -247,6 +247,7 @@ pub struct NodeBuilder {
runtime_handle: Option<tokio::runtime::Handle>,
pathfinding_scores_sync_config: Option<PathfindingScoresSyncConfig>,
recovery_mode: bool,
wallet_birthday_height: Option<u32>,
}

impl NodeBuilder {
Expand All @@ -265,6 +266,7 @@ impl NodeBuilder {
let runtime_handle = None;
let pathfinding_scores_sync_config = None;
let recovery_mode = false;
let wallet_birthday_height = None;
Self {
config,
chain_data_source_config,
Expand All @@ -275,6 +277,7 @@ impl NodeBuilder {
async_payments_role: None,
pathfinding_scores_sync_config,
recovery_mode,
wallet_birthday_height,
}
}

Expand Down Expand Up @@ -559,6 +562,22 @@ impl NodeBuilder {
self
}

/// Sets the wallet birthday height for seed recovery on pruned nodes.
///
/// When set, the on-chain wallet will start scanning from the given block height
/// instead of the current chain tip. This allows recovery of historical funds
/// without scanning from genesis, which is critical for pruned nodes where
/// early blocks are unavailable.
///
/// The birthday height should be set to a block height at or before the wallet's
/// first transaction. If unknown, use a conservative estimate.
///
/// This only takes effect when creating a new wallet (not when loading existing state).
pub fn set_wallet_birthday_height(&mut self, height: u32) -> &mut Self {
self.wallet_birthday_height = Some(height);
self
}

/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
/// previously configured.
pub fn build(&self, node_entropy: NodeEntropy) -> Result<Node, BuildError> {
Expand Down Expand Up @@ -732,6 +751,7 @@ impl NodeBuilder {
self.pathfinding_scores_sync_config.as_ref(),
self.async_payments_role,
self.recovery_mode,
self.wallet_birthday_height,
seed_bytes,
runtime,
logger,
Expand Down Expand Up @@ -981,6 +1001,13 @@ impl ArcedNodeBuilder {
self.inner.write().unwrap().set_wallet_recovery_mode();
}

/// Sets the wallet birthday height for seed recovery on pruned nodes.
///
/// See [`NodeBuilder::set_wallet_birthday_height`] for details.
pub fn set_wallet_birthday_height(&self, height: u32) {
self.inner.write().unwrap().set_wallet_birthday_height(height);
}

/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
/// previously configured.
pub fn build(&self, node_entropy: Arc<NodeEntropy>) -> Result<Arc<Node>, BuildError> {
Expand Down Expand Up @@ -1124,7 +1151,8 @@ fn build_with_store_internal(
gossip_source_config: Option<&GossipSourceConfig>,
liquidity_source_config: Option<&LiquiditySourceConfig>,
pathfinding_scores_sync_config: Option<&PathfindingScoresSyncConfig>,
async_payments_role: Option<AsyncPaymentsRole>, recovery_mode: bool, seed_bytes: [u8; 64],
async_payments_role: Option<AsyncPaymentsRole>, recovery_mode: bool,
wallet_birthday_height: Option<u32>, seed_bytes: [u8; 64],
runtime: Arc<Runtime>, logger: Arc<Logger>, kv_store: Arc<DynStore>,
) -> Result<Node, BuildError> {
optionally_install_rustls_cryptoprovider();
Expand Down Expand Up @@ -1321,10 +1349,65 @@ fn build_with_store_internal(
BuildError::WalletSetupFailed
})?;

if !recovery_mode {
if let Some(birthday_height) = wallet_birthday_height {
// Wallet birthday: checkpoint at the birthday block so the wallet
// syncs from there, allowing fund recovery on pruned nodes.
let birthday_hash_res = runtime.block_on(async {
chain_source.get_block_hash_by_height(birthday_height).await
});
match birthday_hash_res {
Ok(birthday_hash) => {
log_info!(
logger,
"Setting wallet checkpoint at birthday height {} ({})",
birthday_height,
birthday_hash
);
let mut latest_checkpoint = wallet.latest_checkpoint();
let block_id = bdk_chain::BlockId {
height: birthday_height,
hash: birthday_hash,
};
latest_checkpoint = latest_checkpoint.insert(block_id);
let update = bdk_wallet::Update {
chain: Some(latest_checkpoint),
..Default::default()
};
wallet.apply_update(update).map_err(|e| {
log_error!(logger, "Failed to apply birthday checkpoint: {}", e);
BuildError::WalletSetupFailed
})?;
},
Err(e) => {
log_error!(
logger,
"Failed to fetch block hash at birthday height {}: {:?}. \
Falling back to current tip.",
birthday_height,
e
);
// Fall back to current tip
if let Some(best_block) = chain_tip_opt {
let mut latest_checkpoint = wallet.latest_checkpoint();
let block_id = bdk_chain::BlockId {
height: best_block.height,
hash: best_block.block_hash,
};
latest_checkpoint = latest_checkpoint.insert(block_id);
let update = bdk_wallet::Update {
chain: Some(latest_checkpoint),
..Default::default()
};
wallet.apply_update(update).map_err(|e| {
log_error!(logger, "Failed to apply fallback checkpoint: {}", e);
BuildError::WalletSetupFailed
})?;
}
},
}
} else if !recovery_mode {
if let Some(best_block) = chain_tip_opt {
// Insert the first checkpoint if we have it, to avoid resyncing from genesis.
// TODO: Use a proper wallet birthday once BDK supports it.
// No birthday: insert current tip to avoid resyncing from genesis.
let mut latest_checkpoint = wallet.latest_checkpoint();
let block_id = bdk_chain::BlockId {
height: best_block.height,
Expand All @@ -1339,6 +1422,7 @@ fn build_with_store_internal(
})?;
}
}
// else: recovery_mode without birthday syncs from genesis
wallet
},
};
Expand Down
30 changes: 30 additions & 0 deletions src/chain/electrum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,28 @@ impl ElectrumChainSource {
self.electrum_runtime_status.write().unwrap().stop();
}

pub(super) async fn get_block_hash_by_height(
&self, height: u32,
) -> Result<bitcoin::BlockHash, ()> {
// Try the runtime client if started, otherwise create a temporary connection.
let status = self.electrum_runtime_status.read().unwrap();
if let Some(client) = status.client() {
drop(status);
return client.get_block_hash_by_height(height);
}
drop(status);

// Runtime not started yet (called during build). Use a temporary client.
let config = ElectrumConfigBuilder::new()
.timeout(Some(self.sync_config.timeouts_config.per_request_timeout_secs))
.build();
let client = ElectrumClient::from_config(&self.server_url, config).map_err(|_| ())?;
let header_bytes = client.block_header_raw(height as usize).map_err(|_| ())?;
let header: bitcoin::block::Header =
bitcoin::consensus::deserialize(&header_bytes).map_err(|_| ())?;
Ok(header.block_hash())
}

pub(crate) async fn sync_onchain_wallet(
&self, onchain_wallet: Arc<Wallet>,
) -> Result<(), Error> {
Expand Down Expand Up @@ -420,6 +442,14 @@ impl ElectrumRuntimeClient {
})
}

fn get_block_hash_by_height(&self, height: u32) -> Result<bitcoin::BlockHash, ()> {
let header_bytes =
self.electrum_client.block_header_raw(height as usize).map_err(|_| ())?;
let header: bitcoin::block::Header =
bitcoin::consensus::deserialize(&header_bytes).map_err(|_| ())?;
Ok(header.block_hash())
}

async fn sync_confirmables(
&self, confirmables: Vec<Arc<dyn Confirm + Sync + Send>>,
) -> Result<(), Error> {
Expand Down
6 changes: 6 additions & 0 deletions src/chain/esplora.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ impl EsploraChainSource {
}
}

pub(super) async fn get_block_hash_by_height(
&self, height: u32,
) -> Result<bitcoin::BlockHash, ()> {
self.esplora_client.get_block_hash(height).await.map_err(|_| ())
}

pub(super) async fn sync_onchain_wallet(
&self, onchain_wallet: Arc<Wallet>,
) -> Result<(), Error> {
Expand Down
19 changes: 19 additions & 0 deletions src/chain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use bitcoin::{Script, Txid};
use lightning::chain::{BestBlock, Filter};

use crate::chain::bitcoind::{BitcoindChainSource, UtxoSourceClient};
use lightning_block_sync::gossip::UtxoSource;
use crate::chain::electrum::ElectrumChainSource;
use crate::chain::esplora::EsploraChainSource;
use crate::config::{
Expand Down Expand Up @@ -214,6 +215,24 @@ impl ChainSource {
}
}

/// Fetches the block hash at the given height from the chain source.
pub(crate) async fn get_block_hash_by_height(
&self, height: u32,
) -> Result<bitcoin::BlockHash, ()> {
match &self.kind {
ChainSourceKind::Bitcoind(bitcoind_chain_source) => {
let utxo_source = bitcoind_chain_source.as_utxo_source();
utxo_source.get_block_hash_by_height(height).await.map_err(|_| ())
},
ChainSourceKind::Esplora(esplora_chain_source) => {
esplora_chain_source.get_block_hash_by_height(height).await
},
ChainSourceKind::Electrum(electrum_chain_source) => {
electrum_chain_source.get_block_hash_by_height(height).await
},
}
}

pub(crate) fn registered_txids(&self) -> Vec<Txid> {
self.registered_txids.lock().unwrap().clone()
}
Expand Down
Loading