Skip to content
Merged
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 @@ -4,80 +4,78 @@ description: Learn about the outbox mechanism in Aztec portals for sending messa
tags: [portals, contracts]
---

The `Outbox` is a contract deployed on L1 that handles message passing from L2 to L1. Portal contracts call `consume()` to receive and process messages that were sent from L2 contracts. The Rollup contract inserts message roots via `insert()` when checkpoints are proven.
The `Outbox` is a contract deployed on L1 that handles message passing from L2 to L1. Portal contracts call `consume()` to receive and process messages that were sent from L2 contracts. The Rollup contract inserts message roots via `insert()` when epochs are proven.

**Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/l1-contracts/src/core/messagebridge/Outbox.sol).

## `insert()`

Inserts the root of a merkle tree containing all of the L2 to L1 messages in a checkpoint. This function is only callable by the Rollup contract.
Inserts the root of a merkle tree containing all of the L2 to L1 messages in an epoch. This function is only callable by the Rollup contract.

#include_code outbox_insert l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity

| Name | Type | Description |
| ------------------- | --------- | ---------------------------------------------------------------------- |
| `_checkpointNumber` | `uint256` | The checkpoint number in which the L2 to L1 messages reside |
| `_root` | `bytes32` | The merkle root of the tree where all the L2 to L1 messages are leaves |
| Name | Type | Description |
| -------------- | --------- | ---------------------------------------------------------------------- |
| `_epochNumber` | `uint256` | The epoch number in which the L2 to L1 messages reside |
| `_root` | `bytes32` | The merkle root of the tree where all the L2 to L1 messages are leaves |

### Edge cases

- Will revert with `Outbox__Unauthorized()` if `msg.sender != ROLLUP_CONTRACT`.
- Will revert with `Outbox__CheckpointAlreadyProven(uint256 checkpointNumber)` if the checkpoint has already been proven.

## `consume()`

Allows a recipient to consume a message from the `Outbox`.

#include_code outbox_consume l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity

| Name | Type | Description |
| ------------------- | ----------- | -------------------------------------------------------------------------------------------- |
| `_message` | `L2ToL1Msg` | The L2 to L1 message to consume |
| `_checkpointNumber` | `uint256` | The checkpoint number specifying the checkpoint that contains the message to consume |
| `_leafIndex` | `uint256` | The index inside the merkle tree where the message is located |
| `_path` | `bytes32[]` | The sibling path used to prove inclusion of the message |
| Name | Type | Description |
| -------------- | ----------- | -------------------------------------------------------------------------- |
| `_message` | `L2ToL1Msg` | The L2 to L1 message to consume |
| `_epochNumber` | `uint256` | The epoch number specifying the epoch that contains the message to consume |
| `_leafIndex` | `uint256` | The index inside the merkle tree where the message is located |
| `_path` | `bytes32[]` | The sibling path used to prove inclusion of the message |

### Edge cases

- Will revert with `Outbox__PathTooLong()` if the path length is >= 256.
- Will revert with `Outbox__LeafIndexOutOfBounds(uint256 leafIndex, uint256 pathLength)` if the leaf index exceeds the tree capacity for the given path length.
- Will revert with `Outbox__CheckpointNotProven(uint256 checkpointNumber)` if the checkpoint has not been proven yet.
- Will revert with `Outbox__VersionMismatch(uint256 expected, uint256 actual)` if the message version does not match the Outbox version.
- Will revert with `Outbox__InvalidRecipient(address expected, address actual)` if `msg.sender != _message.recipient.actor`.
- Will revert with `Outbox__InvalidChainId()` if `block.chainid != _message.recipient.chainId`.
- Will revert with `Outbox__NothingToConsumeAtCheckpoint(uint256 checkpointNumber)` if the root for the checkpoint has not been set.
- Will revert with `Outbox__AlreadyNullified(uint256 checkpointNumber, uint256 leafIndex)` if the message has already been consumed.
- Will revert with `Outbox__NothingToConsumeAtEpoch(uint256 epochNumber)` if the root for the epoch has not been set.
- Will revert with `Outbox__AlreadyNullified(uint256 epochNumber, uint256 leafIndex)` if the message has already been consumed.
- Will revert with `MerkleLib__InvalidIndexForPathLength()` if the leaf index has bits set beyond the tree height.
- Will revert with `MerkleLib__InvalidRoot(bytes32 expected, bytes32 actual, bytes32 leaf, uint256 leafIndex)` if the merkle proof verification fails.

