Skip to content

Commit d266b30

Browse files
committed
feat: per-block L1-to-L2 message bundles, move parity to checkpoint root (A-1374)
Implements AZIP-22 Fast Inbox FI-04. Parity verification moves from the block-root variants to the checkpoint root. Every block-root variant now takes a per-block message bundle (l1_to_l2_messages, num_msgs, frontier hint) that appends to the L1-to-L2 tree via append_leaves_to_snapshot and absorbs the same leaves into an L1ToL2MessageSponge threaded across the checkpoint's blocks. BlockRollupPublicInputs replaces in_hash and the FI-03 inbox_rolling_hash pair with an explicit is_first_block flag and start/end message sponges. The checkpoint root gains the parity-root proof, asserts sponge continuity and that the merged block sponge equals the parity sponge, and sources the header's legacy in_hash and inbox_rolling_hash from parity. Transitionally the first block carries the whole checkpoint's messages padded to 1024 with num_msgs=1024; non-first blocks carry empty bundles. Behavior stays bit-identical to the current state (same tree roots, same header bytes).
1 parent c818a3d commit d266b30

54 files changed

Lines changed: 1363 additions & 671 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

barretenberg/cpp/src/barretenberg/aztec/aztec_constants.hpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,7 @@
244244
#define AVM_EMITPUBLICLOG_DYN_DA_GAS 32
245245
#define AVM_SSTORE_DYN_DA_GAS 64
246246
#define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_HEIGHT 6
247-
#define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_ROOT \
248-
"0x13954b76f8ab76c38ab687731f802fc90e5d55e0592fa6f56d0a73024b3f8356"
247+
#define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_ROOT "0x13954b76f8ab76c38ab687731f802fc90e5d55e0592fa6f56d0a73024b3f8356"
249248
#define AVM_WRITTEN_PUBLIC_DATA_SLOTS_TREE_INITIAL_SIZE 1
250249
#define AVM_RETRIEVED_BYTECODES_TREE_HEIGHT 5
251250
#define AVM_RETRIEVED_BYTECODES_TREE_INITIAL_ROOT "0x0ff65ad321f05745d4c80e2e18e94241093d2e49017ff8cc151df4f207a43c12"

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/abis/block_rollup_public_inputs.nr

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::abis::L1ToL2MessageSponge;
12
use protocol_test_utils::make_fixture;
23
use types::{
34
abis::{
@@ -39,17 +40,19 @@ pub struct BlockRollupPublicInputs {
3940
// stored in the checkpoint header, enabling validation of the blocks included in a checkpoint given their headers.
4041
pub block_headers_hash: Field,
4142

42-
// Root of the `l1_to_l2` message subtree, set in the first block root and propagated to the checkpoint root.
43-
// Block root rollups that are not the first in a checkpoint will have an `in_hash` value of 0.
44-
pub in_hash: Field,
45-
46-
// Inbox rolling-hash chain segment consumed by this block range, set in the first block root from the parity root
47-
// and propagated to the checkpoint root exactly like `in_hash`: the first block root carries the checkpoint's
48-
// `(start, end)`, non-first block roots carry `(0, 0)`, merges take the left rollup's pair, and
49-
// `validate_consecutive_block_rollups` asserts the right rollup's pair is zero. Unlike `in_hash`, the value may
50-
// legitimately be zero (genesis / empty checkpoint), so it is not asserted nonzero at the checkpoint root.
51-
pub start_inbox_rolling_hash: Field,
52-
pub end_inbox_rolling_hash: Field,
43+
// Whether this block range starts at the first block of its checkpoint. Only the first block root sets this true;
44+
// merges propagate it from the left rollup and `validate_consecutive_block_rollups` asserts the right rollup's is
45+
// false, so it can only reach the checkpoint root through the leftmost leaf. It replaces `in_hash`'s former
46+
// structural role: the checkpoint root asserts the merged value is true, and it drives the block-end blob-absorb
47+
// flag (the l1-to-l2 tree root is absorbed only for the first block).
48+
pub is_first_block: bool,
49+
50+
// Poseidon2 message-bundle sponge threaded across the blocks of the checkpoint. The leftmost block starts from the
51+
// empty sponge and each block absorbs its bundle's leaves; `validate_consecutive_block_rollups` asserts
52+
// `right.start_msg_sponge == left.end_msg_sponge`, and merges take `start` from the left and `end` from the right.
53+
// The checkpoint root asserts the merged end equals the parity root's sponge over the same (padded) leaf list.
54+
pub start_msg_sponge: L1ToL2MessageSponge,
55+
pub end_msg_sponge: L1ToL2MessageSponge,
5356

5457
// Root of the *wonky* tree composed of all tx out hashes in this block. It will be combined with the `out_hash`
5558
// values from other blocks within the same checkpoint to form a *wonky* tree.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// TODO: Move to types/src/abis/rollup/
2+
// `l1_to_l2_message_sponge` is declared first because the serde `#[derive(...)]` macro requires a field type's module
3+
// to be declared before any struct that embeds it (see the ordering note in `types/src/lib.nr`).
4+
mod l1_to_l2_message_sponge;
25
mod public_chonk_verifier_public_inputs;
36
mod tx_rollup_public_inputs;
47
mod block_rollup_public_inputs;
58
mod parity_public_inputs;
69
mod checkpoint_rollup_public_inputs;
710
mod root_rollup_public_inputs;
8-
mod l1_to_l2_message_sponge;
911

1012
pub use block_rollup_public_inputs::BlockRollupPublicInputs;
1113
pub use checkpoint_rollup_public_inputs::CheckpointRollupPublicInputs;

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/abis/parity_public_inputs.nr

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::abis::L1ToL2MessageSponge;
12
use types::traits::{Deserialize, Empty, Serialize};
23

34
#[derive(Deserialize, Eq, Serialize)]
@@ -9,6 +10,13 @@ pub struct ParityPublicInputs {
910
// Rolling hash of the Inbox message chain after absorbing the `num_msgs` real messages in this batch. Each link is
1011
// `sha256ToField(prev || msg)`, matching the truncated-to-field sha256 the L1 Inbox accumulates.
1112
pub end_rolling_hash: Field,
13+
// Poseidon2 message-bundle sponge before absorbing this batch of leaves. Threaded across the four base parity
14+
// segments the same way as the rolling hash, so each segment's start equals the previous segment's end.
15+
pub start_sponge: L1ToL2MessageSponge,
16+
// Message-bundle sponge after absorbing the full (padded) batch of leaves. Unlike the rolling hash, the sponge
17+
// absorbs every leaf inserted into the L1-to-L2 tree, including padding zeros (AZIP-22 transitional asymmetry). The
18+
// checkpoint root asserts this equals the sponge accumulated across the checkpoint's block roots.
19+
pub end_sponge: L1ToL2MessageSponge,
1220
// Number of real (non-padding) messages absorbed into the rolling hash by this batch.
1321
pub num_msgs: u32,
1422
pub vk_tree_root: Field,
@@ -22,6 +30,8 @@ impl Empty for ParityPublicInputs {
2230
converted_root: 0,
2331
start_rolling_hash: 0,
2432
end_rolling_hash: 0,
33+
start_sponge: L1ToL2MessageSponge::empty(),
34+
end_sponge: L1ToL2MessageSponge::empty(),
2535
num_msgs: 0,
2636
vk_tree_root: 0,
2737
prover_id: 0,

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/tests/consecutive_block_rollups_tests.nr

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use super::TestBuilder;
2-
use types::{address::EthAddress, hash::accumulate_sha256};
2+
use types::{
3+
address::EthAddress, constants::BLOCK_ROOT_ROLLUP_VK_INDEX, hash::accumulate_sha256,
4+
};
35

46
#[test]
57
fn accumulated_out_hash_correctly() {
@@ -54,45 +56,53 @@ fn output_zero_when_both_out_hashes_are_zero() {
5456
}
5557

5658
#[test]
57-
fn non_zero_in_hash_in_left_rollup() {
59+
fn first_block_in_left_rollup() {
60+
// The default builder has a first block root on the left and a non-first block on the right.
5861
let mut builder = TestBuilder::default();
5962

60-
builder.left_rollup.in_hash = 123;
61-
6263
let pi = builder.execute();
6364
builder.assert_expected_public_inputs(pi);
6465

65-
assert_eq(pi.in_hash, 123);
66+
assert(pi.is_first_block);
6667
}
6768

6869
#[test]
69-
fn zero_in_hash_in_both_rollups() {
70-
let mut builder = TestBuilder::default();
71-
72-
builder.left_rollup.in_hash = 0;
73-
builder.right_rollup.in_hash = 0;
70+
fn no_first_block_in_either_rollup() {
71+
// A merge of two non-first blocks in the middle of a checkpoint carries no first block.
72+
let mut builder =
73+
TestBuilder::new(BLOCK_ROOT_ROLLUP_VK_INDEX, 1, BLOCK_ROOT_ROLLUP_VK_INDEX, 1);
7474

7575
let pi = builder.execute();
7676
builder.assert_expected_public_inputs(pi);
7777

78-
assert_eq(pi.in_hash, 0);
78+
assert(!pi.is_first_block);
79+
}
80+
81+
#[test(should_fail_with = "Right rollup must not be a first block")]
82+
fn first_block_in_right_rollup() {
83+
let mut builder = TestBuilder::default();
84+
85+
builder.right_rollup.is_first_block = true;
86+
87+
builder.execute_and_fail();
7988
}
8089

81-
#[test(should_fail_with = "Right rollup must not carry in_hash")]
82-
fn non_zero_in_hash_in_right_rollup() {
90+
#[test(should_fail_with = "Right rollup must not be a first block")]
91+
fn first_block_in_both_rollups() {
8392
let mut builder = TestBuilder::default();
8493

85-
builder.right_rollup.in_hash = 123;
94+
builder.left_rollup.is_first_block = true;
95+
builder.right_rollup.is_first_block = true;
8696

8797
builder.execute_and_fail();
8898
}
8999

90-
#[test(should_fail_with = "Right rollup must not carry in_hash")]
91-
fn non_zero_in_hash_in_both_rollups() {
100+
#[test(should_fail_with = "Mismatched message sponge: expected right.start_msg_sponge to match left.end_msg_sponge")]
101+
fn mismatched_message_sponge_fails() {
92102
let mut builder = TestBuilder::default();
93103

94-
builder.left_rollup.in_hash = 123;
95-
builder.right_rollup.in_hash = 123;
104+
// Break the sponge continuity between the left and right rollups.
105+
builder.right_rollup.start_msg_sponge.num_absorbed += 1;
96106

97107
builder.execute_and_fail();
98108
}

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ impl TestBuilder {
5858

5959
if !is_first_block(left_rollup_vk_index)
6060
& (left_rollup_vk_index != BLOCK_MERGE_ROLLUP_VK_INDEX) {
61-
// Change the start_block_number to be smaller than the left rollup's start_block_number so that the in_hash
62-
// won't be set on the left rollup.
61+
// Change the start_block_number to be smaller than the left rollup's start_block_number so that
62+
// `is_first_block` won't be set on the left rollup.
6363
fixture_builder.start_block_number = start_block_number - 1;
6464
}
6565
let left_rollup = fixture_builder.get_merged_block_rollup_public_inputs(
@@ -68,8 +68,8 @@ impl TestBuilder {
6868
);
6969

7070
if is_first_block(right_rollup_vk_index) {
71-
// Change the start_block_number to be the right rollup's start_block_number so that the in_hash will be set
72-
// on the right rollup.
71+
// Change the start_block_number to be the right rollup's start_block_number so that `is_first_block` will be
72+
// set on the right rollup.
7373
fixture_builder.start_block_number = start_block_number + num_left_blocks as u32;
7474
}
7575
let right_rollup = fixture_builder.get_merged_block_rollup_public_inputs(
@@ -143,10 +143,11 @@ impl TestBuilder {
143143
accumulate_block_headers_hash(left.block_headers_hash, right.block_headers_hash);
144144
assert_eq(pi.block_headers_hash, expected_block_headers_hash);
145145

146-
assert_eq(pi.in_hash, left.in_hash);
147-
148-
assert_eq(pi.start_inbox_rolling_hash, left.start_inbox_rolling_hash);
149-
assert_eq(pi.end_inbox_rolling_hash, left.end_inbox_rolling_hash);
146+
// `is_first_block` and the start of the message sponge come from the left rollup; the sponge end comes from the
147+
// right rollup.
148+
assert_eq(pi.is_first_block, left.is_first_block);
149+
assert_eq(pi.start_msg_sponge, left.start_msg_sponge);
150+
assert_eq(pi.end_msg_sponge, right.end_msg_sponge);
150151

151152
let expected_out_hash = if (left.out_hash != 0) & (right.out_hash != 0) {
152153
accumulate_sha256(left.out_hash, right.out_hash)

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/utils/merge_block_rollups.nr

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ pub fn merge_block_rollups(
1616
let block_headers_hash =
1717
accumulate_block_headers_hash(left.block_headers_hash, right.block_headers_hash);
1818

19-
// `in_hash` originates from the first block root and must propagate through all merge steps via the left rollup
20-
// only. It's checked in `validate_consecutive_block_rollups` to make sure only the left rollup carries it.
21-
let in_hash = left.in_hash;
19+
// `is_first_block` originates from the first block root and must propagate through all merge steps via the left
20+
// rollup only. `validate_consecutive_block_rollups` asserts the right rollup's is false, so only the leftmost leaf
21+
// can carry it up to the checkpoint root.
22+
let is_first_block = left.is_first_block;
2223

23-
// The inbox rolling-hash pair propagates the same way: the first block root carries the checkpoint's `(start, end)`,
24-
// so we take the left rollup's pair. `validate_consecutive_block_rollups` asserts the right rollup's pair is zero.
25-
let start_inbox_rolling_hash = left.start_inbox_rolling_hash;
26-
let end_inbox_rolling_hash = left.end_inbox_rolling_hash;
24+
// The message sponge threads across the checkpoint's blocks: the range spans left's start through right's end.
25+
// `validate_consecutive_block_rollups` asserts `right.start_msg_sponge == left.end_msg_sponge` for continuity.
26+
let start_msg_sponge = left.start_msg_sponge;
27+
let end_msg_sponge = right.end_msg_sponge;
2728

2829
let out_hash = accumulate_out_hash(left.out_hash, right.out_hash);
2930

@@ -41,9 +42,9 @@ pub fn merge_block_rollups(
4142
end_sponge_blob: right.end_sponge_blob,
4243
timestamp: left.timestamp, // Both blocks have the same timestamp.
4344
block_headers_hash,
44-
in_hash,
45-
start_inbox_rolling_hash,
46-
end_inbox_rolling_hash,
45+
is_first_block,
46+
start_msg_sponge,
47+
end_msg_sponge,
4748
out_hash,
4849
accumulated_fees,
4950
accumulated_mana_used,

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/utils/validate_consecutive_block_rollups.nr

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,17 @@ fn assert_prev_block_rollups_follow_on_from_each_other(
4444

4545
// TODO: Consider extracting into its own function:
4646

47-
// A non-empty `in_hash` originates from the first block root only, and is propagated through all merge
48-
// steps via the left rollup only (see merge_block_rollups.nr).
49-
// We prevent the right rollup from propagating a nonzero `in_hash`, so it's impossible for any
50-
// block but the first to propagate a nonzero `in_hash` up to the checkpoint root.
51-
assert_eq(right.in_hash, 0, "Right rollup must not carry in_hash");
52-
53-
// The inbox rolling-hash pair follows the same rule: only the first block root (which becomes the leftmost leaf)
54-
// may carry it, so the right rollup's pair must always be zero. The first block's nonzero `in_hash` already pins it
55-
// as the leftmost leaf, so the rolling hash rides along on the same block even when its value is legitimately zero.
47+
// `is_first_block` originates from the first block root only, and is propagated through all merge steps via the
48+
// left rollup only (see merge_block_rollups.nr). We prevent the right rollup from carrying it, so it's impossible
49+
// for any block but the first to propagate `is_first_block == true` up to the checkpoint root.
50+
assert(!right.is_first_block, "Right rollup must not be a first block");
51+
52+
// The message sponge threads across the checkpoint's blocks: the right rollup must start from where the left rollup
53+
// ended. Combined with the checkpoint root asserting the leftmost start is empty and the merged end matches the
54+
// parity root's sponge, this pins the blocks to insert exactly the parity-committed message list, in order.
5655
assert_eq(
57-
right.start_inbox_rolling_hash,
58-
0,
59-
"Right rollup must not carry start_inbox_rolling_hash",
56+
right.start_msg_sponge,
57+
left.end_msg_sponge,
58+
"Mismatched message sponge: expected right.start_msg_sponge to match left.end_msg_sponge",
6059
);
61-
assert_eq(right.end_inbox_rolling_hash, 0, "Right rollup must not carry end_inbox_rolling_hash");
6260
}
Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
use crate::{
2-
abis::{BlockRollupPublicInputs, ParityPublicInputs},
3-
block_root::components::{BlockRollupPublicInputsComposer, validate_parity_root},
2+
abis::{BlockRollupPublicInputs, L1ToL2MessageSponge},
3+
block_root::components::BlockRollupPublicInputsComposer,
44
};
55
use types::{
66
abis::{
77
append_only_tree_snapshot::AppendOnlyTreeSnapshot,
88
checkpoint_constant_data::CheckpointConstantData, state_reference::StateReference,
99
},
10-
constants::{ARCHIVE_HEIGHT, L1_TO_L2_MSG_SUBTREE_ROOT_SIBLING_PATH_LENGTH},
11-
proof::proof_data::UltraHonkProofData,
10+
constants::{ARCHIVE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, MAX_L1_TO_L2_MSGS_PER_BLOCK},
1211
};
1312

1413
pub struct BlockRootEmptyTxFirstRollupPrivateInputs {
15-
pub(crate) parity_root: UltraHonkProofData<ParityPublicInputs>,
16-
1714
pub(crate) previous_archive: AppendOnlyTreeSnapshot,
1815
pub(crate) previous_state: StateReference,
1916
// The previous block is not in the same checkpoint as the current block. So we need to provide the constants for
@@ -23,19 +20,22 @@ pub struct BlockRootEmptyTxFirstRollupPrivateInputs {
2320
// timestamp in the checkpoint root.
2421
pub(crate) timestamp: u64,
2522

26-
// Hint for inserting the new l1 to l2 message subtree.
27-
pub(crate) new_l1_to_l2_message_subtree_root_sibling_path: [Field; L1_TO_L2_MSG_SUBTREE_ROOT_SIBLING_PATH_LENGTH],
23+
// L1-to-L2 messages inserted by this block, padded with zeros beyond `num_msgs`.
24+
pub(crate) l1_to_l2_messages: [Field; MAX_L1_TO_L2_MSGS_PER_BLOCK],
25+
// Number of real (non-padding) leaves in `l1_to_l2_messages`.
26+
pub(crate) num_msgs: u32,
27+
// Frontier hint for appending the bundle to the l1-to-l2 message tree (validated against `previous_state`).
28+
pub(crate) l1_to_l2_message_frontier_hint: [Field; L1_TO_L2_MSG_TREE_HEIGHT],
2829
// Hint for inserting the new block hash to the last archive.
2930
pub(crate) new_archive_sibling_path: [Field; ARCHIVE_HEIGHT],
3031
}
3132

3233
/// The Block Root Empty Tx First Rollup circuit creates the first empty block of a checkpoint.
33-
/// It processes L1-to-L2 messages and creates the block header.
34+
/// It inserts the block's L1-to-L2 message bundle and creates the block header.
3435
/// This variant is used for the first block in a checkpoint that contains no transactions.
3536
///
3637
/// This circuit:
37-
/// - Verifies the parity root proof containing L1-to-L2 messages for the checkpoint
38-
/// - Inserts the L1-to-L2 message subtree into the L1-to-L2 message tree
38+
/// - Appends the block's L1-to-L2 message bundle to the L1-to-L2 message tree and absorbs it into the message sponge
3939
/// - Initializes an empty sponge blob (since it's the first block in a checkpoint)
4040
/// - Computes the block header hash and inserts it into the archive tree
4141
///
@@ -44,21 +44,19 @@ pub struct BlockRootEmptyTxFirstRollupPrivateInputs {
4444
///
4545
/// VkIndex: BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX
4646
pub fn execute(inputs: BlockRootEmptyTxFirstRollupPrivateInputs) -> BlockRollupPublicInputs {
47-
validate_parity_root(
48-
inputs.parity_root,
49-
inputs.constants.vk_tree_root,
50-
inputs.constants.prover_id,
51-
);
52-
5347
BlockRollupPublicInputsComposer::new_from_no_rollups(
5448
inputs.previous_archive,
5549
inputs.previous_state,
5650
inputs.constants,
5751
inputs.timestamp,
5852
)
59-
.with_new_l1_to_l2_messages(
60-
inputs.parity_root.public_inputs,
61-
inputs.new_l1_to_l2_message_subtree_root_sibling_path,
53+
.with_message_bundle(
54+
true,
55+
inputs.previous_state.l1_to_l2_message_tree,
56+
L1ToL2MessageSponge::new(),
57+
inputs.l1_to_l2_messages,
58+
inputs.num_msgs,
59+
inputs.l1_to_l2_message_frontier_hint,
6260
)
6361
.finish(inputs.new_archive_sibling_path)
6462
}

0 commit comments

Comments
 (0)