@@ -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