@@ -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,160 @@ 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
+ let e = header ( d. block_hash ( ) , 0 ) ;
509
+
510
+ // Set a different `nonce` for conflicting `Header`s to ensure different `BlockHash`.
511
+ let c_conflict = header ( b. block_hash ( ) , 1 ) ;
512
+ let d_conflict = header ( c_conflict. block_hash ( ) , 1 ) ;
513
+
514
+ struct TestCase {
515
+ name : & ' static str ,
516
+ updates : Vec < CheckPoint < Header > > ,
517
+ invalidate_heights : Vec < u32 > ,
518
+ expected_placeholder_heights : Vec < u32 > ,
519
+ expected_chain : LocalChain < Header > ,
520
+ }
521
+
522
+ let test_cases = [
523
+ // Test case 1: Create a placeholder for B via C and a placeholder for D via E.
524
+ TestCase {
525
+ name : "insert_placeholder" ,
526
+ updates : vec ! [ update_chain( & [ ( 0 , a) , ( 2 , c) , ( 4 , e) ] ) ] ,
527
+ invalidate_heights : vec ! [ ] ,
528
+ expected_placeholder_heights : vec ! [ 1 , 3 ] ,
529
+ expected_chain : local_chain ( vec ! [ ( 0 , a) , ( 2 , c) , ( 4 , e) ] ) ,
530
+ } ,
531
+ // Test cast 2: Create a placeholder for B via C, then update provides conflicting C'.
532
+ TestCase {
533
+ name : "conflict_at_tip_keeps_placeholder" ,
534
+ updates : vec ! [
535
+ update_chain( & [ ( 0 , a) , ( 2 , c) ] ) ,
536
+ update_chain( & [ ( 2 , c_conflict) ] ) ,
537
+ ] ,
538
+ invalidate_heights : vec ! [ ] ,
539
+ expected_placeholder_heights : vec ! [ 1 ] ,
540
+ expected_chain : local_chain ( vec ! [ ( 0 , a) , ( 1 , b) , ( 2 , c_conflict) ] ) ,
541
+ } ,
542
+ // Test case 3: Create placeholder for C via D.
543
+ TestCase {
544
+ name : "conflict_at_filled_height" ,
545
+ updates : vec ! [ update_chain( & [ ( 0 , a) , ( 3 , d) ] ) ] ,
546
+ invalidate_heights : vec ! [ ] ,
547
+ expected_placeholder_heights : vec ! [ 2 ] ,
548
+ expected_chain : local_chain ( vec ! [ ( 0 , a) , ( 3 , d) ] ) ,
549
+ } ,
550
+ // Test case 4: Create placeholder for C via D, then insert conflicting C' which should
551
+ // drop D and replace C.
552
+ TestCase {
553
+ name : "conflict_at_filled_height" ,
554
+ updates : vec ! [
555
+ update_chain( & [ ( 0 , a) , ( 3 , d) ] ) ,
556
+ update_chain( & [ ( 0 , a) , ( 2 , c_conflict) ] ) ,
557
+ ] ,
558
+ invalidate_heights : vec ! [ ] ,
559
+ expected_placeholder_heights : vec ! [ 1 ] ,
560
+ expected_chain : local_chain ( vec ! [ ( 0 , a) , ( 2 , c_conflict) ] ) ,
561
+ } ,
562
+ // Test case 5: Create placeholder for B via C, then invalidate C.
563
+ TestCase {
564
+ name : "invalidate_tip_falls_back" ,
565
+ updates : vec ! [ update_chain( & [ ( 0 , a) , ( 2 , c) ] ) ] ,
566
+ invalidate_heights : vec ! [ 2 ] ,
567
+ expected_placeholder_heights : vec ! [ ] ,
568
+ expected_chain : local_chain ( vec ! [ ( 0 , a) ] ) ,
569
+ } ,
570
+ // Test case 6: Create placeholder for C via D, then insert D' which has `prev_blockhash`
571
+ // that does not point to C. TODO: Handle error?
572
+ TestCase {
573
+ name : "expected_error" ,
574
+ updates : vec ! [
575
+ update_chain( & [ ( 0 , a) , ( 3 , d) ] ) ,
576
+ update_chain( & [ ( 3 , d_conflict) ] ) ,
577
+ ] ,
578
+ invalidate_heights : vec ! [ ] ,
579
+ expected_placeholder_heights : vec ! [ 2 ] ,
580
+ expected_chain : local_chain ( vec ! [ ( 0 , a) , ( 3 , d) ] ) ,
581
+ } ,
582
+ ] ;
583
+
584
+ for ( i, t) in test_cases. into_iter ( ) . enumerate ( ) {
585
+ let mut chain = local_chain ( vec ! [ ( 0 , a) ] ) ;
586
+ for upd in t. updates {
587
+ // If `apply_update` errors, it is because the new chain cannot be merged. So it should
588
+ // follow that this validates behavior if the final `expected_chain` state is correct.
589
+ if chain. apply_update ( upd) . is_ok ( ) {
590
+ if !t. invalidate_heights . is_empty ( ) {
591
+ let cs: ChangeSet < Header > = t
592
+ . invalidate_heights
593
+ . iter ( )
594
+ . copied ( )
595
+ . map ( |h| ( h, None ) )
596
+ . collect ( ) ;
597
+ chain. apply_changeset ( & cs) . expect ( "changeset should apply" ) ;
598
+ }
599
+
600
+ // Ensure we never end up with a placeholder tip.
601
+ assert ! (
602
+ chain. tip( ) . data_ref( ) . is_some( ) ,
603
+ "[{}] {}: tip must always be materialized" ,
604
+ i,
605
+ t. name
606
+ ) ;
607
+ }
608
+ }
609
+
610
+ let mut placeholder_heights = chain
611
+ . tip ( )
612
+ . iter ( )
613
+ . filter ( |cp| cp. data_ref ( ) . is_none ( ) )
614
+ . map ( |cp| cp. height ( ) )
615
+ . collect :: < Vec < _ > > ( ) ;
616
+ placeholder_heights. sort ( ) ;
617
+ assert_eq ! (
618
+ placeholder_heights, t. expected_placeholder_heights,
619
+ "[{}] {}: placeholder height mismatch" ,
620
+ i, t. name
621
+ ) ;
622
+
623
+ assert_eq ! (
624
+ chain, t. expected_chain,
625
+ "[{}] {}: unexpected final chain" ,
626
+ i, t. name
627
+ ) ;
628
+ }
629
+ }
630
+
477
631
#[ test]
478
632
fn local_chain_disconnect_from ( ) {
479
633
struct TestCase {
0 commit comments