Skip to content

Commit d0ed6a3

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 c7cb7ef commit d0ed6a3

3 files changed

Lines changed: 67 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
- Users of the VSS storage backend must upgrade their VSS server to at least version
77
`v0.1.0-alpha.0` before upgrading LDK Node.
88

9+
## Bug Fixes and Improvements
10+
- Building a fresh node against a Bitcoin Core RPC or REST chain source that fails to return the
11+
current chain tip now aborts with a new `BuildError::ChainTipFetchFailed` variant instead of
12+
silently pinning the wallet birthday to genesis, which would have forced a full-history rescan
13+
once the chain source became reachable again. (#884)
14+
915
# 0.7.0 - Dec. 3, 2025
1016
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.
1117

src/builder.rs

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

201208
impl fmt::Display for BuildError {
@@ -233,6 +240,12 @@ impl fmt::Display for BuildError {
233240
Self::DNSResolverSetupFailed => {
234241
write!(f, "An attempt to setup a DNS resolver has failed.")
235242
},
243+
Self::ChainTipFetchFailed => {
244+
write!(
245+
f,
246+
"Failed to determine the current chain tip on first startup. Verify the chain data source is reachable and correctly configured."
247+
)
248+
},
236249
}
237250
}
238251
}
@@ -1557,6 +1570,23 @@ fn build_with_store_internal(
15571570
let bdk_wallet = match wallet_opt {
15581571
Some(wallet) => wallet,
15591572
None => {
1573+
// Guard against silently setting the wallet birthday to genesis on a fresh node:
1574+
// if we are creating a new wallet but failed to learn the current chain tip from
1575+
// a Bitcoin Core RPC/REST backend, we'd otherwise persist fresh wallet state
1576+
// pinned at height 0 and force a full-history rescan once the backend comes back.
1577+
// Abort cleanly instead so the misconfiguration surfaces on the first startup.
1578+
// Esplora/Electrum backends currently never return a tip at build time, so they
1579+
// retain their existing behavior.
1580+
let is_bitcoind_source =
1581+
matches!(chain_data_source_config, Some(ChainDataSourceConfig::Bitcoind { .. }));
1582+
if !recovery_mode && chain_tip_opt.is_none() && is_bitcoind_source {
1583+
log_error!(
1584+
logger,
1585+
"Failed to determine chain tip on first startup. Aborting to avoid pinning the wallet birthday to genesis."
1586+
);
1587+
return Err(BuildError::ChainTipFetchFailed);
1588+
}
1589+
15601590
let mut wallet = runtime
15611591
.block_on(async {
15621592
BdkWallet::create(descriptor, change_descriptor)

tests/integration_tests_rust.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use ldk_node::payment::{
3636
ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
3737
TransactionType, UnifiedPaymentResult,
3838
};
39-
use ldk_node::{Builder, Event, Node, NodeError};
39+
use ldk_node::{BuildError, Builder, Event, Node, NodeError};
4040
use lightning::ln::channelmanager::PaymentId;
4141
use lightning::routing::gossip::{NodeAlias, NodeId};
4242
use lightning::routing::router::RouteParametersConfig;
@@ -936,6 +936,36 @@ async fn onchain_wallet_recovery() {
936936
);
937937
}
938938

939+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
940+
async fn build_aborts_on_first_startup_bitcoind_tip_fetch_failure() {
941+
// A fresh node pointed at an unreachable bitcoind RPC endpoint must not silently
942+
// fall back to genesis as the wallet birthday. The build must abort cleanly so the
943+
// misconfiguration surfaces immediately.
944+
let config = random_config(false);
945+
let entropy = config.node_entropy;
946+
947+
setup_builder!(builder, config.node_config);
948+
// Pick a localhost port that is extremely unlikely to be bound. The kernel will
949+
// refuse the connection immediately so the test does not have to wait for the
950+
// chain-polling timeout.
951+
let unreachable_port: u16 = 1;
952+
builder.set_chain_source_bitcoind_rpc(
953+
"127.0.0.1".to_string(),
954+
unreachable_port,
955+
"user".to_string(),
956+
"password".to_string(),
957+
);
958+
959+
let res = builder.build(entropy.into());
960+
match res {
961+
Err(BuildError::ChainTipFetchFailed) => {},
962+
other => panic!(
963+
"expected BuildError::ChainTipFetchFailed on fresh node with unreachable bitcoind, got {:?}",
964+
other.map(|_| "Ok(_)")
965+
),
966+
}
967+
}
968+
939969
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
940970
async fn test_rbf_via_mempool() {
941971
run_rbf_test(false).await;

0 commit comments

Comments
 (0)