From 5d6bbf125b0925570e49d3c8ea766e1ccb807f8c Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Sat, 5 Oct 2024 02:12:28 +0800 Subject: [PATCH] PoWChain WIP --- crates/chain/src/lib.rs | 1 + crates/chain/src/pow_chain.rs | 574 ++++++++++++++++++++++++++++++++++ crates/core/src/checkpoint.rs | 266 +++++++++++----- 3 files changed, 758 insertions(+), 83 deletions(-) create mode 100644 crates/chain/src/pow_chain.rs diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 9667bb549..c386d6f92 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -43,6 +43,7 @@ pub mod tx_graph; pub use tx_graph::TxGraph; mod chain_oracle; pub use chain_oracle::*; +pub mod pow_chain; #[doc(hidden)] pub mod example_utils; diff --git a/crates/chain/src/pow_chain.rs b/crates/chain/src/pow_chain.rs new file mode 100644 index 000000000..db967a9ee --- /dev/null +++ b/crates/chain/src/pow_chain.rs @@ -0,0 +1,574 @@ +//! The [`PoWChain`] is an implementation of [`ChainOracle`] that verifies proof of work. + +use crate::{BlockId, ChainOracle, CheckPoint, Merge}; +use bdk_core::ToBlockHash; +use bitcoin::{block::Header, BlockHash, Target}; +use std::collections::{BTreeMap, HashMap}; +use std::convert::Infallible; +use std::vec::Vec; + +//Iterator, +/// Apply `changeset` to the checkpoint. +fn apply_changeset_to_checkpoint( + mut init_cp: CheckPoint
, + changeset: &ChangeSet, + difficulty_map: HashMap, +) -> Result, MissingGenesisError> { + if let Some(start_height) = changeset.blocks.keys().next().cloned() { + // changes after point of agreement + let mut extension = BTreeMap::default(); + // point of agreement + let mut base: Option> = None; + + for cp in init_cp.iter() { + if cp.height() >= start_height { + extension.insert(cp.height(), (*cp.data(), difficulty_map.get(&cp.hash()))); + } else { + base = Some(cp); + break; + } + } + + for (&height, &data) in &changeset.blocks { + match data { + Some(data) => { + extension.insert(height, data); + } + None => { + extension.remove(&height); + } + }; + } + + let new_tip = match base { + Some(base) => base + .extend_data(extension) + .expect("extension is strictly greater than base"), + None => PoWChain::from_data(extension, extension.get(&0).unwrap().0)?.tip(), + }; + init_cp = new_tip; + } + + Ok(init_cp) +} + +/// This is an implementation of [`ChainOracle`] that verifies proof of work. +#[derive(Debug, Clone, PartialEq)] +pub struct PoWChain { + // Stores the difficulty of each block. + difficulty_map: HashMap, + // List of trusted blocks. + trusted_blocks: Vec, + // A vector which keeps track of all chain tips. + all_tips: Vec>, + // Current best chain. + tip: CheckPoint
, +} + +impl ChainOracle for PoWChain { + type Error = Infallible; + + fn is_block_in_chain( + &self, + block: bdk_core::BlockId, + chain_tip: bdk_core::BlockId, + ) -> Result, Self::Error> { + let chain_tip_cp = match self.tip.get(chain_tip.height) { + // we can only determine whether `block` is in chain of `chain_tip` if `chain_tip` can + // be identified in chain + 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), + } + } + + fn get_chain_tip(&self) -> Result { + Ok(self.tip.block_id()) + } +} + +impl PoWChain { + /// Get the genesis hash. + pub fn genesis_hash(&self) -> BlockHash { + self.tip.get(0).expect("genesis must exist").hash() + } + + /// Construct [`PoWChain`] from genesis `Header`. + #[must_use] + pub fn from_genesis_header(header: Header) -> (Self, ChangeSet) { + let tip = CheckPoint::from_data(0, header); + let chain = Self { + difficulty_map: HashMap::from([(header.to_blockhash(), Target::MAX)]), + trusted_blocks: Vec::default(), + all_tips: vec![tip.clone()], + tip, + }; + let changeset = chain.initial_changeset(); + (chain, changeset) + } + + /// Construct a [`PoWChain`] from an initial `changeset`. + pub fn from_changeset(changeset: ChangeSet) -> Result { + let genesis_entry = changeset.blocks.get(&0).copied().flatten(); + let genesis_header = match genesis_entry { + Some(header) => header, + None => return Err(MissingGenesisError), + }; + + let (mut chain, _) = Self::from_genesis_header(genesis_header); + chain.apply_changeset(&changeset)?; + + debug_assert!(chain._check_changeset_is_applied(&changeset)); + + Ok(chain) + } + + /// Initialize [`PoWChain`] with specified data. + pub fn from_data( + input: impl Iterator, + genesis_header: Header, + ) -> Result> { + let tip = CheckPoint::
::from_data(0, genesis_header); + + let mut difficulty_map = HashMap::new(); + let mut trusted_blocks = Vec::new(); + + for (height, hash, target) in input { + difficulty_map.insert(hash, Target::from_hex(&format!("{:x}", target))?); + trusted_blocks.push(BlockId { height, hash }); + } + + Ok(PoWChain { + difficulty_map, + trusted_blocks, + all_tips: vec![tip.clone()], + tip, + }) + } + + /// Load known checkpoints from external `checkpoints.json` file. + pub fn from_electrum_json( + &self, + path: &str, + genesis_header: Header, + ) -> Result> { + let tip = CheckPoint::
::from_data(0, genesis_header); + + let mut difficulty_map = HashMap::new(); + let mut trusted_blocks = Vec::new(); + + // Populate a vector of (blockhash, difficulty target) from external `checkpoints.json` + // file. + let file = std::fs::File::open(path)?; + let checkpoint_entries: Vec<(BlockHash, u64)> = serde_json::from_reader(file)?; + + // Calculate heights based on the starting height of first block in `checkpoints.json`, + // which is block 2015. Subsequent blocks have height gaps of 2016, which is the number of + // blocks between each difficulty adjustment. + let mut height = 2015; + + for (hash, target) in checkpoint_entries { + difficulty_map.insert(hash, Target::from_hex(&format!("{:x}", target))?); + trusted_blocks.push(BlockId { height, hash }); + + // Increment height by 2016, which is the number of blocks between each difficulty + // adjustment. + height += 2016; + } + + Ok(PoWChain { + difficulty_map, + trusted_blocks, + all_tips: vec![tip.clone()], + tip, + }) + } + + /// Get the highest checkpoint. + pub fn tip(&self) -> CheckPoint
{ + self.tip.clone() + } + + /// Apply the given `changeset`. + pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> { + let old_tip = self.tip.clone(); + let new_tip = + apply_changeset_to_checkpoint(old_tip, changeset, self.difficulty_map.clone())?; + self.tip = new_tip; + debug_assert!(self._check_changeset_is_applied(changeset)); + Ok(()) + } + + /// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to + /// recover the current chain. + pub fn initial_changeset(&self) -> ChangeSet { + ChangeSet { + blocks: self + .tip + .iter() + .map(|cp| (cp.height(), Some(*cp.data()))) + .collect(), + } + } + + fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool { + let mut curr_cp = self.tip.clone(); + for (height, exp_header) in changeset.blocks.iter().rev() { + match curr_cp.get(*height) { + Some(query_cp) => { + if query_cp.height() != *height || Some(*query_cp.data()) != *exp_header { + return false; + } + curr_cp = query_cp; + } + None => { + if exp_header.is_some() { + return false; + } + } + } + } + true + } + + // based on current height we should know if we already have current difficulty or not + // account for possibility of reorged chunk of blocks containing difficulty adjustment block + // def get_target(self, index: int) -> int: + // # compute target from chunk x, used in chunk x+1 + // if constants.net.TESTNET: + // return 0 + // if index == -1: + // return MAX_TARGET + // if index < len(self.checkpoints): + // h, t = self.checkpoints[index] + // return t + // # new target + // first = self.read_header(index * 2016) + // last = self.read_header(index * 2016 + 2015) + // if not first or not last: + // raise MissingHeader() + // bits = last.get('bits') + // target = self.bits_to_target(bits) + // nActualTimespan = last.get('timestamp') - first.get('timestamp') + // nTargetTimespan = 14 * 24 * 60 * 60 + // nActualTimespan = max(nActualTimespan, nTargetTimespan // 4) + // nActualTimespan = min(nActualTimespan, nTargetTimespan * 4) + // new_target = min(MAX_TARGET, (target * nActualTimespan) // nTargetTimespan) + // # not any target can be represented in 32 bits: + // new_target = self.bits_to_target(self.target_to_bits(new_target)) + // return new_target + + /// Verifies the proof of work for a given `Header`. If the block does not meet the difficulty + /// target, it is discarded. + pub fn verify_pow(&self, header: Header) -> Result { + self.difficulty_map + .get(&header.block_hash()) + .and_then(|&target| header.validate_pow(target).ok()) + .ok_or(()) + } + + /// Calculates and sets the best chain from all current chain tips. + pub fn calculate_best_chain(&mut self) { + if let Some(best_tip) = self.all_tips.iter().min_by(|&cp1, &cp2| { + // If no difficulty target is found, target is set to maximum to represent minimum + // difficulty. + let difficulty1 = self.difficulty_map.get(&cp1.hash()).unwrap_or(&Target::MAX); + let difficulty2 = self.difficulty_map.get(&cp2.hash()).unwrap_or(&Target::MAX); + + // Compare chain tips by difficulty. Return the chain with lower target which represents + // the chain with higher difficulty. + let difficulty_cmp = difficulty1.cmp(difficulty2); + + // If chain tips have the same difficulty, compare by height. Since we are using + // `min_by`, `cp1` and `cp2` ordering is reversed to return the higher height. + if difficulty_cmp == std::cmp::Ordering::Equal { + cp2.height().cmp(&cp1.height()) + } else { + difficulty_cmp + } + }) { + self.tip = best_tip.clone(); + } + } + + // Applies an update tip to its pertinent chain in `all_tips`. Returns the best chain and + // corresponding `ChangeSet`. + pub fn merge_update( + &mut self, + update_tip: CheckPoint
, + ) -> Result<(CheckPoint
, ChangeSet), CannotConnectError> { + let mut changeset = ChangeSet::default(); + let mut tip: Option> = None; + + // Attempt to merge update with all known tips + for original_tip in self.all_tips.iter_mut() { + match merge_chains(original_tip.clone(), update_tip.clone()) { + // Update the particular tip if merge is successful. + Ok((new_tip, new_changeset)) => { + *original_tip = new_tip.clone(); + tip = Some(new_tip); + changeset.merge(new_changeset); + break; + } + Err(_) => continue, + } + } + + // TODO: ChangeSet needs to be checked for missing heights, if new + // best chain has higher starting height. + if let Some(tip) = tip { + // Purge subsets after attempting to merge + self.purge_subsets(); + + self.calculate_best_chain(); + // If merged tip is not new best chain, do not return a `ChangeSet`. + if self.tip() != tip { + return Ok((self.tip(), ChangeSet::default())); + } + return Ok((tip, changeset)); + } + + // If update tip is not merged, return old best tip and empty `ChangeSet`. + Ok((self.tip(), ChangeSet::default())) + } + + // Purge tips that are complete subsets of another tip from `all_tips`. + fn purge_subsets(&mut self) { + let tips: Vec> = self.all_tips.clone(); + + // Compare tips inside of `all_tips` and filter out any subsets. + // TODO: Edge case where two CheckPoints have the same tip but different lengths? + self.all_tips = tips + .iter() + .filter(|tip| { + !tips.iter().any(|other_tip| *tip != other_tip && self.is_subset(tip, other_tip)) + }) + .cloned() + .collect(); + } + + // Determine if a `CheckPoint` is a subset of another `CheckPoint`. + fn is_subset(&self, cp1: &CheckPoint
, cp2: &CheckPoint
) -> bool { + let subset = cp1.iter().collect::>(); + let superset = cp2.iter().collect::>(); + + // Ensure the subset is smaller. + if subset.len() >= superset.len() { + return false; + } + + // Check if all elements of subset are contained in superset. + subset.iter().all(|elem| superset.contains(elem)) + } +} + +/// The [`ChangeSet`] represents changes to [`PoWChain`]. +#[derive(Debug, Default, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ChangeSet { + /// Changes to the [`PoWChain`] blocks. + /// + /// The key represents the block height, and the value either represents added a new [`CheckPoint`] + /// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]). + pub blocks: BTreeMap>, +} + +impl Merge for ChangeSet { + fn merge(&mut self, other: Self) { + Merge::merge(&mut self.blocks, other.blocks) + } + + fn is_empty(&self) -> bool { + self.blocks.is_empty() + } +} + +/// An error which occurs when a [`PoWChain`] is constructed without a genesis checkpoint. +#[derive(Clone, Debug, PartialEq)] +pub struct MissingGenesisError; + +impl core::fmt::Display for MissingGenesisError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "cannot construct `LocalChain` without a genesis checkpoint" + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for MissingGenesisError {} + +/// Occurs when an update does not have a common checkpoint with the original chain. +#[derive(Clone, Debug, PartialEq)] +pub struct CannotConnectError { + /// The suggested checkpoint to include to connect the two chains. + pub try_include_height: u32, +} + +impl core::fmt::Display for CannotConnectError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "introduced chain cannot connect with the original chain, try include height {}", + self.try_include_height, + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CannotConnectError {} + +/// The error type for [`LocalChain::apply_header_connected_to`]. +#[derive(Debug, Clone, PartialEq)] +pub enum ApplyHeaderError { + /// Occurs when `connected_to` block conflicts with either the current block or previous block. + InconsistentBlocks, + /// Occurs when the update cannot connect with the original chain. + CannotConnect(CannotConnectError), +} + +impl core::fmt::Display for ApplyHeaderError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ApplyHeaderError::InconsistentBlocks => write!( + f, + "the `connected_to` block conflicts with either the current or previous block" + ), + ApplyHeaderError::CannotConnect(err) => core::fmt::Display::fmt(err, f), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ApplyHeaderError {} + +/// Applies `update_tip` onto `original_tip`. +/// +/// On success, a tuple is returned `(changeset, can_replace)`. If `can_replace` is true, then the +/// `update_tip` can replace the `original_tip`. +fn merge_chains( + original_tip: CheckPoint
, + update_tip: CheckPoint
, +) -> Result<(CheckPoint
, ChangeSet), CannotConnectError> { + let mut changeset = ChangeSet::default(); + let mut orig = original_tip.iter(); + let mut update = update_tip.iter(); + let mut curr_orig = None; + let mut curr_update = None; + let mut prev_orig: Option> = None; + let mut prev_update: Option> = None; + let mut point_of_agreement_found = false; + let mut prev_orig_was_invalidated = false; + let mut potentially_invalidated_heights = vec![]; + + // If we can, we want to return the update tip as the new tip because this allows checkpoints + // in multiple locations to keep the same `Arc` pointers when they are being updated from each + // other using this function. We can do this as long as long as the update contains every + // block's height of the original chain. + let mut is_update_height_superset_of_original = true; + + // To find the difference between the new chain and the original we iterate over both of them + // from the tip backwards in tandem. We always dealing with the highest one from either chain + // first and move to the next highest. The crucial logic is applied when they have blocks at the + // same height. + loop { + if curr_orig.is_none() { + curr_orig = orig.next(); + } + if curr_update.is_none() { + curr_update = update.next(); + } + + match (curr_orig.as_ref(), curr_update.as_ref()) { + // Update block that doesn't exist in the original chain + (o, Some(u)) if Some(u.height()) > o.map(|o| o.height()) => { + changeset.blocks.insert(u.height(), Some(*u.data())); + prev_update = curr_update.take(); + } + // Original block that isn't in the update + (Some(o), u) if Some(o.height()) > u.map(|u| u.height()) => { + // this block might be gone if an earlier block gets invalidated + potentially_invalidated_heights.push(o.height()); + prev_orig_was_invalidated = false; + prev_orig = curr_orig.take(); + + is_update_height_superset_of_original = false; + + // OPTIMIZATION: we have run out of update blocks so we don't need to continue + // iterating because there's no possibility of adding anything to changeset. + if u.is_none() { + break; + } + } + (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) { + 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_update = curr_update.take(); + prev_orig = curr_orig.take(); + } + (None, None) => { + break; + } + _ => { + unreachable!("compiler cannot tell that everything has been covered") + } + } + } + + // 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. + if !prev_orig_was_invalidated && !point_of_agreement_found { + if let Some(prev_orig) = prev_orig { + return Err(CannotConnectError { + try_include_height: prev_orig.height(), + }); + } + } + + let new_tip = apply_changeset_to_checkpoint(original_tip, &changeset).map_err(|_| { + CannotConnectError { + try_include_height: 0, + } + })?; + Ok((new_tip, changeset)) +} diff --git a/crates/core/src/checkpoint.rs b/crates/core/src/checkpoint.rs index 23d5731f9..04a946932 100644 --- a/crates/core/src/checkpoint.rs +++ b/crates/core/src/checkpoint.rs @@ -1,7 +1,7 @@ use core::ops::RangeBounds; use alloc::sync::Arc; -use bitcoin::BlockHash; +use bitcoin::{block::Header, BlockHash}; use crate::BlockId; @@ -9,52 +9,71 @@ use crate::BlockId; /// /// Checkpoints are cheaply cloneable and are useful to find the agreement point between two sparse /// block chains. -#[derive(Debug, Clone)] -pub struct CheckPoint(Arc); +#[derive(Debug)] +pub struct CheckPoint(Arc>); + +impl Clone for CheckPoint { + 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 { + /// Block data. + block_id: BlockId, + /// Data. + data: B, /// Previous checkpoint (if any). - prev: Option>, + prev: Option>>, +} + +/// 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 PartialEq for CheckPoint { +impl PartialEq for CheckPoint +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 { + /// 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, - ) -> Result> { - 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
, please use [`CheckPoint::from_data`]." + )] + pub fn from_header(header: &bitcoin::block::Header, height: u32) -> Self { + CheckPoint::blockhash_checkpoint_from_header(header, height) } /// Construct a checkpoint from the given `header` and block `height`. @@ -63,7 +82,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 }; @@ -82,55 +101,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 { - 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, + ) -> Result> { + 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) -> Result { - 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) -> Result { + 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` + /// 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.push_data(block.height, block.hash) + } +} + +impl CheckPoint { + /// 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 { + /// Get the previous checkpoint in the chain. + pub fn prev(&self) -> Option> { self.0.prev.clone().map(CheckPoint) } /// Iterate from this checkpoint in descending height. - pub fn iter(&self) -> CheckPointIter { + pub fn iter(&self) -> CheckPointIter { self.clone().into_iter() } @@ -145,7 +199,7 @@ impl CheckPoint { /// /// Note that we always iterate checkpoints in reverse height order (iteration starts at tip /// height). - pub fn range(&self, range: R) -> impl Iterator + pub fn range(&self, range: R) -> impl Iterator> where R: RangeBounds, { @@ -163,46 +217,92 @@ impl CheckPoint { core::ops::Bound::Unbounded => true, }) } +} - /// 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 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`. +impl CheckPoint +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`. /// - /// # Panics + /// 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) -> Result { + 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. /// - /// This panics if called with a genesis block that differs from that of `self`. + /// The effect of `insert` depends on whether a `height` already exists. If it doesn't, the + /// `data` we inserted and all pre-existing `data` at higher heights 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 following it. The returned chain will have a tip with the `data` + /// passed in. Of course, if the `data` was already present then this just returns `self`. #[must_use] - pub fn insert(self, block_id: BlockId) -> Self { + pub fn insert_data(self, height: u32, data: B) -> Self { + assert_ne!(height, 0, "cannot insert the genesis block"); + 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"); - // if we have a conflict we just return the inserted block because the tail is by + // if we have a conflict we just return the inserted data because the tail is by // implication invalid. tail = vec![]; 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 { + 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) @@ -210,12 +310,12 @@ impl CheckPoint { } /// Iterates over checkpoints backwards. -pub struct CheckPointIter { - current: Option>, +pub struct CheckPointIter { + current: Option>>, } -impl Iterator for CheckPointIter { - type Item = CheckPoint; +impl Iterator for CheckPointIter { + type Item = CheckPoint; fn next(&mut self) -> Option { let current = self.current.clone()?; @@ -224,9 +324,9 @@ impl Iterator for CheckPointIter { } } -impl IntoIterator for CheckPoint { - type Item = CheckPoint; - type IntoIter = CheckPointIter; +impl IntoIterator for CheckPoint { + type Item = CheckPoint; + type IntoIter = CheckPointIter; fn into_iter(self) -> Self::IntoIter { CheckPointIter {