1
1
use bdk_bitcoind_rpc:: bip158:: { Event , EventInner , FilterIter } ;
2
2
use bdk_core:: { BlockId , CheckPoint } ;
3
+ use bdk_testenv:: bitcoincore_rpc:: bitcoincore_rpc_json:: CreateRawTransactionInput ;
3
4
use bdk_testenv:: { anyhow, bitcoind, block_id, TestEnv } ;
4
5
use bitcoin:: { constants, Address , Amount , Network , ScriptBuf } ;
5
6
use bitcoincore_rpc:: RpcApi ;
@@ -198,7 +199,6 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
198
199
// later by a reorg.
199
200
let unspent = client. list_unspent ( None , None , None , None , None ) ?;
200
201
assert ! ( unspent. len( ) >= 2 ) ;
201
- use bdk_testenv:: bitcoincore_rpc:: bitcoincore_rpc_json:: CreateRawTransactionInput ;
202
202
let unspent_1 = & unspent[ 0 ] ;
203
203
let unspent_2 = & unspent[ 1 ] ;
204
204
let utxo_1 = CreateRawTransactionInput {
@@ -267,8 +267,12 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
267
267
// 5. Instantiate FilterIter at start height 104
268
268
println ! ( "STEP: Instantiating FilterIter" ) ;
269
269
// 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) ;
272
276
iter. add_spk ( spk_to_watch. clone ( ) ) ;
273
277
let initial_tip = iter. get_tip ( ) ?. expect ( "Should get initial tip" ) ;
274
278
assert_eq ! ( initial_tip. height, 105 ) ;
@@ -277,14 +281,13 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
277
281
// 6. Iterate once processing block A
278
282
println ! ( "STEP: Iterating once (original block A)" ) ;
279
283
let event_a = iter. next ( ) . expect ( "Iterator should have item A" ) ?;
280
- // println!("First event: {:?}", event_a);
281
284
match event_a {
282
285
Event :: Block ( EventInner { height, block } ) => {
283
286
assert_eq ! ( height, 104 ) ;
284
287
assert_eq ! ( block. block_hash( ) , hash_104) ;
285
288
assert ! ( block. txdata. iter( ) . any( |tx| tx. compute_txid( ) == txid_a) ) ;
286
289
}
287
- _ => panic ! ( "Expected relevant tx at block A 102 " ) ,
290
+ _ => panic ! ( "Expected relevant tx at block A 104 " ) ,
288
291
}
289
292
290
293
// 7. Simulate Reorg (Invalidate blocks B and A)
@@ -388,16 +391,170 @@ fn filter_iter_handles_reorg() -> anyhow::Result<()> {
388
391
}
389
392
390
393
// 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"
396
408
) ;
397
409
398
410
Ok ( ( ) )
399
411
}
400
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
+
555
+ Ok ( ( ) )
556
+ }
557
+
401
558
// Test that while a reorg is detected we delay incrementing the best height
402
559
#[ test]
403
560
fn repeat_reorgs ( ) -> anyhow:: Result < ( ) > {
0 commit comments