Skip to content

Commit de80aaa

Browse files
committed
Abort on first-startup chain tip fetch failure
When a fresh node's bitcoind RPC/REST chain source fails to return the current chain tip, we previously silently fell back to the genesis block as the wallet birthday. The next successful startup would then force a full-history rescan of the whole chain. Instead, return a new BuildError::ChainTipFetchFailed on the first build so the misconfiguration surfaces immediately and no stale fresh state is persisted. Restarts with a previously-persisted wallet are unaffected: a transient chain source outage on an existing node still allows startup to proceed. Esplora/Electrum backends currently never expose a tip at build time so the guard only fires for bitcoind sources; the latent wallet-birthday-at-genesis issue on those backends is left for a follow-up. Co-Authored-By: HAL 9000
1 parent 07654fa commit de80aaa

3 files changed

Lines changed: 66 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# Unreleased
2+
3+
## Bug Fixes and Improvements
4+
- Building a fresh node against a Bitcoin Core RPC or REST chain source that fails to return the current chain tip now aborts with a new `BuildError::ChainTipFetchFailed` variant instead of silently pinning the wallet birthday to genesis, which would have forced a full-history rescan once the chain source became reachable again.
5+
16
# 0.7.0 - Dec. 3, 2025
27
This seventh minor release introduces numerous new features, bug fixes, and API improvements. In particular, it adds support for channel Splicing, Async Payments, as well as sourcing chain data from a Bitcoin Core REST backend.
38

src/builder.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,13 @@ pub enum BuildError {
201201
AsyncPaymentsConfigMismatch,
202202
/// An attempt to setup a DNS Resolver failed.
203203
DNSResolverSetupFailed,
204+
/// We failed to determine the current chain tip on first startup.
205+
///
206+
/// Returned when a fresh node is built against a Bitcoin Core RPC or REST chain source that
207+
/// is unreachable or misconfigured, so we cannot learn the tip height/hash to use as the
208+
/// wallet birthday. Falling back to genesis would silently force a full-history rescan on
209+
/// the next successful startup, so we abort instead.
210+
ChainTipFetchFailed,
204211
}
205212

206213
impl fmt::Display for BuildError {
@@ -238,6 +245,12 @@ impl fmt::Display for BuildError {
238245
Self::DNSResolverSetupFailed => {
239246
write!(f, "An attempt to setup a DNS resolver has failed.")
240247
},
248+
Self::ChainTipFetchFailed => {
249+
write!(
250+
f,
251+
"Failed to determine the current chain tip on first startup. Verify the chain data source is reachable and correctly configured."
252+
)
253+
},
241254
}
242255
}
243256
}
@@ -1440,6 +1453,23 @@ fn build_with_store_internal(
14401453
let bdk_wallet = match wallet_opt {
14411454
Some(wallet) => wallet,
14421455
None => {
1456+
// Guard against silently setting the wallet birthday to genesis on a fresh node:
1457+
// if we are creating a new wallet but failed to learn the current chain tip from
1458+
// a Bitcoin Core RPC/REST backend, we'd otherwise persist fresh wallet state
1459+
// pinned at height 0 and force a full-history rescan once the backend comes back.
1460+
// Abort cleanly instead so the misconfiguration surfaces on the first startup.
1461+
// Esplora/Electrum backends currently never return a tip at build time, so they
1462+
// retain their existing behavior.
1463+
let is_bitcoind_source =
1464+
matches!(chain_data_source_config, Some(ChainDataSourceConfig::Bitcoind { .. }));
1465+
if !recovery_mode && chain_tip_opt.is_none() && is_bitcoind_source {
1466+
log_error!(
1467+
logger,
1468+
"Failed to determine chain tip on first startup. Aborting to avoid pinning the wallet birthday to genesis."
1469+
);
1470+
return Err(BuildError::ChainTipFetchFailed);
1471+
}
1472+
14431473
let mut wallet = BdkWallet::create(descriptor, change_descriptor)
14441474
.network(config.network)
14451475
.create_wallet(&mut wallet_persister)

tests/integration_tests_rust.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use ldk_node::payment::{
3535
ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
3636
UnifiedPaymentResult,
3737
};
38-
use ldk_node::{Builder, Event, NodeError};
38+
use ldk_node::{BuildError, Builder, Event, NodeError};
3939
use lightning::ln::channelmanager::PaymentId;
4040
use lightning::routing::gossip::{NodeAlias, NodeId};
4141
use lightning::routing::router::RouteParametersConfig;
@@ -743,6 +743,36 @@ async fn onchain_wallet_recovery() {
743743
);
744744
}
745745

746+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
747+
async fn build_aborts_on_first_startup_bitcoind_tip_fetch_failure() {
748+
// A fresh node pointed at an unreachable bitcoind RPC endpoint must not silently
749+
// fall back to genesis as the wallet birthday. The build must abort cleanly so the
750+
// misconfiguration surfaces immediately.
751+
let config = random_config(false);
752+
let entropy = config.node_entropy;
753+
754+
setup_builder!(builder, config.node_config);
755+
// Pick a localhost port that is extremely unlikely to be bound. The kernel will
756+
// refuse the connection immediately so the test does not have to wait for the
757+
// chain-polling timeout.
758+
let unreachable_port: u16 = 1;
759+
builder.set_chain_source_bitcoind_rpc(
760+
"127.0.0.1".to_string(),
761+
unreachable_port,
762+
"user".to_string(),
763+
"password".to_string(),
764+
);
765+
766+
let res = builder.build(entropy.into());
767+
match res {
768+
Err(BuildError::ChainTipFetchFailed) => {},
769+
other => panic!(
770+
"expected BuildError::ChainTipFetchFailed on fresh node with unreachable bitcoind, got {:?}",
771+
other.map(|_| "Ok(_)")
772+
),
773+
}
774+
}
775+
746776
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
747777
async fn test_rbf_via_mempool() {
748778
run_rbf_test(false).await;

0 commit comments

Comments
 (0)