]> Untitled Git - bdk/commitdiff
feat(chain): add `apply_header..` methods to `LocalChain`
author志宇 <hello@evanlinjin.me>
Wed, 10 Jan 2024 16:05:04 +0000 (00:05 +0800)
committer志宇 <hello@evanlinjin.me>
Mon, 15 Jan 2024 16:27:00 +0000 (00:27 +0800)
These are convenience methods to transform a header into checkpoints to
update the `LocalChain` with. Tests are included.

crates/chain/src/local_chain.rs
crates/chain/tests/test_local_chain.rs

index f6d8af9f6af0069de186ecba6780d0e4ed2c8fcf..a4c226151f5b2a5ad27517efe43c613e160cc347 100644 (file)
@@ -5,6 +5,7 @@ use core::convert::Infallible;
 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`].
@@ -369,6 +370,91 @@ impl LocalChain {
         Ok(changeset)
     }
 
+    /// Update the chain with a given [`Header`] and a `connected_to` [`BlockId`].
+    ///
+    /// The `header` will be transformed into checkpoints - one for the current block and one for
+    /// the previous block. Note that a genesis header will be transformed into only one checkpoint
+    /// (as there are no previous blocks). The checkpoints will be applied to the chain via
+    /// [`apply_update`].
+    ///
+    /// # 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`]: LocalChain::apply_update
+    pub fn apply_header_connected_to(
+        &mut self,
+        header: &Header,
+        height: u32,
+        connected_to: BlockId,
+    ) -> Result<ChangeSet, ApplyHeaderError> {
+        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 = Update {
+            tip: CheckPoint::from_block_ids([conn, prev, Some(this)].into_iter().flatten())
+                .expect("block ids must be in order"),
+            introduce_older_blocks: false,
+        };
+
+        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<ChangeSet, CannotConnectError> {
+        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> {
         if let Some(start_height) = changeset.keys().next().cloned() {
@@ -579,6 +665,30 @@ impl core::fmt::Display for CannotConnectError {
 #[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 {}
+
 fn merge_chains(
     original_tip: CheckPoint,
     update_tip: CheckPoint,
index 7e6f73bf2cf7f29bc6d47839e9114744b5cf496d..c190ae52bd49b9aa7e895de846f012aef3dad2fe 100644 (file)
@@ -1,11 +1,11 @@
 use bdk_chain::{
     local_chain::{
-        AlterCheckPointError, CannotConnectError, ChangeSet, CheckPoint, LocalChain,
-        MissingGenesisError, Update,
+        AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
+        LocalChain, MissingGenesisError, Update,
     },
     BlockId,
 };
-use bitcoin::BlockHash;
+use bitcoin::{block::Header, hashes::Hash, BlockHash};
 
 #[macro_use]
 mod common;
@@ -506,3 +506,155 @@ fn checkpoint_from_block_ids() {
         }
     }
 }
+
+#[test]
+fn local_chain_apply_header_connected_to() {
+    fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
+        Header {
+            version: bitcoin::block::Version::default(),
+            prev_blockhash,
+            merkle_root: bitcoin::hash_types::TxMerkleNode::all_zeros(),
+            time: 0,
+            bits: bitcoin::CompactTarget::default(),
+            nonce: 0,
+        }
+    }
+
+    struct TestCase {
+        name: &'static str,
+        chain: LocalChain,
+        header: Header,
+        height: u32,
+        connected_to: BlockId,
+        exp_result: Result<Vec<(u32, Option<BlockHash>)>, ApplyHeaderError>,
+    }
+
+    let test_cases = [
+        {
+            let header = header_from_prev_blockhash(h!("A"));
+            let hash = header.block_hash();
+            let height = 2;
+            let connected_to = BlockId { height, hash };
+            TestCase {
+                name: "connected_to_self_header_applied_to_self",
+                chain: local_chain![(0, h!("_")), (height, hash)],
+                header,
+                height,
+                connected_to,
+                exp_result: Ok(vec![]),
+            }
+        },
+        {
+            let prev_hash = h!("A");
+            let prev_height = 1;
+            let header = header_from_prev_blockhash(prev_hash);
+            let hash = header.block_hash();
+            let height = prev_height + 1;
+            let connected_to = BlockId {
+                height: prev_height,
+                hash: prev_hash,
+            };
+            TestCase {
+                name: "connected_to_prev_header_applied_to_self",
+                chain: local_chain![(0, h!("_")), (prev_height, prev_hash)],
+                header,
+                height,
+                connected_to,
+                exp_result: Ok(vec![(height, Some(hash))]),
+            }
+        },
+        {
+            let header = header_from_prev_blockhash(BlockHash::all_zeros());
+            let hash = header.block_hash();
+            let height = 0;
+            let connected_to = BlockId { height, hash };
+            TestCase {
+                name: "genesis_applied_to_self",
+                chain: local_chain![(0, hash)],
+                header,
+                height,
+                connected_to,
+                exp_result: Ok(vec![]),
+            }
+        },
+        {
+            let header = header_from_prev_blockhash(h!("Z"));
+            let height = 10;
+            let hash = header.block_hash();
+            let prev_height = height - 1;
+            let prev_hash = header.prev_blockhash;
+            TestCase {
+                name: "connect_at_connected_to",
+                chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
+                header,
+                height: 10,
+                connected_to: BlockId {
+                    height: 3,
+                    hash: h!("C"),
+                },
+                exp_result: Ok(vec![(prev_height, Some(prev_hash)), (height, Some(hash))]),
+            }
+        },
+        {
+            let prev_hash = h!("A");
+            let prev_height = 1;
+            let header = header_from_prev_blockhash(prev_hash);
+            let connected_to = BlockId {
+                height: prev_height,
+                hash: h!("not_prev_hash"),
+            };
+            TestCase {
+                name: "inconsistent_prev_hash",
+                chain: local_chain![(0, h!("_")), (prev_height, h!("not_prev_hash"))],
+                header,
+                height: prev_height + 1,
+                connected_to,
+                exp_result: Err(ApplyHeaderError::InconsistentBlocks),
+            }
+        },
+        {
+            let prev_hash = h!("A");
+            let prev_height = 1;
+            let header = header_from_prev_blockhash(prev_hash);
+            let height = prev_height + 1;
+            let connected_to = BlockId {
+                height,
+                hash: h!("not_current_hash"),
+            };
+            TestCase {
+                name: "inconsistent_current_block",
+                chain: local_chain![(0, h!("_")), (height, h!("not_current_hash"))],
+                header,
+                height,
+                connected_to,
+                exp_result: Err(ApplyHeaderError::InconsistentBlocks),
+            }
+        },
+        {
+            let header = header_from_prev_blockhash(h!("B"));
+            let height = 3;
+            let connected_to = BlockId {
+                height: 4,
+                hash: h!("D"),
+            };
+            TestCase {
+                name: "connected_to_is_greater",
+                chain: local_chain![(0, h!("_")), (2, h!("B"))],
+                header,
+                height,
+                connected_to,
+                exp_result: Err(ApplyHeaderError::InconsistentBlocks),
+            }
+        },
+    ];
+
+    for (i, t) in test_cases.into_iter().enumerate() {
+        println!("running test case {}: '{}'", i, t.name);
+        let mut chain = t.chain;
+        let result = chain.apply_header_connected_to(&t.header, t.height, t.connected_to);
+        let exp_result = t
+            .exp_result
+            .map(|cs| cs.iter().cloned().collect::<ChangeSet>());
+        assert_eq!(result, exp_result, "[{}:{}] unexpected result", i, t.name);
+    }
+}