Skip to content

Commit 03812a0

Browse files
authored
Merge pull request #972 from tnull/2026-07-fix-flaky-reorg-tests
Fix flaky reorg tests
2 parents 150f370 + 87b780e commit 03812a0

4 files changed

Lines changed: 101 additions & 64 deletions

File tree

tests/common/mod.rs

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ pub(crate) async fn generate_blocks_and_wait<E: ElectrumApi>(
753753
let address = bitcoind.new_address().expect("failed to get new address");
754754
// TODO: expect this Result once the WouldBlock issue is resolved upstream.
755755
let _block_hashes_res = bitcoind.generate_to_address(num, &address);
756-
wait_for_block(electrs, cur_height as usize + num).await;
756+
wait_for_block(bitcoind, electrs, cur_height as usize + num).await;
757757
print!(" Done!");
758758
println!("\n");
759759
}
@@ -773,27 +773,25 @@ pub(crate) fn invalidate_blocks(bitcoind: &BitcoindClient, num_blocks: usize) {
773773
assert!(new_cur_height + num_blocks == cur_height);
774774
}
775775

776-
pub(crate) async fn wait_for_block<E: ElectrumApi>(electrs: &E, min_height: usize) {
777-
let mut header = match electrs.block_headers_subscribe() {
778-
Ok(header) => header,
779-
Err(_) => {
780-
// While subscribing should succeed the first time around, we ran into some cases where
781-
// it didn't. Since we can't proceed without subscribing, we try again after a delay
782-
// and panic if it still fails.
783-
tokio::time::sleep(Duration::from_secs(3)).await;
784-
electrs.block_headers_subscribe().expect("failed to subscribe to block headers")
785-
},
786-
};
787-
loop {
788-
if header.height >= min_height {
789-
break;
776+
pub(crate) async fn wait_for_block<E: ElectrumApi>(
777+
bitcoind: &BitcoindClient, electrs: &E, min_height: usize,
778+
) {
779+
let expected_block_hash = exponential_backoff_poll(|| {
780+
let bitcoind_height =
781+
bitcoind.get_blockchain_info().expect("failed to get blockchain info").blocks as usize;
782+
if bitcoind_height < min_height {
783+
return None;
790784
}
791-
header = exponential_backoff_poll(|| {
792-
electrs.ping().expect("failed to ping electrs");
793-
electrs.block_headers_pop().expect("failed to pop block header")
794-
})
795-
.await;
796-
}
785+
bitcoind.get_block_hash(min_height as u64).ok()?.block_hash().ok()
786+
})
787+
.await;
788+
// A height-only wait can return the old header during a same-height reorg. Require the
789+
// replacement hash so callers cannot sync against the stale chain by mistake.
790+
exponential_backoff_poll(|| {
791+
let header = electrs.block_header(min_height).ok()?;
792+
(header.block_hash() == expected_block_hash).then_some(())
793+
})
794+
.await;
797795
}
798796

799797
pub(crate) async fn wait_for_tx<E: ElectrumApi>(electrs: &E, txid: Txid) {
@@ -812,15 +810,14 @@ pub(crate) async fn wait_for_outpoint_spend<E: ElectrumApi>(electrs: &E, outpoin
812810
let tx = electrs.transaction_get(&outpoint.txid).unwrap();
813811
let txout_script = tx.output.get(outpoint.vout as usize).unwrap().clone().script_pubkey;
814812

815-
let is_spent = !electrs.script_get_history(&txout_script).unwrap().is_empty();
816-
if is_spent {
817-
return;
818-
}
819-
813+
// Script history already contains the funding transaction itself, so wait until the exact
814+
// funding outpoint leaves the unspent set instead of treating any history as a spend.
820815
exponential_backoff_poll(|| {
821816
electrs.ping().unwrap();
822817

823-
let is_spent = !electrs.script_get_history(&txout_script).unwrap().is_empty();
818+
let is_spent = !electrs.script_list_unspent(&txout_script).unwrap().iter().any(|output| {
819+
output.tx_hash == outpoint.txid && output.tx_pos == outpoint.vout as usize
820+
});
824821
is_spent.then_some(())
825822
})
826823
.await;

tests/integration_tests_rust.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ async fn reorged_onchain_payment_returns_to_unconfirmed() {
807807
.call("generateblock", &[json!(replacement_address.to_string()), json!([])])
808808
.expect("failed to generate empty block");
809809
}
810-
wait_for_block(&electrsd.client, original_height as usize + 1).await;
810+
wait_for_block(&bitcoind.client, &electrsd.client, original_height as usize + 1).await;
811811

812812
node_a.sync_wallets().unwrap();
813813
node_b.sync_wallets().unwrap();
@@ -2056,7 +2056,7 @@ async fn splice_payment_reorged_to_unconfirmed() {
20562056
.call("generateblock", &[json!(replacement_address.to_string()), json!([])])
20572057
.expect("failed to generate empty block");
20582058
}
2059-
wait_for_block(&electrsd.client, original_height as usize + 1).await;
2059+
wait_for_block(&bitcoind.client, &electrsd.client, original_height as usize + 1).await;
20602060
node_b.sync_wallets().unwrap();
20612061

20622062
// The funding payment returns to `Unconfirmed` and stays `Pending`, exercising the
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
cc 06354c9b049db51c31557bf46d86a68bdd4049577cbd9190fb81c7824b18f0e6 # shrinks to reorg_depth = 6, force_close = false
2+
cc ffba5725835411b0948e834640dd37fc9a35696a301d7f2d8f2054f158bd2cae # shrinks to reorg_depth = 1, force_close = true

tests/reorg_test.rs

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,27 @@ use proptest::prelude::prop;
88
use proptest::proptest;
99

1010
use crate::common::{
11-
expect_event, generate_blocks_and_wait, invalidate_blocks, open_channel,
12-
premine_and_distribute_funds, random_chain_source, random_config, setup_bitcoind_and_electrsd,
13-
setup_node, wait_for_outpoint_spend,
11+
expect_event, exponential_backoff_poll, generate_blocks_and_wait, invalidate_blocks,
12+
open_channel, premine_and_distribute_funds, random_chain_source, random_config,
13+
setup_bitcoind_and_electrsd, setup_node, wait_for_outpoint_spend, wait_for_tx,
1414
};
1515

16+
async fn wait_for_pending_sweep_balance<F>(
17+
node: &ldk_node::Node, mut matches_balance: F,
18+
) -> PendingSweepBalance
19+
where
20+
F: FnMut(&PendingSweepBalance) -> bool,
21+
{
22+
exponential_backoff_poll(|| {
23+
node.sync_wallets().unwrap();
24+
node.list_balances()
25+
.pending_balances_from_channel_closures
26+
.into_iter()
27+
.find(|balance| matches_balance(balance))
28+
})
29+
.await
30+
}
31+
1632
proptest! {
1733
#![proptest_config(proptest::test_runner::Config::with_cases(5))]
1834
#[test]
@@ -76,7 +92,9 @@ proptest! {
7692
nodes_funding_tx.insert(node.node_id(), funding_txo);
7793
}
7894

79-
generate_blocks_and_wait(bitcoind, electrs, 6).await;
95+
// Keep funding confirmed across the deepest reorg. rust-lightning PR #4231 exempts
96+
// only trusted zero-conf channels; regular channels still force-close at zero confirmations.
97+
generate_blocks_and_wait(bitcoind, electrs, 7).await;
8098
sync_wallets!();
8199

82100
reorg!(reorg_depth);
@@ -144,40 +162,60 @@ proptest! {
144162
sync_wallets!();
145163

146164
if force_close {
147-
for node in &nodes {
148-
node.sync_wallets().unwrap();
149-
// If there is no more balance, there is nothing to process here.
150-
if node.list_balances().lightning_balances.len() < 1 {
151-
return;
152-
}
153-
match node.list_balances().lightning_balances[0] {
154-
LightningBalance::ClaimableAwaitingConfirmations {
155-
confirmation_height,
156-
..
157-
} => {
158-
let cur_height = node.status().current_best_block.height;
159-
let blocks_to_go = confirmation_height - cur_height;
160-
generate_blocks_and_wait(bitcoind, electrs, blocks_to_go as usize).await;
161-
node.sync_wallets().unwrap();
162-
},
163-
_ => panic!("Unexpected balance state for node_hub!"),
164-
}
165+
let claimable_nodes = nodes
166+
.iter()
167+
.filter_map(|node| {
168+
node.list_balances().lightning_balances.iter().find_map(|balance| {
169+
match balance {
170+
LightningBalance::ClaimableAwaitingConfirmations {
171+
confirmation_height,
172+
..
173+
} => Some((node, *confirmation_height)),
174+
_ => None,
175+
}
176+
})
177+
})
178+
.collect::<Vec<_>>();
179+
let confirmation_height = claimable_nodes
180+
.iter()
181+
.map(|(_, confirmation_height)| *confirmation_height)
182+
.max()
183+
.expect("Missing claimable force-close balance");
184+
let cur_height = nodes[0].status().current_best_block.height;
185+
let blocks_to_go = confirmation_height.saturating_sub(cur_height);
186+
if blocks_to_go > 0 {
187+
generate_blocks_and_wait(bitcoind, electrs, blocks_to_go as usize).await;
188+
sync_wallets!();
189+
}
165190

166-
assert!(node.list_balances().lightning_balances.len() < 2);
167-
assert!(node.list_balances().pending_balances_from_channel_closures.len() > 0);
168-
match node.list_balances().pending_balances_from_channel_closures[0] {
169-
PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {},
170-
_ => panic!("Unexpected balance state!"),
191+
// Mining for one node advances the shared chain for every node. Mature all
192+
// claimable outputs together, wait for every sweep to reach the mempool, then
193+
// confirm them and wait for `AwaitingThresholdConfirmations`.
194+
for (node, _) in &claimable_nodes {
195+
let pending_balance = wait_for_pending_sweep_balance(node, |balance| {
196+
matches!(
197+
balance,
198+
PendingSweepBalance::BroadcastAwaitingConfirmation { .. }
199+
| PendingSweepBalance::AwaitingThresholdConfirmations { .. }
200+
)
201+
})
202+
.await;
203+
if let PendingSweepBalance::BroadcastAwaitingConfirmation {
204+
latest_spending_txid,
205+
..
206+
} = pending_balance
207+
{
208+
wait_for_tx(electrs, latest_spending_txid).await;
171209
}
210+
}
172211

173-
generate_blocks_and_wait(&bitcoind, electrs, 1).await;
174-
node.sync_wallets().unwrap();
175-
assert!(node.list_balances().lightning_balances.len() < 2);
176-
assert!(node.list_balances().pending_balances_from_channel_closures.len() > 0);
177-
match node.list_balances().pending_balances_from_channel_closures[0] {
178-
PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {},
179-
_ => panic!("Unexpected balance state!"),
180-
}
212+
generate_blocks_and_wait(bitcoind, electrs, 1).await;
213+
sync_wallets!();
214+
for (node, _) in &claimable_nodes {
215+
wait_for_pending_sweep_balance(node, |balance| {
216+
matches!(balance, PendingSweepBalance::AwaitingThresholdConfirmations { .. })
217+
})
218+
.await;
181219
}
182220
}
183221

0 commit comments

Comments
 (0)