Skip to content

Commit 83861ab

Browse files
committed
test(fast-inbox): gas measurements for Inbox.sendL2Message and timestamp-key note (A-1377)
1 parent 3fcfc65 commit 83861ab

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ contract Inbox is IInbox {
179179
uint64 bucketSeq = currentBucketSeq;
180180
InboxBucket memory bucket = buckets[bucketSeq % BUCKET_RING_SIZE];
181181

182+
// Buckets are keyed by L1 block timestamp: a strictly larger timestamp opens a new bucket. Post-merge
183+
// Ethereum increases block.timestamp strictly per block, so one bucket corresponds to one L1 block. Under
184+
// anvil with manual mining two blocks can share a timestamp and therefore a bucket; this is harmless
185+
// because the consumption cutoff is computed over timestamps, so co-timestamped blocks are equivalent to it.
182186
if (bucketSeq == 0 || bucket.timestamp < block.timestamp || bucket.msgCount == MAX_MSGS_PER_BUCKET) {
183187
bucketSeq += 1;
184188
currentBucketSeq = bucketSeq;

l1-contracts/test/InboxBuckets.t.sol

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ contract InboxBucketsTest is Test {
4141
return leaf;
4242
}
4343

44+
// Sends a message and returns the gas consumed by the external `sendL2Message` call alone. The
45+
// recipient/content/secretHash are built before the measurement window so only the call is timed.
46+
function _measureSend(InboxHarness _inbox, uint256 _salt) internal returns (uint256 gasUsed) {
47+
DataStructures.L2Actor memory recipient =
48+
DataStructures.L2Actor({actor: bytes32(uint256(0x1000 + _salt)), version: version});
49+
bytes32 content = bytes32(uint256(0x2000 + _salt));
50+
bytes32 secretHash = bytes32(uint256(0x3000 + _salt));
51+
52+
uint256 gasBefore = gasleft();
53+
_inbox.sendL2Message(recipient, content, secretHash);
54+
gasUsed = gasBefore - gasleft();
55+
}
56+
4457
// Shared test vectors for the rolling-hash chain, pinned across the noir circuits, the TS mirror,
4558
// and this L1 implementation. Generated from an independent sha256 implementation.
4659
function testRollingHashTestVectors() public pure {
@@ -213,4 +226,57 @@ contract InboxBucketsTest is Test {
213226
vm.expectRevert(abi.encodeWithSelector(Errors.Inbox__BucketOutOfWindow.selector, current + 1, current));
214227
smallRingInbox.getBucket(current + 1);
215228
}
229+
230+
// Gas cost of a message absorbed into an already-open bucket (the common per-message case): the
231+
// second message of an L1 block updates the live bucket in place without opening a new ring slot.
232+
function testGasSendIntoExistingBucket() public {
233+
_send(inbox, 0);
234+
assertEq(inbox.getCurrentBucketSeq(), 1, "warmup opened bucket 1");
235+
236+
uint256 gasUsed = _measureSend(inbox, 1);
237+
emit log_named_uint("gas: absorb into existing bucket", gasUsed);
238+
239+
assertEq(inbox.getCurrentBucketSeq(), 1, "absorbed without opening a new bucket");
240+
}
241+
242+
// Gas cost of the first message of a new L1 block: a larger timestamp opens the next bucket,
243+
// writing a fresh ring slot on top of the per-message frontier-tree insert.
244+
function testGasSendFirstMessageOfNewBlock() public {
245+
_send(inbox, 0);
246+
assertEq(inbox.getCurrentBucketSeq(), 1, "warmup opened bucket 1");
247+
248+
vm.roll(block.number + 1);
249+
vm.warp(block.timestamp + 12);
250+
251+
uint256 gasUsed = _measureSend(inbox, 1);
252+
emit log_named_uint("gas: first message of a new L1 block", gasUsed);
253+
254+
assertEq(inbox.getCurrentBucketSeq(), 2, "new block opened bucket 2");
255+
}
256+
257+
// Gas cost of a rollover opening mid-block: once a bucket reaches MAX_MSGS_PER_BUCKET, the next
258+
// message in the same L1 block opens a new bucket even though the timestamp is unchanged.
259+
function testGasSendRolloverMidBlock() public {
260+
uint256 cap = inbox.MAX_MSGS_PER_BUCKET();
261+
for (uint256 i = 0; i < cap; i++) {
262+
_send(inbox, i);
263+
}
264+
assertEq(inbox.getCurrentBucketSeq(), 1, "cap messages fit in bucket 1");
265+
266+
uint256 gasUsed = _measureSend(inbox, cap);
267+
emit log_named_uint("gas: rollover open mid-block", gasUsed);
268+
269+
assertEq(inbox.getCurrentBucketSeq(), 2, "rollover opened bucket 2");
270+
}
271+
272+
// Gas cost of the first-ever message against a freshly deployed Inbox: every touched slot is cold,
273+
// including the state struct and bucket 1, so this is the worst-case single insert.
274+
function testGasSendFirstEverMessage() public {
275+
assertEq(inbox.getCurrentBucketSeq(), 0, "no message sent yet");
276+
277+
uint256 gasUsed = _measureSend(inbox, 0);
278+
emit log_named_uint("gas: first-ever message", gasUsed);
279+
280+
assertEq(inbox.getCurrentBucketSeq(), 1, "first message opened bucket 1");
281+
}
216282
}

0 commit comments

Comments
 (0)