]> Untitled Git - bdk/commitdiff
[bdk_chain_redesign] Test `LocalChain`
author志宇 <hello@evanlinjin.me>
Thu, 20 Apr 2023 07:29:20 +0000 (15:29 +0800)
committer志宇 <hello@evanlinjin.me>
Thu, 20 Apr 2023 07:29:20 +0000 (15:29 +0800)
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
crates/chain/tests/common/mod.rs
crates/chain/tests/test_local_chain.rs [new file with mode: 0644]

index 9ba64b284a1ae33370d90b1b3770e15f7d63c6cc..88c688fe9344658a9d7f1dffc44a9ffe298809d8 100644 (file)
@@ -62,6 +62,15 @@ impl From<BTreeMap<u32, BlockHash>> for 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> {
         self.blocks
             .iter()
@@ -109,19 +118,37 @@ impl LocalChain {
             }
         }
 
-        let mut changeset = BTreeMap::<u32, BlockHash>::new();
-        for (height, new_hash) in update {
+        let mut changeset: BTreeMap<u32, Option<BlockHash>> = 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<u32> {
@@ -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<u32, BlockHash>;
+pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
 
 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 {
index e9b7a101f8363c23c41171d9a7c76f5949844ab6..7d7288bdfc4d1dd23794f286d1c6e8c0fda32a4e 100644 (file)
@@ -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 (file)
index 0000000..1aea985
--- /dev/null
@@ -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))
+    )
+}