@@ -585,6 +585,106 @@ mod append_leaves_tests {
585585 let _ = append_leaves_to_snapshot ::<5 , 8 >(snapshot , new_leaves , 2 , frontier );
586586 }
587587
588+ #[test]
589+ fn append_ignores_unpinned_frontier_lanes () {
590+ // At start index 3 only levels 0 and 1 are right children, so those are the only hint lanes the validation
591+ // reads; the rest are unpinned and entirely prover-chosen. A tree database answering with a sibling path puts
592+ // the zero-subtree roots there, while the tests here pass zeros. Neither may influence the result.
593+ let before_root = MerkleTree ::<32 >::new (padded ([11 , 22 , 33 ])).get_root ();
594+ let snapshot = AppendOnlyTreeSnapshot { root : before_root , next_available_leaf_index : 3 };
595+ let new_leaves : [Field ; 8 ] = padded ([44 , 55 ]);
596+ let expected_root = MerkleTree ::<32 >::new (padded ([11 , 22 , 33 , 44 , 55 ])).get_root ();
597+
598+ // `compute_zero_hashes[i]` is the root of an all-zero subtree of height `i + 1`, i.e. the right sibling a
599+ // sibling path carries at level `i + 1`.
600+ let zero_hashes = compute_zero_hashes ::<5 >();
601+ let from_db = [33 , merkle_hash (11 , 22 ), zero_hashes [1 ], zero_hashes [2 ], zero_hashes [3 ]];
602+ let adversarial = [33 , merkle_hash (11 , 22 ), 0xdead , 0xbeef , 0xf00d ];
603+
604+ let from_db_result = append_leaves_to_snapshot ::<5 , 8 >(snapshot , new_leaves , 2 , from_db );
605+ assert_eq (from_db_result .root , expected_root );
606+ assert_eq (from_db_result .next_available_leaf_index , 5 );
607+
608+ let junk_result = append_leaves_to_snapshot ::<5 , 8 >(snapshot , new_leaves , 2 , adversarial );
609+ assert_eq (junk_result .root , expected_root );
610+ assert_eq (junk_result .next_available_leaf_index , 5 );
611+ }
612+
613+ #[test]
614+ fn append_reuses_stale_pinned_frontier_entry () {
615+ // Appending one leaf at index 5 leaves level 2 untouched by the merge: the batch has no node there. The final
616+ // root recomputation still reads that entry, because the end index 6 is a right child at level 2. The reuse is
617+ // sound only because the start index 5 is a right child at level 2 as well, so the entry is pinned.
618+ let before_root = MerkleTree ::<32 >::new (padded ([11 , 22 , 33 , 44 , 55 ])).get_root ();
619+ let snapshot = AppendOnlyTreeSnapshot { root : before_root , next_available_leaf_index : 5 };
620+ let level_2_node = merkle_hash (merkle_hash (11 , 22 ), merkle_hash (33 , 44 ));
621+ let frontier = [55 , 0 , level_2_node , 0 , 0 ];
622+
623+ let new_leaves : [Field ; 8 ] = padded ([66 ]);
624+ let result = append_leaves_to_snapshot ::<5 , 8 >(snapshot , new_leaves , 1 , frontier );
625+
626+ let expected_root = MerkleTree ::<32 >::new (padded ([11 , 22 , 33 , 44 , 55 , 66 ])).get_root ();
627+ assert_eq (result .root , expected_root );
628+ assert_eq (result .next_available_leaf_index , 6 );
629+ }
630+
631+ #[test(should_fail_with = "Frontier hint does not match the snapshot root")]
632+ fn append_wrong_stale_pinned_frontier_entry_fails () {
633+ // The level-2 entry that `append_reuses_stale_pinned_frontier_entry` reuses is pinned, so corrupting it is
634+ // rejected instead of silently producing a wrong root.
635+ let before_root = MerkleTree ::<32 >::new (padded ([11 , 22 , 33 , 44 , 55 ])).get_root ();
636+ let snapshot = AppendOnlyTreeSnapshot { root : before_root , next_available_leaf_index : 5 };
637+ let level_2_node = merkle_hash (merkle_hash (11 , 22 ), merkle_hash (33 , 44 ));
638+ let frontier = [55 , 0 , level_2_node + 1 , 0 , 0 ];
639+
640+ let new_leaves : [Field ; 8 ] = padded ([66 ]);
641+ let _ = append_leaves_to_snapshot ::<5 , 8 >(snapshot , new_leaves , 1 , frontier );
642+ }
643+
644+ #[test]
645+ fn append_filling_tree_exactly () {
646+ // Filling the last four leaves of a height-4 tree takes the exact-fill branch: the post-state has no
647+ // representable frontier, so the root comes from the top of the merged batch instead of a recomputation.
648+ // Levels 0 and 1 are left children at index 12, so their junk hint lanes must not reach the result.
649+ let mut pre_leaves = [0 ; 16 ];
650+ for i in 0 ..12 {
651+ pre_leaves [i ] = (100 + i ) as Field ;
652+ }
653+ let pre_tree = MerkleTree ::new (pre_leaves );
654+ let snapshot =
655+ AppendOnlyTreeSnapshot { root : pre_tree .get_root (), next_available_leaf_index : 12 };
656+ // Levels 2 and 3 are right children at index 12: the node over leaves 8..11 and the node over leaves 0..7.
657+ let level_2_node = merkle_hash (merkle_hash (108 , 109 ), merkle_hash (110 , 111 ));
658+ let left_half = MerkleTree ::<8 >::new ([100 , 101 , 102 , 103 , 104 , 105 , 106 , 107 ]);
659+ let frontier = [0xdead , 0xbeef , level_2_node , left_half .get_root ()];
660+
661+ let batch : [Field ; 6 ] = padded ([201 , 202 , 203 , 204 ]);
662+ let result = append_leaves_to_snapshot ::<4 , 6 >(snapshot , batch , 4 , frontier );
663+
664+ let mut all_leaves = pre_leaves ;
665+ for i in 0 ..4 {
666+ all_leaves [12 + i ] = (201 + i ) as Field ;
667+ }
668+ assert_eq (result .root , MerkleTree ::new (all_leaves ).get_root ());
669+ assert_eq (result .next_available_leaf_index , 16 );
670+ }
671+
672+ #[test(should_fail_with = "append_leaves_to_snapshot exceeds the tree capacity")]
673+ fn append_past_tree_capacity_fails () {
674+ // A height-4 tree holds 16 leaves, so appending 3 from index 14 overruns it. The capacity check runs before
675+ // the hint validation, hence the all-zero frontier here.
676+ let mut pre_leaves = [0 ; 16 ];
677+ for i in 0 ..14 {
678+ pre_leaves [i ] = (100 + i ) as Field ;
679+ }
680+ let pre_tree = MerkleTree ::new (pre_leaves );
681+ let snapshot =
682+ AppendOnlyTreeSnapshot { root : pre_tree .get_root (), next_available_leaf_index : 14 };
683+
684+ let batch : [Field ; 6 ] = padded ([201 , 202 , 203 ]);
685+ let _ = append_leaves_to_snapshot ::<4 , 6 >(snapshot , batch , 3 , [0 ; 4 ]);
686+ }
687+
588688 #[test(should_fail_with = "Found non-zero field after breakpoint")]
589689 fn append_non_zero_padding_lane_fails () {
590690 let snapshot = empty_snapshot ::<5 >();
0 commit comments