@@ -11,7 +11,7 @@ use bdk_chain::{
1111 BlockId ,
1212} ;
1313use 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 } ;
1515use proptest:: prelude:: * ;
1616
1717#[ derive( Debug ) ]
@@ -474,6 +474,160 @@ fn local_chain_insert_header() {
474474 }
475475}
476476
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+
477631#[ test]
478632fn local_chain_disconnect_from ( ) {
479633 struct TestCase {
0 commit comments