Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Implement generics for CheckPoint, LocalChain, and spk_client types #1582

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion crates/bitcoind_rpc/tests/test_emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,10 @@ fn process_block(
block: Block,
block_height: u32,
) -> anyhow::Result<()> {
recv_chain.apply_update(CheckPoint::from_header(&block.header, block_height))?;
recv_chain.apply_update(CheckPoint::blockhash_checkpoint_from_header(
&block.header,
block_height,
))?;
let _ = recv_graph.apply_block(block, block_height);
Ok(())
}
Expand Down
251 changes: 177 additions & 74 deletions crates/core/src/checkpoint.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
use core::ops::RangeBounds;

use alloc::sync::Arc;
use bitcoin::BlockHash;
use bitcoin::{block::Header, BlockHash};

use crate::BlockId;

/// 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
/// block chains.
#[derive(Debug, Clone)]
pub struct CheckPoint(Arc<CPInner>);
#[derive(Debug)]
pub struct CheckPoint<B = BlockHash>(Arc<CPInner<B>>);

impl<B> Clone for CheckPoint<B> {
fn clone(&self) -> Self {
CheckPoint(Arc::clone(&self.0))
}
}

/// The internal contents of [`CheckPoint`].
#[derive(Debug, Clone)]
struct CPInner {
/// Block id (hash and height).
block: BlockId,
#[derive(Debug)]
struct CPInner<B> {
/// Block data.
block_id: BlockId,
/// Data.
data: B,
/// Previous checkpoint (if any).
prev: Option<Arc<CPInner>>,
prev: Option<Arc<CPInner<B>>>,
}

/// When a `CPInner` is dropped we need to go back down the chain and manually remove any
/// no-longer referenced checkpoints. Letting the default rust dropping mechanism handle this
/// leads to recursive logic and stack overflows
///
/// https://github.com/bitcoindevkit/bdk/issues/1634
impl Drop for CPInner {
impl<B> Drop for CPInner<B> {
fn drop(&mut self) {
// Take out `prev` so its `drop` won't be called when this drop is finished
let mut current = self.prev.take();
Expand All @@ -49,40 +57,51 @@ impl Drop for CPInner {
}
}

impl PartialEq for CheckPoint {
/// Trait that converts [`CheckPoint`] `data` to [`BlockHash`].
pub trait ToBlockHash {
/// Returns the [`BlockHash`] for the associated [`CheckPoint`] `data` type.
fn to_blockhash(&self) -> BlockHash;
}

impl ToBlockHash for BlockHash {
fn to_blockhash(&self) -> BlockHash {
*self
}
}

impl ToBlockHash for Header {
fn to_blockhash(&self) -> BlockHash {
self.block_hash()
}
}

impl<B> PartialEq for CheckPoint<B>
where
B: core::cmp::PartialEq,
{
fn eq(&self, other: &Self) -> bool {
let self_cps = self.iter().map(|cp| cp.block_id());
let other_cps = other.iter().map(|cp| cp.block_id());
let self_cps = self.iter().map(|cp| cp.0.block_id);
let other_cps = other.iter().map(|cp| cp.0.block_id);
self_cps.eq(other_cps)
}
}

impl CheckPoint {
/// Construct a new base block at the front of a linked list.
impl CheckPoint<BlockHash> {
/// Construct a new base [`CheckPoint`] at the front of a linked list.
pub fn new(block: BlockId) -> Self {
Self(Arc::new(CPInner { block, prev: None }))
CheckPoint::from_data(block.height, block.hash)
}

/// Construct a checkpoint from a list of [`BlockId`]s in ascending height order.
///
/// # Errors
///
/// This method will error if any of the follow occurs:
///
/// - The `blocks` iterator is empty, in which case, the error will be `None`.
/// - The `blocks` iterator is not in ascending height order.
/// - The `blocks` iterator contains multiple [`BlockId`]s of the same height.
/// Construct a checkpoint from the given `header` and block `height`.
///
/// The error type is the last successful checkpoint constructed (if any).
pub fn from_block_ids(
block_ids: impl IntoIterator<Item = BlockId>,
) -> Result<Self, Option<Self>> {
let mut blocks = block_ids.into_iter();
let mut acc = CheckPoint::new(blocks.next().ok_or(None)?);
for id in blocks {
acc = acc.push(id).map_err(Some)?;
}
Ok(acc)
/// If `header` is of the genesis block, the checkpoint won't have a `prev` node. Otherwise,
/// we return a checkpoint linked with the previous block.
#[deprecated(
since = "0.1.1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Is this version correct ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out! The version number for bdk_core has been updated since this PR was created. This will need to be updated as well.

note = "Please use [`CheckPoint::blockhash_checkpoint_from_header`] instead. To create a CheckPoint<Header>, please use [`CheckPoint::from_data`]."
)]
pub fn from_header(header: &bitcoin::block::Header, height: u32) -> Self {
LagginTimes marked this conversation as resolved.
Show resolved Hide resolved
CheckPoint::blockhash_checkpoint_from_header(header, height)
}

/// Construct a checkpoint from the given `header` and block `height`.
Expand All @@ -91,7 +110,7 @@ impl CheckPoint {
/// we return a checkpoint linked with the previous block.
///
/// [`prev`]: CheckPoint::prev
pub fn from_header(header: &bitcoin::block::Header, height: u32) -> Self {
pub fn blockhash_checkpoint_from_header(header: &bitcoin::block::Header, height: u32) -> Self {
let hash = header.block_hash();
let this_block_id = BlockId { height, hash };

Expand All @@ -110,55 +129,90 @@ impl CheckPoint {
.expect("must construct checkpoint")
}

/// Puts another checkpoint onto the linked list representing the blockchain.
/// Construct a checkpoint from a list of [`BlockId`]s in ascending height order.
///
/// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the one you
/// are pushing on to.
pub fn push(self, block: BlockId) -> Result<Self, Self> {
if self.height() < block.height {
Ok(Self(Arc::new(CPInner {
block,
prev: Some(self.0),
})))
} else {
Err(self)
/// # Errors
///
/// This method will error if any of the follow occurs:
///
/// - The `blocks` iterator is empty, in which case, the error will be `None`.
/// - The `blocks` iterator is not in ascending height order.
/// - The `blocks` iterator contains multiple [`BlockId`]s of the same height.
///
/// The error type is the last successful checkpoint constructed (if any).
pub fn from_block_ids(
block_ids: impl IntoIterator<Item = BlockId>,
) -> Result<Self, Option<Self>> {
let mut blocks = block_ids.into_iter();
let block = blocks.next().ok_or(None)?;
let mut acc = CheckPoint::new(block);
for id in blocks {
acc = acc.push(id).map_err(Some)?;
}
Ok(acc)
}

/// Extends the checkpoint linked list by a iterator of block ids.
///
/// Returns an `Err(self)` if there is block which does not have a greater height than the
/// previous one.
pub fn extend(self, blocks: impl IntoIterator<Item = BlockId>) -> Result<Self, Self> {
let mut curr = self.clone();
for block in blocks {
curr = curr.push(block).map_err(|_| self.clone())?;
}
Ok(curr)
pub fn extend(self, blockdata: impl IntoIterator<Item = BlockId>) -> Result<Self, Self> {
self.extend_data(
blockdata
.into_iter()
.map(|block| (block.height, block.hash)),
)
}

/// Inserts `block_id` at its height within the chain.
///
/// The effect of `insert` depends on whether a height already exists. If it doesn't the
/// `block_id` we inserted and all pre-existing blocks higher than it will be re-inserted after
/// it. If the height already existed and has a conflicting block hash then it will be purged
/// along with all block followin it. The returned chain will have a tip of the `block_id`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// along with all block followin it. The returned chain will have a tip of the `block_id`
/// along with all blocks following it. The returned chain will have a tip of the `block_id`

/// passed in. Of course, if the `block_id` was already present then this just returns `self`.
#[must_use]
pub fn insert(self, block_id: BlockId) -> Self {
self.insert_data(block_id.height, block_id.hash)
}

/// Puts another checkpoint onto the linked list representing the blockchain.
///
/// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the one you
/// are pushing on to.
pub fn push(self, block: BlockId) -> Result<Self, Self> {
self.push_data(block.height, block.hash)
}
}

impl<B> CheckPoint<B> {
/// Get the `data` of the checkpoint.
pub fn data(&self) -> &B {
&self.0.data
}

/// Get the [`BlockId`] of the checkpoint.
pub fn block_id(&self) -> BlockId {
self.0.block
self.0.block_id
}

/// Get the height of the checkpoint.
/// Get the `height` of the checkpoint.
pub fn height(&self) -> u32 {
self.0.block.height
self.0.block_id.height
}

/// Get the block hash of the checkpoint.
pub fn hash(&self) -> BlockHash {
self.0.block.hash
self.0.block_id.hash
}

/// Get the previous checkpoint in the chain
pub fn prev(&self) -> Option<CheckPoint> {
/// Get the previous checkpoint in the chain.
pub fn prev(&self) -> Option<CheckPoint<B>> {
self.0.prev.clone().map(CheckPoint)
}

/// Iterate from this checkpoint in descending height.
pub fn iter(&self) -> CheckPointIter {
pub fn iter(&self) -> CheckPointIter<B> {
self.clone().into_iter()
}

Expand All @@ -173,7 +227,7 @@ impl CheckPoint {
///
/// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
/// height).
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint<B>>
where
R: RangeBounds<u32>,
{
Expand All @@ -191,8 +245,38 @@ impl CheckPoint {
core::ops::Bound::Unbounded => true,
})
}
}

/// Inserts `block_id` at its height within the chain.
impl<B> CheckPoint<B>
where
B: Copy + core::fmt::Debug + ToBlockHash,
{
/// Construct a new base [`CheckPoint`] from given `height` and `data` at the front of a linked
/// list.
pub fn from_data(height: u32, data: B) -> Self {
Self(Arc::new(CPInner {
block_id: BlockId {
height,
hash: data.to_blockhash(),
},
data,
prev: None,
}))
}

/// Extends the checkpoint linked list by a iterator containing `height` and `data`.
///
/// Returns an `Err(self)` if there is block which does not have a greater height than the
/// previous one.
pub fn extend_data(self, blockdata: impl IntoIterator<Item = (u32, B)>) -> Result<Self, Self> {
let mut curr = self.clone();
for (height, data) in blockdata {
curr = curr.push_data(height, data).map_err(|_| self.clone())?;
}
Ok(curr)
}

/// Inserts `data` at its `height` within the chain.
///
/// The effect of `insert` depends on whether a height already exists. If it doesn't the
/// `block_id` we inserted and all pre-existing blocks higher than it will be re-inserted after
Expand All @@ -204,12 +288,12 @@ impl CheckPoint {
///
/// This panics if called with a genesis block that differs from that of `self`.
#[must_use]
pub fn insert(self, block_id: BlockId) -> Self {
pub fn insert_data(self, height: u32, data: B) -> Self {
let mut cp = self.clone();
let mut tail = vec![];
let base = loop {
if cp.height() == block_id.height {
if cp.hash() == block_id.hash {
if cp.height() == height {
if cp.hash() == data.to_blockhash() {
return self;
}
assert_ne!(cp.height(), 0, "cannot replace genesis block");
Expand All @@ -219,31 +303,50 @@ impl CheckPoint {
break cp.prev().expect("can't be called on genesis block");
}

if cp.height() < block_id.height {
if cp.height() < height {
break cp;
}

tail.push(cp.block_id());
tail.push((cp.height(), *cp.data()));
cp = cp.prev().expect("will break before genesis block");
};

base.extend(core::iter::once(block_id).chain(tail.into_iter().rev()))
base.extend_data(core::iter::once((height, data)).chain(tail.into_iter().rev()))
.expect("tail is in order")
}

/// Puts another checkpoint onto the linked list representing the blockchain.
///
/// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the one you
/// are pushing on to.
pub fn push_data(self, height: u32, data: B) -> Result<Self, Self> {
if self.height() < height {
Ok(Self(Arc::new(CPInner {
block_id: BlockId {
height,
hash: data.to_blockhash(),
},
data,
prev: Some(self.0),
})))
} else {
Err(self)
}
}

/// This method tests for `self` and `other` to have equal internal pointers.
pub fn eq_ptr(&self, other: &Self) -> bool {
Arc::as_ptr(&self.0) == Arc::as_ptr(&other.0)
}
}

/// Iterates over checkpoints backwards.
pub struct CheckPointIter {
current: Option<Arc<CPInner>>,
pub struct CheckPointIter<B = BlockHash> {
current: Option<Arc<CPInner<B>>>,
}

impl Iterator for CheckPointIter {
type Item = CheckPoint;
impl<B> Iterator for CheckPointIter<B> {
type Item = CheckPoint<B>;

fn next(&mut self) -> Option<Self::Item> {
let current = self.current.clone()?;
Expand All @@ -252,9 +355,9 @@ impl Iterator for CheckPointIter {
}
}

impl IntoIterator for CheckPoint {
type Item = CheckPoint;
type IntoIter = CheckPointIter;
impl<B> IntoIterator for CheckPoint<B> {
type Item = CheckPoint<B>;
type IntoIter = CheckPointIter<B>;

fn into_iter(self) -> Self::IntoIter {
CheckPointIter {
Expand Down