@@ -13,6 +13,11 @@ import {FeeJuicePortal} from "@aztec/core/messagebridge/FeeJuicePortal.sol";
1313import {IERC20 } from "@oz/token/ERC20/IERC20.sol " ;
1414import {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}
0 commit comments