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,16 +391,170 @@ 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"
401+ ) ;
402+
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"
396408 ) ;
397409
398410 Ok ( ( ) )
399411}
400412
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+
555+ Ok ( ( ) )
556+ }
557+
401558// Test that while a reorg is detected we delay incrementing the best height
402559#[ test]
403560fn repeat_reorgs ( ) -> anyhow:: Result < ( ) > {
0 commit comments