Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,30 @@ Sends a message from L1 to L2.
- Will revert with `Inbox__ContentTooLarge(bytes32 content)` if the content is larger than the field size (~254 bits).
- Will revert with `Inbox__SecretHashTooLarge(bytes32 secretHash)` if the secret hash is larger than the field size (~254 bits).

## Buckets and the rolling hash

Every message inserted into the Inbox extends a rolling hash: a truncated sha256 chain over the message leaves, which
the rollup circuits recompute and L1 checks when a checkpoint is proposed. Messages are grouped into **buckets**: a
bucket holds the messages sent within a single L1 block (up to a per-bucket maximum, after which further messages in
the same block spill into the next bucket), and buckets are identified by a dense, monotonically increasing sequence
number. A checkpoint always consumes whole buckets.

Each link of the chain is `sha256ToField(separator || previousRollingHash || leaf)` over a 4-byte big-endian domain
separator followed by the two 32-byte values. There are two separators: `DOM_SEP__INBOX_ROLLING_HASH_BUCKET_START` is
used when the leaf is the first message of its bucket, and `DOM_SEP__INBOX_ROLLING_HASH` for every other message. The
chain therefore commits to how the messages were packed into buckets, not only to their order: the same messages
regrouped across a different set of L1 blocks produce a different rolling hash. The genesis value is zero.

## View functions

These functions allow you to query the current state of the Inbox.

| Function | Returns | Description |
| -------------------------- | ----------------- | ------------------------------------------------ |
| `getState()` | `InboxState` | Returns the current inbox state (rolling hash, total messages inserted). |
| `getState()` | `InboxState` | Returns the current inbox state (rolling hash, total messages inserted, current bucket sequence). |
| `getTotalMessagesInserted()` | `uint64` | Returns the total number of messages inserted into the inbox. |
| `getCurrentBucketSeq()` | `uint64` | Returns the sequence number of the bucket messages are currently absorbed into. |
| `getBucket(uint256 seq)` | `InboxBucket` | Returns the snapshot of the bucket with the given sequence number (rolling hash, cumulative and per-bucket message counts, opening timestamp). Reverts if the bucket is outside the ring the Inbox retains. |
| `getFeeAssetPortal()` | `address` | Returns the address of the Fee Juice portal. |

