Skip to content
Open
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
1 change: 1 addition & 0 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ library Errors {
error Rollup__InvalidCheckpointNumber(uint256 expected, uint256 actual); // 0xd1ba9bfa
error Rollup__InvalidInHash(bytes32 expected, bytes32 actual); // 0xcd6f4233
error Rollup__InvalidInboxRollingHash(bytes32 expected, bytes32 actual); // 0xed1f7bb5
error Rollup__InvalidPreviousInboxRollingHash(bytes32 expected, bytes32 actual); // 0x2fe7cae5
error Rollup__UnconsumedInboxMessages(uint256 nextBucketSeq); // 0x2bd4bf10
error Rollup__InboxConsumptionBehindParent(uint256 expected, uint256 actual); // 0x54e0c025
error Rollup__TooManyInboxMessagesConsumed(uint256 consumed); // 0xf76d1426
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ struct TempCheckpointLog {
bytes32 payloadDigest;
Slot slotNumber;
FeeHeader feeHeader;
// Streaming Inbox consumption record (AZIP-22 Fast Inbox). `inboxRollingHash` is the consensus rolling hash the
// checkpoint header committed to; `inboxMsgTotal` is the cumulative Inbox message count consumed as of this
// checkpoint (the child's parent-total origin); `inboxConsumedBucket` is the bucket sequence number the header's
// rolling hash corresponds to. The two counts pack into a single storage slot.
bytes32 inboxRollingHash;
uint64 inboxMsgTotal;
uint64 inboxConsumedBucket;
}

struct CompressedTempCheckpointLog {
Expand All @@ -45,6 +52,9 @@ struct CompressedTempCheckpointLog {
bytes32 payloadDigest;
CompressedSlot slotNumber;
CompressedFeeHeader feeHeader;
bytes32 inboxRollingHash;
uint64 inboxMsgTotal;
uint64 inboxConsumedBucket;
}

library CompressedTempCheckpointLogLib {
Expand All @@ -61,7 +71,10 @@ library CompressedTempCheckpointLogLib {
attestationsHash: _checkpoint.attestationsHash,
payloadDigest: _checkpoint.payloadDigest,
slotNumber: _checkpoint.slotNumber.compress(),
feeHeader: _checkpoint.feeHeader.compress()
feeHeader: _checkpoint.feeHeader.compress(),
inboxRollingHash: _checkpoint.inboxRollingHash,
inboxMsgTotal: _checkpoint.inboxMsgTotal,
inboxConsumedBucket: _checkpoint.inboxConsumedBucket
});
}

Expand All @@ -77,7 +90,10 @@ library CompressedTempCheckpointLogLib {
attestationsHash: _compressedCheckpoint.attestationsHash,
payloadDigest: _compressedCheckpoint.payloadDigest,
slotNumber: _compressedCheckpoint.slotNumber.decompress(),
feeHeader: _compressedCheckpoint.feeHeader.decompress()
feeHeader: _compressedCheckpoint.feeHeader.decompress(),
inboxRollingHash: _compressedCheckpoint.inboxRollingHash,
inboxMsgTotal: _compressedCheckpoint.inboxMsgTotal,
inboxConsumedBucket: _compressedCheckpoint.inboxConsumedBucket
});
}
}
22 changes: 19 additions & 3 deletions l1-contracts/src/core/libraries/rollup/EpochProofLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,22 @@ library EpochProofLib {
expectedEndArchive == _args.endArchive, Errors.Rollup__InvalidArchive(expectedEndArchive, _args.endArchive)
);
}

{
// Start-boundary anchoring for the Inbox rolling-hash chain (AZIP-22 Fast Inbox), mirroring previousArchive:
// the proof's claimed chain start must match the rolling hash recorded at propose for checkpoint _start - 1.
// No end-side check is needed: the checkpoint root writes the parity end into both the checkpoint header and
// end_inbox_rolling_hash, checkpoint merges assert start/end continuity, and verifyHeaders pins the supplied
// headers (whose hash covers inboxRollingHash) to the stored header hashes, so a wrong endInboxRollingHash
// fails proof verification.
bytes32 expectedPreviousInboxRollingHash = STFLib.getInboxRollingHash(_start - 1);
require(
expectedPreviousInboxRollingHash == _args.previousInboxRollingHash,
Errors.Rollup__InvalidPreviousInboxRollingHash(
expectedPreviousInboxRollingHash, _args.previousInboxRollingHash
)
);
}
}

