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`].
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() {
#[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,
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;
}
}
}
+
+#[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);
+ }
+}