]> Untitled Git - bdk/commitdiff
test(core): add tests for CheckPoint::push and insert methods
author志宇 <hello@evanlinjin.me>
Thu, 25 Sep 2025 02:24:27 +0000 (02:24 +0000)
committer志宇 <hello@evanlinjin.me>
Wed, 22 Apr 2026 04:48:18 +0000 (04:48 +0000)
Add comprehensive tests for CheckPoint::push error cases:
- Push fails when height is not greater than current
- Push fails when prev_blockhash conflicts with self
- Push succeeds when prev_blockhash matches

Include tests for CheckPoint::insert conflict handling:
- Insert with conflicting prev_blockhash
- Insert purges conflicting tail
- Insert between conflicting checkpoints

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: valued mammal <valuedmammal@protonmail.com>
crates/core/tests/test_checkpoint.rs

index 811c56fad46d4f1386cbd7376e20057f16453df9..19c06a4173dbd47287bcef022d8898b7acd33ef6 100644 (file)
@@ -186,3 +186,387 @@ fn test_mtp_sparse_chain() {
     assert_eq!(cp.median_time_past(), None);
     assert_eq!(cp.get(11).unwrap().median_time_past(), None);
 }
+
+// Custom struct for testing with prev_blockhash
+#[derive(Debug, Clone, Copy)]
+struct TestBlock {
+    blockhash: BlockHash,
+    prev_blockhash: BlockHash,
+}
+
+impl ToBlockHash for TestBlock {
+    fn to_blockhash(&self) -> BlockHash {
+        self.blockhash
+    }
+
+    fn prev_blockhash(&self) -> Option<BlockHash> {
+        Some(self.prev_blockhash)
+    }
+}
+
+/// Test inserting data with conflicting prev_blockhash should displace checkpoint and create
+/// placeholder.
+///
+/// When inserting data at height `h` with a `prev_blockhash` that conflicts with the checkpoint
+/// at height `h-1`, the checkpoint at `h-1` should be displaced and replaced with a placeholder
+/// containing the `prev_blockhash` from the inserted data.
+///
+/// Expected: Checkpoint at 99 gets displaced when inserting at 100 with conflicting prev_blockhash.
+#[test]
+fn checkpoint_insert_conflicting_prev_blockhash() {
+    // Create initial checkpoint at height 99
+    let block_99 = TestBlock {
+        blockhash: hash!("block_at_99"),
+        prev_blockhash: hash!("block_at_98"),
+    };
+    let cp = CheckPoint::new(99, block_99);
+
+    // Insert data at height 100 with a prev_blockhash that conflicts with checkpoint at 99
+    let block_100_conflicting = TestBlock {
+        blockhash: hash!("block_at_100"),
+        prev_blockhash: hash!("different_block_at_99"), // Conflicts with block_99.blockhash
+    };
+
+    let result = cp.insert(100, block_100_conflicting);
+
+    // Expected behavior: The checkpoint at 99 should be displaced
+    assert!(result.get(99).is_none(), "99 was displaced");
+
+    // The checkpoint at 100 should be inserted correctly
+    let height_100 = result.get(100).expect("checkpoint at 100 should exist");
+    assert_eq!(height_100.hash(), block_100_conflicting.blockhash);
+
+    // Verify chain structure
+    assert_eq!(result.height(), 100, "tip should be at height 100");
+    assert_eq!(result.iter().count(), 1, "should have 1 checkpoints (100)");
+}
+
+/// Test inserting data that conflicts with prev_blockhash of higher checkpoints should purge them.
+///
+/// When inserting data at height `h` where the blockhash conflicts with the `prev_blockhash` of
+/// checkpoint at height `h+1`, the checkpoint at `h+1` and all checkpoints above it should be
+/// purged from the chain.
+///
+/// Expected: Checkpoints at 100, 101, 102 get purged when inserting at 99 with conflicting
+/// blockhash.
+#[test]
+fn checkpoint_insert_purges_conflicting_tail() {
+    // Create a chain with multiple checkpoints
+    let block_98 = TestBlock {
+        blockhash: hash!("block_at_98"),
+        prev_blockhash: hash!("block_at_97"),
+    };
+    let block_99 = TestBlock {
+        blockhash: hash!("block_at_99"),
+        prev_blockhash: hash!("block_at_98"),
+    };
+    let block_100 = TestBlock {
+        blockhash: hash!("block_at_100"),
+        prev_blockhash: hash!("block_at_99"),
+    };
+    let block_101 = TestBlock {
+        blockhash: hash!("block_at_101"),
+        prev_blockhash: hash!("block_at_100"),
+    };
+    let block_102 = TestBlock {
+        blockhash: hash!("block_at_102"),
+        prev_blockhash: hash!("block_at_101"),
+    };
+
+    let cp = CheckPoint::from_blocks(vec![
+        (98, block_98),
+        (99, block_99),
+        (100, block_100),
+        (101, block_101),
+        (102, block_102),
+    ])
+    .expect("should create valid checkpoint chain");
+
+    // Verify initial chain has all checkpoints
+    assert_eq!(cp.iter().count(), 5);
+
+    // Insert a conflicting block at height 99
+    // The new block's hash will conflict with block_100's prev_blockhash
+    let conflicting_block_99 = TestBlock {
+        blockhash: hash!("different_block_at_99"),
+        prev_blockhash: hash!("block_at_98"), // Matches existing block_98
+    };
+
+    let result = cp.insert(99, conflicting_block_99);
+
+    // Expected: Heights 100, 101, 102 should be purged because block_100's
+    // prev_blockhash conflicts with the new block_99's hash
+    assert_eq!(
+        result.height(),
+        99,
+        "tip should be at height 99 after purging higher checkpoints"
+    );
+
+    // Check that only 98 and 99 remain
+    assert_eq!(
+        result.iter().count(),
+        2,
+        "should have 2 checkpoints (98, 99)"
+    );
+
+    // Verify height 99 has the new conflicting block
+    let height_99 = result.get(99).expect("checkpoint at 99 should exist");
+    assert_eq!(height_99.hash(), conflicting_block_99.blockhash);
+
+    // Verify height 98 remains unchanged
+    let height_98 = result.get(98).expect("checkpoint at 98 should exist");
+    assert_eq!(height_98.hash(), block_98.blockhash);
+
+    // Verify heights 100, 101, 102 are purged
+    assert!(
+        result.get(100).is_none(),
+        "checkpoint at 100 should be purged"
+    );
+    assert!(
+        result.get(101).is_none(),
+        "checkpoint at 101 should be purged"
+    );
+    assert!(
+        result.get(102).is_none(),
+        "checkpoint at 102 should be purged"
+    );
+}
+
+/// Test inserting between checkpoints with conflicts on both sides.
+///
+/// When inserting at height between two checkpoints where the inserted data's `prev_blockhash`
+/// conflicts with the lower checkpoint and its `blockhash` conflicts with the upper checkpoint's
+/// `prev_blockhash`, both checkpoints should be handled: lower displaced, upper purged.
+///
+/// Expected: Checkpoint at 4 displaced with placeholder, checkpoint at 6 purged.
+#[test]
+fn checkpoint_insert_between_conflicting_both_sides() {
+    // Create checkpoints at heights 4 and 6
+    let block_4 = TestBlock {
+        blockhash: hash!("block_at_4"),
+        prev_blockhash: hash!("block_at_3"),
+    };
+    let block_6 = TestBlock {
+        blockhash: hash!("block_at_6"),
+        prev_blockhash: hash!("block_at_5_original"), // This will conflict with inserted block 5
+    };
+
+    let cp = CheckPoint::from_blocks(vec![(4, block_4), (6, block_6)])
+        .expect("should create valid checkpoint chain");
+
+    // Verify initial state
+    assert_eq!(cp.iter().count(), 2);
+
+    // Insert at height 5 with conflicts on both sides
+    let block_5_conflicting = TestBlock {
+        blockhash: hash!("block_at_5_new"), // Conflicts with block_6.prev_blockhash
+        prev_blockhash: hash!("different_block_at_4"), // Conflicts with block_4.blockhash
+    };
+
+    let result = cp.insert(5, block_5_conflicting);
+
+    // Expected behavior:
+    // - Checkpoint at 4 should be displaced (omitted)
+    // - Checkpoint at 5 should have the inserted data
+    // - Checkpoint at 6 should be purged due to prev_blockhash conflict
+
+    // Verify height 4 is displaced with placeholder
+    assert!(result.get(4).is_none());
+
+    // Verify height 5 has the inserted data
+    let checkpoint_5 = result.get(5).expect("checkpoint at 5 should exist");
+    assert_eq!(checkpoint_5.height(), 5);
+    assert_eq!(checkpoint_5.hash(), block_5_conflicting.blockhash);
+
+    // Verify height 6 is purged
+    assert!(
+        result.get(6).is_none(),
+        "checkpoint at 6 should be purged due to prev_blockhash conflict"
+    );
+
+    // Verify chain structure
+    assert_eq!(result.height(), 5, "tip should be at height 5");
+    // Should have: checkpoint 5 only
+    assert_eq!(
+        result.iter().count(),
+        1,
+        "should have 1 checkpoint(s) (4 was displaced, 6 was evicted)"
+    );
+}
+
+/// Test that push returns Err(self) when trying to push at the same height.
+#[test]
+fn checkpoint_push_fails_same_height() {
+    let cp: CheckPoint<BlockHash> = CheckPoint::new(100, hash!("block_100"));
+
+    // Try to push at the same height (100)
+    let result = cp.clone().push(100, hash!("another_block_100"));
+
+    assert!(
+        result.is_err(),
+        "push should fail when height is same as current"
+    );
+    assert!(
+        result.unwrap_err().eq_ptr(&cp),
+        "should return self on error"
+    );
+}
+
+/// Test that push returns Err(self) when trying to push at a lower height.
+#[test]
+fn checkpoint_push_fails_lower_height() {
+    let cp: CheckPoint<BlockHash> = CheckPoint::new(100, hash!("block_100"));
+
+    // Try to push at a lower height (99)
+    let result = cp.clone().push(99, hash!("block_99"));
+
+    assert!(
+        result.is_err(),
+        "push should fail when height is lower than current"
+    );
+    assert!(
+        result.unwrap_err().eq_ptr(&cp),
+        "should return self on error"
+    );
+}
+
+/// Test that push returns Err(self) when prev_blockhash conflicts with self's hash.
+#[test]
+fn checkpoint_push_fails_conflicting_prev_blockhash() {
+    let cp: CheckPoint<TestBlock> = CheckPoint::new(
+        100,
+        TestBlock {
+            blockhash: hash!("block_100"),
+            prev_blockhash: hash!("block_99"),
+        },
+    );
+
+    // Create a block with a prev_blockhash that doesn't match cp's hash
+    let conflicting_block = TestBlock {
+        blockhash: hash!("block_101"),
+        prev_blockhash: hash!("wrong_block_100"), // This conflicts with cp's hash
+    };
+
+    // Try to push at height 101 (contiguous) with conflicting prev_blockhash
+    let result = cp.clone().push(101, conflicting_block);
+
+    assert!(
+        result.is_err(),
+        "push should fail when prev_blockhash conflicts"
+    );
+    assert!(
+        result.unwrap_err().eq_ptr(&cp),
+        "should return self on error"
+    );
+}
+
+/// Test that push succeeds when prev_blockhash matches self's hash for contiguous height.
+#[test]
+fn checkpoint_push_succeeds_matching_prev_blockhash() {
+    let cp: CheckPoint<TestBlock> = CheckPoint::new(
+        100,
+        TestBlock {
+            blockhash: hash!("block_100"),
+            prev_blockhash: hash!("block_99"),
+        },
+    );
+
+    // Create a block with matching prev_blockhash
+    let matching_block = TestBlock {
+        blockhash: hash!("block_101"),
+        prev_blockhash: hash!("block_100"), // Matches cp's hash
+    };
+
+    // Push at height 101 with matching prev_blockhash
+    let result = cp.push(101, matching_block);
+
+    assert!(
+        result.is_ok(),
+        "push should succeed when prev_blockhash matches"
+    );
+    let new_cp = result.unwrap();
+    assert_eq!(new_cp.height(), 101);
+    assert_eq!(new_cp.hash(), hash!("block_101"));
+}
+
+/// Test that push creates a placeholder for non-contiguous heights with prev_blockhash.
+#[test]
+fn checkpoint_push_creates_non_contiguous_chain() {
+    let cp: CheckPoint<TestBlock> = CheckPoint::new(
+        100,
+        TestBlock {
+            blockhash: hash!("block_100"),
+            prev_blockhash: hash!("block_99"),
+        },
+    );
+
+    // Create a block at non-contiguous height with prev_blockhash
+    let block_105 = TestBlock {
+        blockhash: hash!("block_105"),
+        prev_blockhash: hash!("block_104"),
+    };
+
+    // Push at height 105 (non-contiguous)
+    let result = cp.push(105, block_105);
+
+    assert!(
+        result.is_ok(),
+        "push should succeed for non-contiguous height"
+    );
+    let new_cp = result.unwrap();
+
+    // Verify the tip is at 105
+    assert_eq!(new_cp.height(), 105);
+    assert_eq!(new_cp.hash(), hash!("block_105"));
+
+    // Verify chain structure: 100, 105
+    assert_eq!(new_cp.iter().count(), 2);
+}
+
+/// Test `insert` should panic if trying to replace genesis with a different block.
+#[test]
+#[should_panic(expected = "inserted data implies different genesis")]
+fn checkpoint_insert_cannot_replace_genesis() {
+    let block_0 = TestBlock {
+        blockhash: hash!("block_0"),
+        prev_blockhash: hash!("genesis_parent"),
+    };
+    let block_1 = TestBlock {
+        blockhash: hash!("block_1"),
+        prev_blockhash: hash!("block_0"),
+    };
+
+    let cp = CheckPoint::from_blocks(vec![(0, block_0), (1, block_1)])
+        .expect("should create valid chain");
+
+    // Try to replace genesis with a different block - should panic
+    let block_0_new = TestBlock {
+        blockhash: hash!("block_0_new"),
+        prev_blockhash: hash!("genesis_parent_new"),
+    };
+    let _ = cp.insert(0, block_0_new);
+}
+
+/// Test `insert` should panic if inserted data's prev_blockhash implies a different genesis.
+#[test]
+#[should_panic(expected = "inserted data implies different genesis")]
+fn checkpoint_insert_cannot_displace_genesis() {
+    let block_0 = TestBlock {
+        blockhash: hash!("block_0"),
+        prev_blockhash: hash!("genesis_parent"),
+    };
+    let block_1 = TestBlock {
+        blockhash: hash!("block_1"),
+        prev_blockhash: hash!("block_0"),
+    };
+
+    let cp = CheckPoint::from_blocks(vec![(0, block_0), (1, block_1)])
+        .expect("should create valid chain");
+
+    // Insert at height 1 with prev_blockhash that conflicts with genesis - should panic
+    let block_1_new = TestBlock {
+        blockhash: hash!("block_1_new"),
+        prev_blockhash: hash!("different_block_0"), // Conflicts with block_0.hash
+    };
+    let _ = cp.insert(1, block_1_new);
+}