// The fee recipient/value below are sourced from the supplied headers, so the header hashes must be validated here
Expand Down Expand Up @@ -216,9 +232,9 @@ library EpochProofLib {

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.
// Inbox rolling-hash chain segment consumed across the epoch (AZIP-22 Fast Inbox). The start is validated above
// against the record written at propose for checkpoint _start - 1; the end is pinned transitively through the
// stored checkpoint header hashes (see the anchoring block in assertAcceptable).
publicInputs[3] = _args.previousInboxRollingHash;
publicInputs[4] = _args.endInboxRollingHash;
}
Expand Down
40 changes: 30 additions & 10 deletions l1-contracts/src/core/libraries/rollup/ProposeLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {CompressedSlot, CompressedTimeMath} from "@aztec/shared/libraries/Compre
import {Signature} from "@aztec/shared/libraries/SignatureLib.sol";
import {ProposedHeader, ProposedHeaderLib} from "./ProposedHeaderLib.sol";
import {STFLib} from "./STFLib.sol";
import {SafeCast} from "@oz/utils/math/SafeCast.sol";

// Streaming-inbox protocol constants (AZIP-22 Fast Inbox). These mirror the protocol circuit constants and
// should move into the generated Constants library once the Solidity emitter includes them.
Expand All @@ -33,6 +34,10 @@ struct ProposeArgs {
bytes32 archive;
OracleInput oracleInput;
ProposedHeader header;
// Sequence number of the Inbox bucket the header's `inboxRollingHash` corresponds to (AZIP-22 Fast Inbox).
// Unsigned lookup aid kept out of the attested payload digest: a wrong hint can only revert, never change what is
// accepted, since integrity comes from the rolling-hash equality check against the committee-signed header.
uint256 bucketHint;
}

