From 95b86f9ac70ea5a26c72c0b42a77c4c7a7141396 Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Thu, 18 Sep 2025 14:14:29 +0000 Subject: [PATCH] feat(chain)!: add `CheckPointEntry` --- crates/chain/src/local_chain.rs | 162 ++++++++++++++++++++++++-------- crates/core/src/checkpoint.rs | 102 ++++++++++++++++++++ 2 files changed, 225 insertions(+), 39 deletions(-) diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 81f4a1796..788c7a92b 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -6,8 +6,8 @@ use core::ops::RangeBounds; use crate::collections::BTreeMap; use crate::{BlockId, ChainOracle, Merge}; -use bdk_core::ToBlockHash; pub use bdk_core::{CheckPoint, CheckPointIter}; +use bdk_core::{CheckPointEntry, ToBlockHash}; use bitcoin::block::Header; use bitcoin::BlockHash; @@ -69,7 +69,10 @@ impl PartialEq for LocalChain { } } -impl ChainOracle for LocalChain { +impl ChainOracle for LocalChain +where + D: ToBlockHash + Copy, +{ type Error = Infallible; fn is_block_in_chain( @@ -83,10 +86,18 @@ impl ChainOracle for LocalChain { Some(cp) if cp.hash() == chain_tip.hash => cp, _ => return Ok(None), }; - match chain_tip_cp.get(block.height) { - Some(cp) => Ok(Some(cp.hash() == block.hash)), - None => Ok(None), + + if let Some(cp) = chain_tip_cp.get(block.height) { + return Ok(Some(cp.hash() == block.hash)); } + + if let Some(next_cp) = chain_tip_cp.get(block.height.saturating_add(1)) { + if let Some(prev_hash) = next_cp.prev_blockhash() { + return Ok(Some(prev_hash == block.hash)); + } + } + + Ok(None) } fn get_chain_tip(&self) -> Result { @@ -653,59 +664,132 @@ where } } (Some(o), Some(u)) => { - if o.hash() == u.hash() { - // We have found our point of agreement 🎉 -- we require that the previous (i.e. - // higher because we are iterating backwards) block in the original chain was - // invalidated (if it exists). This ensures that there is an unambiguous point - // of connection to the original chain from the update chain - // (i.e. we know the precisely which original blocks are - // invalid). - if !prev_orig_was_invalidated && !point_of_agreement_found { - if let (Some(prev_orig), Some(_prev_update)) = (&prev_orig, &prev_update) { + if o.height() == u.height() { + if o.hash() == u.hash() { + // We have found our point of agreement 🎉 -- we require that the previous + // (i.e. higher because we are iterating backwards) block in the original + // chain was invalidated (if it exists). This ensures that there is an + // unambiguous point of connection to the original chain from the update + // chain (i.e. we know the precisely which original blocks are invalid). + if !prev_orig_was_invalidated && !point_of_agreement_found { + if let (Some(prev_orig), Some(_prev_update)) = + (&prev_orig, &prev_update) + { + return Err(CannotConnectError { + try_include_height: prev_orig.height(), + }); + } + } + point_of_agreement_found = true; + prev_orig_was_invalidated = false; + + // OPTIMIZATION 2 -- if we have the same underlying pointer at this point, + // we can guarantee that no older blocks are introduced. + if o.eq_ptr(u) { + if is_update_height_superset_of_original { + return Ok((update_tip, changeset)); + } else { + let new_tip = + apply_changeset_to_checkpoint(original_tip, &changeset) + .map_err(|_| CannotConnectError { + try_include_height: 0, + })?; + return Ok((new_tip, changeset)); + } + } + } else { + // We have an invalidation height so we set the height to the updated hash + // and also purge all the original chain block hashes above this block. + changeset.blocks.insert(u.height(), Some(u.data())); + for invalidated_height in potentially_invalidated_heights.drain(..) { + changeset.blocks.insert(invalidated_height, None); + } + prev_orig_was_invalidated = true; + } + prev_orig = curr_orig.take(); + prev_update = curr_update.take(); + } + // Compare original and update entries when heights differ by exactly 1. + else if o.height() == u.height() + 1 { + let o_entry = CheckPointEntry::CheckPoint(o.clone()); + if let Some(o_prev) = o_entry.as_prev() { + if o_prev.height() == u.height() && o_prev.hash() == u.hash() { + // Ambiguous: update did not provide a real checkpoint at o.height(). return Err(CannotConnectError { - try_include_height: prev_orig.height(), + try_include_height: o.height(), }); + } else { + // No match: treat as o > u case. + potentially_invalidated_heights.push(o.height()); + prev_orig_was_invalidated = false; + prev_orig = curr_orig.take(); + is_update_height_superset_of_original = false; } + } else { + // No prev available: treat as o > u case. + potentially_invalidated_heights.push(o.height()); + prev_orig_was_invalidated = false; + prev_orig = curr_orig.take(); + is_update_height_superset_of_original = false; } - point_of_agreement_found = true; - prev_orig_was_invalidated = false; - // OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we - // can guarantee that no older blocks are introduced. - if o.eq_ptr(u) { - if is_update_height_superset_of_original { - return Ok((update_tip, changeset)); + } else if u.height() == o.height() + 1 { + let u_entry = CheckPointEntry::CheckPoint(u.clone()); + if let Some(u_as_prev) = u_entry.as_prev() { + if u_as_prev.height() == o.height() && u_as_prev.hash() == o.hash() { + // Agreement via `prev_blockhash`. + if !prev_orig_was_invalidated && !point_of_agreement_found { + if let (Some(prev_orig), Some(_prev_update)) = + (&prev_orig, &prev_update) + { + return Err(CannotConnectError { + try_include_height: prev_orig.height(), + }); + } + } + point_of_agreement_found = true; + prev_orig_was_invalidated = false; + + // Update is missing a real checkpoint at o.height(). + is_update_height_superset_of_original = false; + + // Record the update checkpoint one-above the agreed parent. + changeset.blocks.insert(u.height(), Some(u.data())); + + // Advance both sides after agreement. + prev_orig = curr_orig.take(); + prev_update = curr_update.take(); } else { - let new_tip = apply_changeset_to_checkpoint(original_tip, &changeset) - .map_err(|_| CannotConnectError { - try_include_height: 0, - })?; - return Ok((new_tip, changeset)); + // No match: add update block. + changeset.blocks.insert(u.height(), Some(u.data())); + prev_update = curr_update.take(); } + } else { + // No prev available: just add update block. + changeset.blocks.insert(u.height(), Some(u.data())); + prev_update = curr_update.take(); } + } else if o.height() > u.height() { + // Original > Update: mark original as potentially invalidated. + potentially_invalidated_heights.push(o.height()); + prev_orig_was_invalidated = false; + prev_orig = curr_orig.take(); + is_update_height_superset_of_original = false; } else { - // We have an invalidation height so we set the height to the updated hash and - // also purge all the original chain block hashes above this block. + // Update > Original: add update block. changeset.blocks.insert(u.height(), Some(u.data())); - for invalidated_height in potentially_invalidated_heights.drain(..) { - changeset.blocks.insert(invalidated_height, None); - } - prev_orig_was_invalidated = true; + prev_update = curr_update.take(); } - prev_update = curr_update.take(); - prev_orig = curr_orig.take(); } (None, None) => { break; } _ => { - unreachable!("compiler cannot tell that everything has been covered") + unreachable!("should have been handled above") } } } - // When we don't have a point of agreement you can imagine it is implicitly the - // genesis block so we need to do the final connectivity check which in this case - // just means making sure the entire original chain was invalidated. + // Final connectivity check if !prev_orig_was_invalidated && !point_of_agreement_found { if let Some(prev_orig) = prev_orig { return Err(CannotConnectError { diff --git a/crates/core/src/checkpoint.rs b/crates/core/src/checkpoint.rs index d0a9bacd7..cbfa33a43 100644 --- a/crates/core/src/checkpoint.rs +++ b/crates/core/src/checkpoint.rs @@ -6,6 +6,78 @@ use bitcoin::{block::Header, BlockHash}; use crate::BlockId; +/// Internal type to represent entries in `CheckPoint` that can handle both actual checkpoint data +/// and placeholder entries for types that have `prev_blockhash`. +#[derive(Debug, Clone, PartialEq)] +pub enum CheckPointEntry { + /// An actual `CheckPoint` entry. + CheckPoint(CheckPoint), + /// A `CheckPoint` representing a `prev_blockhash` reference. + PrevBlockHash(CheckPoint), +} + +impl CheckPointEntry { + /// Returns true if this entry is a `prev_blockhash` reference. + pub fn is_prev_blockhash(&self) -> bool { + matches!(self, CheckPointEntry::PrevBlockHash(_)) + } + + /// Returns true if this entry contains actual `CheckPoint` data. + pub fn is_checkpoint(&self) -> bool { + matches!(self, CheckPointEntry::CheckPoint(_)) + } + + /// Get the height of this entry. + pub fn height(&self) -> u32 { + match self { + CheckPointEntry::CheckPoint(cp) => cp.height(), + CheckPointEntry::PrevBlockHash(cp) => cp.height().saturating_sub(1), + } + } + + /// Get the `BlockHash` of this entry. + pub fn hash(&self) -> BlockHash + where + D: ToBlockHash, + { + match self { + CheckPointEntry::CheckPoint(cp) => cp.hash(), + CheckPointEntry::PrevBlockHash(cp) => cp + .prev_blockhash() + .expect("PrevBlockHash variant must have prev_blockhash"), + } + } + + /// Create a synthetic prev entry at height `h - 1`. + pub fn as_prev(&self) -> Option> + where + D: ToBlockHash, + { + match self { + CheckPointEntry::CheckPoint(cp) => { + if cp.prev_blockhash().is_some() && cp.height() > 0 { + Some(CheckPointEntry::PrevBlockHash(cp.clone())) + } else { + None + } + } + CheckPointEntry::PrevBlockHash(_) => None, // Can't create prev of prev + } + } + + /// Move to the next lower height `CheckPoint` entry. + pub fn next(&self) -> Option> { + match self { + CheckPointEntry::CheckPoint(cp) => cp + .prev() + .map(|prev_cp| CheckPointEntry::CheckPoint(prev_cp)), + CheckPointEntry::PrevBlockHash(cp) => cp + .prev() + .map(|prev_cp| CheckPointEntry::CheckPoint(prev_cp)), + } + } +} + /// A checkpoint is a node of a reference-counted linked list of [`BlockId`]s. /// /// Checkpoints are cheaply cloneable and are useful to find the agreement point between two sparse @@ -68,6 +140,11 @@ impl Drop for CPInner { pub trait ToBlockHash { /// Returns the [`BlockHash`] for the associated [`CheckPoint`] `data` type. fn to_blockhash(&self) -> BlockHash; + + /// Returns `None` if the type has no knowledge of the previous [`BlockHash`]. + fn prev_blockhash(&self) -> Option { + None + } } impl ToBlockHash for BlockHash { @@ -80,6 +157,23 @@ impl ToBlockHash for Header { fn to_blockhash(&self) -> BlockHash { self.block_hash() } + + fn prev_blockhash(&self) -> Option { + Some(self.prev_blockhash) + } +} + +impl ToBlockHash for CheckPointEntry { + fn to_blockhash(&self) -> BlockHash { + self.hash() + } + + fn prev_blockhash(&self) -> Option { + match self { + CheckPointEntry::CheckPoint(cp) => cp.prev_blockhash(), + CheckPointEntry::PrevBlockHash(_) => None, + } + } } impl PartialEq for CheckPoint { @@ -188,6 +282,14 @@ impl CheckPoint { pub fn eq_ptr(&self, other: &Self) -> bool { Arc::as_ptr(&self.0) == Arc::as_ptr(&other.0) } + + /// Return the `prev_blockhash` from the `CheckPoint`, if available. + pub fn prev_blockhash(&self) -> Option + where + D: ToBlockHash, + { + self.0.data.prev_blockhash() + } } // Methods where `D: ToBlockHash`