@@ -6,8 +6,9 @@ use core::ops::RangeBounds;
6
6
7
7
use crate :: collections:: BTreeMap ;
8
8
use crate :: { BlockId , ChainOracle , Merge } ;
9
- use bdk_core :: ToBlockHash ;
9
+ use alloc :: vec :: Vec ;
10
10
pub use bdk_core:: { CheckPoint , CheckPointIter } ;
11
+ use bdk_core:: { CheckPointEntry , ToBlockHash } ;
11
12
use bitcoin:: block:: Header ;
12
13
use bitcoin:: BlockHash ;
13
14
@@ -69,7 +70,10 @@ impl<D> PartialEq for LocalChain<D> {
69
70
}
70
71
}
71
72
72
- impl < D > ChainOracle for LocalChain < D > {
73
+ impl < D > ChainOracle for LocalChain < D >
74
+ where
75
+ D : ToBlockHash + Copy ,
76
+ {
73
77
type Error = Infallible ;
74
78
75
79
fn is_block_in_chain (
@@ -83,10 +87,18 @@ impl<D> ChainOracle for LocalChain<D> {
83
87
Some ( cp) if cp. hash ( ) == chain_tip. hash => cp,
84
88
_ => return Ok ( None ) ,
85
89
} ;
86
- match chain_tip_cp . get ( block . height ) {
87
- Some ( cp) => Ok ( Some ( cp . hash ( ) == block. hash ) ) ,
88
- None => Ok ( None ) ,
90
+
91
+ if let Some ( cp) = chain_tip_cp . get ( block. height ) {
92
+ return Ok ( Some ( cp . hash ( ) == block . hash ) ) ;
89
93
}
94
+
95
+ if let Some ( next_cp) = chain_tip_cp. get ( block. height . saturating_add ( 1 ) ) {
96
+ if let Some ( prev_hash) = next_cp. prev_blockhash ( ) {
97
+ return Ok ( Some ( prev_hash == block. hash ) ) ;
98
+ }
99
+ }
100
+
101
+ Ok ( None )
90
102
}
91
103
92
104
fn get_chain_tip ( & self ) -> Result < BlockId , Self :: Error > {
@@ -576,6 +588,34 @@ impl core::fmt::Display for ApplyHeaderError {
576
588
#[ cfg( feature = "std" ) ]
577
589
impl std:: error:: Error for ApplyHeaderError { }
578
590
591
+ /// Convert a `CheckPoint` to a `CheckPointEntry` representation that includes `prev_blockhash`.
592
+ ///
593
+ /// NOTE: We store a block's prev_blockhash under its parent's height `(h - 1)`, so a PrevBlockHash
594
+ /// at height `h` and the hash at height `h` are the same.
595
+ fn checkpoint_to_entries < D > ( checkpoint : CheckPoint < D > ) -> BTreeMap < u32 , CheckPointEntry < D > >
596
+ where
597
+ D : ToBlockHash + Copy ,
598
+ {
599
+ let mut entries = BTreeMap :: new ( ) ;
600
+
601
+ for cp in checkpoint. iter ( ) {
602
+ // Add the actual `CheckPoint`.
603
+ entries. insert ( cp. height ( ) , CheckPointEntry :: CheckPoint ( cp. data ( ) ) ) ;
604
+
605
+ // Add prev_blockhash as a placeholder, if available.
606
+ if let Some ( prev_hash) = cp. prev_blockhash ( ) {
607
+ if let Some ( prev_height) = cp. height ( ) . checked_sub ( 1 ) {
608
+ // Only add if we don't already have data at that height.
609
+ entries
610
+ . entry ( prev_height)
611
+ . or_insert ( CheckPointEntry :: PrevBlockHash ( prev_hash) ) ;
612
+ }
613
+ }
614
+ }
615
+
616
+ entries
617
+ }
618
+
579
619
/// Applies `update_tip` onto `original_tip`.
580
620
///
581
621
/// On success, a tuple is returned ([`CheckPoint`], [`ChangeSet`]).
@@ -598,11 +638,16 @@ where
598
638
{
599
639
let mut changeset = ChangeSet :: < D > :: default ( ) ;
600
640
601
- let mut orig = original_tip. iter ( ) ;
602
- let mut update = update_tip. iter ( ) ;
641
+ // Convert to internal representation that includes prev_blockhash information
642
+ let orig = checkpoint_to_entries ( original_tip. clone ( ) ) ;
643
+ let update = checkpoint_to_entries ( update_tip. clone ( ) ) ;
644
+
645
+ // Collect all heights from both chains to iterate through
646
+ let mut all_heights: Vec < u32 > = orig. keys ( ) . chain ( update. keys ( ) ) . copied ( ) . collect ( ) ;
603
647
604
- let mut curr_orig = None ;
605
- let mut curr_update = None ;
648
+ // Sort in descending order.
649
+ all_heights. sort_by ( |a, b| b. cmp ( a) ) ;
650
+ all_heights. dedup ( ) ;
606
651
607
652
let mut prev_orig: Option < CheckPoint < D > > = None ;
608
653
let mut prev_update: Option < CheckPoint < D > > = None ;
@@ -617,43 +662,55 @@ where
617
662
// in multiple locations to keep the same `Arc` pointers when they are being updated from each
618
663
// other using this function. We can do this as long as the update contains every
619
664
// block's height of the original chain.
620
- let mut is_update_height_superset_of_original = true ;
621
-
622
- // To find the difference between the new chain and the original we iterate over both of them
623
- // from the tip backwards in tandem. We are always dealing with the highest one from either
624
- // chain first and move to the next highest. The crucial logic is applied when they have
625
- // blocks at the same height.
626
- loop {
627
- if curr_orig. is_none ( ) {
628
- curr_orig = orig. next ( ) ;
629
- }
630
- if curr_update. is_none ( ) {
631
- curr_update = update. next ( ) ;
632
- }
665
+ for height in all_heights {
666
+ let orig_entry = orig. get ( & height) ;
667
+ let update_entry = update. get ( & height) ;
633
668
634
- match ( curr_orig. as_ref ( ) , curr_update. as_ref ( ) ) {
669
+ // Get actual `CheckPoint` references.
670
+ let curr_orig = original_tip. get ( height) ;
671
+ let curr_update = update_tip. get ( height) ;
672
+
673
+ match ( orig_entry, update_entry) {
635
674
// Update block that doesn't exist in the original chain
636
- ( o, Some ( u) ) if Some ( u. height ( ) ) > o. map ( |o| o. height ( ) ) => {
637
- changeset. blocks . insert ( u. height ( ) , Some ( u. data ( ) ) ) ;
638
- prev_update = curr_update. take ( ) ;
675
+ ( None , Some ( _u) ) => {
676
+ // Only add actual checkpoint data, not placeholders.
677
+ if let Some ( update_cp) = & curr_update {
678
+ changeset. blocks . insert ( height, Some ( update_cp. data ( ) ) ) ;
679
+ }
680
+ prev_update = curr_update;
639
681
}
640
682
// Original block that isn't in the update
641
- ( Some ( o) , u) if Some ( o. height ( ) ) > u. map ( |u| u. height ( ) ) => {
642
- // this block might be gone if an earlier block gets invalidated
643
- potentially_invalidated_heights. push ( o. height ( ) ) ;
644
- prev_orig_was_invalidated = false ;
645
- prev_orig = curr_orig. take ( ) ;
646
-
647
- is_update_height_superset_of_original = false ;
648
-
649
- // OPTIMIZATION: we have run out of update blocks so we don't need to continue
650
- // iterating because there's no possibility of adding anything to changeset.
651
- if u. is_none ( ) {
652
- break ;
683
+ ( Some ( o) , None ) => {
684
+ // Only consider real checkpoints for invalidation, not placeholders.
685
+ if let CheckPointEntry :: CheckPoint ( _) = o {
686
+ // this block might be gone if an earlier block gets invalidated
687
+ potentially_invalidated_heights. push ( height) ;
688
+ prev_orig_was_invalidated = false ;
689
+ prev_orig = curr_orig;
653
690
}
654
691
}
692
+ // Both chains have entries at this height.
655
693
( Some ( o) , Some ( u) ) => {
656
- if o. hash ( ) == u. hash ( ) {
694
+ // Check for agreement considering both checkpoint data and prev_blockhash.
695
+ let entries_match = match ( o, u) {
696
+ // Both are actual checkpoints.
697
+ (
698
+ CheckPointEntry :: CheckPoint ( orig_data) ,
699
+ CheckPointEntry :: CheckPoint ( update_data) ,
700
+ ) => orig_data. to_blockhash ( ) == update_data. to_blockhash ( ) ,
701
+ // One is checkpoint, other is prev_blockhash: check if they match.
702
+ ( CheckPointEntry :: CheckPoint ( data) , CheckPointEntry :: PrevBlockHash ( hash) )
703
+ | ( CheckPointEntry :: PrevBlockHash ( hash) , CheckPointEntry :: CheckPoint ( data) ) => {
704
+ data. to_blockhash ( ) == * hash
705
+ }
706
+ // Both are prev_blockhash: check if they match.
707
+ (
708
+ CheckPointEntry :: PrevBlockHash ( hash1) ,
709
+ CheckPointEntry :: PrevBlockHash ( hash2) ,
710
+ ) => hash1 == hash2,
711
+ } ;
712
+
713
+ if entries_match {
657
714
// We have found our point of agreement 🎉 -- we require that the previous (i.e.
658
715
// higher because we are iterating backwards) block in the original chain was
659
716
// invalidated (if it exists). This ensures that there is an unambiguous point
@@ -671,34 +728,75 @@ where
671
728
prev_orig_was_invalidated = false ;
672
729
// OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we
673
730
// can guarantee that no older blocks are introduced.
674
- if o. eq_ptr ( u) {
675
- if is_update_height_superset_of_original {
676
- return Ok ( ( update_tip, changeset) ) ;
677
- } else {
678
- let new_tip = apply_changeset_to_checkpoint ( original_tip, & changeset)
679
- . map_err ( |_| CannotConnectError {
680
- try_include_height : 0 ,
681
- } ) ?;
682
- return Ok ( ( new_tip, changeset) ) ;
731
+ if let ( Some ( o) , Some ( u) ) = ( & curr_orig, & curr_update) {
732
+ if o. eq_ptr ( u) {
733
+ // Check if update contains every real checkpoint height from original.
734
+ let is_update_height_superset_of_original = orig
735
+ . iter ( )
736
+ . filter ( |( _, entry) | {
737
+ matches ! ( entry, CheckPointEntry :: CheckPoint ( _) )
738
+ } )
739
+ . all ( |( height, _) | {
740
+ matches ! (
741
+ update. get( height) ,
742
+ Some ( CheckPointEntry :: CheckPoint ( _) )
743
+ )
744
+ } ) ;
745
+
746
+ if is_update_height_superset_of_original {
747
+ return Ok ( ( update_tip, changeset) ) ;
748
+ } else {
749
+ let new_tip =
750
+ apply_changeset_to_checkpoint ( original_tip, & changeset)
751
+ . map_err ( |_| CannotConnectError {
752
+ try_include_height : 0 ,
753
+ } ) ?;
754
+ return Ok ( ( new_tip, changeset) ) ;
755
+ }
683
756
}
684
757
}
685
758
} else {
686
- // We have an invalidation height so we set the height to the updated hash and
687
- // also purge all the original chain block hashes above this block.
688
- changeset. blocks . insert ( u. height ( ) , Some ( u. data ( ) ) ) ;
689
- for invalidated_height in potentially_invalidated_heights. drain ( ..) {
690
- changeset. blocks . insert ( invalidated_height, None ) ;
759
+ // Entries differ: check for ambiguity cases.
760
+ match ( o, u) {
761
+ // We have an invalidation height so we set the height to the updated hash
762
+ // and also purge all the original chain block hashes above this block.
763
+ ( CheckPointEntry :: CheckPoint ( _) , CheckPointEntry :: CheckPoint ( _) ) => {
764
+ if let Some ( update_cp) = & curr_update {
765
+ changeset. blocks . insert ( height, Some ( update_cp. data ( ) ) ) ;
766
+ }
767
+ for invalidated_height in potentially_invalidated_heights. drain ( ..) {
768
+ changeset. blocks . insert ( invalidated_height, None ) ;
769
+ }
770
+ prev_orig_was_invalidated = true ;
771
+ }
772
+ // Original has checkpoint, update only has prev_blockhash.
773
+ ( CheckPointEntry :: CheckPoint ( _) , CheckPointEntry :: PrevBlockHash ( _) ) => {
774
+ return Err ( CannotConnectError {
775
+ try_include_height : height,
776
+ } ) ;
777
+ }
778
+ // Both sides only have prev_blockhash but the two inferred predecessors
779
+ // disagree.
780
+ ( CheckPointEntry :: PrevBlockHash ( _) , CheckPointEntry :: PrevBlockHash ( _) ) => {
781
+ return Err ( CannotConnectError {
782
+ try_include_height : height,
783
+ } ) ;
784
+ }
785
+ // Original only has prev_blockhash, update has checkpoint. Set the height
786
+ // to the updated hash, but do not purge earlier heights since a
787
+ // prev_blockhash is not a hard anchor.
788
+ ( CheckPointEntry :: PrevBlockHash ( _) , CheckPointEntry :: CheckPoint ( _) ) => {
789
+ if let Some ( update_cp) = & curr_update {
790
+ changeset. blocks . insert ( height, Some ( update_cp. data ( ) ) ) ;
791
+ }
792
+ }
691
793
}
692
- prev_orig_was_invalidated = true ;
693
794
}
694
- prev_update = curr_update. take ( ) ;
695
- prev_orig = curr_orig. take ( ) ;
795
+ prev_update = curr_update;
796
+ prev_orig = curr_orig;
696
797
}
697
798
( None , None ) => {
698
- break ;
699
- }
700
- _ => {
701
- unreachable ! ( "compiler cannot tell that everything has been covered" )
799
+ unreachable ! ( "height should exist in at least one map" )
702
800
}
703
801
}
704
802
}
0 commit comments