[bdk_chain_redesign] Test LocalChain
This is mostly copying over the relevant tests from `SparseChain`. Changes are made to `local_chain::ChangeSet` to re-add the ability to remove blocks.
This commit is contained in:
parent
7175a82c04
commit
1003fe2ee6
@ -62,6 +62,15 @@ impl From<BTreeMap<u32, BlockHash>> for LocalChain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LocalChain {
|
impl LocalChain {
|
||||||
|
pub fn from_blocks<B>(blocks: B) -> Self
|
||||||
|
where
|
||||||
|
B: IntoIterator<Item = BlockId>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
blocks: blocks.into_iter().map(|b| (b.height, b.hash)).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tip(&self) -> Option<BlockId> {
|
pub fn tip(&self) -> Option<BlockId> {
|
||||||
self.blocks
|
self.blocks
|
||||||
.iter()
|
.iter()
|
||||||
@ -109,19 +118,37 @@ impl LocalChain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut changeset = BTreeMap::<u32, BlockHash>::new();
|
let mut changeset: BTreeMap<u32, Option<BlockHash>> = match invalidate_from_height {
|
||||||
for (height, new_hash) in update {
|
Some(first_invalid_height) => {
|
||||||
|
// the first block of height to invalidate should be represented in the update
|
||||||
|
if !update.contains_key(&first_invalid_height) {
|
||||||
|
return Err(UpdateNotConnectedError(first_invalid_height));
|
||||||
|
}
|
||||||
|
self.blocks
|
||||||
|
.range(first_invalid_height..)
|
||||||
|
.map(|(height, _)| (*height, None))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
None => BTreeMap::new(),
|
||||||
|
};
|
||||||
|
for (height, update_hash) in update {
|
||||||
let original_hash = self.blocks.get(height);
|
let original_hash = self.blocks.get(height);
|
||||||
if Some(new_hash) != original_hash {
|
if Some(update_hash) != original_hash {
|
||||||
changeset.insert(*height, *new_hash);
|
changeset.insert(*height, Some(*update_hash));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(changeset)
|
Ok(changeset)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies the given `changeset`.
|
/// Applies the given `changeset`.
|
||||||
pub fn apply_changeset(&mut self, mut changeset: ChangeSet) {
|
pub fn apply_changeset(&mut self, changeset: ChangeSet) {
|
||||||
self.blocks.append(&mut changeset)
|
for (height, blockhash) in changeset {
|
||||||
|
match blockhash {
|
||||||
|
Some(blockhash) => self.blocks.insert(height, blockhash),
|
||||||
|
None => self.blocks.remove(&height),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates [`LocalChain`] with an update [`LocalChain`].
|
/// Updates [`LocalChain`] with an update [`LocalChain`].
|
||||||
@ -137,7 +164,10 @@ impl LocalChain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn initial_changeset(&self) -> ChangeSet {
|
pub fn initial_changeset(&self) -> ChangeSet {
|
||||||
self.blocks.clone()
|
self.blocks
|
||||||
|
.iter()
|
||||||
|
.map(|(&height, &hash)| (height, Some(hash)))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn heights(&self) -> BTreeSet<u32> {
|
pub fn heights(&self) -> BTreeSet<u32> {
|
||||||
@ -148,7 +178,7 @@ impl LocalChain {
|
|||||||
/// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`].
|
/// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`].
|
||||||
///
|
///
|
||||||
/// [`determine_changeset`]: LocalChain::determine_changeset
|
/// [`determine_changeset`]: LocalChain::determine_changeset
|
||||||
type ChangeSet = BTreeMap<u32, BlockHash>;
|
pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
|
||||||
|
|
||||||
impl Append for ChangeSet {
|
impl Append for ChangeSet {
|
||||||
fn append(&mut self, mut other: Self) {
|
fn append(&mut self, mut other: Self) {
|
||||||
@ -163,7 +193,7 @@ impl Append for ChangeSet {
|
|||||||
/// connect to the existing chain. This error case contains the checkpoint height to include so
|
/// connect to the existing chain. This error case contains the checkpoint height to include so
|
||||||
/// that the chains can connect.
|
/// that the chains can connect.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct UpdateNotConnectedError(u32);
|
pub struct UpdateNotConnectedError(pub u32);
|
||||||
|
|
||||||
impl core::fmt::Display for UpdateNotConnectedError {
|
impl core::fmt::Display for UpdateNotConnectedError {
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
@ -5,6 +5,14 @@ macro_rules! h {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused_macros)]
|
||||||
|
macro_rules! local_chain {
|
||||||
|
[ $(($height:expr, $block_hash:expr)), * ] => {{
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*])
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused_macros)]
|
#[allow(unused_macros)]
|
||||||
macro_rules! chain {
|
macro_rules! chain {
|
||||||
($([$($tt:tt)*]),*) => { chain!( checkpoints: [$([$($tt)*]),*] ) };
|
($([$($tt:tt)*]),*) => { chain!( checkpoints: [$([$($tt)*]),*] ) };
|
||||||
|
167
crates/chain/tests/test_local_chain.rs
Normal file
167
crates/chain/tests/test_local_chain.rs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
use bdk_chain::local_chain::{LocalChain, UpdateNotConnectedError};
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_first_tip() {
|
||||||
|
let chain = LocalChain::default();
|
||||||
|
assert_eq!(
|
||||||
|
chain.determine_changeset(&local_chain![(0, h!("A"))]),
|
||||||
|
Ok([(0, Some(h!("A")))].into()),
|
||||||
|
"add first tip"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_second_tip() {
|
||||||
|
let chain = local_chain![(0, h!("A"))];
|
||||||
|
assert_eq!(
|
||||||
|
chain.determine_changeset(&local_chain![(0, h!("A")), (1, h!("B"))]),
|
||||||
|
Ok([(1, Some(h!("B")))].into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_disjoint_chains_cannot_merge() {
|
||||||
|
let chain1 = local_chain![(0, h!("A"))];
|
||||||
|
let chain2 = local_chain![(1, h!("B"))];
|
||||||
|
assert_eq!(
|
||||||
|
chain1.determine_changeset(&chain2),
|
||||||
|
Err(UpdateNotConnectedError(0))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_chains_should_merge() {
|
||||||
|
let chain1 = local_chain![(0, h!("A"))];
|
||||||
|
let chain2 = local_chain![(0, h!("A"))];
|
||||||
|
assert_eq!(chain1.determine_changeset(&chain2), Ok(Default::default()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_introduce_older_checkpoints() {
|
||||||
|
let chain1 = local_chain![(2, h!("C")), (3, h!("D"))];
|
||||||
|
let chain2 = local_chain![(1, h!("B")), (2, h!("C"))];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chain1.determine_changeset(&chain2),
|
||||||
|
Ok([(1, Some(h!("B")))].into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fix_blockhash_before_agreement_point() {
|
||||||
|
let chain1 = local_chain![(0, h!("im-wrong")), (1, h!("we-agree"))];
|
||||||
|
let chain2 = local_chain![(0, h!("fix")), (1, h!("we-agree"))];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chain1.determine_changeset(&chain2),
|
||||||
|
Ok([(0, Some(h!("fix")))].into())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// B and C are in both chain and update
|
||||||
|
/// ```
|
||||||
|
/// | 0 | 1 | 2 | 3 | 4
|
||||||
|
/// chain | B C
|
||||||
|
/// update | A B C D
|
||||||
|
/// ```
|
||||||
|
/// This should succeed with the point of agreement being C and A should be added in addition.
|
||||||
|
#[test]
|
||||||
|
fn two_points_of_agreement() {
|
||||||
|
let chain1 = local_chain![(1, h!("B")), (2, h!("C"))];
|
||||||
|
let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chain1.determine_changeset(&chain2),
|
||||||
|
Ok([(0, Some(h!("A"))), (3, Some(h!("D")))].into()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update and chain does not connect:
|
||||||
|
/// ```
|
||||||
|
/// | 0 | 1 | 2 | 3 | 4
|
||||||
|
/// chain | B C
|
||||||
|
/// update | A B D
|
||||||
|
/// ```
|
||||||
|
/// This should fail as we cannot figure out whether C & D are on the same chain
|
||||||
|
#[test]
|
||||||
|
fn update_and_chain_does_not_connect() {
|
||||||
|
let chain1 = local_chain![(1, h!("B")), (2, h!("C"))];
|
||||||
|
let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (3, h!("D"))];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chain1.determine_changeset(&chain2),
|
||||||
|
Err(UpdateNotConnectedError(2)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transient invalidation:
|
||||||
|
/// ```
|
||||||
|
/// | 0 | 1 | 2 | 3 | 4 | 5
|
||||||
|
/// chain | A B C E
|
||||||
|
/// update | A B' C' D
|
||||||
|
/// ```
|
||||||
|
/// This should succeed and invalidate B,C and E with point of agreement being A.
|
||||||
|
#[test]
|
||||||
|
fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation() {
|
||||||
|
let chain1 = local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))];
|
||||||
|
let chain2 = local_chain![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chain1.determine_changeset(&chain2),
|
||||||
|
Ok([
|
||||||
|
(2, Some(h!("B'"))),
|
||||||
|
(3, Some(h!("C'"))),
|
||||||
|
(4, Some(h!("D"))),
|
||||||
|
(5, None),
|
||||||
|
]
|
||||||
|
.into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transient invalidation:
|
||||||
|
/// ```
|
||||||
|
/// | 0 | 1 | 2 | 3 | 4
|
||||||
|
/// chain | B C E
|
||||||
|
/// update | B' C' D
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This should succeed and invalidate B, C and E with no point of agreement
|
||||||
|
#[test]
|
||||||
|
fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation_no_point_of_agreement() {
|
||||||
|
let chain1 = local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))];
|
||||||
|
let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chain1.determine_changeset(&chain2),
|
||||||
|
Ok([
|
||||||
|
(1, Some(h!("B'"))),
|
||||||
|
(2, Some(h!("C'"))),
|
||||||
|
(3, Some(h!("D"))),
|
||||||
|
(4, None)
|
||||||
|
]
|
||||||
|
.into())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transient invalidation:
|
||||||
|
/// ```
|
||||||
|
/// | 0 | 1 | 2 | 3 | 4
|
||||||
|
/// chain | A B C E
|
||||||
|
/// update | B' C' D
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This should fail since although it tells us that B and C are invalid it doesn't tell us whether
|
||||||
|
/// A was invalid.
|
||||||
|
#[test]
|
||||||
|
fn invalidation_but_no_connection() {
|
||||||
|
let chain1 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))];
|
||||||
|
let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chain1.determine_changeset(&chain2),
|
||||||
|
Err(UpdateNotConnectedError(0))
|
||||||
|
)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user