diff --git a/Cargo.toml b/Cargo.toml index 9f1c257cb..8df4b8a68 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 8b575cc3f..2f6fdd287 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 13afeca7e..493d1d08d 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 93d274ff7..2a5f9ec53 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 61d4e7abc..abbe64452 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 c97e16fe6..be7ea56da 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 f1e2378c2..c8999d9f1 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 4503dfa06..1dad9719b 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 c8b792ccb..c46622098 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 160890895..8ae9133f4 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 8037f9347..170611389 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 e24db4d25..8cb8d3082 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, }; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f0148da8a..ae83b03eb 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 }, ] } @@ -635,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"); } @@ -655,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; } } @@ -729,6 +748,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 295d9fdd2..8db5243a3 100644 --- a/tests/reorg_test.rs +++ b/tests/reorg_test.rs @@ -1,18 +1,231 @@ 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, 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, +{ + 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); + } + } +} + +#[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] @@ -143,43 +356,50 @@ 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!"), - } + wait_for_pending_sweep_balance(node, |balance| { + matches!( + balance, + PendingSweepBalance::BroadcastAwaitingConfirmation { .. } + | PendingSweepBalance::AwaitingThresholdConfirmations { .. } + ) + }) + .await; - 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!"), + generate_blocks_and_wait(bitcoind, electrs, 1).await; + node.sync_wallets().unwrap(); + wait_for_pending_sweep_balance(node, |balance| { + matches!(balance, PendingSweepBalance::AwaitingThresholdConfirmations { .. }) + }) + .await; } + assert!(found_claimable_balance); } - } generate_blocks_and_wait(bitcoind, electrs, 6).await; sync_wallets!(); @@ -187,6 +407,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| { @@ -198,6 +423,8 @@ proptest! { assert_eq!(node.next_event(), None); }); + + stop_nodes_concurrently(nodes).await; }) } }