@@ -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