Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions noir-projects/noir-protocol-circuits/Nargo.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -69,6 +70,10 @@ pub global VK_MERKLE_TREE: MerkleTree<VK_TREE_WIDTH> = {
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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" }
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod child_proof_vk_tests;
mod consecutive_block_rollups_tests;
mod msgs_only_tests;
mod rollup_structure_tests;

use crate::{
Expand All @@ -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,
};
Expand Down Expand Up @@ -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),
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
);
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,36 @@ 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,
previous_l1_to_l2: previous_state.l1_to_l2_message_tree,
// 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
Loading
Loading