Skip to content

Commit a0cb974

Browse files
evanlinjinclaude
authored andcommitted
test(core): add tests for CheckPoint::push and insert methods
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 - Push creates placeholder for non-contiguous heights 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 <[email protected]>
1 parent c24dd6a commit a0cb974

File tree

1 file changed

+389
-1
lines changed

1 file changed

+389
-1
lines changed

crates/core/tests/test_checkpoint.rs

Lines changed: 389 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bdk_core::CheckPoint;
1+
use bdk_core::{CheckPoint, ToBlockHash};
22
use bdk_testenv::{block_id, hash};
33
use bitcoin::BlockHash;
44

@@ -55,3 +55,391 @@ fn checkpoint_destruction_is_sound() {
5555
}
5656
assert_eq!(cp.iter().count() as u32, end);
5757
}
58+
59+
// Custom struct for testing with prev_blockhash
60+
#[derive(Debug, Clone, Copy)]
61+
struct TestBlock {
62+
blockhash: BlockHash,
63+
prev_blockhash: BlockHash,
64+
}
65+
66+
impl ToBlockHash for TestBlock {
67+
fn to_blockhash(&self) -> BlockHash {
68+
self.blockhash
69+
}
70+
71+
fn prev_blockhash(&self) -> Option<BlockHash> {
72+
Some(self.prev_blockhash)
73+
}
74+
}
75+
76+
/// Test inserting data with conflicting prev_blockhash should displace checkpoint and create
77+
/// placeholder.
78+
///
79+
/// When inserting data at height `h` with a `prev_blockhash` that conflicts with the checkpoint
80+
/// at height `h-1`, the checkpoint at `h-1` should be displaced and replaced with a placeholder
81+
/// containing the `prev_blockhash` from the inserted data.
82+
///
83+
/// Expected: Checkpoint at 99 gets displaced when inserting at 100 with conflicting prev_blockhash.
84+
#[test]
85+
fn checkpoint_insert_conflicting_prev_blockhash() {
86+
// Create initial checkpoint at height 99
87+
let block_99 = TestBlock {
88+
blockhash: hash!("block_at_99"),
89+
prev_blockhash: hash!("block_at_98"),
90+
};
91+
let cp = CheckPoint::new(99, block_99);
92+
93+
// The initial chain has a placeholder at 98 (from block_99's prev_blockhash)
94+
assert_eq!(cp.iter().count(), 2);
95+
let height_98_before = cp.get(98).expect("should have checkpoint at 98");
96+
assert_eq!(height_98_before.hash(), block_99.prev_blockhash);
97+
assert!(
98+
height_98_before.data_ref().is_none(),
99+
"98 should be placeholder initially"
100+
);
101+
102+
// Insert data at height 100 with a prev_blockhash that conflicts with checkpoint at 99
103+
let block_100_conflicting = TestBlock {
104+
blockhash: hash!("block_at_100"),
105+
prev_blockhash: hash!("different_block_at_99"), // Conflicts with block_99.blockhash
106+
};
107+
108+
let result = cp.insert(100, block_100_conflicting);
109+
110+
// Expected behavior: The checkpoint at 99 should be displaced and replaced with a placeholder
111+
let height_99_after = result.get(99).expect("checkpoint at 99 should exist");
112+
assert_eq!(
113+
height_99_after.hash(),
114+
block_100_conflicting.prev_blockhash,
115+
"checkpoint at 99 should be displaced and have the prev_blockhash from inserted data"
116+
);
117+
assert!(
118+
height_99_after.data_ref().is_none(),
119+
"checkpoint at 99 should be a placeholder (no data) after displacement"
120+
);
121+
122+
// The checkpoint at 100 should be inserted correctly
123+
let height_100 = result.get(100).expect("checkpoint at 100 should exist");
124+
assert_eq!(height_100.hash(), block_100_conflicting.blockhash);
125+
assert!(
126+
height_100.data_ref().is_some(),
127+
"checkpoint at 100 should have data"
128+
);
129+
130+
// Verify chain structure
131+
assert_eq!(result.height(), 100, "tip should be at height 100");
132+
assert_eq!(
133+
result.iter().count(),
134+
3,
135+
"should have 3 checkpoints (98 placeholder, 99 placeholder, 100)"
136+
);
137+
}
138+
139+
/// Test inserting data that conflicts with prev_blockhash of higher checkpoints should purge them.
140+
///
141+
/// When inserting data at height `h` where the blockhash conflicts with the `prev_blockhash` of
142+
/// checkpoint at height `h+1`, the checkpoint at `h+1` and all checkpoints above it should be
143+
/// purged from the chain.
144+
///
145+
/// Expected: Checkpoints at 100, 101, 102 get purged when inserting at 99 with conflicting
146+
/// blockhash.
147+
#[test]
148+
fn checkpoint_insert_purges_conflicting_tail() {
149+
// Create a chain with multiple checkpoints
150+
let block_98 = TestBlock {
151+
blockhash: hash!("block_at_98"),
152+
prev_blockhash: hash!("block_at_97"),
153+
};
154+
let block_99 = TestBlock {
155+
blockhash: hash!("block_at_99"),
156+
prev_blockhash: hash!("block_at_98"),
157+
};
158+
let block_100 = TestBlock {
159+
blockhash: hash!("block_at_100"),
160+
prev_blockhash: hash!("block_at_99"),
161+
};
162+
let block_101 = TestBlock {
163+
blockhash: hash!("block_at_101"),
164+
prev_blockhash: hash!("block_at_100"),
165+
};
166+
let block_102 = TestBlock {
167+
blockhash: hash!("block_at_102"),
168+
prev_blockhash: hash!("block_at_101"),
169+
};
170+
171+
let cp = CheckPoint::from_blocks(vec![
172+
(98, block_98),
173+
(99, block_99),
174+
(100, block_100),
175+
(101, block_101),
176+
(102, block_102),
177+
])
178+
.expect("should create valid checkpoint chain");
179+
180+
// Verify initial chain has all checkpoints
181+
assert_eq!(cp.iter().count(), 6); // 97 (placeholder), 98, 99, 100, 101, 102
182+
183+
// Insert a conflicting block at height 99
184+
// The new block's hash will conflict with block_100's prev_blockhash
185+
let conflicting_block_99 = TestBlock {
186+
blockhash: hash!("different_block_at_99"),
187+
prev_blockhash: hash!("block_at_98"), // Matches existing block_98
188+
};
189+
190+
let result = cp.insert(99, conflicting_block_99);
191+
192+
// Expected: Heights 100, 101, 102 should be purged because block_100's
193+
// prev_blockhash conflicts with the new block_99's hash
194+
assert_eq!(
195+
result.height(),
196+
99,
197+
"tip should be at height 99 after purging higher checkpoints"
198+
);
199+
200+
// Check that only 98 and 99 remain (plus placeholder at 97)
201+
assert_eq!(
202+
result.iter().count(),
203+
3,
204+
"should have 3 checkpoints (97 placeholder, 98, 99)"
205+
);
206+
207+
// Verify height 99 has the new conflicting block
208+
let height_99 = result.get(99).expect("checkpoint at 99 should exist");
209+
assert_eq!(height_99.hash(), conflicting_block_99.blockhash);
210+
assert!(
211+
height_99.data_ref().is_some(),
212+
"checkpoint at 99 should have data"
213+
);
214+
215+
// Verify height 98 remains unchanged
216+
let height_98 = result.get(98).expect("checkpoint at 98 should exist");
217+
assert_eq!(height_98.hash(), block_98.blockhash);
218+
assert!(
219+
height_98.data_ref().is_some(),
220+
"checkpoint at 98 should have data"
221+
);
222+
223+
// Verify heights 100, 101, 102 are purged
224+
assert!(
225+
result.get(100).is_none(),
226+
"checkpoint at 100 should be purged"
227+
);
228+
assert!(
229+
result.get(101).is_none(),
230+
"checkpoint at 101 should be purged"
231+
);
232+
assert!(
233+
result.get(102).is_none(),
234+
"checkpoint at 102 should be purged"
235+
);
236+
}
237+
238+
/// Test inserting between checkpoints with conflicts on both sides.
239+
///
240+
/// When inserting at height between two checkpoints where the inserted data's `prev_blockhash`
241+
/// conflicts with the lower checkpoint and its `blockhash` conflicts with the upper checkpoint's
242+
/// `prev_blockhash`, both checkpoints should be handled: lower displaced, upper purged.
243+
///
244+
/// Expected: Checkpoint at 4 displaced with placeholder, checkpoint at 6 purged.
245+
#[test]
246+
fn checkpoint_insert_between_conflicting_both_sides() {
247+
// Create checkpoints at heights 4 and 6
248+
let block_4 = TestBlock {
249+
blockhash: hash!("block_at_4"),
250+
prev_blockhash: hash!("block_at_3"),
251+
};
252+
let block_6 = TestBlock {
253+
blockhash: hash!("block_at_6"),
254+
prev_blockhash: hash!("block_at_5_original"), // This will conflict with inserted block 5
255+
};
256+
257+
let cp = CheckPoint::from_blocks(vec![(4, block_4), (6, block_6)])
258+
.expect("should create valid checkpoint chain");
259+
260+
// Verify initial state
261+
assert_eq!(cp.iter().count(), 4); // 3 (placeholder), 4, 5 (placeholder from 6's prev), 6
262+
263+
// Insert at height 5 with conflicts on both sides
264+
let block_5_conflicting = TestBlock {
265+
blockhash: hash!("block_at_5_new"), // Conflicts with block_6.prev_blockhash
266+
prev_blockhash: hash!("different_block_at_4"), // Conflicts with block_4.blockhash
267+
};
268+
269+
let result = cp.insert(5, block_5_conflicting);
270+
271+
// Expected behavior:
272+
// - Checkpoint at 4 should be displaced with a placeholder containing block_5's prev_blockhash
273+
// - Checkpoint at 5 should have the inserted data
274+
// - Checkpoint at 6 should be purged due to prev_blockhash conflict
275+
276+
// Verify height 4 is displaced with placeholder
277+
let height_4 = result.get(4).expect("checkpoint at 4 should exist");
278+
assert_eq!(height_4.height(), 4);
279+
assert_eq!(
280+
height_4.hash(),
281+
block_5_conflicting.prev_blockhash,
282+
"checkpoint at 4 should be displaced with block 5's prev_blockhash"
283+
);
284+
assert!(
285+
height_4.data_ref().is_none(),
286+
"checkpoint at 4 should be a placeholder"
287+
);
288+
289+
// Verify height 5 has the inserted data
290+
let height_5 = result.get(5).expect("checkpoint at 5 should exist");
291+
assert_eq!(height_5.height(), 5);
292+
assert_eq!(height_5.hash(), block_5_conflicting.blockhash);
293+
assert!(
294+
height_5.data_ref().is_some(),
295+
"checkpoint at 5 should have data"
296+
);
297+
298+
// Verify height 6 is purged
299+
assert!(
300+
result.get(6).is_none(),
301+
"checkpoint at 6 should be purged due to prev_blockhash conflict"
302+
);
303+
304+
// Verify chain structure
305+
assert_eq!(result.height(), 5, "tip should be at height 5");
306+
// Should have: 3 (placeholder), 4 (placeholder), 5
307+
assert_eq!(result.iter().count(), 3, "should have 3 checkpoints");
308+
}
309+
310+
/// Test that push returns Err(self) when trying to push at the same height.
311+
#[test]
312+
fn checkpoint_push_fails_same_height() {
313+
let cp: CheckPoint<BlockHash> = CheckPoint::new(100, hash!("block_100"));
314+
315+
// Try to push at the same height (100)
316+
let result = cp.clone().push(100, hash!("another_block_100"));
317+
318+
assert!(
319+
result.is_err(),
320+
"push should fail when height is same as current"
321+
);
322+
assert!(
323+
result.unwrap_err().eq_ptr(&cp),
324+
"should return self on error"
325+
);
326+
}
327+
328+
/// Test that push returns Err(self) when trying to push at a lower height.
329+
#[test]
330+
fn checkpoint_push_fails_lower_height() {
331+
let cp: CheckPoint<BlockHash> = CheckPoint::new(100, hash!("block_100"));
332+
333+
// Try to push at a lower height (99)
334+
let result = cp.clone().push(99, hash!("block_99"));
335+
336+
assert!(
337+
result.is_err(),
338+
"push should fail when height is lower than current"
339+
);
340+
assert!(
341+
result.unwrap_err().eq_ptr(&cp),
342+
"should return self on error"
343+
);
344+
}
345+
346+
/// Test that push returns Err(self) when prev_blockhash conflicts with self's hash.
347+
#[test]
348+
fn checkpoint_push_fails_conflicting_prev_blockhash() {
349+
let cp: CheckPoint<TestBlock> = CheckPoint::new(
350+
100,
351+
TestBlock {
352+
blockhash: hash!("block_100"),
353+
prev_blockhash: hash!("block_99"),
354+
},
355+
);
356+
357+
// Create a block with a prev_blockhash that doesn't match cp's hash
358+
let conflicting_block = TestBlock {
359+
blockhash: hash!("block_101"),
360+
prev_blockhash: hash!("wrong_block_100"), // This conflicts with cp's hash
361+
};
362+
363+
// Try to push at height 101 (contiguous) with conflicting prev_blockhash
364+
let result = cp.clone().push(101, conflicting_block);
365+
366+
assert!(
367+
result.is_err(),
368+
"push should fail when prev_blockhash conflicts"
369+
);
370+
assert!(
371+
result.unwrap_err().eq_ptr(&cp),
372+
"should return self on error"
373+
);
374+
}
375+
376+
/// Test that push succeeds when prev_blockhash matches self's hash for contiguous height.
377+
#[test]
378+
fn checkpoint_push_succeeds_matching_prev_blockhash() {
379+
let cp: CheckPoint<TestBlock> = CheckPoint::new(
380+
100,
381+
TestBlock {
382+
blockhash: hash!("block_100"),
383+
prev_blockhash: hash!("block_99"),
384+
},
385+
);
386+
387+
// Create a block with matching prev_blockhash
388+
let matching_block = TestBlock {
389+
blockhash: hash!("block_101"),
390+
prev_blockhash: hash!("block_100"), // Matches cp's hash
391+
};
392+
393+
// Push at height 101 with matching prev_blockhash
394+
let result = cp.push(101, matching_block);
395+
396+
assert!(
397+
result.is_ok(),
398+
"push should succeed when prev_blockhash matches"
399+
);
400+
let new_cp = result.unwrap();
401+
assert_eq!(new_cp.height(), 101);
402+
assert_eq!(new_cp.hash(), hash!("block_101"));
403+
}
404+
405+
/// Test that push creates a placeholder for non-contiguous heights with prev_blockhash.
406+
#[test]
407+
fn checkpoint_push_creates_placeholder_non_contiguous() {
408+
let cp: CheckPoint<TestBlock> = CheckPoint::new(
409+
100,
410+
TestBlock {
411+
blockhash: hash!("block_100"),
412+
prev_blockhash: hash!("block_99"),
413+
},
414+
);
415+
416+
// Create a block at non-contiguous height with prev_blockhash
417+
let block_105 = TestBlock {
418+
blockhash: hash!("block_105"),
419+
prev_blockhash: hash!("block_104"),
420+
};
421+
422+
// Push at height 105 (non-contiguous)
423+
let result = cp.push(105, block_105);
424+
425+
assert!(
426+
result.is_ok(),
427+
"push should succeed for non-contiguous height"
428+
);
429+
let new_cp = result.unwrap();
430+
431+
// Verify the tip is at 105
432+
assert_eq!(new_cp.height(), 105);
433+
assert_eq!(new_cp.hash(), hash!("block_105"));
434+
435+
// Verify placeholder was created at 104
436+
let placeholder = new_cp.get(104).expect("should have placeholder at 104");
437+
assert_eq!(placeholder.hash(), hash!("block_104"));
438+
assert!(
439+
placeholder.data_ref().is_none(),
440+
"placeholder should have no data"
441+
);
442+
443+
// Verify chain structure: 100 -> 99 (placeholder) -> 104 (placeholder) -> 105
444+
assert_eq!(new_cp.iter().count(), 4);
445+
}

0 commit comments

Comments
 (0)