Skip to content

Commit 630c509

Browse files
committed
feat: rolling-hash buckets in the Inbox alongside frontier trees
1 parent c18b69c commit 630c509

13 files changed

Lines changed: 401 additions & 13 deletions

File tree

l1-contracts/src/core/RollupCore.sol

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {ProposeArgs} from "@aztec/core/libraries/rollup/ProposeLib.sol";
2727
import {STFLib, GenesisState} from "@aztec/core/libraries/rollup/STFLib.sol";
2828
import {StakingLib} from "@aztec/core/libraries/rollup/StakingLib.sol";
2929
import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol";
30-
import {Inbox} from "@aztec/core/messagebridge/Inbox.sol";
30+
import {Inbox, INBOX_BUCKET_RING_SIZE} from "@aztec/core/messagebridge/Inbox.sol";
3131
import {Outbox} from "@aztec/core/messagebridge/Outbox.sol";
3232
import {ISlasher} from "@aztec/core/slashing/Slasher.sol";
3333
import {GSE} from "@aztec/governance/GSE.sol";
@@ -621,7 +621,14 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali
621621

622622
IInbox inbox = IInbox(
623623
address(
624-
new Inbox(address(this), _feeAsset, _config.version, Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT, _config.inboxLag)
624+
new Inbox(
625+
address(this),
626+
_feeAsset,
627+
_config.version,
628+
Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT,
629+
_config.inboxLag,
630+
INBOX_BUCKET_RING_SIZE
631+
)
625632
)
626633
);
627634

