Skip to content

Commit 1675d07

Browse files
committed
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
1 parent 7a25f90 commit 1675d07

1 file changed

Lines changed: 191 additions & 4 deletions

File tree

tests/reorg_test.rs

Lines changed: 191 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,111 @@ mod common;
22
use std::collections::HashMap;
33
use std::time::Duration;
44

5-
use bitcoin::Amount;
5+
use bitcoin::{Amount, BlockHash};
6+
use electrsd::corepc_node::Client as BitcoindClient;
7+
use electrsd::ElectrsD;
68
use ldk_node::payment::{PaymentDirection, PaymentKind};
79
use ldk_node::{Event, LightningBalance, PendingSweepBalance};
810
use proptest::prelude::prop;
911
use proptest::proptest;
12+
use serde_json::{json, Value};
1013

1114
use crate::common::{
12-
expect_event, generate_blocks_and_wait, invalidate_blocks, open_channel,
13-
premine_and_distribute_funds, random_chain_source, random_config, setup_bitcoind_and_electrsd,
14-
setup_node, stop_nodes_concurrently, wait_for_outpoint_spend,
15+
expect_channel_ready_event, expect_event, generate_blocks_and_wait, invalidate_blocks,
16+
open_channel, premine_and_distribute_funds, random_chain_source, random_config,
17+
setup_bitcoind_and_electrsd, setup_node, stop_node, stop_nodes_concurrently, wait_for_block,
18+
wait_for_outpoint_spend, TestChainSource, TestStoreType,
1519
};
1620

21+
#[derive(Clone, Copy)]
22+
enum TransactionChainSource {
23+
Esplora,
24+
Electrum,
25+
}
26+
27+
fn transaction_chain_source<'a>(
28+
source: TransactionChainSource, electrsd: &'a ElectrsD,
29+
) -> TestChainSource<'a> {
30+
match source {
31+
TransactionChainSource::Esplora => TestChainSource::Esplora(electrsd),
32+
TransactionChainSource::Electrum => TestChainSource::Electrum(electrsd),
33+
}
34+
}
35+
36+
fn best_block(bitcoind: &BitcoindClient) -> (BlockHash, u32) {
37+
let block_hash = bitcoind
38+
.call::<String>("getbestblockhash", &[])
39+
.expect("failed to get best block hash")
40+
.parse()
41+
.expect("best block hash should parse");
42+
let height = bitcoind.get_blockchain_info().expect("failed to get blockchain info").blocks;
43+
(block_hash, height as u32)
44+
}
45+
46+
fn assert_node_synced_to_tip(node: &ldk_node::Node, bitcoind: &BitcoindClient) {
47+
let (block_hash, height) = best_block(bitcoind);
48+
let node_best_block = node.status().current_best_block;
49+
assert_eq!(node_best_block.block_hash, block_hash);
50+
assert_eq!(node_best_block.height, height);
51+
}
52+
53+
async fn wait_for_node_to_reach_tip(node: &ldk_node::Node, bitcoind: &BitcoindClient) -> bool {
54+
let (block_hash, height) = best_block(bitcoind);
55+
for _ in 0..80 {
56+
let node_best_block = node.status().current_best_block;
57+
if node_best_block.block_hash == block_hash && node_best_block.height == height {
58+
return true;
59+
}
60+
61+
tokio::time::sleep(Duration::from_millis(250)).await;
62+
}
63+
false
64+
}
65+
66+
async fn assert_node_reaches_tip(
67+
node: ldk_node::Node, bitcoind: &BitcoindClient,
68+
) -> ldk_node::Node {
69+
if wait_for_node_to_reach_tip(&node, bitcoind).await {
70+
node
71+
} else {
72+
let expected = best_block(bitcoind);
73+
let actual = node.status().current_best_block;
74+
stop_node(node).await;
75+
panic!(
76+
"source-switch sync did not reach backend tip: expected {:?}, actual {:?}",
77+
expected, actual
78+
);
79+
}
80+
}
81+
82+
async fn copy_active_chain(
83+
source: &BitcoindClient, target: &BitcoindClient, target_electrsd: &ElectrsD,
84+
) {
85+
let source_height =
86+
source.get_blockchain_info().expect("failed to get blockchain info").blocks as usize;
87+
for height in 1..=source_height {
88+
let block_hash = source
89+
.get_block_hash(height as u64)
90+
.expect("failed to get block hash")
91+
.block_hash()
92+
.expect("block hash should be present");
93+
let block_hex = source
94+
.call::<String>("getblock", &[json!(block_hash.to_string()), json!(0)])
95+
.expect("failed to get raw block");
96+
let submit_res = target
97+
.call::<Value>("submitblock", &[json!(block_hex)])
98+
.expect("failed to submit block");
99+
assert!(
100+
submit_res.is_null() || submit_res == json!("inconclusive"),
101+
"submitblock failed at height {}: {}",
102+
height,
103+
submit_res
104+
);
105+
}
106+
wait_for_block(&target_electrsd.client, source_height).await;
107+
assert_eq!(best_block(source), best_block(target));
108+
}
109+
17110
async fn wait_for_pending_sweep_balance<F>(node: &ldk_node::Node, mut matches_pending_balance: F)
18111
where
19112
F: FnMut(&PendingSweepBalance) -> bool,
@@ -39,6 +132,100 @@ where
39132
}
40133
}
41134

