From 2bddfe01f844ed4ecfeda61718fc4b89741061f4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 16 Jun 2026 12:03:34 +0200 Subject: [PATCH 1/6] Stabilize reorg test setup Use available listener ports and tolerate intermediate force-close balances so the property test exercises reorg behavior instead of setup timing. Co-Authored-By: HAL 9000 --- tests/common/mod.rs | 13 ++++---- tests/reorg_test.rs | 77 ++++++++++++++++++++++++++------------------- 2 files changed, 52 insertions(+), 38 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f0148da8a4..111a904601 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -21,9 +21,9 @@ pub(crate) mod lnd; use std::collections::{HashMap, HashSet}; use std::env; use std::future::Future; +use std::net::TcpListener; use std::path::PathBuf; use std::str::FromStr; -use std::sync::atomic::{AtomicU16, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -358,13 +358,14 @@ pub(crate) fn random_storage_path() -> PathBuf { temp_path } -static NEXT_PORT: AtomicU16 = AtomicU16::new(20000); - pub(crate) fn generate_listening_addresses() -> Vec { - let port = NEXT_PORT.fetch_add(2, Ordering::Relaxed); + let listener_a = TcpListener::bind(("127.0.0.1", 0)).expect("available listener port"); + let listener_b = TcpListener::bind(("127.0.0.1", 0)).expect("available listener port"); + let port_a = listener_a.local_addr().expect("listener address").port(); + let port_b = listener_b.local_addr().expect("listener address").port(); vec![ - SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port }, - SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port: port + 1 }, + SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port: port_a }, + SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], port: port_b }, ] } diff --git a/tests/reorg_test.rs b/tests/reorg_test.rs index 295d9fdd24..97770067ad 100644 --- a/tests/reorg_test.rs +++ b/tests/reorg_test.rs @@ -143,43 +143,56 @@ proptest! { generate_blocks_and_wait(bitcoind, electrs, 1).await; sync_wallets!(); - if force_close { - for node in &nodes { - node.sync_wallets().unwrap(); - // If there is no more balance, there is nothing to process here. - if node.list_balances().lightning_balances.len() < 1 { - return; - } - match node.list_balances().lightning_balances[0] { - LightningBalance::ClaimableAwaitingConfirmations { - confirmation_height, - .. - } => { - let cur_height = node.status().current_best_block.height; - let blocks_to_go = confirmation_height - cur_height; + if force_close { + let mut found_claimable_balance = false; + for node in &nodes { + node.sync_wallets().unwrap(); + let balances = node.list_balances(); + let confirmation_height = balances.lightning_balances.iter().find_map(|b| { + match b { + LightningBalance::ClaimableAwaitingConfirmations { + confirmation_height, + .. + } => Some(*confirmation_height), + _ => None, + } + }); + let Some(confirmation_height) = confirmation_height else { + continue; + }; + found_claimable_balance = true; + + let cur_height = node.status().current_best_block.height; + let blocks_to_go = confirmation_height.saturating_sub(cur_height); + if blocks_to_go > 0 { generate_blocks_and_wait(bitcoind, electrs, blocks_to_go as usize).await; node.sync_wallets().unwrap(); - }, - _ => panic!("Unexpected balance state for node_hub!"), - } - - assert!(node.list_balances().lightning_balances.len() < 2); - assert!(node.list_balances().pending_balances_from_channel_closures.len() > 0); - match node.list_balances().pending_balances_from_channel_closures[0] { - PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {}, - _ => panic!("Unexpected balance state!"), - } + } - generate_blocks_and_wait(&bitcoind, electrs, 1).await; - node.sync_wallets().unwrap(); - assert!(node.list_balances().lightning_balances.len() < 2); - assert!(node.list_balances().pending_balances_from_channel_closures.len() > 0); - match node.list_balances().pending_balances_from_channel_closures[0] { - PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {}, - _ => panic!("Unexpected balance state!"), + let balances = node.list_balances(); + assert!( + balances.pending_balances_from_channel_closures.iter().any(|b| matches!( + b, + PendingSweepBalance::BroadcastAwaitingConfirmation { .. } + | PendingSweepBalance::AwaitingThresholdConfirmations { .. } + )), + "Unexpected balance state: {:?}", + balances + ); + + generate_blocks_and_wait(bitcoind, electrs, 1).await; + node.sync_wallets().unwrap(); + let balances = node.list_balances(); + assert!( + balances.pending_balances_from_channel_closures.iter().any(|b| { + matches!(b, PendingSweepBalance::AwaitingThresholdConfirmations { .. }) + }), + "Unexpected balance state: {:?}", + balances + ); } + assert!(found_claimable_balance); } - } generate_blocks_and_wait(bitcoind, electrs, 6).await; sync_wallets!(); From 15edfdd55bfb39a2a13a28f03ad1786987c28c8e Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 16 Jun 2026 12:03:34 +0200 Subject: [PATCH 2/6] Stabilize post-reorg balance checks Wait for the replacement chain to confirm close and sweep outputs before checking final balances after the reorg. Co-Authored-By: HAL 9000 --- tests/reorg_test.rs | 57 ++++++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/tests/reorg_test.rs b/tests/reorg_test.rs index 97770067ad..8fafcdf5b2 100644 --- a/tests/reorg_test.rs +++ b/tests/reorg_test.rs @@ -1,5 +1,6 @@ mod common; use std::collections::HashMap; +use std::time::Duration; use bitcoin::Amount; use ldk_node::payment::{PaymentDirection, PaymentKind}; @@ -13,6 +14,31 @@ use crate::common::{ setup_node, wait_for_outpoint_spend, }; +async fn wait_for_pending_sweep_balance(node: &ldk_node::Node, mut matches_pending_balance: F) +where + F: FnMut(&PendingSweepBalance) -> bool, +{ + let mut delay = Duration::from_millis(64); + let mut tries = 0; + loop { + node.sync_wallets().unwrap(); + let balances = node.list_balances(); + if balances + .pending_balances_from_channel_closures + .iter() + .any(|balance| matches_pending_balance(balance)) + { + return; + } + assert!(tries < 20, "Unexpected balance state: {:?}", balances); + tries += 1; + tokio::time::sleep(delay).await; + if delay.as_millis() < 512 { + delay = delay.mul_f32(2.0); + } + } +} + proptest! { #![proptest_config(proptest::test_runner::Config::with_cases(5))] #[test] @@ -169,27 +195,21 @@ proptest! { node.sync_wallets().unwrap(); } - let balances = node.list_balances(); - assert!( - balances.pending_balances_from_channel_closures.iter().any(|b| matches!( - b, + wait_for_pending_sweep_balance(node, |balance| { + matches!( + balance, PendingSweepBalance::BroadcastAwaitingConfirmation { .. } | PendingSweepBalance::AwaitingThresholdConfirmations { .. } - )), - "Unexpected balance state: {:?}", - balances - ); + ) + }) + .await; generate_blocks_and_wait(bitcoind, electrs, 1).await; node.sync_wallets().unwrap(); - let balances = node.list_balances(); - assert!( - balances.pending_balances_from_channel_closures.iter().any(|b| { - matches!(b, PendingSweepBalance::AwaitingThresholdConfirmations { .. }) - }), - "Unexpected balance state: {:?}", - balances - ); + wait_for_pending_sweep_balance(node, |balance| { + matches!(balance, PendingSweepBalance::AwaitingThresholdConfirmations { .. }) + }) + .await; } assert!(found_claimable_balance); } @@ -200,6 +220,11 @@ proptest! { reorg!(reorg_depth); sync_wallets!(); + // The final reorg can leave close or sweep transactions below the wallet's + // trusted spendable depth even after they are confirmed on the replacement chain. + generate_blocks_and_wait(bitcoind, electrs, 6).await; + sync_wallets!(); + let fee_sat = 7000; // Check balance after close channel nodes.iter().for_each(|node| { From 7c8f15448fdde60a23201b1c114218c814336041 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 16 Jun 2026 12:05:02 +0200 Subject: [PATCH 3/6] Stop reorg nodes before runtime exit Stop all reorg test nodes on helper threads before leaving each property case so shutdown does not park the Tokio worker. Co-Authored-By: HAL 9000 --- tests/common/mod.rs | 37 +++++++++++++++++++++++++++++++++++++ tests/reorg_test.rs | 4 +++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 111a904601..b892502a5c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -730,6 +730,43 @@ where } } +pub(crate) async fn stop_nodes(node_a: TestNode, node_b: TestNode) { + let (stop_a_sender, stop_a_receiver) = tokio::sync::oneshot::channel(); + let (stop_b_sender, stop_b_receiver) = tokio::sync::oneshot::channel(); + std::thread::spawn(move || { + let _ = stop_a_sender.send(node_a.stop()); + }); + std::thread::spawn(move || { + let _ = stop_b_sender.send(node_b.stop()); + }); + stop_a_receiver.await.expect("node_a stop thread panicked").unwrap(); + stop_b_receiver.await.expect("node_b stop thread panicked").unwrap(); +} + +pub(crate) async fn stop_nodes_concurrently(nodes: Vec) { + let stop_receivers = nodes + .into_iter() + .map(|node| { + let (stop_sender, stop_receiver) = tokio::sync::oneshot::channel(); + std::thread::spawn(move || { + let _ = stop_sender.send(node.stop()); + }); + stop_receiver + }) + .collect::>(); + + for stop_receiver in stop_receivers { + stop_receiver.await.expect("node stop thread panicked").unwrap(); + } +} + +pub(crate) async fn stop_node(node: TestNode) { + let (stop_sender, stop_receiver) = tokio::sync::oneshot::channel(); + std::thread::spawn(move || { + let _ = stop_sender.send(node.stop()); + }); + stop_receiver.await.expect("node stop thread panicked").unwrap(); +} pub(crate) async fn premine_and_distribute_funds( bitcoind: &BitcoindClient, electrs: &E, addrs: Vec
, amount: Amount, ) { diff --git a/tests/reorg_test.rs b/tests/reorg_test.rs index 8fafcdf5b2..1ee9b2dd60 100644 --- a/tests/reorg_test.rs +++ b/tests/reorg_test.rs @@ -11,7 +11,7 @@ use proptest::proptest; use crate::common::{ expect_event, generate_blocks_and_wait, invalidate_blocks, open_channel, premine_and_distribute_funds, random_chain_source, random_config, setup_bitcoind_and_electrsd, - setup_node, wait_for_outpoint_spend, + setup_node, stop_nodes_concurrently, wait_for_outpoint_spend, }; async fn wait_for_pending_sweep_balance(node: &ldk_node::Node, mut matches_pending_balance: F) @@ -236,6 +236,8 @@ proptest! { assert_eq!(node.next_event(), None); }); + + stop_nodes_concurrently(nodes).await; }) } } From 557a404db49ab249d24c4fbce3ef64e988734c56 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 16 Jun 2026 12:08:31 +0200 Subject: [PATCH 4/6] Harden generated block waits Wait for bitcoind to reach the generated height before polling electrs for the corresponding block header. Co-Authored-By: HAL 9000 --- tests/common/mod.rs | 54 ++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b892502a5c..ae83b03ebc 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -636,7 +636,9 @@ pub(crate) async fn generate_blocks_and_wait( let address = bitcoind.new_address().expect("failed to get new address"); // TODO: expect this Result once the WouldBlock issue is resolved upstream. let _block_hashes_res = bitcoind.generate_to_address(num, &address); - wait_for_block(electrs, cur_height as usize + num).await; + let min_height = cur_height as usize + num; + wait_for_bitcoind_block(bitcoind, min_height).await; + wait_for_block(electrs, min_height).await; print!(" Done!"); println!("\n"); } @@ -656,26 +658,42 @@ pub(crate) fn invalidate_blocks(bitcoind: &BitcoindClient, num_blocks: usize) { assert!(new_cur_height + num_blocks == cur_height); } +async fn wait_for_bitcoind_block(bitcoind: &BitcoindClient, min_height: usize) { + let mut delay = Duration::from_millis(64); + let mut tries = 0; + loop { + let height = + bitcoind.get_blockchain_info().expect("failed to get blockchain info").blocks as usize; + if height >= min_height { + return; + } + assert!( + tries < 120, + "bitcoind height did not reach {} within 60 seconds, current height {}", + min_height, + height + ); + tries += 1; + tokio::time::sleep(delay).await; + if delay.as_millis() < 512 { + delay = delay.mul_f32(2.0); + } + } +} + pub(crate) async fn wait_for_block(electrs: &E, min_height: usize) { - let mut header = match electrs.block_headers_subscribe() { - Ok(header) => header, - Err(_) => { - // While subscribing should succeed the first time around, we ran into some cases where - // it didn't. Since we can't proceed without subscribing, we try again after a delay - // and panic if it still fails. - tokio::time::sleep(Duration::from_secs(3)).await; - electrs.block_headers_subscribe().expect("failed to subscribe to block headers") - }, - }; + let mut delay = Duration::from_millis(64); + let mut tries = 0; loop { - if header.height >= min_height { - break; + if electrs.block_header(min_height).is_ok() { + return; + } + assert!(tries < 120, "electrs did not serve block header {} within 60 seconds", min_height); + tries += 1; + tokio::time::sleep(delay).await; + if delay.as_millis() < 512 { + delay = delay.mul_f32(2.0); } - header = exponential_backoff_poll(|| { - electrs.ping().expect("failed to ping electrs"); - electrs.block_headers_pop().expect("failed to pop block header") - }) - .await; } } From 7a25f90e54b615fdab13a879b4ceea7b7b23b529 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 3 Jul 2026 11:07:15 +0200 Subject: [PATCH 5/6] DROPME Patch LDK Confirm refactor Point rust-lightning dependencies at the Confirm refactor branch so this branch can exercise the pending LDK API changes. Keep these compatibility updates separate because the patch is not meant to land as-is. Co-Authored-By: HAL 9000 --- Cargo.toml | 15 +++++++++++++++ src/builder.rs | 1 + src/data_store.rs | 2 +- src/event.rs | 18 ++++++++++++++++-- src/io/vss_store.rs | 2 +- src/lib.rs | 2 +- .../asynchronous/static_invoice_store.rs | 2 +- src/payment/bolt11.rs | 4 ++-- src/payment/pending_payment_store.rs | 2 +- src/payment/store.rs | 4 ++-- src/peer_store.rs | 2 +- src/types.rs | 2 +- 12 files changed, 43 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9f1c257cb8..8df4b8a688 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -197,3 +197,18 @@ harness = false #lightning-liquidity = { path = "../rust-lightning/lightning-liquidity" } #lightning-macros = { path = "../rust-lightning/lightning-macros" } #lightning-dns-resolver = { path = "../rust-lightning/lightning-dns-resolver" } + +[patch."https://github.com/lightningdevkit/rust-lightning"] +lightning = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +lightning-types = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +lightning-invoice = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +lightning-net-tokio = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +lightning-persister = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +lightning-background-processor = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +lightning-rapid-gossip-sync = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +lightning-block-sync = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +lightning-transaction-sync = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +lightning-liquidity = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +lightning-macros = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +lightning-dns-resolver = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } +possiblyrandom = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", rev = "e076821425a4aceb9006bcac6e252417b1b9f8bd" } diff --git a/src/builder.rs b/src/builder.rs index 8b575cc3f2..2f6fdd2871 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2012,6 +2012,7 @@ fn build_with_store_internal( Arc::clone(&channel_manager), Arc::clone(&om_resolver), IgnoringMessageHandler {}, + false, )) } else { Arc::new(OnionMessenger::new( diff --git a/src/data_store.rs b/src/data_store.rs index 13afeca7e3..493d1d08dd 100644 --- a/src/data_store.rs +++ b/src/data_store.rs @@ -223,7 +223,7 @@ where #[cfg(test)] mod tests { - use lightning::impl_writeable_tlv_based; + use lightning::impl_ser_tlv_based as impl_writeable_tlv_based; use lightning::io; use lightning::util::persist::{PageToken, PaginatedKVStore, PaginatedListResponse}; use lightning::util::test_utils::TestLogger; diff --git a/src/event.rs b/src/event.rs index 93d274ff7f..2a5f9ec538 100644 --- a/src/event.rs +++ b/src/event.rs @@ -14,6 +14,7 @@ use std::sync::{Arc, Mutex}; use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::secp256k1::PublicKey; use bitcoin::{Amount, OutPoint}; +use lightning::blinded_path::message::NextMessageHop; use lightning::events::bump_transaction::BumpTransactionEvent; #[cfg(not(feature = "uniffi"))] use lightning::events::PaidBolt12Invoice; @@ -29,7 +30,10 @@ use lightning::util::config::{ChannelConfigOverrides, ChannelConfigUpdate}; use lightning::util::errors::APIError; use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; -use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum}; +use lightning::{ + impl_ser_tlv_based as impl_writeable_tlv_based, + impl_ser_tlv_based_enum as impl_writeable_tlv_based_enum, +}; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -1725,7 +1729,11 @@ where self.bump_tx_event_handler.handle_event(&bte).await; }, - LdkEvent::OnionMessageIntercepted { peer_node_id, message } => { + LdkEvent::OnionMessageIntercepted { + next_hop: NextMessageHop::NodeId(peer_node_id), + message, + .. + } => { if let Some(om_mailbox) = self.om_mailbox.as_ref() { om_mailbox.onion_message_intercepted(peer_node_id, message); } else { @@ -1735,6 +1743,12 @@ where ); } }, + LdkEvent::OnionMessageIntercepted { + next_hop: NextMessageHop::ShortChannelId(_), + .. + } => { + log_trace!(self.logger, "Ignoring onion message intercepted for unknown SCID"); + }, LdkEvent::OnionMessagePeerConnected { peer_node_id } => { if let Some(om_mailbox) = self.om_mailbox.as_ref() { let messages = om_mailbox.onion_message_peer_connected(peer_node_id); diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 61d4e7abc2..abbe64452f 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -21,7 +21,7 @@ use bitcoin::bip32::{ChildNumber, Xpriv}; use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; use bitcoin::key::Secp256k1; use bitcoin::Network; -use lightning::impl_writeable_tlv_based_enum; +use lightning::impl_ser_tlv_based_enum as impl_writeable_tlv_based_enum; use lightning::io::{self, Error, ErrorKind}; use lightning::sign::{EntropySource as LdkEntropySource, RandomBytes}; use lightning::util::persist::{ diff --git a/src/lib.rs b/src/lib.rs index c97e16fe67..be7ea56dae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,7 +146,7 @@ use graph::NetworkGraph; use io::utils::update_and_persist_node_metrics; pub use lightning; use lightning::chain::BlockLocator; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based as impl_writeable_tlv_based; use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; pub use lightning::ln::channel_state::ChannelShutdownState; diff --git a/src/payment/asynchronous/static_invoice_store.rs b/src/payment/asynchronous/static_invoice_store.rs index f1e2378c23..c8999d9f1f 100644 --- a/src/payment/asynchronous/static_invoice_store.rs +++ b/src/payment/asynchronous/static_invoice_store.rs @@ -13,7 +13,7 @@ use std::time::Duration; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use lightning::blinded_path::message::BlindedMessagePath; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based as impl_writeable_tlv_based; use lightning::offers::static_invoice::StaticInvoice; use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, Writeable}; diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 4503dfa061..1dad9719b5 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -13,7 +13,7 @@ use std::sync::{Arc, RwLock}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based as impl_writeable_tlv_based; use lightning::ln::channelmanager::{ Bolt11InvoiceParameters, OptionalBolt11PaymentParams, PaymentId, }; @@ -313,7 +313,7 @@ impl Bolt11Payment { let payee_pubkey = invoice.recover_payee_pub_key(); log_info!( self.logger, - "Initiated sending {} msat to {}", + "Initiated sending {} msat to {:?}", payment_amount_msat, payee_pubkey ); diff --git a/src/payment/pending_payment_store.rs b/src/payment/pending_payment_store.rs index c8b792ccb1..c466220984 100644 --- a/src/payment/pending_payment_store.rs +++ b/src/payment/pending_payment_store.rs @@ -6,7 +6,7 @@ // accordance with one or both of these licenses. use bitcoin::Txid; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based as impl_writeable_tlv_based; use lightning::ln::channelmanager::PaymentId; use crate::data_store::{StorableObject, StorableObjectUpdate}; diff --git a/src/payment/store.rs b/src/payment/store.rs index 1608908958..8ae9133f44 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -16,8 +16,8 @@ use lightning::ln::types::ChannelId; use lightning::offers::offer::OfferId; use lightning::util::ser::{Readable, Writeable}; use lightning::{ - _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, - impl_writeable_tlv_based_enum, write_tlv_fields, + _init_and_read_len_prefixed_tlv_fields, impl_ser_tlv_based as impl_writeable_tlv_based, + impl_ser_tlv_based_enum as impl_writeable_tlv_based_enum, write_tlv_fields, }; use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning_types::string::UntrustedString; diff --git a/src/peer_store.rs b/src/peer_store.rs index 8037f93471..1706113893 100644 --- a/src/peer_store.rs +++ b/src/peer_store.rs @@ -10,7 +10,7 @@ use std::ops::Deref; use std::sync::{Arc, RwLock}; use bitcoin::secp256k1::PublicKey; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based as impl_writeable_tlv_based; use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; diff --git a/src/types.rs b/src/types.rs index e24db4d253..8cb8d30825 100644 --- a/src/types.rs +++ b/src/types.rs @@ -19,7 +19,7 @@ use bitcoin_payment_instructions::hrn_resolution::{ }; use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use lightning::chain::chainmonitor; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based as impl_writeable_tlv_based; use lightning::ln::channel_state::{ ChannelDetails as LdkChannelDetails, ChannelShutdownState, CounterpartyForwardingInfo, }; From 1675d07ef285133e7b3492121c1838989549bfcb Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 3 Jul 2026 11:14:59 +0200 Subject: [PATCH 6/6] Test chain source switching reorgs Exercise back-and-forth switching between transaction-based chain sources and bitcoind after both backends diverge from a shared tip. This guards the persisted locator data needed to recover from a stale source-specific tip. Co-Authored-By: HAL 9000 --- tests/reorg_test.rs | 195 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 4 deletions(-) diff --git a/tests/reorg_test.rs b/tests/reorg_test.rs index 1ee9b2dd60..8db5243a36 100644 --- a/tests/reorg_test.rs +++ b/tests/reorg_test.rs @@ -2,18 +2,111 @@ mod common; use std::collections::HashMap; use std::time::Duration; -use bitcoin::Amount; +use bitcoin::{Amount, BlockHash}; +use electrsd::corepc_node::Client as BitcoindClient; +use electrsd::ElectrsD; use ldk_node::payment::{PaymentDirection, PaymentKind}; use ldk_node::{Event, LightningBalance, PendingSweepBalance}; use proptest::prelude::prop; use proptest::proptest; +use serde_json::{json, Value}; use crate::common::{ - expect_event, generate_blocks_and_wait, invalidate_blocks, open_channel, - premine_and_distribute_funds, random_chain_source, random_config, setup_bitcoind_and_electrsd, - setup_node, stop_nodes_concurrently, wait_for_outpoint_spend, + expect_channel_ready_event, expect_event, generate_blocks_and_wait, invalidate_blocks, + open_channel, premine_and_distribute_funds, random_chain_source, random_config, + setup_bitcoind_and_electrsd, setup_node, stop_node, stop_nodes_concurrently, wait_for_block, + wait_for_outpoint_spend, TestChainSource, TestStoreType, }; +#[derive(Clone, Copy)] +enum TransactionChainSource { + Esplora, + Electrum, +} + +fn transaction_chain_source<'a>( + source: TransactionChainSource, electrsd: &'a ElectrsD, +) -> TestChainSource<'a> { + match source { + TransactionChainSource::Esplora => TestChainSource::Esplora(electrsd), + TransactionChainSource::Electrum => TestChainSource::Electrum(electrsd), + } +} + +fn best_block(bitcoind: &BitcoindClient) -> (BlockHash, u32) { + let block_hash = bitcoind + .call::("getbestblockhash", &[]) + .expect("failed to get best block hash") + .parse() + .expect("best block hash should parse"); + let height = bitcoind.get_blockchain_info().expect("failed to get blockchain info").blocks; + (block_hash, height as u32) +} + +fn assert_node_synced_to_tip(node: &ldk_node::Node, bitcoind: &BitcoindClient) { + let (block_hash, height) = best_block(bitcoind); + let node_best_block = node.status().current_best_block; + assert_eq!(node_best_block.block_hash, block_hash); + assert_eq!(node_best_block.height, height); +} + +async fn wait_for_node_to_reach_tip(node: &ldk_node::Node, bitcoind: &BitcoindClient) -> bool { + let (block_hash, height) = best_block(bitcoind); + for _ in 0..80 { + let node_best_block = node.status().current_best_block; + if node_best_block.block_hash == block_hash && node_best_block.height == height { + return true; + } + + tokio::time::sleep(Duration::from_millis(250)).await; + } + false +} + +async fn assert_node_reaches_tip( + node: ldk_node::Node, bitcoind: &BitcoindClient, +) -> ldk_node::Node { + if wait_for_node_to_reach_tip(&node, bitcoind).await { + node + } else { + let expected = best_block(bitcoind); + let actual = node.status().current_best_block; + stop_node(node).await; + panic!( + "source-switch sync did not reach backend tip: expected {:?}, actual {:?}", + expected, actual + ); + } +} + +async fn copy_active_chain( + source: &BitcoindClient, target: &BitcoindClient, target_electrsd: &ElectrsD, +) { + let source_height = + source.get_blockchain_info().expect("failed to get blockchain info").blocks as usize; + for height in 1..=source_height { + let block_hash = source + .get_block_hash(height as u64) + .expect("failed to get block hash") + .block_hash() + .expect("block hash should be present"); + let block_hex = source + .call::("getblock", &[json!(block_hash.to_string()), json!(0)]) + .expect("failed to get raw block"); + let submit_res = target + .call::("submitblock", &[json!(block_hex)]) + .expect("failed to submit block"); + assert!( + submit_res.is_null() || submit_res == json!("inconclusive"), + "submitblock failed at height {}: {}", + height, + submit_res + ); + } + wait_for_block(&target_electrsd.client, source_height).await; + assert_eq!(best_block(source), best_block(target)); +} + async fn wait_for_pending_sweep_balance(node: &ldk_node::Node, mut matches_pending_balance: F) where F: FnMut(&PendingSweepBalance) -> bool, @@ -39,6 +132,100 @@ where } } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn chain_source_switch_reorg_esplora() { + do_chain_source_switch_reorg_test(TransactionChainSource::Esplora).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn chain_source_switch_reorg_electrum() { + do_chain_source_switch_reorg_test(TransactionChainSource::Electrum).await; +} + +async fn do_chain_source_switch_reorg_test(source_kind: TransactionChainSource) { + let (confirm_bitcoind, confirm_electrsd) = setup_bitcoind_and_electrsd(); + let (listen_bitcoind, listen_electrsd) = setup_bitcoind_and_electrsd(); + let confirm_source = transaction_chain_source(source_kind, &confirm_electrsd); + + let mut node_a_config = random_config(true); + node_a_config.store_type = TestStoreType::Sqlite; + let node_b_config = random_config(true); + + let node_a = setup_node(&confirm_source, node_a_config.clone()); + let node_b = setup_node(&confirm_source, node_b_config); + + let amount_sat = 2_100_000; + let addr_a = node_a.onchain_payment().new_address().unwrap(); + premine_and_distribute_funds( + &confirm_bitcoind.client, + &confirm_electrsd.client, + vec![addr_a], + Amount::from_sat(amount_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + open_channel(&node_a, &node_b, 2_000_000, true, &confirm_electrsd).await; + generate_blocks_and_wait(&confirm_bitcoind.client, &confirm_electrsd.client, 6).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // At this point both backend clusters share the same active chain tip. The + // channel funding transaction is in this shared prefix, so later divergent + // suffixes only exercise tip switching and reorg handling. + copy_active_chain(&confirm_bitcoind.client, &listen_bitcoind.client, &listen_electrsd).await; + assert_node_synced_to_tip(&node_a, &confirm_bitcoind.client); + + // The Confirm backend now has a five-block private suffix unknown to the + // Listen backend. Syncing through Esplora/Electrum persists Node A at this + // Confirm-only tip. + generate_blocks_and_wait(&confirm_bitcoind.client, &confirm_electrsd.client, 5).await; + node_a.sync_wallets().unwrap(); + assert_node_synced_to_tip(&node_a, &confirm_bitcoind.client); + let confirm_private_tip = best_block(&confirm_bitcoind.client); + stop_node(node_a).await; + + // The Listen backend gets a different five-block suffix from the same shared + // ancestor. This is a reorg within the six-confirmation safety target, and + // the Listen backend cannot resolve the Confirm-only tip by hash. + generate_blocks_and_wait(&listen_bitcoind.client, &listen_electrsd.client, 5).await; + let listen_private_tip = best_block(&listen_bitcoind.client); + assert_ne!(confirm_private_tip.0, listen_private_tip.0); + assert_eq!(confirm_private_tip.1, listen_private_tip.1); + + let listen_source = TestChainSource::BitcoindRpcSync(&listen_bitcoind); + let node_a = setup_node(&listen_source, node_a_config.clone()); + // Old LDK would fail to reach the Listen tip here: bitcoind cannot resolve + // Node A's stale Confirm-only tip, so block sync needs the persisted + // BlockLocator previous hashes to find the shared ancestor and disconnect + // the private suffix. + let node_a = assert_node_reaches_tip(node_a, &listen_bitcoind.client).await; + assert_node_synced_to_tip(&node_a, &listen_bitcoind.client); + assert_eq!(node_a.list_channels().len(), 1); + stop_node(node_a).await; + + // Switch back from the Listen-only tip to the Confirm chain. The same + // persisted node should now process the opposite reorg through the + // transaction-based Confirm client. + let node_a = setup_node(&confirm_source, node_a_config.clone()); + node_a.sync_wallets().unwrap(); + assert_node_synced_to_tip(&node_a, &confirm_bitcoind.client); + assert_eq!(node_a.list_channels().len(), 1); + stop_node(node_a).await; + + // Finally switch once more to the Listen source, proving the safety property + // is repeatable instead of only working for the first source change. + let node_a = setup_node(&listen_source, node_a_config); + let node_a = assert_node_reaches_tip(node_a, &listen_bitcoind.client).await; + assert_node_synced_to_tip(&node_a, &listen_bitcoind.client); + assert_eq!(node_a.list_channels().len(), 1); + + stop_nodes_concurrently(vec![node_a, node_b]).await; +} + proptest! { #![proptest_config(proptest::test_runner::Config::with_cases(5))] #[test]