## Related pages
Expand Down
12 changes: 6 additions & 6 deletions l1-contracts/gas_report.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"contract": "src/core/messagebridge/Inbox.sol:Inbox",
"deployment": {
"gas": 0,
"size": 6805
"size": 6859
},
"functions": {
"getBucket(uint256)": {
Expand Down Expand Up @@ -43,10 +43,10 @@
},
"sendL2Message((bytes32,uint256),bytes32,bytes32)": {
"calls": 37409,
"min": 43269,
"mean": 46588,
"median": 43269,
"max": 102063
"min": 43360,
"mean": 46678,
"median": 43360,
"max": 102144
}
}
},
Expand Down Expand Up @@ -132,7 +132,7 @@
"contract": "test/RollupWithPreheating.sol:RollupWithPreheating",
"deployment": {
"gas": 0,
"size": 43818
"size": 43872
},
"functions": {
"archive()": {
Expand Down
3 changes: 2 additions & 1 deletion l1-contracts/scripts/constants-codegen/solidity.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"FEE_JUICE_ADDRESS",
"BLS12_POINT_COMPRESSED_BYTES",
"ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH",
"DOM_SEP__INBOX_ROLLING_HASH"
"DOM_SEP__INBOX_ROLLING_HASH",
"DOM_SEP__INBOX_ROLLING_HASH_BUCKET_START"
]
69 changes: 69 additions & 0 deletions l1-contracts/scripts/inbox_rolling_hash_vectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Derives the Inbox rolling-hash reference vectors pinned by the L1, Noir and TypeScript tests.

The rolling hash is a sha256 chain over the Inbox message leaves. Each link is

h' = sha256ToField(u32_be(separator) || h(32) || leaf(32))

where `sha256ToField(x) = 0x00 || sha256(x)[0..31]` (the last digest byte is dropped so the result fits a field), and
the separator is INBOX_ROLLING_HASH_BUCKET_START when the leaf is the first message of an L1 Inbox bucket and
INBOX_ROLLING_HASH otherwise. The genesis rolling hash is zero.

This script depends on nothing but hashlib, so the vectors it prints are independent of all three implementations.
Run it and paste the values into:

- l1-contracts/test/InboxBuckets.t.sol (testRollingHashTestVectors)
- noir-projects/fnd/noir-protocol-circuits/crates/rollup-lib/src/inbox_rolling_hash.nr (tests module)
- yarn-project/stdlib/src/messaging/inbox_rolling_hash.test.ts
"""

import hashlib

# Domain separators, mirroring DOM_SEP__INBOX_ROLLING_HASH and DOM_SEP__INBOX_ROLLING_HASH_BUCKET_START in
# noir-projects/fnd/noir-protocol-circuits/crates/types/src/constants.nr. They are poseidon2 hashes of a name, so
# their derivation is checked by the Noir constants test rather than here.
LINK = 3737216265
BUCKET_START = 3204844280


def sha256_to_field(preimage: bytes) -> int:
return int.from_bytes(b"\x00" + hashlib.sha256(preimage).digest()[:31], "big")


def link(prev: int, leaf: int, opens_bucket: bool) -> int:
separator = BUCKET_START if opens_bucket else LINK
preimage = separator.to_bytes(4, "big") + prev.to_bytes(32, "big") + leaf.to_bytes(32, "big")
return sha256_to_field(preimage)


def chain(start: int, buckets: list[list[int]]) -> int:
"""Chains message leaves grouped per Inbox bucket; the first leaf of each group opens a bucket."""
acc = start
for bucket in buckets:
assert bucket, "an Inbox bucket always holds at least one message"
for i, leaf in enumerate(bucket):
acc = link(acc, leaf, i == 0)
return acc


def main() -> None:
vectors = [
("single leaf: chain(0, [[11]])", chain(0, [[11]])),
("three leaves in one bucket: chain(0, [[11, 22, 33]])", chain(0, [[11, 22, 33]])),
("256 leaves 1..=256 in one bucket", chain(0, [list(range(1, 257))])),
("non-zero start, one leaf: chain(0x2a, [[7]])", chain(0x2A, [[7]])),
("non-zero start, two leaves in one bucket: chain(0x2a, [[7, 8]])", chain(0x2A, [[7, 8]])),
("one bucket: chain(0, [[11, 22, 33, 44]])", chain(0, [[11, 22, 33, 44]])),
("two buckets: chain(0, [[11, 22], [33, 44]])", chain(0, [[11, 22], [33, 44]])),
]
for name, value in vectors:
print(f"{value:#066x} {name}")

# The pair above is the boundary-commitment vector: identical leaves, different bucket grouping.
assert chain(0, [[11, 22, 33, 44]]) != chain(0, [[11, 22], [33, 44]])
# Continuity: a chain split into segments threads the intermediate hash, flags following the leaves.
assert chain(0x2A, [[7, 8]]) == link(chain(0x2A, [[7]]), 8, False)


if __name__ == "__main__":
main()
6 changes: 4 additions & 2 deletions l1-contracts/src/core/interfaces/messagebridge/IInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ interface IInbox {
*/
struct InboxBucket {
// Rolling hash after the last message absorbed into this bucket. Each link is
// `sha256ToField(DOM_SEP__INBOX_ROLLING_HASH || previousRollingHash || leaf)`, over the 4-byte big-endian domain
// separator followed by the two 32-byte big-endian values; the genesis value is zero.
// `sha256ToField(separator || previousRollingHash || leaf)`, over the 4-byte big-endian domain separator followed
// by the two 32-byte big-endian values; the separator is `DOM_SEP__INBOX_ROLLING_HASH_BUCKET_START` for the first
// message of a bucket and `DOM_SEP__INBOX_ROLLING_HASH` for the rest, so the chain commits to the bucket
// boundaries. The genesis value is zero.
bytes32 rollingHash;
// Cumulative number of messages inserted into the Inbox up to and including this bucket.
uint64 totalMsgCount;
Expand Down
24 changes: 17 additions & 7 deletions l1-contracts/src/core/libraries/crypto/Hash.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,26 @@ library Hash {

/**
* @notice Advances the Inbox consensus rolling hash by one message leaf
* @dev Each link is `sha256ToField(DOM_SEP__INBOX_ROLLING_HASH || rollingHash || leaf)` over the 4-byte big-endian
* domain separator followed by the two 32-byte big-endian values. The separator keeps a chain link from being
* reinterpreted as an untagged two-field sha256 hash, such as an `outHash` merkle node. 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. The genesis value is zero.
* @dev Each link is `sha256ToField(separator || rollingHash || leaf)` over the 4-byte big-endian domain separator
* followed by the two 32-byte big-endian values. The separator is `DOM_SEP__INBOX_ROLLING_HASH_BUCKET_START` when
* the leaf is the first message of a bucket and `DOM_SEP__INBOX_ROLLING_HASH` otherwise, so the chain commits to
* how the messages were packed into buckets and not just to their order. Both separators keep a chain link from
* being reinterpreted as an untagged two-field sha256 hash, such as an `outHash` merkle node. 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. The genesis value is zero.
* @param _rollingHash - The current rolling hash
* @param _leaf - The message leaf to absorb
* @param _opensBucket - Whether the leaf is the first message of its bucket
* @return The updated rolling hash
*/
function accumulateInboxRollingHash(bytes32 _rollingHash, bytes32 _leaf) internal pure returns (bytes32) {
return sha256ToField(abi.encodePacked(uint32(Constants.DOM_SEP__INBOX_ROLLING_HASH), _rollingHash, _leaf));
function accumulateInboxRollingHash(bytes32 _rollingHash, bytes32 _leaf, bool _opensBucket)
internal
pure
returns (bytes32)
{
uint32 separator = _opensBucket
? uint32(Constants.DOM_SEP__INBOX_ROLLING_HASH_BUCKET_START)
: uint32(Constants.DOM_SEP__INBOX_ROLLING_HASH);
return sha256ToField(abi.encodePacked(separator, _rollingHash, _leaf));
}
}
5 changes: 4 additions & 1 deletion l1-contracts/src/core/messagebridge/Inbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ contract Inbox is IInbox {
* open reverts unless the proven chain has consumed that entry, so in-flight messages are never destroyed —
* sends halt instead until proving catches up.
*
* The first message of a bucket is tagged with its own domain separator in the rolling hash, so the chain
* commits to the bucket boundaries and not just to the message order.
*
* @param _leaf - The message leaf to absorb
*
* @return The sequence number of the bucket the leaf was absorbed into and the updated rolling hash
Expand Down Expand Up @@ -217,7 +220,7 @@ contract Inbox is IInbox {
});
}

bucket.rollingHash = Hash.accumulateInboxRollingHash(bucket.rollingHash, _leaf);
bucket.rollingHash = Hash.accumulateInboxRollingHash(bucket.rollingHash, _leaf, bucket.msgCount == 0);
bucket.totalMsgCount += 1;
bucket.msgCount += 1;
buckets[bucketSeq % BUCKET_RING_SIZE] = bucket;
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/test/Inbox.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ contract InboxTest is Test {
DataStructures.L1ToL2Msg memory message = _boundMessage(_message, globalLeafIndex);

bytes32 leaf = message.sha256ToField();
bytes32 expectedInboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), leaf);
bytes32 expectedInboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), leaf, true);
vm.expectEmit(true, true, true, true);
// event we expect
emit IInbox.MessageSent(leaf, expectedInboxRollingHash, 1, message);
Expand Down
82 changes: 62 additions & 20 deletions l1-contracts/test/InboxBuckets.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,16 @@ contract InboxBucketsTest is Test {
}

function _send(InboxHarness _inbox, uint256 _salt) internal returns (bytes32) {
uint64 seqBefore = _inbox.getCurrentBucketSeq();
(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);
// A message opens a bucket exactly when it advances the bucket sequence: the first message of an L1 block, or
// the message that spills over out of a full bucket.
bool opensBucket = _inbox.getCurrentBucketSeq() != seqBefore;
expectedRollingHash = Hash.accumulateInboxRollingHash(expectedRollingHash, leaf, opensBucket);
return leaf;
}

Expand All @@ -52,27 +56,45 @@ contract InboxBucketsTest is Test {
gasUsed = gasBefore - gasleft();
}

// 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.
// Shared test vectors for the rolling-hash chain, pinned across the noir circuits, the TS mirror, and this L1
// implementation. Derived independently of all three by `scripts/inbox_rolling_hash_vectors.py`. Leaves are
// grouped per bucket: the first leaf of each group opens a bucket.
function testRollingHashTestVectors() public pure {
bytes32 h = Hash.accumulateInboxRollingHash(bytes32(0), bytes32(uint256(11)));
assertEq(h, 0x00066dfa22681f66d50aae7d84f190e3555d2d82e4a5e33c2291c3060d441f04, "chain(0, [11])");
bytes32 h = Hash.accumulateInboxRollingHash(bytes32(0), bytes32(uint256(11)), true);
assertEq(h, 0x00551b59fed79dcce036e55050cf38ef367abfec03557e234866ac023879b245, "chain(0, [[11]])");

h = Hash.accumulateInboxRollingHash(h, bytes32(uint256(22)));
h = Hash.accumulateInboxRollingHash(h, bytes32(uint256(33)));
assertEq(h, 0x0077423b713a725ce4bf0b792847c68da87c316d52921de25652756bfe4c3e81, "chain(0, [11, 22, 33])");
h = Hash.accumulateInboxRollingHash(h, bytes32(uint256(22)), false);
h = Hash.accumulateInboxRollingHash(h, bytes32(uint256(33)), false);
assertEq(h, 0x00e6cba8a055d279f8568edc4d0969a107fcda0c48347afdfd3dfeb053aa22c7, "chain(0, [[11, 22, 33]])");

h = bytes32(0);
for (uint256 i = 1; i <= 256; i++) {
h = Hash.accumulateInboxRollingHash(h, bytes32(i));
h = Hash.accumulateInboxRollingHash(h, bytes32(i), i == 1);
}
assertEq(h, 0x0030493fcb5915459bba42f03f283b58dfaa082dac02fbb3a494d5db8063238b, "chain(0, [1..=256])");
assertEq(h, 0x009ff152cad9525e1c092ae6d4fb390149de5599eac09b76b0ebd1c6e26bb504, "chain(0, [[1..=256]])");

h = Hash.accumulateInboxRollingHash(bytes32(uint256(0x2a)), bytes32(uint256(7)));
assertEq(h, 0x0048097cafad7fed00ccb578806b3855d5ee7bf11045fb8d41b2880ba36ef28f, "chain(0x2a, [7])");
h = Hash.accumulateInboxRollingHash(bytes32(uint256(0x2a)), bytes32(uint256(7)), true);
assertEq(h, 0x00f13cb848052a7ab6f1de788a5979f5a5caa8c11cf176715d63481618e3b575, "chain(0x2a, [[7]])");

h = Hash.accumulateInboxRollingHash(h, bytes32(uint256(8)));
assertEq(h, 0x00a64d14c4b0234f5d835dc202bf8f9a857bc0734baf281dccd4b4978a48b2f9, "chain(0x2a, [7, 8])");
h = Hash.accumulateInboxRollingHash(h, bytes32(uint256(8)), false);
assertEq(h, 0x00d84d0b60599b1c7380a723d84310d40efaa4f5673dd62e0af41b03bc9a07a6, "chain(0x2a, [[7, 8]])");

// The same four leaves in one bucket and split across two buckets reach different chain positions.
h = bytes32(0);
uint256[4] memory leaves = [uint256(11), 22, 33, 44];
for (uint256 i = 0; i < 4; i++) {
h = Hash.accumulateInboxRollingHash(h, bytes32(leaves[i]), i == 0);
}
assertEq(h, 0x00e37b7cc5526ab379c54209bc1c6a4ba2c457d024330281b97a533561701551, "chain(0, [[11, 22, 33, 44]])");

bytes32 split = bytes32(0);
for (uint256 i = 0; i < 4; i++) {
split = Hash.accumulateInboxRollingHash(split, bytes32(leaves[i]), i == 0 || i == 2);
}
assertEq(
split, 0x00fa0346e7c4ee1bdf29a48af28182fdc236e2936e4d0c2e951dbd4b9b6464fc, "chain(0, [[11, 22], [33, 44]])"
);
assertTrue(h != split, "bucket boundaries change the chain");
}

function testGenesisBucket() public {
Expand Down Expand Up @@ -101,12 +123,13 @@ contract InboxBucketsTest is Test {
}

function testAccumulationWithinSingleBlock() public {
// Only the first message of the block opens a bucket; the rest continue it.
bytes32 leaf1 = _send(inbox, 1);
bytes32 chain1 = Hash.accumulateInboxRollingHash(bytes32(0), leaf1);
bytes32 chain1 = Hash.accumulateInboxRollingHash(bytes32(0), leaf1, true);
bytes32 leaf2 = _send(inbox, 2);
bytes32 chain2 = Hash.accumulateInboxRollingHash(chain1, leaf2);
bytes32 chain2 = Hash.accumulateInboxRollingHash(chain1, leaf2, false);
bytes32 leaf3 = _send(inbox, 3);
bytes32 chain3 = Hash.accumulateInboxRollingHash(chain2, leaf3);
bytes32 chain3 = Hash.accumulateInboxRollingHash(chain2, leaf3, false);

assertEq(inbox.getCurrentBucketSeq(), 1, "all messages share one bucket");

Expand All @@ -117,6 +140,25 @@ contract InboxBucketsTest is Test {
assertEq(bucket.msgCount, 3, "bucket msg count");
}

function testBucketBoundariesChangeTheChain() public {
// Two messages sent in one L1 block share a bucket; the same two messages one L1 block apart open two buckets.
// The leaves are identical either way, so only the bucket-start separator tells the two histories apart.
InboxHarness oneBucket = _deployInbox(TestConstants.AZTEC_INBOX_BUCKET_RING_SIZE);
bytes32 leafA = _send(oneBucket, 1);
bytes32 leafB = _send(oneBucket, 2);
assertEq(oneBucket.getCurrentBucketSeq(), 1, "both messages in one bucket");
bytes32 oneBucketHash = oneBucket.getState().rollingHash;

InboxHarness twoBuckets = _deployInbox(TestConstants.AZTEC_INBOX_BUCKET_RING_SIZE);
assertEq(_send(twoBuckets, 1), leafA, "same first leaf");
vm.roll(block.number + 1);
vm.warp(block.timestamp + 12);
assertEq(_send(twoBuckets, 2), leafB, "same second leaf");
assertEq(twoBuckets.getCurrentBucketSeq(), 2, "one bucket per block");

assertTrue(oneBucketHash != twoBuckets.getState().rollingHash, "packing is committed to");
}

function testStateReturnsCurrentPositionAtomically() public {
// Genesis: nothing inserted, bucket 0 current, zero rolling hash.
IInbox.InboxState memory state = inbox.getState();
Expand Down Expand Up @@ -161,7 +203,7 @@ contract InboxBucketsTest is Test {
index: inbox.getState().totalMessagesInserted
});
bytes32 leaf = Hash.sha256ToField(message);
bytes32 inboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), leaf);
bytes32 inboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), leaf, true);

vm.expectEmit(true, true, true, true, address(inbox));
emit IInbox.MessageSent(leaf, inboxRollingHash, 1, message);
Expand Down Expand Up @@ -189,7 +231,7 @@ contract InboxBucketsTest is Test {

// 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, Hash.accumulateInboxRollingHash(bucket1.rollingHash, leaf3, true), "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");
Expand All @@ -210,7 +252,7 @@ contract InboxBucketsTest is Test {
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.rollingHash, Hash.accumulateInboxRollingHash(bucket1.rollingHash, leaf, true), "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");
Expand Down
3 changes: 2 additions & 1 deletion l1-contracts/test/InboxBucketsFuzz.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ contract InboxBucketsFuzzTest is Test {
assertLe(bucket.msgCount, cap, "per-bucket cap");

for (uint256 i = 0; i < bucket.msgCount; i++) {
rollingHash = Hash.accumulateInboxRollingHash(rollingHash, leaves[counted + i]);
// The first message of each bucket opens it and so takes the bucket-start separator.
rollingHash = Hash.accumulateInboxRollingHash(rollingHash, leaves[counted + i], i == 0);
}
counted += bucket.msgCount;

Expand Down
Loading
Loading