From 1e566662bc4ae5ec8400eaa486d269e7ba0acce9 Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Tue, 10 Sep 2024 23:35:23 +0800 Subject: [PATCH] fixup! refactor(chain): `LocalChain` takes a generic --- crates/chain/src/local_chain.rs | 232 +++++++++++++++++++------------- 1 file changed, 141 insertions(+), 91 deletions(-) diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index af7950064..9952272e9 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -10,15 +10,18 @@ use bitcoin::block::Header; use bitcoin::BlockHash; /// Apply `changeset` to the checkpoint. -fn apply_changeset_to_checkpoint( - mut init_cp: CheckPoint, - changeset: &ChangeSet, -) -> Result { +fn apply_changeset_to_checkpoint( + mut init_cp: CheckPoint, + changeset: &ChangeSet, +) -> Result, MissingGenesisError> +where + B: Copy + core::fmt::Debug + bdk_core::ToBlockHash, +{ 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; + let mut base: Option> = None; for cp in init_cp.iter() { if cp.height() >= start_height { @@ -44,7 +47,7 @@ fn apply_changeset_to_checkpoint( Some(base) => base .extend_data(extension) .expect("extension is strictly greater than base"), - None => LocalChain::from_blocks(extension)?.tip(), + None => LocalChain::from_data(extension)?.tip(), }; init_cp = new_tip; } @@ -96,11 +99,6 @@ where } impl LocalChain { - /// Get the genesis hash. - pub fn genesis_hash(&self) -> BlockHash { - self.tip.get(0).expect("genesis must exist").block_id().hash - } - /// Construct [`LocalChain`] from genesis `hash`. #[must_use] pub fn from_genesis_hash(hash: BlockHash) -> (Self, ChangeSet) { @@ -128,45 +126,12 @@ impl LocalChain { 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() + LocalChain::from_data(blocks) } /// Applies the given `update` to the chain. @@ -278,11 +243,7 @@ impl LocalChain { /// 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.tip = new_tip; - debug_assert!(self._check_changeset_is_applied(changeset)); - Ok(()) + self.apply_data_changeset(changeset) } /// Insert a [`BlockId`]. @@ -291,29 +252,7 @@ impl LocalChain { /// /// 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 - .blocks - .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.blocks.get(&0).cloned().flatten(), - })?; - Ok(changeset) + self.insert_data(block_id.height, block_id.hash) } /// Removes blocks from (and inclusive of) the given `block_id`. @@ -372,23 +311,28 @@ impl LocalChain { } fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool { - let mut curr_cp = self.tip.clone(); - for (height, exp_hash) in changeset.blocks.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; - } - } - } + self._check_data_changeset_is_applied(changeset) + } +} + +impl LocalChain { + /// Get the highest checkpoint. + pub fn tip(&self) -> CheckPoint { + self.tip.clone() + } + + /// Get the genesis hash. + pub fn genesis_hash(&self) -> BlockHash { + self.tip.get(0).expect("genesis must exist").block_id().hash + } + + /// 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); } - true + Ok(Self { tip }) } /// Get checkpoint at given `height` (if it exists). @@ -396,7 +340,7 @@ impl LocalChain { /// This is a shorthand for calling [`CheckPoint::get`] on the [`tip`]. /// /// [`tip`]: LocalChain::tip - pub fn get(&self, height: u32) -> Option { + pub fn get(&self, height: u32) -> Option> { self.tip.get(height) } @@ -408,7 +352,7 @@ impl LocalChain { /// This is a shorthand for calling [`CheckPoint::range`] on the [`tip`]. /// /// [`tip`]: LocalChain::tip - pub fn range(&self, range: R) -> impl Iterator + pub fn range(&self, range: R) -> impl Iterator> where R: RangeBounds, { @@ -416,6 +360,112 @@ impl LocalChain { } } +impl LocalChain +where + B: Copy + core::fmt::Debug + bdk_core::ToBlockHash, +{ + /// Apply the given `changeset`. + pub fn apply_data_changeset( + &mut self, + changeset: &ChangeSet, + ) -> Result<(), MissingGenesisError> { + let old_tip = self.tip.clone(); + let new_tip = apply_changeset_to_checkpoint(old_tip, changeset)?; + self.tip = new_tip; + debug_assert!(self._check_data_changeset_is_applied(changeset)); + Ok(()) + } + + fn _check_data_changeset_is_applied(&self, changeset: &ChangeSet) -> bool { + let mut curr_cp = self.tip.clone(); + for (height, data) in changeset.blocks.iter().rev() { + match curr_cp.get(*height) { + Some(query_cp) => { + if let Some(data) = data { + if query_cp.height() != *height || query_cp.hash() != data.to_blockhash() { + return false; + } + } else { + return false; + } + curr_cp = query_cp; + } + None => { + if data.is_some() { + return false; + } + } + } + } + true + } + + /// Insert data into a [`LocalChain`]. + /// + /// # Errors + /// + /// Replacing the block hash of an existing checkpoint will result in an error. + pub fn insert_data( + &mut self, + height: u32, + data: B, + ) -> Result, AlterCheckPointError> { + if let Some(original_cp) = self.tip.get(height) { + let original_hash = original_cp.hash(); + if original_hash != data.to_blockhash() { + return Err(AlterCheckPointError { + height, + original_hash, + update_hash: Some(data.to_blockhash()), + }); + } + return Ok(ChangeSet::default()); + } + + let mut changeset = ChangeSet::::default(); + changeset.blocks.insert(height, Some(data)); + self.apply_data_changeset(&changeset) + .map_err(|_| AlterCheckPointError { + height: 0, + original_hash: self.genesis_hash(), + update_hash: Some( + changeset + .blocks + .get(&0) + .cloned() + .flatten() + .unwrap() + .to_blockhash(), + ), + })?; + Ok(changeset) + } + + /// Constructs a [`LocalChain`] from a [`BTreeMap`] of height and data. + /// + /// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are + /// all of the same chain. + pub fn from_data(blocks: BTreeMap) -> Result { + if !blocks.contains_key(&0) { + return Err(MissingGenesisError); + } + + let mut tip: Option> = None; + for (height, data) in &blocks { + match tip { + Some(curr) => { + tip = Some(curr.push_data(*height, *data).expect("BTreeMap is ordered")) + } + None => tip = Some(CheckPoint::from_data(*height, *data)), + } + } + + Ok(Self { + tip: tip.expect("already checked to have genesis"), + }) + } +} + /// The [`ChangeSet`] represents changes to [`LocalChain`]. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]