Skip to content

Commit 3923fc4

Browse files
committed
test(fast-inbox): constrain the frontier-hint and capacity edge cases of append_leaves_to_snapshot (A-1372)
1 parent 9a4faed commit 3923fc4

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

noir-projects/noir-protocol-circuits/crates/types/src/merkle_tree/append_only_tree.nr

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)