Skip to content

Commit 3b37c67

Browse files
committed
feat(fast-inbox): same-block L1-to-L2 message consumption for non-first blocks (A-1432)
The non-first tx-carrying block-root variants (block_root, block_root_single_tx) now take their start L1-to-L2 tree snapshot as a witness input, pinned by block-merge continuity (right.start_state == left.end_state) to the previous block's end state, and assert the tx constants carry their own post-bundle root via validate_l1_to_l2_tree_snapshot_in_constants, exactly like the first-block variants. This lets a public/AVM tx in block N read the messages block N inserts (same-block consumption) instead of only from block N+1. Previously the non-first variants read their start snapshot from constants.l1_to_l2_tree_snapshot and never asserted the post-bundle root, so mid-checkpoint insertions were next-block-visible. Bit-identical pre-flip: non-first bundles are empty, so post-bundle == start == today's constants value, and the new assert passes with unchanged values. The tx-less variants (empty-first, msgs-only) carry no tx constants and already witness their start snapshot, so they are unchanged. Adds negative nargo tests (a non-first block whose constants snapshot differs from its post-bundle root must fail) and mirrors the new witness field through stdlib serialization, the Noir ABI conversion, and the prover-client block-root input builders. VK/artifact regen, checkpoint-root/block-root Prover.toml sample-input regen, and the generated noir-protocol-circuits-types (index.ts) regen ride the mainframe/CI flow; they are not regenerated locally (bb write_vk OOMs on large circuits on dev boxes).
1 parent 347a6e1 commit 3b37c67

11 files changed

Lines changed: 152 additions & 43 deletions

