Skip to content

Commit 95b86f9

Browse files
committed
feat(chain)!: add CheckPointEntry
1 parent faf520d commit 95b86f9

File tree

2 files changed

+225
-39
lines changed

2 files changed

+225
-39
lines changed

crates/chain/src/local_chain.rs

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

77
use crate::collections::BTreeMap;
88
use crate::{BlockId, ChainOracle, Merge};
9-
use bdk_core::ToBlockHash;
109
pub use bdk_core::{CheckPoint, CheckPointIter};
10+
use bdk_core::{CheckPointEntry, ToBlockHash};
1111
use bitcoin::block::Header;
1212
use bitcoin::BlockHash;
1313

@@ -69,7 +69,10 @@ impl<D> PartialEq for LocalChain<D> {
6969
}
7070
}
7171

72-
impl<D> ChainOracle for LocalChain<D> {
72+
impl<D> ChainOracle for LocalChain<D>
73+
where
74+
D: ToBlockHash + Copy,
75+
{
7376
type Error = Infallible;
7477

7578
fn is_block_in_chain(
@@ -83,10 +86,18 @@ impl<D> ChainOracle for LocalChain<D> {
8386
Some(cp) if cp.hash() == chain_tip.hash => cp,
8487
_ => return Ok(None),
8588
};
86-
match chain_tip_cp.get(block.height) {
87-
Some(cp) => Ok(Some(cp.hash() == block.hash)),
88-
None => Ok(None),
89+
90+
if let Some(cp) = chain_tip_cp.get(block.height) {
91+
return Ok(Some(cp.hash() == block.hash));
8992
}
93+
94+
if let Some(next_cp) = chain_tip_cp.get(block.height.saturating_add(1)) {
95+
if let Some(prev_hash) = next_cp.prev_blockhash() {
96+
return Ok(Some(prev_hash == block.hash));
97+
}
98+
}
99+
100+
Ok(None)
90101
}
91102

92103
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
@@ -653,59 +664,132 @@ where
653664
}
654665
}
655666
(Some(o), Some(u)) => {
656-
if o.hash() == u.hash() {
657-
// We have found our point of agreement 🎉 -- we require that the previous (i.e.
658-
// higher because we are iterating backwards) block in the original chain was
659-
// invalidated (if it exists). This ensures that there is an unambiguous point
660-
// of connection to the original chain from the update chain
661-
// (i.e. we know the precisely which original blocks are
662-
// invalid).
663-
if !prev_orig_was_invalidated && !point_of_agreement_found {
664-
if let (Some(prev_orig), Some(_prev_update)) = (&prev_orig, &prev_update) {
667+
if o.height() == u.height() {
668+
if o.hash() == u.hash() {
669+
// We have found our point of agreement 🎉 -- we require that the previous
670+
// (i.e. higher because we are iterating backwards) block in the original
671+
// chain was invalidated (if it exists). This ensures that there is an
672+
// unambiguous point of connection to the original chain from the update
673+
// chain (i.e. we know the precisely which original blocks are invalid).
674+
if !prev_orig_was_invalidated && !point_of_agreement_found {
675+
if let (Some(prev_orig), Some(_prev_update)) =
676+
(&prev_orig, &prev_update)
677+
{
678+
return Err(CannotConnectError {
679+
try_include_height: prev_orig.height(),
680+
});
681+
}
682+
}
683+
point_of_agreement_found = true;
684+
prev_orig_was_invalidated = false;
685+
686+
// OPTIMIZATION 2 -- if we have the same underlying pointer at this point,
687+
// we can guarantee that no older blocks are introduced.
688+
if o.eq_ptr(u) {
689+
if is_update_height_superset_of_original {
690+
return Ok((update_tip, changeset));
691+
} else {
692+
let new_tip =
693+
apply_changeset_to_checkpoint(original_tip, &changeset)
694+
.map_err(|_| CannotConnectError {
695+
try_include_height: 0,
696+
})?;
697+
return Ok((new_tip, changeset));
698+
}
699+
}
700+
} else {
701+
// We have an invalidation height so we set the height to the updated hash
702+
// and also purge all the original chain block hashes above this block.
703+
changeset.blocks.insert(u.height(), Some(u.data()));
704+
for invalidated_height in potentially_invalidated_heights.drain(..) {
705+
changeset.blocks.insert(invalidated_height, None);
706+
}
707+
prev_orig_was_invalidated = true;
708+
}
709+
prev_orig = curr_orig.take();
710+
prev_update = curr_update.take();
711+
}
712+
// Compare original and update entries when heights differ by exactly 1.
713+
else if o.height() == u.height() + 1 {
714+
let o_entry = CheckPointEntry::CheckPoint(o.clone());
715+
if let Some(o_prev) = o_entry.as_prev() {
716+
if o_prev.height() == u.height() && o_prev.hash() == u.hash() {
717+
// Ambiguous: update did not provide a real checkpoint at o.height().
665718
return Err(CannotConnectError {
666-
try_include_height: prev_orig.height(),
719+
try_include_height: o.height(),
667720
});
721+
} else {
722+
// No match: treat as o > u case.
723+
potentially_invalidated_heights.push(o.height());
724+
prev_orig_was_invalidated = false;
725+
prev_orig = curr_orig.take();
726+
is_update_height_superset_of_original = false;
668727
}
728+
} else {
729+
// No prev available: treat as o > u case.
730+
potentially_invalidated_heights.push(o.height());
731+
prev_orig_was_invalidated = false;
732+
prev_orig = curr_orig.take();
733+
is_update_height_superset_of_original = false;
669734
}
670-
point_of_agreement_found = true;
671-
prev_orig_was_invalidated = false;
672-
// OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we
673-
// 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));
735+
} else if u.height() == o.height() + 1 {
736+
let u_entry = CheckPointEntry::CheckPoint(u.clone());
737+
if let Some(u_as_prev) = u_entry.as_prev() {
738+
if u_as_prev.height() == o.height() && u_as_prev.hash() == o.hash() {
739+
// Agreement via `prev_blockhash`.
740+
if !prev_orig_was_invalidated && !point_of_agreement_found {
741+
if let (Some(prev_orig), Some(_prev_update)) =
742+
(&prev_orig, &prev_update)
743+
{
744+
return Err(CannotConnectError {
745+
try_include_height: prev_orig.height(),
746+
});
747+
}
748+
}
749+
point_of_agreement_found = true;
750+
prev_orig_was_invalidated = false;
751+
752+
// Update is missing a real checkpoint at o.height().
753+
is_update_height_superset_of_original = false;
754+
755+
// Record the update checkpoint one-above the agreed parent.
756+
changeset.blocks.insert(u.height(), Some(u.data()));
757+
758+
// Advance both sides after agreement.
759+
prev_orig = curr_orig.take();
760+
prev_update = curr_update.take();
677761
} 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));
762+
// No match: add update block.
763+
changeset.blocks.insert(u.height(), Some(u.data()));
764+
prev_update = curr_update.take();
683765
}
766+
} else {
767+
// No prev available: just add update block.
768+
changeset.blocks.insert(u.height(), Some(u.data()));
769+
prev_update = curr_update.take();
684770
}
771+
} else if o.height() > u.height() {
772+
// Original > Update: mark original as potentially invalidated.
773+
potentially_invalidated_heights.push(o.height());
774+
prev_orig_was_invalidated = false;
775+
prev_orig = curr_orig.take();
776+
is_update_height_superset_of_original = false;
685777
} 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.
778+
// Update > Original: add update block.
688779
changeset.blocks.insert(u.height(), Some(u.data()));
689-
for invalidated_height in potentially_invalidated_heights.drain(..) {
690-
changeset.blocks.insert(invalidated_height, None);
691-
}
692-
prev_orig_was_invalidated = true;
780+
prev_update = curr_update.take();
693781
}
694-
prev_update = curr_update.take();
695-
prev_orig = curr_orig.take();
696782
}
697783
(None, None) => {
698784
break;
699785
}
700786
_ => {
701-
unreachable!("compiler cannot tell that everything has been covered")
787+
unreachable!("should have been handled above")
702788
}
703789
}
704790
}
705791

