Skip to content
10 changes: 0 additions & 10 deletions .test_patterns.yml
Original file line number Diff line number Diff line change
Expand Up @@ -378,16 +378,6 @@ tests:
owners:
- *alex

# multi_proof runs three prover nodes; with fake proofs the orchestrator still does circuit witness
# generation, and the per-block-bundle change from this point makes constrained parity (in_hash) hashing
# heavier. Under CI's 2-CPU limit that extra witgen starves the sequencer's L1 publish past its tx timeout,
# so the test is transitionally slow here. It passes on adequate hardware, and a later stack change that
# turns the in_hash parity into an unconstrained hint restores the margin — this skip is removed there.
- regex: "yarn-project/end-to-end/scripts/run_test.sh simple src/single-node/proving/multi_proof.test.ts"
skip: true
owners:
- *palla

- regex: "yarn-project/end-to-end/scripts/run_test.sh compose src/composed/docs_examples.test.ts"
error_regex: "maxFeesPerGas.feePerL2Gas must be greater than or equal to gasFees.feePerL2Gas,"
owners:
Expand Down
7 changes: 6 additions & 1 deletion l1-contracts/src/core/interfaces/messagebridge/IInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ pragma solidity >=0.8.27;

import {DataStructures} from "../../libraries/DataStructures.sol";

// Maximum number of messages a single bucket can hold before further messages in the same L1 block spill over
// into the next bucket. Matches the number of L1 to L2 messages a single L2 block can insert once the streaming
// inbox is live, so any one bucket is always consumable by one block.
uint256 constant MAX_MSGS_PER_BUCKET = 256;

