diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 73b3da4c4b41..cc66c96895e2 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -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"; @@ -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 + ) ) ); diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 19f24b72fdb4..6a347b11d8d5 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -27,6 +27,11 @@ struct PublicInputArgs { bytes32 previousArchive; bytes32 endArchive; bytes32 outHash; + // Inbox rolling-hash chain segment consumed across the proven epoch (AZIP-22 Fast Inbox). Deliberately UNVALIDATED + // until the Fast Inbox flip, when they get checked against per-checkpoint records written at propose; for now they + // are only passed through to the proof's public inputs. + bytes32 previousInboxRollingHash; + bytes32 endInboxRollingHash; address proverId; } diff --git a/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol b/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol index a001403791da..be17910ef368 100644 --- a/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol +++ b/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol @@ -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. @@ -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 /** @@ -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); } diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index 54d849e5893a..24786f85132e 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -25,7 +25,7 @@ library Constants { 355_785_372_471_781_095_838_790_036_702_437_931_769_306_153_278_986_832_745_847_530_947_941_691_539; uint256 internal constant FEE_JUICE_ADDRESS = 3; uint256 internal constant BLS12_POINT_COMPRESSED_BYTES = 48; - uint256 internal constant ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH = 111; + uint256 internal constant ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH = 113; uint256 internal constant NUM_MSGS_PER_BASE_PARITY = 256; uint256 internal constant NUM_BASE_PARITY_PER_ROOT_PARITY = 4; } diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 01152320a831..46dccf4a7788 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -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 diff --git a/l1-contracts/src/core/libraries/crypto/Hash.sol b/l1-contracts/src/core/libraries/crypto/Hash.sol index b2d4df0aac56..15789aeae21e 100644 --- a/l1-contracts/src/core/libraries/crypto/Hash.sol +++ b/l1-contracts/src/core/libraries/crypto/Hash.sol @@ -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)); + } } diff --git a/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol b/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol index 628a359204bf..cf59ae6b4f31 100644 --- a/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol +++ b/l1-contracts/src/core/libraries/rollup/EpochProofLib.sol @@ -191,6 +191,8 @@ library EpochProofLib { // previous_archive_root: Field, // end_archive_root: Field, // out_hash: Field, + // previous_inbox_rolling_hash: Field, + // end_inbox_rolling_hash: Field, // checkpointHeaderHashes: [Field; Constants.MAX_CHECKPOINTS_PER_EPOCH], // fees: [FeeRecipient; Constants.MAX_CHECKPOINTS_PER_EPOCH], // chain_id: Field, @@ -208,15 +210,21 @@ library EpochProofLib { publicInputs[1] = _args.endArchive; publicInputs[2] = _args.outHash; + + // Inbox rolling-hash chain segment consumed across the epoch (AZIP-22 Fast Inbox). Deliberately UNVALIDATED + // until the Fast Inbox flip, when they get checked against per-checkpoint records written at propose; for now + // they are only passed through to the proof's public inputs. + publicInputs[3] = _args.previousInboxRollingHash; + publicInputs[4] = _args.endInboxRollingHash; } uint256 numCheckpoints = _end - _start + 1; for (uint256 i = 0; i < numCheckpoints; i++) { - publicInputs[3 + i] = STFLib.getHeaderHash(_start + i); + publicInputs[5 + i] = STFLib.getHeaderHash(_start + i); } - uint256 offset = 3 + Constants.MAX_CHECKPOINTS_PER_EPOCH; + uint256 offset = 5 + Constants.MAX_CHECKPOINTS_PER_EPOCH; // Taking recipient/value from the checkpoint headers rather than the prover // as defense in depth. Slots past numCheckpoints stay zero. diff --git a/l1-contracts/src/core/libraries/rollup/ProposedHeaderLib.sol b/l1-contracts/src/core/libraries/rollup/ProposedHeaderLib.sol index d60e1edd6056..5a456dda77bb 100644 --- a/l1-contracts/src/core/libraries/rollup/ProposedHeaderLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ProposedHeaderLib.sol @@ -22,6 +22,7 @@ struct ProposedHeader { bytes32 blockHeadersHash; bytes32 blobsHash; bytes32 inHash; + bytes32 inboxRollingHash; bytes32 outHash; Slot slotNumber; Timestamp timestamp; @@ -56,6 +57,7 @@ library ProposedHeaderLib { _header.blockHeadersHash, _header.blobsHash, _header.inHash, + _header.inboxRollingHash, _header.outHash, _header.slotNumber, Timestamp.unwrap(_header.timestamp).toUint64(), diff --git a/l1-contracts/src/core/messagebridge/Inbox.sol b/l1-contracts/src/core/messagebridge/Inbox.sol index 8355421b6178..2dde615f5507 100644 --- a/l1-contracts/src/core/messagebridge/Inbox.sol +++ b/l1-contracts/src/core/messagebridge/Inbox.sol @@ -13,6 +13,11 @@ 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; + /** * @title Inbox * @author Aztec Labs @@ -23,12 +28,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 @@ -40,7 +52,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; @@ -50,10 +75,18 @@ contract Inbox is IInbox { require(_lag > 0, "LAG TOO SMALL"); LAG = _lag; + require(_bucketRingSize > 1, "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); @@ -122,11 +155,49 @@ 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]; + + 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 * @@ -177,4 +248,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]; + } } diff --git a/l1-contracts/test/Inbox.t.sol b/l1-contracts/test/Inbox.t.sol index daac928af500..68cccd760645 100644 --- a/l1-contracts/test/Inbox.t.sol +++ b/l1-contracts/test/Inbox.t.sol @@ -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(); } @@ -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); diff --git a/l1-contracts/test/InboxBuckets.t.sol b/l1-contracts/test/InboxBuckets.t.sol new file mode 100644 index 000000000000..9bd81233ffa3 --- /dev/null +++ b/l1-contracts/test/InboxBuckets.t.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {TestERC20} from "src/mock/TestERC20.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; +import {InboxHarness} from "./harnesses/InboxHarness.sol"; +import {TestConstants} from "./harnesses/TestConstants.sol"; +import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {Hash} from "@aztec/core/libraries/crypto/Hash.sol"; +import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; + +contract InboxBucketsTest is Test { + uint256 internal constant FIRST_REAL_TREE_NUM = Constants.INITIAL_CHECKPOINT_NUMBER + TestConstants.AZTEC_INBOX_LAG; + uint256 internal constant HEIGHT = 10; + uint256 internal constant SMALL_RING_SIZE = 4; + + InboxHarness internal inbox; + uint256 internal version = 0; + bytes32 internal expectedRollingHash; + + function setUp() public { + inbox = _deployInbox(TestConstants.AZTEC_INBOX_BUCKET_RING_SIZE); + } + + function _deployInbox(uint256 _ringSize) internal returns (InboxHarness) { + IERC20 feeAsset = new TestERC20("Fee Asset", "FA", address(this)); + return new InboxHarness(address(this), feeAsset, version, HEIGHT, TestConstants.AZTEC_INBOX_LAG, _ringSize); + } + + function _send(InboxHarness _inbox, uint256 _salt) internal returns (bytes32) { + (bytes32 leaf,) = _inbox.sendL2Message( + DataStructures.L2Actor({actor: bytes32(uint256(0x1000 + _salt)), version: version}), + bytes32(uint256(0x2000 + _salt)), + bytes32(uint256(0x3000 + _salt)) + ); + expectedRollingHash = Hash.accumulateInboxRollingHash(expectedRollingHash, leaf); + return leaf; + } + + // Shared test vectors for the rolling-hash chain, pinned across the noir circuits, the TS mirror, + // and this L1 implementation. Generated from an independent sha256 implementation. + function testRollingHashTestVectors() public pure { + bytes32 h = Hash.accumulateInboxRollingHash(bytes32(0), bytes32(uint256(11))); + assertEq(h, 0x00815fb1e9d2076ae5761439b6144ad11da69eb6c41ab2aca39e770407ad8d12, "chain(0, [11])"); + + h = Hash.accumulateInboxRollingHash(h, bytes32(uint256(22))); + h = Hash.accumulateInboxRollingHash(h, bytes32(uint256(33))); + assertEq(h, 0x0014cae968461979aab6d33266a2310ed234d3f6cf4472737c57551db07bd0da, "chain(0, [11, 22, 33])"); + + h = bytes32(0); + for (uint256 i = 1; i <= 256; i++) { + h = Hash.accumulateInboxRollingHash(h, bytes32(i)); + } + assertEq(h, 0x00ea95b96f17b75be03525b35a2a1918b42f03ad8c00a437cf641751825f3992, "chain(0, [1..=256])"); + + h = Hash.accumulateInboxRollingHash(bytes32(uint256(0x2a)), bytes32(uint256(7))); + assertEq(h, 0x0032a934005556d1b9d22708666ee8b05f91fafad624dd64a6ea878e048e5438, "chain(0x2a, [7])"); + + h = Hash.accumulateInboxRollingHash(h, bytes32(uint256(8))); + assertEq(h, 0x0054d96b8a074a5030a5838972d0a3c04ba47cf5956348c853e02e9566233f65, "chain(0x2a, [7, 8])"); + } + + function testGenesisBucket() public { + assertEq(inbox.getCurrentBucketSeq(), 0, "genesis seq"); + + IInbox.InboxBucket memory bucket = inbox.getBucket(0); + assertEq(bucket.rollingHash, bytes32(0), "genesis rolling hash"); + assertEq(bucket.totalMsgCount, 0, "genesis total"); + assertEq(bucket.timestamp, uint64(block.timestamp), "genesis timestamp"); + assertEq(bucket.msgCount, 0, "genesis msg count"); + + vm.expectRevert(abi.encodeWithSelector(Errors.Inbox__BucketOutOfWindow.selector, 1, 0)); + inbox.getBucket(1); + } + + function testFirstMessageOpensBucketOne() public { + // Even in the deployment block, the first message must not absorb into the genesis bucket: a + // checkpoint consuming no messages needs a bucket whose rolling hash matches its parent's chain + // position, which for the first checkpoint is the zero genesis bucket. + _send(inbox, 0); + + assertEq(inbox.getCurrentBucketSeq(), 1, "current seq"); + assertEq(inbox.getBucket(0).rollingHash, bytes32(0), "genesis untouched"); + assertEq(inbox.getBucket(0).msgCount, 0, "genesis still empty"); + assertEq(inbox.getBucket(1).msgCount, 1, "bucket 1 has the message"); + } + + function testAccumulationWithinSingleBlock() public { + bytes32 leaf1 = _send(inbox, 1); + bytes32 chain1 = Hash.accumulateInboxRollingHash(bytes32(0), leaf1); + bytes32 leaf2 = _send(inbox, 2); + bytes32 chain2 = Hash.accumulateInboxRollingHash(chain1, leaf2); + bytes32 leaf3 = _send(inbox, 3); + bytes32 chain3 = Hash.accumulateInboxRollingHash(chain2, leaf3); + + assertEq(inbox.getCurrentBucketSeq(), 1, "all messages share one bucket"); + + IInbox.InboxBucket memory bucket = inbox.getBucket(1); + assertEq(bucket.rollingHash, chain3, "bucket rolling hash"); + assertEq(bucket.totalMsgCount, 3, "bucket cumulative total"); + assertEq(bucket.timestamp, uint64(block.timestamp), "bucket timestamp"); + assertEq(bucket.msgCount, 3, "bucket msg count"); + } + + function testMessageSentEventCarriesBucketData() public { + DataStructures.L2Actor memory recipient = + DataStructures.L2Actor({actor: bytes32(uint256(0x1000)), version: version}); + bytes32 content = bytes32(uint256(0x2000)); + bytes32 secretHash = bytes32(uint256(0x3000)); + + DataStructures.L1ToL2Msg memory message = DataStructures.L1ToL2Msg({ + sender: DataStructures.L1Actor(address(this), block.chainid), + recipient: recipient, + content: content, + secretHash: secretHash, + index: (FIRST_REAL_TREE_NUM - 1) * (2 ** HEIGHT) + }); + bytes32 leaf = Hash.sha256ToField(message); + bytes16 legacyHash = bytes16(keccak256(abi.encodePacked(inbox.getState().rollingHash, leaf))); + bytes32 inboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), leaf); + + vm.expectEmit(true, true, true, true, address(inbox)); + emit IInbox.MessageSent(FIRST_REAL_TREE_NUM, message.index, leaf, legacyHash, inboxRollingHash, 1); + inbox.sendL2Message(recipient, content, secretHash); + } + + function testSnapshotBoundariesAcrossBlocks() public { + _send(inbox, 1); + _send(inbox, 2); + IInbox.InboxBucket memory bucket1 = inbox.getBucket(1); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 12); + + bytes32 leaf3 = _send(inbox, 3); + + assertEq(inbox.getCurrentBucketSeq(), 2, "new block opens a new bucket"); + + // The previous bucket's snapshot is frozen at its end-of-block state. + IInbox.InboxBucket memory bucket1After = inbox.getBucket(1); + assertEq(bucket1After.rollingHash, bucket1.rollingHash, "bucket 1 rolling hash frozen"); + assertEq(bucket1After.totalMsgCount, 2, "bucket 1 total frozen"); + assertEq(bucket1After.msgCount, 2, "bucket 1 msg count frozen"); + assertEq(bucket1After.timestamp, bucket1.timestamp, "bucket 1 timestamp frozen"); + + // The new bucket continues the chain from the previous bucket. + IInbox.InboxBucket memory bucket2 = inbox.getBucket(2); + assertEq(bucket2.rollingHash, Hash.accumulateInboxRollingHash(bucket1.rollingHash, leaf3), "chain continuity"); + assertEq(bucket2.rollingHash, expectedRollingHash, "chain matches reference"); + assertEq(bucket2.totalMsgCount, 3, "cumulative total spans buckets"); + assertEq(bucket2.timestamp, uint64(block.timestamp), "bucket 2 timestamp"); + assertEq(bucket2.msgCount, 1, "bucket 2 msg count"); + } + + function testRolloverIntoNextBucket() public { + uint256 cap = inbox.MAX_MSGS_PER_BUCKET(); + for (uint256 i = 0; i < cap; i++) { + _send(inbox, i); + } + assertEq(inbox.getCurrentBucketSeq(), 1, "cap messages fit in one bucket"); + IInbox.InboxBucket memory bucket1 = inbox.getBucket(1); + assertEq(bucket1.msgCount, cap, "bucket 1 full"); + + // The next message in the same L1 block spills over into a new bucket with the same timestamp. + bytes32 leaf = _send(inbox, cap); + assertEq(inbox.getCurrentBucketSeq(), 2, "rollover opened next bucket"); + + IInbox.InboxBucket memory bucket2 = inbox.getBucket(2); + assertEq(bucket2.rollingHash, Hash.accumulateInboxRollingHash(bucket1.rollingHash, leaf), "chain continuity"); + assertEq(bucket2.totalMsgCount, cap + 1, "cumulative total"); + assertEq(bucket2.timestamp, bucket1.timestamp, "same block, same timestamp"); + assertEq(bucket2.msgCount, 1, "spilled message only"); + + assertEq(inbox.getBucket(1).msgCount, cap, "bucket 1 untouched by rollover"); + } + + function testRingWraparound() public { + InboxHarness smallRingInbox = _deployInbox(SMALL_RING_SIZE); + expectedRollingHash = 0; + + // One bucket per L1 block; after SMALL_RING_SIZE + 1 buckets the ring has wrapped past bucket 1. + for (uint256 i = 1; i <= SMALL_RING_SIZE + 1; i++) { + vm.roll(block.number + 1); + vm.warp(block.timestamp + 12); + _send(smallRingInbox, i); + } + + uint256 current = smallRingInbox.getCurrentBucketSeq(); + assertEq(current, SMALL_RING_SIZE + 1, "one bucket per block"); + + // Buckets 0 and 1 have been overwritten (their ring slots were reused by buckets 4 and 5). + vm.expectRevert(abi.encodeWithSelector(Errors.Inbox__BucketOutOfWindow.selector, 0, current)); + smallRingInbox.getBucket(0); + vm.expectRevert(abi.encodeWithSelector(Errors.Inbox__BucketOutOfWindow.selector, 1, current)); + smallRingInbox.getBucket(1); + + // The live window is intact, with per-bucket data at the right ring slots. + uint64 previousTimestamp = 0; + for (uint256 seq = current - SMALL_RING_SIZE + 1; seq <= current; seq++) { + IInbox.InboxBucket memory bucket = smallRingInbox.getBucket(seq); + assertEq(bucket.totalMsgCount, seq, "cumulative total"); + assertEq(bucket.msgCount, 1, "one message per bucket"); + assertGt(bucket.timestamp, previousTimestamp, "timestamps increase per bucket"); + previousTimestamp = bucket.timestamp; + } + assertEq(smallRingInbox.getBucket(current).rollingHash, expectedRollingHash, "chain matches reference"); + + // Buckets ahead of the current one do not exist yet. + vm.expectRevert(abi.encodeWithSelector(Errors.Inbox__BucketOutOfWindow.selector, current + 1, current)); + smallRingInbox.getBucket(current + 1); + } +} diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 9f578e8cefd7..2456474939b7 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -885,7 +885,12 @@ contract RollupTest is RollupBase { CheckpointLog memory checkpoint = rollup.getCheckpoint(0); PublicInputArgs memory args = PublicInputArgs({ - previousArchive: checkpoint.archive, endArchive: data.archive, outHash: data.header.outHash, proverId: address(0) + previousArchive: checkpoint.archive, + endArchive: data.archive, + outHash: data.header.outHash, + previousInboxRollingHash: 0, + endInboxRollingHash: data.header.inboxRollingHash, + proverId: address(0) }); ProposedHeader[] memory headers = new ProposedHeader[](1); @@ -896,7 +901,7 @@ contract RollupTest is RollupBase { bytes32[] memory publicInputs = rollup.getEpochProofPublicInputs(1, 1, args, headers, data.batchedBlobInputs); assertEq(publicInputs.length, Constants.ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH, "Unexpected public inputs length"); - uint256 feesOffset = 3 + Constants.MAX_CHECKPOINTS_PER_EPOCH; + uint256 feesOffset = 5 + Constants.MAX_CHECKPOINTS_PER_EPOCH; assertEq( publicInputs[feesOffset], bytes32(uint256(uint160(headers[0].coinbase))), "Coinbase not sourced from header" ); @@ -936,7 +941,12 @@ contract RollupTest is RollupBase { address _prover ) internal { PublicInputArgs memory args = PublicInputArgs({ - previousArchive: _prevArchive, endArchive: _archive, outHash: _outHash, proverId: _prover + previousArchive: _prevArchive, + endArchive: _archive, + outHash: _outHash, + previousInboxRollingHash: 0, + endInboxRollingHash: 0, + proverId: _prover }); uint256 size = _end - _start + 1; diff --git a/l1-contracts/test/base/DecoderBase.sol b/l1-contracts/test/base/DecoderBase.sol index 24e74617950d..7abcb375c1aa 100644 --- a/l1-contracts/test/base/DecoderBase.sol +++ b/l1-contracts/test/base/DecoderBase.sol @@ -47,6 +47,7 @@ contract DecoderBase is TestBase { bytes32 feeRecipient; GasFees gasFees; bytes32 inHash; + bytes32 inboxRollingHash; bytes32 lastArchiveRoot; bytes32 outHash; uint256 slotNumber; @@ -104,6 +105,7 @@ contract DecoderBase is TestBase { blockHeadersHash: full.checkpoint.header.blockHeadersHash, blobsHash: full.checkpoint.header.blobsHash, inHash: full.checkpoint.header.inHash, + inboxRollingHash: full.checkpoint.header.inboxRollingHash, outHash: full.checkpoint.header.outHash, slotNumber: Slot.wrap(full.checkpoint.header.slotNumber), timestamp: Timestamp.wrap(full.checkpoint.header.timestamp), diff --git a/l1-contracts/test/base/RollupBase.sol b/l1-contracts/test/base/RollupBase.sol index 78829b61135d..f75b9875d013 100644 --- a/l1-contracts/test/base/RollupBase.sol +++ b/l1-contracts/test/base/RollupBase.sol @@ -78,6 +78,8 @@ contract RollupBase is DecoderBase { previousArchive: parentCheckpointLog.archive, endArchive: endFull.checkpoint.archive, outHash: endFull.checkpoint.header.outHash, + previousInboxRollingHash: 0, + endInboxRollingHash: 0, proverId: _prover }); diff --git a/l1-contracts/test/benchmark/happy.t.sol b/l1-contracts/test/benchmark/happy.t.sol index e558b9b6f2f2..f5f0a900b888 100644 --- a/l1-contracts/test/benchmark/happy.t.sol +++ b/l1-contracts/test/benchmark/happy.t.sol @@ -511,6 +511,8 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase { previousArchive: rollup.getCheckpoint(start).archive, endArchive: endCheckpoint.archive, outHash: endCheckpoint.outHash, + previousInboxRollingHash: 0, + endInboxRollingHash: 0, proverId: address(0) }); diff --git a/l1-contracts/test/compression/PreHeating.t.sol b/l1-contracts/test/compression/PreHeating.t.sol index e3b891e195b2..7cf07a0c9298 100644 --- a/l1-contracts/test/compression/PreHeating.t.sol +++ b/l1-contracts/test/compression/PreHeating.t.sol @@ -263,6 +263,8 @@ contract PreHeatingTest is FeeModelTestPoints, DecoderBase { previousArchive: rollup.getCheckpoint(start).archive, endArchive: endCheckpoint.archive, outHash: endCheckpoint.outHash, + previousInboxRollingHash: 0, + endInboxRollingHash: 0, proverId: address(0) }); diff --git a/l1-contracts/test/escape-hatch/integration/EscapeHatchIntegrationBase.sol b/l1-contracts/test/escape-hatch/integration/EscapeHatchIntegrationBase.sol index 61f679d78805..7b2e16301bbb 100644 --- a/l1-contracts/test/escape-hatch/integration/EscapeHatchIntegrationBase.sol +++ b/l1-contracts/test/escape-hatch/integration/EscapeHatchIntegrationBase.sol @@ -142,6 +142,7 @@ abstract contract EscapeHatchIntegrationBase is ValidatorSelectionTestBase { blockHeadersHash: full.checkpoint.header.blockHeadersHash, blobsHash: full.checkpoint.header.blobsHash, inHash: full.checkpoint.header.inHash, + inboxRollingHash: full.checkpoint.header.inboxRollingHash, outHash: full.checkpoint.header.outHash, slotNumber: slotNumber, timestamp: rollup.getTimestampForSlot(slotNumber), @@ -324,6 +325,8 @@ abstract contract EscapeHatchIntegrationBase is ValidatorSelectionTestBase { previousArchive: previousArchive, endArchive: endArchive, outHash: endFull.checkpoint.header.outHash, + previousInboxRollingHash: 0, + endInboxRollingHash: 0, proverId: _prover }); diff --git a/l1-contracts/test/escape-hatch/integration/regression/OutHashValidationSkipped.t.sol b/l1-contracts/test/escape-hatch/integration/regression/OutHashValidationSkipped.t.sol index 72b48358d16a..5baa9903924d 100644 --- a/l1-contracts/test/escape-hatch/integration/regression/OutHashValidationSkipped.t.sol +++ b/l1-contracts/test/escape-hatch/integration/regression/OutHashValidationSkipped.t.sol @@ -60,6 +60,8 @@ contract OutHashValidationSkippedTest is EscapeHatchIntegrationBase { previousArchive: rollup.archiveAt(0), endArchive: rollup.archiveAt(1), outHash: wrongOutHash, + previousInboxRollingHash: 0, + endInboxRollingHash: 0, proverId: address(this) }); diff --git a/l1-contracts/test/escape-hatch/integration/submitEpochRootProof.t.sol b/l1-contracts/test/escape-hatch/integration/submitEpochRootProof.t.sol index 4e57aef3f5f6..6e46a60fad48 100644 --- a/l1-contracts/test/escape-hatch/integration/submitEpochRootProof.t.sol +++ b/l1-contracts/test/escape-hatch/integration/submitEpochRootProof.t.sol @@ -112,7 +112,12 @@ contract submitEpochRootProofTest is EscapeHatchIntegrationBase { bytes32 outHash = rollup.getCheckpoint(1).outHash; PublicInputArgs memory args = PublicInputArgs({ - previousArchive: previousArchive, endArchive: endArchive, outHash: outHash, proverId: address(this) + previousArchive: previousArchive, + endArchive: endArchive, + outHash: outHash, + previousInboxRollingHash: 0, + endInboxRollingHash: 0, + proverId: address(this) }); ProposedHeader[] memory headers = new ProposedHeader[](1); diff --git a/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol b/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol index 0a4b0c99374d..61a5ff73a90f 100644 --- a/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol +++ b/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol @@ -111,8 +111,9 @@ contract DepositToAztecPublic is Test { bytes16 expectedHash = bytes16(keccak256(abi.encodePacked(inbox.getState().rollingHash, expectedKey))); uint256 expectedInProgress = Constants.INITIAL_CHECKPOINT_NUMBER + TestConstants.AZTEC_INBOX_LAG; + bytes32 expectedInboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), expectedKey); vm.expectEmit(true, true, true, true, address(inbox)); - emit IInbox.MessageSent(expectedInProgress, expectedIndex, expectedKey, expectedHash); + emit IInbox.MessageSent(expectedInProgress, expectedIndex, expectedKey, expectedHash, expectedInboxRollingHash, 1); vm.expectEmit(true, true, true, true, address(feeJuicePortal)); emit IFeeJuicePortal.DepositToAztecPublic(to, amount, secretHash, expectedKey, expectedIndex); diff --git a/l1-contracts/test/fees/FeeRollup.t.sol b/l1-contracts/test/fees/FeeRollup.t.sol index e34b85ed6677..26d858ee7e71 100644 --- a/l1-contracts/test/fees/FeeRollup.t.sol +++ b/l1-contracts/test/fees/FeeRollup.t.sol @@ -236,6 +236,8 @@ contract FeeRollupTest is FeeModelTestPoints, DecoderBase { previousArchive: rollup.getCheckpoint(_start).archive, endArchive: endCheckpoint.archive, outHash: endCheckpoint.outHash, + previousInboxRollingHash: 0, + endInboxRollingHash: 0, proverId: address(0) }); diff --git a/l1-contracts/test/fixtures/empty_checkpoint_1.json b/l1-contracts/test/fixtures/empty_checkpoint_1.json index a59c66145db8..1f829f1b819b 100644 --- a/l1-contracts/test/fixtures/empty_checkpoint_1.json +++ b/l1-contracts/test/fixtures/empty_checkpoint_1.json @@ -35,6 +35,7 @@ "blockHeadersHash": "0x2e3e0911389bc48fa8126a93273d016cc7dc08019f8ffc5f1f5ae7d90745eaa2", "blobsHash": "0x00e5b752fe6bc2154155ff3a979c4c5fa91d3ac0d716169ac521e1560fd83b2b", "inHash": "0x00de7b349d2306334734e4f58b1302a6ed5a6c796a706f6597a5641b6d468223", + "inboxRollingHash": "0x00cad6497cf81748167158f149ba70c31f34c68b0ae1b156117de63e073d14b5", "outHash": "0x00c95e0ceb41951039e1592745ec2faea9866f6eaf01bf189a4463b4143af093", "slotNumber": 99, "timestamp": 1776857814, @@ -47,7 +48,7 @@ "totalManaUsed": 0, "accumulatedFees": 0 }, - "headerHash": "0x0069eb49ccf7b0dd2902a19712dd167fd3b5d890df7f0711fdcfe4d2987838ad", + "headerHash": "0x0002fc455005d993521be4e6f8ed92d1e0b50e58b21445881c87d02ed51a1e4c", "numTxs": 0 } -} \ No newline at end of file +} diff --git a/l1-contracts/test/fixtures/empty_checkpoint_2.json b/l1-contracts/test/fixtures/empty_checkpoint_2.json index 52323cc08975..d2dcd961b92f 100644 --- a/l1-contracts/test/fixtures/empty_checkpoint_2.json +++ b/l1-contracts/test/fixtures/empty_checkpoint_2.json @@ -35,6 +35,7 @@ "blockHeadersHash": "0x0b3bda1754ca30707b8c0bbe72760c68e574cf23309e7e4fd7cabea36b4078da", "blobsHash": "0x000e9acabf609c9c113078ecb383ba6310573ce246958b605452132617d2c960", "inHash": "0x006504de282a40084bb8098456a915c645d53482d351db52fa9433b6cd638763", + "inboxRollingHash": "0x0080dbf2bd9576e81fd99fc0f9d35bfd7fc15a6221d0b6e1e92a1884f14d0c84", "outHash": "0x00c95e0ceb41951039e1592745ec2faea9866f6eaf01bf189a4463b4143af093", "slotNumber": 102, "timestamp": 1776858030, @@ -47,7 +48,7 @@ "totalManaUsed": 0, "accumulatedFees": 0 }, - "headerHash": "0x0004d4907090fd10f0c7fa759b7ce89c3b7055385423b585c5f7e959c17290ef", + "headerHash": "0x00120ecaf7a72f61adf3c8ee180acd930401221668a4792a8f91584755840d16", "numTxs": 0 } -} \ No newline at end of file +} diff --git a/l1-contracts/test/fixtures/mixed_checkpoint_1.json b/l1-contracts/test/fixtures/mixed_checkpoint_1.json index 6e47dc89c331..b1604aceb038 100644 --- a/l1-contracts/test/fixtures/mixed_checkpoint_1.json +++ b/l1-contracts/test/fixtures/mixed_checkpoint_1.json @@ -68,6 +68,7 @@ "blockHeadersHash": "0x087b6f59388fa4207876ee1b50521ff838d6eed422d0ad07ff996393f32a77e6", "blobsHash": "0x001bef3ff3f657c565ff86d5072186f7f2bfddb8a31ca0714562c164fe954d84", "inHash": "0x00de7b349d2306334734e4f58b1302a6ed5a6c796a706f6597a5641b6d468223", + "inboxRollingHash": "0x00cad6497cf81748167158f149ba70c31f34c68b0ae1b156117de63e073d14b5", "outHash": "0x00cffdbb0e7f5e164d314d781d38ae31230910a3bae4c34e7df6222df71b1539", "slotNumber": 99, "timestamp": 1776857848, @@ -80,7 +81,7 @@ "totalManaUsed": 0, "accumulatedFees": 0 }, - "headerHash": "0x003ce6d2bb91ea8852841510f5485595dfbddbaa0420e083caf37f182be2140a", + "headerHash": "0x00c1431d117825d25f8169e35eb70ed4b16c0fc4b3cac85506852c240d03740e", "numTxs": 4 } -} \ No newline at end of file +} diff --git a/l1-contracts/test/fixtures/mixed_checkpoint_2.json b/l1-contracts/test/fixtures/mixed_checkpoint_2.json index 577e335441eb..d2125d5786a4 100644 --- a/l1-contracts/test/fixtures/mixed_checkpoint_2.json +++ b/l1-contracts/test/fixtures/mixed_checkpoint_2.json @@ -68,6 +68,7 @@ "blockHeadersHash": "0x144dbe32a03df9ccc672207c93cd22099eb91e44a71e1676148cd3c6c6c98b9e", "blobsHash": "0x008bd0b669b942b57ccf85d3401214db65cde3608afa0b2ae0e57f35ec60d72e", "inHash": "0x006504de282a40084bb8098456a915c645d53482d351db52fa9433b6cd638763", + "inboxRollingHash": "0x0080dbf2bd9576e81fd99fc0f9d35bfd7fc15a6221d0b6e1e92a1884f14d0c84", "outHash": "0x008a85da85a596471f2e5fe402fde332723da8d24b6e7affd60d16c7cb7e9020", "slotNumber": 102, "timestamp": 1776858064, @@ -80,7 +81,7 @@ "totalManaUsed": 0, "accumulatedFees": 0 }, - "headerHash": "0x00eb0f37a51d2df835ea48d6cdde28398df86078d8507d467f34434dac062456", + "headerHash": "0x00ab5faa05cb38faa2cd1fc62c40007f64594c9fdc3bb8b7b478a351a7c7dc5a", "numTxs": 4 } -} \ No newline at end of file +} diff --git a/l1-contracts/test/fixtures/single_tx_checkpoint_1.json b/l1-contracts/test/fixtures/single_tx_checkpoint_1.json index 83aa2756dd96..6d409f05d521 100644 --- a/l1-contracts/test/fixtures/single_tx_checkpoint_1.json +++ b/l1-contracts/test/fixtures/single_tx_checkpoint_1.json @@ -44,6 +44,7 @@ "blockHeadersHash": "0x07db5c24565ad9a2c9d39ef7d9a4446e9742d6090567ff28aef9a45f4738a5cb", "blobsHash": "0x00ec2400a7cfc9d975cb0802980d49387588738160a0cf0301f07e1abad6456c", "inHash": "0x00de7b349d2306334734e4f58b1302a6ed5a6c796a706f6597a5641b6d468223", + "inboxRollingHash": "0x00cad6497cf81748167158f149ba70c31f34c68b0ae1b156117de63e073d14b5", "outHash": "0x007c92c6cf05665e1c02a305370a4d38bcb8b555261c6b39c862f8c067d6bcb7", "slotNumber": 99, "timestamp": 1776857833, @@ -56,7 +57,7 @@ "totalManaUsed": 0, "accumulatedFees": 0 }, - "headerHash": "0x00628120df9716928e25a69ca7b1ac2497f22fdaab439f64315ad4daf7bc5191", + "headerHash": "0x0072a35395a79027db315321cbdaba5bc0b070979c3395b7369ec902ca48d9b2", "numTxs": 1 } -} \ No newline at end of file +} diff --git a/l1-contracts/test/fixtures/single_tx_checkpoint_2.json b/l1-contracts/test/fixtures/single_tx_checkpoint_2.json index b4b8e0a60a51..dc1244dcd73c 100644 --- a/l1-contracts/test/fixtures/single_tx_checkpoint_2.json +++ b/l1-contracts/test/fixtures/single_tx_checkpoint_2.json @@ -44,6 +44,7 @@ "blockHeadersHash": "0x12813725f2d16ce92088d2401ffa4a53ce6061bf75b77320ca7b8ef6c5145adf", "blobsHash": "0x002b8ae4c9f405529e2b689b829852ad52f77acdac57d1dbac3dabea1760affc", "inHash": "0x006504de282a40084bb8098456a915c645d53482d351db52fa9433b6cd638763", + "inboxRollingHash": "0x0080dbf2bd9576e81fd99fc0f9d35bfd7fc15a6221d0b6e1e92a1884f14d0c84", "outHash": "0x0007eac1d76cddf92b28b8f11cd292f199f35dfc588376092986575cef487f59", "slotNumber": 102, "timestamp": 1776858049, @@ -56,7 +57,7 @@ "totalManaUsed": 0, "accumulatedFees": 0 }, - "headerHash": "0x00f2d5075ed8fdbb50bc0171790cb495308b229325c0aa3e1c3fadd5772d11f4", + "headerHash": "0x000b3c0bf27d3123a81b66570e4b63734cb06bbfd260a70fabbbfd41dec13255", "numTxs": 1 } -} \ No newline at end of file +} diff --git a/l1-contracts/test/harnesses/InboxHarness.sol b/l1-contracts/test/harnesses/InboxHarness.sol index 5257aa8a196b..1ba2b3b446ca 100644 --- a/l1-contracts/test/harnesses/InboxHarness.sol +++ b/l1-contracts/test/harnesses/InboxHarness.sol @@ -10,8 +10,8 @@ import {FrontierLib} from "@aztec/core/libraries/crypto/FrontierLib.sol"; contract InboxHarness is Inbox { using FrontierLib for FrontierLib.Tree; - constructor(address _rollup, IERC20 _feeAsset, uint256 _version, uint256 _height, uint256 _lag) - Inbox(_rollup, _feeAsset, _version, _height, _lag) + constructor(address _rollup, IERC20 _feeAsset, uint256 _version, uint256 _height, uint256 _lag, uint256 _ringSize) + Inbox(_rollup, _feeAsset, _version, _height, _lag, _ringSize) {} function getSize() external view returns (uint256) { diff --git a/l1-contracts/test/harnesses/TestConstants.sol b/l1-contracts/test/harnesses/TestConstants.sol index c0d7a6738981..44aee456e1d4 100644 --- a/l1-contracts/test/harnesses/TestConstants.sol +++ b/l1-contracts/test/harnesses/TestConstants.sol @@ -26,6 +26,7 @@ library TestConstants { uint256 internal constant AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET = 3; uint256 internal constant AZTEC_LAG_IN_EPOCHS_FOR_RANDAO = 2; uint256 internal constant AZTEC_INBOX_LAG = 2; + uint256 internal constant AZTEC_INBOX_BUCKET_RING_SIZE = 1024; uint256 internal constant AZTEC_PROOF_SUBMISSION_EPOCHS = 1; uint256 internal constant AZTEC_SLASHING_QUORUM = 17; // Must be > ROUND_SIZE / 2 (ROUND_SIZE derived from // EPOCH_DURATION) diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index 47e341d38f68..cb6bf77e3bd6 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -124,10 +124,11 @@ contract TokenPortalTest is Test { bytes32 expectedLeaf = expectedMessage.sha256ToField(); bytes16 expectedHash = bytes16(keccak256(abi.encodePacked(inbox.getState().rollingHash, expectedLeaf))); + bytes32 expectedInboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), expectedLeaf); // Check the event was emitted vm.expectEmit(true, true, true, true); // event we expect - emit IInbox.MessageSent(FIRST_REAL_TREE_NUM, expectedIndex, expectedLeaf, expectedHash); + emit IInbox.MessageSent(FIRST_REAL_TREE_NUM, expectedIndex, expectedLeaf, expectedHash, expectedInboxRollingHash, 1); // event we will get // Perform op @@ -150,11 +151,12 @@ contract TokenPortalTest is Test { DataStructures.L1ToL2Msg memory expectedMessage = _createExpectedMintPublicL1ToL2Message(expectedIndex); bytes32 expectedLeaf = expectedMessage.sha256ToField(); bytes16 expectedHash = bytes16(keccak256(abi.encodePacked(inbox.getState().rollingHash, expectedLeaf))); + bytes32 expectedInboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), expectedLeaf); // Check the event was emitted vm.expectEmit(true, true, true, true); // event we expect - emit IInbox.MessageSent(FIRST_REAL_TREE_NUM, expectedIndex, expectedLeaf, expectedHash); + emit IInbox.MessageSent(FIRST_REAL_TREE_NUM, expectedIndex, expectedLeaf, expectedHash, expectedInboxRollingHash, 1); // Perform op (bytes32 leaf, uint256 index) = tokenPortal.depositToAztecPublic(to, amount, secretHashForL2MessageConsumption); diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 0062c9b6e58f..722e97dc320a 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -745,6 +745,8 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { previousArchive: parentCheckpointLog.archive, endArchive: endFull.checkpoint.archive, outHash: endFull.checkpoint.header.outHash, + previousInboxRollingHash: 0, + endInboxRollingHash: 0, proverId: prover }); diff --git a/l1-contracts/test/validator-selection/tmnt207.t.sol b/l1-contracts/test/validator-selection/tmnt207.t.sol index 89bd948f5541..85a34b8f84a1 100644 --- a/l1-contracts/test/validator-selection/tmnt207.t.sol +++ b/l1-contracts/test/validator-selection/tmnt207.t.sol @@ -230,6 +230,8 @@ contract Tmnt207Test is RollupBase { previousArchive: rollup.getCheckpoint(0).archive, endArchive: rollup.getCheckpoint(1).archive, outHash: rollup.getCheckpoint(1).outHash, + previousInboxRollingHash: 0, + endInboxRollingHash: 0, proverId: address(0) }), headers: headers, diff --git a/yarn-project/archiver/src/test/fake_l1_state.ts b/yarn-project/archiver/src/test/fake_l1_state.ts index 0b9cbcb177f2..d5e5bf6e2197 100644 --- a/yarn-project/archiver/src/test/fake_l1_state.ts +++ b/yarn-project/archiver/src/test/fake_l1_state.ts @@ -628,6 +628,8 @@ export class FakeL1State { index: msg.index, leaf: msg.leaf, rollingHash: msg.rollingHash, + inboxRollingHash: Fr.ZERO, + bucketSeq: 0n, }, })); } @@ -651,6 +653,8 @@ export class FakeL1State { index: msg.index, leaf: msg.leaf, rollingHash: msg.rollingHash, + inboxRollingHash: Fr.ZERO, + bucketSeq: 0n, }, }; } diff --git a/yarn-project/ethereum/src/contracts/inbox.ts b/yarn-project/ethereum/src/contracts/inbox.ts index 77bcfd1a1f15..23d7f6ce3df4 100644 --- a/yarn-project/ethereum/src/contracts/inbox.ts +++ b/yarn-project/ethereum/src/contracts/inbox.ts @@ -20,6 +20,10 @@ export type MessageSentArgs = { leaf: Fr; checkpointNumber: CheckpointNumber; rollingHash: Buffer16; + /** Consensus rolling hash (truncated sha256 chain) after this message (AZIP-22 Fast Inbox). Not yet consumed by the node. */ + inboxRollingHash: Fr; + /** Sequence number of the Inbox bucket this message was absorbed into (AZIP-22 Fast Inbox). Not yet consumed by the node. */ + bucketSeq: bigint; }; /** Log type for MessageSent events. */ @@ -100,7 +104,14 @@ export class InboxContract { blockNumber: bigint | null; blockHash: `0x${string}` | null; transactionHash: `0x${string}` | null; - args: { index?: bigint; hash?: `0x${string}`; checkpointNumber?: bigint; rollingHash?: `0x${string}` }; + args: { + index?: bigint; + hash?: `0x${string}`; + checkpointNumber?: bigint; + rollingHash?: `0x${string}`; + inboxRollingHash?: `0x${string}`; + bucketSeq?: bigint; + }; }): MessageSentLog { return { l1BlockNumber: log.blockNumber!, @@ -111,6 +122,8 @@ export class InboxContract { leaf: Fr.fromString(log.args.hash!), checkpointNumber: CheckpointNumber.fromBigInt(log.args.checkpointNumber!), rollingHash: Buffer16.fromString(log.args.rollingHash!), + inboxRollingHash: Fr.fromString(log.args.inboxRollingHash!), + bucketSeq: log.args.bucketSeq!, }, }; }