706-
// When we don't have a point of agreement you can imagine it is implicitly the
707-
// genesis block so we need to do the final connectivity check which in this case
708-
// just means making sure the entire original chain was invalidated.
792+
// Final connectivity check
709793
if !prev_orig_was_invalidated && !point_of_agreement_found {
710794
if let Some(prev_orig) = prev_orig {
711795
return Err(CannotConnectError {

crates/core/src/checkpoint.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,78 @@ 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.
14+
CheckPoint(CheckPoint<D>),
15+
/// A `CheckPoint` representing a `prev_blockhash` reference.
16+
PrevBlockHash(CheckPoint<D>),
17+
}
18+
19+
impl<D> CheckPointEntry<D> {
20+
/// Returns true if this entry is a `prev_blockhash` reference.
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+
/// Get the height of this entry.
31+
pub fn height(&self) -> u32 {
32+
match self {
33+
CheckPointEntry::CheckPoint(cp) => cp.height(),
34+
CheckPointEntry::PrevBlockHash(cp) => cp.height().saturating_sub(1),
35+
}
36+
}
37+
38+
/// Get the `BlockHash` of this entry.
39+
pub fn hash(&self) -> BlockHash
40+
where
41+
D: ToBlockHash,
42+
{
43+
match self {
44+
CheckPointEntry::CheckPoint(cp) => cp.hash(),
45+
CheckPointEntry::PrevBlockHash(cp) => cp
46+
.prev_blockhash()
47+
.expect("PrevBlockHash variant must have prev_blockhash"),
48+
}
49+
}
50+
51+
/// Create a synthetic prev entry at height `h - 1`.
52+
pub fn as_prev(&self) -> Option<CheckPointEntry<D>>
53+
where
54+
D: ToBlockHash,
55+
{
56+
match self {
57+
CheckPointEntry::CheckPoint(cp) => {
58+
if cp.prev_blockhash().is_some() && cp.height() > 0 {
59+
Some(CheckPointEntry::PrevBlockHash(cp.clone()))
60+
} else {
61+
None
62+
}
63+
}
64+
CheckPointEntry::PrevBlockHash(_) => None, // Can't create prev of prev
65+
}
66+
}
67+
68+
/// Move to the next lower height `CheckPoint` entry.
69+
pub fn next(&self) -> Option<CheckPointEntry<D>> {
70+
match self {
71+
CheckPointEntry::CheckPoint(cp) => cp
72+
.prev()
73+
.map(|prev_cp| CheckPointEntry::CheckPoint(prev_cp)),
74+
CheckPointEntry::PrevBlockHash(cp) => cp
75+
.prev()
76+
.map(|prev_cp| CheckPointEntry::CheckPoint(prev_cp)),
77+
}
78+
}
79+
}
80+
981
/// A checkpoint is a node of a reference-counted linked list of [`BlockId`]s.
1082
///
1183
/// Checkpoints are cheaply cloneable and are useful to find the agreement point between two sparse
@@ -68,6 +140,11 @@ impl<D> Drop for CPInner<D> {
68140
pub trait ToBlockHash {
69141
/// Returns the [`BlockHash`] for the associated [`CheckPoint`] `data` type.
70142
fn to_blockhash(&self) -> BlockHash;
143+
144+
/// Returns `None` if the type has no knowledge of the previous [`BlockHash`].
145+
fn prev_blockhash(&self) -> Option<BlockHash> {
146+
None
147+
}
71148
}
72149

73150
impl ToBlockHash for BlockHash {
@@ -80,6 +157,23 @@ impl ToBlockHash for Header {
80157
fn to_blockhash(&self) -> BlockHash {
81158
self.block_hash()
82159
}
160+
161+
fn prev_blockhash(&self) -> Option<BlockHash> {
162+
Some(self.prev_blockhash)
163+
}
164+
}
165+
166+
impl<D: ToBlockHash> ToBlockHash for CheckPointEntry<D> {
167+
fn to_blockhash(&self) -> BlockHash {
168+
self.hash()
169+
}
170+
171+
fn prev_blockhash(&self) -> Option<BlockHash> {
172+
match self {
173+
CheckPointEntry::CheckPoint(cp) => cp.prev_blockhash(),
174+
CheckPointEntry::PrevBlockHash(_) => None,
175+
}
176+
}
83177
}
84178

85179
impl<D> PartialEq for CheckPoint<D> {
@@ -188,6 +282,14 @@ impl<D> CheckPoint<D> {
188282
pub fn eq_ptr(&self, other: &Self) -> bool {
189283
Arc::as_ptr(&self.0) == Arc::as_ptr(&other.0)
190284
}
285+
286+
/// Return the `prev_blockhash` from the `CheckPoint`, if available.
287+
pub fn prev_blockhash(&self) -> Option<BlockHash>
288+
where
289+
D: ToBlockHash,
290+
{
291+
self.0.data.prev_blockhash()
292+
}
191293
}
192294

193295
// Methods where `D: ToBlockHash`

0 commit comments

Comments
 (0)