## `hasMessageBeenConsumedAtCheckpoint()`
## `hasMessageBeenConsumedAtEpoch()`

Checks if an L2 to L1 message in a specific checkpoint has been consumed.
Checks if an L2 to L1 message in a specific epoch has been consumed.

#include_code outbox_has_message_been_consumed_at_checkpoint_and_index l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity
#include_code outbox_has_message_been_consumed_at_epoch_and_index l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity

| Name | Type | Description |
| ------------------- | --------- | ------------------------------------------------------------------------------------------------------- |
| `_checkpointNumber` | `uint256` | The checkpoint number specifying the checkpoint that contains the message to check |
| `_leafId` | `uint256` | The unique id of the message leaf |
| Name | Type | Description |
| -------------- | --------- | ------------------------------------------------------------------------ |
| `_epochNumber` | `uint256` | The epoch number specifying the epoch that contains the message to check |
| `_leafId` | `uint256` | The unique id of the message leaf |

### Edge cases

- This function does not throw. Out-of-bounds access is considered valid, but will always return false.

## `getRootData()`

Returns the merkle root for a given checkpoint number. Returns `bytes32(0)` if the checkpoint has not been proven.
Returns the merkle root for a given epoch number. Returns `bytes32(0)` if the epoch has not been proven.

```solidity
function getRootData(uint256 _checkpointNumber) external view returns (bytes32);
function getRootData(uint256 _epochNumber) external view returns (bytes32);
```

| Name | Type | Description |
| ------------------- | --------- | ------------------------------------------------ |
| `_checkpointNumber` | `uint256` | The checkpoint number to fetch the root data for |
| Name | Type | Description |
| -------------- | --------- | ------------------------------------------- |
| `_epochNumber` | `uint256` | The epoch number to fetch the root data for |

**Returns**: The merkle root of the L2 to L1 message tree for the checkpoint, or `bytes32(0)` if not proven.
**Returns**: The merkle root of the L2 to L1 message tree for the epoch, or `bytes32(0)` if not proven.

## Related pages

Expand Down
31 changes: 31 additions & 0 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,37 @@ Additionally, any function or struct that previously referenced an L2 block numb

Note: current node softwares still produce exactly one L2 block per checkpoint, so for now checkpoint numbers and L2 block numbers remain equal. This may change once multi-block checkpoints are enabled.

### [L1 Contracts] L2-to-L1 messages are now grouped by epoch.

L2-to-L1 messages are now aggregated and organized per epoch rather than per block. This change affects how you compute membership witnesses for consuming messages on L1. You now need to know the epoch number in which the message was emitted to retrieve and consume the message.

**Note**: This is only an API change. The protocol behavior remains the same - messages can still only be consumed once an epoch is proven as before.

#### What changed

Previously, you might have computed the membership witness without explicitly needing the epoch:

```typescript
const witness = await computeL2ToL1MembershipWitness(
node,
l2TxReceipt.blockNumber,
l2ToL1Message
);
```

Now, you should provide the epoch number:

```typescript
const epoch = await rollup.getEpochNumberForCheckpoint(
CheckpointNumber.fromBlockNumber(l2TxReceipt.blockNumber)
);
const witness = await computeL2ToL1MembershipWitness(
node,
epoch,
l2ToL1Message
);
```

### [Aztec.js] Wallet interface changes

#### `simulateTx` is now batchable
Expand Down
1 change: 1 addition & 0 deletions l1-contracts/src/core/interfaces/IRollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {IERC20} from "@oz/token/ERC20/IERC20.sol";
struct PublicInputArgs {
bytes32 previousArchive;
bytes32 endArchive;
bytes32 outHash;
address proverId;
}