l1-contracts/src/core/interfaces/messagebridge/IInbox.sol

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ interface IInbox {
1313
struct InboxState {
1414
// Rolling hash of all messages inserted into the inbox.
1515
// Used by clients to check for consistency.
16+
// TODO: remove once the streaming inbox (AZIP-22 Fast Inbox) flips on and clients rely on the
17+
// consensus rolling hash tracked in the buckets instead.
1618
bytes16 rollingHash;
1719
// This value is not used much by the contract, but it is useful for synching the node faster
1820
// as it can more easily figure out if it can just skip looking for events for a time period.
@@ -21,14 +23,43 @@ interface IInbox {
2123
uint64 inProgress;
2224
}
2325

26+
/**
27+
* @notice Snapshot of the consensus rolling hash over the messages inserted into the Inbox, stored in a
28+
* fixed-size ring indexed by a dense bucket sequence number (`seq % ringSize`). A bucket only accumulates
29+
* messages sent within a single L1 block, so its final state is the chain position as of the end of that
30+
* block; the censorship check at `propose` compares the checkpoint header's rolling hash against these
31+
* snapshots (AZIP-22 Fast Inbox).
32+
*/
33+
struct InboxBucket {
34+
// Rolling hash after the last message absorbed into this bucket. Each link is
35+
// `sha256ToField(previousRollingHash || leaf)`; the genesis value is zero.
36+
bytes32 rollingHash;
37+
// Cumulative number of messages inserted into the Inbox up to and including this bucket.
38+
uint64 totalMsgCount;
39+
// L1 block timestamp at which this bucket was opened. Recency comparisons (message lag,
40+
// censorship cutoff) are done in seconds against this value.
41+
uint64 timestamp;
42+
// Number of messages absorbed into this bucket, capped at the per-bucket maximum.
43+
uint32 msgCount;
44+
}
45+
2446
/**
2547
* @notice Emitted when a message is sent
2648
* @param checkpointNumber - The checkpoint number in which the message is included
2749
* @param index - The index of the message in the L1 to L2 messages tree
2850
* @param hash - The hash of the message
2951
* @param rollingHash - The rolling hash of all messages inserted into the inbox
52+
* @param inboxRollingHash - The consensus rolling hash (truncated sha256 chain) after this message
53+
* @param bucketSeq - The sequence number of the bucket this message was absorbed into
3054
*/
31-
event MessageSent(uint256 indexed checkpointNumber, uint256 index, bytes32 indexed hash, bytes16 rollingHash);
55+
event MessageSent(
56+
uint256 indexed checkpointNumber,
57+
uint256 index,
58+
bytes32 indexed hash,
59+
bytes16 rollingHash,
60+
bytes32 inboxRollingHash,
61+
uint256 bucketSeq
62+
);
3263

3364
// docs:start:send_l1_to_l2_message
3465
/**
@@ -68,4 +99,18 @@ interface IInbox {
6899
function getTotalMessagesInserted() external view returns (uint64);
69100

70101
function getInProgress() external view returns (uint64);
102+
103+
/**
104+
* @notice Returns the sequence number of the bucket currently accumulating messages
105+
* @return The current bucket sequence number
106+
*/
107+
function getCurrentBucketSeq() external view returns (uint64);
108+
109+
/**
110+
* @notice Returns the bucket with the given sequence number
111+
* @dev Reverts if the bucket is ahead of the current one or has already been overwritten in the ring
112+
* @param _seq - The bucket sequence number
113+
* @return The bucket
114+
*/
115+
function getBucket(uint256 _seq) external view returns (InboxBucket memory);
71116
}

l1-contracts/src/core/libraries/Errors.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ library Errors {
2828
error Inbox__ContentTooLarge(bytes32 content); // 0x47452014
2929
error Inbox__SecretHashTooLarge(bytes32 secretHash); // 0xecde7e2c
3030
error Inbox__MustBuildBeforeConsume(); // 0xc4901999
31+
error Inbox__BucketOutOfWindow(uint256 seq, uint256 current); // 0xfee255b7
3132

3233
// Outbox
3334
error Outbox__Unauthorized(); // 0x2c9490c2

l1-contracts/src/core/libraries/crypto/Hash.sol

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,16 @@ library Hash {
4949
function sha256ToField(bytes memory _data) internal pure returns (bytes32) {
5050
return bytes32(bytes.concat(new bytes(1), bytes31(sha256(_data))));
5151
}
52+
53+
/**
54+
* @notice Advances the Inbox consensus rolling hash by one message leaf
55+
* @dev Truncated at every link so the value is always a field element; the rollup circuits recompute the
56+
* identical chain over the message leaves they insert (AZIP-22 Fast Inbox). The genesis value is zero.
57+
* @param _rollingHash - The current rolling hash
58+
* @param _leaf - The message leaf to absorb
59+
* @return The updated rolling hash
60+
*/
61+
function accumulateInboxRollingHash(bytes32 _rollingHash, bytes32 _leaf) internal pure returns (bytes32) {
62+
return sha256ToField(abi.encodePacked(_rollingHash, _leaf));
63+
}
5264
}

l1-contracts/src/core/messagebridge/Inbox.sol

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import {FeeJuicePortal} from "@aztec/core/messagebridge/FeeJuicePortal.sol";
1313
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
1414
import {SafeCast} from "@oz/utils/math/SafeCast.sol";
1515

16+
// Number of buckets in the rolling-hash ring. Sized far beyond normal consumption lag (the censorship
17+
// cutoff bounds it to roughly one build frame); outages longer than the ring are handled by overwrite
18+
// protection on unconsumed buckets, not by growing the ring.
19+
uint256 constant INBOX_BUCKET_RING_SIZE = 1024;
20+
1621
/**
1722
* @title Inbox
1823
* @author Aztec Labs
@@ -23,12 +28,19 @@ contract Inbox is IInbox {
2328
using FrontierLib for FrontierLib.Forest;
2429
using FrontierLib for FrontierLib.Tree;
2530

31+
// Maximum number of messages a single bucket can hold before further messages in the same L1 block
32+
// spill over into the next bucket. Matches the number of L1 to L2 messages a single L2 block can
33+
// insert once the streaming inbox is live, so any one bucket is always consumable by one block.
34+
uint256 public constant MAX_MSGS_PER_BUCKET = 256;
35+
2636
address public immutable ROLLUP;
2737
uint256 public immutable VERSION;
2838
address public immutable FEE_ASSET_PORTAL;
2939

3040
uint256 public immutable LAG;
3141

42+
uint256 public immutable BUCKET_RING_SIZE;
43+
3244
uint256 internal immutable HEIGHT;
3345
uint256 internal immutable SIZE;
3446
bytes32 internal immutable EMPTY_ROOT; // The root of an empty frontier tree
@@ -40,7 +52,20 @@ contract Inbox is IInbox {
4052

4153
InboxState internal state;
4254

43-
constructor(address _rollup, IERC20 _feeAsset, uint256 _version, uint256 _height, uint256 _lag) {
55+
// Ring of rolling-hash buckets, keyed by `bucketSeq % BUCKET_RING_SIZE`. Inert for the legacy
56+
// frontier-tree flow; consumed by the streaming inbox checks at `propose` (AZIP-22 Fast Inbox).
57+
mapping(uint256 ringIndex => InboxBucket bucket) internal buckets;
58+
59+
uint64 internal currentBucketSeq;
60+
61+
constructor(
62+
address _rollup,
63+
IERC20 _feeAsset,
64+
uint256 _version,
65+
uint256 _height,
66+
uint256 _lag,
67+
uint256 _bucketRingSize
68+
) {
4469
ROLLUP = _rollup;
4570
VERSION = _version;
4671

@@ -50,10 +75,18 @@ contract Inbox is IInbox {
5075
require(_lag > 0, "LAG TOO SMALL");
5176
LAG = _lag;
5277

78+
require(_bucketRingSize > 1, "BUCKET RING TOO SMALL");
79+
BUCKET_RING_SIZE = _bucketRingSize;
80+
5381
state = InboxState({
5482
rollingHash: 0, totalMessagesInserted: 0, inProgress: SafeCast.toUint64(Constants.INITIAL_CHECKPOINT_NUMBER + LAG)
5583
});
5684

85+
// Genesis bucket: a checkpoint consuming no messages references the same bucket as its parent, so the
86+
// first checkpoint against an empty Inbox references this one and no base case leaks into `propose`.
87+
buckets[0] =
88+
InboxBucket({rollingHash: 0, totalMsgCount: 0, timestamp: SafeCast.toUint64(block.timestamp), msgCount: 0});
89+
5790
forest.initialize(_height);
5891
EMPTY_ROOT = trees[type(uint256).max].root(forest, HEIGHT, SIZE);
5992

@@ -122,11 +155,49 @@ contract Inbox is IInbox {
122155
rollingHash: updatedRollingHash, totalMessagesInserted: totalMessagesInserted + 1, inProgress: inProgress
123156
});
124157

125-
emit MessageSent(inProgress, index, leaf, updatedRollingHash);
158+
(uint64 bucketSeq, bytes32 inboxRollingHash) = _absorbIntoBucket(leaf);
159+
160+
emit MessageSent(inProgress, index, leaf, updatedRollingHash, inboxRollingHash, bucketSeq);
126161

127162
return (leaf, index);
128163
}
129164

165+
/**
166+
* @notice Absorbs a message leaf into the consensus rolling hash and snapshots it into the bucket ring
167+
*
168+
* @dev A bucket only holds messages from a single L1 block, up to MAX_MSGS_PER_BUCKET; the first message
169+
* of a new L1 block — or the message after a full bucket, spilling over within the same block — opens the
170+
* next bucket, inheriting the rolling hash and cumulative count. Bucket 0 is the pristine genesis base
171+
* case and never absorbs. Opening a bucket overwrites the ring entry from BUCKET_RING_SIZE buckets ago;
172+
* protection against overwriting unconsumed buckets is not enforced yet.
173+
*
174+
* @param _leaf - The message leaf to absorb
175+
*
176+
* @return The sequence number of the bucket the leaf was absorbed into and the updated rolling hash
177+
*/
178+
function _absorbIntoBucket(bytes32 _leaf) internal returns (uint64, bytes32) {
179+
uint64 bucketSeq = currentBucketSeq;
180+
InboxBucket memory bucket = buckets[bucketSeq % BUCKET_RING_SIZE];
181+
182+
if (bucketSeq == 0 || bucket.timestamp < block.timestamp || bucket.msgCount == MAX_MSGS_PER_BUCKET) {
183+
bucketSeq += 1;
184+
currentBucketSeq = bucketSeq;
185+
bucket = InboxBucket({
186+
rollingHash: bucket.rollingHash,
187+
totalMsgCount: bucket.totalMsgCount,
188+
timestamp: SafeCast.toUint64(block.timestamp),
189+
msgCount: 0
190+
});
191+
}
192+
193+
bucket.rollingHash = Hash.accumulateInboxRollingHash(bucket.rollingHash, _leaf);
194+
bucket.totalMsgCount += 1;
195+
bucket.msgCount += 1;
196+
buckets[bucketSeq % BUCKET_RING_SIZE] = bucket;
197+
198+
return (bucketSeq, bucket.rollingHash);
199+
}
200+
130201
/**
131202
* @notice Consumes the current tree, and starts a new one if needed
132203
*
@@ -177,4 +248,14 @@ contract Inbox is IInbox {
177248
function getInProgress() external view override(IInbox) returns (uint64) {
178249
return state.inProgress;
179250
}
251+
252+
function getCurrentBucketSeq() external view override(IInbox) returns (uint64) {
253+
return currentBucketSeq;
254+
}
255+
256+
function getBucket(uint256 _seq) external view override(IInbox) returns (InboxBucket memory) {
257+
uint256 current = currentBucketSeq;
258+
require(_seq <= current && current - _seq < BUCKET_RING_SIZE, Errors.Inbox__BucketOutOfWindow(_seq, current));
259+
return buckets[_seq % BUCKET_RING_SIZE];
260+
}
180261
}

l1-contracts/test/Inbox.t.sol

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ contract InboxTest is Test {
3030
function setUp() public {
3131
address rollup = address(this);
3232
IERC20 feeAsset = new TestERC20("Fee Asset", "FA", address(this));
33-
inbox = new InboxHarness(rollup, feeAsset, version, HEIGHT, TestConstants.AZTEC_INBOX_LAG);
33+
inbox = new InboxHarness(
34+
rollup, feeAsset, version, HEIGHT, TestConstants.AZTEC_INBOX_LAG, TestConstants.AZTEC_INBOX_BUCKET_RING_SIZE
35+
);
3436
emptyTreeRoot = inbox.getEmptyRoot();
3537
}
3638

@@ -96,9 +98,12 @@ contract InboxTest is Test {
9698

9799
bytes32 leaf = message.sha256ToField();
98100
bytes16 expectedRollingHash = bytes16(keccak256(abi.encodePacked(stateBefore.rollingHash, leaf)));
101+
bytes32 expectedInboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), leaf);
99102
vm.expectEmit(true, true, true, true);
100103
// event we expect
101-
emit IInbox.MessageSent(FIRST_REAL_TREE_NUM, globalLeafIndex, leaf, expectedRollingHash);
104+
emit IInbox.MessageSent(
105+
FIRST_REAL_TREE_NUM, globalLeafIndex, leaf, expectedRollingHash, expectedInboxRollingHash, 1
106+
);
102107
// event we will get
103108
(bytes32 insertedLeaf, uint256 insertedIndex) =
104109
inbox.sendL2Message(message.recipient, message.content, message.secretHash);

0 commit comments

Comments
 (0)