1- use bitcoin:: { constants, Address , Amount , Network , ScriptBuf } ;
2-
3- use bdk_bitcoind_rpc:: bip158:: FilterIter ;
1+ use bdk_bitcoind_rpc:: bip158:: { Event , EventInner , FilterIter } ;
42use bdk_core:: { BlockId , CheckPoint } ;
53use bdk_testenv:: { anyhow, bitcoind, block_id, TestEnv } ;
4+ use bitcoin:: { constants, Address , Amount , Network , ScriptBuf } ;
65use bitcoincore_rpc:: RpcApi ;
76
87fn testenv ( ) -> anyhow:: Result < TestEnv > {
@@ -100,6 +99,7 @@ fn get_tip_and_chain_update() -> anyhow::Result<()> {
10099 let cp = CheckPoint :: from_block_ids ( test. chain ) . unwrap ( ) ;
101100 let mut iter = FilterIter :: new_with_checkpoint ( env. rpc_client ( ) , cp) ;
102101 assert_eq ! ( iter. get_tip( ) . unwrap( ) , Some ( new_tip) ) ;
102+ for _res in iter. by_ref ( ) { }
103103 let update_cp = iter. chain_update ( ) . unwrap ( ) ;
104104 let mut update_blocks: Vec < _ > = update_cp. iter ( ) . map ( |cp| cp. block_id ( ) ) . collect ( ) ;
105105 update_blocks. reverse ( ) ;
@@ -111,7 +111,6 @@ fn get_tip_and_chain_update() -> anyhow::Result<()> {
111111
112112#[ test]
113113fn filter_iter_returns_matched_blocks ( ) -> anyhow:: Result < ( ) > {
114- use bdk_bitcoind_rpc:: bip158:: { Event , EventInner } ;
115114 let env = testenv ( ) ?;
116115 let rpc = env. rpc_client ( ) ;
117116 while rpc. get_block_count ( ) ? < 101 {
@@ -163,3 +162,278 @@ fn filter_iter_error_no_scripts() -> anyhow::Result<()> {
163162
164163 Ok ( ( ) )
165164}
165+
166+ #[ test]
167+ #[ allow( clippy:: print_stdout) ]
168+ fn filter_iter_handles_reorg ( ) -> anyhow:: Result < ( ) > {
169+ let env = testenv ( ) ?;
170+ let client = env. rpc_client ( ) ;
171+
172+ // 1. Initial setup & mining
173+ println ! ( "STEP: Initial mining (target height 102 for maturity)" ) ;
174+
175+ let expected_initial_height = 102 ;
176+ while env. rpc_client ( ) . get_block_count ( ) ? < expected_initial_height {
177+ let _ = env. mine_blocks ( 1 , None ) ?;
178+ }
179+ // *****************************
180+ // Check the expected initial height
181+ assert_eq ! (
182+ client. get_block_count( ) ?,
183+ expected_initial_height,
184+ "Block count should be {} after initial mine" ,
185+ expected_initial_height
186+ ) ;
187+
188+ // 2. Create watched script
189+ println ! ( "STEP: Creating watched script" ) ;
190+ // Ensure address and spk_to_watch are defined here *****
191+ // ******************************************************************
192+ let spk_to_watch = ScriptBuf :: from_hex ( "0014446906a6560d8ad760db3156706e72e171f3a2aa" ) ?;
193+ let address = Address :: from_script ( & spk_to_watch, Network :: Regtest ) ?;
194+ println ! ( "Watching SPK: {}" , spk_to_watch. to_hex_string( ) ) ;
195+
196+ // Create 2 txs to be confirmed at consecutive heights.
197+ // We have to choose our UTXOs now to make sure one doesn't get invalidated
198+ // later by a reorg.
199+ let unspent = client. list_unspent ( None , None , None , None , None ) ?;
200+ assert ! ( unspent. len( ) >= 2 ) ;
201+ use bdk_testenv:: bitcoincore_rpc:: bitcoincore_rpc_json:: CreateRawTransactionInput ;
202+ let unspent_1 = & unspent[ 0 ] ;
203+ let unspent_2 = & unspent[ 1 ] ;
204+ let utxo_1 = CreateRawTransactionInput {
205+ txid : unspent_1. txid ,
206+ vout : unspent_1. vout ,
207+ sequence : None ,
208+ } ;
209+ let utxo_2 = CreateRawTransactionInput {
210+ txid : unspent_2. txid ,
211+ vout : unspent_2. vout ,
212+ sequence : None ,
213+ } ;
214+
215+ // create tx 1
216+ println ! ( "STEP: Creating transactions to send" ) ;
217+ let to_send = Amount :: ONE_BTC ;
218+ let fee = Amount :: from_sat ( 1_000 ) ;
219+ let change_addr = client. get_new_address ( None , None ) ?. assume_checked ( ) ;
220+ let change_amt = unspent_1. amount - to_send - fee;
221+ let out = [
222+ ( address. to_string ( ) , to_send) ,
223+ ( change_addr. to_string ( ) , change_amt) ,
224+ ]
225+ . into ( ) ;
226+ let to_send = Amount :: ONE_BTC * 2 ;
227+ let tx = client. create_raw_transaction ( & [ utxo_1] , & out, None , None ) ?;
228+ let res = client. sign_raw_transaction_with_wallet ( & tx, None , None ) ?;
229+ let tx_1 = res. transaction ( ) ?;
230+ // create tx 2
231+ let change_addr = client. get_new_address ( None , None ) ?. assume_checked ( ) ;
232+ let change_amt = unspent_2. amount - to_send - fee;
233+ let out = [
234+ ( address. to_string ( ) , to_send) ,
235+ ( change_addr. to_string ( ) , change_amt) ,
236+ ]
237+ . into ( ) ;
238+ let tx = client. create_raw_transaction ( & [ utxo_2] , & out, None , None ) ?;
239+ let res = client. sign_raw_transaction_with_wallet ( & tx, None , None ) ?;
240+ let tx_2 = res. transaction ( ) ?;
241+
242+ // let mine_to: u32 = 103;
243+
244+ println ! ( "STEP: Mining to height {}" , 103 ) ;
245+ while env. rpc_client ( ) . get_block_count ( ) ? < 103 {
246+ let _ = env. mine_blocks ( 1 , None ) ?;
247+ }
248+
249+ // 3. Mine block A WITH relevant tx
250+ println ! ( "STEP: Sending tx for original block A" ) ;
251+ let txid_a = client. send_raw_transaction ( & tx_1) ?;
252+ println ! ( "STEP: Mining original block A" ) ;
253+ let hash_104 = env. mine_blocks ( 1 , None ) ?[ 0 ] ;
254+
255+ // 4. Mine block B WITH relevant tx 2
256+ println ! ( "STEP: Sending tx 2 for original block B" ) ;
257+ let txid_b = client. send_raw_transaction ( & tx_2) ?;
258+ println ! ( "STEP: Mining original block B" ) ;
259+ let hash_105 = env. mine_blocks ( 1 , None ) ?[ 0 ] ;
260+
261+ assert_eq ! (
262+ client. get_block_count( ) ?,
263+ 105 ,
264+ "Block count should be 105 after mining block B"
265+ ) ;
266+
267+ // 5. Instantiate FilterIter at start height 104
268+ println ! ( "STEP: Instantiating FilterIter" ) ;
269+ // Start processing from height 104
270+ let start_height = 104 ;
271+ let mut iter = FilterIter :: new_with_height ( client, start_height) ;
272+ iter. add_spk ( spk_to_watch. clone ( ) ) ;
273+ let initial_tip = iter. get_tip ( ) ?. expect ( "Should get initial tip" ) ;
274+ assert_eq ! ( initial_tip. height, 105 ) ;
275+ assert_eq ! ( initial_tip. hash, hash_105) ;
276+
277+ // 6. Iterate once processing block A
278+ println ! ( "STEP: Iterating once (original block A)" ) ;
279+ let event_a = iter. next ( ) . expect ( "Iterator should have item A" ) ?;
280+ // println!("First event: {:?}", event_a);
281+ match event_a {
282+ Event :: Block ( EventInner { height, block } ) => {
283+ assert_eq ! ( height, 104 ) ;
284+ assert_eq ! ( block. block_hash( ) , hash_104) ;
285+ assert ! ( block. txdata. iter( ) . any( |tx| tx. compute_txid( ) == txid_a) ) ;
286+ }
287+ _ => panic ! ( "Expected relevant tx at block A 102" ) ,
288+ }
289+
290+ // 7. Simulate Reorg (Invalidate blocks B and A)
291+ println ! ( "STEP: Invalidating original blocks B and A" ) ;
292+ println ! ( "Invalidating blocks B ({}) and A ({})" , hash_105, hash_104) ;
293+ client. invalidate_block ( & hash_105) ?;
294+ client. invalidate_block ( & hash_104) ?;
295+ // We should see 2 unconfirmed txs in mempool
296+ let raw_mempool = client. get_raw_mempool ( ) ?;
297+ assert_eq ! ( raw_mempool. len( ) , 2 ) ;
298+ println ! (
299+ "{} txs in mempool at height {}" ,
300+ raw_mempool. len( ) ,
301+ client. get_block_count( ) ?
302+ ) ;
303+
304+ // 8. Mine Replacement Blocks WITH relevant txs
305+ // First mine Block A'
306+ println ! ( "STEP: Mining replacement block A' (with send tx x2)" ) ;
307+ let hash_104_prime = env. mine_blocks ( 1 , None ) ?[ 0 ] ;
308+ let height = client. get_block_count ( ) ?;
309+ println ! ( "Block {} (A') hash: {}" , height, hash_104_prime) ;
310+ assert_eq ! ( height, 104 ) ;
311+ assert_ne ! ( hash_104, hash_104_prime) ;
312+
313+ // Mine Block B' - empty or unrelated txs
314+ println ! ( "STEP: Mining replacement block B' (no send tx)" ) ;
315+ let hash_105_prime = env. mine_blocks ( 1 , None ) ?[ 0 ] ;
316+ let height = client. get_block_count ( ) ?;
317+ println ! ( "Block {} (B') hash: {}" , height, hash_105_prime) ;
318+ assert_eq ! ( height, 105 ) ;
319+ assert_ne ! ( hash_105, hash_105_prime) ;
320+
321+ // 9. Continue Iterating & Collect Events AFTER reorg
322+ // Iterator should now process heights 109 (A') and 110 (B').
323+ let mut post_reorg_events: Vec < Event > = vec ! [ ] ;
324+
325+ println ! ( "STEP: Starting post-reorg iteration loop" ) ;
326+ println ! ( "Continuing iteration after reorg..." ) ;
327+ for event_result in iter. by_ref ( ) {
328+ let event = event_result?;
329+ println ! (
330+ "Post-reorg event height: {}, matched: {}" ,
331+ event. height( ) ,
332+ event. is_match( ) ,
333+ ) ;
334+ post_reorg_events. push ( event) ;
335+ }
336+
337+ // 10. Assertions
338+ println ! ( "STEP: Checking post-reorg assertions" ) ;
339+
340+ // Check for event post-reorg (Block A')
341+ let event_104_post = post_reorg_events. iter ( ) . find ( |e| e. height ( ) == 104 ) ;
342+ assert ! (
343+ event_104_post. is_some( ) ,
344+ "Should have yielded an event for post-reorg (Block A')"
345+ ) ;
346+ match event_104_post. unwrap ( ) {
347+ Event :: Block ( inner) => {
348+ assert_eq ! (
349+ inner. block. block_hash( ) ,
350+ hash_104_prime,
351+ "BUG: Iterator yielded wrong block for height 104! Expected A'"
352+ ) ;
353+ assert ! (
354+ inner
355+ . block
356+ . txdata
357+ . iter( )
358+ . any( |tx| tx. compute_txid( ) == txid_a) ,
359+ "Expected relevant tx A"
360+ ) ;
361+ assert ! (
362+ inner
363+ . block
364+ . txdata
365+ . iter( )
366+ . any( |tx| tx. compute_txid( ) == txid_b) ,
367+ "Expected relevant tx B"
368+ ) ;
369+ }
370+ Event :: NoMatch ( ..) => {
371+ panic ! ( "Expected to match height 104" ) ;
372+ }
373+ }
374+
375+ // Check for event post-reorg (Block B')
376+ let event_105_post = post_reorg_events. iter ( ) . find ( |e| e. height ( ) == 105 ) ;
377+ assert ! (
378+ event_105_post. is_some( ) ,
379+ "Should have yielded an event for post-reorg (Block B')"
380+ ) ;
381+ match event_105_post. unwrap ( ) {
382+ Event :: NoMatch ( h) => {
383+ assert_eq ! ( * h, 105 , "Should be NoMatch for block B'" ) ;
384+ }
385+ Event :: Block ( ..) => {
386+ panic ! ( "Expected NoMatch for block B'" ) ;
387+ }
388+ }
389+
390+ // Check chain update tip
391+ // println!("STEP: Checking chain_update");
392+ let final_update = iter. chain_update ( ) ;
393+ assert ! (
394+ final_update. is_none( ) ,
395+ "We didn't instantiate FilterIter with a checkpoint"
396+ ) ;
397+
398+ Ok ( ( ) )
399+ }
400+
401+ // Test that while a reorg is detected we delay incrementing the best height
402+ #[ test]
403+ #[ ignore]
404+ fn repeat_reorgs ( ) -> anyhow:: Result < ( ) > {
405+ const MINE_TO : u32 = 11 ;
406+
407+ let env = testenv ( ) ?;
408+ let rpc = env. rpc_client ( ) ;
409+ while rpc. get_block_count ( ) ? < MINE_TO as u64 {
410+ let _ = env. mine_blocks ( 1 , None ) ?;
411+ }
412+
413+ let spk = ScriptBuf :: from_hex ( "0014446906a6560d8ad760db3156706e72e171f3a2aa" ) ?;
414+
415+ let mut iter = FilterIter :: new_with_height ( env. rpc_client ( ) , 1 ) ;
416+ iter. add_spk ( spk) ;
417+ assert_eq ! ( iter. get_tip( ) ?. unwrap( ) . height, MINE_TO ) ;
418+
419+ // Process events to height (MINE_TO - 1)
420+ loop {
421+ if iter. next ( ) . unwrap ( ) ?. height ( ) == MINE_TO - 1 {
422+ break ;
423+ }
424+ }
425+
426+ for _ in 0 ..3 {
427+ // Invalidate 2 blocks and remine to height = MINE_TO
428+ let _ = env. reorg ( 2 ) ?;
429+
430+ // Call next. If we detect a reorg, we'll see no change in the event height
431+ assert_eq ! ( iter. next( ) . unwrap( ) ?. height( ) , MINE_TO - 1 ) ;
432+ }
433+
434+ // If no reorg, then height should increment normally from here on
435+ assert_eq ! ( iter. next( ) . unwrap( ) ?. height( ) , MINE_TO ) ;
436+ assert ! ( iter. next( ) . is_none( ) ) ;
437+
438+ Ok ( ( ) )
439+ }
0 commit comments