//! The [`LocalChain`] is a local implementation of [`ChainOracle`]. use core::convert::Infallible; use core::ops::RangeBounds; use crate::collections::BTreeMap; use crate::{BlockId, ChainOracle}; use alloc::sync::Arc; use bitcoin::block::Header; use bitcoin::BlockHash; /// The [`ChangeSet`] represents changes to [`LocalChain`]. /// /// The key represents the block height, and the value either represents added a new [`CheckPoint`] /// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]). pub type ChangeSet = BTreeMap>; /// A [`LocalChain`] checkpoint is used to find the agreement point between two chains and as a /// transaction anchor. /// /// Each checkpoint contains the height and hash of a block ([`BlockId`]). /// /// Internally, checkpoints are nodes of a reference-counted linked-list. This allows the caller to /// cheaply clone a [`CheckPoint`] without copying the whole list and to view the entire chain /// without holding a lock on [`LocalChain`]. #[derive(Debug, Clone)] pub struct CheckPoint(Arc); /// The internal contents of [`CheckPoint`]. #[derive(Debug, Clone)] struct CPInner { /// Block id (hash and height). block: BlockId, /// Previous checkpoint (if any). prev: Option>, } impl PartialEq for CheckPoint { 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()); self_cps.eq(other_cps) } } impl CheckPoint { /// Construct a new base block at the front of a linked list. pub fn new(block: BlockId) -> Self { Self(Arc::new(CPInner { block, prev: None })) } /// 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. /// /// 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) } /// Construct a checkpoint from the given `header` and block `height`. /// /// 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. /// /// [`prev`]: CheckPoint::prev pub fn from_header(header: &bitcoin::block::Header, height: u32) -> Self { let hash = header.block_hash(); let this_block_id = BlockId { height, hash }; let prev_height = match height.checked_sub(1) { Some(h) => h, None => return Self::new(this_block_id), }; let prev_block_id = BlockId { height: prev_height, hash: header.prev_blockhash, }; CheckPoint::new(prev_block_id) .push(this_block_id) .expect("must construct checkpoint") } /// 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 { if self.height() < block.height { Ok(Self(Arc::new(CPInner { block, prev: Some(self.0), }))) } else { Err(self) } } /// 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) } /// Get the [`BlockId`] of the checkpoint. pub fn block_id(&self) -> BlockId { self.0.block } /// Get the height of the checkpoint. pub fn height(&self) -> u32 { self.0.block.height } /// Get the block hash of the checkpoint. pub fn hash(&self) -> BlockHash { self.0.block.hash } /// 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 { self.clone().into_iter() } /// Get checkpoint at `height`. /// /// Returns `None` if checkpoint at `height` does not exist`. pub fn get(&self, height: u32) -> Option { self.range(height..=height).next() } /// Iterate checkpoints over a height range. /// /// Note that we always iterate checkpoints in reverse height order (iteration starts at tip /// height). pub fn range(&self, range: R) -> impl Iterator where R: RangeBounds, { let start_bound = range.start_bound().cloned(); let end_bound = range.end_bound().cloned(); self.iter() .skip_while(move |cp| match end_bound { core::ops::Bound::Included(inc_bound) => cp.height() > inc_bound, core::ops::Bound::Excluded(exc_bound) => cp.height() >= exc_bound, core::ops::Bound::Unbounded => false, }) .take_while(move |cp| match start_bound { core::ops::Bound::Included(inc_bound) => cp.height() >= inc_bound, core::ops::Bound::Excluded(exc_bound) => cp.height() > exc_bound, 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 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 { assert_ne!(block_id.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 { return self; } // if we have a conflict we just return the inserted block 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 { break cp; } tail.push(cp.block_id()); cp = cp.prev().expect("will break before genesis block"); }; base.extend(core::iter::once(block_id).chain(tail.into_iter().rev())) .expect("tail is in order") } /// Apply `changeset` to the checkpoint. fn apply_changeset(mut self, changeset: &ChangeSet) -> Result { if let Some(start_height) = changeset.keys().next().cloned() { // changes after point of agreement let mut extension = BTreeMap::default(); // point of agreement let mut base: Option = None; for cp in self.iter() { if cp.height() >= start_height { extension.insert(cp.height(), cp.hash()); } else { base = Some(cp); break; } } for (&height, &hash) in changeset { match hash { Some(hash) => { extension.insert(height, hash); } None => { extension.remove(&height); } }; } let new_tip = match base { Some(base) => base .extend(extension.into_iter().map(BlockId::from)) .expect("extension is strictly greater than base"), None => LocalChain::from_blocks(extension)?.tip(), }; self = new_tip; } Ok(self) } } /// Iterates over checkpoints backwards. pub struct CheckPointIter { current: Option>, } impl Iterator for CheckPointIter { type Item = CheckPoint; fn next(&mut self) -> Option { let current = self.current.clone()?; self.current.clone_from(¤t.prev); Some(CheckPoint(current)) } } impl IntoIterator for CheckPoint { type Item = CheckPoint; type IntoIter = CheckPointIter; fn into_iter(self) -> Self::IntoIter { CheckPointIter { current: Some(self.0), } } } /// This is a local implementation of [`ChainOracle`]. #[derive(Debug, Clone, PartialEq)] pub struct LocalChain { tip: CheckPoint, } impl ChainOracle for LocalChain { type Error = Infallible; fn is_block_in_chain( &self, block: BlockId, chain_tip: 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 LocalChain { /// Get the genesis hash. pub fn genesis_hash(&self) -> BlockHash { self.tip.get(0).expect("genesis must exist").hash() } /// Construct [`LocalChain`] from genesis `hash`. #[must_use] pub fn from_genesis_hash(hash: BlockHash) -> (Self, ChangeSet) { let height = 0; let chain = Self { tip: CheckPoint::new(BlockId { height, hash }), }; let changeset = chain.initial_changeset(); (chain, changeset) } /// Construct a [`LocalChain`] from an initial `changeset`. pub fn from_changeset(changeset: ChangeSet) -> Result { let genesis_entry = changeset.get(&0).copied().flatten(); let genesis_hash = match genesis_entry { Some(hash) => hash, None => return Err(MissingGenesisError), }; let (mut chain, _) = Self::from_genesis_hash(genesis_hash); chain.apply_changeset(&changeset)?; debug_assert!(chain._check_changeset_is_applied(&changeset)); Ok(chain) } /// Construct a [`LocalChain`] from a given `checkpoint` tip. pub fn from_tip(tip: CheckPoint) -> Result { let genesis_cp = tip.iter().last().expect("must have at least one element"); if genesis_cp.height() != 0 { return Err(MissingGenesisError); } Ok(Self { tip }) } /// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`]. /// /// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are /// all of the same chain. pub fn from_blocks(blocks: BTreeMap) -> Result { if !blocks.contains_key(&0) { return Err(MissingGenesisError); } let mut tip: Option = None; for block in &blocks { match tip { Some(curr) => { tip = Some( curr.push(BlockId::from(block)) .expect("BTreeMap is ordered"), ) } None => tip = Some(CheckPoint::new(BlockId::from(block))), } } Ok(Self { tip: tip.expect("already checked to have genesis"), }) } /// Get the highest checkpoint. pub fn tip(&self) -> CheckPoint { self.tip.clone() } /// Applies the given `update` to the chain. /// /// The method returns [`ChangeSet`] on success. This represents the changes applied to `self`. /// /// There must be no ambiguity about which of the existing chain's blocks are still valid and /// which are now invalid. That is, the new chain must implicitly connect to a definite block in /// the existing chain and invalidate the block after it (if it exists) by including a block at /// the same height but with a different hash to explicitly exclude it as a connection point. /// /// # Errors /// /// An error will occur if the update does not correctly connect with `self`. /// /// [module-level documentation]: crate::local_chain pub fn apply_update(&mut self, update: CheckPoint) -> Result { let (new_tip, changeset) = merge_chains(self.tip.clone(), update)?; self.tip = new_tip; self._check_changeset_is_applied(&changeset); Ok(changeset) } /// Update the chain with a given [`Header`] at `height` which you claim is connected to a existing block in the chain. /// /// This is useful when you have a block header that you want to record as part of the chain but /// don't necessarily know that the `prev_blockhash` is in the chain. /// /// This will usually insert two new [`BlockId`]s into the chain: the header's block and the /// header's `prev_blockhash` block. `connected_to` must already be in the chain but is allowed /// to be `prev_blockhash` (in which case only one new block id will be inserted). /// To be successful, `connected_to` must be chosen carefully so that `LocalChain`'s [update /// rules][`apply_update`] are satisfied. /// /// # Errors /// /// [`ApplyHeaderError::InconsistentBlocks`] occurs if the `connected_to` block and the /// [`Header`] is inconsistent. For example, if the `connected_to` block is the same height as /// `header` or `prev_blockhash`, but has a different block hash. Or if the `connected_to` /// height is greater than the header's `height`. /// /// [`ApplyHeaderError::CannotConnect`] occurs if the internal call to [`apply_update`] fails. /// /// [`apply_update`]: Self::apply_update pub fn apply_header_connected_to( &mut self, header: &Header, height: u32, connected_to: BlockId, ) -> Result { let this = BlockId { height, hash: header.block_hash(), }; let prev = height.checked_sub(1).map(|prev_height| BlockId { height: prev_height, hash: header.prev_blockhash, }); let conn = match connected_to { // `connected_to` can be ignored if same as `this` or `prev` (duplicate) conn if conn == this || Some(conn) == prev => None, // this occurs if: // - `connected_to` height is the same as `prev`, but different hash // - `connected_to` height is the same as `this`, but different hash // - `connected_to` height is greater than `this` (this is not allowed) conn if conn.height >= height.saturating_sub(1) => { return Err(ApplyHeaderError::InconsistentBlocks) } conn => Some(conn), }; let update = CheckPoint::from_block_ids([conn, prev, Some(this)].into_iter().flatten()) .expect("block ids must be in order"); self.apply_update(update) .map_err(ApplyHeaderError::CannotConnect) } /// Update the chain with a given [`Header`] connecting it with the previous block. /// /// This is a convenience method to call [`apply_header_connected_to`] with the `connected_to` /// parameter being `height-1:prev_blockhash`. If there is no previous block (i.e. genesis), we /// use the current block as `connected_to`. /// /// [`apply_header_connected_to`]: LocalChain::apply_header_connected_to pub fn apply_header( &mut self, header: &Header, height: u32, ) -> Result { let connected_to = match height.checked_sub(1) { Some(prev_height) => BlockId { height: prev_height, hash: header.prev_blockhash, }, None => BlockId { height, hash: header.block_hash(), }, }; self.apply_header_connected_to(header, height, connected_to) .map_err(|err| match err { ApplyHeaderError::InconsistentBlocks => { unreachable!("connected_to is derived from the block so is always consistent") } ApplyHeaderError::CannotConnect(err) => err, }) } /// Apply the given `changeset`. pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> { let old_tip = self.tip.clone(); let new_tip = old_tip.apply_changeset(changeset)?; self.tip = new_tip; debug_assert!(self._check_changeset_is_applied(changeset)); Ok(()) } /// Insert a [`BlockId`]. /// /// # Errors /// /// Replacing the block hash of an existing checkpoint will result in an error. pub fn insert_block(&mut self, block_id: BlockId) -> Result { if let Some(original_cp) = self.tip.get(block_id.height) { let original_hash = original_cp.hash(); if original_hash != block_id.hash { return Err(AlterCheckPointError { height: block_id.height, original_hash, update_hash: Some(block_id.hash), }); } return Ok(ChangeSet::default()); } let mut changeset = ChangeSet::default(); changeset.insert(block_id.height, Some(block_id.hash)); self.apply_changeset(&changeset) .map_err(|_| AlterCheckPointError { height: 0, original_hash: self.genesis_hash(), update_hash: changeset.get(&0).cloned().flatten(), })?; Ok(changeset) } /// Removes blocks from (and inclusive of) the given `block_id`. /// /// This will remove blocks with a height equal or greater than `block_id`, but only if /// `block_id` exists in the chain. /// /// # Errors /// /// This will fail with [`MissingGenesisError`] if the caller attempts to disconnect from the /// genesis block. pub fn disconnect_from(&mut self, block_id: BlockId) -> Result { let mut remove_from = Option::::None; let mut changeset = ChangeSet::default(); for cp in self.tip().iter() { let cp_id = cp.block_id(); if cp_id.height < block_id.height { break; } changeset.insert(cp_id.height, None); if cp_id == block_id { remove_from = Some(cp); } } self.tip = match remove_from.map(|cp| cp.prev()) { // The checkpoint below the earliest checkpoint to remove will be the new tip. Some(Some(new_tip)) => new_tip, // If there is no checkpoint below the earliest checkpoint to remove, it means the // "earliest checkpoint to remove" is the genesis block. We disallow removing the // genesis block. Some(None) => return Err(MissingGenesisError), // If there is nothing to remove, we return an empty changeset. None => return Ok(ChangeSet::default()), }; Ok(changeset) } /// 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 { self.tip .iter() .map(|cp| { let block_id = cp.block_id(); (block_id.height, Some(block_id.hash)) }) .collect() } /// Iterate over checkpoints in descending height order. pub fn iter_checkpoints(&self) -> CheckPointIter { CheckPointIter { current: Some(self.tip.0.clone()), } } fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool { let mut curr_cp = self.tip.clone(); for (height, exp_hash) in changeset.iter().rev() { match curr_cp.get(*height) { Some(query_cp) => { if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash { return false; } curr_cp = query_cp; } None => { if exp_hash.is_some() { return false; } } } } true } /// Get checkpoint at given `height` (if it exists). /// /// This is a shorthand for calling [`CheckPoint::get`] on the [`tip`]. /// /// [`tip`]: LocalChain::tip pub fn get(&self, height: u32) -> Option { self.tip.get(height) } /// Iterate checkpoints over a height range. /// /// Note that we always iterate checkpoints in reverse height order (iteration starts at tip /// height). /// /// This is a shorthand for calling [`CheckPoint::range`] on the [`tip`]. /// /// [`tip`]: LocalChain::tip pub fn range(&self, range: R) -> impl Iterator where R: RangeBounds, { self.tip.range(range) } } /// An error which occurs when a [`LocalChain`] 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 {} /// Represents a failure when trying to insert/remove a checkpoint to/from [`LocalChain`]. #[derive(Clone, Debug, PartialEq)] pub struct AlterCheckPointError { /// The checkpoint's height. pub height: u32, /// The original checkpoint's block hash which cannot be replaced/removed. pub original_hash: BlockHash, /// The attempted update to the `original_block` hash. pub update_hash: Option, } impl core::fmt::Display for AlterCheckPointError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self.update_hash { Some(update_hash) => write!( f, "failed to insert block at height {}: original={} update={}", self.height, self.original_hash, update_hash ), None => write!( f, "failed to remove block at height {}: original={}", self.height, self.original_hash ), } } } #[cfg(feature = "std")] impl std::error::Error for AlterCheckPointError {} /// 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.insert(u.height(), Some(u.hash())); 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 Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) { if is_update_height_superset_of_original { return Ok((update_tip, changeset)); } else { let new_tip = original_tip.apply_changeset(&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.insert(u.height(), Some(u.hash())); for invalidated_height in potentially_invalidated_heights.drain(..) { changeset.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 = original_tip .apply_changeset(&changeset) .map_err(|_| CannotConnectError { try_include_height: 0, })?; Ok((new_tip, changeset)) }