Skip to content

Commit 8b88545

Browse files
committed
feat: add block_root_msgs_only_rollup circuit for no-tx blocks with messages (A-1375)
Adds the block_root_msgs_only_rollup variant (crate rollup-block-root-msgs-only, VK index BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX) — a non-first, transaction-less block that still inserts a non-empty L1-to-L2 message bundle, so a proposer can keep draining the Inbox when the tx pool is dry (AZIP-22 Fast Inbox, FI-05). The circuit asserts num_msgs > 0, inherits the sponge blob and message sponge from the previous block, sets is_first_block = false, appends the bundle and absorbs the message sponge, and threads the start sponge blob through unchanged (no tx effects). It is deliberately unable to be leftmost: the checkpoint root requires the leftmost rollup's is_first_block to be true. Wires the new VK into the allowed-VK lists of the block merge and both checkpoint-root variants, the vk tree, and the proving request / artifact / bb-prover registrations. Noir tests prove a checkpoint [first block, msgs-only block, normal block] and cover the num_msgs == 0, non-leftmost, and sponge-continuity failure cases. The orchestrator selection (block-proving-state picking this variant) and its integration test are deferred to FI-12/FI-15: driving a mid-checkpoint msgs-only block requires reworking the transitional per-block message distribution (today the first block carries the whole padded checkpoint bundle and non-first blocks are empty), which belongs with the sequencer that actually produces these blocks. Nothing produces the shape pre-flip.
1 parent baffb9d commit 8b88545

34 files changed

Lines changed: 726 additions & 24 deletions

File tree

