@@ -62,6 +62,7 @@ pub struct PersistedShardTreeState {
6262 pub checkpoints : Vec < SerializedCheckpoint > ,
6363 pub tip_height : Option < u32 > ,
6464 pub leaf_count : u64 ,
65+ pub max_checkpoints : usize ,
6566}
6667
6768// ---------------------------------------------------------------------------
@@ -121,6 +122,7 @@ pub fn extract_state(
121122 tree : & ShieldedShardTree ,
122123 tip_height : Option < u32 > ,
123124 leaf_count : u64 ,
125+ max_checkpoints : usize ,
124126) -> Result < PersistedShardTreeState , String > {
125127 let store = tree. store ( ) ;
126128
@@ -175,6 +177,7 @@ pub fn extract_state(
175177 checkpoints,
176178 tip_height,
177179 leaf_count,
180+ max_checkpoints,
178181 } )
179182}
180183
@@ -212,7 +215,7 @@ pub fn restore_state(state: &PersistedShardTreeState) -> Result<ShieldedShardTre
212215 . map_err ( |e| format ! ( "add_checkpoint error: {:?}" , e) ) ?;
213216 }
214217
215- Ok ( ShardTree :: new ( store, MAX_CHECKPOINTS ) )
218+ Ok ( ShardTree :: new ( store, state . max_checkpoints ) )
216219}
217220
218221// ---------------------------------------------------------------------------
@@ -321,6 +324,7 @@ pub struct OwnedTree {
321324 tree : ShieldedShardTree ,
322325 tip_height : Option < u32 > ,
323326 leaf_count : u64 ,
327+ max_checkpoints : usize ,
324328}
325329
326330impl OwnedTree {
@@ -334,11 +338,17 @@ impl OwnedTree {
334338 tree,
335339 tip_height : persisted. tip_height ,
336340 leaf_count : persisted. leaf_count ,
341+ max_checkpoints : persisted. max_checkpoints ,
337342 } )
338343 }
339344
340345 /// Initialize from a CommitmentTree v0 frontier (raw bytes, not hex-encoded).
341- pub fn from_frontier ( frontier : & [ u8 ] , block_height : u32 ) -> Result < Self , String > {
346+ pub fn from_frontier (
347+ frontier : & [ u8 ] ,
348+ block_height : u32 ,
349+ max_checkpoints : Option < usize > ,
350+ ) -> Result < Self , String > {
351+ let max_checkpoints = max_checkpoints. unwrap_or ( MAX_CHECKPOINTS ) ;
342352 use incrementalmerkletree:: frontier:: NonEmptyFrontier ;
343353
344354 let mut offset = 0 ;
@@ -395,7 +405,7 @@ impl OwnedTree {
395405
396406 let leaf_count = u64:: from ( nef. position ( ) ) + 1 ;
397407
398- let mut tree = ShardTree :: new ( MemoryShardStore :: empty ( ) , MAX_CHECKPOINTS ) ;
408+ let mut tree = ShardTree :: new ( MemoryShardStore :: empty ( ) , max_checkpoints ) ;
399409 tree. insert_frontier_nodes (
400410 nef,
401411 Retention :: Checkpoint {
@@ -409,12 +419,18 @@ impl OwnedTree {
409419 tree,
410420 tip_height : Some ( block_height) ,
411421 leaf_count,
422+ max_checkpoints,
412423 } )
413424 }
414425
415426 /// Serialize the tree state to bytes (UTF-8 JSON of `PersistedShardTreeState`).
416427 pub fn save ( & self ) -> Result < Vec < u8 > , String > {
417- let state = extract_state ( & self . tree , self . tip_height , self . leaf_count ) ?;
428+ let state = extract_state (
429+ & self . tree ,
430+ self . tip_height ,
431+ self . leaf_count ,
432+ self . max_checkpoints ,
433+ ) ?;
418434 serde_json:: to_vec ( & state) . map_err ( |e| format ! ( "JSON serialize error: {}" , e) )
419435 }
420436
@@ -540,7 +556,7 @@ mod tests {
540556 const F_CHECKPOINT_MARKED : u8 = 3 ;
541557
542558 fn empty_tree ( ) -> OwnedTree {
543- let json = r#"{"shards":[],"cap":{"type":"Nil"},"checkpoints":[],"tip_height":null,"leaf_count":0}"# ;
559+ let json = r#"{"shards":[],"cap":{"type":"Nil"},"checkpoints":[],"tip_height":null,"leaf_count":0,"max_checkpoints":100 }"# ;
544560 OwnedTree :: from_state ( json. as_bytes ( ) ) . expect ( "empty state" )
545561 }
546562
@@ -582,7 +598,13 @@ mod tests {
582598 None ,
583599 )
584600 . unwrap ( ) ;
585- let state = extract_state ( & tree. tree , tree. tip_height , tree. leaf_count ) . unwrap ( ) ;
601+ let state = extract_state (
602+ & tree. tree ,
603+ tree. tip_height ,
604+ tree. leaf_count ,
605+ tree. max_checkpoints ,
606+ )
607+ . unwrap ( ) ;
586608
587609 assert_eq ! (
588610 find_f_in_state( & state, cmx1_hex) ,
@@ -603,7 +625,13 @@ mod tests {
603625 let mut tree = empty_tree ( ) ;
604626 tree. append_commitments ( 1 , vec ! [ cmx( 1 ) ] , vec ! [ false ] , None )
605627 . unwrap ( ) ;
606- let state = extract_state ( & tree. tree , tree. tip_height , tree. leaf_count ) . unwrap ( ) ;
628+ let state = extract_state (
629+ & tree. tree ,
630+ tree. tip_height ,
631+ tree. leaf_count ,
632+ tree. max_checkpoints ,
633+ )
634+ . unwrap ( ) ;
607635
608636 assert_eq ! (
609637 find_f_in_state( & state, cmx_hex) ,
@@ -619,4 +647,53 @@ mod tests {
619647 tree. append_commitments ( 1 , vec ! [ cmx( 1 ) , cmx( 2 ) ] , vec ! [ true , false , true ] , None ) ;
620648 assert ! ( result. is_err( ) ) ;
621649 }
650+
651+ // -------------------------------------------------------------------------
652+ // max_checkpoints
653+ // -------------------------------------------------------------------------
654+
655+ fn frontier_bytes ( ) -> Vec < u8 > {
656+ hex:: decode ( "0101000000000000000000000000000000000000000000000000000000000000000000" )
657+ . unwrap ( )
658+ }
659+
660+ #[ test]
661+ fn from_frontier_defaults_max_checkpoints_when_not_specified ( ) {
662+ let tree = OwnedTree :: from_frontier ( & frontier_bytes ( ) , 1 , None ) . unwrap ( ) ;
663+ assert_eq ! ( tree. max_checkpoints, MAX_CHECKPOINTS ) ;
664+ }
665+
666+ #[ test]
667+ fn from_frontier_uses_custom_max_checkpoints_when_specified ( ) {
668+ let tree = OwnedTree :: from_frontier ( & frontier_bytes ( ) , 1 , Some ( 5 ) ) . unwrap ( ) ;
669+ assert_eq ! ( tree. max_checkpoints, 5 ) ;
670+ }
671+
672+ #[ test]
673+ fn max_checkpoints_enforced_evicts_oldest_checkpoint ( ) {
674+ let mut tree = OwnedTree :: from_frontier ( & frontier_bytes ( ) , 1 , Some ( 2 ) ) . unwrap ( ) ;
675+ tree. append_commitments ( 2 , vec ! [ ] , vec ! [ ] , None ) . unwrap ( ) ;
676+ tree. append_commitments ( 3 , vec ! [ ] , vec ! [ ] , None ) . unwrap ( ) ;
677+ tree. append_commitments ( 4 , vec ! [ ] , vec ! [ ] , None ) . unwrap ( ) ;
678+
679+ let ( _, _, checkpoint_count) = tree. get_info ( ) . unwrap ( ) ;
680+ assert_eq ! (
681+ checkpoint_count, 2 ,
682+ "checkpoint_count should be capped at 2"
683+ ) ;
684+
685+ let result = tree. truncate_to_checkpoint ( 1 ) ;
686+ assert ! (
687+ result. is_err( ) ,
688+ "oldest checkpoint should have been evicted"
689+ ) ;
690+ }
691+
692+ #[ test]
693+ fn save_round_trip_preserves_max_checkpoints ( ) {
694+ let tree = OwnedTree :: from_frontier ( & frontier_bytes ( ) , 1 , Some ( 7 ) ) . unwrap ( ) ;
695+ let bytes = tree. save ( ) . unwrap ( ) ;
696+ let restored = OwnedTree :: from_state ( & bytes) . unwrap ( ) ;
697+ assert_eq ! ( restored. max_checkpoints, 7 ) ;
698+ }
622699}
0 commit comments