Expand Down
45 changes: 20 additions & 25 deletions l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pragma solidity >=0.8.27;

import {DataStructures} from "../../libraries/DataStructures.sol";
import {Epoch} from "../../libraries/TimeLib.sol";

/**
* @title IOutbox
Expand All @@ -11,21 +12,18 @@ import {DataStructures} from "../../libraries/DataStructures.sol";
* and will be consumed by the portal contracts.
*/
interface IOutbox {
event RootAdded(uint256 indexed checkpointNumber, bytes32 indexed root);
event MessageConsumed(
uint256 indexed checkpointNumber, bytes32 indexed root, bytes32 indexed messageHash, uint256 leafId
);
event RootAdded(Epoch indexed epoch, bytes32 indexed root);
event MessageConsumed(Epoch indexed epoch, bytes32 indexed root, bytes32 indexed messageHash, uint256 leafId);

// docs:start:outbox_insert
/**
* @notice Inserts the root of a merkle tree containing all of the L2 to L1 messages in
* a checkpoint specified by _checkpointNumber.
* @notice Inserts the root of a merkle tree containing all of the L2 to L1 messages in an epoch specified by _epoch.
* @dev Only callable by the rollup contract
* @dev Emits `RootAdded` upon inserting the root successfully
* @param _checkpointNumber - The checkpoint number in which the L2 to L1 messages reside
* @param _epoch - The epoch in which the L2 to L1 messages reside
* @param _root - The merkle root of the tree where all the L2 to L1 messages are leaves
*/
function insert(uint256 _checkpointNumber, bytes32 _root) external;
function insert(Epoch _epoch, bytes32 _root) external;
// docs:end:outbox_insert

// docs:start:outbox_consume
Expand All @@ -34,39 +32,36 @@ interface IOutbox {
* @dev Only useable by portals / recipients of messages
* @dev Emits `MessageConsumed` when consuming messages
* @param _message - The L2 to L1 message
* @param _checkpointNumber - The checkpoint number specifying the checkpoint that contains the message we want to
* consume
* @param _leafIndex - The index inside the merkle tree where the message is located
* @param _path - The sibling path used to prove inclusion of the message, the _path length directly depends
* on the total amount of L2 to L1 messages in the checkpoint. i.e. the length of _path is equal to the depth of the
* L1 to L2 message tree.
* @param _epoch - The epoch that contains the message we want to consume
* @param _leafIndex - The index at the level in the epoch message tree where the message is located
* @param _path - The sibling path used to prove inclusion of the message, the _path length depends
* on the location of the L2 to L1 message in the epoch message tree.
*/
function consume(
DataStructures.L2ToL1Msg calldata _message,
uint256 _checkpointNumber,
Epoch _epoch,
uint256 _leafIndex,
bytes32[] calldata _path
) external;
// docs:end:outbox_consume

// docs:start:outbox_has_message_been_consumed_at_checkpoint_and_index
// docs:start:outbox_has_message_been_consumed_at_epoch_and_index
/**
* @notice Checks to see if an L2 to L1 message in a specific checkpoint has been consumed
* @notice Checks to see if an L2 to L1 message in a specific epoch has been consumed
* @dev - This function does not throw. Out-of-bounds access is considered valid, but will always return false
* @param _checkpointNumber - The checkpoint number specifying the checkpoint that contains the message we want to
* check
* @param _epoch - The epoch that contains the message we want to check
* @param _leafId - The unique id of the message leaf
*/
function hasMessageBeenConsumedAtCheckpoint(uint256 _checkpointNumber, uint256 _leafId) external view returns (bool);
// docs:end:outbox_has_message_been_consumed_at_checkpoint_and_index
function hasMessageBeenConsumedAtEpoch(Epoch _epoch, uint256 _leafId) external view returns (bool);
// docs:end:outbox_has_message_been_consumed_at_epoch_and_index

/**
* @notice Fetch the root data for a given checkpoint number
* Returns (0, 0) if the checkpoint is not proven
* @notice Fetch the root data for a given epoch
* Returns (0, 0) if the epoch is not proven
*
* @param _checkpointNumber - The checkpoint number to fetch the root data for
* @param _epoch - The epoch to fetch the root data for
*
* @return bytes32 - The root of the merkle tree containing the L2 to L1 messages
*/
function getRootData(uint256 _checkpointNumber) external view returns (bytes32);
function getRootData(Epoch _epoch) external view returns (bytes32);
}
2 changes: 1 addition & 1 deletion l1-contracts/src/core/libraries/ConstantsGen.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ library Constants {
14_269_942_583_723_164_841_365_114_274_712_143_548_835_546_030_057_296_325_580_016_468_921_911_294_613;
uint256 internal constant FEE_JUICE_ADDRESS = 5;
uint256 internal constant BLS12_POINT_COMPRESSED_BYTES = 48;
uint256 internal constant ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH = 158;
uint256 internal constant ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH = 159;
uint256 internal constant NUM_MSGS_PER_BASE_PARITY = 256;
uint256 internal constant NUM_BASE_PARITY_PER_ROOT_PARITY = 4;
}
7 changes: 2 additions & 5 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,9 @@ library Errors {
uint32 storedDeadline,
uint32 deadlinePassed
); // 0x5e789f34
error Outbox__RootAlreadySetAtCheckpoint(uint256 checkpointNumber); // 0x6eb83cef
error Outbox__InvalidRecipient(address expected, address actual); // 0x57aad581
error Outbox__AlreadyNullified(uint256 checkpointNumber, uint256 leafIndex); // 0xfd71c2d4
error Outbox__NothingToConsumeAtCheckpoint(uint256 checkpointNumber); // 0x0279277d
error Outbox__CheckpointNotProven(uint256 checkpointNumber); // 0x104bcfc1
error Outbox__CheckpointAlreadyProven(uint256 checkpointNumber);
error Outbox__AlreadyNullified(Epoch epoch, uint256 leafIndex); // 0xfd71c2d4
error Outbox__NothingToConsumeAtEpoch(Epoch epoch); // 0x5e3d32ce
error Outbox__PathTooLong();
error Outbox__LeafIndexOutOfBounds(uint256 leafIndex, uint256 pathLength);

Expand Down
26 changes: 20 additions & 6 deletions l1-contracts/src/core/libraries/rollup/EpochProofLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {STFLib} from "@aztec/core/libraries/rollup/STFLib.sol";
import {ValidatorSelectionLib} from "@aztec/core/libraries/rollup/ValidatorSelectionLib.sol";
import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol";
import {CompressedSlot, CompressedTimeMath} from "@aztec/shared/libraries/CompressedTimeMath.sol";
import {Math} from "@oz/utils/math/Math.sol";
import {SafeCast} from "@oz/utils/math/SafeCast.sol";

/**
Expand Down Expand Up @@ -117,7 +116,20 @@ library EpochProofLib {
require(verifyEpochRootProof(_args), Errors.Rollup__InvalidProof());

RollupStore storage rollupStore = STFLib.getStorage();
rollupStore.tips = rollupStore.tips.updateProven(Math.max(rollupStore.tips.getProven(), _args.end));

// Advance the proven block number and insert the out hash if the chain is extended.
if (_args.end > rollupStore.tips.getProven()) {
rollupStore.tips = rollupStore.tips.updateProven(_args.end);

// Handle L2->L1 message processing.
// The circuit outputs a zero out hash if the epoch contains no messages. It is also impossible for a partial
// epoch to produce a non-zero out hash, then later produce a zero out hash once more checkpoints are included.
// Therefore, we can safely skip the insertion for a zero out hash here.
if (_args.args.outHash != bytes32(0)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as the circuits are sound, there should be no case where it is possible for us to end up needing to overwrite an outhash with 0 if it was previously non-zero.

This is kinda more a comment for myself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to also make a note that if the tree is empty it will return 0 here, as that is not always the case. But having that makes it possible for us to skip the "isIgnition" check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️

I think you still have the issue last discussed. There are outhashes in the proposalHeader again, but I'm not sure that they are actually linked to the eventual outHash that one seem to be provided by the prover? So if the circuits are broken what is stopping someone from proposing A_1, A_2, A_n, and then the prover posting B_1 as the outhash? I'm probably just missing something here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you are right, if the circuits are broken, there's nothing stopping someone to post a completely different out hash via the root rollup proof. To actually use the out hashes in the proposed headers to validate the root out hash, I think we have to store the out hash in the block log. And when proving an epoch, we could either:

  • compute the tree root with all out hashes and compare it with the value in public inputs.
  • or change the out hash in the header to be the "accumulated" out hash. And compare the root out hash with the out hash in the last proposed header.

I think the second one is better because it's cheaper. Although it means the checkpoint header now depends on the state of the previous checkpoints in the same epoch, making it slightly complicated to compute and verify. But when we move to multiple blocks per checkpoint, the similar situation will apply to the block headers anyway.

Are we good with adding the out hash to the block log?

Or any other ideas?

On a similar note, if the circuits are broken, it would allow a prover to set a high fee in the root rollup circuit's public inputs. Do we want to do something about this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think storing it in the block log is acceptable. Since we are doing circular storage there it would add ~3K on a propose, but it is one of the things that can be optimized by storing just the hash of the blocklog.

I think as you say, that solution 2 is probably the way to go if we anyway would need to do that for multiple blocks per checkpoint.

If circuits are broken, it is acceptable that the fee asset controlled by them run into issues. The damage is much less troublesome than the outbox generally

// Insert L2->L1 messages root into outbox for consumption.
rollupStore.config.outbox.insert(endEpoch, _args.args.outHash);
}
}

RewardLib.handleRewardsAndFees(_args, endEpoch);

Expand Down Expand Up @@ -171,30 +183,33 @@ library EpochProofLib {
// struct RootRollupPublicInputs {
// previous_archive_root: Field,
// end_archive_root: Field,
// out_hash: Field,
// checkpointHeaderHashes: [Field; Constants.AZTEC_MAX_EPOCH_DURATION],
// fees: [FeeRecipient; Constants.AZTEC_MAX_EPOCH_DURATION],
// chain_id: Field,
// version: Field,
// vk_tree_root: Field,
// protocol_contracts_hash: Field,
// prover_id: Field,
// blob_public_inputs: FinalBlobAccumulatorPublicInputs,
// blob_public_inputs: FinalBlobAccumulator,
// }
{
// previous_archive.root: the previous archive tree root
publicInputs[0] = _args.previousArchive;

// end_archive.root: the new archive tree root
publicInputs[1] = _args.endArchive;

publicInputs[2] = _args.outHash;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned elsewhere this one seem to be essentially unconstrained (on L1) to the outhashes that was published during the proposals.

}

uint256 numCheckpoints = _end - _start + 1;

for (uint256 i = 0; i < numCheckpoints; i++) {
publicInputs[2 + i] = STFLib.getHeaderHash(_start + i);
publicInputs[3 + i] = STFLib.getHeaderHash(_start + i);
}

uint256 offset = 2 + Constants.AZTEC_MAX_EPOCH_DURATION;
uint256 offset = 3 + Constants.AZTEC_MAX_EPOCH_DURATION;

uint256 feesLength = Constants.AZTEC_MAX_EPOCH_DURATION * 2;
// fees[2n to 2n + 1]: a fee element, which contains of a recipient and a value
Expand Down Expand Up @@ -249,7 +264,6 @@ library EpochProofLib {
publicInputs[offset] = bytes32(uint256(uint248(bytes31((_blobPublicInputs[96:127])))));
// c[1]
publicInputs[offset + 1] = bytes32(uint256(uint136(bytes17((_blobPublicInputs[127:144])))));
offset += 2;

return publicInputs;
}
Expand Down
Loading
Loading