File tree

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/block_root_rollup.nr

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
use crate::{
22
abis::{BlockRollupPublicInputs, L1ToL2MessageBundle, L1ToL2MessageSponge, TxRollupPublicInputs},
3-
block_root::components::{BlockRollupPublicInputsComposer, validate_previous_rollups},
3+
block_root::components::{
4+
BlockRollupPublicInputsComposer, validate_l1_to_l2_tree_snapshot_in_constants,
5+
validate_previous_rollups,
6+
},
47
};
58
use types::{
9+
abis::append_only_tree_snapshot::AppendOnlyTreeSnapshot,
610
constants::{
711
ARCHIVE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, PRIVATE_TX_BASE_ROLLUP_VK_INDEX,
812
PUBLIC_TX_BASE_ROLLUP_VK_INDEX, TX_MERGE_ROLLUP_VK_INDEX,
@@ -17,11 +21,15 @@ pub struct BlockRootRollupPrivateInputs {
1721
pub(crate) previous_rollups: [RollupHonkProofData<TxRollupPublicInputs>; 2],
1822
// L1-to-L2 message bundle inserted by this block.
1923
pub(crate) message_bundle: L1ToL2MessageBundle,
24+
// The l1-to-l2 tree snapshot this block builds on (the previous block's post-insertion snapshot). It is a witnessed
25+
// value, pinned by block-merge continuity (`right.start_state == left.end_state`) to the previous block's end
26+
// state; since the checkpoint root forces the leftmost block to be a first-block variant, every non-first block
27+
// has a left neighbour that pins it.
28+
pub(crate) previous_l1_to_l2: AppendOnlyTreeSnapshot,
2029
// Message sponge inherited from the previous block. Checked against the previous block's `end_msg_sponge` in the
2130
// block merge or checkpoint root circuit.
2231
pub(crate) start_msg_sponge: L1ToL2MessageSponge,
23-
// Frontier hint for appending the bundle to the l1-to-l2 message tree (validated against the tree snapshot in the
24-
// constants, which is where the l1-to-l2 tree stands for this non-first block).
32+
// Frontier hint for appending the bundle to the l1-to-l2 message tree (validated against `previous_l1_to_l2`).
2533
pub(crate) l1_to_l2_message_frontier_hint: [Field; L1_TO_L2_MSG_TREE_HEIGHT],
2634
// Hint for inserting the new block hash to the last archive.
2735
pub(crate) new_archive_sibling_path: [Field; ARCHIVE_HEIGHT],
@@ -43,18 +51,29 @@ pub struct BlockRootRollupPrivateInputs {
4351
///
4452
/// VkIndex: BLOCK_ROOT_ROLLUP_VK_INDEX
4553
pub fn execute(inputs: BlockRootRollupPrivateInputs) -> BlockRollupPublicInputs {
46-
validate_previous_rollups(inputs.previous_rollups, ALLOWED_PREVIOUS_VK_INDICES);
47-
4854
let previous_rollups = inputs.previous_rollups.map(|rollup| rollup.public_inputs);
49-
// Non-first blocks build on the l1-to-l2 tree snapshot carried in the checkpoint constants.
50-
let previous_l1_to_l2 = previous_rollups[0].constants.l1_to_l2_tree_snapshot;
51-
BlockRollupPublicInputsComposer::new_from_two_rollups(previous_rollups)
55+
// Non-first blocks build on a witnessed start snapshot (pinned by block-merge continuity to the previous block's
56+
// end state) rather than the constants value, so this block can append its own bundle and still assert the tx
57+
// constants carry the post-bundle snapshot.
58+
let mut composer = BlockRollupPublicInputsComposer::new_from_two_rollups(previous_rollups);
59+
let new_l1_to_l2 = composer
5260
.with_message_bundle(
5361
false,
54-
previous_l1_to_l2,
62+
inputs.previous_l1_to_l2,
5563
inputs.start_msg_sponge,
5664
inputs.message_bundle,
5765
inputs.l1_to_l2_message_frontier_hint,
5866
)
59-
.finish(inputs.new_archive_sibling_path)
67+
.get_new_l1_to_l2();
68+
69+
validate_previous_rollups(inputs.previous_rollups, ALLOWED_PREVIOUS_VK_INDICES);
70+
71+
// The tx constants pin the l1-to-l2 snapshot the AVM validates message reads against; asserting it equals this
72+
// block's post-bundle root lets this block's txs read the messages this block inserts (same-block consumption).
73+
validate_l1_to_l2_tree_snapshot_in_constants(
74+
inputs.previous_rollups[0].public_inputs.constants,
75+
new_l1_to_l2,
76+
);
77+
78+
composer.finish(inputs.new_archive_sibling_path)
6079
}

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/block_root_single_tx_rollup.nr

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
use crate::{
22
abis::{BlockRollupPublicInputs, L1ToL2MessageBundle, L1ToL2MessageSponge, TxRollupPublicInputs},
3-
block_root::components::{BlockRollupPublicInputsComposer, validate_previous_rollups},
3+
block_root::components::{
4+
BlockRollupPublicInputsComposer, validate_l1_to_l2_tree_snapshot_in_constants,
5+
validate_previous_rollups,
6+
},
47
};
58
use types::{
9+
abis::append_only_tree_snapshot::AppendOnlyTreeSnapshot,
610
constants::{
711
ARCHIVE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, PRIVATE_TX_BASE_ROLLUP_VK_INDEX,
812
PUBLIC_TX_BASE_ROLLUP_VK_INDEX,
@@ -18,11 +22,15 @@ pub struct BlockRootSingleTxRollupPrivateInputs {
1822
pub(crate) previous_rollup: RollupHonkProofData<TxRollupPublicInputs>,
1923
// L1-to-L2 message bundle inserted by this block.
2024
pub(crate) message_bundle: L1ToL2MessageBundle,
25+
// The l1-to-l2 tree snapshot this block builds on (the previous block's post-insertion snapshot). It is a witnessed
26+
// value, pinned by block-merge continuity (`right.start_state == left.end_state`) to the previous block's end
27+
// state; since the checkpoint root forces the leftmost block to be a first-block variant, every non-first block
28+
// has a left neighbour that pins it.
29+
pub(crate) previous_l1_to_l2: AppendOnlyTreeSnapshot,
2130
// Message sponge inherited from the previous block. Checked against the previous block's `end_msg_sponge` in the
2231
// block merge or checkpoint root circuit.
2332
pub(crate) start_msg_sponge: L1ToL2MessageSponge,
24-
// Frontier hint for appending the bundle to the l1-to-l2 message tree (validated against the tree snapshot in the
25-
// constants, which is where the l1-to-l2 tree stands for this non-first block).
33+
// Frontier hint for appending the bundle to the l1-to-l2 message tree (validated against `previous_l1_to_l2`).
2634
pub(crate) l1_to_l2_message_frontier_hint: [Field; L1_TO_L2_MSG_TREE_HEIGHT],
2735
// Hint for inserting the new block hash to the last archive.
2836
pub(crate) new_archive_sibling_path: [Field; ARCHIVE_HEIGHT],
@@ -43,17 +51,30 @@ pub struct BlockRootSingleTxRollupPrivateInputs {
4351
///
4452
/// VkIndex: BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX
4553
pub fn execute(inputs: BlockRootSingleTxRollupPrivateInputs) -> BlockRollupPublicInputs {
46-
validate_previous_rollups([inputs.previous_rollup], ALLOWED_PREVIOUS_VK_INDICES);
47-
48-
// Non-first blocks build on the l1-to-l2 tree snapshot carried in the checkpoint constants.
49-
let previous_l1_to_l2 = inputs.previous_rollup.public_inputs.constants.l1_to_l2_tree_snapshot;
50-
BlockRollupPublicInputsComposer::new_from_single_rollup(inputs.previous_rollup.public_inputs)
54+
// Non-first blocks build on a witnessed start snapshot (pinned by block-merge continuity to the previous block's
55+
// end state) rather than the constants value, so this block can append its own bundle and still assert the tx
56+
// constants carry the post-bundle snapshot.
57+
let mut composer = BlockRollupPublicInputsComposer::new_from_single_rollup(
58+
inputs.previous_rollup.public_inputs,
59+
);
60+
let new_l1_to_l2 = composer
5161
.with_message_bundle(
5262
false,
53-
previous_l1_to_l2,
63+
inputs.previous_l1_to_l2,
5464
inputs.start_msg_sponge,
5565
inputs.message_bundle,
5666
inputs.l1_to_l2_message_frontier_hint,
5767
)
58-
.finish(inputs.new_archive_sibling_path)
68+
.get_new_l1_to_l2();
69+
70+
validate_previous_rollups([inputs.previous_rollup], ALLOWED_PREVIOUS_VK_INDICES);
71+
72+
// The tx constants pin the l1-to-l2 snapshot the AVM validates message reads against; asserting it equals this
73+
// block's post-bundle root lets this block's txs read the messages this block inserts (same-block consumption).
74+
validate_l1_to_l2_tree_snapshot_in_constants(
75+
inputs.previous_rollup.public_inputs.constants,
76+
new_l1_to_l2,
77+
);
78+
79+
composer.finish(inputs.new_archive_sibling_path)
5980
}

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/components/block_rollup_public_inputs_composer.nr

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,10 @@ impl BlockRollupPublicInputsComposer {
133133
/// Appends this block's L1-to-L2 message bundle to the tree and absorbs its real messages into the message sponge.
134134
///
135135
/// `previous_l1_to_l2` is the tree snapshot this block builds on: for the first block it is the pre-insertion
136-
/// snapshot (hinted, and later checked against the previous checkpoint), for subsequent blocks it is the snapshot
137-
/// carried in the constants. `start_msg_sponge` is the sponge inherited from the previous block (empty for the
138-
/// first block).
136+
/// snapshot (hinted, and later checked against the previous checkpoint), for subsequent blocks it is a witnessed
137+
/// snapshot pinned by block-merge continuity to the previous block's end state. Either way, the caller asserts the
138+
/// tx constants carry the resulting post-bundle snapshot. `start_msg_sponge` is the sponge inherited from the
139+
/// previous block (empty for the first block).
139140
///
140141
/// The tree append and the sponge absorb use different counts transitionally (see `L1ToL2MessageBundle`): the tree
141142
/// inserts `bundle.num_msgs` leaves (a full padded subtree for a message-bearing block), while the sponge absorbs
@@ -182,8 +183,8 @@ impl BlockRollupPublicInputsComposer {
182183
*self
183184
}
184185

185-
/// The l1-to-l2 tree snapshot after appending this block's bundle. First-block variants check this against the
186-
/// snapshot in the tx constants (consumed by the AVM to read new messages).
186+
/// The l1-to-l2 tree snapshot after appending this block's bundle. Tx-carrying variants (first and non-first)
187+
/// check this against the snapshot in the tx constants (consumed by the AVM to read new messages).
187188
pub fn get_new_l1_to_l2(self) -> AppendOnlyTreeSnapshot {
188189
self.new_l1_to_l2
189190
}

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/components/block_root_rollup_inputs_validator.nr

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ pub fn validate_previous_rollups<let NumPreviousRollups: u32, let NumVkIndices:
5050
/// This snapshot is used in the AVM to validate l1-to-l2 message read requests, allowing public functions to
5151
/// read new messages before they are inserted into the tree in the block root circuit.
5252
///
53-
/// This check is only required for the first block in a checkpoint, where the new l1-to-l2 messages are inserted
54-
/// into the tree. For subsequent blocks, the value in the constants is simply propagated to the start and end states of
55-
/// the public inputs. However, the start state of each block must match the end state of the previous block, which is
56-
/// validated in the block merge or checkpoint root circuit. This ensures that the snapshot used in subsequent blocks
57-
/// matches the `computed_new_l1_to_l2` from the first block.
53+
/// Every tx-carrying block-root variant (first and non-first) runs this check against its own post-bundle snapshot, so
54+
/// each block's txs can read the messages that block inserts (same-block consumption). Non-first variants take their
55+
/// start snapshot as a witness rather than reading it from the constants; that witness is pinned to the previous
56+
/// block's end state by the block merge or checkpoint root circuit, so it cannot be forged. (The tx-less variants —
57+
/// empty-first and msgs-only — carry no tx constants to read against and so skip this check.)
5858
pub fn validate_l1_to_l2_tree_snapshot_in_constants(
5959
constants: BlockConstantData,
6060
computed_new_l1_to_l2: AppendOnlyTreeSnapshot,

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/tests/failures_tests.nr

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,49 @@ fn incorrect_l1_to_l2_tree_next_available_leaf_index__first_single_tx_block() {
160160

161161
builder.execute_and_fail();
162162
}
163+
164+
// Non-first blocks also insert their own bundle and must pin the post-bundle snapshot in their tx constants, so a
165+
// non-first block whose constants snapshot differs from its computed post-bundle root must fail (same-block
166+
// consumption). Both child rollups are tweaked together so the mismatch surfaces here rather than as unequal
167+
// constants between the two tx rollups.
168+
#[test(should_fail_with = "l1_to_l2 tree snapshot in constants does not match the computed value")]
169+
fn incorrect_l1_to_l2_tree_root__non_first_block() {
170+
let mut builder = TestBuilder::default(false, false);
171+
172+
// Tweak the root of the l1_to_l2_tree_snapshot in the rollups' constants.
173+
builder.left_rollup.constants.l1_to_l2_tree_snapshot.root += 1;
174+
builder.right_rollup.constants.l1_to_l2_tree_snapshot.root += 1;
175+
176+
builder.execute_and_fail();
177+
}
178+
179+
#[test(should_fail_with = "l1_to_l2 tree snapshot in constants does not match the computed value")]
180+
fn incorrect_l1_to_l2_tree_next_available_leaf_index__non_first_block() {
181+
let mut builder = TestBuilder::default(false, false);
182+
183+
// Tweak the next_available_leaf_index of the l1_to_l2_tree_snapshot in the rollups' constants.
184+
builder.left_rollup.constants.l1_to_l2_tree_snapshot.next_available_leaf_index += 1;
185+
builder.right_rollup.constants.l1_to_l2_tree_snapshot.next_available_leaf_index += 1;
186+
187+
builder.execute_and_fail();
188+
}
189+
190+
#[test(should_fail_with = "l1_to_l2 tree snapshot in constants does not match the computed value")]
191+
fn incorrect_l1_to_l2_tree_root__non_first_single_tx_block() {
192+
let mut builder = TestBuilder::default(false, true);
193+
194+
// Tweak the root of the l1_to_l2_tree_snapshot in the rollup's constants.
195+
builder.left_rollup.constants.l1_to_l2_tree_snapshot.root += 1;
196+
197+
builder.execute_and_fail();
198+
}
199+
200+
#[test(should_fail_with = "l1_to_l2 tree snapshot in constants does not match the computed value")]
201+
fn incorrect_l1_to_l2_tree_next_available_leaf_index__non_first_single_tx_block() {
202+
let mut builder = TestBuilder::default(false, true);
203+
204+
// Tweak the next_available_leaf_index of the l1_to_l2_tree_snapshot in the rollup's constants.
205+
builder.left_rollup.constants.l1_to_l2_tree_snapshot.next_available_leaf_index += 1;
206+
207+
builder.execute_and_fail();
208+
}

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,10 @@ impl TestBuilder {
108108
let (previous_l1_to_l2, l1_to_l2_messages, l1_to_l2_message_frontier_hint, new_l1_to_l2) =
109109
fixture_builder.build_l1_to_l2_message_bundle(TEST_NUM_MSGS);
110110

111-
// The l1-to-l2 tree snapshot carried in the constants is the pre-append snapshot for non-first blocks (the tree
112-
// where this block picks up), and the post-append snapshot for first blocks (checked by the circuit against the
113-
// computed value).
114-
let constants_l1_to_l2 =
115-
if is_first_block_root { new_l1_to_l2 } else { previous_l1_to_l2 };
116-
left_rollup.constants.l1_to_l2_tree_snapshot = constants_l1_to_l2;
117-
right_rollup.constants.l1_to_l2_tree_snapshot = constants_l1_to_l2;
111+
// Every tx-carrying block-root variant (first and non-first) asserts the l1-to-l2 tree snapshot carried in the
112+
// tx constants equals the post-append snapshot, so its txs read the messages this block inserts.
113+
left_rollup.constants.l1_to_l2_tree_snapshot = new_l1_to_l2;
114+
right_rollup.constants.l1_to_l2_tree_snapshot = new_l1_to_l2;
118115

119116
// First blocks start the message sponge from empty; non-first blocks inherit a (non-empty) sponge.
120117
let start_msg_sponge = if is_first_block_root {
@@ -242,6 +239,7 @@ impl TestBuilder {
242239
BlockRootSingleTxRollupPrivateInputs {
243240
previous_rollup: previous_rollups[0],
244241
message_bundle,
242+
previous_l1_to_l2: self.previous_l1_to_l2,
245243
start_msg_sponge,
246244
l1_to_l2_message_frontier_hint,
247245
new_archive_sibling_path,
@@ -252,6 +250,7 @@ impl TestBuilder {
252250
BlockRootRollupPrivateInputs {
253251
previous_rollups,
254252
message_bundle,
253+
previous_l1_to_l2: self.previous_l1_to_l2,
255254
start_msg_sponge,
256255
l1_to_l2_message_frontier_hint,
257256
new_archive_sibling_path,

noir-projects/noir-protocol-circuits/crates/types/src/abis/block_constant_data.nr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ use std::meta::derive;
88
pub struct BlockConstantData {
99
// Archive tree snapshot at the very beginning of the entire block.
1010
pub last_archive: AppendOnlyTreeSnapshot,
11-
// L1-to-L2 message tree snapshot after this block lands.
12-
// For the first block in a checkpoint, this should be the snapshot after inserting the new l1-to-l2 message subtree
13-
// into the last l1-to-l2 tree snapshot in `last_archive`.
14-
// For subsequent blocks, this should match the snapshot of the previous block.
11+
// L1-to-L2 message tree snapshot after this block's own message bundle has been inserted. The AVM validates this
12+
// block's l1-to-l2 message read requests against it, so a tx in this block can read the messages this block
13+
// inserts (same-block consumption). Every block-root variant that carries txs asserts this equals its computed
14+
// post-bundle root, whether or not the block is the first in its checkpoint.
1515
pub l1_to_l2_tree_snapshot: AppendOnlyTreeSnapshot,
1616
pub vk_tree_root: Field,
1717
pub protocol_contracts_hash: Field,

yarn-project/noir-protocol-circuits-types/src/conversion/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,7 @@ export function mapBlockRootRollupPrivateInputsToNoir(
906906
mapProofDataToNoir(inputs.previousRollups[1], mapTxRollupPublicInputsToNoir),
907907
],
908908
message_bundle: mapL1ToL2MessageBundleToNoir(inputs.messageBundle),
909+
previous_l1_to_l2: mapAppendOnlyTreeSnapshotToNoir(inputs.previousL1ToL2),
909910
start_msg_sponge: mapL1ToL2MessageSpongeToNoir(inputs.startMsgSponge),
910911
l1_to_l2_message_frontier_hint: mapTuple(inputs.l1ToL2MessageFrontierHint, mapFieldToNoir),
911912
new_archive_sibling_path: mapTuple(inputs.newArchiveSiblingPath, mapFieldToNoir),
@@ -918,6 +919,7 @@ export function mapBlockRootSingleTxRollupPrivateInputsToNoir(
918919
return {
919920
previous_rollup: mapProofDataToNoir(inputs.previousRollup, mapTxRollupPublicInputsToNoir),
920921
message_bundle: mapL1ToL2MessageBundleToNoir(inputs.messageBundle),
922+
previous_l1_to_l2: mapAppendOnlyTreeSnapshotToNoir(inputs.previousL1ToL2),
921923
start_msg_sponge: mapL1ToL2MessageSpongeToNoir(inputs.startMsgSponge),
922924
l1_to_l2_message_frontier_hint: mapTuple(inputs.l1ToL2MessageFrontierHint, mapFieldToNoir),
923925
new_archive_sibling_path: mapTuple(inputs.newArchiveSiblingPath, mapFieldToNoir),

yarn-project/prover-client/src/orchestrator/block-proving-state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ export class BlockProvingState {
303303
inputs: new BlockRootSingleTxRollupPrivateInputs(
304304
leftRollup,
305305
messageBundle,
306+
this.lastL1ToL2MessageTreeSnapshot,
306307
startMsgSponge,
307308
frontierHint,
308309
this.lastArchiveSiblingPath,
@@ -314,6 +315,7 @@ export class BlockProvingState {
314315
inputs: new BlockRootRollupPrivateInputs(
315316
[leftRollup, rightRollup],
316317
messageBundle,
318+
this.lastL1ToL2MessageTreeSnapshot,
317319
startMsgSponge,
318320
frontierHint,
319321
this.lastArchiveSiblingPath,

0 commit comments

Comments
 (0)