struct ProposePayload {
Expand All @@ -46,7 +51,9 @@ struct InterimProposeValues {
bytes32[] blobHashes;
bytes32 blobsHashesCommitment;
bytes[] blobCommitments;
bytes32 inHash;
bytes32 blobCommitmentsHash;
FeeHeader feeHeader;
uint256 consumedInboxMsgTotal;
bytes32 headerHash;
bytes32 attestationsHash;
bytes32 payloadDigest;
Expand Down Expand Up @@ -121,6 +128,7 @@ library ProposeLib {
using TimeLib for Epoch;
using CompressedTimeMath for CompressedSlot;
using ChainTipsLib for CompressedChainTips;
using SafeCast for uint256;

/**
* @notice Publishes a new checkpoint to the pending chain.
Expand Down Expand Up @@ -261,17 +269,31 @@ library ProposeLib {
uint256 checkpointNumber = tips.getPending() + 1;
tips = tips.updatePending(checkpointNumber);

// Validate the streaming Inbox consumption against the parent checkpoint's consumed position (AZIP-22 Fast
// Inbox). The parent is checkpointNumber - 1, always available: checkpoint 0 carries the {0,0,0} genesis base
// case written at initialization. rollupStore.tips is not committed until below, so the parent read still sees
// the parent as the pending tip. The returned cumulative total is stored in this checkpoint's record so its
// child validates against it and, since temp-log records rewind with the pending chain on a prune, the record
// stays prune-consistent.
v.consumedInboxMsgTotal = validateInboxConsumption(
rollupStore.config.inbox,
v.header.inboxRollingHash,
_args.bucketHint,
v.header.slotNumber,
STFLib.getInboxMsgTotal(checkpointNumber - 1)
);

// Calculate accumulated blob commitments hash for this checkpoint
// Blob commitments are collected and proven per root rollup proof (per epoch),
// so we need to know whether we are at the epoch start:
v.isFirstCheckpointOfEpoch =
v.currentEpoch > STFLib.getEpochForCheckpoint(checkpointNumber - 1) || checkpointNumber == 1;
bytes32 blobCommitmentsHash = BlobLib.calculateBlobCommitmentsHash(
v.blobCommitmentsHash = BlobLib.calculateBlobCommitmentsHash(
STFLib.getBlobCommitmentsHash(checkpointNumber - 1), v.blobCommitments, v.isFirstCheckpointOfEpoch
);

// Compute fee header for checkpoint metadata
FeeHeader memory feeHeader = FeeLib.computeFeeHeader(
v.feeHeader = FeeLib.computeFeeHeader(
checkpointNumber,
_args.oracleInput.feeAssetPriceModifier,
v.header.totalManaUsed,
Expand All @@ -289,20 +311,18 @@ library ProposeLib {
STFLib.addTempCheckpointLog(
TempCheckpointLog({
headerHash: v.headerHash,
blobCommitmentsHash: blobCommitmentsHash,
blobCommitmentsHash: v.blobCommitmentsHash,
outHash: v.header.outHash,
attestationsHash: v.attestationsHash,
payloadDigest: v.payloadDigest,
slotNumber: v.header.slotNumber,
feeHeader: feeHeader
feeHeader: v.feeHeader,
inboxRollingHash: v.header.inboxRollingHash,
inboxMsgTotal: v.consumedInboxMsgTotal.toUint64(),
inboxConsumedBucket: _args.bucketHint.toUint64()
})
);

// Consume pending L1->L2 messages and validate against header commitment
// @note The checkpoint number here will always be >=1 as the genesis checkpoint is at 0
v.inHash = rollupStore.config.inbox.consume(checkpointNumber);
require(v.header.inHash == v.inHash, Errors.Rollup__InvalidInHash(v.inHash, v.header.inHash));

{
bytes32 archive = _args.archive;
if (v.isEscapeHatch) {
Expand Down
27 changes: 26 additions & 1 deletion l1-contracts/src/core/libraries/rollup/STFLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,12 @@ library STFLib {
slotNumber: Slot.wrap(0),
feeHeader: FeeHeader({
excessMana: 0, manaUsed: 0, ethPerFeeAsset: _initialEthPerFeeAsset, congestionCost: 0, proverCost: 0
})
}),
// Genesis Inbox consumption base case, matching the Inbox's genesis bucket-0 sentinel {0, 0, 0}, so
// checkpoint 1 validates its consumption against it (AZIP-22 Fast Inbox).
inboxRollingHash: bytes32(0),
inboxMsgTotal: 0,
inboxConsumedBucket: 0
}).compress();
}

Expand Down Expand Up @@ -305,6 +310,26 @@ library STFLib {
return getStorageTempCheckpointLog(_checkpointNumber).slotNumber.decompress();
}

/**
* @notice Retrieves the cumulative Inbox message count consumed as of a checkpoint (AZIP-22 Fast Inbox)
* @dev Gas-efficient accessor reading only the streaming-inbox consumed total. Reverts if the checkpoint is stale.
* @param _checkpointNumber The checkpoint number to get the consumed total for
* @return The cumulative Inbox message count consumed as of the checkpoint
*/
function getInboxMsgTotal(uint256 _checkpointNumber) internal view returns (uint64) {
return getStorageTempCheckpointLog(_checkpointNumber).inboxMsgTotal;
}

/**
* @notice Retrieves the Inbox rolling hash a checkpoint committed to (AZIP-22 Fast Inbox)
* @dev Gas-efficient accessor reading only the streaming-inbox rolling hash. Reverts if the checkpoint is stale.
* @param _checkpointNumber The checkpoint number to get the rolling hash for
* @return The consensus Inbox rolling hash recorded for the checkpoint
*/
function getInboxRollingHash(uint256 _checkpointNumber) internal view returns (bytes32) {
return getStorageTempCheckpointLog(_checkpointNumber).inboxRollingHash;
}

/**
* @notice Gets the effective pending checkpoint number based on pruning eligibility
* @dev Returns either the pending checkpoint number or proven checkpoint number depending on
Expand Down
8 changes: 4 additions & 4 deletions l1-contracts/src/core/messagebridge/Inbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,10 @@ contract Inbox is IInbox {
currentTree = trees[inProgress];
}

// this is the global leaf index and not index in the checkpoint subtree
// such that users can simply use it and don't need access to a node if they are to consume it in public.
// trees are constant size so global index = tree number * size + subtree index
uint256 index = (inProgress - Constants.INITIAL_CHECKPOINT_NUMBER) * SIZE + currentTree.nextIndex;
// Compact cumulative message index (AZIP-22 Fast Inbox): the zero-based position of this message in the Inbox's
// insertion order, equal to the number of messages inserted before it. It is embedded in the leaf preimage and
// matches the streaming L1-to-L2 tree's leaf count, so consumers do not need per-checkpoint tree geometry.
uint256 index = totalMessagesInserted;

// If the sender is the fee asset portal, we use a magic address to simpler have it initialized at genesis.
// We assume that no-one will know the private key for this address and that the precompile won't change to
Expand Down
3 changes: 2 additions & 1 deletion l1-contracts/test/Inbox.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ contract InboxTest is Test {

function testFuzzInsert(DataStructures.L1ToL2Msg memory _message) public checkInvariant {
Inbox.InboxState memory stateBefore = inbox.getState();
uint256 globalLeafIndex = (FIRST_REAL_TREE_NUM - 1) * SIZE;
// Compact cumulative index (AZIP-22 Fast Inbox): the message's index is the count inserted before it.
uint256 globalLeafIndex = stateBefore.totalMessagesInserted;
DataStructures.L1ToL2Msg memory message = _boundMessage(_message, globalLeafIndex);

bytes32 leaf = message.sha256ToField();
Expand Down
3 changes: 2 additions & 1 deletion l1-contracts/test/InboxBuckets.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ contract InboxBucketsTest is Test {
recipient: recipient,
content: content,
secretHash: secretHash,
index: (FIRST_REAL_TREE_NUM - 1) * (2 ** HEIGHT)
// Compact cumulative index (AZIP-22 Fast Inbox): the first message against a fresh Inbox has index 0.
index: inbox.getState().totalMessagesInserted
});
bytes32 leaf = Hash.sha256ToField(message);
bytes16 legacyHash = bytes16(keccak256(abi.encodePacked(inbox.getState().rollingHash, leaf)));
Expand Down
Loading
Loading