Skip to content

Commit 173cb46

Browse files
committed
feat(chain)!: add CheckPointEntry
1 parent faf520d commit 173cb46

File tree

2 files changed

+229
-60
lines changed

2 files changed

+229
-60
lines changed

crates/chain/src/local_chain.rs

Lines changed: 158 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ use core::ops::RangeBounds;
66

77
use crate::collections::BTreeMap;
88
use crate::{BlockId, ChainOracle, Merge};
9-
use bdk_core::ToBlockHash;
9+
use alloc::vec::Vec;
1010
pub use bdk_core::{CheckPoint, CheckPointIter};
11+
use bdk_core::{CheckPointEntry, ToBlockHash};
1112
use bitcoin::block::Header;
1213
use 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")]
577589
impl 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
}

crates/core/src/checkpoint.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,44 @@ use bitcoin::{block::Header, BlockHash};
66

77
use crate::BlockId;
88

9+
/// Internal type to represent entries in `CheckPoint` that can handle both actual checkpoint data
10+
/// and placeholder entries for types that have `prev_blockhash`.
11+
#[derive(Debug, Clone, PartialEq)]
12+
pub enum CheckPointEntry<D> {
13+
/// An actual `CheckPoint` entry with `data`.
14+
CheckPoint(D),
15+
/// A placeholder entry containing only a `prev_blockhash`.
16+
PrevBlockHash(BlockHash),
17+
}
18+
19+
impl<D> CheckPointEntry<D> {
20+
/// Returns true if this entry is a placeholder entry.
21+
pub fn is_prev_blockhash(&self) -> bool {
22+
matches!(self, CheckPointEntry::PrevBlockHash(_))
23+
}
24+
25+
/// Returns true if this entry contains actual `CheckPoint` data.
26+
pub fn is_checkpoint(&self) -> bool {
27+
matches!(self, CheckPointEntry::CheckPoint(_))
28+
}
29+
30+
/// Return the `data` if this is a `CheckPoint` entry.
31+
pub fn checkpoint_data(&self) -> Option<&D> {
32+
match self {
33+
CheckPointEntry::CheckPoint(data) => Some(data),
34+
CheckPointEntry::PrevBlockHash(_) => None,
35+
}
36+
}
37+
38+
/// Return the `prev_blockhash` if this is a placeholder entry.
39+
pub fn prev_blockhash(&self) -> Option<BlockHash> {
40+
match self {
41+
CheckPointEntry::CheckPoint(_) => None,
42+
CheckPointEntry::PrevBlockHash(hash) => Some(*hash),
43+
}
44+
}
45+
}
46+
947
/// A checkpoint is a node of a reference-counted linked list of [`BlockId`]s.
1048
///
1149
/// Checkpoints are cheaply cloneable and are useful to find the agreement point between two sparse
@@ -68,6 +106,11 @@ impl<D> Drop for CPInner<D> {
68106
pub trait ToBlockHash {
69107
/// Returns the [`BlockHash`] for the associated [`CheckPoint`] `data` type.
70108
fn to_blockhash(&self) -> BlockHash;
109+
110+
/// Returns `None` if the type has no knowledge of the previous [`BlockHash`].
111+
fn prev_blockhash(&self) -> Option<BlockHash> {
112+
None
113+
}
71114
}
72115

73116
impl ToBlockHash for BlockHash {
@@ -80,6 +123,26 @@ impl ToBlockHash for Header {
80123
fn to_blockhash(&self) -> BlockHash {
81124
self.block_hash()
82125
}
126+
127+
fn prev_blockhash(&self) -> Option<BlockHash> {
128+
Some(self.prev_blockhash)
129+
}
130+
}
131+
132+
impl<D: ToBlockHash> ToBlockHash for CheckPointEntry<D> {
133+
fn to_blockhash(&self) -> BlockHash {
134+
match self {
135+
CheckPointEntry::CheckPoint(data) => data.to_blockhash(),
136+
CheckPointEntry::PrevBlockHash(hash) => *hash,
137+
}
138+
}
139+
140+
fn prev_blockhash(&self) -> Option<BlockHash> {
141+
match self {
142+
CheckPointEntry::CheckPoint(data) => data.prev_blockhash(),
143+
CheckPointEntry::PrevBlockHash(_) => None,
144+
}
145+
}
83146
}
84147

85148
impl<D> PartialEq for CheckPoint<D> {
@@ -188,6 +251,14 @@ impl<D> CheckPoint<D> {
188251
pub fn eq_ptr(&self, other: &Self) -> bool {
189252
Arc::as_ptr(&self.0) == Arc::as_ptr(&other.0)
190253
}
254+
255+
/// Return the `prev_blockhash` from the `CheckPoint`, if available.
256+
pub fn prev_blockhash(&self) -> Option<BlockHash>
257+
where
258+
D: ToBlockHash,
259+
{
260+
self.0.data.prev_blockhash()
261+
}
191262
}
192263

193264
// Methods where `D: ToBlockHash`

0 commit comments

Comments
 (0)