From db26f5dc04a506c23c55de29ca8cbc4306b030b0 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 8 Jul 2026 14:35:50 -0300 Subject: [PATCH 1/5] feat: add block_root_msgs_only_rollup circuit for no-tx blocks with messages (A-1375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Nargo.template.toml | 1 + .../src/fixtures/vk_tree.nr | 9 +- .../rollup-block-root-msgs-only/Nargo.toml | 8 + .../rollup-block-root-msgs-only/src/main.nr | 7 + .../src/block_merge/block_merge_rollup.nr | 16 +- .../rollup-lib/src/block_merge/tests/mod.nr | 9 +- .../src/block_merge/tests/msgs_only_tests.nr | 100 ++++++++++ .../block_root/block_root_msgs_only_rollup.nr | 81 ++++++++ .../block_rollup_public_inputs_composer.nr | 26 ++- .../crates/rollup-lib/src/block_root/mod.nr | 2 + .../rollup-lib/src/block_root/tests/mod.nr | 1 + .../src/block_root/tests/msgs_only_tests.nr | 184 ++++++++++++++++++ .../checkpoint_root/checkpoint_root_rollup.nr | 8 +- .../checkpoint_root_single_block_rollup.nr | 9 +- .../src/checkpoint_root/tests/mod.nr | 10 +- .../checkpoint_root/tests/msgs_only_tests.nr | 30 +++ .../crates/types/src/constants.nr | 3 + .../bb-prover/src/prover/server/bb_prover.ts | 12 ++ .../bb-prover/src/test/delay_values.ts | 2 + .../bb-prover/src/test/test_circuit_prover.ts | 18 ++ .../src/artifacts/server.ts | 5 + .../src/artifacts/types.ts | 3 + .../src/artifacts/vks/server.ts | 4 + .../src/conversion/server.ts | 19 ++ .../src/execution/server.ts | 26 +++ .../src/scripts/generate_ts_from_abi.ts | 1 + .../proving_broker/broker_prover_facade.ts | 15 ++ .../prover-client/src/test/mock_prover.ts | 15 ++ .../stdlib/src/interfaces/proving-job.ts | 12 ++ .../src/interfaces/server_circuit_prover.ts | 7 + .../stdlib/src/proofs/proving_request_type.ts | 1 + .../block_root_rollup_private_inputs.ts | 104 ++++++++++ yarn-project/stdlib/src/stats/stats.ts | 1 + 33 files changed, 725 insertions(+), 24 deletions(-) create mode 100644 noir-projects/noir-protocol-circuits/crates/rollup-block-root-msgs-only/Nargo.toml create mode 100644 noir-projects/noir-protocol-circuits/crates/rollup-block-root-msgs-only/src/main.nr create mode 100644 noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/tests/msgs_only_tests.nr create mode 100644 noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/block_root_msgs_only_rollup.nr create mode 100644 noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/tests/msgs_only_tests.nr create mode 100644 noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/tests/msgs_only_tests.nr diff --git a/noir-projects/noir-protocol-circuits/Nargo.template.toml b/noir-projects/noir-protocol-circuits/Nargo.template.toml index 17a33b4a805f..554193b55744 100644 --- a/noir-projects/noir-protocol-circuits/Nargo.template.toml +++ b/noir-projects/noir-protocol-circuits/Nargo.template.toml @@ -47,6 +47,7 @@ members = [ "crates/rollup-block-root-first-single-tx", "crates/rollup-block-root-first-single-tx-simulated", "crates/rollup-block-root-first-empty-tx", + "crates/rollup-block-root-msgs-only", "crates/rollup-block-merge", "crates/rollup-checkpoint-root", "crates/rollup-checkpoint-root-simulated", diff --git a/noir-projects/noir-protocol-circuits/crates/protocol-test-utils/src/fixtures/vk_tree.nr b/noir-projects/noir-protocol-circuits/crates/protocol-test-utils/src/fixtures/vk_tree.nr index 7c511b3d8ae4..fa3a29c542e9 100644 --- a/noir-projects/noir-protocol-circuits/crates/protocol-test-utils/src/fixtures/vk_tree.nr +++ b/noir-projects/noir-protocol-circuits/crates/protocol-test-utils/src/fixtures/vk_tree.nr @@ -1,8 +1,9 @@ use crate::utils::pad_end; use types::{ constants::{ - HIDING_KERNEL_TO_PUBLIC_VK_INDEX, MEGA_KERNEL_VK_LENGTH_IN_FIELDS, PARITY_BASE_VK_INDEX, - PARITY_ROOT_VK_INDEX, PRIVATE_KERNEL_INIT_2_VK_INDEX, PRIVATE_KERNEL_INIT_3_VK_INDEX, + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, HIDING_KERNEL_TO_PUBLIC_VK_INDEX, + MEGA_KERNEL_VK_LENGTH_IN_FIELDS, PARITY_BASE_VK_INDEX, PARITY_ROOT_VK_INDEX, + PRIVATE_KERNEL_INIT_2_VK_INDEX, PRIVATE_KERNEL_INIT_3_VK_INDEX, PRIVATE_KERNEL_INIT_4_VK_INDEX, PRIVATE_KERNEL_INIT_5_VK_INDEX, PRIVATE_KERNEL_INNER_2_VK_INDEX, PRIVATE_KERNEL_INNER_3_VK_INDEX, PRIVATE_KERNEL_INNER_4_VK_INDEX, PRIVATE_KERNEL_INNER_5_VK_INDEX, @@ -69,6 +70,10 @@ pub global VK_MERKLE_TREE: MerkleTree = { leaves[PRIVATE_KERNEL_INNER_5_VK_INDEX] = generate_fake_chonk_vk_for_index(PRIVATE_KERNEL_INNER_5_VK_INDEX).hash; + // The message-only block root lives past the reset-variant range, so it's not covered by the rollup-honk loop below. + leaves[BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX] = + generate_fake_rollup_honk_vk_for_index(BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX).hash; + // Rollup Honk for i in HIDING_KERNEL_TO_PUBLIC_VK_INDEX + 1..PRIVATE_KERNEL_RESET_VK_INDEX { leaves[i] = generate_fake_rollup_honk_vk_for_index(i).hash; diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-block-root-msgs-only/Nargo.toml b/noir-projects/noir-protocol-circuits/crates/rollup-block-root-msgs-only/Nargo.toml new file mode 100644 index 000000000000..01ba21dedeb3 --- /dev/null +++ b/noir-projects/noir-protocol-circuits/crates/rollup-block-root-msgs-only/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rollup_block_root_msgs_only" +type = "bin" +authors = [""] +compiler_version = ">=0.18.0" + +[dependencies] +rollup_lib = { path = "../rollup-lib" } diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-block-root-msgs-only/src/main.nr b/noir-projects/noir-protocol-circuits/crates/rollup-block-root-msgs-only/src/main.nr new file mode 100644 index 000000000000..6b2a522ff1e7 --- /dev/null +++ b/noir-projects/noir-protocol-circuits/crates/rollup-block-root-msgs-only/src/main.nr @@ -0,0 +1,7 @@ +use rollup_lib::block_root::{ + block_root_msgs_only_rollup, BlockRollupPublicInputs, BlockRootMsgsOnlyRollupPrivateInputs, +}; + +fn main(inputs: BlockRootMsgsOnlyRollupPrivateInputs) -> pub BlockRollupPublicInputs { + block_root_msgs_only_rollup::execute(inputs) +} diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/block_merge_rollup.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/block_merge_rollup.nr index 53bff1d8de69..83563c6e8825 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/block_merge_rollup.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/block_merge_rollup.nr @@ -8,17 +8,23 @@ use crate::{ use types::{ constants::{ BLOCK_MERGE_ROLLUP_VK_INDEX, BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX, - BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_ROLLUP_VK_INDEX, - BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, + BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, + BLOCK_ROOT_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, + BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, }, proof::proof_data::RollupHonkProofData, }; // Note: see `rollup_structure_tests.nr` for valid combinations of left and right vks. -global ALLOWED_RIGHT_ROLLUP_VK_INDICES: [u32; 3] = - [BLOCK_ROOT_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, BLOCK_MERGE_ROLLUP_VK_INDEX]; +// The message-only block root is a non-first variant, so it appears alongside the other non-first block roots. +global ALLOWED_RIGHT_ROLLUP_VK_INDICES: [u32; 4] = [ + BLOCK_ROOT_ROLLUP_VK_INDEX, + BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, + BLOCK_MERGE_ROLLUP_VK_INDEX, +]; -global ALLOWED_LEFT_ROLLUP_VK_INDICES: [u32; 6] = ALLOWED_RIGHT_ROLLUP_VK_INDICES.concat([ +global ALLOWED_LEFT_ROLLUP_VK_INDICES: [u32; 7] = ALLOWED_RIGHT_ROLLUP_VK_INDICES.concat([ // The first block roots may only appear as the left child of the block merge rollup. // However, this doesn't prevent having 2 first block roots in the same checkpoint. We rely on the checks in // `validate_consecutive_block_rollups` and `checkpoint_root_inputs_validator` to enforce this. diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/tests/mod.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/tests/mod.nr index da19b3c21caf..d1e88b148729 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/tests/mod.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/tests/mod.nr @@ -1,5 +1,6 @@ mod child_proof_vk_tests; mod consecutive_block_rollups_tests; +mod msgs_only_tests; mod rollup_structure_tests; use crate::{ @@ -13,8 +14,9 @@ use crate::{ use types::{ constants::{ BLOCK_MERGE_ROLLUP_VK_INDEX, BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX, - BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_ROLLUP_VK_INDEX, - BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, + BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, + BLOCK_ROOT_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, + BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, }, hash::accumulate_sha256, }; @@ -96,7 +98,8 @@ impl TestBuilder { | (vk_index == BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX) | (vk_index == BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX) | (vk_index == BLOCK_ROOT_ROLLUP_VK_INDEX) - | (vk_index == BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX), + | (vk_index == BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX) + | (vk_index == BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX), ); } } diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/tests/msgs_only_tests.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/tests/msgs_only_tests.nr new file mode 100644 index 000000000000..3de3212dd36d --- /dev/null +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_merge/tests/msgs_only_tests.nr @@ -0,0 +1,100 @@ +use crate::{ + abis::BlockRollupPublicInputs, + block_merge::block_merge_rollup::{self, BlockMergeRollupPrivateInputs}, + tests::RollupFixtureBuilder, +}; +use types::constants::{ + BLOCK_MERGE_ROLLUP_VK_INDEX, BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, BLOCK_ROOT_ROLLUP_VK_INDEX, +}; + +// Merges two consecutive single-block rollups into one. +fn merge( + left: BlockRollupPublicInputs, + left_vk_index: u32, + right: BlockRollupPublicInputs, + right_vk_index: u32, +) -> BlockRollupPublicInputs { + block_merge_rollup::execute(BlockMergeRollupPrivateInputs { + previous_rollups: [ + RollupFixtureBuilder::make_proof_data(left, left_vk_index), + RollupFixtureBuilder::make_proof_data(right, right_vk_index), + ], + }) +} + +// The roadmap's done-when: a checkpoint made of [first block, message-only block, normal block] proves. The +// message-only block sits in the middle, so the merge circuits must accept it as a non-first child and thread the state, +// archive and sponges through it. +#[test] +fn checkpoint_with_mid_checkpoint_msgs_only_block() { + let fixture_builder = RollupFixtureBuilder::new(); + let start_block_number = fixture_builder.start_block_number; + + // The consecutive single-block fixtures chain naturally by block number (archives, states and sponges line up). + let first = fixture_builder.get_block_rollup_public_inputs(start_block_number); + let msgs_only = fixture_builder.get_block_rollup_public_inputs(start_block_number + 1); + let normal = fixture_builder.get_block_rollup_public_inputs(start_block_number + 2); + + assert(first.is_first_block); + assert(!msgs_only.is_first_block); + assert(!normal.is_first_block); + + // ((first, msgs_only), normal) + let left = merge( + first, + BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, + msgs_only, + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, + ); + let merged = merge(left, BLOCK_MERGE_ROLLUP_VK_INDEX, normal, BLOCK_ROOT_ROLLUP_VK_INDEX); + + assert_eq(merged.num_blocks(), 3); + // `is_first_block` propagates from the leftmost (first) block only. + assert(merged.is_first_block); + assert_eq(merged.previous_archive, first.previous_archive); + assert_eq(merged.new_archive, normal.new_archive); + assert_eq(merged.start_msg_sponge, first.start_msg_sponge); + assert_eq(merged.end_msg_sponge, normal.end_msg_sponge); +} + +// A message-only block is a valid right (non-first) child of a block merge. +#[test] +fn msgs_only_block_accepted_as_right_child() { + let fixture_builder = RollupFixtureBuilder::new(); + let start_block_number = fixture_builder.start_block_number; + + let first = fixture_builder.get_block_rollup_public_inputs(start_block_number); + let msgs_only = fixture_builder.get_block_rollup_public_inputs(start_block_number + 1); + + let merged = merge( + first, + BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, + msgs_only, + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, + ); + + assert_eq(merged.num_blocks(), 2); + assert(merged.is_first_block); +} + +// The message-only block's `start_msg_sponge` is a free input; the merge continuity check must catch it if it does not +// follow on from the previous block's `end_msg_sponge`. +#[test(should_fail_with = "Mismatched message sponge: expected right.start_msg_sponge to match left.end_msg_sponge")] +fn msgs_only_block_bad_start_msg_sponge_fails() { + let fixture_builder = RollupFixtureBuilder::new(); + let start_block_number = fixture_builder.start_block_number; + + let first = fixture_builder.get_block_rollup_public_inputs(start_block_number); + let mut msgs_only = fixture_builder.get_block_rollup_public_inputs(start_block_number + 1); + + // Break the sponge continuity with the previous (first) block. + msgs_only.start_msg_sponge.num_absorbed += 1; + + let _ = merge( + first, + BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, + msgs_only, + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, + ); +} diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/block_root_msgs_only_rollup.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/block_root_msgs_only_rollup.nr new file mode 100644 index 000000000000..9409d78877f0 --- /dev/null +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/block_root_msgs_only_rollup.nr @@ -0,0 +1,81 @@ +use crate::{ + abis::{BlockRollupPublicInputs, L1ToL2MessageSponge}, + block_root::components::BlockRollupPublicInputsComposer, +}; +use types::{ + abis::{ + append_only_tree_snapshot::AppendOnlyTreeSnapshot, + checkpoint_constant_data::CheckpointConstantData, state_reference::StateReference, + }, + blob_data::SpongeBlob, + constants::{ARCHIVE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, MAX_L1_TO_L2_MSGS_PER_BLOCK}, +}; + +pub struct BlockRootMsgsOnlyRollupPrivateInputs { + pub(crate) previous_archive: AppendOnlyTreeSnapshot, + pub(crate) previous_state: StateReference, + // The previous block is in the same checkpoint as this block, but this block has no txs, so there are no tx + // constants to carry them. The checkpoint constants are given as a free value here and pinned by the checkpoint + // root's previous-header check via the merge continuity of the states below. + pub(crate) constants: CheckpointConstantData, + // The timestamp of this block. It's given as a free value here, but will be checked against the previous block's + // timestamp in the block merge or checkpoint root. + pub(crate) timestamp: u64, + // Sponge blob inherited from the previous block. Checked against the previous block's `end_sponge_blob` in the + // block merge or checkpoint root circuit. + pub(crate) start_sponge_blob: SpongeBlob, + // Message sponge inherited from the previous block. Checked against the previous block's `end_msg_sponge` in the + // block merge or checkpoint root circuit. + pub(crate) start_msg_sponge: L1ToL2MessageSponge, + + // L1-to-L2 messages inserted by this block, padded with zeros beyond `num_msgs`. + pub(crate) l1_to_l2_messages: [Field; MAX_L1_TO_L2_MSGS_PER_BLOCK], + // Number of real (non-padding) leaves in `l1_to_l2_messages`. + pub(crate) num_msgs: u32, + // Frontier hint for appending the bundle to the l1-to-l2 message tree (validated against `previous_state`). + pub(crate) l1_to_l2_message_frontier_hint: [Field; L1_TO_L2_MSG_TREE_HEIGHT], + // Hint for inserting the new block hash to the last archive. + pub(crate) new_archive_sibling_path: [Field; ARCHIVE_HEIGHT], +} + +/// The Block Root Msgs Only Rollup circuit creates a non-first, transaction-less block that inserts an L1-to-L2 message +/// bundle. It lets a proposer keep draining the Inbox into a checkpoint when the tx pool is empty. +/// +/// Unlike the first-empty variant, this block sits in the middle of a checkpoint: it inherits the sponge blob and +/// message sponge from the previous block (rather than starting them from empty) and sets `is_first_block = false`. It +/// is deliberately unable to be the leftmost block of a checkpoint: the checkpoint root asserts the leftmost rollup's +/// `is_first_block` is true, so a checkpoint made only of this variant cannot prove. +/// +/// This circuit: +/// - Requires the bundle to be non-empty (`num_msgs > 0`); an empty non-first block carries no reason to exist and +/// would otherwise be a provable filler block +/// - Appends the block's L1-to-L2 message bundle to the L1-to-L2 message tree and absorbs it into the message sponge +/// - Threads the (non-empty) start sponge blob through unchanged, since there are no tx effects +/// - Computes the block header hash and inserts it into the archive tree +/// +/// The output feeds into a Block Merge circuit if more blocks need to be combined, or directly into a Checkpoint Root +/// circuit. +/// +/// VkIndex: BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX +pub fn execute(inputs: BlockRootMsgsOnlyRollupPrivateInputs) -> BlockRollupPublicInputs { + // A message-only block exists solely to insert L1-to-L2 messages, so it must carry at least one. This prevents a + // provable, fully-empty non-first filler block. + assert(inputs.num_msgs != 0, "A message-only block must insert at least one L1-to-L2 message"); + + BlockRollupPublicInputsComposer::new_from_no_rollups_with_start_sponge_blob( + inputs.previous_archive, + inputs.previous_state, + inputs.constants, + inputs.timestamp, + inputs.start_sponge_blob, + ) + .with_message_bundle( + false, + inputs.previous_state.l1_to_l2_message_tree, + inputs.start_msg_sponge, + inputs.l1_to_l2_messages, + inputs.num_msgs, + inputs.l1_to_l2_message_frontier_hint, + ) + .finish(inputs.new_archive_sibling_path) +} diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/components/block_rollup_public_inputs_composer.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/components/block_rollup_public_inputs_composer.nr index 522a791296e6..79f986f6f3a0 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/components/block_rollup_public_inputs_composer.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/components/block_rollup_public_inputs_composer.nr @@ -46,8 +46,27 @@ impl BlockRollupPublicInputsComposer { constants: CheckpointConstantData, timestamp: u64, ) -> Self { - let empty_sponge_blob = SpongeBlob::init(); + // The first block of a checkpoint starts from an empty sponge blob. + Self::new_from_no_rollups_with_start_sponge_blob( + previous_archive, + previous_state, + constants, + timestamp, + SpongeBlob::init(), + ) + } + /// Same as `new_from_no_rollups` but for a non-first block, which inherits a (non-empty) sponge blob from the + /// previous block instead of starting from empty. The `start_sponge_blob` is threaded unchanged as the end sponge + /// blob (before this block's end data is absorbed in `finish`), since a block with no txs adds no tx effects. It is + /// pinned against the previous block's `end_sponge_blob` at the block merge or checkpoint root. + pub fn new_from_no_rollups_with_start_sponge_blob( + previous_archive: AppendOnlyTreeSnapshot, + previous_state: StateReference, + constants: CheckpointConstantData, + timestamp: u64, + start_sponge_blob: SpongeBlob, + ) -> Self { Self { constants, previous_archive, @@ -55,9 +74,8 @@ impl BlockRollupPublicInputsComposer { // The state remains the same since there are no tx effects in this block. start_tree_snapshots: previous_state.partial, end_tree_snapshots: previous_state.partial, - // Since it's the first block in a checkpoint, the start sponge blob is empty. - start_sponge_blob: empty_sponge_blob, - end_sponge_blob: empty_sponge_blob, + start_sponge_blob, + end_sponge_blob: start_sponge_blob, timestamp, // The followings are 0 since there are no tx effects in this block. out_hash: 0, diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/mod.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/mod.nr index 6443dd58f68a..5a72b9bd3da2 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/mod.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/mod.nr @@ -4,12 +4,14 @@ pub mod block_root_single_tx_first_rollup; pub mod block_root_empty_tx_first_rollup; pub mod block_root_rollup; pub mod block_root_single_tx_rollup; +pub mod block_root_msgs_only_rollup; mod tests; // Re-exports pub use crate::abis::BlockRollupPublicInputs; pub use block_root_empty_tx_first_rollup::BlockRootEmptyTxFirstRollupPrivateInputs; pub use block_root_first_rollup::BlockRootFirstRollupPrivateInputs; +pub use block_root_msgs_only_rollup::BlockRootMsgsOnlyRollupPrivateInputs; pub use block_root_rollup::BlockRootRollupPrivateInputs; pub use block_root_single_tx_first_rollup::BlockRootSingleTxFirstRollupPrivateInputs; pub use block_root_single_tx_rollup::BlockRootSingleTxRollupPrivateInputs; diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/tests/mod.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/tests/mod.nr index 9c963a53d9ba..cc26b5fbaf3d 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/tests/mod.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/tests/mod.nr @@ -2,6 +2,7 @@ mod child_proof_vk_tests; mod consecutive_rollups_tests; mod failures_tests; mod message_bundle_tests; +mod msgs_only_tests; mod rollup_structure_tests; use crate::{ diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/tests/msgs_only_tests.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/tests/msgs_only_tests.nr new file mode 100644 index 000000000000..1fcaa77a8cfe --- /dev/null +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/tests/msgs_only_tests.nr @@ -0,0 +1,184 @@ +use crate::{ + abis::{BlockRollupPublicInputs, L1ToL2MessageSponge}, + block_root::block_root_msgs_only_rollup::{self, BlockRootMsgsOnlyRollupPrivateInputs}, + tests::RollupFixtureBuilder, +}; +use types::{ + abis::{ + append_only_tree_snapshot::AppendOnlyTreeSnapshot, block_header::BlockHeader, + checkpoint_constant_data::CheckpointConstantData, global_variables::GlobalVariables, + partial_state_reference::PartialStateReference, state_reference::StateReference, + }, + blob_data::SpongeBlob, + constants::{ARCHIVE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, MAX_L1_TO_L2_MSGS_PER_BLOCK}, + merkle_tree::root_from_sibling_path, + traits::Hash, +}; + +global TEST_NUM_MSGS: u32 = 5; + +// Builds the inputs for a non-first, transaction-less block that inserts an L1-to-L2 message bundle. +struct TestBuilder { + previous_archive: AppendOnlyTreeSnapshot, + previous_state: StateReference, + constants: CheckpointConstantData, + timestamp: u64, + start_sponge_blob: SpongeBlob, + start_msg_sponge: L1ToL2MessageSponge, + l1_to_l2_messages: [Field; MAX_L1_TO_L2_MSGS_PER_BLOCK], + num_msgs: u32, + l1_to_l2_message_frontier_hint: [Field; L1_TO_L2_MSG_TREE_HEIGHT], + new_archive_sibling_path: [Field; ARCHIVE_HEIGHT], + // Retained for assertions. + new_l1_to_l2: AppendOnlyTreeSnapshot, + partial: PartialStateReference, +} + +impl TestBuilder { + pub fn new() -> Self { + let fixture_builder = RollupFixtureBuilder::new(); + let slot_number = fixture_builder.start_slot_number; + let block_number = fixture_builder.start_block_number; + + let (previous_l1_to_l2, l1_to_l2_messages, l1_to_l2_message_frontier_hint, new_l1_to_l2) = + fixture_builder.build_l1_to_l2_message_bundle(TEST_NUM_MSGS); + + let (previous_archive, new_archive_sibling_path) = + fixture_builder.build_archive_tree_for_insertion(block_number); + + // Borrow a partial state from a tx rollup fixture; it is simply propagated to the start and end states. + let partial = fixture_builder.get_tx_rollup_public_inputs(6).start_tree_snapshots; + let previous_state = + StateReference { l1_to_l2_message_tree: previous_l1_to_l2, partial }; + + Self { + previous_archive, + previous_state, + constants: fixture_builder.get_checkpoint_constant_data(slot_number), + timestamp: fixture_builder.get_global_variables(block_number).timestamp, + // A non-first block inherits a non-empty sponge blob and message sponge from the previous block. + start_sponge_blob: fixture_builder.get_sponge_blob(block_number), + start_msg_sponge: fixture_builder.get_msg_sponge(slot_number), + l1_to_l2_messages, + num_msgs: TEST_NUM_MSGS, + l1_to_l2_message_frontier_hint, + new_archive_sibling_path, + new_l1_to_l2, + partial, + } + } + + fn to_inputs(self) -> BlockRootMsgsOnlyRollupPrivateInputs { + BlockRootMsgsOnlyRollupPrivateInputs { + previous_archive: self.previous_archive, + previous_state: self.previous_state, + constants: self.constants, + timestamp: self.timestamp, + start_sponge_blob: self.start_sponge_blob, + start_msg_sponge: self.start_msg_sponge, + l1_to_l2_messages: self.l1_to_l2_messages, + num_msgs: self.num_msgs, + l1_to_l2_message_frontier_hint: self.l1_to_l2_message_frontier_hint, + new_archive_sibling_path: self.new_archive_sibling_path, + } + } + + pub fn execute(self) -> BlockRollupPublicInputs { + block_root_msgs_only_rollup::execute(self.to_inputs()) + } + + pub fn execute_and_fail(self) { + let _ = self.execute(); + } + + pub fn assert_expected_public_inputs(self, pi: BlockRollupPublicInputs) { + // A message-only block is a single, non-first block. + assert_eq(pi.num_blocks(), 1); + assert_eq(pi.constants, self.constants); + assert(!pi.is_first_block); + + // --- Sponge blobs: threaded from the (non-empty) provided start, with the block end data absorbed. --- + assert_eq(pi.start_sponge_blob, self.start_sponge_blob); + let mut expected_end_sponge_blob = self.start_sponge_blob; + expected_end_sponge_blob.absorb_block_end_data( + self.constants_as_global_variables(), + pi.previous_archive, + pi.end_state, + 0, // num_txs + 0, // total_mana_used + false, // is_first_block + ); + assert_eq(pi.end_sponge_blob, expected_end_sponge_blob); + + // --- Message sponge: threaded from the provided start, absorbing this block's bundle. --- + assert_eq(pi.start_msg_sponge, self.start_msg_sponge); + let mut expected_end_msg_sponge = self.start_msg_sponge; + expected_end_msg_sponge.absorb(self.l1_to_l2_messages, self.num_msgs); + assert_eq(pi.end_msg_sponge, expected_end_msg_sponge); + + // --- Archive: the block header hash is inserted at the next available index. --- + assert_eq(pi.previous_archive, self.previous_archive); + let sponge_blob_hash = expected_end_sponge_blob.squeeze(); + let block_header = BlockHeader { + last_archive: pi.previous_archive, + state: pi.end_state, + sponge_blob_hash, + global_variables: self.constants_as_global_variables(), + total_fees: pi.accumulated_fees, + total_mana_used: pi.accumulated_mana_used, + }; + let expected_archive_root = root_from_sibling_path( + block_header.hash(), + pi.previous_archive.next_available_leaf_index, + self.new_archive_sibling_path, + ); + assert_eq(pi.new_archive.root, expected_archive_root); + assert_eq( + pi.new_archive.next_available_leaf_index, + pi.previous_archive.next_available_leaf_index + 1, + ); + + // --- State: partial unchanged (no tx effects), l1-to-l2 tree moves by the appended bundle. --- + assert_eq(pi.start_state.partial, self.partial); + assert_eq(pi.end_state.partial, self.partial); + assert_eq(pi.start_state.l1_to_l2_message_tree, self.previous_state.l1_to_l2_message_tree); + assert_eq(pi.end_state.l1_to_l2_message_tree, self.new_l1_to_l2); + + // --- Timestamp / no tx effects. --- + assert_eq(pi.timestamp, self.timestamp); + assert_eq(pi.out_hash, 0); + assert_eq(pi.accumulated_fees, 0); + assert_eq(pi.accumulated_mana_used, 0); + } + + fn constants_as_global_variables(self) -> GlobalVariables { + GlobalVariables { + chain_id: self.constants.chain_id, + version: self.constants.version, + block_number: self.previous_archive.next_available_leaf_index as u32, + slot_number: self.constants.slot_number, + timestamp: self.timestamp, + coinbase: self.constants.coinbase, + fee_recipient: self.constants.fee_recipient, + gas_fees: self.constants.gas_fees, + } + } +} + +#[test] +fn msgs_only_block_root_success() { + let builder = TestBuilder::new(); + let pi = builder.execute(); + builder.assert_expected_public_inputs(pi); +} + +#[test(should_fail_with = "A message-only block must insert at least one L1-to-L2 message")] +fn msgs_only_block_root_rejects_empty_bundle() { + let mut builder = TestBuilder::new(); + + // A non-first empty block with no messages has no reason to exist and must be rejected. + builder.num_msgs = 0; + builder.l1_to_l2_messages = [0; MAX_L1_TO_L2_MSGS_PER_BLOCK]; + + builder.execute_and_fail(); +} diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_rollup.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_rollup.nr index 634ab2039dff..c967c585c7e7 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_rollup.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_rollup.nr @@ -10,18 +10,20 @@ use types::{ constants::{ ARCHIVE_HEIGHT, BLOBS_PER_CHECKPOINT, BLOCK_MERGE_ROLLUP_VK_INDEX, BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, - BLOCK_ROOT_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, - BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, FIELDS_PER_BLOB, OUT_HASH_TREE_HEIGHT, + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, BLOCK_ROOT_ROLLUP_VK_INDEX, + BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, + FIELDS_PER_BLOB, OUT_HASH_TREE_HEIGHT, }, proof::proof_data::{RollupHonkProofData, UltraHonkProofData}, }; -global ALLOWED_PREVIOUS_VK_INDICES: [u32; 6] = [ +global ALLOWED_PREVIOUS_VK_INDICES: [u32; 7] = [ BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, BLOCK_MERGE_ROLLUP_VK_INDEX, ]; diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_single_block_rollup.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_single_block_rollup.nr index 9b33eb095d64..d44d5a99d02f 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_single_block_rollup.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_single_block_rollup.nr @@ -10,16 +10,19 @@ use crate::{ use types::{ constants::{ BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, - BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, }, proof::proof_data::{RollupHonkProofData, UltraHonkProofData}, }; -// If there's only one previous rollup (i.e., one block in the checkpoint), it must be the first block root rollup. -global ALLOWED_PREVIOUS_VK_INDICES: [u32; 3] = [ +// If there's only one previous rollup (i.e., one block in the checkpoint), it must be a first block root: the inputs +// validator asserts the leftmost rollup's `is_first_block`. The message-only block root sets `is_first_block = false`, +// so it is listed here (for symmetry with the multi-block variant) but can never actually pass as the sole block. +global ALLOWED_PREVIOUS_VK_INDICES: [u32; 4] = [ BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX, + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, ]; pub struct CheckpointRootSingleBlockRollupPrivateInputs { diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/tests/mod.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/tests/mod.nr index 33e66b683c47..12712b7fda7c 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/tests/mod.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/tests/mod.nr @@ -1,6 +1,7 @@ mod blob_tests; mod child_proof_vk_tests; mod consecutive_rollups_tests; +mod msgs_only_tests; mod out_hash_tests; mod parity_tests; mod rollup_structure_tests; @@ -28,9 +29,9 @@ use types::{ blob_data::{create_checkpoint_end_marker, SpongeBlob}, constants::{ BLOCK_MERGE_ROLLUP_VK_INDEX, BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX, - BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_ROLLUP_VK_INDEX, - BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, - OUT_HASH_TREE_LEAF_COUNT, PARITY_ROOT_VK_INDEX, + BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, + BLOCK_ROOT_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, + BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, OUT_HASH_TREE_LEAF_COUNT, PARITY_ROOT_VK_INDEX, }, merkle_tree::compute_sha_tree_root, traits::{Hash, Serialize}, @@ -262,7 +263,8 @@ impl TestBuilder { | (vk_index == BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX) | (vk_index == BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX) | (vk_index == BLOCK_ROOT_ROLLUP_VK_INDEX) - | (vk_index == BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX), + | (vk_index == BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX) + | (vk_index == BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX), ); } } diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/tests/msgs_only_tests.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/tests/msgs_only_tests.nr new file mode 100644 index 000000000000..444a26cac87a --- /dev/null +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/tests/msgs_only_tests.nr @@ -0,0 +1,30 @@ +use super::TestBuilder; +use types::constants::{BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, BLOCK_ROOT_ROLLUP_VK_INDEX, BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX}; + +// A checkpoint of [first block, message-only block] proves: the message-only block is accepted as a non-first direct +// child of the checkpoint root. +#[test] +fn checkpoint_with_msgs_only_right_block() { + let builder = TestBuilder::new( + BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, + 1, + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, + 1, + ); + let (pi, mock) = builder.execute_with_mock(); + builder.assert_expected_public_inputs(pi); + builder.assert_mock_called(mock); +} + +// The message-only block sets `is_first_block = false`, so it can never be the leftmost block of a checkpoint: the +// checkpoint root requires the leftmost rollup's `is_first_block` to be true. +#[test(should_fail_with = "The first block of the checkpoint must be a first block root")] +fn msgs_only_block_cannot_be_leftmost() { + let builder = TestBuilder::new( + BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, + 1, + BLOCK_ROOT_ROLLUP_VK_INDEX, + 1, + ); + builder.execute_with_mock_and_fail(); +} diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index c5f90c48b692..a2b2a6a41f28 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -153,6 +153,9 @@ pub global PRIVATE_KERNEL_INNER_2_VK_INDEX: u32 = 69; pub global PRIVATE_KERNEL_INNER_3_VK_INDEX: u32 = 70; pub global PRIVATE_KERNEL_INNER_4_VK_INDEX: u32 = 71; pub global PRIVATE_KERNEL_INNER_5_VK_INDEX: u32 = 72; +// The block-root variants at 10-14 are full; the non-first message-only block root is appended after the +// multi-app kernel indices (65-72), since 43-64 are reserved for reset variants. +pub global BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX: u32 = 73; // Maximum number of apps absorbed by a single private kernel iteration. pub global MAX_APPS_PER_KERNEL: u32 = 5; diff --git a/yarn-project/bb-prover/src/prover/server/bb_prover.ts b/yarn-project/bb-prover/src/prover/server/bb_prover.ts index c33794188bc0..e7777e7320d5 100644 --- a/yarn-project/bb-prover/src/prover/server/bb_prover.ts +++ b/yarn-project/bb-prover/src/prover/server/bb_prover.ts @@ -316,6 +316,18 @@ export class BBNativeRollupProver implements ServerCircuitProver { ); } + public getBlockRootMsgsOnlyRollupProof( + input: BlockRootMsgsOnlyRollupPrivateInputs, + ): Promise> { + return this.createRecursiveProofAndVerify( + input, + 'BlockRootMsgsOnlyRollupArtifact', + NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH, + convertBlockRootMsgsOnlyRollupPrivateInputsToWitnessMap, + convertBlockRootMsgsOnlyRollupOutputsFromWitnessMap, + ); + } + public getBlockMergeRollupProof( input: BlockMergeRollupPrivateInputs, ): Promise> { diff --git a/yarn-project/bb-prover/src/test/delay_values.ts b/yarn-project/bb-prover/src/test/delay_values.ts index 023b6c540b5e..e5ff5832fb04 100644 --- a/yarn-project/bb-prover/src/test/delay_values.ts +++ b/yarn-project/bb-prover/src/test/delay_values.ts @@ -6,6 +6,7 @@ export const WITGEN_DELAY_MS: Record = { [ProvingRequestType.PARITY_ROOT]: 40, [ProvingRequestType.BLOCK_ROOT_FIRST_ROLLUP]: 45, [ProvingRequestType.BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP]: 18, + [ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP]: 18, [ProvingRequestType.BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP]: 27, [ProvingRequestType.CHECKPOINT_MERGE_ROLLUP]: 30, [ProvingRequestType.CHECKPOINT_ROOT_SINGLE_BLOCK_ROLLUP]: 36_000, @@ -29,6 +30,7 @@ export const PROOF_DELAY_MS: Record = { [ProvingRequestType.PARITY_ROOT]: 18_600, [ProvingRequestType.BLOCK_ROOT_FIRST_ROLLUP]: 17_400, [ProvingRequestType.BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP]: 4_500, + [ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP]: 4_500, [ProvingRequestType.BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP]: 9_200, [ProvingRequestType.CHECKPOINT_MERGE_ROLLUP]: 10_200, [ProvingRequestType.CHECKPOINT_ROOT_SINGLE_BLOCK_ROLLUP]: 37_100, diff --git a/yarn-project/bb-prover/src/test/test_circuit_prover.ts b/yarn-project/bb-prover/src/test/test_circuit_prover.ts index 9ef5ef33cfe2..1433598d67ca 100644 --- a/yarn-project/bb-prover/src/test/test_circuit_prover.ts +++ b/yarn-project/bb-prover/src/test/test_circuit_prover.ts @@ -15,6 +15,8 @@ import { convertBlockRootEmptyTxFirstRollupPrivateInputsToWitnessMap, convertBlockRootFirstRollupOutputsFromWitnessMap, convertBlockRootFirstRollupPrivateInputsToWitnessMap, + convertBlockRootMsgsOnlyRollupOutputsFromWitnessMap, + convertBlockRootMsgsOnlyRollupPrivateInputsToWitnessMap, convertBlockRootRollupOutputsFromWitnessMap, convertBlockRootRollupPrivateInputsToWitnessMap, convertBlockRootSingleTxFirstRollupOutputsFromWitnessMap, @@ -67,6 +69,7 @@ import { type BlockRollupPublicInputs, type BlockRootEmptyTxFirstRollupPrivateInputs, type BlockRootFirstRollupPrivateInputs, + type BlockRootMsgsOnlyRollupPrivateInputs, type BlockRootRollupPrivateInputs, type BlockRootSingleTxFirstRollupPrivateInputs, type BlockRootSingleTxRollupPrivateInputs, @@ -269,6 +272,21 @@ export class TestCircuitProver implements ServerCircuitProver { ); } + @trackSpan('TestCircuitProver.getBlockRootMsgsOnlyRollupProof') + public getBlockRootMsgsOnlyRollupProof( + input: BlockRootMsgsOnlyRollupPrivateInputs, + ): Promise> { + return this.applyDelay(ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP, () => + this.simulate( + input, + 'BlockRootMsgsOnlyRollupArtifact', + NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH, + convertBlockRootMsgsOnlyRollupPrivateInputsToWitnessMap, + convertBlockRootMsgsOnlyRollupOutputsFromWitnessMap, + ), + ); + } + @trackSpan('TestCircuitProver.getBlockRootRollupProof') public getBlockRootRollupProof( input: BlockRootRollupPrivateInputs, diff --git a/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts b/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts index c2c2fea39c78..09e3ff25a296 100644 --- a/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts +++ b/yarn-project/noir-protocol-circuits-types/src/artifacts/server.ts @@ -10,6 +10,7 @@ import BlockRootEmptyTxFirstRollupJson from '../../artifacts/rollup_block_root_f import BlockRootFirstRollupSimulatedJson from '../../artifacts/rollup_block_root_first_simulated.json' with { type: 'json' }; import BlockRootSingleTxFirstRollupJson from '../../artifacts/rollup_block_root_first_single_tx.json' with { type: 'json' }; import BlockRootSingleTxFirstRollupSimulatedJson from '../../artifacts/rollup_block_root_first_single_tx_simulated.json' with { type: 'json' }; +import BlockRootMsgsOnlyRollupJson from '../../artifacts/rollup_block_root_msgs_only.json' with { type: 'json' }; import BlockRootRollupSimulatedJson from '../../artifacts/rollup_block_root_simulated.json' with { type: 'json' }; import BlockRootSingleTxRollupJson from '../../artifacts/rollup_block_root_single_tx.json' with { type: 'json' }; import BlockRootSingleTxRollupSimulatedJson from '../../artifacts/rollup_block_root_single_tx_simulated.json' with { type: 'json' }; @@ -39,6 +40,7 @@ export const ServerCircuitArtifacts: Record = { BlockRootEmptyTxFirstRollupArtifact: BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP_VK_INDEX, BlockRootRollupArtifact: BLOCK_ROOT_ROLLUP_VK_INDEX, BlockRootSingleTxRollupArtifact: BLOCK_ROOT_SINGLE_TX_ROLLUP_VK_INDEX, + BlockRootMsgsOnlyRollupArtifact: BLOCK_ROOT_MSGS_ONLY_ROLLUP_VK_INDEX, BlockMergeRollupArtifact: BLOCK_MERGE_ROLLUP_VK_INDEX, CheckpointRootRollupArtifact: CHECKPOINT_ROOT_ROLLUP_VK_INDEX, CheckpointRootSingleBlockRollupArtifact: CHECKPOINT_ROOT_SINGLE_BLOCK_ROLLUP_VK_INDEX, diff --git a/yarn-project/noir-protocol-circuits-types/src/conversion/server.ts b/yarn-project/noir-protocol-circuits-types/src/conversion/server.ts index 7573a66ced79..14fe025e93a5 100644 --- a/yarn-project/noir-protocol-circuits-types/src/conversion/server.ts +++ b/yarn-project/noir-protocol-circuits-types/src/conversion/server.ts @@ -41,6 +41,7 @@ import { BlockRollupPublicInputs, BlockRootEmptyTxFirstRollupPrivateInputs, BlockRootFirstRollupPrivateInputs, + BlockRootMsgsOnlyRollupPrivateInputs, BlockRootRollupPrivateInputs, BlockRootSingleTxFirstRollupPrivateInputs, BlockRootSingleTxRollupPrivateInputs, @@ -77,6 +78,7 @@ import type { BlockRollupPublicInputs as BlockRollupPublicInputsNoir, BlockRootEmptyTxFirstRollupPrivateInputs as BlockRootEmptyTxFirstRollupPrivateInputsNoir, BlockRootFirstRollupPrivateInputs as BlockRootFirstRollupPrivateInputsNoir, + BlockRootMsgsOnlyRollupPrivateInputs as BlockRootMsgsOnlyRollupPrivateInputsNoir, BlockRootRollupPrivateInputs as BlockRootRollupPrivateInputsNoir, BlockRootSingleTxFirstRollupPrivateInputs as BlockRootSingleTxFirstRollupPrivateInputsNoir, BlockRootSingleTxRollupPrivateInputs as BlockRootSingleTxRollupPrivateInputsNoir, @@ -880,6 +882,23 @@ export function mapBlockRootEmptyTxFirstRollupPrivateInputsToNoir( }; } +export function mapBlockRootMsgsOnlyRollupPrivateInputsToNoir( + inputs: BlockRootMsgsOnlyRollupPrivateInputs, +): BlockRootMsgsOnlyRollupPrivateInputsNoir { + return { + previous_archive: mapAppendOnlyTreeSnapshotToNoir(inputs.previousArchive), + previous_state: mapStateReferenceToNoir(inputs.previousState), + constants: mapCheckpointConstantDataToNoir(inputs.constants), + timestamp: mapU64ToNoir(inputs.timestamp), + start_sponge_blob: mapSpongeBlobToNoir(inputs.startSpongeBlob), + start_msg_sponge: mapL1ToL2MessageSpongeToNoir(inputs.startMsgSponge), + l1_to_l2_messages: mapFieldArrayToNoir(inputs.l1ToL2Messages), + num_msgs: mapNumberToNoir(inputs.numMsgs), + l1_to_l2_message_frontier_hint: mapTuple(inputs.l1ToL2MessageFrontierHint, mapFieldToNoir), + new_archive_sibling_path: mapTuple(inputs.newArchiveSiblingPath, mapFieldToNoir), + }; +} + export function mapBlockRootRollupPrivateInputsToNoir( inputs: BlockRootRollupPrivateInputs, ): BlockRootRollupPrivateInputsNoir { diff --git a/yarn-project/noir-protocol-circuits-types/src/execution/server.ts b/yarn-project/noir-protocol-circuits-types/src/execution/server.ts index a2719392b148..e25e578cce5a 100644 --- a/yarn-project/noir-protocol-circuits-types/src/execution/server.ts +++ b/yarn-project/noir-protocol-circuits-types/src/execution/server.ts @@ -7,6 +7,7 @@ import type { BlockRollupPublicInputs, BlockRootEmptyTxFirstRollupPrivateInputs, BlockRootFirstRollupPrivateInputs, + BlockRootMsgsOnlyRollupPrivateInputs, BlockRootRollupPrivateInputs, BlockRootSingleTxFirstRollupPrivateInputs, BlockRootSingleTxRollupPrivateInputs, @@ -32,6 +33,7 @@ import { mapBlockRollupPublicInputsFromNoir, mapBlockRootEmptyTxFirstRollupPrivateInputsToNoir, mapBlockRootFirstRollupPrivateInputsToNoir, + mapBlockRootMsgsOnlyRollupPrivateInputsToNoir, mapBlockRootRollupPrivateInputsToNoir, mapBlockRootSingleTxFirstRollupPrivateInputsToNoir, mapBlockRootSingleTxRollupPrivateInputsToNoir, @@ -59,6 +61,7 @@ import type { RollupBlockRootFirstEmptyTxReturnType, RollupBlockRootFirstReturnType, RollupBlockRootFirstSingleTxReturnType, + RollupBlockRootMsgsOnlyReturnType, RollupBlockRootReturnType, RollupBlockRootSingleTxReturnType, RollupCheckpointMergeReturnType, @@ -201,6 +204,17 @@ export function convertBlockRootSingleTxRollupPrivateInputsToWitnessMap( ); } +export function convertBlockRootMsgsOnlyRollupPrivateInputsToWitnessMap( + inputs: BlockRootMsgsOnlyRollupPrivateInputs, + simulated = false, +): WitnessMap { + return convertPrivateInputsToWitnessMap( + 'BlockRootMsgsOnlyRollupArtifact', + mapBlockRootMsgsOnlyRollupPrivateInputsToNoir(inputs), + simulated, + ); +} + export function convertBlockMergeRollupPrivateInputsToWitnessMap( inputs: BlockMergeRollupPrivateInputs, simulated = false, @@ -370,6 +384,18 @@ export function convertBlockRootSingleTxRollupOutputsFromWitnessMap( return mapBlockRollupPublicInputsFromNoir(publicInputs); } +export function convertBlockRootMsgsOnlyRollupOutputsFromWitnessMap( + outputs: WitnessMap, + simulated = false, +): BlockRollupPublicInputs { + const publicInputs = convertOutputsFromWitnessMap( + 'BlockRootMsgsOnlyRollupArtifact', + outputs, + simulated, + ); + return mapBlockRollupPublicInputsFromNoir(publicInputs); +} + export function convertBlockMergeRollupOutputsFromWitnessMap( outputs: WitnessMap, simulated = false, diff --git a/yarn-project/noir-protocol-circuits-types/src/scripts/generate_ts_from_abi.ts b/yarn-project/noir-protocol-circuits-types/src/scripts/generate_ts_from_abi.ts index 8cca923f21ef..48630c78f333 100644 --- a/yarn-project/noir-protocol-circuits-types/src/scripts/generate_ts_from_abi.ts +++ b/yarn-project/noir-protocol-circuits-types/src/scripts/generate_ts_from_abi.ts @@ -34,6 +34,7 @@ const circuits = [ 'rollup_block_root_first', 'rollup_block_root_first_single_tx', 'rollup_block_root_first_empty_tx', + 'rollup_block_root_msgs_only', 'rollup_block_merge', 'rollup_checkpoint_root', 'rollup_checkpoint_root_single_block', diff --git a/yarn-project/prover-client/src/proving_broker/broker_prover_facade.ts b/yarn-project/prover-client/src/proving_broker/broker_prover_facade.ts index acb80833f38f..921eda16106d 100644 --- a/yarn-project/prover-client/src/proving_broker/broker_prover_facade.ts +++ b/yarn-project/prover-client/src/proving_broker/broker_prover_facade.ts @@ -29,6 +29,7 @@ import type { BlockRollupPublicInputs, BlockRootEmptyTxFirstRollupPrivateInputs, BlockRootFirstRollupPrivateInputs, + BlockRootMsgsOnlyRollupPrivateInputs, BlockRootRollupPrivateInputs, BlockRootSingleTxFirstRollupPrivateInputs, BlockRootSingleTxRollupPrivateInputs, @@ -534,6 +535,20 @@ export class BrokerCircuitProverFacade implements ServerCircuitProver { ); } + getBlockRootMsgsOnlyRollupProof( + input: BlockRootMsgsOnlyRollupPrivateInputs, + signal?: AbortSignal, + epochNumber?: EpochNumber, + ): Promise> { + return this.enqueueJob( + this.generateId(ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP, input, epochNumber), + ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP, + input, + epochNumber, + signal, + ); + } + getBlockRootRollupProof( input: BlockRootRollupPrivateInputs, signal?: AbortSignal, diff --git a/yarn-project/prover-client/src/test/mock_prover.ts b/yarn-project/prover-client/src/test/mock_prover.ts index 51415ebf677e..2257b9d8f4c9 100644 --- a/yarn-project/prover-client/src/test/mock_prover.ts +++ b/yarn-project/prover-client/src/test/mock_prover.ts @@ -22,6 +22,7 @@ import type { BlockRollupPublicInputs, BlockRootEmptyTxFirstRollupPrivateInputs, BlockRootFirstRollupPrivateInputs, + BlockRootMsgsOnlyRollupPrivateInputs, BlockRootRollupPrivateInputs, BlockRootSingleTxFirstRollupPrivateInputs, BlockRootSingleTxRollupPrivateInputs, @@ -227,6 +228,20 @@ export class MockProver implements ServerCircuitProver { ); } + getBlockRootMsgsOnlyRollupProof( + _input: BlockRootMsgsOnlyRollupPrivateInputs, + _signal?: AbortSignal, + _epochNumber?: number, + ): Promise> { + return Promise.resolve( + makePublicInputsAndRecursiveProof( + makeBlockRollupPublicInputs(), + makeRecursiveProof(NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH), + VerificationKeyData.makeFakeRollupHonk(), + ), + ); + } + getBlockRootRollupProof( _input: BlockRootRollupPrivateInputs, _signal?: AbortSignal, diff --git a/yarn-project/stdlib/src/interfaces/proving-job.ts b/yarn-project/stdlib/src/interfaces/proving-job.ts index a84e82b9af2b..4517592f5d1c 100644 --- a/yarn-project/stdlib/src/interfaces/proving-job.ts +++ b/yarn-project/stdlib/src/interfaces/proving-job.ts @@ -21,6 +21,7 @@ import { BlockRollupPublicInputs } from '../rollup/block_rollup_public_inputs.js import { BlockRootEmptyTxFirstRollupPrivateInputs, BlockRootFirstRollupPrivateInputs, + BlockRootMsgsOnlyRollupPrivateInputs, BlockRootRollupPrivateInputs, BlockRootSingleTxFirstRollupPrivateInputs, BlockRootSingleTxRollupPrivateInputs, @@ -113,6 +114,10 @@ export const ProvingJobInputs = z.discriminatedUnion('type', [ type: z.literal(ProvingRequestType.BLOCK_ROOT_SINGLE_TX_ROLLUP), inputs: BlockRootSingleTxRollupPrivateInputs.schema, }), + z.object({ + type: z.literal(ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP), + inputs: BlockRootMsgsOnlyRollupPrivateInputs.schema, + }), z.object({ type: z.literal(ProvingRequestType.BLOCK_MERGE_ROLLUP), inputs: BlockMergeRollupPrivateInputs.schema }), z.object({ type: z.literal(ProvingRequestType.CHECKPOINT_ROOT_ROLLUP), @@ -155,6 +160,8 @@ export function getProvingJobInputClassFor(type: ProvingRequestType) { return BlockRootRollupPrivateInputs; case ProvingRequestType.BLOCK_ROOT_SINGLE_TX_ROLLUP: return BlockRootSingleTxRollupPrivateInputs; + case ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP: + return BlockRootMsgsOnlyRollupPrivateInputs; case ProvingRequestType.BLOCK_MERGE_ROLLUP: return BlockMergeRollupPrivateInputs; case ProvingRequestType.CHECKPOINT_ROOT_ROLLUP: @@ -191,6 +198,7 @@ export type ProvingJobInputsMap = { [ProvingRequestType.BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP]: BlockRootEmptyTxFirstRollupPrivateInputs; [ProvingRequestType.BLOCK_ROOT_ROLLUP]: BlockRootRollupPrivateInputs; [ProvingRequestType.BLOCK_ROOT_SINGLE_TX_ROLLUP]: BlockRootSingleTxRollupPrivateInputs; + [ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP]: BlockRootMsgsOnlyRollupPrivateInputs; [ProvingRequestType.BLOCK_MERGE_ROLLUP]: BlockMergeRollupPrivateInputs; [ProvingRequestType.CHECKPOINT_ROOT_ROLLUP]: CheckpointRootRollupPrivateInputs; [ProvingRequestType.CHECKPOINT_ROOT_SINGLE_BLOCK_ROLLUP]: CheckpointRootSingleBlockRollupPrivateInputs; @@ -348,6 +356,10 @@ export type ProvingJobResultsMap = { BlockRollupPublicInputs, typeof NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH >; + [ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP]: PublicInputsAndRecursiveProof< + BlockRollupPublicInputs, + typeof NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH + >; [ProvingRequestType.BLOCK_ROOT_ROLLUP]: PublicInputsAndRecursiveProof< BlockRollupPublicInputs, typeof NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH diff --git a/yarn-project/stdlib/src/interfaces/server_circuit_prover.ts b/yarn-project/stdlib/src/interfaces/server_circuit_prover.ts index 21f1ea48f2d5..5503be50be64 100644 --- a/yarn-project/stdlib/src/interfaces/server_circuit_prover.ts +++ b/yarn-project/stdlib/src/interfaces/server_circuit_prover.ts @@ -15,6 +15,7 @@ import type { BlockRollupPublicInputs } from '../rollup/block_rollup_public_inpu import type { BlockRootEmptyTxFirstRollupPrivateInputs, BlockRootFirstRollupPrivateInputs, + BlockRootMsgsOnlyRollupPrivateInputs, BlockRootRollupPrivateInputs, BlockRootSingleTxFirstRollupPrivateInputs, BlockRootSingleTxRollupPrivateInputs, @@ -129,6 +130,12 @@ export interface ServerCircuitProver { epochNumber?: number, ): Promise>; + getBlockRootMsgsOnlyRollupProof( + input: BlockRootMsgsOnlyRollupPrivateInputs, + signal?: AbortSignal, + epochNumber?: number, + ): Promise>; + /** * Creates a proof for the given input. * @param input - Input to the circuit. diff --git a/yarn-project/stdlib/src/proofs/proving_request_type.ts b/yarn-project/stdlib/src/proofs/proving_request_type.ts index d7517c61cd56..43f3d7147c31 100644 --- a/yarn-project/stdlib/src/proofs/proving_request_type.ts +++ b/yarn-project/stdlib/src/proofs/proving_request_type.ts @@ -10,6 +10,7 @@ export enum ProvingRequestType { BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP, BLOCK_ROOT_ROLLUP, BLOCK_ROOT_SINGLE_TX_ROLLUP, + BLOCK_ROOT_MSGS_ONLY_ROLLUP, BLOCK_MERGE_ROLLUP, CHECKPOINT_ROOT_ROLLUP, CHECKPOINT_ROOT_SINGLE_BLOCK_ROLLUP, diff --git a/yarn-project/stdlib/src/rollup/block_root_rollup_private_inputs.ts b/yarn-project/stdlib/src/rollup/block_root_rollup_private_inputs.ts index 956b6cde9889..7723a0402bef 100644 --- a/yarn-project/stdlib/src/rollup/block_root_rollup_private_inputs.ts +++ b/yarn-project/stdlib/src/rollup/block_root_rollup_private_inputs.ts @@ -1,3 +1,4 @@ +import { SpongeBlob } from '@aztec/blob-lib/types'; import { ARCHIVE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, MAX_L1_TO_L2_MSGS_PER_BLOCK } from '@aztec/constants'; import { Fr } from '@aztec/foundation/curves/bn254'; import { bufferSchemaFor } from '@aztec/foundation/schemas'; @@ -260,6 +261,109 @@ export class BlockRootEmptyTxFirstRollupPrivateInputs { } } +export class BlockRootMsgsOnlyRollupPrivateInputs { + constructor( + /** + * The archive after applying the previous block. + */ + public previousArchive: AppendOnlyTreeSnapshot, + /** + * The state reference of the previous block. + */ + public previousState: StateReference, + /** + * The constants of the checkpoint. + */ + public constants: CheckpointConstantData, + /** + * The timestamp of this block. + */ + public timestamp: UInt64, + /** + * Sponge blob inherited from the previous block (checked against its `endSpongeBlob` in the merge/checkpoint root). + */ + public startSpongeBlob: SpongeBlob, + /** + * Message sponge inherited from the previous block (checked against its `endMsgSponge` in the merge/checkpoint root). + */ + public startMsgSponge: L1ToL2MessageSponge, + /** + * L1-to-L2 messages inserted by this block, padded with zeros to `MAX_L1_TO_L2_MSGS_PER_BLOCK`. + */ + public l1ToL2Messages: Fr[], + /** + * Number of real (non-padding) leaves in `l1ToL2Messages`. + */ + public numMsgs: number, + /** + * Frontier hint for appending the message bundle to the previous state's l1 to l2 message tree. + */ + public l1ToL2MessageFrontierHint: Tuple, + /** + * Hint for inserting the new block hash to the last archive. + */ + public newArchiveSiblingPath: Tuple, + ) {} + + static from(fields: FieldsOf) { + return new BlockRootMsgsOnlyRollupPrivateInputs(...BlockRootMsgsOnlyRollupPrivateInputs.getFields(fields)); + } + + static getFields(fields: FieldsOf) { + return [ + fields.previousArchive, + fields.previousState, + fields.constants, + fields.timestamp, + fields.startSpongeBlob, + fields.startMsgSponge, + fields.l1ToL2Messages, + fields.numMsgs, + fields.l1ToL2MessageFrontierHint, + fields.newArchiveSiblingPath, + ] as const; + } + + toBuffer() { + return serializeToBuffer( + this.previousArchive, + this.previousState, + this.constants, + bigintToUInt64BE(this.timestamp), + this.startSpongeBlob, + this.startMsgSponge, + this.l1ToL2Messages, + this.numMsgs, + this.l1ToL2MessageFrontierHint, + this.newArchiveSiblingPath, + ); + } + + static fromBuffer(buffer: Buffer | BufferReader) { + const reader = BufferReader.asReader(buffer); + return new BlockRootMsgsOnlyRollupPrivateInputs( + AppendOnlyTreeSnapshot.fromBuffer(reader), + StateReference.fromBuffer(reader), + CheckpointConstantData.fromBuffer(reader), + reader.readUInt64(), + reader.readObject(SpongeBlob), + reader.readObject(L1ToL2MessageSponge), + readL1ToL2Messages(reader), + reader.readNumber(), + reader.readArray(L1_TO_L2_MSG_TREE_HEIGHT, Fr), + reader.readArray(ARCHIVE_HEIGHT, Fr), + ); + } + + toJSON() { + return this.toBuffer(); + } + + static get schema() { + return bufferSchemaFor(BlockRootMsgsOnlyRollupPrivateInputs); + } +} + export class BlockRootRollupPrivateInputs { constructor( /** diff --git a/yarn-project/stdlib/src/stats/stats.ts b/yarn-project/stdlib/src/stats/stats.ts index cc79352f8bd7..dd8cccab16e0 100644 --- a/yarn-project/stdlib/src/stats/stats.ts +++ b/yarn-project/stdlib/src/stats/stats.ts @@ -112,6 +112,7 @@ export type ServerCircuitName = | 'rollup-block-root-first-empty-tx' | 'rollup-block-root' | 'rollup-block-root-single-tx' + | 'rollup-block-root-msgs-only' | 'rollup-block-merge' | 'rollup-checkpoint-root' | 'rollup-checkpoint-root-single-block' From e53f7941ad6154ed7a18933bdfa842a05259233b Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 8 Jul 2026 14:50:53 -0300 Subject: [PATCH 2/5] fix: complete proving-broker and bb-prover wiring for msgs-only block root (A-1375) --- yarn-project/bb-prover/src/prover/server/bb_prover.ts | 3 +++ .../prover-client/src/proving_broker/proving_broker.ts | 2 ++ .../src/proving_broker/proving_job_controller.ts | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/yarn-project/bb-prover/src/prover/server/bb_prover.ts b/yarn-project/bb-prover/src/prover/server/bb_prover.ts index e7777e7320d5..815caf5730a2 100644 --- a/yarn-project/bb-prover/src/prover/server/bb_prover.ts +++ b/yarn-project/bb-prover/src/prover/server/bb_prover.ts @@ -17,6 +17,8 @@ import { convertBlockRootEmptyTxFirstRollupPrivateInputsToWitnessMap, convertBlockRootFirstRollupOutputsFromWitnessMap, convertBlockRootFirstRollupPrivateInputsToWitnessMap, + convertBlockRootMsgsOnlyRollupOutputsFromWitnessMap, + convertBlockRootMsgsOnlyRollupPrivateInputsToWitnessMap, convertBlockRootRollupOutputsFromWitnessMap, convertBlockRootRollupPrivateInputsToWitnessMap, convertBlockRootSingleTxFirstRollupOutputsFromWitnessMap, @@ -65,6 +67,7 @@ import { BlockRollupPublicInputs, BlockRootEmptyTxFirstRollupPrivateInputs, BlockRootFirstRollupPrivateInputs, + BlockRootMsgsOnlyRollupPrivateInputs, BlockRootRollupPrivateInputs, BlockRootSingleTxFirstRollupPrivateInputs, BlockRootSingleTxRollupPrivateInputs, diff --git a/yarn-project/prover-client/src/proving_broker/proving_broker.ts b/yarn-project/prover-client/src/proving_broker/proving_broker.ts index 66167dec7c35..3de09ebc4565 100644 --- a/yarn-project/prover-client/src/proving_broker/proving_broker.ts +++ b/yarn-project/prover-client/src/proving_broker/proving_broker.ts @@ -55,6 +55,7 @@ export class ProvingBroker implements ProvingJobProducer, ProvingJobConsumer, Pr [ProvingRequestType.BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP]: new PriorityMemoryQueue( provingJobComparator, ), + [ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP]: new PriorityMemoryQueue(provingJobComparator), [ProvingRequestType.BLOCK_ROOT_ROLLUP]: new PriorityMemoryQueue(provingJobComparator), [ProvingRequestType.BLOCK_ROOT_SINGLE_TX_ROLLUP]: new PriorityMemoryQueue(provingJobComparator), @@ -808,6 +809,7 @@ export const PROOF_TYPES_IN_PRIORITY_ORDER: ProvingRequestType[] = [ ProvingRequestType.BLOCK_ROOT_EMPTY_TX_FIRST_ROLLUP, ProvingRequestType.BLOCK_ROOT_ROLLUP, ProvingRequestType.BLOCK_ROOT_SINGLE_TX_ROLLUP, + ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP, ProvingRequestType.BLOCK_MERGE_ROLLUP, ProvingRequestType.CHECKPOINT_ROOT_ROLLUP, ProvingRequestType.CHECKPOINT_ROOT_SINGLE_BLOCK_ROLLUP, diff --git a/yarn-project/prover-client/src/proving_broker/proving_job_controller.ts b/yarn-project/prover-client/src/proving_broker/proving_job_controller.ts index 043d0293eef5..f9517cb227fd 100644 --- a/yarn-project/prover-client/src/proving_broker/proving_job_controller.ts +++ b/yarn-project/prover-client/src/proving_broker/proving_job_controller.ts @@ -162,6 +162,10 @@ export class ProvingJobController { return await this.circuitProver.getBlockRootEmptyTxFirstRollupProof(inputs, signal, this.epochNumber); } + case ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP: { + return await this.circuitProver.getBlockRootMsgsOnlyRollupProof(inputs, signal, this.epochNumber); + } + case ProvingRequestType.BLOCK_ROOT_ROLLUP: { return await this.circuitProver.getBlockRootRollupProof(inputs, signal, this.epochNumber); } From 7fcb4afd128f9c2777d2c619573fc81d799eeb8d Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 13 Jul 2026 18:53:41 -0300 Subject: [PATCH 3/5] docs: warn that the msgs-only single-block allowlist entry rests on is_first_block (A-1375) The message-only block root is listed in the single-block checkpoint-root allowlist only for symmetry; today the inputs validator's is_first_block assertion is the sole reason it cannot stand as a checkpoint's only block. Note that if that assertion is ever relaxed, this entry must be dropped in the same change, so the two decisions are not made independently. --- .../checkpoint_root/checkpoint_root_single_block_rollup.nr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_single_block_rollup.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_single_block_rollup.nr index d44d5a99d02f..947eb6fb6b66 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_single_block_rollup.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/checkpoint_root_single_block_rollup.nr @@ -18,6 +18,11 @@ use types::{ // If there's only one previous rollup (i.e., one block in the checkpoint), it must be a first block root: the inputs // validator asserts the leftmost rollup's `is_first_block`. The message-only block root sets `is_first_block = false`, // so it is listed here (for symmetry with the multi-block variant) but can never actually pass as the sole block. +// +// That `is_first_block` assertion is the only thing keeping a message-only block from standing as a checkpoint's sole +// block. If it is ever relaxed (e.g. replaced by a "leftmost block's start_msg_sponge is empty" check), this entry +// must be dropped in the same change, or a transaction-less single-block checkpoint — one whose blob omits the +// first-block l1-to-l2 root — would silently become provable. global ALLOWED_PREVIOUS_VK_INDICES: [u32; 4] = [ BLOCK_ROOT_FIRST_ROLLUP_VK_INDEX, BLOCK_ROOT_SINGLE_TX_FIRST_ROLLUP_VK_INDEX, From 182ecbd9fe182bb600924aa3bdc7f6beb1ca68e4 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 20 Jul 2026 21:38:13 -0300 Subject: [PATCH 4/5] fix(fast-inbox): include the message-only block-root proof in the ProvingJobResult schema (A-1375) BLOCK_ROOT_MSGS_ONLY_ROLLUP was added to the enum, the ProvingJobInputs union, and the result type map, but omitted from the ProvingJobResult discriminated union. A checkpoint that builds a message-only block produces this proof output, which the proof store then fails to decode with an invalid-discriminator error, stalling proving. Add the missing union member, mirroring the other block-root results. Adds a schema round-trip regression test. --- .../src/interfaces/proving-job-source.test.ts | 27 ++++++++++++++++++- .../stdlib/src/interfaces/proving-job.ts | 7 +++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/yarn-project/stdlib/src/interfaces/proving-job-source.test.ts b/yarn-project/stdlib/src/interfaces/proving-job-source.test.ts index f37a4832f708..9dcf9d2ec8aa 100644 --- a/yarn-project/stdlib/src/interfaces/proving-job-source.test.ts +++ b/yarn-project/stdlib/src/interfaces/proving-job-source.test.ts @@ -1,16 +1,19 @@ import { NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH } from '@aztec/constants'; import { EpochNumber } from '@aztec/foundation/branded-types'; +import { jsonParseWithSchema, jsonStringify } from '@aztec/foundation/json-rpc'; import { type JsonRpcTestContext, createJsonRpcTestSetup } from '@aztec/foundation/json-rpc/test'; import { ProvingRequestType } from '../proofs/proving_request_type.js'; import { makeRecursiveProof } from '../proofs/recursive_proof.js'; +import { BlockRollupPublicInputs } from '../rollup/block_rollup_public_inputs.js'; import { TxRollupPublicInputs } from '../rollup/tx_rollup_public_inputs.js'; +import { makeBlockRollupPublicInputs } from '../tests/factories.js'; import { VerificationKeyData } from '../vks/verification_key.js'; import { type ProvingJobSource, ProvingJobSourceSchema } from './proving-job-source.js'; import { type ProofUri, type ProvingJob, - type ProvingJobResult, + ProvingJobResult, type ProvingRequestResultFor, makePublicInputsAndRecursiveProof, } from './proving-job.js'; @@ -63,6 +66,28 @@ describe('ProvingJobSourceSchema', () => { }); }); +describe('ProvingJobResult', () => { + it('round-trips a message-only block-root rollup result through the schema', () => { + // The message-only block-root proof type must survive serialization: a checkpoint that builds a message-only + // block produces this result, and the proof store decodes it via the ProvingJobResult schema. Omitting it from + // the union throws "Invalid discriminator value" and stalls proving. + const result: ProvingJobResult = { + type: ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP, + result: makePublicInputsAndRecursiveProof< + BlockRollupPublicInputs, + typeof NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH + >( + makeBlockRollupPublicInputs(), + makeRecursiveProof(NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH), + VerificationKeyData.makeFakeRollupHonk(), + ), + }; + + const roundTripped = jsonParseWithSchema(jsonStringify(result), ProvingJobResult); + expect(roundTripped.type).toEqual(ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP); + }); +}); + class MockProvingJobSource implements ProvingJobSource { getProvingJob(): Promise { return Promise.resolve({ diff --git a/yarn-project/stdlib/src/interfaces/proving-job.ts b/yarn-project/stdlib/src/interfaces/proving-job.ts index 4517592f5d1c..349ed4642e57 100644 --- a/yarn-project/stdlib/src/interfaces/proving-job.ts +++ b/yarn-project/stdlib/src/interfaces/proving-job.ts @@ -277,6 +277,13 @@ export const ProvingJobResult = z.discriminatedUnion('type', [ NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH, ), }), + z.object({ + type: z.literal(ProvingRequestType.BLOCK_ROOT_MSGS_ONLY_ROLLUP), + result: schemaForPublicInputsAndRecursiveProof( + BlockRollupPublicInputs.schema, + NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH, + ), + }), z.object({ type: z.literal(ProvingRequestType.BLOCK_MERGE_ROLLUP), result: schemaForPublicInputsAndRecursiveProof( From b3f77b19f24927b8589be8ab3848b1d6688d0865 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Mon, 27 Jul 2026 19:38:20 -0300 Subject: [PATCH 5/5] fix(fast-inbox): bump the proving broker database schema version (A-1375) Persisted broker jobs are keyed by the numeric ProvingRequestType, and this branch inserts BLOCK_ROOT_MSGS_ONLY_ROLLUP into the middle of the enum, shifting every later value. A prover restarted in place over an existing data directory would otherwise dequeue a stale job and hand it to the wrong prover method; the version check already discards a database whose schema version does not match. --- .../src/proving_broker/proving_broker_database/persisted.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/prover-client/src/proving_broker/proving_broker_database/persisted.ts b/yarn-project/prover-client/src/proving_broker/proving_broker_database/persisted.ts index 3dd2686583ed..36d764c6ce44 100644 --- a/yarn-project/prover-client/src/proving_broker/proving_broker_database/persisted.ts +++ b/yarn-project/prover-client/src/proving_broker/proving_broker_database/persisted.ts @@ -27,7 +27,7 @@ import type { ProverBrokerConfig } from '../config.js'; import type { ProvingBrokerDatabase } from '../proving_broker_database.js'; class SingleEpochDatabase { - public static readonly SCHEMA_VERSION = 1; + public static readonly SCHEMA_VERSION = 2; private jobs: AztecAsyncMap; private jobResults: AztecAsyncMap;