@@ -11,7 +11,7 @@ use bdk_chain::{
11
11
BlockId ,
12
12
} ;
13
13
use bdk_testenv:: { chain_update, hash, local_chain} ;
14
- use bitcoin:: { block:: Header , hashes:: Hash , BlockHash } ;
14
+ use bitcoin:: { block:: Header , hashes:: Hash , BlockHash , CompactTarget , TxMerkleNode } ;
15
15
use proptest:: prelude:: * ;
16
16
17
17
#[ derive( Debug ) ]
@@ -474,6 +474,157 @@ fn local_chain_insert_header() {
474
474
}
475
475
}
476
476
477
+ /// Validates `merge_chains` behavior on chains that contain placeholder checkpoints (`data: None`).
478
+ ///
479
+ /// Placeholders are created when a `CheckPoint`’s `prev_blockhash` references a block at a height
480
+ /// with no stored checkpoint. This test ensures `merge_chains` handles them correctly and that the
481
+ /// resulting chain never exposes a placeholder checkpoint.
482
+ #[ test]
483
+ fn merge_chains_handles_placeholders ( ) {
484
+ fn header ( prev_blockhash : bitcoin:: BlockHash , nonce : u32 ) -> Header {
485
+ Header {
486
+ version : bitcoin:: block:: Version :: default ( ) ,
487
+ prev_blockhash,
488
+ merkle_root : TxMerkleNode :: all_zeros ( ) ,
489
+ time : 0 ,
490
+ bits : CompactTarget :: default ( ) ,
491
+ nonce,
492
+ }
493
+ }
494
+
495
+ fn local_chain ( blocks : Vec < ( u32 , Header ) > ) -> LocalChain < Header > {
496
+ LocalChain :: from_blocks ( blocks. into_iter ( ) . collect :: < BTreeMap < _ , _ > > ( ) )
497
+ . expect ( "chain must have genesis block" )
498
+ }
499
+
500
+ fn update_chain ( blocks : & [ ( u32 , Header ) ] ) -> CheckPoint < Header > {
501
+ CheckPoint :: from_blocks ( blocks. iter ( ) . copied ( ) ) . expect ( "checkpoint must be valid" )
502
+ }
503
+
504
+ let a = header ( hash ! ( "genesis" ) , 0 ) ;
505
+ let b = header ( a. block_hash ( ) , 0 ) ;
506
+ let c = header ( b. block_hash ( ) , 0 ) ;
507
+ let d = header ( c. block_hash ( ) , 0 ) ;
508
+
509
+ // Set a different `nonce` for conflicting `Header`s to ensure different `BlockHash`.
510
+ let c_conflict = header ( b. block_hash ( ) , 1 ) ;
511
+ let d_conflict = header ( c_conflict. block_hash ( ) , 1 ) ;
512
+
513
+ struct TestCase {
514
+ name : & ' static str ,
515
+ updates : Vec < CheckPoint < Header > > ,
516
+ invalidate_heights : Vec < u32 > ,
517
+ expected_placeholder_heights : Vec < u32 > ,
518
+ expected_chain : LocalChain < Header > ,
519
+ }
520
+
521
+ let test_cases = [
522
+ // Test case 1: Create a placeholder for B via C.
523
+ TestCase {
524
+ name : "insert_placeholder" ,
525
+ updates : vec ! [ update_chain( & [ ( 0 , a) , ( 2 , c) ] ) ] ,
526
+ invalidate_heights : vec ! [ ] ,
527
+ expected_placeholder_heights : vec ! [ 1 ] ,
528
+ expected_chain : local_chain ( vec ! [ ( 0 , a) , ( 2 , c) ] ) ,
529
+ } ,
530
+ // Test cast 2: Create a placeholder for B via C, then update provides conflicting C'.
531
+ TestCase {
532
+ name : "conflict_at_tip_keeps_placeholder" ,
533
+ updates : vec ! [
534
+ update_chain( & [ ( 0 , a) , ( 2 , c) ] ) ,
535
+ update_chain( & [ ( 2 , c_conflict) ] ) ,
536
+ ] ,
537
+ invalidate_heights : vec ! [ ] ,
538
+ expected_placeholder_heights : vec ! [ 1 ] ,
539
+ expected_chain : local_chain ( vec ! [ ( 0 , a) , ( 1 , b) , ( 2 , c_conflict) ] ) ,
540
+ } ,
541
+ // Test case 3: Create placeholder for C via D.
542
+ TestCase {
543
+ name : "conflict_at_filled_height" ,
544
+ updates : vec ! [ update_chain( & [ ( 0 , a) , ( 3 , d) ] ) ] ,
545
+ invalidate_heights : vec ! [ ] ,
546
+ expected_placeholder_heights : vec ! [ 2 ] ,
547
+ expected_chain : local_chain ( vec ! [ ( 0 , a) , ( 3 , d) ] ) ,
548
+ } ,
549
+ // Test case 4: Create placeholder for C via D, then insert conflicting C' which should
550
+ // drop D and replace C.
551
+ TestCase {
552
+ name : "conflict_at_filled_height" ,
553
+ updates : vec ! [
554
+ update_chain( & [ ( 0 , a) , ( 3 , d) ] ) ,
555
+ update_chain( & [ ( 0 , a) , ( 2 , c_conflict) ] ) ,
556
+ ] ,
557
+ invalidate_heights : vec ! [ ] ,
558
+ expected_placeholder_heights : vec ! [ ] ,
559
+ expected_chain : local_chain ( vec ! [ ( 0 , a) , ( 2 , c_conflict) ] ) ,
560
+ } ,
561
+ // Test case 5: Create placeholder for B via C, then invalidate C.
562
+ TestCase {
563
+ name : "invalidate_tip_falls_back" ,
564
+ updates : vec ! [ update_chain( & [ ( 0 , a) , ( 2 , c) ] ) ] ,
565
+ invalidate_heights : vec ! [ 2 ] ,
566
+ expected_placeholder_heights : vec ! [ 1 ] ,
567
+ expected_chain : local_chain ( vec ! [ ( 0 , a) ] ) ,
568
+ } ,
569
+ // Test case 6: Create placeholder for C via D, then insert D' which has `prev_blockhash`
570
+ // that does not point to C. TODO: Handle error?
571
+ TestCase {
572
+ name : "expected_error" ,
573
+ updates : vec ! [
574
+ update_chain( & [ ( 0 , a) , ( 3 , d) ] ) ,
575
+ update_chain( & [ ( 3 , d_conflict) ] ) ,
576
+ ] ,
577
+ invalidate_heights : vec ! [ ] ,
578
+ expected_placeholder_heights : vec ! [ ] ,
579
+ expected_chain : local_chain ( vec ! [ ( 0 , a) , ( 3 , d) ] ) ,
580
+ } ,
581
+ ] ;
582
+
583
+ for ( i, t) in test_cases. into_iter ( ) . enumerate ( ) {
584
+ let mut chain = local_chain ( vec ! [ ( 0 , a) ] ) ;
585
+ for upd in t. updates {
586
+ // If `apply_update` errors, it is because the new chain cannot be merged. So it should
587
+ // follow that this validates behavior if the final `expected_chain` state is correct.
588
+ if chain. apply_update ( upd) . is_ok ( ) {
589
+ for & height in & t. expected_placeholder_heights {
590
+ let has_placeholder = chain
591
+ . tip ( )
592
+ . iter ( )
593
+ . any ( |cp| cp. height ( ) == height && cp. data_ref ( ) . is_none ( ) ) ;
594
+ assert ! (
595
+ has_placeholder,
596
+ "[{}] {}: expected placeholder at height {}" ,
597
+ i, t. name, height
598
+ ) ;
599
+ }
600
+
601
+ if !t. invalidate_heights . is_empty ( ) {
602
+ let cs: ChangeSet < Header > = t
603
+ . invalidate_heights
604
+ . iter ( )
605
+ . copied ( )
606
+ . map ( |h| ( h, None ) )
607
+ . collect ( ) ;
608
+ chain. apply_changeset ( & cs) . expect ( "changeset should apply" ) ;
609
+ }
610
+
611
+ // Ensure we never end up with a placeholder tip.
612
+ assert ! (
613
+ chain. tip( ) . data_ref( ) . is_some( ) ,
614
+ "[{}] {}: tip must always be materialized" ,
615
+ i,
616
+ t. name
617
+ ) ;
618
+ }
619
+ }
620
+ assert_eq ! (
621
+ chain, t. expected_chain,
622
+ "[{}] {}: unexpected final chain" ,
623
+ i, t. name
624
+ ) ;
625
+ }
626
+ }
627
+
477
628
#[ test]
478
629
fn local_chain_disconnect_from ( ) {
479
630
struct TestCase {
0 commit comments