@@ -6,8 +6,9 @@ use core::ops::RangeBounds;
66
77use crate :: collections:: BTreeMap ;
88use crate :: { BlockId , ChainOracle , Merge } ;
9- use bdk_core :: ToBlockHash ;
9+ use alloc :: vec :: Vec ;
1010pub use bdk_core:: { CheckPoint , CheckPointIter } ;
11+ use bdk_core:: { CheckPointEntry , ToBlockHash } ;
1112use bitcoin:: block:: Header ;
1213use bitcoin:: BlockHash ;
1314
@@ -69,7 +70,10 @@ impl<D> PartialEq for LocalChain<D> {
6970 }
7071}
7172
72- impl < D > ChainOracle for LocalChain < D > {
73+ impl < D > ChainOracle for LocalChain < D >
74+ where
75+ D : ToBlockHash + Copy ,
76+ {
7377 type Error = Infallible ;
7478
7579 fn is_block_in_chain (
@@ -83,10 +87,18 @@ impl<D> ChainOracle for LocalChain<D> {
8387 Some ( cp) if cp. hash ( ) == chain_tip. hash => cp,
8488 _ => return Ok ( None ) ,
8589 } ;
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 ) ) ;
8993 }
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 )
90102 }
91103
92104 fn get_chain_tip ( & self ) -> Result < BlockId , Self :: Error > {
@@ -576,6 +588,34 @@ impl core::fmt::Display for ApplyHeaderError {
576588#[ cfg( feature = "std" ) ]
577589impl std:: error:: Error for ApplyHeaderError { }
578590
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+
579619/// Applies `update_tip` onto `original_tip`.
580620///
581621/// On success, a tuple is returned ([`CheckPoint`], [`ChangeSet`]).
@@ -598,11 +638,16 @@ where
598638{
599639 let mut changeset = ChangeSet :: < D > :: default ( ) ;
600640
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 ( ) ;
603647
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 ( ) ;
606651
607652 let mut prev_orig: Option < CheckPoint < D > > = None ;
608653 let mut prev_update: Option < CheckPoint < D > > = None ;
@@ -617,43 +662,55 @@ where
617662 // in multiple locations to keep the same `Arc` pointers when they are being updated from each
618663 // other using this function. We can do this as long as the update contains every
619664 // 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) ;
633668
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) {
635674 // 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;
639681 }
640682 // 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;
653690 }
654691 }
692+ // Both chains have entries at this height.
655693 ( 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 {
657714 // We have found our point of agreement 🎉 -- we require that the previous (i.e.
658715 // higher because we are iterating backwards) block in the original chain was
659716 // invalidated (if it exists). This ensures that there is an unambiguous point
@@ -671,34 +728,75 @@ where
671728 prev_orig_was_invalidated = false ;
672729 // OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we
673730 // 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+ }
683756 }
684757 }
685758 } 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+ }
691793 }
692- prev_orig_was_invalidated = true ;
693794 }
694- prev_update = curr_update. take ( ) ;
695- prev_orig = curr_orig. take ( ) ;
795+ prev_update = curr_update;
796+ prev_orig = curr_orig;
696797 }
697798 ( 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" )
702800 }
703801 }
704802 }
0 commit comments