From 1003fe2ee6167e110b0195e2431560b5b222e2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 20 Apr 2023 15:29:20 +0800 Subject: [PATCH] [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. --- crates/chain/src/local_chain.rs | 48 +++++-- crates/chain/tests/common/mod.rs | 8 ++ crates/chain/tests/test_local_chain.rs | 167 +++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 crates/chain/tests/test_local_chain.rs diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 9ba64b28..88c688fe 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -62,6 +62,15 @@ impl From> for LocalChain { } impl LocalChain { + pub fn from_blocks(blocks: B) -> Self + where + B: IntoIterator, + { + Self { + blocks: blocks.into_iter().map(|b| (b.height, b.hash)).collect(), + } + } + pub fn tip(&self) -> Option { self.blocks .iter() @@ -109,19 +118,37 @@ impl LocalChain { } } - let mut changeset = BTreeMap::::new(); - for (height, new_hash) in update { + let mut changeset: BTreeMap> = match invalidate_from_height { + 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); - if Some(new_hash) != original_hash { - changeset.insert(*height, *new_hash); + if Some(update_hash) != original_hash { + changeset.insert(*height, Some(*update_hash)); } } + Ok(changeset) } /// Applies the given `changeset`. - pub fn apply_changeset(&mut self, mut changeset: ChangeSet) { - self.blocks.append(&mut changeset) + pub fn apply_changeset(&mut self, changeset: 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`]. @@ -137,7 +164,10 @@ impl LocalChain { } pub fn initial_changeset(&self) -> ChangeSet { - self.blocks.clone() + self.blocks + .iter() + .map(|(&height, &hash)| (height, Some(hash))) + .collect() } pub fn heights(&self) -> BTreeSet { @@ -148,7 +178,7 @@ impl LocalChain { /// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`]. /// /// [`determine_changeset`]: LocalChain::determine_changeset -type ChangeSet = BTreeMap; +pub type ChangeSet = BTreeMap>; impl Append for ChangeSet { 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 /// that the chains can connect. #[derive(Clone, Debug, PartialEq)] -pub struct UpdateNotConnectedError(u32); +pub struct UpdateNotConnectedError(pub u32); impl core::fmt::Display for UpdateNotConnectedError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { diff --git a/crates/chain/tests/common/mod.rs b/crates/chain/tests/common/mod.rs index e9b7a101..7d7288bd 100644 --- a/crates/chain/tests/common/mod.rs +++ b/crates/chain/tests/common/mod.rs @@ -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)] macro_rules! chain { ($([$($tt:tt)*]),*) => { chain!( checkpoints: [$([$($tt)*]),*] ) }; diff --git a/crates/chain/tests/test_local_chain.rs b/crates/chain/tests/test_local_chain.rs new file mode 100644 index 00000000..1aea9850 --- /dev/null +++ b/crates/chain/tests/test_local_chain.rs @@ -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)) + ) +}