From b476e05ad9944d51454ac8f0ae88f7e32f1d5ca4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 23 Jan 2026 14:08:39 +0100 Subject: [PATCH 1/5] Move `async_payment_role` to `TestConfig` It's weird to have a special intermediary `setup_node` method if we have `TestConfig` for exactly that reason by now. So we move `async_payment_role` over. --- tests/common/mod.rs | 13 ++++--------- tests/integration_tests_rust.rs | 24 +++++++----------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 5f6657260..f0065e89a 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -292,6 +292,7 @@ pub(crate) struct TestConfig { pub log_writer: TestLogWriter, pub store_type: TestStoreType, pub node_entropy: NodeEntropy, + pub async_payments_role: Option, } impl Default for TestConfig { @@ -302,7 +303,8 @@ impl Default for TestConfig { let mnemonic = generate_entropy_mnemonic(None); let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); - TestConfig { node_config, log_writer, store_type, node_entropy } + let async_payments_role = None; + TestConfig { node_config, log_writer, store_type, node_entropy, async_payments_role } } } @@ -359,13 +361,6 @@ pub(crate) fn setup_two_nodes_with_store( } pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> TestNode { - setup_node_for_async_payments(chain_source, config, None) -} - -pub(crate) fn setup_node_for_async_payments( - chain_source: &TestChainSource, config: TestConfig, - async_payments_role: Option, -) -> TestNode { setup_builder!(builder, config.node_config); match chain_source { TestChainSource::Esplora(electrsd) => { @@ -419,7 +414,7 @@ pub(crate) fn setup_node_for_async_payments( }, } - builder.set_async_payments_role(async_payments_role).unwrap(); + builder.set_async_payments_role(config.async_payments_role).unwrap(); let node = match config.store_type { TestStoreType::TestSyncStore => { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 605dd0613..a598b6879 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -23,8 +23,7 @@ use common::{ expect_splice_pending_event, generate_blocks_and_wait, open_channel, open_channel_push_amt, premine_and_distribute_funds, premine_blocks, prepare_rbf, random_config, random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node, - setup_node_for_async_payments, setup_two_nodes, wait_for_tx, TestChainSource, TestStoreType, - TestSyncStore, + setup_two_nodes, wait_for_tx, TestChainSource, TestStoreType, TestSyncStore, }; use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig}; use ldk_node::entropy::NodeEntropy; @@ -1317,30 +1316,21 @@ async fn async_payment() { config_sender.node_config.node_alias = None; config_sender.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender ".to_string()))); - let node_sender = setup_node_for_async_payments( - &chain_source, - config_sender, - Some(AsyncPaymentsRole::Client), - ); + config_sender.async_payments_role = Some(AsyncPaymentsRole::Client); + let node_sender = setup_node(&chain_source, config_sender); let mut config_sender_lsp = random_config(true); config_sender_lsp.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender_lsp ".to_string()))); - let node_sender_lsp = setup_node_for_async_payments( - &chain_source, - config_sender_lsp, - Some(AsyncPaymentsRole::Server), - ); + config_sender_lsp.async_payments_role = Some(AsyncPaymentsRole::Server); + let node_sender_lsp = setup_node(&chain_source, config_sender_lsp); let mut config_receiver_lsp = random_config(true); config_receiver_lsp.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver_lsp".to_string()))); + config_receiver_lsp.async_payments_role = Some(AsyncPaymentsRole::Server); - let node_receiver_lsp = setup_node_for_async_payments( - &chain_source, - config_receiver_lsp, - Some(AsyncPaymentsRole::Server), - ); + let node_receiver_lsp = setup_node(&chain_source, config_receiver_lsp); let mut config_receiver = random_config(true); config_receiver.node_config.listening_addresses = None; From 325c936f21c08601a09c45900ab432606980d771 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 23 Jan 2026 14:36:08 +0100 Subject: [PATCH 2/5] Randomize chain sources in tests .. all of our tests should be robust against switching chain sources. We here opt to pick a random one each time to considerably extend our test coverage, instead of just running some cases against non-Esplora chain sources. Signed-off-by: Elias Rohrer --- benches/payments.rs | 4 +- tests/common/mod.rs | 25 ++++++++ tests/integration_tests_rust.rs | 101 +++++++++++--------------------- tests/reorg_test.rs | 16 ++--- 4 files changed, 68 insertions(+), 78 deletions(-) diff --git a/benches/payments.rs b/benches/payments.rs index ba69e046d..52769d794 100644 --- a/benches/payments.rs +++ b/benches/payments.rs @@ -8,7 +8,7 @@ use bitcoin::hex::DisplayHex; use bitcoin::Amount; use common::{ expect_channel_ready_event, generate_blocks_and_wait, premine_and_distribute_funds, - setup_bitcoind_and_electrsd, setup_two_nodes_with_store, TestChainSource, + random_chain_source, setup_bitcoind_and_electrsd, setup_two_nodes_with_store, }; use criterion::{criterion_group, criterion_main, Criterion}; use ldk_node::{Event, Node}; @@ -119,7 +119,7 @@ async fn send_payments(node_a: Arc, node_b: Arc) -> std::time::Durat fn payment_benchmark(c: &mut Criterion) { // Set up two nodes. Because this is slow, we reuse the same nodes for each sample. let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes_with_store( &chain_source, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f0065e89a..c743ec120 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -206,6 +206,31 @@ pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { (bitcoind, electrsd) } +pub(crate) fn random_chain_source<'a>( + bitcoind: &'a BitcoinD, electrsd: &'a ElectrsD, +) -> TestChainSource<'a> { + let r = rand::random_range(0..3); + match r { + 0 => { + println!("Randomly setting up Esplora chain syncing..."); + TestChainSource::Esplora(electrsd) + }, + 1 => { + println!("Randomly setting up Electrum chain syncing..."); + TestChainSource::Electrum(electrsd) + }, + 2 => { + println!("Randomly setting up Bitcoind RPC chain syncing..."); + TestChainSource::BitcoindRpcSync(bitcoind) + }, + 3 => { + println!("Randomly setting up Bitcoind REST chain syncing..."); + TestChainSource::BitcoindRestSync(bitcoind) + }, + _ => unreachable!(), + } +} + pub(crate) fn random_storage_path() -> PathBuf { let mut temp_path = std::env::temp_dir(); let mut rng = rng(); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index a598b6879..2152ce5fd 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -21,7 +21,7 @@ use common::{ expect_channel_pending_event, expect_channel_ready_event, expect_event, expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event, expect_splice_pending_event, generate_blocks_and_wait, open_channel, open_channel_push_amt, - premine_and_distribute_funds, premine_blocks, prepare_rbf, random_config, + premine_and_distribute_funds, premine_blocks, prepare_rbf, random_chain_source, random_config, random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestStoreType, TestSyncStore, }; @@ -43,34 +43,7 @@ use log::LevelFilter; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) - .await; -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn channel_full_cycle_electrum() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Electrum(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) - .await; -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn channel_full_cycle_bitcoind_rpc_sync() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) - .await; -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn channel_full_cycle_bitcoind_rest_sync() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::BitcoindRestSync(&bitcoind); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) .await; @@ -79,7 +52,7 @@ async fn channel_full_cycle_bitcoind_rest_sync() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_force_close() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true) .await; @@ -88,7 +61,7 @@ async fn channel_full_cycle_force_close() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_force_close_trusted_no_reserve() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, true); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true) .await; @@ -97,7 +70,7 @@ async fn channel_full_cycle_force_close_trusted_no_reserve() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_0conf() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true, false) .await; @@ -106,7 +79,7 @@ async fn channel_full_cycle_0conf() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_legacy_staticremotekey() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, false, false) .await; @@ -115,7 +88,7 @@ async fn channel_full_cycle_legacy_staticremotekey() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_open_fails_when_funds_insufficient() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); @@ -322,7 +295,7 @@ async fn start_stop_reinit() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn onchain_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); @@ -523,7 +496,7 @@ async fn onchain_send_receive() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn onchain_send_all_retains_reserve() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); // Setup nodes @@ -608,7 +581,7 @@ async fn onchain_send_all_retains_reserve() { async fn onchain_wallet_recovery() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let original_config = random_config(true); let original_node_entropy = original_config.node_entropy; @@ -823,9 +796,9 @@ async fn run_rbf_test(is_insert_block: bool) { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn sign_verify_msg() { - let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let config = random_config(true); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let node = setup_node(&chain_source, config); // Tests arbitrary message signing and later verification @@ -837,8 +810,8 @@ async fn sign_verify_msg() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn connection_multi_listen() { - let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); let node_id_b = node_b.node_id(); @@ -857,8 +830,8 @@ async fn connection_restart_behavior() { } async fn do_connection_restart_behavior(persist: bool) { - let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); let node_id_a = node_a.node_id(); @@ -904,8 +877,8 @@ async fn do_connection_restart_behavior(persist: bool) { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn concurrent_connections_succeed() { - let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); let node_a = Arc::new(node_a); @@ -929,13 +902,11 @@ async fn concurrent_connections_succeed() { } } -async fn run_splice_channel_test(bitcoind_chain_source: bool) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn splice_channel() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = if bitcoind_chain_source { - TestChainSource::BitcoindRpcSync(&bitcoind) - } else { - TestChainSource::Esplora(&electrsd) - }; + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); let address_a = node_a.onchain_payment().new_address().unwrap(); @@ -1071,16 +1042,10 @@ async fn run_splice_channel_test(bitcoind_chain_source: bool) { ); } -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn splice_channel() { - run_splice_channel_test(false).await; - run_splice_channel_test(true).await; -} - #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn simple_bolt12_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); let address_a = node_a.onchain_payment().new_address().unwrap(); @@ -1309,7 +1274,7 @@ async fn simple_bolt12_send_receive() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn async_payment() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let mut config_sender = random_config(true); config_sender.node_config.listening_addresses = None; @@ -1435,7 +1400,7 @@ async fn async_payment() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_node_announcement_propagation() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); // Node A will use both listening and announcement addresses let mut config_a = random_config(true); @@ -1527,7 +1492,7 @@ async fn test_node_announcement_propagation() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn generate_bip21_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); @@ -1582,7 +1547,7 @@ async fn generate_bip21_uri() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn unified_send_receive_bip21_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); @@ -1921,8 +1886,8 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn facade_logging() { - let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); let logger = init_log_logger(LevelFilter::Trace); let mut config = random_config(false); @@ -1940,7 +1905,7 @@ async fn facade_logging() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn spontaneous_send_with_custom_preimage() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); let address_a = node_a.onchain_payment().new_address().unwrap(); @@ -2006,8 +1971,8 @@ async fn spontaneous_send_with_custom_preimage() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn drop_in_async_context() { - let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); let config = random_config(true); let node = setup_node(&chain_source, config); node.stop().unwrap(); @@ -2318,7 +2283,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn payment_persistence_after_restart() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::Esplora(&electrsd); + let chain_source = random_chain_source(&bitcoind, &electrsd); // Setup nodes manually so we can restart node_a with the same config println!("== Node A =="); diff --git a/tests/reorg_test.rs b/tests/reorg_test.rs index 89660a407..295d9fdd2 100644 --- a/tests/reorg_test.rs +++ b/tests/reorg_test.rs @@ -9,8 +9,8 @@ use proptest::proptest; use crate::common::{ expect_event, generate_blocks_and_wait, invalidate_blocks, open_channel, - premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, setup_node, - wait_for_outpoint_spend, TestChainSource, + premine_and_distribute_funds, random_chain_source, random_config, setup_bitcoind_and_electrsd, + setup_node, wait_for_outpoint_spend, }; proptest! { @@ -24,9 +24,9 @@ proptest! { rt.block_on(async { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind); - let chain_source_electrsd = TestChainSource::Electrum(&electrsd); - let chain_source_esplora = TestChainSource::Esplora(&electrsd); + let chain_source_a = random_chain_source(&bitcoind, &electrsd); + let chain_source_b = random_chain_source(&bitcoind, &electrsd); + let chain_source_c = random_chain_source(&bitcoind, &electrsd); macro_rules! config_node { ($chain_source: expr, $anchor_channels: expr) => {{ @@ -37,9 +37,9 @@ proptest! { } let anchor_channels = true; let nodes = vec![ - config_node!(chain_source_electrsd, anchor_channels), - config_node!(chain_source_bitcoind, anchor_channels), - config_node!(chain_source_esplora, anchor_channels), + config_node!(chain_source_a, anchor_channels), + config_node!(chain_source_b, anchor_channels), + config_node!(chain_source_c, anchor_channels), ]; let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client); From 4a5fb96f1c7ec74fbe3614cf4a26ff29a5bffbc3 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 23 Jan 2026 16:19:49 +0100 Subject: [PATCH 3/5] Fix: Also update the payment store for mempool transactions When we intially implemented `bitcoind` syncing polling the mempool was very frequent and rather inefficient so we made a choice not to unnecessarily update the payment store for mempool changes, especially since we only consider transactions `Succeeded` after `ANTI_REORG_DELAY` anyways. However, since then we made quite a few peformance improvements to the mempool syncing, and by now we should just update they payment store as not doing so will lead to rather unexpected behavior, making some tests fail for `TestChainSource::Bitcoind`, e.g., `channel_full_cycle_0conf`, which we fix here. As we recently switched to updating the payment store based on BDK's `WalletEvent`, but they currently don't offer an API returning such events when applying mempool transactions, we copy over the respective method for generating events from `bdk_wallet`, with the intention of dropping it again once they do. Signed-off-by: Elias Rohrer --- src/wallet/mod.rs | 133 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index f808e9a3f..dd3eacb95 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -152,16 +152,47 @@ impl Wallet { pub(crate) fn apply_mempool_txs( &self, unconfirmed_txs: Vec<(Transaction, u64)>, evicted_txids: Vec<(Txid, u64)>, ) -> Result<(), Error> { + if unconfirmed_txs.is_empty() { + return Ok(()); + } + let mut locked_wallet = self.inner.lock().unwrap(); + + let chain_tip1 = locked_wallet.latest_checkpoint().block_id(); + let wallet_txs1 = locked_wallet + .transactions() + .map(|wtx| (wtx.tx_node.txid, (wtx.tx_node.tx.clone(), wtx.chain_position))) + .collect::, bdk_chain::ChainPosition), + >>(); + locked_wallet.apply_unconfirmed_txs(unconfirmed_txs); locked_wallet.apply_evicted_txs(evicted_txids); + let chain_tip2 = locked_wallet.latest_checkpoint().block_id(); + let wallet_txs2 = locked_wallet + .transactions() + .map(|wtx| (wtx.tx_node.txid, (wtx.tx_node.tx.clone(), wtx.chain_position))) + .collect::, bdk_chain::ChainPosition), + >>(); + + let events = + wallet_events(&mut *locked_wallet, chain_tip1, chain_tip2, wallet_txs1, wallet_txs2); + let mut locked_persister = self.persister.lock().unwrap(); locked_wallet.persist(&mut locked_persister).map_err(|e| { log_error!(self.logger, "Failed to persist wallet: {}", e); Error::PersistenceFailed })?; + self.update_payment_store(&mut *locked_wallet, events).map_err(|e| { + log_error!(self.logger, "Failed to update payment store: {}", e); + Error::PersistenceFailed + })?; + Ok(()) } @@ -1215,3 +1246,105 @@ impl ChangeDestinationSource for WalletKeysManager { } } } + +// FIXME/TODO: This is copied-over from bdk_wallet and only used to generate `WalletEvent`s after +// applying mempool transactions. We should drop this when BDK offers to generate events for +// mempool transactions natively. +pub(crate) fn wallet_events( + wallet: &mut bdk_wallet::Wallet, chain_tip1: bdk_chain::BlockId, + chain_tip2: bdk_chain::BlockId, + wallet_txs1: std::collections::BTreeMap< + Txid, + (Arc, bdk_chain::ChainPosition), + >, + wallet_txs2: std::collections::BTreeMap< + Txid, + (Arc, bdk_chain::ChainPosition), + >, +) -> Vec { + let mut events: Vec = Vec::new(); + + if chain_tip1 != chain_tip2 { + events.push(WalletEvent::ChainTipChanged { old_tip: chain_tip1, new_tip: chain_tip2 }); + } + + wallet_txs2.iter().for_each(|(txid2, (tx2, cp2))| { + if let Some((tx1, cp1)) = wallet_txs1.get(txid2) { + assert_eq!(tx1.compute_txid(), *txid2); + match (cp1, cp2) { + ( + bdk_chain::ChainPosition::Unconfirmed { .. }, + bdk_chain::ChainPosition::Confirmed { anchor, .. }, + ) => { + events.push(WalletEvent::TxConfirmed { + txid: *txid2, + tx: tx2.clone(), + block_time: *anchor, + old_block_time: None, + }); + }, + ( + bdk_chain::ChainPosition::Confirmed { anchor, .. }, + bdk_chain::ChainPosition::Unconfirmed { .. }, + ) => { + events.push(WalletEvent::TxUnconfirmed { + txid: *txid2, + tx: tx2.clone(), + old_block_time: Some(*anchor), + }); + }, + ( + bdk_chain::ChainPosition::Confirmed { anchor: anchor1, .. }, + bdk_chain::ChainPosition::Confirmed { anchor: anchor2, .. }, + ) => { + if *anchor1 != *anchor2 { + events.push(WalletEvent::TxConfirmed { + txid: *txid2, + tx: tx2.clone(), + block_time: *anchor2, + old_block_time: Some(*anchor1), + }); + } + }, + ( + bdk_chain::ChainPosition::Unconfirmed { .. }, + bdk_chain::ChainPosition::Unconfirmed { .. }, + ) => { + // do nothing if still unconfirmed + }, + } + } else { + match cp2 { + bdk_chain::ChainPosition::Confirmed { anchor, .. } => { + events.push(WalletEvent::TxConfirmed { + txid: *txid2, + tx: tx2.clone(), + block_time: *anchor, + old_block_time: None, + }); + }, + bdk_chain::ChainPosition::Unconfirmed { .. } => { + events.push(WalletEvent::TxUnconfirmed { + txid: *txid2, + tx: tx2.clone(), + old_block_time: None, + }); + }, + } + } + }); + + // find tx that are no longer canonical + wallet_txs1.iter().for_each(|(txid1, (tx1, _))| { + if !wallet_txs2.contains_key(txid1) { + let conflicts = wallet.tx_graph().direct_conflicts(tx1).collect::>(); + if !conflicts.is_empty() { + events.push(WalletEvent::TxReplaced { txid: *txid1, tx: tx1.clone(), conflicts }); + } else { + events.push(WalletEvent::TxDropped { txid: *txid1, tx: tx1.clone() }); + } + } + }); + + events +} From 579f65b7effe3fa95eb04fce649dc16d98714256 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 23 Jan 2026 17:04:34 +0100 Subject: [PATCH 4/5] Fix: Add (onchain) recovery mode Previously, we fixed than a fresh node syncing via `bitcoind` RPC would resync all chain data back to genesis. However, while introducing a wallet birthday is great, it disallowed discovery of historical funds if a wallet would be imported from seed. Here, we add a recovery mode flag to the builder that explictly allows to re-enable resyncing from genesis in such a scenario. Going forward, we intend to reuse that API for an upcoming Lightning recoery flow, too. --- bindings/ldk_node.udl | 1 + src/builder.rs | 57 ++++++++++++++++++++++++--------- tests/common/mod.rs | 19 ++++++++++- tests/integration_tests_rust.rs | 1 + 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 6bd031379..92622fda4 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -129,6 +129,7 @@ interface Builder { void set_node_alias(string node_alias); [Throws=BuildError] void set_async_payments_role(AsyncPaymentsRole? role); + void set_wallet_recovery_mode(); [Throws=BuildError] Node build(NodeEntropy node_entropy); [Throws=BuildError] diff --git a/src/builder.rs b/src/builder.rs index 5d8a5a7a9..ecfe4878c 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -244,6 +244,7 @@ pub struct NodeBuilder { async_payments_role: Option, runtime_handle: Option, pathfinding_scores_sync_config: Option, + recovery_mode: bool, } impl NodeBuilder { @@ -261,6 +262,7 @@ impl NodeBuilder { let log_writer_config = None; let runtime_handle = None; let pathfinding_scores_sync_config = None; + let recovery_mode = false; Self { config, chain_data_source_config, @@ -270,6 +272,7 @@ impl NodeBuilder { runtime_handle, async_payments_role: None, pathfinding_scores_sync_config, + recovery_mode, } } @@ -544,6 +547,16 @@ impl NodeBuilder { Ok(self) } + /// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any + /// historical wallet funds. + /// + /// This should only be set on first startup when importing an older wallet from a previously + /// used [`NodeEntropy`]. + pub fn set_wallet_recovery_mode(&mut self) -> &mut Self { + self.recovery_mode = true; + self + } + /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. pub fn build(&self, node_entropy: NodeEntropy) -> Result { @@ -679,6 +692,7 @@ impl NodeBuilder { self.liquidity_source_config.as_ref(), self.pathfinding_scores_sync_config.as_ref(), self.async_payments_role, + self.recovery_mode, seed_bytes, runtime, logger, @@ -919,6 +933,15 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_async_payments_role(role).map(|_| ()) } + /// Configures the [`Node`] to resync chain data from genesis on first startup, recovering any + /// historical wallet funds. + /// + /// This should only be set on first startup when importing an older wallet from a previously + /// used [`NodeEntropy`]. + pub fn set_wallet_recovery_mode(&self) { + self.inner.write().unwrap().set_wallet_recovery_mode(); + } + /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. pub fn build(&self, node_entropy: Arc) -> Result, BuildError> { @@ -1033,8 +1056,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, seed_bytes: [u8; 64], runtime: Arc, - logger: Arc, kv_store: Arc, + async_payments_role: Option, recovery_mode: bool, seed_bytes: [u8; 64], + runtime: Arc, logger: Arc, kv_store: Arc, ) -> Result { optionally_install_rustls_cryptoprovider(); @@ -1230,19 +1253,23 @@ fn build_with_store_internal( BuildError::WalletSetupFailed })?; - 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. - 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 checkpoint during wallet setup: {}", e); - BuildError::WalletSetupFailed - })?; + 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. + 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 checkpoint during wallet setup: {}", e); + BuildError::WalletSetupFailed + })?; + } } wallet }, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index c743ec120..41d22c690 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -318,6 +318,7 @@ pub(crate) struct TestConfig { pub store_type: TestStoreType, pub node_entropy: NodeEntropy, pub async_payments_role: Option, + pub recovery_mode: bool, } impl Default for TestConfig { @@ -329,7 +330,15 @@ impl Default for TestConfig { let mnemonic = generate_entropy_mnemonic(None); let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); let async_payments_role = None; - TestConfig { node_config, log_writer, store_type, node_entropy, async_payments_role } + let recovery_mode = false; + TestConfig { + node_config, + log_writer, + store_type, + node_entropy, + async_payments_role, + recovery_mode, + } } } @@ -441,6 +450,10 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> builder.set_async_payments_role(config.async_payments_role).unwrap(); + if config.recovery_mode { + builder.set_wallet_recovery_mode(); + } + let node = match config.store_type { TestStoreType::TestSyncStore => { let kv_store = TestSyncStore::new(config.node_config.storage_dir_path.into()); @@ -449,6 +462,10 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> TestStoreType::Sqlite => builder.build(config.node_entropy.into()).unwrap(), }; + if config.recovery_mode { + builder.set_wallet_recovery_mode(); + } + node.start().unwrap(); assert!(node.status().is_running); assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some()); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 2152ce5fd..9ea05aa1e 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -626,6 +626,7 @@ async fn onchain_wallet_recovery() { // Now we start from scratch, only the seed remains the same. let mut recovered_config = random_config(true); recovered_config.node_entropy = original_node_entropy; + recovered_config.recovery_mode = true; let recovered_node = setup_node(&chain_source, recovered_config); recovered_node.sync_wallets().unwrap(); From c4ca6d3d89702b71e59e1d458c6299483bf6fb7e Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 12 Feb 2026 10:55:06 +0100 Subject: [PATCH 5/5] Fix: Insert all LDK-registered transaction outputs into wallet Previously, we'd selectively insert the funding outputs into the onchain wallet to later allow calculating `fees_paid` when creating payment store entries (only for splicing mostly). However, this didn't always work, and we might for example end up with a missing funding output (and hence would fall back to `fees_paid: Some(0)`) if it was a counterparty-initiated channel and we synced via `bitcoind` RPC. Here, we fix this by tracking all LDK-registered `txids` in `ChainSource` and then in the `Wallet`'s `Listen` implementation insert all outputs of all registered transactions into the `Wallet`, ensuring we'd always have sufficient data for `calculate_fee` available. Thereby we also fix the `onchain_send_receive` test which previously failed when using `TestChainSource::Bitcoind`. Signed-off-by: Elias Rohrer --- src/builder.rs | 1 + src/chain/mod.rs | 20 +++++++++++++++----- src/lib.rs | 16 ---------------- src/wallet/mod.rs | 41 +++++++++++++++++++++++++---------------- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index ecfe4878c..7a285876f 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1294,6 +1294,7 @@ fn build_with_store_internal( wallet_persister, Arc::clone(&tx_broadcaster), Arc::clone(&fee_estimator), + Arc::clone(&chain_source), Arc::clone(&payment_store), Arc::clone(&config), Arc::clone(&logger), diff --git a/src/chain/mod.rs b/src/chain/mod.rs index afd502363..49c011a78 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -10,7 +10,7 @@ mod electrum; mod esplora; use std::collections::HashMap; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; use bitcoin::{Script, Txid}; @@ -84,6 +84,7 @@ impl WalletSyncStatus { pub(crate) struct ChainSource { kind: ChainSourceKind, + registered_txids: Mutex>, tx_broadcaster: Arc, logger: Arc, } @@ -112,7 +113,8 @@ impl ChainSource { node_metrics, ); let kind = ChainSourceKind::Esplora(esplora_chain_source); - (Self { kind, tx_broadcaster, logger }, None) + let registered_txids = Mutex::new(Vec::new()); + (Self { kind, registered_txids, tx_broadcaster, logger }, None) } pub(crate) fn new_electrum( @@ -131,7 +133,8 @@ impl ChainSource { node_metrics, ); let kind = ChainSourceKind::Electrum(electrum_chain_source); - (Self { kind, tx_broadcaster, logger }, None) + let registered_txids = Mutex::new(Vec::new()); + (Self { kind, registered_txids, tx_broadcaster, logger }, None) } pub(crate) async fn new_bitcoind_rpc( @@ -153,7 +156,8 @@ impl ChainSource { ); let best_block = bitcoind_chain_source.poll_best_block().await.ok(); let kind = ChainSourceKind::Bitcoind(bitcoind_chain_source); - (Self { kind, tx_broadcaster, logger }, best_block) + let registered_txids = Mutex::new(Vec::new()); + (Self { kind, registered_txids, tx_broadcaster, logger }, best_block) } pub(crate) async fn new_bitcoind_rest( @@ -176,7 +180,8 @@ impl ChainSource { ); let best_block = bitcoind_chain_source.poll_best_block().await.ok(); let kind = ChainSourceKind::Bitcoind(bitcoind_chain_source); - (Self { kind, tx_broadcaster, logger }, best_block) + let registered_txids = Mutex::new(Vec::new()); + (Self { kind, registered_txids, tx_broadcaster, logger }, best_block) } pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { @@ -209,6 +214,10 @@ impl ChainSource { } } + pub(crate) fn registered_txids(&self) -> Vec { + self.registered_txids.lock().unwrap().clone() + } + pub(crate) fn is_transaction_based(&self) -> bool { match &self.kind { ChainSourceKind::Esplora(_) => true, @@ -463,6 +472,7 @@ impl ChainSource { impl Filter for ChainSource { fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + self.registered_txids.lock().unwrap().push(*txid); match &self.kind { ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source.register_tx(txid, script_pubkey) diff --git a/src/lib.rs b/src/lib.rs index 64a9ac1a2..2b60307b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1329,10 +1329,6 @@ impl Node { Error::ChannelSplicingFailed })?; - // insert channel's funding utxo into the wallet so we can later calculate fees - // correctly when viewing this splice-in. - self.wallet.insert_txo(funding_txo.into_bitcoin_outpoint(), funding_output)?; - let change_address = self.wallet.get_new_internal_address()?; let contribution = SpliceContribution::splice_in( @@ -1426,18 +1422,6 @@ impl Node { }, }; - let funding_txo = channel_details.funding_txo.ok_or_else(|| { - log_error!(self.logger, "Failed to splice channel: channel not yet ready",); - Error::ChannelSplicingFailed - })?; - - let funding_output = channel_details.get_funding_output().ok_or_else(|| { - log_error!(self.logger, "Failed to splice channel: channel not yet ready"); - Error::ChannelSplicingFailed - })?; - - self.wallet.insert_txo(funding_txo.into_bitcoin_outpoint(), funding_output)?; - self.channel_manager .splice_channel( &channel_details.channel_id, diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index dd3eacb95..2decf981b 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -54,7 +54,7 @@ use crate::payment::{ PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PendingPaymentDetails, }; use crate::types::{Broadcaster, PaymentStore, PendingPaymentStore}; -use crate::Error; +use crate::{ChainSource, Error}; pub(crate) enum OnchainSendAmount { ExactRetainingReserve { amount_sats: u64, cur_anchor_reserve_sats: u64 }, @@ -71,6 +71,7 @@ pub(crate) struct Wallet { persister: Mutex, broadcaster: Arc, fee_estimator: Arc, + chain_source: Arc, payment_store: Arc, config: Arc, logger: Arc, @@ -81,8 +82,9 @@ impl Wallet { pub(crate) fn new( wallet: bdk_wallet::PersistedWallet, wallet_persister: KVStoreWalletPersister, broadcaster: Arc, - fee_estimator: Arc, payment_store: Arc, - config: Arc, logger: Arc, pending_payment_store: Arc, + fee_estimator: Arc, chain_source: Arc, + payment_store: Arc, config: Arc, logger: Arc, + pending_payment_store: Arc, ) -> Self { let inner = Mutex::new(wallet); let persister = Mutex::new(wallet_persister); @@ -91,6 +93,7 @@ impl Wallet { persister, broadcaster, fee_estimator, + chain_source, payment_store, config, logger, @@ -196,19 +199,6 @@ impl Wallet { Ok(()) } - pub(crate) fn insert_txo(&self, outpoint: OutPoint, txout: TxOut) -> Result<(), Error> { - let mut locked_wallet = self.inner.lock().unwrap(); - locked_wallet.insert_txout(outpoint, txout); - - let mut locked_persister = self.persister.lock().unwrap(); - locked_wallet.persist(&mut locked_persister).map_err(|e| { - log_error!(self.logger, "Failed to persist wallet: {}", e); - Error::PersistenceFailed - })?; - - Ok(()) - } - fn update_payment_store<'a>( &self, locked_wallet: &'a mut PersistedWallet, mut events: Vec, @@ -1040,6 +1030,25 @@ impl Listen for Wallet { ); } + // In order to be able to reliably calculate fees the `Wallet` needs access to the previous + // ouput data. To this end, we here insert any ouputs of transactions that LDK is intersted + // in (e.g., funding transaction ouputs) into the wallet's transaction graph when we see + // them, so it is reliably able to calculate fees for subsequent spends. + // + // FIXME: technically, we should also do this for mempool transactions. However, at the + // current time fixing the edge case doesn't seem worth the additional conplexity / + // additional overhead.. + let registered_txids = self.chain_source.registered_txids(); + for tx in &block.txdata { + let txid = tx.compute_txid(); + if registered_txids.contains(&txid) { + for (vout, txout) in tx.output.iter().enumerate() { + let outpoint = OutPoint { txid, vout: vout as u32 }; + locked_wallet.insert_txout(outpoint, txout.clone()); + } + } + } + match locked_wallet.apply_block_events(block, height) { Ok(events) => { if let Err(e) = self.update_payment_store(&mut *locked_wallet, events) {