Skip to content
Open
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
11 changes: 9 additions & 2 deletions l1-contracts/src/core/RollupCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {ProposeArgs} from "@aztec/core/libraries/rollup/ProposeLib.sol";
import {STFLib, GenesisState} from "@aztec/core/libraries/rollup/STFLib.sol";
import {StakingLib} from "@aztec/core/libraries/rollup/StakingLib.sol";
import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol";
import {Inbox} from "@aztec/core/messagebridge/Inbox.sol";
import {Inbox, INBOX_BUCKET_RING_SIZE} from "@aztec/core/messagebridge/Inbox.sol";
import {Outbox} from "@aztec/core/messagebridge/Outbox.sol";
import {ISlasher} from "@aztec/core/slashing/Slasher.sol";
import {GSE} from "@aztec/governance/GSE.sol";
Expand Down Expand Up @@ -621,7 +621,14 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali

IInbox inbox = IInbox(
address(
new Inbox(address(this), _feeAsset, _config.version, Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT, _config.inboxLag)
new Inbox(
address(this),
_feeAsset,
_config.version,
Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT,
_config.inboxLag,
INBOX_BUCKET_RING_SIZE
)
)
);

Expand Down
47 changes: 46 additions & 1 deletion l1-contracts/src/core/interfaces/messagebridge/IInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ interface IInbox {
struct InboxState {
// Rolling hash of all messages inserted into the inbox.
// Used by clients to check for consistency.
// TODO: remove once the streaming inbox (AZIP-22 Fast Inbox) flips on and clients rely on the
// consensus rolling hash tracked in the buckets instead.
bytes16 rollingHash;
// This value is not used much by the contract, but it is useful for synching the node faster
// as it can more easily figure out if it can just skip looking for events for a time period.
Expand All @@ -21,14 +23,43 @@ interface IInbox {
uint64 inProgress;
}

/**
* @notice Snapshot of the consensus rolling hash over the messages inserted into the Inbox, stored in a
* 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).
*/
struct InboxBucket {
// Rolling hash after the last message absorbed into this bucket. Each link is
// `sha256ToField(previousRollingHash || leaf)`; the genesis value is zero.
bytes32 rollingHash;
// Cumulative number of messages inserted into the Inbox up to and including this bucket.
uint64 totalMsgCount;
// L1 block timestamp at which this bucket was opened. Recency comparisons (message lag,
// censorship cutoff) are done in seconds against this value.
uint64 timestamp;
// Number of messages absorbed into this bucket, capped at the per-bucket maximum.
uint32 msgCount;
}

/**
* @notice Emitted when a message is sent
* @param checkpointNumber - The checkpoint number in which the message is included
* @param index - The index of the message in the L1 to L2 messages tree
* @param hash - The hash of the message
* @param rollingHash - The rolling hash of all messages inserted into the inbox
* @param inboxRollingHash - The consensus rolling hash (truncated sha256 chain) after this message
* @param bucketSeq - The sequence number of the bucket this message was absorbed into
*/
event MessageSent(uint256 indexed checkpointNumber, uint256 index, bytes32 indexed hash, bytes16 rollingHash);
event MessageSent(
uint256 indexed checkpointNumber,
uint256 index,
bytes32 indexed hash,
bytes16 rollingHash,
bytes32 inboxRollingHash,
uint256 bucketSeq
);

// docs:start:send_l1_to_l2_message
/**
Expand Down Expand Up @@ -68,4 +99,18 @@ interface IInbox {
function getTotalMessagesInserted() external view returns (uint64);

function getInProgress() external view returns (uint64);

/**
* @notice Returns the sequence number of the bucket currently accumulating messages
* @return The current bucket sequence number
*/
function getCurrentBucketSeq() external view returns (uint64);

/**
* @notice Returns the bucket with the given sequence number
* @dev Reverts if the bucket is ahead of the current one or has already been overwritten in the ring
* @param _seq - The bucket sequence number
* @return The bucket
*/
function getBucket(uint256 _seq) external view returns (InboxBucket memory);
}
1 change: 1 addition & 0 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ library Errors {
error Inbox__ContentTooLarge(bytes32 content); // 0x47452014
error Inbox__SecretHashTooLarge(bytes32 secretHash); // 0xecde7e2c
error Inbox__MustBuildBeforeConsume(); // 0xc4901999
error Inbox__BucketOutOfWindow(uint256 seq, uint256 current); // 0xfee255b7

// Outbox
error Outbox__Unauthorized(); // 0x2c9490c2
Expand Down
12 changes: 12 additions & 0 deletions l1-contracts/src/core/libraries/crypto/Hash.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,16 @@ library Hash {
function sha256ToField(bytes memory _data) internal pure returns (bytes32) {
return bytes32(bytes.concat(new bytes(1), bytes31(sha256(_data))));
}

/**
* @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.
* @param _rollingHash - The current rolling hash
* @param _leaf - The message leaf to absorb
* @return The updated rolling hash
*/
function accumulateInboxRollingHash(bytes32 _rollingHash, bytes32 _leaf) internal pure returns (bytes32) {
return sha256ToField(abi.encodePacked(_rollingHash, _leaf));
}
}
96 changes: 94 additions & 2 deletions l1-contracts/src/core/messagebridge/Inbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ import {FeeJuicePortal} from "@aztec/core/messagebridge/FeeJuicePortal.sol";
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
import {SafeCast} from "@oz/utils/math/SafeCast.sol";

// Number of buckets in the rolling-hash ring. Sized far beyond normal consumption lag (the censorship
// cutoff bounds it to roughly one build frame); outages longer than the ring are handled by overwrite
// protection on unconsumed buckets, not by growing the ring.
uint256 constant INBOX_BUCKET_RING_SIZE = 1024;

// Constructor floor for the bucket ring. The ring must cover the longest stall the chain recovers from on
// its own: the prune-and-repropose window of 64 checkpoints (2 epochs = 384 L1 blocks) at the natural cadence
// of one bucket per L1 block, so the buckets it must re-consume after a prune have not been overwritten. 384
// rounded up to the next power of two, kept at or below the production ring.
uint256 constant MIN_BUCKET_RING_SIZE = 512;

/**
* @title Inbox
* @author Aztec Labs
Expand All @@ -23,12 +34,19 @@ 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;

uint256 public immutable LAG;

uint256 public immutable BUCKET_RING_SIZE;

uint256 internal immutable HEIGHT;
uint256 internal immutable SIZE;
bytes32 internal immutable EMPTY_ROOT; // The root of an empty frontier tree
Expand All @@ -40,7 +58,20 @@ contract Inbox is IInbox {

InboxState internal state;

constructor(address _rollup, IERC20 _feeAsset, uint256 _version, uint256 _height, uint256 _lag) {
// Ring of rolling-hash buckets, keyed by `bucketSeq % BUCKET_RING_SIZE`. Inert for the legacy
// frontier-tree flow; consumed by the streaming inbox checks at `propose` (AZIP-22 Fast Inbox).
mapping(uint256 ringIndex => InboxBucket bucket) internal buckets;

uint64 internal currentBucketSeq;

constructor(
address _rollup,
IERC20 _feeAsset,
uint256 _version,
uint256 _height,
uint256 _lag,
uint256 _bucketRingSize
) {
ROLLUP = _rollup;
VERSION = _version;

Expand All @@ -50,10 +81,18 @@ contract Inbox is IInbox {
require(_lag > 0, "LAG TOO SMALL");
LAG = _lag;

require(_bucketRingSize >= MIN_BUCKET_RING_SIZE, "BUCKET RING TOO SMALL");
BUCKET_RING_SIZE = _bucketRingSize;

state = InboxState({
rollingHash: 0, totalMessagesInserted: 0, inProgress: SafeCast.toUint64(Constants.INITIAL_CHECKPOINT_NUMBER + LAG)
});

// Genesis bucket: a checkpoint consuming no messages references the same bucket as its parent, so the
// first checkpoint against an empty Inbox references this one and no base case leaks into `propose`.
buckets[0] =
InboxBucket({rollingHash: 0, totalMsgCount: 0, timestamp: SafeCast.toUint64(block.timestamp), msgCount: 0});

forest.initialize(_height);
EMPTY_ROOT = trees[type(uint256).max].root(forest, HEIGHT, SIZE);

Expand Down Expand Up @@ -122,11 +161,54 @@ contract Inbox is IInbox {
rollingHash: updatedRollingHash, totalMessagesInserted: totalMessagesInserted + 1, inProgress: inProgress
});

emit MessageSent(inProgress, index, leaf, updatedRollingHash);
(uint64 bucketSeq, bytes32 inboxRollingHash) = _absorbIntoBucket(leaf);

emit MessageSent(inProgress, index, leaf, updatedRollingHash, inboxRollingHash, bucketSeq);

return (leaf, index);
}

/**
* @notice Absorbs a message leaf into the consensus rolling hash and snapshots it into the bucket ring
*
* @dev A bucket only holds messages from a single L1 block, up to MAX_MSGS_PER_BUCKET; the first message
* of a new L1 block — or the message after a full bucket, spilling over within the same block — opens the
* next bucket, inheriting the rolling hash and cumulative count. Bucket 0 is the pristine genesis base
* case and never absorbs. Opening a bucket overwrites the ring entry from BUCKET_RING_SIZE buckets ago;
* protection against overwriting unconsumed buckets is not enforced yet.
*
* @param _leaf - The message leaf to absorb
*
* @return The sequence number of the bucket the leaf was absorbed into and the updated rolling hash
*/
function _absorbIntoBucket(bytes32 _leaf) internal returns (uint64, bytes32) {
uint64 bucketSeq = currentBucketSeq;
InboxBucket memory bucket = buckets[bucketSeq % BUCKET_RING_SIZE];

// Buckets are keyed by L1 block timestamp: a strictly larger timestamp opens a new bucket (a full bucket
// also rolls over within the same block). Post-merge Ethereum increases block.timestamp strictly per block,
// so messages from different L1 blocks always land in different buckets. Under anvil with manual mining two
// blocks can share a timestamp and therefore a bucket; this is harmless because the consumption cutoff is
// computed over timestamps, so co-timestamped blocks are indistinguishable to it.
if (bucketSeq == 0 || bucket.timestamp < block.timestamp || bucket.msgCount == MAX_MSGS_PER_BUCKET) {
bucketSeq += 1;
currentBucketSeq = bucketSeq;
bucket = InboxBucket({
rollingHash: bucket.rollingHash,
totalMsgCount: bucket.totalMsgCount,
timestamp: SafeCast.toUint64(block.timestamp),
msgCount: 0
});
}

bucket.rollingHash = Hash.accumulateInboxRollingHash(bucket.rollingHash, _leaf);
bucket.totalMsgCount += 1;
bucket.msgCount += 1;
buckets[bucketSeq % BUCKET_RING_SIZE] = bucket;

return (bucketSeq, bucket.rollingHash);
}

/**
* @notice Consumes the current tree, and starts a new one if needed
*
Expand Down Expand Up @@ -177,4 +259,14 @@ contract Inbox is IInbox {
function getInProgress() external view override(IInbox) returns (uint64) {
return state.inProgress;
}

function getCurrentBucketSeq() external view override(IInbox) returns (uint64) {
return currentBucketSeq;
}

function getBucket(uint256 _seq) external view override(IInbox) returns (InboxBucket memory) {
uint256 current = currentBucketSeq;
require(_seq <= current && current - _seq < BUCKET_RING_SIZE, Errors.Inbox__BucketOutOfWindow(_seq, current));
return buckets[_seq % BUCKET_RING_SIZE];
}
}
9 changes: 7 additions & 2 deletions l1-contracts/test/Inbox.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ contract InboxTest is Test {
function setUp() public {
address rollup = address(this);
IERC20 feeAsset = new TestERC20("Fee Asset", "FA", address(this));
inbox = new InboxHarness(rollup, feeAsset, version, HEIGHT, TestConstants.AZTEC_INBOX_LAG);
inbox = new InboxHarness(
rollup, feeAsset, version, HEIGHT, TestConstants.AZTEC_INBOX_LAG, TestConstants.AZTEC_INBOX_BUCKET_RING_SIZE
);
emptyTreeRoot = inbox.getEmptyRoot();
}

Expand Down Expand Up @@ -96,9 +98,12 @@ contract InboxTest is Test {

bytes32 leaf = message.sha256ToField();
bytes16 expectedRollingHash = bytes16(keccak256(abi.encodePacked(stateBefore.rollingHash, leaf)));
bytes32 expectedInboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), leaf);
vm.expectEmit(true, true, true, true);
// event we expect
emit IInbox.MessageSent(FIRST_REAL_TREE_NUM, globalLeafIndex, leaf, expectedRollingHash);
emit IInbox.MessageSent(
FIRST_REAL_TREE_NUM, globalLeafIndex, leaf, expectedRollingHash, expectedInboxRollingHash, 1
);
// event we will get
(bytes32 insertedLeaf, uint256 insertedIndex) =
inbox.sendL2Message(message.recipient, message.content, message.secretHash);
Expand Down
Loading
Loading