/**
* @title Inbox
* @author Aztec Labs
Expand All @@ -28,7 +33,7 @@ interface IInbox {
* fixed-size ring indexed by a dense bucket sequence number (`seq % ringSize`). A bucket only accumulates
* messages sent within a single L1 block, so its final state is the chain position as of the end of that
* block; the censorship check at `propose` compares the checkpoint header's rolling hash against these
* snapshots (AZIP-22 Fast Inbox).
* snapshots.
*/
struct InboxBucket {
// Rolling hash after the last message absorbed into this bucket. Each link is
Expand Down
5 changes: 5 additions & 0 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ library Errors {
error Rollup__InvalidCheckpointHeaderCount(uint256 expected, uint256 actual);
error Rollup__InvalidCheckpointNumber(uint256 expected, uint256 actual); // 0xd1ba9bfa
error Rollup__InvalidInHash(bytes32 expected, bytes32 actual); // 0xcd6f4233
error Rollup__InvalidInboxRollingHash(bytes32 expected, bytes32 actual); // 0xed1f7bb5
error Rollup__InboxBucketStillMutable(uint256 bucketSeq); // 0x7254b980
error Rollup__UnconsumedInboxMessages(uint256 nextBucketSeq); // 0x2bd4bf10
error Rollup__InboxConsumptionBehindParent(uint256 expected, uint256 actual); // 0x54e0c025
error Rollup__TooManyInboxMessagesConsumed(uint256 consumed); // 0xf76d1426
error Rollup__InvalidOutHash(bytes32 expected, bytes32 actual); // 0x8eb39062
error Rollup__InvalidPreviousArchive(bytes32 expected, bytes32 actual); // 0xb682a40e
error Rollup__InvalidProof(); // 0xa5b2ba17
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/src/core/libraries/crypto/Hash.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ library Hash {
/**
* @notice Advances the Inbox consensus rolling hash by one message leaf
* @dev Truncated at every link so the value is always a field element; the rollup circuits recompute the
* identical chain over the message leaves they insert (AZIP-22 Fast Inbox). The genesis value is zero.
* identical chain over the message leaves they insert. The genesis value is zero.
* @param _rollingHash - The current rolling hash
* @param _leaf - The message leaf to absorb
* @return The updated rolling hash
Expand Down
94 changes: 94 additions & 0 deletions l1-contracts/src/core/libraries/rollup/ProposeLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity >=0.8.27;
import {BlobLib} from "@aztec-blob-lib/BlobLib.sol";
import {IEscapeHatch} from "@aztec/core/interfaces/IEscapeHatch.sol";
import {RollupStore, IRollupCore, CheckpointHeaderValidationFlags} from "@aztec/core/interfaces/IRollup.sol";
import {IInbox, MAX_MSGS_PER_BUCKET} from "@aztec/core/interfaces/messagebridge/IInbox.sol";
import {TempCheckpointLog} from "@aztec/core/libraries/compressed-data/CheckpointLog.sol";
import {FeeHeader} from "@aztec/core/libraries/compressed-data/fees/FeeStructs.sol";
import {ChainTipsLib, CompressedChainTips} from "@aztec/core/libraries/compressed-data/Tips.sol";
Expand All @@ -20,6 +21,14 @@ import {Signature} from "@aztec/shared/libraries/SignatureLib.sol";
import {ProposedHeader, ProposedHeaderLib} from "./ProposedHeaderLib.sol";
import {STFLib} from "./STFLib.sol";

// Streaming-inbox protocol constants (AZIP-22 Fast Inbox). These mirror the protocol circuit constants and
// should move into the generated Constants library once the Solidity emitter includes them.
// Minimum bucket age, in seconds, at the start of a checkpoint's build frame for its consumption to be
// mandatory. One L1 slot: validators cannot be required to act on buckets they may not have seen.
uint256 constant INBOX_LAG_SECONDS = 12;
// Maximum number of L1 to L2 messages a single checkpoint can insert.
uint256 constant MAX_L1_TO_L2_MSGS_PER_CHECKPOINT = 1024;

struct ProposeArgs {
bytes32 archive;
OracleInput oracleInput;
Expand Down Expand Up @@ -370,6 +379,91 @@ library ProposeLib {
);
}

/**
* @notice Validates a checkpoint's Inbox consumption against the streaming inbox buckets and returns how
* far consumption has reached. Not yet called from propose(): the legacy `consume()`/`inHash` flow
* remains the enforced path until the streaming inbox (AZIP-22 Fast Inbox) flips on.
*
* @dev Read-only; performs no Inbox write. Checks, in order:
* 1. The checkpoint header's `inboxRollingHash` must equal the rolling hash snapshotted in the Inbox
* bucket referenced by `_bucketHint`. The hint is a plain calldata lookup aid, not signed and not
* part of the header: a wrong hint cannot change what gets accepted, it only reverts. A checkpoint
* that consumes no messages references the same bucket as its parent.
* 2. The referenced bucket must be settled: a bucket that can still absorb another message is not a
* snapshot of anything.
* 3. Consumption moves forward: the referenced bucket's cumulative total must be at least the parent
* checkpoint's (equal consumes nothing; behind is a hard revert). This precedes the subtractions
* below, which rely on `bucket.totalMsgCount >= _parentTotalMsgCount` to not underflow.
* 4. Cap upper bound: a single checkpoint cannot consume more than MAX_L1_TO_L2_MSGS_PER_CHECKPOINT
* messages, the maximum the circuits can insert.
* 5. Mandatory consumption (the censorship assert): the first unconsumed bucket (`_bucketHint + 1`)
* must either not exist, sit past the consumption cutoff, or be cap-escaped — consuming through it
* would exceed MAX_L1_TO_L2_MSGS_PER_CHECKPOINT messages since the parent checkpoint's cumulative
* total. The cutoff is the start of the checkpoint's build frame minus INBOX_LAG_SECONDS: a
* checkpoint proposed in slot S is built during slot S-1, and validators are not required to have
* seen buckets younger than one L1 slot at build start.
*
* No consumed-bucket pointer is written here. The caller (FI-14) stores the returned consumed
* position in the checkpoint's temp-log record, which is the authoritative consumed total: temp logs
* rewind with the pending chain on a prune, so the record stays prune-consistent — unlike an
* Inbox-side pointer advanced with the pending chain, which would sit ahead of the replacement chain.
*
* @param _inbox - The Inbox holding the rolling-hash buckets
* @param _inboxRollingHash - The checkpoint header's inbox rolling hash
* @param _bucketHint - Sequence number of the bucket the header's rolling hash corresponds to
* @param _slotNumber - The slot the checkpoint is proposed in
* @param _parentTotalMsgCount - Cumulative Inbox message count consumed as of the parent checkpoint
* @return The cumulative Inbox message count consumed as of this checkpoint (`bucket.totalMsgCount`), for
* the caller to store in the checkpoint's temp-log record
*/
function validateInboxConsumption(
IInbox _inbox,
bytes32 _inboxRollingHash,
uint256 _bucketHint,
Slot _slotNumber,
uint256 _parentTotalMsgCount
) internal view returns (uint256) {
IInbox.InboxBucket memory bucket = _inbox.getBucket(_bucketHint);
require(
bucket.rollingHash == _inboxRollingHash,
Errors.Rollup__InvalidInboxRollingHash(bucket.rollingHash, _inboxRollingHash)
);

// A bucket that can still absorb a message mutates in place: a proposer bundling a send after its own propose
// in one L1 transaction would leave the checkpoint committed to a rolling hash that exists neither on L1 nor
// in any node, which only ever observes a bucket's end-of-block state, and no honest node could then resolve
// the consumed position. Settled is the negation of the Inbox's rollover condition: the genesis bucket never
// absorbs, a bucket whose L1 block has passed cannot be reopened, and a full bucket spills the next message
// into a new one.
require(
_bucketHint == 0 || bucket.timestamp < block.timestamp || bucket.msgCount == MAX_MSGS_PER_BUCKET,
Errors.Rollup__InboxBucketStillMutable(_bucketHint)
);

require(
bucket.totalMsgCount >= _parentTotalMsgCount,
Errors.Rollup__InboxConsumptionBehindParent(_parentTotalMsgCount, bucket.totalMsgCount)
);

require(
bucket.totalMsgCount - _parentTotalMsgCount <= MAX_L1_TO_L2_MSGS_PER_CHECKPOINT,
Errors.Rollup__TooManyInboxMessagesConsumed(bucket.totalMsgCount - _parentTotalMsgCount)
);

if (_bucketHint < _inbox.getCurrentBucketSeq()) {
IInbox.InboxBucket memory next = _inbox.getBucket(_bucketHint + 1);
Timestamp buildFrameStart = TimeLib.toTimestamp(_slotNumber - Slot.wrap(1));
Timestamp cutoff = buildFrameStart - Timestamp.wrap(INBOX_LAG_SECONDS);
require(
next.timestamp > Timestamp.unwrap(cutoff)
|| next.totalMsgCount - _parentTotalMsgCount > MAX_L1_TO_L2_MSGS_PER_CHECKPOINT,
Errors.Rollup__UnconsumedInboxMessages(_bucketHint + 1)
);
}

return bucket.totalMsgCount;
}

/**
* @notice Gets the mana min fee components
* For more context, consult:
Expand Down
7 changes: 1 addition & 6 deletions l1-contracts/src/core/messagebridge/Inbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pragma solidity >=0.8.27;

import {IRollup} from "@aztec/core/interfaces/IRollup.sol";
import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol";
import {IInbox, MAX_MSGS_PER_BUCKET} from "@aztec/core/interfaces/messagebridge/IInbox.sol";
import {Constants} from "@aztec/core/libraries/ConstantsGen.sol";
import {FrontierLib} from "@aztec/core/libraries/crypto/FrontierLib.sol";
import {Hash} from "@aztec/core/libraries/crypto/Hash.sol";
Expand Down Expand Up @@ -34,11 +34,6 @@ contract Inbox is IInbox {
using FrontierLib for FrontierLib.Forest;
using FrontierLib for FrontierLib.Tree;

// Maximum number of messages a single bucket can hold before further messages in the same L1 block
// spill over into the next bucket. Matches the number of L1 to L2 messages a single L2 block can
// insert once the streaming inbox is live, so any one bucket is always consumable by one block.
uint256 public constant MAX_MSGS_PER_BUCKET = 256;

address public immutable ROLLUP;
uint256 public immutable VERSION;
address public immutable FEE_ASSET_PORTAL;
Expand Down
6 changes: 3 additions & 3 deletions l1-contracts/test/InboxBuckets.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pragma solidity >=0.8.27;
import {Test} from "forge-std/Test.sol";
import {TestERC20} from "src/mock/TestERC20.sol";
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol";
import {IInbox, MAX_MSGS_PER_BUCKET} from "@aztec/core/interfaces/messagebridge/IInbox.sol";
import {MIN_BUCKET_RING_SIZE} from "@aztec/core/messagebridge/Inbox.sol";
import {InboxHarness} from "./harnesses/InboxHarness.sol";
import {TestConstants} from "./harnesses/TestConstants.sol";
Expand Down Expand Up @@ -172,7 +172,7 @@ contract InboxBucketsTest is Test {
}

function testRolloverIntoNextBucket() public {
uint256 cap = inbox.MAX_MSGS_PER_BUCKET();
uint256 cap = MAX_MSGS_PER_BUCKET;
for (uint256 i = 0; i < cap; i++) {
_send(inbox, i);
}
Expand Down Expand Up @@ -266,7 +266,7 @@ contract InboxBucketsTest is Test {
// Gas cost of a rollover opening mid-block: once a bucket reaches MAX_MSGS_PER_BUCKET, the next
// message in the same L1 block opens a new bucket even though the timestamp is unchanged.
function testGasSendRolloverMidBlock() public {
uint256 cap = inbox.MAX_MSGS_PER_BUCKET();
uint256 cap = MAX_MSGS_PER_BUCKET;
for (uint256 i = 0; i < cap; i++) {
_send(inbox, i);
}
Expand Down
Loading
Loading