11use bdk_bitcoind_rpc:: bip158:: { Event , EventInner , FilterIter } ;
22use bdk_core:: { BlockId , CheckPoint } ;
3+ use bdk_testenv:: bitcoincore_rpc:: bitcoincore_rpc_json:: CreateRawTransactionInput ;
34use bdk_testenv:: { anyhow, bitcoind, block_id, TestEnv } ;
45use bitcoin:: { constants, Address , Amount , Network , ScriptBuf } ;
56use bitcoincore_rpc:: RpcApi ;
@@ -198,7 +199,6 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
198199 // later by a reorg.
199200 let unspent = client. list_unspent ( None , None , None , None , None ) ?;
200201 assert ! ( unspent. len( ) >= 2 ) ;
201- use bdk_testenv:: bitcoincore_rpc:: bitcoincore_rpc_json:: CreateRawTransactionInput ;
202202 let unspent_1 = & unspent[ 0 ] ;
203203 let unspent_2 = & unspent[ 1 ] ;
204204 let utxo_1 = CreateRawTransactionInput {
@@ -267,8 +267,12 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
267267 // 5. Instantiate FilterIter at start height 104
268268 println ! ( "STEP: Instantiating FilterIter" ) ;
269269 // Start processing from height 104
270- let start_height = 104 ;
271- let mut iter = FilterIter :: new_with_height ( client, start_height) ;
270+ let checkpoint = CheckPoint :: from_block_ids ( [ BlockId {
271+ height : 103 ,
272+ hash : client. get_block_hash ( 103 ) ?,
273+ } ] )
274+ . unwrap ( ) ;
275+ let mut iter = FilterIter :: new_with_checkpoint ( client, checkpoint) ;
272276 iter. add_spk ( spk_to_watch. clone ( ) ) ;
273277 let initial_tip = iter. get_tip ( ) ?. expect ( "Should get initial tip" ) ;
274278 assert_eq ! ( initial_tip. height, 105 ) ;
@@ -277,14 +281,13 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
277281 // 6. Iterate once processing block A
278282 println ! ( "STEP: Iterating once (original block A)" ) ;
279283 let event_a = iter. next ( ) . expect ( "Iterator should have item A" ) ?;
280- // println!("First event: {:?}", event_a);
281284 match event_a {
282285 Event :: Block ( EventInner { height, block } ) => {
283286 assert_eq ! ( height, 104 ) ;
284287 assert_eq ! ( block. block_hash( ) , hash_104) ;
285288 assert ! ( block. txdata. iter( ) . any( |tx| tx. compute_txid( ) == txid_a) ) ;
286289 }
287- _ => panic ! ( "Expected relevant tx at block A 102 " ) ,
290+ _ => panic ! ( "Expected relevant tx at block A 104 " ) ,
288291 }
289292
290293 // 7. Simulate Reorg (Invalidate blocks B and A)
@@ -388,13 +391,167 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
388391 }
389392
390393 // 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"
394+ let final_update = iter. chain_update ( ) . expect ( "Should return a checkpoint" ) ;
395+
396+ let block_104 = final_update. get ( 104 ) . expect ( "Expected block at height 104" ) ;
397+ assert_eq ! (
398+ block_104. hash( ) ,
399+ hash_104_prime,
400+ "Checkpoint should contain replacement block A′ at height 104"
396401 ) ;
397402
403+ let block_105 = final_update. get ( 105 ) . expect ( "Expected block at height 105" ) ;
404+ assert_eq ! (
405+ block_105. hash( ) ,
406+ hash_105_prime,
407+ "Checkpoint should contain replacement block B′ at height 105"
408+ ) ;
409+
410+ Ok ( ( ) )
411+ }
412+
413+ #[ test]
414+ #[ allow( clippy:: print_stdout) ]
415+ fn filter_iter_handles_reorg_between_next_calls ( ) -> anyhow:: Result < ( ) > {
416+ let env = testenv ( ) ?;
417+ let client = env. rpc_client ( ) ;
418+
419+ // 1. Initial setup & mining
420+ println ! ( "STEP: Initial mining (target height 102 for maturity)" ) ;
421+ let expected_initial_height = 102 ;
422+ while env. rpc_client ( ) . get_block_count ( ) ? < expected_initial_height {
423+ let _ = env. mine_blocks ( 1 , None ) ?;
424+ }
425+ assert_eq ! (
426+ client. get_block_count( ) ?,
427+ expected_initial_height,
428+ "Block count should be {} after initial mine" ,
429+ expected_initial_height
430+ ) ;
431+
432+ // 2. Create watched script
433+ println ! ( "STEP: Creating watched script" ) ;
434+ let spk_to_watch = ScriptBuf :: from_hex ( "0014446906a6560d8ad760db3156706e72e171f3a2aa" ) ?;
435+ let address = Address :: from_script ( & spk_to_watch, Network :: Regtest ) ?;
436+ println ! ( "Watching SPK: {}" , spk_to_watch. to_hex_string( ) ) ;
437+
438+ // 3. Create two transactions to be confirmed in consecutive blocks
439+ println ! ( "STEP: Creating transactions to send" ) ;
440+ let unspent = client. list_unspent ( None , None , None , None , None ) ?;
441+ assert ! ( unspent. len( ) >= 2 ) ;
442+ let ( utxo_1, utxo_2) = (
443+ CreateRawTransactionInput {
444+ txid : unspent[ 0 ] . txid ,
445+ vout : unspent[ 0 ] . vout ,
446+ sequence : None ,
447+ } ,
448+ CreateRawTransactionInput {
449+ txid : unspent[ 1 ] . txid ,
450+ vout : unspent[ 1 ] . vout ,
451+ sequence : None ,
452+ } ,
453+ ) ;
454+
455+ let fee = Amount :: from_sat ( 1000 ) ;
456+ let to_send = Amount :: from_sat ( 50_000 ) ;
457+ let change_1 = ( unspent[ 0 ] . amount - to_send - fee) . to_sat ( ) ;
458+ let change_2 = ( unspent[ 1 ] . amount - to_send - fee) . to_sat ( ) ;
459+
460+ let make_tx = |utxo, change_amt| {
461+ let out = [
462+ ( address. to_string ( ) , to_send) ,
463+ (
464+ client
465+ . get_new_address ( None , None ) ?
466+ . assume_checked ( )
467+ . to_string ( ) ,
468+ Amount :: from_sat ( change_amt) ,
469+ ) ,
470+ ]
471+ . into ( ) ;
472+ let tx = client. create_raw_transaction ( & [ utxo] , & out, None , None ) ?;
473+ Ok :: < _ , anyhow:: Error > (
474+ client
475+ . sign_raw_transaction_with_wallet ( & tx, None , None ) ?
476+ . transaction ( ) ?,
477+ )
478+ } ;
479+
480+ let tx_1 = make_tx ( utxo_1, change_1) ?;
481+ let tx_2 = make_tx ( utxo_2. clone ( ) , change_2) ?;
482+
483+ // 4. Mine up to height 103
484+ println ! ( "STEP: Mining to height 103" ) ;
485+ while env. rpc_client ( ) . get_block_count ( ) ? < 103 {
486+ let _ = env. mine_blocks ( 1 , None ) ?;
487+ }
488+
489+ // 5. Send tx1 and tx2, mine block A and block B
490+ println ! ( "STEP: Sending tx1 for block A" ) ;
491+ let txid_a = client. send_raw_transaction ( & tx_1) ?;
492+ let hash_104 = env. mine_blocks ( 1 , None ) ?[ 0 ] ;
493+
494+ println ! ( "STEP: Sending tx2 for block B" ) ;
495+ let _txid_b = client. send_raw_transaction ( & tx_2) ?;
496+ let hash_105 = env. mine_blocks ( 1 , None ) ?[ 0 ] ;
497+
498+ // 6. Instantiate FilterIter and iterate once
499+ println ! ( "STEP: Instantiating FilterIter" ) ;
500+ let mut iter = FilterIter :: new_with_height ( client, 104 ) ;
501+ iter. add_spk ( spk_to_watch. clone ( ) ) ;
502+ iter. get_tip ( ) ?;
503+
504+ println ! ( "STEP: Iterating once (original block A)" ) ;
505+ let event_a = iter. next ( ) . expect ( "Expected block A" ) ?;
506+ match event_a {
507+ Event :: Block ( EventInner { height, block } ) => {
508+ assert_eq ! ( height, 104 ) ;
509+ assert_eq ! ( block. block_hash( ) , hash_104) ;
510+ assert ! ( block. txdata. iter( ) . any( |tx| tx. compute_txid( ) == txid_a) ) ;
511+ }
512+ _ => panic ! ( "Expected match in block A" ) ,
513+ }
514+
515+ // 7. Simulate reorg at height 105
516+ println ! ( "STEP: Invalidating original block B" ) ;
517+ client. invalidate_block ( & hash_105) ?;
518+
519+ let unrelated_addr = client. get_new_address ( None , None ) ?. assume_checked ( ) ;
520+ let input_amt = unspent[ 1 ] . amount . to_sat ( ) ;
521+ let fee_sat = 2000 ;
522+ let change_sat = input_amt - to_send. to_sat ( ) - fee_sat;
523+ assert ! ( change_sat > 500 , "Change would be too small" ) ;
524+
525+ let change_addr = client. get_new_address ( None , None ) ?. assume_checked ( ) ;
526+ let out = [
527+ ( unrelated_addr. to_string ( ) , to_send) ,
528+ ( change_addr. to_string ( ) , Amount :: from_sat ( change_sat) ) ,
529+ ]
530+ . into ( ) ;
531+
532+ let tx_ds = {
533+ let tx = client. create_raw_transaction ( & [ utxo_2] , & out, None , None ) ?;
534+ let res = client. sign_raw_transaction_with_wallet ( & tx, None , None ) ?;
535+ res. transaction ( ) ?
536+ } ;
537+ client. send_raw_transaction ( & tx_ds) ?;
538+
539+ println ! ( "STEP: Mining replacement block B'" ) ;
540+ let _hash_105_prime = env. mine_blocks ( 1 , None ) ?[ 0 ] ;
541+ let new_tip = iter. get_tip ( ) ?. expect ( "Should have tip after reorg" ) ;
542+ assert_eq ! ( new_tip. height, 105 ) ;
543+ assert_ne ! ( new_tip. hash, hash_105, "BUG: still sees old block B" ) ;
544+
545+ // 8. Iterate again — should detect reorg and yield NoMatch for B'
546+ println ! ( "STEP: Iterating again (should detect reorg and yield B')" ) ;
547+ let event_b_prime = iter. next ( ) . expect ( "Expected B'" ) ?;
548+ match event_b_prime {
549+ Event :: NoMatch ( h) => {
550+ assert_eq ! ( h, 105 ) ;
551+ }
552+ Event :: Block ( _) => panic ! ( "Expected NoMatch for B' (replacement)" ) ,
553+ }
554+
398555 Ok ( ( ) )
399556}
400557
@@ -436,3 +593,70 @@ fn repeat_reorgs() -> anyhow::Result<()> {
436593
437594 Ok ( ( ) )
438595}
596+
597+ #[ test]
598+ #[ allow( clippy:: print_stdout) ]
599+ fn filter_iter_max_reorg_depth ( ) -> anyhow:: Result < ( ) > {
600+ use bdk_bitcoind_rpc:: bip158:: { Error , FilterIter } ;
601+ use bdk_chain:: {
602+ bitcoin:: { Address , Amount , Network , ScriptBuf } ,
603+ BlockId , CheckPoint ,
604+ } ;
605+
606+ let env = testenv ( ) ?;
607+ let client = env. rpc_client ( ) ;
608+
609+ const BASE_HEIGHT : u32 = 110 ;
610+ const REORG_LENGTH : u32 = 101 ;
611+ const START_HEIGHT : u32 = BASE_HEIGHT + 1 ;
612+ const FINAL_HEIGHT : u32 = START_HEIGHT + REORG_LENGTH - 1 ;
613+
614+ while client. get_block_count ( ) ? < BASE_HEIGHT as u64 {
615+ env. mine_blocks ( 1 , None ) ?;
616+ }
617+
618+ let spk = ScriptBuf :: from_hex ( "0014446906a6560d8ad760db3156706e72e171f3a2aa" ) ?;
619+ let addr = Address :: from_script ( & spk, Network :: Regtest ) ?;
620+
621+ // Mine blocks 111-211, each with a tx.
622+ for _ in 0 ..REORG_LENGTH {
623+ env. send ( & addr, Amount :: from_sat ( 1000 ) ) ?;
624+ env. mine_blocks ( 1 , None ) ?;
625+ }
626+
627+ // Create `CheckPoint` and build `FilterIter`.
628+ let cp = CheckPoint :: from_block_ids ( [ BlockId {
629+ height : BASE_HEIGHT ,
630+ hash : client. get_block_hash ( BASE_HEIGHT as u64 ) ?,
631+ } ] )
632+ . unwrap ( ) ;
633+ let mut iter = FilterIter :: new_with_checkpoint ( client, cp) ;
634+ iter. add_spk ( spk. clone ( ) ) ;
635+ iter. get_tip ( ) ?;
636+
637+ // Scan blocks 111–211 so their hashes reside in `self.blocks`.
638+ for res in iter. by_ref ( ) {
639+ res?;
640+ }
641+
642+ // Invalidate blocks 111-211 and mine replacement blocks.
643+ for h in ( START_HEIGHT ..=FINAL_HEIGHT ) . rev ( ) {
644+ let hash = client. get_block_hash ( h as u64 ) ?;
645+ client. invalidate_block ( & hash) ?;
646+ }
647+
648+ // Mine one extra block so the new tip (212) isn’t below `iter.height`.
649+ env. mine_blocks ( ( REORG_LENGTH + 1 ) as usize , None ) ?;
650+
651+ iter. get_tip ( ) ?;
652+ match iter. next ( ) {
653+ Some ( Err ( Error :: ReorgDepthExceeded ) ) => {
654+ println ! ( "SUCCESS: Detected ReorgDepthExceeded" ) ;
655+ }
656+ Some ( Err ( e) ) => panic ! ( "Expected ReorgDepthExceeded, got {:?}" , e) ,
657+ Some ( Ok ( ev) ) => panic ! ( "Expected error, got event {:?}" , ev) ,
658+ None => panic ! ( "Expected error, got None" ) ,
659+ }
660+
661+ Ok ( ( ) )
662+ }
0 commit comments