135+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
136+
async fn chain_source_switch_reorg_esplora() {
137+
do_chain_source_switch_reorg_test(TransactionChainSource::Esplora).await;
138+
}
139+
140+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
141+
async fn chain_source_switch_reorg_electrum() {
142+
do_chain_source_switch_reorg_test(TransactionChainSource::Electrum).await;
143+
}
144+
145+
async fn do_chain_source_switch_reorg_test(source_kind: TransactionChainSource) {
146+
let (confirm_bitcoind, confirm_electrsd) = setup_bitcoind_and_electrsd();
147+
let (listen_bitcoind, listen_electrsd) = setup_bitcoind_and_electrsd();
148+
let confirm_source = transaction_chain_source(source_kind, &confirm_electrsd);
149+
150+
let mut node_a_config = random_config(true);
151+
node_a_config.store_type = TestStoreType::Sqlite;
152+
let node_b_config = random_config(true);
153+
154+
let node_a = setup_node(&confirm_source, node_a_config.clone());
155+
let node_b = setup_node(&confirm_source, node_b_config);
156+
157+
let amount_sat = 2_100_000;
158+
let addr_a = node_a.onchain_payment().new_address().unwrap();
159+
premine_and_distribute_funds(
160+
&confirm_bitcoind.client,
161+
&confirm_electrsd.client,
162+
vec![addr_a],
163+
Amount::from_sat(amount_sat),
164+
)
165+
.await;
166+
node_a.sync_wallets().unwrap();
167+
node_b.sync_wallets().unwrap();
168+
169+
open_channel(&node_a, &node_b, 2_000_000, true, &confirm_electrsd).await;
170+
generate_blocks_and_wait(&confirm_bitcoind.client, &confirm_electrsd.client, 6).await;
171+
node_a.sync_wallets().unwrap();
172+
node_b.sync_wallets().unwrap();
173+
expect_channel_ready_event!(node_a, node_b.node_id());
174+
expect_channel_ready_event!(node_b, node_a.node_id());
175+
176+
// At this point both backend clusters share the same active chain tip. The
177+
// channel funding transaction is in this shared prefix, so later divergent
178+
// suffixes only exercise tip switching and reorg handling.
179+
copy_active_chain(&confirm_bitcoind.client, &listen_bitcoind.client, &listen_electrsd).await;
180+
assert_node_synced_to_tip(&node_a, &confirm_bitcoind.client);
181+
182+
// The Confirm backend now has a five-block private suffix unknown to the
183+
// Listen backend. Syncing through Esplora/Electrum persists Node A at this
184+
// Confirm-only tip.
185+
generate_blocks_and_wait(&confirm_bitcoind.client, &confirm_electrsd.client, 5).await;
186+
node_a.sync_wallets().unwrap();
187+
assert_node_synced_to_tip(&node_a, &confirm_bitcoind.client);
188+
let confirm_private_tip = best_block(&confirm_bitcoind.client);
189+
stop_node(node_a).await;
190+
191+
// The Listen backend gets a different five-block suffix from the same shared
192+
// ancestor. This is a reorg within the six-confirmation safety target, and
193+
// the Listen backend cannot resolve the Confirm-only tip by hash.
194+
generate_blocks_and_wait(&listen_bitcoind.client, &listen_electrsd.client, 5).await;
195+
let listen_private_tip = best_block(&listen_bitcoind.client);
196+
assert_ne!(confirm_private_tip.0, listen_private_tip.0);
197+
assert_eq!(confirm_private_tip.1, listen_private_tip.1);
198+
199+
let listen_source = TestChainSource::BitcoindRpcSync(&listen_bitcoind);
200+
let node_a = setup_node(&listen_source, node_a_config.clone());
201+
// Old LDK would fail to reach the Listen tip here: bitcoind cannot resolve
202+
// Node A's stale Confirm-only tip, so block sync needs the persisted
203+
// BlockLocator previous hashes to find the shared ancestor and disconnect
204+
// the private suffix.
205+
let node_a = assert_node_reaches_tip(node_a, &listen_bitcoind.client).await;
206+
assert_node_synced_to_tip(&node_a, &listen_bitcoind.client);
207+
assert_eq!(node_a.list_channels().len(), 1);
208+
stop_node(node_a).await;
209+
210+
// Switch back from the Listen-only tip to the Confirm chain. The same
211+
// persisted node should now process the opposite reorg through the
212+
// transaction-based Confirm client.
213+
let node_a = setup_node(&confirm_source, node_a_config.clone());
214+
node_a.sync_wallets().unwrap();
215+
assert_node_synced_to_tip(&node_a, &confirm_bitcoind.client);
216+
assert_eq!(node_a.list_channels().len(), 1);
217+
stop_node(node_a).await;
218+
219+
// Finally switch once more to the Listen source, proving the safety property
220+
// is repeatable instead of only working for the first source change.
221+
let node_a = setup_node(&listen_source, node_a_config);
222+
let node_a = assert_node_reaches_tip(node_a, &listen_bitcoind.client).await;
223+
assert_node_synced_to_tip(&node_a, &listen_bitcoind.client);
224+
assert_eq!(node_a.list_channels().len(), 1);
225+
226+
stop_nodes_concurrently(vec![node_a, node_b]).await;
227+
}
228+
42229
proptest! {
43230
#![proptest_config(proptest::test_runner::Config::with_cases(5))]
44231
#[test]

0 commit comments

Comments
 (0)