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
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use std::meta::derive;
use types::{
hash::poseidon2_absorb_in_chunks_existing_sponge,
poseidon2::Poseidon2Sponge,
traits::{Deserialize, Empty, Serialize},
};

/// An absorb-only Poseidon2 sponge over L1-to-L2 message leaves.
///
/// It threads across the blocks of a checkpoint the way `SpongeBlob` threads tx effects: each block absorbs its message
/// bundle into the sponge it inherited from the previous block, and the checkpoint root recomputes the sponge over the
/// flat concatenation of every block's messages and asserts the final states are equal. There are no block separators
/// or markers in the absorbed stream, so the recomputation is identical regardless of how messages were split across
/// blocks. The sponge is never squeezed on the block path — equality of the accumulated state is the only check.
#[derive(Deserialize, Eq, Serialize)]
pub struct L1ToL2MessageSponge {
pub sponge: Poseidon2Sponge,
/// The number of message leaves absorbed so far.
pub num_absorbed: u32,
}

impl L1ToL2MessageSponge {
pub fn new() -> Self {
Self { sponge: Poseidon2Sponge::new(0), num_absorbed: 0 }
}

/// Absorb the first `num` leaves of `leaves` in order. Lanes beyond `num` are ignored.
pub fn absorb<let N: u32>(&mut self, leaves: [Field; N], num: u32) {
self.sponge = poseidon2_absorb_in_chunks_existing_sponge(self.sponge, leaves, num);
self.num_absorbed += num;
}

/// Finalize the sponge and output the (non-standard) Poseidon2 hash of all absorbed leaves.
pub fn squeeze(&mut self) -> Field {
self.sponge.squeeze()
}
}

impl Empty for L1ToL2MessageSponge {
fn empty() -> Self {
Self { sponge: Poseidon2Sponge::new(0), num_absorbed: 0 }
}
}

