fn apply_changeset_to_checkpoint<D>(
mut init_cp: CheckPoint<D>,
changeset: &ChangeSet<D>,
-) -> Result<CheckPoint<D>, ApplyBlockError>
+) -> Result<CheckPoint<D>, ApplyChangeSetError>
where
D: ToBlockHash + fmt::Debug + Clone,
{
};
}
- let new_tip = match base {
- Some(base) => base
- .extend(extension)
- // Since `extension` is in height order, the only failure case is `prev_blockhash`
- // mismatch.
- .map_err(|last_cp| ApplyBlockError::PrevBlockhashMismatch {
- expected: last_cp.block_id(),
- })?,
- None => LocalChain::from_blocks(extension)?.tip(),
+ let base = match base {
+ Some(base) => base,
+ // No point of agreement means `changeset` touches genesis, which cannot be altered.
+ None => {
+ let genesis = init_cp.get(0).expect("`LocalChain` always has genesis");
+ let update_hash = extension.remove(&0).as_ref().map(ToBlockHash::to_blockhash);
+ if update_hash != Some(genesis.hash()) {
+ return Err(ApplyChangeSetError::CannotAlterGenesis {
+ original_hash: genesis.hash(),
+ update_hash,
+ });
+ }
+ genesis
+ }
};
- init_cp = new_tip;
+
+ init_cp = base
+ .extend(extension)
+ // Since `extension` is in height order, the only failure case is `prev_blockhash`
+ // mismatch.
+ .map_err(|last_cp| ApplyChangeSetError::PrevBlockhashMismatch {
+ expected: last_cp.block_id(),
+ })?;
}
Ok(init_cp)
}
/// Construct a [`LocalChain`] from an initial `changeset`.
+ ///
+ /// # Errors
+ ///
+ /// Fails with [`ApplyBlockError::MissingGenesis`] if `changeset` has no genesis block, or
+ /// [`ApplyBlockError::PrevBlockhashMismatch`] if its blocks do not link by `prev_blockhash`.
pub fn from_changeset(changeset: ChangeSet<D>) -> Result<Self, ApplyBlockError> {
- let genesis_entry = changeset.blocks.get(&0).cloned().flatten();
- let genesis_data = match genesis_entry {
- Some(data) => data,
- None => return Err(ApplyBlockError::MissingGenesis),
- };
-
- let (mut chain, _) = Self::from_genesis(genesis_data);
- chain.apply_changeset(&changeset)?;
+ let chain = Self::from_blocks(
+ changeset
+ .blocks
+ .iter()
+ .filter_map(|(&height, data)| Some((height, data.clone()?)))
+ .collect(),
+ )?;
debug_assert!(chain._check_changeset_is_applied(&changeset));
Ok(chain)
}
}
/// Apply the given `changeset`.
- pub fn apply_changeset(&mut self, changeset: &ChangeSet<D>) -> Result<(), ApplyBlockError> {
+ ///
+ /// # Errors
+ ///
+ /// Fails if `changeset` replaces or removes the genesis block, or if the resulting blocks do
+ /// not link by `prev_blockhash`. `self` is unchanged on error.
+ pub fn apply_changeset(&mut self, changeset: &ChangeSet<D>) -> Result<(), ApplyChangeSetError> {
let old_tip = self.tip.clone();
let new_tip = apply_changeset_to_checkpoint(old_tip, changeset)?;
self.tip = new_tip;
impl core::error::Error for ApplyBlockError {}
+/// Error when applying a [`ChangeSet`] to a [`LocalChain`].
+#[derive(Clone, Debug, PartialEq)]
+#[non_exhaustive]
+pub enum ApplyChangeSetError {
+ /// The changeset attempts to replace or remove the genesis block.
+ CannotAlterGenesis {
+ /// The genesis block hash which cannot be replaced or removed.
+ original_hash: BlockHash,
+ /// The attempted replacement, or `None` if the changeset removes genesis.
+ update_hash: Option<BlockHash>,
+ },
+ /// Block's `prev_blockhash` doesn't match the expected block.
+ PrevBlockhashMismatch {
+ /// The block that `prev_blockhash` should reference.
+ expected: BlockId,
+ },
+}
+
+impl core::fmt::Display for ApplyChangeSetError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ ApplyChangeSetError::CannotAlterGenesis {
+ original_hash,
+ update_hash: Some(update_hash),
+ } => write!(
+ f,
+ "cannot replace genesis block: original={original_hash} update={update_hash}"
+ ),
+ ApplyChangeSetError::CannotAlterGenesis {
+ original_hash,
+ update_hash: None,
+ } => write!(f, "cannot remove genesis block: original={original_hash}"),
+ ApplyChangeSetError::PrevBlockhashMismatch { expected } => write!(
+ f,
+ "`prev_blockhash` doesn't match block at height {} ({})",
+ expected.height, expected.hash
+ ),
+ }
+ }
+}
+
+impl core::error::Error for ApplyChangeSetError {}
+
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
#[derive(Clone, Debug, PartialEq)]
pub struct MissingGenesisError;
{
let new_tip = apply_changeset_to_checkpoint(original_tip, &changeset).map_err(|err| {
match err {
- ApplyBlockError::MissingGenesis => CannotConnectError {
- try_include_height: 0,
- },
+ // `merge_chains` rejects a genesis mismatch before reaching here.
+ err @ ApplyChangeSetError::CannotAlterGenesis { .. } => {
+ debug_assert!(false, "merge_chains should have rejected {err:?}");
+ CannotConnectError {
+ try_include_height: 0,
+ }
+ }
// The merge iteration is supposed to detect `prev_blockhash` conflicts and resolve
// them by invalidating conflicting blocks in the changeset. Reaching this arm means
// either the original chain was internally inconsistent or the iteration missed a
// case — a bug on our side. Debug builds panic; release builds surface the height
// where the mismatch surfaced so the caller at least has a useful pointer.
- ApplyBlockError::PrevBlockhashMismatch { expected } => {
+ ApplyChangeSetError::PrevBlockhashMismatch { expected } => {
debug_assert!(
false,
"merge_chains should have resolved prev_blockhash mismatch at {expected:?}",
use bdk_chain::{
local_chain::{
- AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
- LocalChain, MissingGenesisError,
+ AlterCheckPointError, ApplyBlockError, ApplyChangeSetError, ApplyHeaderError,
+ CannotConnectError, ChangeSet, CheckPoint, LocalChain, MissingGenesisError,
},
BlockId,
};
}
}
-#[test]
-fn local_chain_insert_header() {
- fn header(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,
- }
+fn header(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,
}
+}
+#[test]
+fn local_chain_insert_header() {
// Create consecutive headers of height `n`, where the genesis header is height 0.
fn build_headers(n: u32) -> Vec<Header> {
let mut headers = Vec::new();
}
}
+#[test]
+fn local_chain_apply_changeset_cannot_alter_genesis() {
+ let chain: LocalChain = local_chain![(0, hash!("G")), (1, hash!("A"))];
+ for (update_hash, changeset) in [
+ (
+ Some(hash!("not_G")),
+ ChangeSet::from([(0, Some(hash!("not_G")))]),
+ ),
+ (None, ChangeSet::from([(0, None)])),
+ ] {
+ let mut chain = chain.clone();
+ assert_eq!(
+ chain.apply_changeset(&changeset),
+ Err(ApplyChangeSetError::CannotAlterGenesis {
+ original_hash: hash!("G"),
+ update_hash,
+ }),
+ );
+ assert_eq!(chain, local_chain![(0, hash!("G")), (1, hash!("A"))]);
+ }
+
+ // A matching genesis entry is fine.
+ let mut chain = chain;
+ let changeset = ChangeSet::from([(0, Some(hash!("G"))), (2, Some(hash!("B")))]);
+ assert_eq!(chain.apply_changeset(&changeset), Ok(()));
+ assert_eq!(
+ chain,
+ local_chain![(0, hash!("G")), (1, hash!("A")), (2, hash!("B"))]
+ );
+}
+
+#[test]
+fn local_chain_apply_changeset_with_genesis_checks_prev_blockhash() {
+ let genesis = header(hash!("_"));
+ let (mut chain, _) = LocalChain::from_genesis(genesis);
+ let changeset = ChangeSet::from([(0, Some(genesis)), (1, Some(header(hash!("not_G"))))]);
+ assert_eq!(
+ chain.apply_changeset(&changeset),
+ Err(ApplyChangeSetError::PrevBlockhashMismatch {
+ expected: BlockId {
+ height: 0,
+ hash: genesis.block_hash(),
+ },
+ }),
+ );
+ assert_eq!(chain, LocalChain::from_genesis(genesis).0);
+}
+
+#[test]
+fn local_chain_from_changeset_without_genesis() {
+ let changeset: ChangeSet = [(0, None), (1, Some(hash!("A")))].into_iter().collect();
+ assert_eq!(
+ LocalChain::from_changeset(changeset),
+ Err(ApplyBlockError::MissingGenesis),
+ );
+}
+
// Test that `apply_update` can connect 1 `Header` at a time
// and fails if a `prev_blockhash` conflict is detected.
#[test]