feat(chain): add apply_header..
methods to LocalChain
These are convenience methods to transform a header into checkpoints to update the `LocalChain` with. Tests are included.
This commit is contained in:
parent
2b61a122ff
commit
d3e5095df1
@ -5,6 +5,7 @@ use core::convert::Infallible;
|
|||||||
use crate::collections::BTreeMap;
|
use crate::collections::BTreeMap;
|
||||||
use crate::{BlockId, ChainOracle};
|
use crate::{BlockId, ChainOracle};
|
||||||
use alloc::sync::Arc;
|
use alloc::sync::Arc;
|
||||||
|
use bitcoin::block::Header;
|
||||||
use bitcoin::BlockHash;
|
use bitcoin::BlockHash;
|
||||||
|
|
||||||
/// The [`ChangeSet`] represents changes to [`LocalChain`].
|
/// The [`ChangeSet`] represents changes to [`LocalChain`].
|
||||||
@ -369,6 +370,91 @@ impl LocalChain {
|
|||||||
Ok(changeset)
|
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`.
|
/// Apply the given `changeset`.
|
||||||
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
|
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
|
||||||
if let Some(start_height) = changeset.keys().next().cloned() {
|
if let Some(start_height) = changeset.keys().next().cloned() {
|
||||||
@ -579,6 +665,30 @@ impl core::fmt::Display for CannotConnectError {
|
|||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
impl std::error::Error for CannotConnectError {}
|
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(
|
fn merge_chains(
|
||||||
original_tip: CheckPoint,
|
original_tip: CheckPoint,
|
||||||
update_tip: CheckPoint,
|
update_tip: CheckPoint,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
local_chain::{
|
local_chain::{
|
||||||
AlterCheckPointError, CannotConnectError, ChangeSet, CheckPoint, LocalChain,
|
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
|
||||||
MissingGenesisError, Update,
|
LocalChain, MissingGenesisError, Update,
|
||||||
},
|
},
|
||||||
BlockId,
|
BlockId,
|
||||||
};
|
};
|
||||||
use bitcoin::BlockHash;
|
use bitcoin::{block::Header, hashes::Hash, BlockHash};
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod common;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user