-
Notifications
You must be signed in to change notification settings - Fork 322
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
LagginTimes
wants to merge
4
commits into
bitcoindevkit:master
Choose a base branch
from
LagginTimes:generic_checkpoint
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+502
−236
Open
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
5014c69
refactor(core): `CheckPoint` takes a generic
LagginTimes b254906
refactor(chain): `LocalChain` takes a generic
LagginTimes a07f926
refactor(core): `spk_client` types take generics
LagginTimes 27fe3bb
test(chain): Add test for inserting `Header` into `LocalChain`
LagginTimes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||||||
|
@@ -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", | ||||||
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`. | ||||||
|
@@ -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 }; | ||||||
|
||||||
|
@@ -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` | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
/// 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() | ||||||
} | ||||||
|
||||||
|
@@ -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>, | ||||||
{ | ||||||
|
@@ -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 | ||||||
|
@@ -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"); | ||||||
|
@@ -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()?; | ||||||
|
@@ -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 { | ||||||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ Is this version correct ?
There was a problem hiding this comment.
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.