mod tests {
use super::L1ToL2MessageSponge;
use types::{poseidon2::Poseidon2Sponge, traits::Empty};

#[test]
fn new_equals_empty() {
assert_eq(L1ToL2MessageSponge::new(), L1ToL2MessageSponge::empty());
assert_eq(L1ToL2MessageSponge::new().num_absorbed, 0);
}

#[test]
fn absorb_tracks_count() {
let mut sponge = L1ToL2MessageSponge::new();
sponge.absorb([11, 22, 33, 0, 0], 3);
assert_eq(sponge.num_absorbed, 3);
sponge.absorb([44, 55], 2);
assert_eq(sponge.num_absorbed, 5);
}

#[test]
fn absorb_ignores_lanes_past_count() {
// Lanes beyond `num` must not affect the accumulated state.
let mut with_padding = L1ToL2MessageSponge::new();
with_padding.absorb([11, 22, 33, 999, 888], 3);

let mut exact = L1ToL2MessageSponge::new();
exact.absorb([11, 22, 33], 3);

assert_eq(with_padding, exact);
}

#[test]
fn absorb_equality_across_split_boundaries() {
// Absorbing [11,22,33,44,55] in one call equals absorbing [11,22,33] then [44,55] across two threaded sponges.
let mut single = L1ToL2MessageSponge::new();
single.absorb([11, 22, 33, 44, 55], 5);

let mut threaded = L1ToL2MessageSponge::new();
threaded.absorb([11, 22, 33, 44, 55], 3);
threaded.absorb([44, 55, 0, 0, 0], 2);

assert_eq(threaded, single);
assert_eq(threaded.num_absorbed, 5);

let mut single_squeeze = single;
let mut threaded_squeeze = threaded;
assert_eq(threaded_squeeze.squeeze(), single_squeeze.squeeze());
}

#[test]
fn empty_bundle_is_noop() {
let mut sponge = L1ToL2MessageSponge::new();
sponge.absorb([0; 5], 0);
assert_eq(sponge, L1ToL2MessageSponge::empty());
}

#[test]
fn matches_raw_poseidon2_sponge() {
// The accumulated state must be a plain absorb-only Poseidon2 sponge with iv = 0.
let mut sponge = L1ToL2MessageSponge::new();
sponge.absorb([11, 22, 33, 44, 55], 5);

let mut expected = Poseidon2Sponge::new(0);
expected.absorb(11);
expected.absorb(22);
expected.absorb(33);
expected.absorb(44);
expected.absorb(55);

assert_eq(sponge.sponge, expected);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ mod block_rollup_public_inputs;
mod parity_public_inputs;
mod checkpoint_rollup_public_inputs;
mod root_rollup_public_inputs;
mod l1_to_l2_message_sponge;

pub use block_rollup_public_inputs::BlockRollupPublicInputs;
pub use checkpoint_rollup_public_inputs::CheckpointRollupPublicInputs;
pub use l1_to_l2_message_sponge::L1ToL2MessageSponge;
pub use parity_public_inputs::ParityPublicInputs;
pub use public_chonk_verifier_public_inputs::PublicChonkVerifierPublicInputs;
pub use root_rollup_public_inputs::RootRollupPublicInputs;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use types::hash::accumulate_sha256;

/// Extends the Inbox rolling sha256 chain by the first `num_leaves` of `leaves`, returning the new rolling hash.
///
/// Each link is `h' = sha256ToField(h || leaf)` over the two 32-byte big-endian values, i.e. exactly one
/// `accumulate_sha256` step, matching the truncated-to-field sha256 the L1 `Inbox` accumulates and today's `inHash`
/// frontier tree uses at every node. The genesis rolling hash is zero.
///
/// The function takes a `start` and returns an end with no assumption about chunk position, so it chains sequentially
/// across chunked circuits: a segment's start hash is the previous segment's end. Lanes beyond `num_leaves` are never
/// absorbed — the Inbox chain only ever sees real messages.
pub fn accumulate_inbox_rolling_hash<let N: u32>(
start: Field,
leaves: [Field; N],
num_leaves: u32,
) -> Field {
assert(num_leaves <= N, "num_leaves is greater than the leaves array length");

let mut acc = start;
for i in 0..N {
if i < num_leaves {
acc = accumulate_sha256(acc, leaves[i]);
}
}
acc
}

mod tests {
use super::accumulate_inbox_rolling_hash;
use types::{constants::NUM_MSGS_PER_BASE_PARITY, hash::accumulate_sha256};

#[test]
fn empty_bundle_passes_start_through() {
assert_eq(accumulate_inbox_rolling_hash(0, [0; 8], 0), 0);
// A non-zero start with no leaves is returned unchanged (segment threading base case).
assert_eq(accumulate_inbox_rolling_hash(0x2a, [11, 22, 33], 0), 0x2a);
}

#[test]
fn single_leaf_matches_reference() {
// Independent sha256 reference (see PR description): sha256ToField(0x00..00 || 0x00..0b), last byte dropped.
let expected = 0x00815fb1e9d2076ae5761439b6144ad11da69eb6c41ab2aca39e770407ad8d12;
assert_eq(accumulate_inbox_rolling_hash(0, [11, 0, 0, 0], 1), expected);
assert_eq(accumulate_sha256(0, 11), expected);
}

#[test]
fn three_leaves_matches_reference() {
let expected = 0x0014cae968461979aab6d33266a2310ed234d3f6cf4472737c57551db07bd0da;
assert_eq(accumulate_inbox_rolling_hash(0, [11, 22, 33, 0, 0], 3), expected);
}

#[test]
fn base_parity_batch_matches_reference() {
// NUM_MSGS_PER_BASE_PARITY leaves valued 1..=256 from a zero start.
let mut leaves = [0; NUM_MSGS_PER_BASE_PARITY];
for i in 0..NUM_MSGS_PER_BASE_PARITY {
leaves[i] = (i + 1) as Field;
}
let expected = 0x00ea95b96f17b75be03525b35a2a1918b42f03ad8c00a437cf641751825f3992;
assert_eq(accumulate_inbox_rolling_hash(0, leaves, NUM_MSGS_PER_BASE_PARITY), expected);
}

#[test]
fn non_zero_start_matches_reference() {
let expected = 0x0054d96b8a074a5030a5838972d0a3c04ba47cf5956348c853e02e9566233f65;
assert_eq(accumulate_inbox_rolling_hash(0x2a, [7, 8], 2), expected);
}

#[test]
fn chain_is_continuous_across_segments() {
// Chaining [7,8,9] from a start equals chaining [7] then [8,9] threaded by the intermediate hash.
let start = 0x2a;
let full = accumulate_inbox_rolling_hash(start, [7, 8, 9, 0, 0], 3);

let mid = accumulate_inbox_rolling_hash(start, [7, 0], 1);
let split = accumulate_inbox_rolling_hash(mid, [8, 9], 2);

assert_eq(full, split);
}

#[test]
fn padding_lanes_are_not_absorbed() {
// A padded array with num_leaves set correctly matches the exact-length chain regardless of lane contents.
let padded = accumulate_inbox_rolling_hash(0, [11, 22, 999, 888], 2);
let exact = accumulate_inbox_rolling_hash(0, [11, 22], 2);
assert_eq(padded, exact);
}

#[test(should_fail_with = "num_leaves is greater than the leaves array length")]
fn num_leaves_past_array_fails() {
let _ = accumulate_inbox_rolling_hash(0, [11, 22, 33], 4);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub(crate) mod abis;
pub(crate) mod tests;

pub mod inbox_rolling_hash;
pub mod tx_base;
pub mod tx_merge;
pub mod block_root;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ pub global NULLIFIER_SUBTREE_ROOT_SIBLING_PATH_LENGTH: u32 =
NULLIFIER_TREE_HEIGHT - NULLIFIER_SUBTREE_HEIGHT;
pub global L1_TO_L2_MSG_SUBTREE_ROOT_SIBLING_PATH_LENGTH: u32 =
L1_TO_L2_MSG_TREE_HEIGHT - L1_TO_L2_MSG_SUBTREE_HEIGHT;
// Cap on L1-to-L2 messages bundled into a single L2 block. Transitional value equal to the per-checkpoint cap: the
// constant is deliberately oversized so the first block's bundle can carry a whole checkpoint's padded messages during
// the transition, and drops to 256 at the flip.
pub global MAX_L1_TO_L2_MSGS_PER_BLOCK: u32 = 1024;
// Cap on L1-to-L2 messages consumed by a single checkpoint. Semantically today's NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP.
pub global MAX_L1_TO_L2_MSGS_PER_CHECKPOINT: u32 = 1024;
// Maximum number of subtrees a L2ToL1Msg unbalanced tree can have. Used when calculating the out hash of a tx.
pub global MAX_L2_TO_L1_MSG_SUBTREES_PER_TX: u32 = 3; // ceil(log2(MAX_L2_TO_L1_MSGS_PER_TX))

Expand Down
Loading
Loading