noir-projects/noir-protocol-circuits/Nargo.template.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ members = [
4747
"crates/rollup-block-root-first-single-tx",
4848
"crates/rollup-block-root-first-single-tx-simulated",
4949
"crates/rollup-block-root-first-empty-tx",
50+
"crates/rollup-block-root-msgs-only",
5051
"crates/rollup-block-merge",
5152
"crates/rollup-checkpoint-root",
5253
"crates/rollup-checkpoint-root-simulated",

noir-projects/noir-protocol-circuits/crates/protocol-test-utils/src/fixtures/vk_tree.nr

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use crate::utils::pad_end;
22
use types::{
33
constants::{
4-
HIDING_KERNEL_TO_PUBLIC_VK_INDEX, MEGA_KERNEL_VK_LENGTH_IN_FIELDS, PARITY_BASE_VK_INDEX,
5-
PARITY_ROOT_VK_INDEX, PRIVATE_KERNEL_INIT_2_VK_INDEX, PRIVATE_KERNEL_INIT_3_VK_INDEX,
4+
BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, HIDING_KERNEL_TO_PUBLIC_VK_INDEX,
5+
MEGA_KERNEL_VK_LENGTH_IN_FIELDS, PARITY_BASE_VK_INDEX, PARITY_ROOT_VK_INDEX,
6+
PRIVATE_KERNEL_INIT_2_VK_INDEX, PRIVATE_KERNEL_INIT_3_VK_INDEX,
67
PRIVATE_KERNEL_INIT_4_VK_INDEX, PRIVATE_KERNEL_INIT_5_VK_INDEX,
78
PRIVATE_KERNEL_INNER_2_VK_INDEX, PRIVATE_KERNEL_INNER_3_VK_INDEX,
89
PRIVATE_KERNEL_INNER_4_VK_INDEX, PRIVATE_KERNEL_INNER_5_VK_INDEX,
@@ -69,6 +70,10 @@ pub global VK_MERKLE_TREE: MerkleTree<VK_TREE_WIDTH> = {
6970
leaves[PRIVATE_KERNEL_INNER_5_VK_INDEX] =
7071
generate_fake_chonk_vk_for_index(PRIVATE_KERNEL_INNER_5_VK_INDEX).hash;
7172

73+
// The message-only block root lives past the reset-variant range, so it's not covered by the rollup-honk loop below.
74+
leaves[BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX] =
75+
generate_fake_rollup_honk_vk_for_index(BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX).hash;
76+
7277
// Rollup Honk
7378
for i in HIDING_KERNEL_TO_PUBLIC_VK_INDEX + 1..PRIVATE_KERNEL_RESET_VK_INDEX {
7479
leaves[i] = generate_fake_rollup_honk_vk_for_index(i).hash;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "rollup_block_root_msgs_only"
3+
type = "bin"
4+
authors = [""]
5+
compiler_version = ">=0.18.0"
6+
7+
[dependencies]
8+
rollup_lib = { path = "../rollup-lib" }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use rollup_lib::block_root::{
2+
block_root_msgs_only_rollup, BlockRollupPublicInputs, BlockRootMsgsOnlyRollupPrivateInputs,
3+
};
4+
5+
fn main(inputs: BlockRootMsgsOnlyRollupPrivateInputs) -> pub BlockRollupPublicInputs {
6+
block_root_msgs_only_rollup::execute(inputs)
7+
}

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,23 @@ use crate::{
88
use types::{
99
constants::{
1010
BLOCK_MERGE_ROLLUP_VK_INDEX, BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX,
11-
BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_ROLLUP_VK_INDEX,
12-
BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX,
11+
BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX,
12+
BLOCK_ROOT_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX,
13+
BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX,
1314
},
1415
proof::proof_data::RollupHonkProofData,
1516
};
1617

1718
// Note: see `rollup_structure_tests.nr` for valid combinations of left and right vks.
18-
global ALLOWED_RIGHT_ROLLUP_VK_INDICES: [u32; 3] =
19-
[BLOCK_ROOT_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, BLOCK_MERGE_ROLLUP_VK_INDEX];
19+
// The message-only block root is a non-first variant, so it appears alongside the other non-first block roots.
20+
global ALLOWED_RIGHT_ROLLUP_VK_INDICES: [u32; 4] = [
21+
BLOCK_ROOT_ROLLUP_VK_INDEX,
22+
BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX,
23+
BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX,
24+
BLOCK_MERGE_ROLLUP_VK_INDEX,
25+
];
2026

21-
global ALLOWED_LEFT_ROLLUP_VK_INDICES: [u32; 6] = ALLOWED_RIGHT_ROLLUP_VK_INDICES.concat([
27+
global ALLOWED_LEFT_ROLLUP_VK_INDICES: [u32; 7] = ALLOWED_RIGHT_ROLLUP_VK_INDICES.concat([
2228
// The first block roots may only appear as the left child of the block merge rollup.
2329
// However, this doesn't prevent having 2 first block roots in the same checkpoint. We rely on the checks in
2430
// `validate_consecutive_block_rollups` and `checkpoint_root_inputs_validator` to enforce this.

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod child_proof_vk_tests;
22
mod consecutive_block_rollups_tests;
3+
mod msgs_only_tests;
34
mod rollup_structure_tests;
45

56
use crate::{
@@ -13,8 +14,9 @@ use crate::{
1314
use types::{
1415
constants::{
1516
BLOCK_MERGE_ROLLUP_VK_INDEX, BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX,
16-
BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_ROLLUP_VK_INDEX,
17-
BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX,
17+
BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX,
18+
BLOCK_ROOT_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX,
19+
BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX,
1820
},
1921
hash::accumulate_sha256,
2022
};
@@ -96,7 +98,8 @@ impl TestBuilder {
9698
| (vk_index == BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX)
9799
| (vk_index == BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX)
98100
| (vk_index == BLOCK_ROOT_ROLLUP_VK_INDEX)
99-
| (vk_index == BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX),
101+
| (vk_index == BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX)
102+
| (vk_index == BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX),
100103
);
101104
}
102105
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use crate::{
2+
abis::BlockRollupPublicInputs,
3+
block_merge::block_merge_rollup::{self, BlockMergeRollupPrivateInputs},
4+
tests::RollupFixtureBuilder,
5+
};
6+
use types::constants::{
7+
BLOCK_MERGE_ROLLUP_VK_INDEX, BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX,
8+
BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, BLOCK_ROOT_ROLLUP_VK_INDEX,
9+
};
10+
11+
// Merges two consecutive single-block rollups into one.
12+
fn merge(
13+
left: BlockRollupPublicInputs,
14+
left_vk_index: u32,
15+
right: BlockRollupPublicInputs,
16+
right_vk_index: u32,
17+
) -> BlockRollupPublicInputs {
18+
block_merge_rollup::execute(BlockMergeRollupPrivateInputs {
19+
previous_rollups: [
20+
RollupFixtureBuilder::make_proof_data(left, left_vk_index),
21+
RollupFixtureBuilder::make_proof_data(right, right_vk_index),
22+
],
23+
})
24+
}
25+
26+
// The roadmap's done-when: a checkpoint made of [first block, message-only block, normal block] proves. The
27+
// message-only block sits in the middle, so the merge circuits must accept it as a non-first child and thread the state,
28+
// archive and sponges through it.
29+
#[test]
30+
fn checkpoint_with_mid_checkpoint_msgs_only_block() {
31+
let fixture_builder = RollupFixtureBuilder::new();
32+
let start_block_number = fixture_builder.start_block_number;
33+
34+
// The consecutive single-block fixtures chain naturally by block number (archives, states and sponges line up).
35+
let first = fixture_builder.get_block_rollup_public_inputs(start_block_number);
36+
let msgs_only = fixture_builder.get_block_rollup_public_inputs(start_block_number + 1);
37+
let normal = fixture_builder.get_block_rollup_public_inputs(start_block_number + 2);
38+
39+
assert(first.is_first_block);
40+
assert(!msgs_only.is_first_block);
41+
assert(!normal.is_first_block);
42+
43+
// ((first, msgs_only), normal)
44+
let left = merge(
45+
first,
46+
BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX,
47+
msgs_only,
48+
BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX,
49+
);
50+
let merged = merge(left, BLOCK_MERGE_ROLLUP_VK_INDEX, normal, BLOCK_ROOT_ROLLUP_VK_INDEX);
51+
52+
assert_eq(merged.num_blocks(), 3);
53+
// `is_first_block` propagates from the leftmost (first) block only.
54+
assert(merged.is_first_block);
55+
assert_eq(merged.previous_archive, first.previous_archive);
56+
assert_eq(merged.new_archive, normal.new_archive);
57+
assert_eq(merged.start_msg_sponge, first.start_msg_sponge);
58+
assert_eq(merged.end_msg_sponge, normal.end_msg_sponge);
59+
}
60+
61+
// A message-only block is a valid right (non-first) child of a block merge.
62+
#[test]
63+
fn msgs_only_block_accepted_as_right_child() {
64+
let fixture_builder = RollupFixtureBuilder::new();
65+
let start_block_number = fixture_builder.start_block_number;
66+
67+
let first = fixture_builder.get_block_rollup_public_inputs(start_block_number);
68+
let msgs_only = fixture_builder.get_block_rollup_public_inputs(start_block_number + 1);
69+
70+
let merged = merge(
71+
first,
72+
BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX,
73+
msgs_only,
74+
BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX,
75+
);
76+
77+
assert_eq(merged.num_blocks(), 2);
78+
assert(merged.is_first_block);
79+
}
80+
81+
// The message-only block's `start_msg_sponge` is a free input; the merge continuity check must catch it if it does not
82+
// follow on from the previous block's `end_msg_sponge`.
83+
#[test(should_fail_with = "Mismatched message sponge: expected right.start_msg_sponge to match left.end_msg_sponge")]
84+
fn msgs_only_block_bad_start_msg_sponge_fails() {
85+
let fixture_builder = RollupFixtureBuilder::new();
86+
let start_block_number = fixture_builder.start_block_number;
87+
88+
let first = fixture_builder.get_block_rollup_public_inputs(start_block_number);
89+
let mut msgs_only = fixture_builder.get_block_rollup_public_inputs(start_block_number + 1);
90+
91+
// Break the sponge continuity with the previous (first) block.
92+
msgs_only.start_msg_sponge.num_absorbed += 1;
93+
94+
let _ = merge(
95+
first,
96+
BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX,
97+
msgs_only,
98+
BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX,
99+
);
100+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use crate::{
2+
abis::{BlockRollupPublicInputs, L1ToL2MessageSponge},
3+
block_root::components::BlockRollupPublicInputsComposer,
4+
};
5+
use types::{
6+
abis::{
7+
append_only_tree_snapshot::AppendOnlyTreeSnapshot,
8+
checkpoint_constant_data::CheckpointConstantData, state_reference::StateReference,
9+
},
10+
blob_data::SpongeBlob,
11+
constants::{ARCHIVE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, MAX_L1_TO_L2_MSGS_PER_BLOCK},
12+
};
13+
14+
pub struct BlockRootMsgsOnlyRollupPrivateInputs {
15+
pub(crate) previous_archive: AppendOnlyTreeSnapshot,
16+
pub(crate) previous_state: StateReference,
17+
// The previous block is in the same checkpoint as this block, but this block has no txs, so there are no tx
18+
// constants to carry them. The checkpoint constants are given as a free value here and pinned by the checkpoint
19+
// root's previous-header check via the merge continuity of the states below.
20+
pub(crate) constants: CheckpointConstantData,
21+
// The timestamp of this block. It's given as a free value here, but will be checked against the previous block's
22+
// timestamp in the block merge or checkpoint root.
23+
pub(crate) timestamp: u64,
24+
// Sponge blob inherited from the previous block. Checked against the previous block's `end_sponge_blob` in the
25+
// block merge or checkpoint root circuit.
26+
pub(crate) start_sponge_blob: SpongeBlob,
27+
// Message sponge inherited from the previous block. Checked against the previous block's `end_msg_sponge` in the
28+
// block merge or checkpoint root circuit.
29+
pub(crate) start_msg_sponge: L1ToL2MessageSponge,
30+
31+
// L1-to-L2 messages inserted by this block, padded with zeros beyond `num_msgs`.
32+
pub(crate) l1_to_l2_messages: [Field; MAX_L1_TO_L2_MSGS_PER_BLOCK],
33+
// Number of real (non-padding) leaves in `l1_to_l2_messages`.
34+
pub(crate) num_msgs: u32,
35+
// Frontier hint for appending the bundle to the l1-to-l2 message tree (validated against `previous_state`).
36+
pub(crate) l1_to_l2_message_frontier_hint: [Field; L1_TO_L2_MSG_TREE_HEIGHT],
37+
// Hint for inserting the new block hash to the last archive.
38+
pub(crate) new_archive_sibling_path: [Field; ARCHIVE_HEIGHT],
39+
}
40+
41+
/// The Block Root Msgs Only Rollup circuit creates a non-first, transaction-less block that inserts an L1-to-L2 message
42+
/// bundle. It lets a proposer keep draining the Inbox into a checkpoint when the tx pool is empty.
43+
///
44+
/// Unlike the first-empty variant, this block sits in the middle of a checkpoint: it inherits the sponge blob and
45+
/// message sponge from the previous block (rather than starting them from empty) and sets `is_first_block = false`. It
46+
/// is deliberately unable to be the leftmost block of a checkpoint: the checkpoint root asserts the leftmost rollup's
47+
/// `is_first_block` is true, so a checkpoint made only of this variant cannot prove.
48+
///
49+
/// This circuit:
50+
/// - Requires the bundle to be non-empty (`num_msgs > 0`); an empty non-first block carries no reason to exist and
51+
/// would otherwise be a provable filler block
52+
/// - Appends the block's L1-to-L2 message bundle to the L1-to-L2 message tree and absorbs it into the message sponge
53+
/// - Threads the (non-empty) start sponge blob through unchanged, since there are no tx effects
54+
/// - Computes the block header hash and inserts it into the archive tree
55+
///
56+
/// The output feeds into a Block Merge circuit if more blocks need to be combined, or directly into a Checkpoint Root
57+
/// circuit.
58+
///
59+
/// VkIndex: BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX
60+
pub fn execute(inputs: BlockRootMsgsOnlyRollupPrivateInputs) -> BlockRollupPublicInputs {
61+
// A message-only block exists solely to insert L1-to-L2 messages, so it must carry at least one. This prevents a
62+
// provable, fully-empty non-first filler block.
63+
assert(inputs.num_msgs != 0, "A message-only block must insert at least one L1-to-L2 message");
64+
65+
BlockRollupPublicInputsComposer::new_from_no_rollups_with_start_sponge_blob(
66+
inputs.previous_archive,
67+
inputs.previous_state,
68+
inputs.constants,
69+
inputs.timestamp,
70+
inputs.start_sponge_blob,
71+
)
72+
.with_message_bundle(
73+
false,
74+
inputs.previous_state.l1_to_l2_message_tree,
75+
inputs.start_msg_sponge,
76+
inputs.l1_to_l2_messages,
77+
inputs.num_msgs,
78+
inputs.l1_to_l2_message_frontier_hint,
79+
)
80+
.finish(inputs.new_archive_sibling_path)
81+
}

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,36 @@ impl BlockRollupPublicInputsComposer {
4242
constants: CheckpointConstantData,
4343
timestamp: u64,
4444
) -> Self {
45-
let empty_sponge_blob = SpongeBlob::init();
45+
// The first block of a checkpoint starts from an empty sponge blob.
46+
Self::new_from_no_rollups_with_start_sponge_blob(
47+
previous_archive,
48+
previous_state,
49+
constants,
50+
timestamp,
51+
SpongeBlob::init(),
52+
)
53+
}
4654

55+
/// Same as `new_from_no_rollups` but for a non-first block, which inherits a (non-empty) sponge blob from the
56+
/// previous block instead of starting from empty. The `start_sponge_blob` is threaded unchanged as the end sponge
57+
/// blob (before this block's end data is absorbed in `finish`), since a block with no txs adds no tx effects. It is
58+
/// pinned against the previous block's `end_sponge_blob` at the block merge or checkpoint root.
59+
pub fn new_from_no_rollups_with_start_sponge_blob(
60+
previous_archive: AppendOnlyTreeSnapshot,
61+
previous_state: StateReference,
62+
constants: CheckpointConstantData,
63+
timestamp: u64,
64+
start_sponge_blob: SpongeBlob,
65+
) -> Self {
4766
Self {
4867
constants,
4968
previous_archive,
5069
previous_l1_to_l2: previous_state.l1_to_l2_message_tree,
5170
// The state remains the same since there are no tx effects in this block.
5271
start_tree_snapshots: previous_state.partial,
5372
end_tree_snapshots: previous_state.partial,
54-
// Since it's the first block in a checkpoint, the start sponge blob is empty.
55-
start_sponge_blob: empty_sponge_blob,
56-
end_sponge_blob: empty_sponge_blob,
73+
start_sponge_blob,
74+
end_sponge_blob: start_sponge_blob,
5775
timestamp,
5876
// The followings are 0 since there are no tx effects in this block.
5977
out_hash: 0,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ pub mod block_root_single_tx_first_rollup;
44
pub mod block_root_empty_tx_first_rollup;
55
pub mod block_root_rollup;
66
pub mod block_root_single_tx_rollup;
7+
pub mod block_root_msgs_only_rollup;
78
mod tests;
89

910
// Re-exports
1011
pub use crate::abis::BlockRollupPublicInputs;
1112
pub use block_root_empty_tx_first_rollup::BlockRootEmptyTxFirstRollupPrivateInputs;
1213
pub use block_root_first_rollup::BlockRootFirstRollupPrivateInputs;
14+
pub use block_root_msgs_only_rollup::BlockRootMsgsOnlyRollupPrivateInputs;
1315
pub use block_root_rollup::BlockRootRollupPrivateInputs;
1416
pub use block_root_single_tx_first_rollup::BlockRootSingleTxFirstRollupPrivateInputs;
1517
pub use block_root_single_tx_rollup::BlockRootSingleTxRollupPrivateInputs;

0 commit comments

Comments
 (0)