@@ -2,18 +2,111 @@ mod common;
22use std:: collections:: HashMap ;
33use 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 ;
68use ldk_node:: payment:: { PaymentDirection , PaymentKind } ;
79use ldk_node:: { Event , LightningBalance , PendingSweepBalance } ;
810use proptest:: prelude:: prop;
911use proptest:: proptest;
12+ use serde_json:: { json, Value } ;
1013
1114use 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+
17110async fn wait_for_pending_sweep_balance < F > ( node : & ldk_node:: Node , mut matches_pending_balance : F )
18111where
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+
42229proptest ! {
43230 #![ proptest_config( proptest:: test_runner:: Config :: with_cases( 5 ) ) ]
44231 #[ test]
0 commit comments