Skip to content

Commit 209a4ee

Browse files
evanlinjinclaudeValuedMammal
committed
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 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>
1 parent 5339155 commit 209a4ee

1 file changed

Lines changed: 384 additions & 0 deletions

File tree

crates/core/tests/test_checkpoint.rs

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,387 @@ fn test_mtp_sparse_chain() {
186186
assert_eq!(cp.median_time_past(), None);
187187
assert_eq!(cp.get(11).unwrap().median_time_past(), None);
188188
}
189+
190+
// Custom struct for testing with prev_blockhash
191+
#[derive(Debug, Clone, Copy)]
192+
struct TestBlock {
193+
blockhash: BlockHash,
194+
prev_blockhash: BlockHash,
195+
}
196+
197+
impl ToBlockHash for TestBlock {
198+
fn to_blockhash(&self) -> BlockHash {
199+
self.blockhash
200+
}
201+
202+
fn prev_blockhash(&self) -> Option<BlockHash> {
203+
Some(self.prev_blockhash)
204+
}
205+
}
206+
207+
/// Test inserting data with conflicting prev_blockhash should displace checkpoint and create
208+
/// placeholder.
209+
///
210+
/// When inserting data at height `h` with a `prev_blockhash` that conflicts with the checkpoint
211+
/// at height `h-1`, the checkpoint at `h-1` should be displaced and replaced with a placeholder
212+
/// containing the `prev_blockhash` from the inserted data.
213+
///
214+
/// Expected: Checkpoint at 99 gets displaced when inserting at 100 with conflicting prev_blockhash.
215+
#[test]
216+
fn checkpoint_insert_conflicting_prev_blockhash() {
217+
// Create initial checkpoint at height 99
218+
let block_99 = TestBlock {
219+
blockhash: hash!("block_at_99"),
220+
prev_blockhash: hash!("block_at_98"),
221+
};
222+
let cp = CheckPoint::new(99, block_99);
223+
224+
// Insert data at height 100 with a prev_blockhash that conflicts with checkpoint at 99
225+
let block_100_conflicting = TestBlock {
226+
blockhash: hash!("block_at_100"),
227+
prev_blockhash: hash!("different_block_at_99"), // Conflicts with block_99.blockhash
228+
};
229+
230+
let result = cp.insert(100, block_100_conflicting);
231+
232+
// Expected behavior: The checkpoint at 99 should be displaced
233+
assert!(result.get(99).is_none(), "99 was displaced");
234+
235+
// The checkpoint at 100 should be inserted correctly
236+
let height_100 = result.get(100).expect("checkpoint at 100 should exist");
237+
assert_eq!(height_100.hash(), block_100_conflicting.blockhash);
238+
239+
// Verify chain structure
240+
assert_eq!(result.height(), 100, "tip should be at height 100");
241+
assert_eq!(result.iter().count(), 1, "should have 1 checkpoints (100)");
242+
}
243+
244+
/// Test inserting data that conflicts with prev_blockhash of higher checkpoints should purge them.
245+
///
246+
/// When inserting data at height `h` where the blockhash conflicts with the `prev_blockhash` of
247+
/// checkpoint at height `h+1`, the checkpoint at `h+1` and all checkpoints above it should be
248+
/// purged from the chain.
249+
///
250+
/// Expected: Checkpoints at 100, 101, 102 get purged when inserting at 99 with conflicting
251+
/// blockhash.
252+
#[test]
253+
fn checkpoint_insert_purges_conflicting_tail() {
254+
// Create a chain with multiple checkpoints
255+
let block_98 = TestBlock {
256+
blockhash: hash!("block_at_98"),
257+
prev_blockhash: hash!("block_at_97"),
258+
};
259+
let block_99 = TestBlock {
260+
blockhash: hash!("block_at_99"),
261+
prev_blockhash: hash!("block_at_98"),
262+
};
263+
let block_100 = TestBlock {
264+
blockhash: hash!("block_at_100"),
265+
prev_blockhash: hash!("block_at_99"),
266+
};
267+
let block_101 = TestBlock {
268+
blockhash: hash!("block_at_101"),
269+
prev_blockhash: hash!("block_at_100"),
270+
};
271+
let block_102 = TestBlock {
272+
blockhash: hash!("block_at_102"),
273+
prev_blockhash: hash!("block_at_101"),
274+
};
275+
276+
let cp = CheckPoint::from_blocks(vec![
277+
(98, block_98),
278+
(99, block_99),
279+
(100, block_100),
280+
(101, block_101),
281+
(102, block_102),
282+
])
283+
.expect("should create valid checkpoint chain");
284+
285+
// Verify initial chain has all checkpoints
286+
assert_eq!(cp.iter().count(), 5);
287+
288+
// Insert a conflicting block at height 99
289+
// The new block's hash will conflict with block_100's prev_blockhash
290+
let conflicting_block_99 = TestBlock {
291+
blockhash: hash!("different_block_at_99"),
292+
prev_blockhash: hash!("block_at_98"), // Matches existing block_98
293+
};
294+
295+
let result = cp.insert(99, conflicting_block_99);
296+
297+
// Expected: Heights 100, 101, 102 should be purged because block_100's
298+
// prev_blockhash conflicts with the new block_99's hash
299+
assert_eq!(
300+
result.height(),
301+
99,
302+
"tip should be at height 99 after purging higher checkpoints"
303+
);
304+
305+
// Check that only 98 and 99 remain
306+
assert_eq!(
307+
result.iter().count(),
308+
2,
309+
"should have 2 checkpoints (98, 99)"
310+
);
311+
312+
// Verify height 99 has the new conflicting block
313+
let height_99 = result.get(99).expect("checkpoint at 99 should exist");
314+
assert_eq!(height_99.hash(), conflicting_block_99.blockhash);
315+
316+
// Verify height 98 remains unchanged
317+
let height_98 = result.get(98).expect("checkpoint at 98 should exist");
318+
assert_eq!(height_98.hash(), block_98.blockhash);
319+
320+
// Verify heights 100, 101, 102 are purged
321+
assert!(
322+
result.get(100).is_none(),
323+
"checkpoint at 100 should be purged"
324+
);
325+
assert!(
326+
result.get(101).is_none(),
327+
"checkpoint at 101 should be purged"
328+
);
329+
assert!(
330+
result.get(102).is_none(),
331+
"checkpoint at 102 should be purged"
332+
);
333+
}
334+
335+
/// Test inserting between checkpoints with conflicts on both sides.
336+
///
337+
/// When inserting at height between two checkpoints where the inserted data's `prev_blockhash`
338+
/// conflicts with the lower checkpoint and its `blockhash` conflicts with the upper checkpoint's
339+
/// `prev_blockhash`, both checkpoints should be handled: lower displaced, upper purged.
340+
///
341+
/// Expected: Checkpoint at 4 displaced with placeholder, checkpoint at 6 purged.
342+
#[test]
343+
fn checkpoint_insert_between_conflicting_both_sides() {
344+
// Create checkpoints at heights 4 and 6
345+
let block_4 = TestBlock {
346+
blockhash: hash!("block_at_4"),
347+
prev_blockhash: hash!("block_at_3"),
348+
};
349+
let block_6 = TestBlock {
350+
blockhash: hash!("block_at_6"),
351+
prev_blockhash: hash!("block_at_5_original"), // This will conflict with inserted block 5
352+
};
353+
354+
let cp = CheckPoint::from_blocks(vec![(4, block_4), (6, block_6)])
355+
.expect("should create valid checkpoint chain");
356+
357+
// Verify initial state
358+
assert_eq!(cp.iter().count(), 2);
359+
360+
// Insert at height 5 with conflicts on both sides
361+
let block_5_conflicting = TestBlock {
362+
blockhash: hash!("block_at_5_new"), // Conflicts with block_6.prev_blockhash
363+
prev_blockhash: hash!("different_block_at_4"), // Conflicts with block_4.blockhash
364+
};
365+
366+
let result = cp.insert(5, block_5_conflicting);
367+
368+
// Expected behavior:
369+
// - Checkpoint at 4 should be displaced (omitted)
370+
// - Checkpoint at 5 should have the inserted data
371+
// - Checkpoint at 6 should be purged due to prev_blockhash conflict
372+
373+
// Verify height 4 is displaced with placeholder
374+
assert!(result.get(4).is_none());
375+
376+
// Verify height 5 has the inserted data
377+
let checkpoint_5 = result.get(5).expect("checkpoint at 5 should exist");
378+
assert_eq!(checkpoint_5.height(), 5);
379+
assert_eq!(checkpoint_5.hash(), block_5_conflicting.blockhash);
380+
381+
// Verify height 6 is purged
382+
assert!(
383+
result.get(6).is_none(),
384+
"checkpoint at 6 should be purged due to prev_blockhash conflict"
385+
);
386+
387+
// Verify chain structure
388+
assert_eq!(result.height(), 5, "tip should be at height 5");
389+
// Should have: checkpoint 5 only
390+
assert_eq!(
391+
result.iter().count(),
392+
1,
393+
"should have 1 checkpoint(s) (4 was displaced, 6 was evicted)"
394+
);
395+
}
396+
397+
/// Test that push returns Err(self) when trying to push at the same height.
398+
#[test]
399+
fn checkpoint_push_fails_same_height() {
400+
let cp: CheckPoint<BlockHash> = CheckPoint::new(100, hash!("block_100"));
401+
402+
// Try to push at the same height (100)
403+
let result = cp.clone().push(100, hash!("another_block_100"));
404+
405+
assert!(
406+
result.is_err(),
407+
"push should fail when height is same as current"
408+
);
409+
assert!(
410+
result.unwrap_err().eq_ptr(&cp),
411+
"should return self on error"
412+
);
413+
}
414+
415+
/// Test that push returns Err(self) when trying to push at a lower height.
416+
#[test]
417+
fn checkpoint_push_fails_lower_height() {
418+
let cp: CheckPoint<BlockHash> = CheckPoint::new(100, hash!("block_100"));
419+
420+
// Try to push at a lower height (99)
421+
let result = cp.clone().push(99, hash!("block_99"));
422+
423+
assert!(
424+
result.is_err(),
425+
"push should fail when height is lower than current"
426+
);
427+
assert!(
428+
result.unwrap_err().eq_ptr(&cp),
429+
"should return self on error"
430+
);
431+
}
432+
433+
/// Test that push returns Err(self) when prev_blockhash conflicts with self's hash.
434+
#[test]
435+
fn checkpoint_push_fails_conflicting_prev_blockhash() {
436+
let cp: CheckPoint<TestBlock> = CheckPoint::new(
437+
100,
438+
TestBlock {
439+
blockhash: hash!("block_100"),
440+
prev_blockhash: hash!("block_99"),
441+
},
442+
);
443+
444+
// Create a block with a prev_blockhash that doesn't match cp's hash
445+
let conflicting_block = TestBlock {
446+
blockhash: hash!("block_101"),
447+
prev_blockhash: hash!("wrong_block_100"), // This conflicts with cp's hash
448+
};
449+
450+
// Try to push at height 101 (contiguous) with conflicting prev_blockhash
451+
let result = cp.clone().push(101, conflicting_block);
452+
453+
assert!(
454+
result.is_err(),
455+
"push should fail when prev_blockhash conflicts"
456+
);
457+
assert!(
458+
result.unwrap_err().eq_ptr(&cp),
459+
"should return self on error"
460+
);
461+
}
462+
463+
/// Test that push succeeds when prev_blockhash matches self's hash for contiguous height.
464+
#[test]
465+
fn checkpoint_push_succeeds_matching_prev_blockhash() {
466+
let cp: CheckPoint<TestBlock> = CheckPoint::new(
467+
100,
468+
TestBlock {
469+
blockhash: hash!("block_100"),
470+
prev_blockhash: hash!("block_99"),
471+
},
472+
);
473+
474+
// Create a block with matching prev_blockhash
475+
let matching_block = TestBlock {
476+
blockhash: hash!("block_101"),
477+
prev_blockhash: hash!("block_100"), // Matches cp's hash
478+
};
479+
480+
// Push at height 101 with matching prev_blockhash
481+
let result = cp.push(101, matching_block);
482+
483+
assert!(
484+
result.is_ok(),
485+
"push should succeed when prev_blockhash matches"
486+
);
487+
let new_cp = result.unwrap();
488+
assert_eq!(new_cp.height(), 101);
489+
assert_eq!(new_cp.hash(), hash!("block_101"));
490+
}
491+
492+
/// Test that push creates a placeholder for non-contiguous heights with prev_blockhash.
493+
#[test]
494+
fn checkpoint_push_creates_non_contiguous_chain() {
495+
let cp: CheckPoint<TestBlock> = CheckPoint::new(
496+
100,
497+
TestBlock {
498+
blockhash: hash!("block_100"),
499+
prev_blockhash: hash!("block_99"),
500+
},
501+
);
502+
503+
// Create a block at non-contiguous height with prev_blockhash
504+
let block_105 = TestBlock {
505+
blockhash: hash!("block_105"),
506+
prev_blockhash: hash!("block_104"),
507+
};
508+
509+
// Push at height 105 (non-contiguous)
510+
let result = cp.push(105, block_105);
511+
512+
assert!(
513+
result.is_ok(),
514+
"push should succeed for non-contiguous height"
515+
);
516+
let new_cp = result.unwrap();
517+
518+
// Verify the tip is at 105
519+
assert_eq!(new_cp.height(), 105);
520+
assert_eq!(new_cp.hash(), hash!("block_105"));
521+
522+
// Verify chain structure: 100, 105
523+
assert_eq!(new_cp.iter().count(), 2);
524+
}
525+
526+
/// Test `insert` should panic if trying to replace genesis with a different block.
527+
#[test]
528+
#[should_panic(expected = "inserted data implies different genesis")]
529+
fn checkpoint_insert_cannot_replace_genesis() {
530+
let block_0 = TestBlock {
531+
blockhash: hash!("block_0"),
532+
prev_blockhash: hash!("genesis_parent"),
533+
};
534+
let block_1 = TestBlock {
535+
blockhash: hash!("block_1"),
536+
prev_blockhash: hash!("block_0"),
537+
};
538+
539+
let cp = CheckPoint::from_blocks(vec![(0, block_0), (1, block_1)])
540+
.expect("should create valid chain");
541+
542+
// Try to replace genesis with a different block - should panic
543+
let block_0_new = TestBlock {
544+
blockhash: hash!("block_0_new"),
545+
prev_blockhash: hash!("genesis_parent_new"),
546+
};
547+
let _ = cp.insert(0, block_0_new);
548+
}
549+
550+
/// Test `insert` should panic if inserted data's prev_blockhash implies a different genesis.
551+
#[test]
552+
#[should_panic(expected = "inserted data implies different genesis")]
553+
fn checkpoint_insert_cannot_displace_genesis() {
554+
let block_0 = TestBlock {
555+
blockhash: hash!("block_0"),
556+
prev_blockhash: hash!("genesis_parent"),
557+
};
558+
let block_1 = TestBlock {
559+
blockhash: hash!("block_1"),
560+
prev_blockhash: hash!("block_0"),
561+
};
562+
563+
let cp = CheckPoint::from_blocks(vec![(0, block_0), (1, block_1)])
564+
.expect("should create valid chain");
565+
566+
// Insert at height 1 with prev_blockhash that conflicts with genesis - should panic
567+
let block_1_new = TestBlock {
568+
blockhash: hash!("block_1_new"),
569+
prev_blockhash: hash!("different_block_0"), // Conflicts with block_0.hash
570+
};
571+
let _ = cp.insert(1, block_1_new);
572+
}

0 commit comments

Comments
 (0)