Skip to content

Commit d22f230

Browse files
authored
fix: preserve empty items when merging checkpoints (#19288)
The L1 contract processes fees in the array one by one, assuming each entry corresponds to the checkpoint at the same position, and uses the index to get the fee header. But the circuits allow zero fee and recipient. So when merging fees from two checkpoints' public inputs, their indexes need to be preserved, we can't skip the empty items.
2 parents de1c0ac + e72f246 commit d22f230

File tree

5 files changed

+451
-7
lines changed

5 files changed

+451
-7
lines changed

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/tests/consecutive_checkpoint_rollups_tests.nr

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
use super::TestBuilder;
2+
use types::{
3+
abis::fee_recipient::FeeRecipient, address::EthAddress,
4+
constants::CHECKPOINT_MERGE_ROLLUP_VK_INDEX, tests::utils::pad_end, traits::Empty,
5+
};
26

37
#[test]
48
fn correct_default_inputs() {
@@ -7,6 +11,102 @@ fn correct_default_inputs() {
711
builder.assert_expected_public_inputs(pi);
812
}
913

14+
// --- fees ---
15+
16+
fn mock_fee(recipient: Field, value: Field) -> FeeRecipient {
17+
FeeRecipient { recipient: EthAddress::from_field(recipient), value }
18+
}
19+
20+
#[test]
21+
fn accumulated_fees_from_both_side() {
22+
let mut builder = TestBuilder::new(
23+
CHECKPOINT_MERGE_ROLLUP_VK_INDEX,
24+
2,
25+
CHECKPOINT_MERGE_ROLLUP_VK_INDEX,
26+
2,
27+
);
28+
29+
let fees = [mock_fee(11, 23), mock_fee(456, 7), mock_fee(8, 90), mock_fee(1234, 56789)];
30+
31+
builder.left_rollup.fees[0] = fees[0];
32+
builder.left_rollup.fees[1] = fees[1];
33+
builder.right_rollup.fees[0] = fees[2];
34+
builder.right_rollup.fees[1] = fees[3];
35+
36+
let pi = builder.execute();
37+
builder.assert_expected_public_inputs(pi);
38+
39+
assert_eq(pi.fees, pad_end(fees));
40+
}
41+
42+
#[test]
43+
fn preserve_empty_fees_from_left() {
44+
let mut builder = TestBuilder::new(
45+
CHECKPOINT_MERGE_ROLLUP_VK_INDEX,
46+
2,
47+
CHECKPOINT_MERGE_ROLLUP_VK_INDEX,
48+
2,
49+
);
50+
51+
let fees = [mock_fee(0, 0), mock_fee(11, 23), mock_fee(456, 7), mock_fee(8, 90)];
52+
53+
builder.left_rollup.fees[0] = FeeRecipient::empty();
54+
builder.left_rollup.fees[1] = fees[1];
55+
builder.right_rollup.fees[0] = fees[2];
56+
builder.right_rollup.fees[1] = fees[3];
57+
58+
let pi = builder.execute();
59+
builder.assert_expected_public_inputs(pi);
60+
61+
assert_eq(pi.fees, pad_end(fees));
62+
}
63+
64+
#[test]
65+
fn preserve_empty_fees_from_right() {
66+
let mut builder = TestBuilder::new(
67+
CHECKPOINT_MERGE_ROLLUP_VK_INDEX,
68+
2,
69+
CHECKPOINT_MERGE_ROLLUP_VK_INDEX,
70+
2,
71+
);
72+
73+
let fees = [mock_fee(11, 23), mock_fee(456, 7), mock_fee(0, 0), mock_fee(8, 90)];
74+
75+
builder.left_rollup.fees[0] = fees[0];
76+
builder.left_rollup.fees[1] = fees[1];
77+
builder.right_rollup.fees[0] = FeeRecipient::empty();
78+
builder.right_rollup.fees[1] = fees[3];
79+
80+
let pi = builder.execute();
81+
builder.assert_expected_public_inputs(pi);
82+
83+
assert_eq(pi.fees, pad_end(fees));
84+
}
85+
86+
#[test]
87+
fn preserve_empty_fees_from_both_sides() {
88+
let mut builder = TestBuilder::new(
89+
CHECKPOINT_MERGE_ROLLUP_VK_INDEX,
90+
2,
91+
CHECKPOINT_MERGE_ROLLUP_VK_INDEX,
92+
2,
93+
);
94+
95+
let fees = [mock_fee(11, 23), mock_fee(0, 0), mock_fee(0, 0), mock_fee(456, 7)];
96+
97+
builder.left_rollup.fees[0] = fees[0];
98+
builder.left_rollup.fees[1] = FeeRecipient::empty();
99+
builder.right_rollup.fees[0] = FeeRecipient::empty();
100+
builder.right_rollup.fees[1] = fees[3];
101+
102+
let pi = builder.execute();
103+
builder.assert_expected_public_inputs(pi);
104+
105+
assert_eq(pi.fees, pad_end(fees));
106+
}
107+
108+
// -- constants ---
109+
10110
#[test(should_fail_with = "Mismatched constants in checkpoint rollups")]
11111
fn mismatch_chain_id() {
12112
let mut builder = TestBuilder::default();
@@ -69,6 +169,8 @@ fn mismatch_prover_id() {
69169
builder.execute_and_fail();
70170
}
71171

172+
// --- archives ---
173+
72174
#[test(should_fail_with = "Mismatched archives: expected right.previous_archive to match left.new_archive")]
73175
fn non_consecutive_archive_roots() {
74176
let mut builder = TestBuilder::default();
@@ -89,6 +191,8 @@ fn non_consecutive_archive_next_available_leaf_indices() {
89191
builder.execute_and_fail();
90192
}
91193

194+
// --- blob accumulators ---
195+
92196
#[test(should_fail_with = "Mismatched blob accumulators: expected right.start_blob_accumulator to match left.end_blob_accumulator")]
93197
fn non_consecutive_blob_accumulators() {
94198
let mut builder = TestBuilder::default();

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/tests/mod.nr

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,12 @@ impl TestBuilder {
109109
expected_header_hashes[i] = left.checkpoint_header_hashes[i];
110110
expected_fees[i] = left.fees[i];
111111
assert(left.checkpoint_header_hashes[i] != 0);
112-
assert(!left.fees[i].is_empty());
113112
}
114113
let offset = self.num_left_checkpoints as u32;
115114
for i in 0..self.num_right_checkpoints as u32 {
116115
expected_header_hashes[i + offset] = right.checkpoint_header_hashes[i];
117116
expected_fees[i + offset] = right.fees[i];
118117
assert(right.checkpoint_header_hashes[i] != 0);
119-
assert(!right.fees[i].is_empty());
120118
}
121119
assert_eq(pi.checkpoint_header_hashes, expected_header_hashes);
122120
assert_eq(pi.fees, expected_fees);

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/utils/merge_checkpoint_rollups.nr

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,43 @@
11
use crate::abis::CheckpointRollupPublicInputs;
2-
use types::{constants::AZTEC_MAX_EPOCH_DURATION, utils::arrays::array_merge};
2+
use types::{constants::AZTEC_MAX_EPOCH_DURATION, utils::arrays::copy_items_into_array};
33

44
pub fn merge_checkpoint_rollups(
55
left: CheckpointRollupPublicInputs,
66
right: CheckpointRollupPublicInputs,
77
) -> CheckpointRollupPublicInputs {
8+
let num_left_checkpoints = left.num_checkpoints() as u32;
9+
let num_right_checkpoints = right.num_checkpoints() as u32;
10+
811
// Make sure that the total number of checkpoints does not exceed the maximum allowed in an epoch, preventing the
912
// merged arrays (`checkpoint_header_hashes`, `fees`) from being truncated.
10-
let num_checkpoints = left.num_checkpoints() + right.num_checkpoints();
13+
let num_checkpoints = num_left_checkpoints + num_right_checkpoints;
1114
assert(
12-
num_checkpoints <= AZTEC_MAX_EPOCH_DURATION as u16,
15+
num_checkpoints <= AZTEC_MAX_EPOCH_DURATION,
1316
"total number of checkpoints exceeds max allowed in an epoch",
1417
);
1518

16-
let checkpoint_header_hashes = array_merge(
19+
// We use `copy_items_into_array` below because we know exactly how many items should be taken from each side when
20+
// merging checkpoints.
21+
// It is especially critical for `fees` to copy the exact amount of items from both checkpoints. Because the L1
22+
// contracts will process fees by index, assuming each entry corresponds to the checkpoint at the same position.
23+
// However, the existence of zero fees or recipients is not prohibited by the circuit. Therefore, preserving empty
24+
// values is important.
25+
// Using this helper (instead of helper like `array_merge`) ensures those empty entries are copied over.
26+
27+
let checkpoint_header_hashes = copy_items_into_array(
1728
left.checkpoint_header_hashes,
29+
num_left_checkpoints,
1830
right.checkpoint_header_hashes,
31+
num_right_checkpoints,
1932
);
2033

2134
// We can't merge fees for the same recipient since the l1 contract iterates per checkpoint.
22-
let fees = array_merge(left.fees, right.fees);
35+
let fees = copy_items_into_array(
36+
left.fees,
37+
num_left_checkpoints,
38+
right.fees,
39+
num_right_checkpoints,
40+
);
2341

2442
CheckpointRollupPublicInputs {
2543
constants: left.constants,

noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays.nr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
pub(crate) mod assert_trailing_zeros;
2+
pub(crate) mod copy_items_into_array;
23
pub(crate) mod find_index;
34
pub(crate) mod get_sorted_tuples;
45

56
// Re-exports.
67
pub use assert_trailing_zeros::assert_trailing_zeros;
8+
pub use copy_items_into_array::copy_items_into_array;
79
pub use find_index::{find_first_index, find_last_index};
810
pub use get_sorted_tuples::{get_sorted_tuples, SortedTuple};
911

0 commit comments

Comments
 (0)