diff --git a/docs/docs/developers/contracts/references/portals/outbox.md b/docs/docs/developers/contracts/references/portals/outbox.md index bbc1289c35d..07ed07128cc 100644 --- a/docs/docs/developers/contracts/references/portals/outbox.md +++ b/docs/docs/developers/contracts/references/portals/outbox.md @@ -6,20 +6,24 @@ The `Outbox` is a contract deployed on L1 that handles message passing from the **Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/messagebridge/Outbox.sol). -## `sendL1Messages()` +## `insert()` -Inserts multiple messages from the `Rollup`. +Inserts the root of a merkle tree containing all of the L2 to L1 messages in a block specified by _l2BlockNumber. + +#include_code outbox_insert l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity -#include_code outbox_send_l1_msg l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity | Name | Type | Description | | -------------- | ------- | ----------- | -| `_entryKeys` | `bytes32[]` | A list of message hashes to insert into the outbox for later consumption | +| `_l2BlockNumber` | `uint256` | The L2 Block 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 | +| `_height` | `uint256` | The height of the merkle tree that the root corresponds to | #### Edge cases -- Will revert with `Registry__RollupNotRegistered(address rollup)` if `msg.sender` is not registered as a rollup on the [`Registry`](./registry.md) -- Will revert `Outbox__IncompatibleEntryArguments(bytes32 entryKey, uint64 storedFee, uint64 feePassed, uint32 storedVersion, uint32 versionPassed, uint32 storedDeadline, uint32 deadlinePassed)` if insertion is not possible due to invalid entry arguments. +- Will revert with `Outbox__Unauthorized()` if `msg.sender != ROLLUP_CONTRACT`. +- Will revert with `Errors.Outbox__RootAlreadySetAtBlock(uint256 l2BlockNumber)` if the root for the specific block has already been set. +- Will revert with `Errors.Outbox__InsertingInvalidRoot()` if the rollup is trying to insert bytes32(0) as the root. ## `consume()` @@ -30,45 +34,33 @@ Allows a recipient to consume a message from the `Outbox`. | Name | Type | Description | | -------------- | ------- | ----------- | -| `_message` | `L2ToL1Msg` | The message to consume | -| ReturnValue | `bytes32` | The hash of the message | +| `_message` | `L2ToL1Msg` | The L2 to L1 message we want to consume | +| `_l2BlockNumber` | `uint256` | The block number specifying the block that contains the message we want 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, the _path length directly depends | #### Edge cases -- Will revert with `Outbox__Unauthorized()` if `msg.sender != _message.recipient.actor`. +- 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__NothingToConsume(bytes32 entryKey)` if the message does not exist. -- Will revert with `Outbox__InvalidVersion(uint256 entry, uint256 message)` if the version of the entry and message sender don't match (wrong rollup). +- Will revert with `Outbox__NothingToConsumeAtBlock(uint256 l2BlockNumber)` if the root for the block has not been set yet. +- Will revert with `Outbox__AlreadyNullified(uint256 l2BlockNumber, uint256 leafIndex)` if the message at leafIndex for the block has already been consumed. +- Will revert with `Outbox__InvalidPathLength(uint256 expected, uint256 actual)` if the existing height of the L2 to L1 message tree, and the supplied height do not match. +- Will revert with `MerkleLib__InvalidRoot(bytes32 expected, bytes32 actual, bytes32 leaf, uint256 leafIndex)` if unable to verify the message existence in the tree. It returns the message as a leaf, as well as the index of the leaf to expose more info about the error. -## `get()` -Retrieves the `entry` for a given message. The entry contains fee, occurrences, deadline and version information. -#include_code outbox_get l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity +## `hasMessageBeenConsumedAtBlockAndIndex()` -| Name | Type | Description | -| -------------- | ------- | ----------- | -| `_entryKey` | `bytes32` | The entry key (message hash) | -| ReturnValue | `Entry` | The entry for the given key | - -#### Edge cases -- Will revert with `Outbox__NothingToConsume(bytes32 entryKey)` if the message does not exist. +Checks to see if an index of the L2 to L1 message tree for a specific block has been consumed. -## `contains()` -Returns whether the key is found in the inbox. +#include_code outbox_has_message_been_consumed_at_block_and_index l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity -#include_code outbox_contains l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity | Name | Type | Description | | -------------- | ------- | ----------- | -| `_entryKey` | `bytes32` | The entry key (message hash)| -| ReturnValue | `bool` | True if contained, false otherwise| +| `_l2BlockNumber` | `uint256` | The block number specifying the block that contains the index of the message we want to check | +| `_leafIndex` | `uint256` | The index of the message inside the merkle tree | -## `computeEntryKey()` -Computes the hash of a message. - -#include_code outbox_compute_entry_key l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity +#### Edge cases -| Name | Type | Description | -| -------------- | ------- | ----------- | -| `_message` | `L2ToL1Msg` | The message to compute hash for | -| ReturnValue | `bytes32` | The hash of the message | \ No newline at end of file +- This function does not throw. Out-of-bounds access is considered valid, but will always return false. diff --git a/l1-contracts/slither_output.md b/l1-contracts/slither_output.md index 68958b0132b..0804d318cdd 100644 --- a/l1-contracts/slither_output.md +++ b/l1-contracts/slither_output.md @@ -2,13 +2,13 @@ Summary - [pess-unprotected-setter](#pess-unprotected-setter) (1 results) (High) - [uninitialized-local](#uninitialized-local) (2 results) (Medium) - [unused-return](#unused-return) (1 results) (Medium) - - [pess-dubious-typecast](#pess-dubious-typecast) (6 results) (Medium) + - [pess-dubious-typecast](#pess-dubious-typecast) (5 results) (Medium) - [missing-zero-check](#missing-zero-check) (2 results) (Low) - [reentrancy-events](#reentrancy-events) (2 results) (Low) - [timestamp](#timestamp) (1 results) (Low) - - [pess-public-vs-external](#pess-public-vs-external) (6 results) (Low) + - [pess-public-vs-external](#pess-public-vs-external) (5 results) (Low) - [assembly](#assembly) (2 results) (Informational) - - [dead-code](#dead-code) (3 results) (Informational) + - [dead-code](#dead-code) (5 results) (Informational) - [solc-version](#solc-version) (1 results) (Informational) - [similar-names](#similar-names) (3 results) (Informational) - [constable-states](#constable-states) (1 results) (Optimization) @@ -17,9 +17,9 @@ Summary Impact: High Confidence: Medium - [ ] ID-0 -Function [Rollup.process(bytes,bytes32,bytes,bytes)](src/core/Rollup.sol#L57-L96) is a non-protected setter archive is written +Function [Rollup.process(bytes,bytes32,bytes,bytes)](src/core/Rollup.sol#L60-L104) is a non-protected setter archive is written -src/core/Rollup.sol#L57-L96 +src/core/Rollup.sol#L60-L104 ## uninitialized-local @@ -41,9 +41,9 @@ src/core/libraries/decoders/TxsDecoder.sol#L81 Impact: Medium Confidence: Medium - [ ] ID-3 -[Rollup.process(bytes,bytes32,bytes,bytes)](src/core/Rollup.sol#L57-L96) ignores return value by [(l2ToL1Msgs) = MessagesDecoder.decode(_body)](src/core/Rollup.sol#L73) +[Rollup.process(bytes,bytes32,bytes,bytes)](src/core/Rollup.sol#L60-L104) ignores return value by [(l2ToL1Msgs) = MessagesDecoder.decode(_body)](src/core/Rollup.sol#L77) -src/core/Rollup.sol#L57-L96 +src/core/Rollup.sol#L60-L104 ## pess-dubious-typecast @@ -71,13 +71,6 @@ src/core/libraries/decoders/MessagesDecoder.sol#L164-L166 - [ ] ID-7 -Dubious typecast in [Outbox.sendL1Messages(bytes32[])](src/core/messagebridge/Outbox.sol#L38-L46): - uint256 => uint32 casting occurs in [version = uint32(REGISTRY.getVersionFor(msg.sender))](src/core/messagebridge/Outbox.sol#L40) - -src/core/messagebridge/Outbox.sol#L38-L46 - - - - [ ] ID-8 Dubious typecast in [HeaderLib.decode(bytes)](src/core/libraries/HeaderLib.sol#L143-L184): bytes => bytes32 casting occurs in [header.lastArchive = AppendOnlyTreeSnapshot(bytes32(_header),uint32(bytes4(_header)))](src/core/libraries/HeaderLib.sol#L151-L153) bytes => bytes4 casting occurs in [header.lastArchive = AppendOnlyTreeSnapshot(bytes32(_header),uint32(bytes4(_header)))](src/core/libraries/HeaderLib.sol#L151-L153) @@ -103,7 +96,7 @@ Dubious typecast in [HeaderLib.decode(bytes)](src/core/libraries/HeaderLib.sol#L src/core/libraries/HeaderLib.sol#L143-L184 - - [ ] ID-9 + - [ ] ID-8 Dubious typecast in [MessagesDecoder.read1(bytes,uint256)](src/core/libraries/decoders/MessagesDecoder.sol#L154-L156): bytes => bytes1 casting occurs in [uint256(uint8(bytes1(_data)))](src/core/libraries/decoders/MessagesDecoder.sol#L155) @@ -113,24 +106,24 @@ src/core/libraries/decoders/MessagesDecoder.sol#L154-L156 ## missing-zero-check Impact: Low Confidence: Medium - - [ ] ID-10 + - [ ] ID-9 [Inbox.constructor(address,uint256)._rollup](src/core/messagebridge/Inbox.sol#L40) lacks a zero-check on : - [ROLLUP = _rollup](src/core/messagebridge/Inbox.sol#L41) src/core/messagebridge/Inbox.sol#L40 - - [ ] ID-11 -[NewOutbox.constructor(address)._rollup](src/core/messagebridge/NewOutbox.sol#L31) lacks a zero-check on : - - [ROLLUP_CONTRACT = _rollup](src/core/messagebridge/NewOutbox.sol#L32) + - [ ] ID-10 +[Outbox.constructor(address)._rollup](src/core/messagebridge/Outbox.sol#L31) lacks a zero-check on : + - [ROLLUP_CONTRACT = _rollup](src/core/messagebridge/Outbox.sol#L32) -src/core/messagebridge/NewOutbox.sol#L31 +src/core/messagebridge/Outbox.sol#L31 ## reentrancy-events Impact: Low Confidence: Medium - - [ ] ID-12 + - [ ] ID-11 Reentrancy in [Inbox.sendL2Message(DataStructures.L2Actor,bytes32,bytes32)](src/core/messagebridge/Inbox.sol#L61-L95): External calls: - [index = currentTree.insertLeaf(leaf)](src/core/messagebridge/Inbox.sol#L91) @@ -140,21 +133,21 @@ Reentrancy in [Inbox.sendL2Message(DataStructures.L2Actor,bytes32,bytes32)](src/ src/core/messagebridge/Inbox.sol#L61-L95 - - [ ] ID-13 -Reentrancy in [Rollup.process(bytes,bytes32,bytes,bytes)](src/core/Rollup.sol#L57-L96): + - [ ] ID-12 +Reentrancy in [Rollup.process(bytes,bytes32,bytes,bytes)](src/core/Rollup.sol#L60-L104): External calls: - - [inHash = INBOX.consume()](src/core/Rollup.sol#L87) - - [outbox.sendL1Messages(l2ToL1Msgs)](src/core/Rollup.sol#L93) + - [inHash = INBOX.consume()](src/core/Rollup.sol#L91) + - [OUTBOX.insert(header.globalVariables.blockNumber,header.contentCommitment.outHash,l2ToL1TreeHeight)](src/core/Rollup.sol#L99-L101) Event emitted after the call(s): - - [L2BlockProcessed(header.globalVariables.blockNumber)](src/core/Rollup.sol#L95) + - [L2BlockProcessed(header.globalVariables.blockNumber)](src/core/Rollup.sol#L103) -src/core/Rollup.sol#L57-L96 +src/core/Rollup.sol#L60-L104 ## timestamp Impact: Low Confidence: Medium - - [ ] ID-14 + - [ ] ID-13 [HeaderLib.validate(HeaderLib.Header,uint256,uint256,bytes32)](src/core/libraries/HeaderLib.sol#L106-L136) uses timestamp for comparisons Dangerous comparisons: - [_header.globalVariables.timestamp > block.timestamp](src/core/libraries/HeaderLib.sol#L120) @@ -165,54 +158,45 @@ src/core/libraries/HeaderLib.sol#L106-L136 ## pess-public-vs-external Impact: Low Confidence: Medium - - [ ] ID-15 + - [ ] ID-14 The following public functions could be turned into external in [FrontierMerkle](src/core/messagebridge/frontier_tree/Frontier.sol#L7-L93) contract: [FrontierMerkle.constructor(uint256)](src/core/messagebridge/frontier_tree/Frontier.sol#L19-L27) src/core/messagebridge/frontier_tree/Frontier.sol#L7-L93 - - [ ] ID-16 + - [ ] ID-15 The following public functions could be turned into external in [Registry](src/core/messagebridge/Registry.sol#L22-L129) contract: [Registry.constructor()](src/core/messagebridge/Registry.sol#L29-L33) src/core/messagebridge/Registry.sol#L22-L129 - - [ ] ID-17 + - [ ] ID-16 The following public functions could be turned into external in [Inbox](src/core/messagebridge/Inbox.sol#L24-L124) contract: [Inbox.constructor(address,uint256)](src/core/messagebridge/Inbox.sol#L40-L51) src/core/messagebridge/Inbox.sol#L24-L124 - - [ ] ID-18 -The following public functions could be turned into external in [Rollup](src/core/Rollup.sol#L29-L105) contract: - [Rollup.constructor(IRegistry,IAvailabilityOracle)](src/core/Rollup.sol#L42-L48) - -src/core/Rollup.sol#L29-L105 - - - - [ ] ID-19 -The following public functions could be turned into external in [Outbox](src/core/messagebridge/Outbox.sol#L21-L148) contract: - [Outbox.constructor(address)](src/core/messagebridge/Outbox.sol#L29-L31) - [Outbox.get(bytes32)](src/core/messagebridge/Outbox.sol#L77-L84) - [Outbox.contains(bytes32)](src/core/messagebridge/Outbox.sol#L91-L93) + - [ ] ID-17 +The following public functions could be turned into external in [Rollup](src/core/Rollup.sol#L30-L113) contract: + [Rollup.constructor(IRegistry,IAvailabilityOracle)](src/core/Rollup.sol#L44-L51) -src/core/messagebridge/Outbox.sol#L21-L148 +src/core/Rollup.sol#L30-L113 - - [ ] ID-20 -The following public functions could be turned into external in [NewOutbox](src/core/messagebridge/NewOutbox.sol#L18-L132) contract: - [NewOutbox.constructor(address)](src/core/messagebridge/NewOutbox.sol#L31-L33) + - [ ] ID-18 +The following public functions could be turned into external in [Outbox](src/core/messagebridge/Outbox.sol#L18-L132) contract: + [Outbox.constructor(address)](src/core/messagebridge/Outbox.sol#L31-L33) -src/core/messagebridge/NewOutbox.sol#L18-L132 +src/core/messagebridge/Outbox.sol#L18-L132 ## assembly Impact: Informational Confidence: High - - [ ] ID-21 + - [ ] ID-19 [MessagesDecoder.decode(bytes)](src/core/libraries/decoders/MessagesDecoder.sol#L61-L146) uses assembly - [INLINE ASM](src/core/libraries/decoders/MessagesDecoder.sol#L80-L82) - [INLINE ASM](src/core/libraries/decoders/MessagesDecoder.sol#L116-L122) @@ -220,7 +204,7 @@ Confidence: High src/core/libraries/decoders/MessagesDecoder.sol#L61-L146 - - [ ] ID-22 + - [ ] ID-20 [TxsDecoder.computeRoot(bytes32[])](src/core/libraries/decoders/TxsDecoder.sol#L265-L284) uses assembly - [INLINE ASM](src/core/libraries/decoders/TxsDecoder.sol#L272-L274) @@ -230,22 +214,34 @@ src/core/libraries/decoders/TxsDecoder.sol#L265-L284 ## dead-code Impact: Informational Confidence: Medium + - [ ] ID-21 +[MessageBox.consume(mapping(bytes32 => DataStructures.Entry),bytes32,function(bytes32))](src/core/libraries/MessageBox.sol#L71-L79) is never used and should be removed + +src/core/libraries/MessageBox.sol#L71-L79 + + + - [ ] ID-22 +[MessageBox.contains(mapping(bytes32 => DataStructures.Entry),bytes32)](src/core/libraries/MessageBox.sol#L87-L92) is never used and should be removed + +src/core/libraries/MessageBox.sol#L87-L92 + + - [ ] ID-23 -[Outbox._errNothingToConsume(bytes32)](src/core/messagebridge/Outbox.sol#L114-L116) is never used and should be removed +[MessageBox.get(mapping(bytes32 => DataStructures.Entry),bytes32,function(bytes32))](src/core/libraries/MessageBox.sol#L104-L112) is never used and should be removed -src/core/messagebridge/Outbox.sol#L114-L116 +src/core/libraries/MessageBox.sol#L104-L112 - [ ] ID-24 -[Hash.sha256ToField(bytes32)](src/core/libraries/Hash.sol#L52-L54) is never used and should be removed +[MessageBox.insert(mapping(bytes32 => DataStructures.Entry),bytes32,uint64,uint32,uint32,function(bytes32,uint64,uint64,uint32,uint32,uint32,uint32))](src/core/libraries/MessageBox.sol#L30-L60) is never used and should be removed -src/core/libraries/Hash.sol#L52-L54 +src/core/libraries/MessageBox.sol#L30-L60 - [ ] ID-25 -[Outbox._errIncompatibleEntryArguments(bytes32,uint64,uint64,uint32,uint32,uint32,uint32)](src/core/messagebridge/Outbox.sol#L129-L147) is never used and should be removed +[Hash.sha256ToField(bytes32)](src/core/libraries/Hash.sol#L52-L54) is never used and should be removed -src/core/messagebridge/Outbox.sol#L129-L147 +src/core/libraries/Hash.sol#L52-L54 ## solc-version @@ -270,27 +266,27 @@ src/core/libraries/ConstantsGen.sol#L110 - [ ] ID-29 -Variable [Rollup.AVAILABILITY_ORACLE](src/core/Rollup.sol#L32) is too similar to [Rollup.constructor(IRegistry,IAvailabilityOracle)._availabilityOracle](src/core/Rollup.sol#L42) +Variable [Rollup.AVAILABILITY_ORACLE](src/core/Rollup.sol#L33) is too similar to [Rollup.constructor(IRegistry,IAvailabilityOracle)._availabilityOracle](src/core/Rollup.sol#L44) -src/core/Rollup.sol#L32 +src/core/Rollup.sol#L33 ## constable-states Impact: Optimization Confidence: High - [ ] ID-30 -[Rollup.lastWarpedBlockTs](src/core/Rollup.sol#L40) should be constant +[Rollup.lastWarpedBlockTs](src/core/Rollup.sol#L42) should be constant -src/core/Rollup.sol#L40 +src/core/Rollup.sol#L42 ## pess-multiple-storage-read Impact: Optimization Confidence: High - [ ] ID-31 -In a function [NewOutbox.insert(uint256,bytes32,uint256)](src/core/messagebridge/NewOutbox.sol#L44-L64) variable [NewOutbox.roots](src/core/messagebridge/NewOutbox.sol#L29) is read multiple times +In a function [Outbox.insert(uint256,bytes32,uint256)](src/core/messagebridge/Outbox.sol#L44-L64) variable [Outbox.roots](src/core/messagebridge/Outbox.sol#L29) is read multiple times -src/core/messagebridge/NewOutbox.sol#L44-L64 +src/core/messagebridge/Outbox.sol#L44-L64 - [ ] ID-32 diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 6fd21cf6792..ad0a9bcc899 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -19,6 +19,7 @@ import {Constants} from "./libraries/ConstantsGen.sol"; // Contracts import {MockVerifier} from "../mock/MockVerifier.sol"; import {Inbox} from "./messagebridge/Inbox.sol"; +import {Outbox} from "./messagebridge/Outbox.sol"; /** * @title Rollup @@ -31,6 +32,7 @@ contract Rollup is IRollup { IRegistry public immutable REGISTRY; IAvailabilityOracle public immutable AVAILABILITY_ORACLE; IInbox public immutable INBOX; + IOutbox public immutable OUTBOX; uint256 public immutable VERSION; bytes32 public archive; // Root of the archive tree @@ -44,6 +46,7 @@ contract Rollup is IRollup { REGISTRY = _registry; AVAILABILITY_ORACLE = _availabilityOracle; INBOX = new Inbox(address(this), Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT); + OUTBOX = new Outbox(address(this)); VERSION = 1; } @@ -70,6 +73,7 @@ contract Rollup is IRollup { } // Decode the cross-chain messages (Will be removed as part of message model change) + // TODO(#5339) (,,, bytes32[] memory l2ToL1Msgs) = MessagesDecoder.decode(_body); bytes32[] memory publicInputs = new bytes32[](1); @@ -89,8 +93,12 @@ contract Rollup is IRollup { revert Errors.Rollup__InvalidInHash(inHash, header.contentCommitment.inHash); } - IOutbox outbox = REGISTRY.getOutbox(); - outbox.sendL1Messages(l2ToL1Msgs); + // We assume here that the number of L2 to L1 messages per tx is 2. Therefore we just need a tree that is one height + // larger (as we can just extend the tree one layer down to hold all the L2 to L1 messages) + uint256 l2ToL1TreeHeight = header.contentCommitment.txTreeHeight + 1; + OUTBOX.insert( + header.globalVariables.blockNumber, header.contentCommitment.outHash, l2ToL1TreeHeight + ); emit L2BlockProcessed(header.globalVariables.blockNumber); } diff --git a/l1-contracts/src/core/interfaces/messagebridge/INewOutbox.sol b/l1-contracts/src/core/interfaces/messagebridge/INewOutbox.sol deleted file mode 100644 index 3a7c9e03af0..00000000000 --- a/l1-contracts/src/core/interfaces/messagebridge/INewOutbox.sol +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2024 Aztec Labs. -pragma solidity >=0.8.18; - -import {DataStructures} from "../../libraries/DataStructures.sol"; - -/** - * @title INewOutbox - * @author Aztec Labs - * @notice Lives on L1 and is used to consume L2 -> L1 messages. Messages are inserted by the Rollup - * and will be consumed by the portal contracts. - */ -// TODO: rename to IOutbox once all the pieces of the new message model are in place. -interface INewOutbox { - event RootAdded(uint256 indexed l2BlockNumber, bytes32 indexed root, uint256 height); - event MessageConsumed( - uint256 indexed l2BlockNumber, - bytes32 indexed root, - bytes32 indexed messageHash, - uint256 leafIndex - ); - - /** - * @notice Inserts the root of a merkle tree containing all of the L2 to L1 messages in - * a block specified by _l2BlockNumber. - * @dev Only callable by the rollup contract - * @dev Emits `RootAdded` upon inserting the root successfully - * @param _l2BlockNumber - The L2 Block Number 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 - * @param _height - The height of the merkle tree that the root corresponds to - */ - function insert(uint256 _l2BlockNumber, bytes32 _root, uint256 _height) external; - - /** - * @notice Consumes an entry from the Outbox - * @dev Only useable by portals / recipients of messages - * @dev Emits `MessageConsumed` when consuming messages - * @param _l2BlockNumber - The block number specifying the block that contains the message we want to consume - * @param _leafIndex - The index inside the merkle tree where the message is located - * @param _message - The L2 to L1 message - * @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 block. i.e. the length of _path is equal to the depth of the - * L1 to L2 message tree. - */ - function consume( - uint256 _l2BlockNumber, - uint256 _leafIndex, - DataStructures.L2ToL1Msg calldata _message, - bytes32[] calldata _path - ) external; - - /** - * @notice Checks to see if an index of the L2 to L1 message tree for a specific block has been consumed - * @dev - This function does not throw. Out-of-bounds access is considered valid, but will always return false - * @param _l2BlockNumber - The block number specifying the block that contains the index of the message we want to check - * @param _leafIndex - The index of the message inside the merkle tree - */ - function hasMessageBeenConsumedAtBlockAndIndex(uint256 _l2BlockNumber, uint256 _leafIndex) - external - view - returns (bool); -} diff --git a/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol b/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol index ecbfcfd98de..d3ff717cf53 100644 --- a/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol +++ b/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2023 Aztec Labs. +// Copyright 2024 Aztec Labs. pragma solidity >=0.8.18; import {DataStructures} from "../../libraries/DataStructures.sol"; @@ -7,59 +7,61 @@ import {DataStructures} from "../../libraries/DataStructures.sol"; /** * @title IOutbox * @author Aztec Labs - * @notice Lives on L1 and is used to consume L2 -> L1 messages. Messages are inserted by the rollup contract + * @notice Lives on L1 and is used to consume L2 -> L1 messages. Messages are inserted by the Rollup * and will be consumed by the portal contracts. */ interface IOutbox { - // to make it easier for portal to know when to consume the message. - event MessageAdded(bytes32 indexed entryKey); + event RootAdded(uint256 indexed l2BlockNumber, bytes32 indexed root, uint256 height); + event MessageConsumed( + uint256 indexed l2BlockNumber, + bytes32 indexed root, + bytes32 indexed messageHash, + uint256 leafIndex + ); - event MessageConsumed(bytes32 indexed entryKey, address indexed recipient); - - // docs:start:outbox_compute_entry_key - /** - * @notice Computes an entry key for the Outbox - * @param _message - The L2 to L1 message - * @return The key of the entry in the set - */ - function computeEntryKey(DataStructures.L2ToL1Msg memory _message) external returns (bytes32); - // docs:end:outbox_compute_entry_key - - // docs:start:outbox_send_l1_msg + // docs:start:outbox_insert /** - * @notice Inserts an array of entries into the Outbox + * @notice Inserts the root of a merkle tree containing all of the L2 to L1 messages in + * a block specified by _l2BlockNumber. * @dev Only callable by the rollup contract - * @param _entryKeys - Array of entry keys (hash of the message) - computed by the L2 counterpart and sent to L1 via rollup block + * @dev Emits `RootAdded` upon inserting the root successfully + * @param _l2BlockNumber - The L2 Block Number 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 + * @param _height - The height of the merkle tree that the root corresponds to */ - function sendL1Messages(bytes32[] memory _entryKeys) external; - // docs:end:outbox_send_l1_msg + function insert(uint256 _l2BlockNumber, bytes32 _root, uint256 _height) external; + // docs:end:outbox_insert // docs:start:outbox_consume /** * @notice Consumes an entry from the Outbox - * @dev Only meaningfully callable by portals, otherwise should never hit an entry - * @dev Emits the `MessageConsumed` event when consuming messages + * @dev Only useable by portals / recipients of messages + * @dev Emits `MessageConsumed` when consuming messages * @param _message - The L2 to L1 message - * @return entryKey - The key of the entry removed + * @param _l2BlockNumber - The block number specifying the block 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 block. i.e. the length of _path is equal to the depth of the + * L1 to L2 message tree. */ - function consume(DataStructures.L2ToL1Msg memory _message) external returns (bytes32 entryKey); + function consume( + DataStructures.L2ToL1Msg calldata _message, + uint256 _l2BlockNumber, + uint256 _leafIndex, + bytes32[] calldata _path + ) external; // docs:end:outbox_consume - // docs:start:outbox_get - /** - * @notice Fetch an entry - * @param _entryKey - The key to lookup - * @return The entry matching the provided key - */ - function get(bytes32 _entryKey) external view returns (DataStructures.Entry memory); - // docs:end:outbox_get - - // docs:start:outbox_contains + // docs:start:outbox_has_message_been_consumed_at_block_and_index /** - * @notice Check if entry exists - * @param _entryKey - The key to lookup - * @return True if entry exists, false otherwise + * @notice Checks to see if an index of the L2 to L1 message tree for a specific block has been consumed + * @dev - This function does not throw. Out-of-bounds access is considered valid, but will always return false + * @param _l2BlockNumber - The block number specifying the block that contains the index of the message we want to check + * @param _leafIndex - The index of the message inside the merkle tree */ - function contains(bytes32 _entryKey) external view returns (bool); - // docs:end:outbox_contains + function hasMessageBeenConsumedAtBlockAndIndex(uint256 _l2BlockNumber, uint256 _leafIndex) + external + view + returns (bool); + // docs:end:outbox_has_message_been_consumed_at_block_and_index } diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 07c288df57d..92e3006eb8b 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -55,5 +55,5 @@ library Errors { error HeaderLib__InvalidHeaderSize(uint256 expected, uint256 actual); // 0xf3ccb247 // MerkleLib - error MerkleLib__InvalidRoot(bytes32 expected, bytes32 actual); // 0xb77e99 + error MerkleLib__InvalidRoot(bytes32 expected, bytes32 actual, bytes32 leaf, uint256 leafIndex); // 0x5f216bf1 } diff --git a/l1-contracts/src/core/libraries/MerkleLib.sol b/l1-contracts/src/core/libraries/MerkleLib.sol index bd7c5fbb8bd..e511fefc9ee 100644 --- a/l1-contracts/src/core/libraries/MerkleLib.sol +++ b/l1-contracts/src/core/libraries/MerkleLib.sol @@ -47,7 +47,7 @@ library MerkleLib { } if (subtreeRoot != _expectedRoot) { - revert Errors.MerkleLib__InvalidRoot(_expectedRoot, subtreeRoot); + revert Errors.MerkleLib__InvalidRoot(_expectedRoot, subtreeRoot, _leaf, _index); } } } diff --git a/l1-contracts/src/core/messagebridge/NewOutbox.sol b/l1-contracts/src/core/messagebridge/NewOutbox.sol deleted file mode 100644 index 186f460b0c7..00000000000 --- a/l1-contracts/src/core/messagebridge/NewOutbox.sol +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2024 Aztec Labs. -pragma solidity >=0.8.18; - -// Libraries -import {DataStructures} from "../libraries/DataStructures.sol"; -import {Errors} from "../libraries/Errors.sol"; -import {MerkleLib} from "../libraries/MerkleLib.sol"; -import {Hash} from "../libraries/Hash.sol"; -import {INewOutbox} from "../interfaces/messagebridge/INewOutbox.sol"; - -/** - * @title NewOutbox - * @author Aztec Labs - * @notice Lives on L1 and is used to consume L2 -> L1 messages. Messages are inserted by the Rollup - * and will be consumed by the portal contracts. - */ -contract NewOutbox is INewOutbox { - using Hash for DataStructures.L2ToL1Msg; - - struct RootData { - // This is the outhash specified by header.globalvariables.outHash of any given block. - bytes32 root; - uint256 height; - mapping(uint256 => bool) nullified; - } - - address public immutable ROLLUP_CONTRACT; - mapping(uint256 l2BlockNumber => RootData) public roots; - - constructor(address _rollup) { - ROLLUP_CONTRACT = _rollup; - } - - /** - * @notice Inserts the root of a merkle tree containing all of the L2 to L1 messages in - * a block specified by _l2BlockNumber. - * @dev Only callable by the rollup contract - * @dev Emits `RootAdded` upon inserting the root successfully - * @param _l2BlockNumber - The L2 Block Number 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 - * @param _height - The height of the merkle tree that the root corresponds to - */ - function insert(uint256 _l2BlockNumber, bytes32 _root, uint256 _height) - external - override(INewOutbox) - { - if (msg.sender != ROLLUP_CONTRACT) { - revert Errors.Outbox__Unauthorized(); - } - - if (roots[_l2BlockNumber].root != bytes32(0)) { - revert Errors.Outbox__RootAlreadySetAtBlock(_l2BlockNumber); - } - - if (_root == bytes32(0)) { - revert Errors.Outbox__InsertingInvalidRoot(); - } - - roots[_l2BlockNumber].root = _root; - roots[_l2BlockNumber].height = _height; - - emit RootAdded(_l2BlockNumber, _root, _height); - } - - /** - * @notice Consumes an entry from the Outbox - * @dev Only useable by portals / recipients of messages - * @dev Emits `MessageConsumed` when consuming messages - * @param _l2BlockNumber - The block number specifying the block that contains the message we want to consume - * @param _leafIndex - The index inside the merkle tree where the message is located - * @param _message - The L2 to L1 message - * @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 block. i.e. the length of _path is equal to the depth of the - * L1 to L2 message tree. - */ - function consume( - uint256 _l2BlockNumber, - uint256 _leafIndex, - DataStructures.L2ToL1Msg calldata _message, - bytes32[] calldata _path - ) external override(INewOutbox) { - if (msg.sender != _message.recipient.actor) { - revert Errors.Outbox__InvalidRecipient(_message.recipient.actor, msg.sender); - } - - if (block.chainid != _message.recipient.chainId) { - revert Errors.Outbox__InvalidChainId(); - } - - RootData storage rootData = roots[_l2BlockNumber]; - - bytes32 blockRoot = rootData.root; - - if (blockRoot == 0) { - revert Errors.Outbox__NothingToConsumeAtBlock(_l2BlockNumber); - } - - if (rootData.nullified[_leafIndex]) { - revert Errors.Outbox__AlreadyNullified(_l2BlockNumber, _leafIndex); - } - - uint256 treeHeight = rootData.height; - - if (treeHeight != _path.length) { - revert Errors.Outbox__InvalidPathLength(treeHeight, _path.length); - } - - bytes32 messageHash = _message.sha256ToField(); - - MerkleLib.verifyMembership(_path, messageHash, _leafIndex, blockRoot); - - rootData.nullified[_leafIndex] = true; - - emit MessageConsumed(_l2BlockNumber, blockRoot, messageHash, _leafIndex); - } - - /** - * @notice Checks to see if an index of the L2 to L1 message tree for a specific block has been consumed - * @dev - This function does not throw. Out-of-bounds access is considered valid, but will always return false - * @param _l2BlockNumber - The block number specifying the block that contains the index of the message we want to check - * @param _leafIndex - The index of the message inside the merkle tree - */ - function hasMessageBeenConsumedAtBlockAndIndex(uint256 _l2BlockNumber, uint256 _leafIndex) - external - view - override(INewOutbox) - returns (bool) - { - return roots[_l2BlockNumber].nullified[_leafIndex]; - } -} diff --git a/l1-contracts/src/core/messagebridge/Outbox.sol b/l1-contracts/src/core/messagebridge/Outbox.sol index 9befd4084f2..82085cad908 100644 --- a/l1-contracts/src/core/messagebridge/Outbox.sol +++ b/l1-contracts/src/core/messagebridge/Outbox.sol @@ -1,148 +1,132 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2023 Aztec Labs. +// Copyright 2024 Aztec Labs. pragma solidity >=0.8.18; -// Interfaces -import {IOutbox} from "../interfaces/messagebridge/IOutbox.sol"; -import {IRegistry} from "../interfaces/messagebridge/IRegistry.sol"; - // Libraries import {DataStructures} from "../libraries/DataStructures.sol"; import {Errors} from "../libraries/Errors.sol"; +import {MerkleLib} from "../libraries/MerkleLib.sol"; import {Hash} from "../libraries/Hash.sol"; -import {MessageBox} from "../libraries/MessageBox.sol"; +import {IOutbox} from "../interfaces/messagebridge/IOutbox.sol"; /** * @title Outbox * @author Aztec Labs - * @notice Lives on L1 and is used to consume L2 -> L1 messages. Messages are inserted by the rollup contract + * @notice Lives on L1 and is used to consume L2 -> L1 messages. Messages are inserted by the Rollup * and will be consumed by the portal contracts. */ contract Outbox is IOutbox { - using MessageBox for mapping(bytes32 entryKey => DataStructures.Entry entry); using Hash for DataStructures.L2ToL1Msg; - IRegistry public immutable REGISTRY; + struct RootData { + // This is the outhash specified by header.globalvariables.outHash of any given block. + bytes32 root; + uint256 height; + mapping(uint256 => bool) nullified; + } - mapping(bytes32 entryKey => DataStructures.Entry entry) internal entries; + address public immutable ROLLUP_CONTRACT; + mapping(uint256 l2BlockNumber => RootData) public roots; - constructor(address _registry) { - REGISTRY = IRegistry(_registry); + constructor(address _rollup) { + ROLLUP_CONTRACT = _rollup; } /** - * @notice Inserts an array of entries into the Outbox + * @notice Inserts the root of a merkle tree containing all of the L2 to L1 messages in + * a block specified by _l2BlockNumber. * @dev Only callable by the rollup contract - * @param _entryKeys - Array of entry keys (hash of the message) - computed by the L2 counterpart and sent to L1 via rollup block - */ - function sendL1Messages(bytes32[] memory _entryKeys) external override(IOutbox) { - // This MUST revert if not called by a listed rollup contract - uint32 version = uint32(REGISTRY.getVersionFor(msg.sender)); - for (uint256 i = 0; i < _entryKeys.length; i++) { - if (_entryKeys[i] == bytes32(0)) continue; - entries.insert(_entryKeys[i], 0, version, 0, _errIncompatibleEntryArguments); - emit MessageAdded(_entryKeys[i]); - } - } - - /** - * @notice Consumes an entry from the Outbox - * @dev Emits the `MessageConsumed` event when consuming messages - * @param _message - The L2 to L1 message - * @return entryKey - The key of the entry removed + * @dev Emits `RootAdded` upon inserting the root successfully + * @param _l2BlockNumber - The L2 Block Number 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 + * @param _height - The height of the merkle tree that the root corresponds to */ - function consume(DataStructures.L2ToL1Msg memory _message) + function insert(uint256 _l2BlockNumber, bytes32 _root, uint256 _height) external override(IOutbox) - returns (bytes32 entryKey) { - if (msg.sender != _message.recipient.actor) revert Errors.Outbox__Unauthorized(); - if (block.chainid != _message.recipient.chainId) revert Errors.Outbox__InvalidChainId(); + if (msg.sender != ROLLUP_CONTRACT) { + revert Errors.Outbox__Unauthorized(); + } - entryKey = computeEntryKey(_message); - DataStructures.Entry memory entry = entries.get(entryKey, _errNothingToConsume); - if (entry.version != _message.sender.version) { - revert Errors.Outbox__InvalidVersion(entry.version, _message.sender.version); + if (roots[_l2BlockNumber].root != bytes32(0)) { + revert Errors.Outbox__RootAlreadySetAtBlock(_l2BlockNumber); } - entries.consume(entryKey, _errNothingToConsume); - emit MessageConsumed(entryKey, msg.sender); - } + if (_root == bytes32(0)) { + revert Errors.Outbox__InsertingInvalidRoot(); + } - /** - * @notice Fetch an entry - * @param _entryKey - The key to lookup - * @return The entry matching the provided key - */ - function get(bytes32 _entryKey) - public - view - override(IOutbox) - returns (DataStructures.Entry memory) - { - return entries.get(_entryKey, _errNothingToConsume); - } + roots[_l2BlockNumber].root = _root; + roots[_l2BlockNumber].height = _height; - /** - * @notice Check if entry exists - * @param _entryKey - The key to lookup - * @return True if entry exists, false otherwise - */ - function contains(bytes32 _entryKey) public view override(IOutbox) returns (bool) { - return entries.contains(_entryKey); + emit RootAdded(_l2BlockNumber, _root, _height); } /** - * @notice Computes an entry key for the Outbox + * @notice Consumes an entry from the Outbox + * @dev Only useable by portals / recipients of messages + * @dev Emits `MessageConsumed` when consuming messages * @param _message - The L2 to L1 message - * @return The key of the entry in the set + * @param _l2BlockNumber - The block number specifying the block 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 block. i.e. the length of _path is equal to the depth of the + * L1 to L2 message tree. */ - function computeEntryKey(DataStructures.L2ToL1Msg memory _message) - public - pure - override(IOutbox) - returns (bytes32) - { - return _message.sha256ToField(); - } + function consume( + DataStructures.L2ToL1Msg calldata _message, + uint256 _l2BlockNumber, + uint256 _leafIndex, + bytes32[] calldata _path + ) external override(IOutbox) { + if (msg.sender != _message.recipient.actor) { + revert Errors.Outbox__InvalidRecipient(_message.recipient.actor, msg.sender); + } - /** - * @notice Error function passed in cases where there might be nothing to consume - * @dev Used to have message box library throw `Outbox__` prefixed errors - * @param _entryKey - The key to lookup - */ - function _errNothingToConsume(bytes32 _entryKey) internal pure { - revert Errors.Outbox__NothingToConsume(_entryKey); + if (block.chainid != _message.recipient.chainId) { + revert Errors.Outbox__InvalidChainId(); + } + + RootData storage rootData = roots[_l2BlockNumber]; + + bytes32 blockRoot = rootData.root; + + if (blockRoot == 0) { + revert Errors.Outbox__NothingToConsumeAtBlock(_l2BlockNumber); + } + + if (rootData.nullified[_leafIndex]) { + revert Errors.Outbox__AlreadyNullified(_l2BlockNumber, _leafIndex); + } + + uint256 treeHeight = rootData.height; + + if (treeHeight != _path.length) { + revert Errors.Outbox__InvalidPathLength(treeHeight, _path.length); + } + + bytes32 messageHash = _message.sha256ToField(); + + MerkleLib.verifyMembership(_path, messageHash, _leafIndex, blockRoot); + + rootData.nullified[_leafIndex] = true; + + emit MessageConsumed(_l2BlockNumber, blockRoot, messageHash, _leafIndex); } /** - * @notice Error function passed in cases where insertions can fail - * @dev Used to have message box library throw `Outbox__` prefixed errors - * @param _entryKey - The key to lookup - * @param _storedFee - The fee stored in the entry - * @param _feePassed - The fee passed into the insertion - * @param _storedVersion - The version stored in the entry - * @param _versionPassed - The version passed into the insertion - * @param _storedDeadline - The deadline stored in the entry - * @param _deadlinePassed - The deadline passed into the insertion + * @notice Checks to see if an index of the L2 to L1 message tree for a specific block has been consumed + * @dev - This function does not throw. Out-of-bounds access is considered valid, but will always return false + * @param _l2BlockNumber - The block number specifying the block that contains the index of the message we want to check + * @param _leafIndex - The index of the message inside the merkle tree */ - function _errIncompatibleEntryArguments( - bytes32 _entryKey, - uint64 _storedFee, - uint64 _feePassed, - uint32 _storedVersion, - uint32 _versionPassed, - uint32 _storedDeadline, - uint32 _deadlinePassed - ) internal pure { - revert Errors.Outbox__IncompatibleEntryArguments( - _entryKey, - _storedFee, - _feePassed, - _storedVersion, - _versionPassed, - _storedDeadline, - _deadlinePassed - ); + function hasMessageBeenConsumedAtBlockAndIndex(uint256 _l2BlockNumber, uint256 _leafIndex) + external + view + override(IOutbox) + returns (bool) + { + return roots[_l2BlockNumber].nullified[_leafIndex]; } } diff --git a/l1-contracts/test/NewOutbox.t.sol b/l1-contracts/test/NewOutbox.t.sol deleted file mode 100644 index 2a22f78115d..00000000000 --- a/l1-contracts/test/NewOutbox.t.sol +++ /dev/null @@ -1,254 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2024 Aztec Labs. -pragma solidity >=0.8.18; - -import {Test} from "forge-std/Test.sol"; -import {NewOutbox} from "../src/core/messagebridge/NewOutbox.sol"; -import {INewOutbox} from "../src/core/interfaces/messagebridge/INewOutbox.sol"; -import {Errors} from "../src/core/libraries/Errors.sol"; -import {DataStructures} from "../src/core/libraries/DataStructures.sol"; -import {Hash} from "../src/core/libraries/Hash.sol"; -import {NaiveMerkle} from "./merkle/Naive.sol"; -import {MerkleTestUtil} from "./merkle/TestUtil.sol"; - -contract NewOutboxTest is Test { - using Hash for DataStructures.L2ToL1Msg; - - address internal constant ROLLUP_CONTRACT = address(0x42069123); - address internal constant NOT_ROLLUP_CONTRACT = address(0x69); - address internal constant NOT_RECIPIENT = address(0x420); - uint256 internal constant DEFAULT_TREE_HEIGHT = 2; - uint256 internal constant AZTEC_VERSION = 1; - - NewOutbox internal outbox; - NaiveMerkle internal zeroedTree; - MerkleTestUtil internal merkleTestUtil; - - function setUp() public { - outbox = new NewOutbox(ROLLUP_CONTRACT); - zeroedTree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); - merkleTestUtil = new MerkleTestUtil(); - } - - function _fakeMessage(address _recipient) internal view returns (DataStructures.L2ToL1Msg memory) { - return DataStructures.L2ToL1Msg({ - sender: DataStructures.L2Actor({ - actor: 0x2000000000000000000000000000000000000000000000000000000000000000, - version: AZTEC_VERSION - }), - recipient: DataStructures.L1Actor({actor: _recipient, chainId: block.chainid}), - content: 0x3000000000000000000000000000000000000000000000000000000000000000 - }); - } - - function testRevertIfInsertingFromNonRollup() public { - bytes32 root = zeroedTree.computeRoot(); - - vm.prank(NOT_ROLLUP_CONTRACT); - vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__Unauthorized.selector)); - outbox.insert(1, root, DEFAULT_TREE_HEIGHT); - } - - function testRevertIfInsertingDuplicate() public { - bytes32 root = zeroedTree.computeRoot(); - - vm.prank(ROLLUP_CONTRACT); - outbox.insert(1, root, DEFAULT_TREE_HEIGHT); - - vm.prank(ROLLUP_CONTRACT); - vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__RootAlreadySetAtBlock.selector, 1)); - outbox.insert(1, root, DEFAULT_TREE_HEIGHT); - } - - // This function tests the insertion of random arrays of L2 to L1 messages - // We make a naive tree with a computed height, insert the leafs into it, and compute a root. We then add the root as the root of the - // L2 to L1 message tree, expect for the correct event to be emitted, and then query for the root in the contract—making sure the roots, as well as the - // the tree height (which is also the length of the sibling path) match - function testInsertVariedLeafs(bytes32[] calldata _messageLeafs) public { - uint256 treeHeight = merkleTestUtil.calculateTreeHeightFromSize(_messageLeafs.length); - NaiveMerkle tree = new NaiveMerkle(treeHeight); - - for (uint256 i = 0; i < _messageLeafs.length; i++) { - vm.assume(_messageLeafs[i] != bytes32(0)); - tree.insertLeaf(_messageLeafs[i]); - } - - bytes32 root = tree.computeRoot(); - - vm.expectEmit(true, true, true, true, address(outbox)); - emit INewOutbox.RootAdded(1, root, treeHeight); - vm.prank(ROLLUP_CONTRACT); - outbox.insert(1, root, treeHeight); - - (bytes32 actualRoot, uint256 actualHeight) = outbox.roots(1); - assertEq(root, actualRoot); - assertEq(treeHeight, actualHeight); - } - - function testRevertIfConsumingMessageBelongingToOther() public { - DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); - - (bytes32[] memory path,) = zeroedTree.computeSiblingPath(0); - - vm.prank(NOT_RECIPIENT); - vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__InvalidRecipient.selector, address(this), NOT_RECIPIENT) - ); - outbox.consume(1, 1, fakeMessage, path); - } - - function testRevertIfConsumingMessageWithInvalidChainId() public { - DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); - - (bytes32[] memory path,) = zeroedTree.computeSiblingPath(0); - - fakeMessage.recipient.chainId = block.chainid + 1; - - vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__InvalidChainId.selector)); - outbox.consume(1, 1, fakeMessage, path); - } - - function testRevertIfNothingInsertedAtBlockNumber() public { - uint256 blockNumber = 1; - DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); - - (bytes32[] memory path,) = zeroedTree.computeSiblingPath(0); - - vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__NothingToConsumeAtBlock.selector, blockNumber) - ); - outbox.consume(blockNumber, 1, fakeMessage, path); - } - - function testRevertIfTryingToConsumeSameMessage() public { - DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); - bytes32 leaf = fakeMessage.sha256ToField(); - - NaiveMerkle tree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); - tree.insertLeaf(leaf); - bytes32 root = tree.computeRoot(); - - vm.prank(ROLLUP_CONTRACT); - outbox.insert(1, root, DEFAULT_TREE_HEIGHT); - - (bytes32[] memory path,) = tree.computeSiblingPath(0); - outbox.consume(1, 0, fakeMessage, path); - vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__AlreadyNullified.selector, 1, 0)); - outbox.consume(1, 0, fakeMessage, path); - } - - function testRevertIfPathHeightMismatch() public { - DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); - bytes32 leaf = fakeMessage.sha256ToField(); - - NaiveMerkle tree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); - tree.insertLeaf(leaf); - bytes32 root = tree.computeRoot(); - - vm.prank(ROLLUP_CONTRACT); - outbox.insert(1, root, DEFAULT_TREE_HEIGHT); - - NaiveMerkle biggerTree = new NaiveMerkle(DEFAULT_TREE_HEIGHT + 1); - tree.insertLeaf(leaf); - - (bytes32[] memory path,) = biggerTree.computeSiblingPath(0); - vm.expectRevert( - abi.encodeWithSelector( - Errors.Outbox__InvalidPathLength.selector, DEFAULT_TREE_HEIGHT, DEFAULT_TREE_HEIGHT + 1 - ) - ); - outbox.consume(1, 0, fakeMessage, path); - } - - function testRevertIfTryingToConsumeMessageNotInTree() public { - DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); - bytes32 leaf = fakeMessage.sha256ToField(); - fakeMessage.content = bytes32(uint256(42069)); - bytes32 modifiedLeaf = fakeMessage.sha256ToField(); - - NaiveMerkle tree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); - tree.insertLeaf(leaf); - bytes32 root = tree.computeRoot(); - - NaiveMerkle modifiedTree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); - modifiedTree.insertLeaf(modifiedLeaf); - bytes32 modifiedRoot = modifiedTree.computeRoot(); - - vm.prank(ROLLUP_CONTRACT); - outbox.insert(1, root, DEFAULT_TREE_HEIGHT); - - (bytes32[] memory path,) = modifiedTree.computeSiblingPath(0); - - vm.expectRevert( - abi.encodeWithSelector(Errors.MerkleLib__InvalidRoot.selector, root, modifiedRoot) - ); - outbox.consume(1, 0, fakeMessage, path); - } - - function testValidInsertAndConsume() public { - DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); - bytes32 leaf = fakeMessage.sha256ToField(); - - NaiveMerkle tree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); - tree.insertLeaf(leaf); - bytes32 root = tree.computeRoot(); - - vm.prank(ROLLUP_CONTRACT); - outbox.insert(1, root, DEFAULT_TREE_HEIGHT); - - (bytes32[] memory path,) = tree.computeSiblingPath(0); - - bool statusBeforeConsumption = outbox.hasMessageBeenConsumedAtBlockAndIndex(1, 0); - assertEq(abi.encode(0), abi.encode(statusBeforeConsumption)); - - vm.expectEmit(true, true, true, true, address(outbox)); - emit INewOutbox.MessageConsumed(1, root, leaf, 0); - outbox.consume(1, 0, fakeMessage, path); - - bool statusAfterConsumption = outbox.hasMessageBeenConsumedAtBlockAndIndex(1, 0); - assertEq(abi.encode(1), abi.encode(statusAfterConsumption)); - } - - // This test takes awhile so to keep it somewhat reasonable we've set a limit on the amount of fuzz runs - /// forge-config: default.fuzz.runs = 64 - function testInsertAndConsumeWithVariedRecipients( - address[256] calldata _recipients, - uint256 _blockNumber, - uint8 _size - ) public { - uint256 numberOfMessages = bound(_size, 1, _recipients.length); - DataStructures.L2ToL1Msg[] memory messages = new DataStructures.L2ToL1Msg[](numberOfMessages); - - uint256 treeHeight = merkleTestUtil.calculateTreeHeightFromSize(numberOfMessages); - NaiveMerkle tree = new NaiveMerkle(treeHeight); - - for (uint256 i = 0; i < numberOfMessages; i++) { - DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(_recipients[i]); - messages[i] = fakeMessage; - bytes32 modifiedLeaf = fakeMessage.sha256ToField(); - - tree.insertLeaf(modifiedLeaf); - } - - bytes32 root = tree.computeRoot(); - - vm.expectEmit(true, true, true, true, address(outbox)); - emit INewOutbox.RootAdded(_blockNumber, root, treeHeight); - vm.prank(ROLLUP_CONTRACT); - outbox.insert(_blockNumber, root, treeHeight); - - for (uint256 i = 0; i < numberOfMessages; i++) { - (bytes32[] memory path, bytes32 leaf) = tree.computeSiblingPath(i); - - vm.expectEmit(true, true, true, true, address(outbox)); - emit INewOutbox.MessageConsumed(_blockNumber, root, leaf, i); - vm.prank(_recipients[i]); - outbox.consume(_blockNumber, i, messages[i], path); - } - } - - function testCheckOutOfBoundsStatus(uint256 _blockNumber, uint256 _leafIndex) external { - bool outOfBounds = outbox.hasMessageBeenConsumedAtBlockAndIndex(_blockNumber, _leafIndex); - assertEq(abi.encode(0), abi.encode(outOfBounds)); - } -} diff --git a/l1-contracts/test/Outbox.t.sol b/l1-contracts/test/Outbox.t.sol index 4ad45b5abc3..c111b43a48b 100644 --- a/l1-contracts/test/Outbox.t.sol +++ b/l1-contracts/test/Outbox.t.sol @@ -1,135 +1,256 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2023 Aztec Labs. +// Copyright 2024 Aztec Labs. pragma solidity >=0.8.18; import {Test} from "forge-std/Test.sol"; -import {IOutbox} from "../src/core/interfaces/messagebridge/IOutbox.sol"; import {Outbox} from "../src/core/messagebridge/Outbox.sol"; -import {Registry} from "../src/core/messagebridge/Registry.sol"; +import {IOutbox} from "../src/core/interfaces/messagebridge/IOutbox.sol"; import {Errors} from "../src/core/libraries/Errors.sol"; import {DataStructures} from "../src/core/libraries/DataStructures.sol"; -import {MessageBox} from "../src/core/libraries/MessageBox.sol"; +import {Hash} from "../src/core/libraries/Hash.sol"; +import {NaiveMerkle} from "./merkle/Naive.sol"; +import {MerkleTestUtil} from "./merkle/TestUtil.sol"; contract OutboxTest is Test { - Registry internal registry; - Outbox internal outbox; - uint256 internal version = 0; + using Hash for DataStructures.L2ToL1Msg; - event MessageAdded(bytes32 indexed entryKey); - event MessageConsumed(bytes32 indexed entryKey, address indexed recipient); + address internal constant ROLLUP_CONTRACT = address(0x42069123); + address internal constant NOT_RECIPIENT = address(0x420); + uint256 internal constant DEFAULT_TREE_HEIGHT = 2; + uint256 internal constant AZTEC_VERSION = 1; + + Outbox internal outbox; + NaiveMerkle internal zeroedTree; + MerkleTestUtil internal merkleTestUtil; function setUp() public { - address rollup = address(this); - registry = new Registry(); - outbox = new Outbox(address(registry)); - version = registry.upgrade(rollup, address(0x0), address(outbox)); + outbox = new Outbox(ROLLUP_CONTRACT); + zeroedTree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); + merkleTestUtil = new MerkleTestUtil(); } - function _fakeMessage() internal view returns (DataStructures.L2ToL1Msg memory) { + function _fakeMessage(address _recipient) internal view returns (DataStructures.L2ToL1Msg memory) { return DataStructures.L2ToL1Msg({ sender: DataStructures.L2Actor({ actor: 0x2000000000000000000000000000000000000000000000000000000000000000, - version: version + version: AZTEC_VERSION }), - recipient: DataStructures.L1Actor({actor: address(this), chainId: block.chainid}), + recipient: DataStructures.L1Actor({actor: _recipient, chainId: block.chainid}), content: 0x3000000000000000000000000000000000000000000000000000000000000000 }); } - function testRevertIfInsertingFromNonRollup() public { - vm.prank(address(0x1)); - bytes32[] memory entryKeys = new bytes32[](1); - entryKeys[0] = bytes32("random"); - vm.expectRevert( - abi.encodeWithSelector(Errors.Registry__RollupNotRegistered.selector, address(1)) - ); - outbox.sendL1Messages(entryKeys); + function testRevertIfInsertingFromNonRollup(address _caller) public { + vm.assume(ROLLUP_CONTRACT != _caller); + bytes32 root = zeroedTree.computeRoot(); + + vm.prank(_caller); + vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__Unauthorized.selector)); + outbox.insert(1, root, DEFAULT_TREE_HEIGHT); } - // fuzz batch insert -> check inserted. event emitted - function testFuzzBatchInsert(bytes32[] memory _entryKeys) public { - // expected events - for (uint256 i = 0; i < _entryKeys.length; i++) { - if (_entryKeys[i] == bytes32(0)) continue; - vm.expectEmit(true, false, false, false); - emit MessageAdded(_entryKeys[i]); - } + function testRevertIfInsertingDuplicate() public { + bytes32 root = zeroedTree.computeRoot(); - outbox.sendL1Messages(_entryKeys); - for (uint256 i = 0; i < _entryKeys.length; i++) { - if (_entryKeys[i] == bytes32(0)) continue; - bytes32 key = _entryKeys[i]; - DataStructures.Entry memory entry = outbox.get(key); - assertGt(entry.count, 0); - assertEq(entry.fee, 0); - assertEq(entry.deadline, 0); + vm.prank(ROLLUP_CONTRACT); + outbox.insert(1, root, DEFAULT_TREE_HEIGHT); + + vm.prank(ROLLUP_CONTRACT); + vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__RootAlreadySetAtBlock.selector, 1)); + outbox.insert(1, root, DEFAULT_TREE_HEIGHT); + } + + // This function tests the insertion of random arrays of L2 to L1 messages + // We make a naive tree with a computed height, insert the leafs into it, and compute a root. We then add the root as the root of the + // L2 to L1 message tree, expect for the correct event to be emitted, and then query for the root in the contract—making sure the roots, as well as the + // the tree height (which is also the length of the sibling path) match + function testInsertVariedLeafs(bytes32[] calldata _messageLeafs) public { + uint256 treeHeight = merkleTestUtil.calculateTreeHeightFromSize(_messageLeafs.length); + NaiveMerkle tree = new NaiveMerkle(treeHeight); + + for (uint256 i = 0; i < _messageLeafs.length; i++) { + vm.assume(_messageLeafs[i] != bytes32(0)); + tree.insertLeaf(_messageLeafs[i]); } + + bytes32 root = tree.computeRoot(); + + vm.expectEmit(true, true, true, true, address(outbox)); + emit IOutbox.RootAdded(1, root, treeHeight); + vm.prank(ROLLUP_CONTRACT); + outbox.insert(1, root, treeHeight); + + (bytes32 actualRoot, uint256 actualHeight) = outbox.roots(1); + assertEq(root, actualRoot); + assertEq(treeHeight, actualHeight); } - function testRevertIfConsumingFromWrongRecipient() public { - DataStructures.L2ToL1Msg memory message = _fakeMessage(); - message.recipient.actor = address(0x1); - vm.expectRevert(Errors.Outbox__Unauthorized.selector); - outbox.consume(message); + function testRevertIfConsumingMessageBelongingToOther() public { + DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); + + (bytes32[] memory path,) = zeroedTree.computeSiblingPath(0); + + vm.prank(NOT_RECIPIENT); + vm.expectRevert( + abi.encodeWithSelector(Errors.Outbox__InvalidRecipient.selector, address(this), NOT_RECIPIENT) + ); + outbox.consume(fakeMessage, 1, 1, path); } - function testRevertIfConsumingForWrongChain() public { - DataStructures.L2ToL1Msg memory message = _fakeMessage(); - message.recipient.chainId = 2; - vm.expectRevert(Errors.Outbox__InvalidChainId.selector); - outbox.consume(message); + function testRevertIfConsumingMessageWithInvalidChainId() public { + DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); + + (bytes32[] memory path,) = zeroedTree.computeSiblingPath(0); + + fakeMessage.recipient.chainId = block.chainid + 1; + + vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__InvalidChainId.selector)); + outbox.consume(fakeMessage, 1, 1, path); + } + + function testRevertIfNothingInsertedAtBlockNumber() public { + uint256 blockNumber = 1; + DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); + + (bytes32[] memory path,) = zeroedTree.computeSiblingPath(0); + + vm.expectRevert( + abi.encodeWithSelector(Errors.Outbox__NothingToConsumeAtBlock.selector, blockNumber) + ); + outbox.consume(fakeMessage, blockNumber, 1, path); } - function testRevertIfConsumingMessageThatDoesntExist() public { - DataStructures.L2ToL1Msg memory message = _fakeMessage(); - bytes32 entryKey = outbox.computeEntryKey(message); - vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKey)); - outbox.consume(message); + function testRevertIfTryingToConsumeSameMessage() public { + DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); + bytes32 leaf = fakeMessage.sha256ToField(); + + NaiveMerkle tree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); + tree.insertLeaf(leaf); + bytes32 root = tree.computeRoot(); + + vm.prank(ROLLUP_CONTRACT); + outbox.insert(1, root, DEFAULT_TREE_HEIGHT); + + (bytes32[] memory path,) = tree.computeSiblingPath(0); + outbox.consume(fakeMessage, 1, 0, path); + vm.expectRevert(abi.encodeWithSelector(Errors.Outbox__AlreadyNullified.selector, 1, 0)); + outbox.consume(fakeMessage, 1, 0, path); } - function testRevertIfInsertingFromWrongRollup() public { - address wrongRollup = address(0xbeeffeed); - uint256 wrongVersion = registry.upgrade(wrongRollup, address(0x0), address(outbox)); + function testRevertIfPathHeightMismatch() public { + DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); + bytes32 leaf = fakeMessage.sha256ToField(); - DataStructures.L2ToL1Msg memory message = _fakeMessage(); - // correctly set message.recipient to this address - message.recipient = DataStructures.L1Actor({actor: address(this), chainId: block.chainid}); + NaiveMerkle tree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); + tree.insertLeaf(leaf); + bytes32 root = tree.computeRoot(); - bytes32 expectedEntryKey = outbox.computeEntryKey(message); - bytes32[] memory entryKeys = new bytes32[](1); - entryKeys[0] = expectedEntryKey; + vm.prank(ROLLUP_CONTRACT); + outbox.insert(1, root, DEFAULT_TREE_HEIGHT); - vm.prank(wrongRollup); - outbox.sendL1Messages(entryKeys); + NaiveMerkle biggerTree = new NaiveMerkle(DEFAULT_TREE_HEIGHT + 1); + tree.insertLeaf(leaf); - vm.prank(message.recipient.actor); + (bytes32[] memory path,) = biggerTree.computeSiblingPath(0); vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__InvalidVersion.selector, wrongVersion, version) + abi.encodeWithSelector( + Errors.Outbox__InvalidPathLength.selector, DEFAULT_TREE_HEIGHT, DEFAULT_TREE_HEIGHT + 1 + ) ); - outbox.consume(message); + outbox.consume(fakeMessage, 1, 0, path); } - function testFuzzConsume(DataStructures.L2ToL1Msg memory _message) public { - // correctly set message.recipient to this address - _message.recipient = DataStructures.L1Actor({actor: address(this), chainId: block.chainid}); + function testRevertIfTryingToConsumeMessageNotInTree() public { + DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); + bytes32 leaf = fakeMessage.sha256ToField(); + fakeMessage.content = bytes32(uint256(42069)); + bytes32 modifiedLeaf = fakeMessage.sha256ToField(); + + NaiveMerkle tree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); + tree.insertLeaf(leaf); + bytes32 root = tree.computeRoot(); - // correctly set the message.sender.version - _message.sender.version = version; + NaiveMerkle modifiedTree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); + modifiedTree.insertLeaf(modifiedLeaf); + bytes32 modifiedRoot = modifiedTree.computeRoot(); - bytes32 expectedEntryKey = outbox.computeEntryKey(_message); - bytes32[] memory entryKeys = new bytes32[](1); - entryKeys[0] = expectedEntryKey; - outbox.sendL1Messages(entryKeys); + vm.prank(ROLLUP_CONTRACT); + outbox.insert(1, root, DEFAULT_TREE_HEIGHT); - vm.prank(_message.recipient.actor); - vm.expectEmit(true, true, false, false); - emit MessageConsumed(expectedEntryKey, _message.recipient.actor); - outbox.consume(_message); + (bytes32[] memory path,) = modifiedTree.computeSiblingPath(0); - // ensure no such message to consume: vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, expectedEntryKey) + abi.encodeWithSelector( + Errors.MerkleLib__InvalidRoot.selector, root, modifiedRoot, modifiedLeaf, 0 + ) ); - outbox.consume(_message); + outbox.consume(fakeMessage, 1, 0, path); + } + + function testValidInsertAndConsume() public { + DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(address(this)); + bytes32 leaf = fakeMessage.sha256ToField(); + + NaiveMerkle tree = new NaiveMerkle(DEFAULT_TREE_HEIGHT); + tree.insertLeaf(leaf); + bytes32 root = tree.computeRoot(); + + vm.prank(ROLLUP_CONTRACT); + outbox.insert(1, root, DEFAULT_TREE_HEIGHT); + + (bytes32[] memory path,) = tree.computeSiblingPath(0); + + bool statusBeforeConsumption = outbox.hasMessageBeenConsumedAtBlockAndIndex(1, 0); + assertEq(abi.encode(0), abi.encode(statusBeforeConsumption)); + + vm.expectEmit(true, true, true, true, address(outbox)); + emit IOutbox.MessageConsumed(1, root, leaf, 0); + outbox.consume(fakeMessage, 1, 0, path); + + bool statusAfterConsumption = outbox.hasMessageBeenConsumedAtBlockAndIndex(1, 0); + assertEq(abi.encode(1), abi.encode(statusAfterConsumption)); + } + + // This test takes awhile so to keep it somewhat reasonable we've set a limit on the amount of fuzz runs + /// forge-config: default.fuzz.runs = 64 + function testInsertAndConsumeWithVariedRecipients( + address[256] calldata _recipients, + uint256 _blockNumber, + uint8 _size + ) public { + uint256 numberOfMessages = bound(_size, 1, _recipients.length); + DataStructures.L2ToL1Msg[] memory messages = new DataStructures.L2ToL1Msg[](numberOfMessages); + + uint256 treeHeight = merkleTestUtil.calculateTreeHeightFromSize(numberOfMessages); + NaiveMerkle tree = new NaiveMerkle(treeHeight); + + for (uint256 i = 0; i < numberOfMessages; i++) { + DataStructures.L2ToL1Msg memory fakeMessage = _fakeMessage(_recipients[i]); + messages[i] = fakeMessage; + bytes32 modifiedLeaf = fakeMessage.sha256ToField(); + + tree.insertLeaf(modifiedLeaf); + } + + bytes32 root = tree.computeRoot(); + + vm.expectEmit(true, true, true, true, address(outbox)); + emit IOutbox.RootAdded(_blockNumber, root, treeHeight); + vm.prank(ROLLUP_CONTRACT); + outbox.insert(_blockNumber, root, treeHeight); + + for (uint256 i = 0; i < numberOfMessages; i++) { + (bytes32[] memory path, bytes32 leaf) = tree.computeSiblingPath(i); + + vm.expectEmit(true, true, true, true, address(outbox)); + emit IOutbox.MessageConsumed(_blockNumber, root, leaf, i); + vm.prank(_recipients[i]); + outbox.consume(messages[i], _blockNumber, i, path); + } + } + + function testCheckOutOfBoundsStatus(uint256 _blockNumber, uint256 _leafIndex) external { + bool outOfBounds = outbox.hasMessageBeenConsumedAtBlockAndIndex(_blockNumber, _leafIndex); + assertEq(abi.encode(0), abi.encode(outOfBounds)); } } diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 97865988f55..e5b7c3e9e46 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -12,6 +12,8 @@ import {Outbox} from "../src/core/messagebridge/Outbox.sol"; import {Errors} from "../src/core/libraries/Errors.sol"; import {Rollup} from "../src/core/Rollup.sol"; import {AvailabilityOracle} from "../src/core/availability_oracle/AvailabilityOracle.sol"; +import {NaiveMerkle} from "./merkle/Naive.sol"; +import {MerkleTestUtil} from "./merkle/TestUtil.sol"; /** * Blocks are generated using the `integration_l1_publisher.test.ts` tests. @@ -22,16 +24,20 @@ contract RollupTest is DecoderBase { Inbox internal inbox; Outbox internal outbox; Rollup internal rollup; + MerkleTestUtil internal merkleTestUtil; + AvailabilityOracle internal availabilityOracle; function setUp() public virtual { registry = new Registry(); - outbox = new Outbox(address(registry)); availabilityOracle = new AvailabilityOracle(); rollup = new Rollup(registry, availabilityOracle); inbox = Inbox(address(rollup.INBOX())); + outbox = Outbox(address(rollup.OUTBOX())); registry.upgrade(address(rollup), address(inbox), address(outbox)); + + merkleTestUtil = new MerkleTestUtil(); } function testMixedBlock() public { @@ -137,20 +143,22 @@ contract RollupTest is DecoderBase { assertEq(inbox.toConsume(), toConsume + 1, "Message subtree not consumed"); - (, bytes32[] memory outboxWrites) = vm.accesses(address(outbox)); - + bytes32 l2ToL1MessageTreeRoot; { - uint256 count = 0; + uint256 treeHeight = + merkleTestUtil.calculateTreeHeightFromSize(full.messages.l2ToL1Messages.length); + NaiveMerkle tree = new NaiveMerkle(treeHeight); for (uint256 i = 0; i < full.messages.l2ToL1Messages.length; i++) { - if (full.messages.l2ToL1Messages[i] == bytes32(0)) { - continue; - } - assertTrue(outbox.contains(full.messages.l2ToL1Messages[i]), "msg not in outbox"); - count++; + tree.insertLeaf(full.messages.l2ToL1Messages[i]); } - assertEq(outboxWrites.length, count, "Invalid outbox writes"); + + l2ToL1MessageTreeRoot = tree.computeRoot(); } + (bytes32 root,) = outbox.roots(full.block.decodedHeader.globalVariables.blockNumber); + + assertEq(l2ToL1MessageTreeRoot, root); + assertEq(rollup.archive(), archive, "Invalid archive"); } diff --git a/l1-contracts/test/portals/DataStructures.sol b/l1-contracts/test/portals/DataStructures.sol new file mode 100644 index 00000000000..7f6291a0f98 --- /dev/null +++ b/l1-contracts/test/portals/DataStructures.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +library DataStructures { + struct OutboxMessageMetadata { + uint256 _l2BlockNumber; + uint256 _leafIndex; + bytes32[] _path; + } +} diff --git a/l1-contracts/test/portals/TokenPortal.sol b/l1-contracts/test/portals/TokenPortal.sol index 398134cd451..1553673d1ef 100644 --- a/l1-contracts/test/portals/TokenPortal.sol +++ b/l1-contracts/test/portals/TokenPortal.sol @@ -6,6 +6,7 @@ import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; // Messaging import {IRegistry} from "../../src/core/interfaces/messagebridge/IRegistry.sol"; import {IInbox} from "../../src/core/interfaces/messagebridge/IInbox.sol"; +import {IOutbox} from "../../src/core/interfaces/messagebridge/IOutbox.sol"; import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; // docs:start:content_hash_sol_import import {Hash} from "../../src/core/libraries/Hash.sol"; @@ -93,13 +94,19 @@ contract TokenPortal { * @param _recipient - The address to send the funds to * @param _amount - The amount to withdraw * @param _withCaller - Flag to use `msg.sender` as caller, otherwise address(0) + * @param _l2BlockNumber - The address to send the funds to + * @param _leafIndex - The amount to withdraw + * @param _path - Flag to use `msg.sender` as caller, otherwise address(0) * Must match the caller of the message (specified from L2) to consume it. - * @return The key of the entry in the Outbox */ - function withdraw(address _recipient, uint256 _amount, bool _withCaller) - external - returns (bytes32) - { + function withdraw( + address _recipient, + uint256 _amount, + bool _withCaller, + uint256 _l2BlockNumber, + uint256 _leafIndex, + bytes32[] calldata _path + ) external { DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ sender: DataStructures.L2Actor(l2Bridge, 1), recipient: DataStructures.L1Actor(address(this), block.chainid), @@ -113,11 +120,11 @@ contract TokenPortal { ) }); - bytes32 entryKey = registry.getOutbox().consume(message); + IOutbox outbox = registry.getOutbox(); - underlying.transfer(_recipient, _amount); + outbox.consume(message, _l2BlockNumber, _leafIndex, _path); - return entryKey; + underlying.transfer(_recipient, _amount); } // docs:end:token_portal_withdraw } diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index 6a171b91e4d..8ec35f3db7d 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -7,19 +7,20 @@ import {Rollup} from "../../src/core/Rollup.sol"; import {AvailabilityOracle} from "../../src/core/availability_oracle/AvailabilityOracle.sol"; import {Constants} from "../../src/core/libraries/ConstantsGen.sol"; import {Registry} from "../../src/core/messagebridge/Registry.sol"; -import {Outbox} from "../../src/core/messagebridge/Outbox.sol"; import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; import {Hash} from "../../src/core/libraries/Hash.sol"; import {Errors} from "../../src/core/libraries/Errors.sol"; // Interfaces -import {IRegistry} from "../../src/core/interfaces/messagebridge/IRegistry.sol"; import {IInbox} from "../../src/core/interfaces/messagebridge/IInbox.sol"; +import {IOutbox} from "../../src/core/interfaces/messagebridge/IOutbox.sol"; // Portal tokens import {TokenPortal} from "./TokenPortal.sol"; import {PortalERC20} from "./PortalERC20.sol"; +import {NaiveMerkle} from "../merkle/Naive.sol"; + contract TokenPortalTest is Test { using Hash for DataStructures.L1ToL2Msg; @@ -28,8 +29,10 @@ contract TokenPortalTest is Test { event MessageConsumed(bytes32 indexed entryKey, address indexed recipient); Registry internal registry; + IInbox internal inbox; - Outbox internal outbox; + IOutbox internal outbox; + Rollup internal rollup; bytes32 internal l2TokenAddress = bytes32(uint256(0x42)); @@ -54,9 +57,9 @@ contract TokenPortalTest is Test { function setUp() public { registry = new Registry(); - outbox = new Outbox(address(registry)); rollup = new Rollup(registry, new AvailabilityOracle()); inbox = rollup.INBOX(); + outbox = rollup.OUTBOX(); registry.upgrade(address(rollup), address(inbox), address(outbox)); @@ -148,10 +151,9 @@ contract TokenPortalTest is Test { function _createWithdrawMessageForOutbox(address _designatedCaller) internal - view - returns (bytes32) + returns (bytes32, bytes32) { - bytes32 entryKey = outbox.computeEntryKey( + bytes32 l2ToL1Message = Hash.sha256ToField( DataStructures.L2ToL1Msg({ sender: DataStructures.L2Actor({actor: l2TokenAddress, version: 1}), recipient: DataStructures.L1Actor({actor: address(tokenPortal), chainId: block.chainid}), @@ -162,71 +164,100 @@ contract TokenPortalTest is Test { ) }) ); - return entryKey; + + uint256 treeHeight = 1; + NaiveMerkle tree = new NaiveMerkle(treeHeight); + tree.insertLeaf(l2ToL1Message); + bytes32 treeRoot = tree.computeRoot(); + + return (l2ToL1Message, treeRoot); } - function _addWithdrawMessageInOutbox(address _designatedCaller) internal returns (bytes32) { + function _addWithdrawMessageInOutbox(address _designatedCaller, uint256 _l2BlockNumber) + internal + returns (bytes32, bytes32[] memory, bytes32) + { // send assets to the portal portalERC20.mint(address(tokenPortal), withdrawAmount); // Create the message - bytes32[] memory entryKeys = new bytes32[](1); - entryKeys[0] = _createWithdrawMessageForOutbox(_designatedCaller); + (bytes32 l2ToL1Message,) = _createWithdrawMessageForOutbox(_designatedCaller); + + uint256 treeHeight = 1; + NaiveMerkle tree = new NaiveMerkle(treeHeight); + tree.insertLeaf(l2ToL1Message); + + (bytes32[] memory siblingPath,) = tree.computeSiblingPath(0); + + bytes32 treeRoot = tree.computeRoot(); // Insert messages into the outbox (impersonating the rollup contract) vm.prank(address(rollup)); - outbox.sendL1Messages(entryKeys); - return entryKeys[0]; + outbox.insert(_l2BlockNumber, treeRoot, treeHeight); + + return (l2ToL1Message, siblingPath, treeRoot); } function testAnyoneCanCallWithdrawIfNoDesignatedCaller(address _caller) public { vm.assume(_caller != address(0)); - bytes32 expectedEntryKey = _addWithdrawMessageInOutbox(address(0)); + + uint256 l2BlockNumber = 69; + // add message with caller as this address + (bytes32 l2ToL1Message, bytes32[] memory siblingPath, bytes32 treeRoot) = + _addWithdrawMessageInOutbox(address(0), l2BlockNumber); assertEq(portalERC20.balanceOf(recipient), 0); vm.startPrank(_caller); vm.expectEmit(true, true, true, true); - emit MessageConsumed(expectedEntryKey, address(tokenPortal)); - bytes32 actualEntryKey = tokenPortal.withdraw(recipient, withdrawAmount, false); - assertEq(expectedEntryKey, actualEntryKey); + emit IOutbox.MessageConsumed(l2BlockNumber, treeRoot, l2ToL1Message, 0); + tokenPortal.withdraw(recipient, withdrawAmount, false, l2BlockNumber, 0, siblingPath); + // Should have received 654 RNA tokens assertEq(portalERC20.balanceOf(recipient), withdrawAmount); // Should not be able to withdraw again vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, actualEntryKey) + abi.encodeWithSelector(Errors.Outbox__AlreadyNullified.selector, l2BlockNumber, 0) ); - tokenPortal.withdraw(recipient, withdrawAmount, false); + tokenPortal.withdraw(recipient, withdrawAmount, false, l2BlockNumber, 0, siblingPath); vm.stopPrank(); } function testWithdrawWithDesignatedCallerFailsForOtherCallers(address _caller) public { vm.assume(_caller != address(this)); + uint256 l2BlockNumber = 69; // add message with caller as this address - _addWithdrawMessageInOutbox(address(this)); + (, bytes32[] memory siblingPath, bytes32 treeRoot) = + _addWithdrawMessageInOutbox(address(this), l2BlockNumber); vm.startPrank(_caller); - bytes32 entryKeyPortalChecksAgainst = _createWithdrawMessageForOutbox(_caller); + (bytes32 l2ToL1MessageHash, bytes32 consumedRoot) = _createWithdrawMessageForOutbox(_caller); vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) + abi.encodeWithSelector( + Errors.MerkleLib__InvalidRoot.selector, treeRoot, consumedRoot, l2ToL1MessageHash, 0 + ) ); - tokenPortal.withdraw(recipient, withdrawAmount, true); + tokenPortal.withdraw(recipient, withdrawAmount, true, l2BlockNumber, 0, siblingPath); - entryKeyPortalChecksAgainst = _createWithdrawMessageForOutbox(address(0)); + (l2ToL1MessageHash, consumedRoot) = _createWithdrawMessageForOutbox(address(0)); vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) + abi.encodeWithSelector( + Errors.MerkleLib__InvalidRoot.selector, treeRoot, consumedRoot, l2ToL1MessageHash, 0 + ) ); - tokenPortal.withdraw(recipient, withdrawAmount, false); + tokenPortal.withdraw(recipient, withdrawAmount, false, l2BlockNumber, 0, siblingPath); vm.stopPrank(); } function testWithdrawWithDesignatedCallerSucceedsForDesignatedCaller() public { + uint256 l2BlockNumber = 69; // add message with caller as this address - bytes32 expectedEntryKey = _addWithdrawMessageInOutbox(address(this)); + (bytes32 l2ToL1Message, bytes32[] memory siblingPath, bytes32 treeRoot) = + _addWithdrawMessageInOutbox(address(this), l2BlockNumber); vm.expectEmit(true, true, true, true); - emit MessageConsumed(expectedEntryKey, address(tokenPortal)); - bytes32 actualEntryKey = tokenPortal.withdraw(recipient, withdrawAmount, true); - assertEq(expectedEntryKey, actualEntryKey); + emit IOutbox.MessageConsumed(l2BlockNumber, treeRoot, l2ToL1Message, 0); + tokenPortal.withdraw(recipient, withdrawAmount, true, l2BlockNumber, 0, siblingPath); + // Should have received 654 RNA tokens assertEq(portalERC20.balanceOf(recipient), withdrawAmount); } diff --git a/l1-contracts/test/portals/UniswapPortal.sol b/l1-contracts/test/portals/UniswapPortal.sol index 5a76055e451..8eb22e31163 100644 --- a/l1-contracts/test/portals/UniswapPortal.sol +++ b/l1-contracts/test/portals/UniswapPortal.sol @@ -3,7 +3,9 @@ pragma solidity >=0.8.18; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {IRegistry} from "../../src/core/interfaces/messagebridge/IRegistry.sol"; +import {IOutbox} from "../../src/core/interfaces/messagebridge/IOutbox.sol"; import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; +import {DataStructures as PortalDataStructures} from "./DataStructures.sol"; import {Hash} from "../../src/core/libraries/Hash.sol"; // docs:start:setup @@ -61,7 +63,9 @@ contract UniswapPortal { uint256 _amountOutMinimum, bytes32 _aztecRecipient, bytes32 _secretHashForL1ToL2Message, - bool _withCaller + bool _withCaller, + // Avoiding stack too deep + PortalDataStructures.OutboxMessageMetadata[2] calldata _outboxMessageMetadata ) public returns (bytes32) { LocalSwapVars memory vars; @@ -69,7 +73,17 @@ contract UniswapPortal { vars.outputAsset = TokenPortal(_outputTokenPortal).underlying(); // Withdraw the input asset from the portal - TokenPortal(_inputTokenPortal).withdraw(address(this), _inAmount, true); + { + TokenPortal(_inputTokenPortal).withdraw( + address(this), + _inAmount, + true, + _outboxMessageMetadata[0]._l2BlockNumber, + _outboxMessageMetadata[0]._leafIndex, + _outboxMessageMetadata[0]._path + ); + } + { // prevent stack too deep errors vars.contentHash = Hash.sha256ToField( @@ -88,13 +102,20 @@ contract UniswapPortal { } // Consume the message from the outbox - registry.getOutbox().consume( - DataStructures.L2ToL1Msg({ - sender: DataStructures.L2Actor(l2UniswapAddress, 1), - recipient: DataStructures.L1Actor(address(this), block.chainid), - content: vars.contentHash - }) - ); + { + IOutbox outbox = registry.getOutbox(); + + outbox.consume( + DataStructures.L2ToL1Msg({ + sender: DataStructures.L2Actor(l2UniswapAddress, 1), + recipient: DataStructures.L1Actor(address(this), block.chainid), + content: vars.contentHash + }), + _outboxMessageMetadata[1]._l2BlockNumber, + _outboxMessageMetadata[1]._leafIndex, + _outboxMessageMetadata[1]._path + ); + } // Perform the swap ISwapRouter.ExactInputSingleParams memory swapParams; @@ -149,15 +170,26 @@ contract UniswapPortal { uint256 _amountOutMinimum, bytes32 _secretHashForRedeemingMintedNotes, bytes32 _secretHashForL1ToL2Message, - bool _withCaller + bool _withCaller, + // Avoiding stack too deep + PortalDataStructures.OutboxMessageMetadata[2] calldata _outboxMessageMetadata ) public returns (bytes32) { LocalSwapVars memory vars; vars.inputAsset = TokenPortal(_inputTokenPortal).underlying(); vars.outputAsset = TokenPortal(_outputTokenPortal).underlying(); - // Withdraw the input asset from the portal - TokenPortal(_inputTokenPortal).withdraw(address(this), _inAmount, true); + { + TokenPortal(_inputTokenPortal).withdraw( + address(this), + _inAmount, + true, + _outboxMessageMetadata[0]._l2BlockNumber, + _outboxMessageMetadata[0]._leafIndex, + _outboxMessageMetadata[0]._path + ); + } + { // prevent stack too deep errors vars.contentHash = Hash.sha256ToField( @@ -176,13 +208,20 @@ contract UniswapPortal { } // Consume the message from the outbox - registry.getOutbox().consume( - DataStructures.L2ToL1Msg({ - sender: DataStructures.L2Actor(l2UniswapAddress, 1), - recipient: DataStructures.L1Actor(address(this), block.chainid), - content: vars.contentHash - }) - ); + { + IOutbox outbox = registry.getOutbox(); + + outbox.consume( + DataStructures.L2ToL1Msg({ + sender: DataStructures.L2Actor(l2UniswapAddress, 1), + recipient: DataStructures.L1Actor(address(this), block.chainid), + content: vars.contentHash + }), + _outboxMessageMetadata[1]._l2BlockNumber, + _outboxMessageMetadata[1]._leafIndex, + _outboxMessageMetadata[1]._path + ); + } // Perform the swap ISwapRouter.ExactInputSingleParams memory swapParams; diff --git a/l1-contracts/test/portals/UniswapPortal.t.sol b/l1-contracts/test/portals/UniswapPortal.t.sol index 51caae33470..d1abc1de5e6 100644 --- a/l1-contracts/test/portals/UniswapPortal.t.sol +++ b/l1-contracts/test/portals/UniswapPortal.t.sol @@ -6,26 +6,30 @@ import "forge-std/Test.sol"; import {Rollup} from "../../src/core/Rollup.sol"; import {AvailabilityOracle} from "../../src/core/availability_oracle/AvailabilityOracle.sol"; import {Registry} from "../../src/core/messagebridge/Registry.sol"; -import {Outbox} from "../../src/core/messagebridge/Outbox.sol"; import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; +import {DataStructures as PortalDataStructures} from "./DataStructures.sol"; import {Hash} from "../../src/core/libraries/Hash.sol"; import {Errors} from "../../src/core/libraries/Errors.sol"; // Interfaces import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {IOutbox} from "../../src/core/interfaces/messagebridge/IOutbox.sol"; +import {NaiveMerkle} from "../merkle/Naive.sol"; // Portals import {TokenPortal} from "./TokenPortal.sol"; import {UniswapPortal} from "./UniswapPortal.sol"; contract UniswapPortalTest is Test { + using Hash for DataStructures.L2ToL1Msg; + event L1ToL2MessageCancelled(bytes32 indexed entryKey); IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); IERC20 public constant WETH9 = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - Outbox internal outbox; Rollup internal rollup; + Registry internal registry; bytes32 internal l2TokenAddress = bytes32(uint256(0x1)); bytes32 internal l2UniswapAddress = bytes32(uint256(0x2)); @@ -45,10 +49,9 @@ contract UniswapPortalTest is Test { uint256 forkId = vm.createFork(vm.rpcUrl("mainnet_fork")); vm.selectFork(forkId); - Registry registry = new Registry(); - outbox = new Outbox(address(registry)); + registry = new Registry(); rollup = new Rollup(registry, new AvailabilityOracle()); - registry.upgrade(address(rollup), address(rollup.INBOX()), address(outbox)); + registry.upgrade(address(rollup), address(rollup.INBOX()), address(rollup.OUTBOX())); daiTokenPortal = new TokenPortal(); daiTokenPortal.initialize(address(registry), address(DAI), l2TokenAddress); @@ -72,7 +75,7 @@ contract UniswapPortalTest is Test { function _createDaiWithdrawMessage(address _recipient, address _caller) internal view - returns (bytes32 entryKey) + returns (bytes32 l2ToL1MessageHash) { DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ sender: DataStructures.L2Actor(l2TokenAddress, 1), @@ -81,7 +84,8 @@ contract UniswapPortalTest is Test { abi.encodeWithSignature("withdraw(address,uint256,address)", _recipient, amount, _caller) ) }); - entryKey = outbox.computeEntryKey(message); + + return message.sha256ToField(); } /** @@ -93,7 +97,7 @@ contract UniswapPortalTest is Test { function _createUniswapSwapMessagePublic(bytes32 _aztecRecipient, address _caller) internal view - returns (bytes32 entryKey) + returns (bytes32 l2ToL1MessageHash) { DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ sender: DataStructures.L2Actor(l2UniswapAddress, 1), @@ -112,7 +116,8 @@ contract UniswapPortalTest is Test { ) ) }); - entryKey = outbox.computeEntryKey(message); + + return message.sha256ToField(); } /** @@ -124,7 +129,7 @@ contract UniswapPortalTest is Test { function _createUniswapSwapMessagePrivate( bytes32 _secretHashForRedeemingMintedNotes, address _caller - ) internal view returns (bytes32 entryKey) { + ) internal view returns (bytes32) { DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ sender: DataStructures.L2Actor(l2UniswapAddress, 1), recipient: DataStructures.L1Actor(address(uniswapPortal), block.chainid), @@ -142,28 +147,72 @@ contract UniswapPortalTest is Test { ) ) }); - entryKey = outbox.computeEntryKey(message); + + return message.sha256ToField(); } - function _addMessagesToOutbox(bytes32 daiWithdrawEntryKey, bytes32 swapEntryKey) internal { - bytes32[] memory entryKeys = new bytes32[](2); - entryKeys[0] = daiWithdrawEntryKey; - entryKeys[1] = swapEntryKey; + function _addMessagesToOutbox( + bytes32 daiWithdrawEntryKey, + bytes32 swapEntryKey, + uint256 _l2BlockNumber + ) internal returns (bytes32, bytes32[] memory, bytes32[] memory) { + uint256 treeHeight = 1; + NaiveMerkle tree = new NaiveMerkle(treeHeight); + tree.insertLeaf(daiWithdrawEntryKey); + tree.insertLeaf(swapEntryKey); + + bytes32 treeRoot = tree.computeRoot(); + (bytes32[] memory withdrawSiblingPath,) = tree.computeSiblingPath(0); + (bytes32[] memory swapSiblingPath,) = tree.computeSiblingPath(1); + + IOutbox outbox = registry.getOutbox(); + vm.prank(address(rollup)); + outbox.insert(_l2BlockNumber, treeRoot, treeHeight); - outbox.sendL1Messages(entryKeys); + return (treeRoot, withdrawSiblingPath, swapSiblingPath); } // Creates a withdraw transaction without a designated caller. // Should fail when uniswap portal tries to consume it since it tries using a designated caller. function testRevertIfWithdrawMessageHasNoDesignatedCaller() public { - bytes32 entryKey = _createDaiWithdrawMessage(address(uniswapPortal), address(0)); - _addMessagesToOutbox(entryKey, bytes32(uint256(0x1))); - bytes32 entryKeyPortalChecksAgainst = + uint256 l2BlockNumber = 69; + bytes32 l2ToL1MessageToInsert = _createDaiWithdrawMessage(address(uniswapPortal), address(0)); + (, bytes32[] memory withdrawSiblingPath, bytes32[] memory swapSiblingPath) = + _addMessagesToOutbox(l2ToL1MessageToInsert, bytes32(uint256(0x1)), l2BlockNumber); + bytes32 l2ToL1MessageToConsume = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); + + uint256 treeHeight = 1; + NaiveMerkle tree1 = new NaiveMerkle(treeHeight); + tree1.insertLeaf(l2ToL1MessageToInsert); + tree1.insertLeaf(bytes32(uint256(0x1))); + bytes32 actualRoot = tree1.computeRoot(); + + NaiveMerkle tree2 = new NaiveMerkle(treeHeight); + tree2.insertLeaf(l2ToL1MessageToConsume); + tree2.insertLeaf(bytes32(uint256(0x1))); + bytes32 consumedRoot = tree2.computeRoot(); + vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) + abi.encodeWithSelector( + Errors.MerkleLib__InvalidRoot.selector, actualRoot, consumedRoot, l2ToL1MessageToConsume, 0 + ) ); + + PortalDataStructures.OutboxMessageMetadata[2] memory outboxMessageMetadata = [ + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 0, + _path: withdrawSiblingPath + }), + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 1, + _path: swapSiblingPath + }) + ]; + uniswapPortal.swapPublic( address(daiTokenPortal), amount, @@ -172,23 +221,55 @@ contract UniswapPortalTest is Test { amountOutMinimum, aztecRecipient, secretHash, - true + true, + outboxMessageMetadata ); } // Inserts a wrong outbox message (where `_recipient` is not the uniswap portal). function testRevertIfExpectedOutboxMessageNotFound(address _recipient) public { vm.assume(_recipient != address(uniswapPortal)); + // malformed withdraw message (wrong recipient) - _addMessagesToOutbox( - _createDaiWithdrawMessage(_recipient, address(uniswapPortal)), bytes32(uint256(0x1)) - ); + uint256 l2BlockNumber = 69; + bytes32 l2ToL1MessageToInsert = _createDaiWithdrawMessage(_recipient, address(uniswapPortal)); - bytes32 entryKeyPortalChecksAgainst = + (, bytes32[] memory withdrawSiblingPath, bytes32[] memory swapSiblingPath) = + _addMessagesToOutbox(l2ToL1MessageToInsert, bytes32(uint256(0x1)), l2BlockNumber); + + bytes32 l2ToL1MessageToConsume = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); + + uint256 treeHeight = 1; + NaiveMerkle tree1 = new NaiveMerkle(treeHeight); + tree1.insertLeaf(l2ToL1MessageToInsert); + tree1.insertLeaf(bytes32(uint256(0x1))); + bytes32 actualRoot = tree1.computeRoot(); + + NaiveMerkle tree2 = new NaiveMerkle(treeHeight); + tree2.insertLeaf(l2ToL1MessageToConsume); + tree2.insertLeaf(bytes32(uint256(0x1))); + bytes32 consumedRoot = tree2.computeRoot(); + vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) + abi.encodeWithSelector( + Errors.MerkleLib__InvalidRoot.selector, actualRoot, consumedRoot, l2ToL1MessageToConsume, 0 + ) ); + + PortalDataStructures.OutboxMessageMetadata[2] memory outboxMessageMetadata = [ + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 0, + _path: withdrawSiblingPath + }), + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 1, + _path: swapSiblingPath + }) + ]; + uniswapPortal.swapPublic( address(daiTokenPortal), amount, @@ -197,22 +278,63 @@ contract UniswapPortalTest is Test { amountOutMinimum, aztecRecipient, secretHash, - true + true, + outboxMessageMetadata ); } function testRevertIfSwapParamsDifferentToOutboxMessage() public { + uint256 l2BlockNumber = 69; + bytes32 daiWithdrawEntryKey = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); bytes32 swapEntryKey = _createUniswapSwapMessagePublic(aztecRecipient, address(this)); - _addMessagesToOutbox(daiWithdrawEntryKey, swapEntryKey); + (, bytes32[] memory withdrawSiblingPath, bytes32[] memory swapSiblingPath) = + _addMessagesToOutbox(daiWithdrawEntryKey, swapEntryKey, l2BlockNumber); bytes32 newAztecRecipient = bytes32(uint256(0x4)); bytes32 entryKeyPortalChecksAgainst = _createUniswapSwapMessagePublic(newAztecRecipient, address(this)); + + bytes32 actualRoot; + bytes32 consumedRoot; + + { + uint256 treeHeight = 1; + NaiveMerkle tree1 = new NaiveMerkle(treeHeight); + tree1.insertLeaf(daiWithdrawEntryKey); + tree1.insertLeaf(swapEntryKey); + actualRoot = tree1.computeRoot(); + + NaiveMerkle tree2 = new NaiveMerkle(treeHeight); + tree2.insertLeaf(daiWithdrawEntryKey); + tree2.insertLeaf(entryKeyPortalChecksAgainst); + consumedRoot = tree2.computeRoot(); + } + vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) + abi.encodeWithSelector( + Errors.MerkleLib__InvalidRoot.selector, + actualRoot, + consumedRoot, + entryKeyPortalChecksAgainst, + 1 + ) ); + + PortalDataStructures.OutboxMessageMetadata[2] memory outboxMessageMetadata = [ + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 0, + _path: withdrawSiblingPath + }), + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 1, + _path: swapSiblingPath + }) + ]; + uniswapPortal.swapPublic( address(daiTokenPortal), amount, @@ -221,15 +343,33 @@ contract UniswapPortalTest is Test { amountOutMinimum, newAztecRecipient, // change recipient of swapped token to some other address secretHash, - true + true, + outboxMessageMetadata ); } function testSwapWithDesignatedCaller() public { + uint256 l2BlockNumber = 69; + bytes32 daiWithdrawEntryKey = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); bytes32 swapEntryKey = _createUniswapSwapMessagePublic(aztecRecipient, address(this)); - _addMessagesToOutbox(daiWithdrawEntryKey, swapEntryKey); + + (, bytes32[] memory withdrawSiblingPath, bytes32[] memory swapSiblingPath) = + _addMessagesToOutbox(daiWithdrawEntryKey, swapEntryKey, l2BlockNumber); + + PortalDataStructures.OutboxMessageMetadata[2] memory outboxMessageMetadata = [ + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 0, + _path: withdrawSiblingPath + }), + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 1, + _path: swapSiblingPath + }) + ]; uniswapPortal.swapPublic( address(daiTokenPortal), @@ -239,25 +379,44 @@ contract UniswapPortalTest is Test { amountOutMinimum, aztecRecipient, secretHash, - true + true, + outboxMessageMetadata ); // dai should be taken away from dai portal assertEq(DAI.balanceOf(address(daiTokenPortal)), 0); // there should be some weth in the weth portal assertGt(WETH9.balanceOf(address(wethTokenPortal)), 0); - // there should be no message in the outbox: - assertFalse(outbox.contains(daiWithdrawEntryKey)); - assertFalse(outbox.contains(swapEntryKey)); + // there the message should be nullified at index 0 and 1 + IOutbox outbox = registry.getOutbox(); + assertTrue(outbox.hasMessageBeenConsumedAtBlockAndIndex(l2BlockNumber, 0)); + assertTrue(outbox.hasMessageBeenConsumedAtBlockAndIndex(l2BlockNumber, 1)); } function testSwapCalledByAnyoneIfDesignatedCallerNotSet(address _caller) public { vm.assume(_caller != address(uniswapPortal)); + uint256 l2BlockNumber = 69; + bytes32 daiWithdrawEntryKey = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); // don't set caller on swapPublic() -> so anyone can call this method. bytes32 swapEntryKey = _createUniswapSwapMessagePublic(aztecRecipient, address(0)); - _addMessagesToOutbox(daiWithdrawEntryKey, swapEntryKey); + + (, bytes32[] memory withdrawSiblingPath, bytes32[] memory swapSiblingPath) = + _addMessagesToOutbox(daiWithdrawEntryKey, swapEntryKey, l2BlockNumber); + + PortalDataStructures.OutboxMessageMetadata[2] memory outboxMessageMetadata = [ + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 0, + _path: withdrawSiblingPath + }), + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 1, + _path: swapSiblingPath + }) + ]; vm.prank(_caller); uniswapPortal.swapPublic( @@ -268,7 +427,8 @@ contract UniswapPortalTest is Test { amountOutMinimum, aztecRecipient, secretHash, - false + false, + outboxMessageMetadata ); // check that swap happened: // dai should be taken away from dai portal @@ -276,22 +436,64 @@ contract UniswapPortalTest is Test { // there should be some weth in the weth portal assertGt(WETH9.balanceOf(address(wethTokenPortal)), 0); // there should be no message in the outbox: - assertFalse(outbox.contains(daiWithdrawEntryKey)); - assertFalse(outbox.contains(swapEntryKey)); + IOutbox outbox = registry.getOutbox(); + assertTrue(outbox.hasMessageBeenConsumedAtBlockAndIndex(l2BlockNumber, 0)); + assertTrue(outbox.hasMessageBeenConsumedAtBlockAndIndex(l2BlockNumber, 1)); } function testRevertIfSwapWithDesignatedCallerCalledByWrongCaller(address _caller) public { vm.assume(_caller != address(this)); + uint256 l2BlockNumber = 69; + bytes32 daiWithdrawEntryKey = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); bytes32 swapEntryKey = _createUniswapSwapMessagePublic(aztecRecipient, address(this)); - _addMessagesToOutbox(daiWithdrawEntryKey, swapEntryKey); + + (, bytes32[] memory withdrawSiblingPath, bytes32[] memory swapSiblingPath) = + _addMessagesToOutbox(daiWithdrawEntryKey, swapEntryKey, l2BlockNumber); + + PortalDataStructures.OutboxMessageMetadata[2] memory outboxMessageMetadata = [ + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 0, + _path: withdrawSiblingPath + }), + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 1, + _path: swapSiblingPath + }) + ]; vm.startPrank(_caller); bytes32 entryKeyPortalChecksAgainst = _createUniswapSwapMessagePublic(aztecRecipient, _caller); + + bytes32 actualRoot; + bytes32 consumedRoot; + + { + uint256 treeHeight = 1; + NaiveMerkle tree1 = new NaiveMerkle(treeHeight); + tree1.insertLeaf(daiWithdrawEntryKey); + tree1.insertLeaf(swapEntryKey); + actualRoot = tree1.computeRoot(); + + NaiveMerkle tree2 = new NaiveMerkle(treeHeight); + tree2.insertLeaf(daiWithdrawEntryKey); + tree2.insertLeaf(entryKeyPortalChecksAgainst); + consumedRoot = tree2.computeRoot(); + } + vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) + abi.encodeWithSelector( + Errors.MerkleLib__InvalidRoot.selector, + actualRoot, + consumedRoot, + entryKeyPortalChecksAgainst, + 1 + ) ); + uniswapPortal.swapPublic( address(daiTokenPortal), amount, @@ -300,12 +502,32 @@ contract UniswapPortalTest is Test { amountOutMinimum, aztecRecipient, secretHash, - true + true, + outboxMessageMetadata ); entryKeyPortalChecksAgainst = _createUniswapSwapMessagePublic(aztecRecipient, address(0)); + + { + uint256 treeHeight = 1; + NaiveMerkle tree1 = new NaiveMerkle(treeHeight); + tree1.insertLeaf(daiWithdrawEntryKey); + tree1.insertLeaf(swapEntryKey); + actualRoot = tree1.computeRoot(); + + NaiveMerkle tree2 = new NaiveMerkle(treeHeight); + tree2.insertLeaf(daiWithdrawEntryKey); + tree2.insertLeaf(entryKeyPortalChecksAgainst); + consumedRoot = tree2.computeRoot(); + } vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) + abi.encodeWithSelector( + Errors.MerkleLib__InvalidRoot.selector, + actualRoot, + consumedRoot, + entryKeyPortalChecksAgainst, + 1 + ) ); uniswapPortal.swapPublic( address(daiTokenPortal), @@ -315,23 +537,63 @@ contract UniswapPortalTest is Test { amountOutMinimum, aztecRecipient, secretHash, - false + false, + outboxMessageMetadata ); vm.stopPrank(); } function testRevertIfSwapMessageWasForDifferentPublicOrPrivateFlow() public { + uint256 l2BlockNumber = 69; + bytes32 daiWithdrawEntryKey = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); // Create message for `_isPrivateFlow`: bytes32 swapEntryKey = _createUniswapSwapMessagePublic(aztecRecipient, address(this)); - _addMessagesToOutbox(daiWithdrawEntryKey, swapEntryKey); + (, bytes32[] memory withdrawSiblingPath, bytes32[] memory swapSiblingPath) = + _addMessagesToOutbox(daiWithdrawEntryKey, swapEntryKey, l2BlockNumber); + + PortalDataStructures.OutboxMessageMetadata[2] memory outboxMessageMetadata = [ + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 0, + _path: withdrawSiblingPath + }), + PortalDataStructures.OutboxMessageMetadata({ + _l2BlockNumber: l2BlockNumber, + _leafIndex: 1, + _path: swapSiblingPath + }) + ]; bytes32 entryKeyPortalChecksAgainst = _createUniswapSwapMessagePrivate(secretHashForRedeemingMintedNotes, address(this)); + + bytes32 actualRoot; + bytes32 consumedRoot; + + { + uint256 treeHeight = 1; + NaiveMerkle tree1 = new NaiveMerkle(treeHeight); + tree1.insertLeaf(daiWithdrawEntryKey); + tree1.insertLeaf(swapEntryKey); + actualRoot = tree1.computeRoot(); + + NaiveMerkle tree2 = new NaiveMerkle(treeHeight); + tree2.insertLeaf(daiWithdrawEntryKey); + tree2.insertLeaf(entryKeyPortalChecksAgainst); + consumedRoot = tree2.computeRoot(); + } + vm.expectRevert( - abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) + abi.encodeWithSelector( + Errors.MerkleLib__InvalidRoot.selector, + actualRoot, + consumedRoot, + entryKeyPortalChecksAgainst, + 1 + ) ); uniswapPortal.swapPrivate( @@ -342,7 +604,8 @@ contract UniswapPortalTest is Test { amountOutMinimum, secretHashForRedeemingMintedNotes, secretHash, - true + true, + outboxMessageMetadata ); } // TODO(#887) - what if uniswap fails? diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index d1784a4c0e0..34ce2da58d0 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -1,6 +1,7 @@ import { AccountWallet, AztecAddress, + AztecNode, DebugLogger, EthAddress, Fr, @@ -19,6 +20,7 @@ import { setup } from './fixtures/utils.js'; import { CrossChainTestHarness } from './shared/cross_chain_test_harness.js'; describe('e2e_cross_chain_messaging', () => { + let aztecNode: AztecNode; let logger: DebugLogger; let teardown: () => Promise; @@ -30,13 +32,19 @@ describe('e2e_cross_chain_messaging', () => { let crossChainTestHarness: CrossChainTestHarness; let l2Token: TokenContract; let l2Bridge: TokenBridgeContract; - let outbox: any; beforeEach(async () => { - const { aztecNode, pxe, deployL1ContractsValues, wallets, logger: logger_, teardown: teardown_ } = await setup(2); + const { + aztecNode: aztecNode_, + pxe, + deployL1ContractsValues, + wallets, + logger: logger_, + teardown: teardown_, + } = await setup(2); crossChainTestHarness = await CrossChainTestHarness.new( - aztecNode, + aztecNode_, pxe, deployL1ContractsValues.publicClient, deployL1ContractsValues.walletClient, @@ -48,10 +56,10 @@ describe('e2e_cross_chain_messaging', () => { l2Bridge = crossChainTestHarness.l2Bridge; ethAccount = crossChainTestHarness.ethAccount; ownerAddress = crossChainTestHarness.ownerAddress; - outbox = crossChainTestHarness.outbox; user1Wallet = wallets[0]; user2Wallet = wallets[1]; logger = logger_; + aztecNode = aztecNode_; teardown = teardown_; logger('Successfully deployed contracts and initialized portal'); }, 100_000); @@ -107,16 +115,24 @@ describe('e2e_cross_chain_messaging', () => { // docs:end:authwit_to_another_sc // 5. Withdraw owner's funds from L2 to L1 - const entryKey = await crossChainTestHarness.checkEntryIsNotInOutbox(withdrawAmount); - await crossChainTestHarness.withdrawPrivateFromAztecToL1(withdrawAmount, nonce); + const l2ToL1Message = crossChainTestHarness.getL2ToL1MessageLeaf(withdrawAmount); + const l2TxReceipt = await crossChainTestHarness.withdrawPrivateFromAztecToL1(withdrawAmount, nonce); await crossChainTestHarness.expectPrivateBalanceOnL2(ownerAddress, bridgeAmount - withdrawAmount); + const [l2ToL1MessageIndex, siblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( + l2TxReceipt.blockNumber!, + l2ToL1Message, + ); + // Check balance before and after exit. expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); - await crossChainTestHarness.withdrawFundsFromBridgeOnL1(withdrawAmount, entryKey); + await crossChainTestHarness.withdrawFundsFromBridgeOnL1( + withdrawAmount, + l2TxReceipt.blockNumber!, + l2ToL1MessageIndex, + siblingPath, + ); expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); - - expect(await outbox.read.contains([entryKey.toString()])).toBeFalsy(); }, 120_000); // docs:end:e2e_private_cross_chain diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index f11215642d8..34bc3259b4b 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -21,7 +21,7 @@ import { TestContract } from '@aztec/noir-contracts.js'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { TokenBridgeContract } from '@aztec/noir-contracts.js/TokenBridge'; -import { Hex } from 'viem'; +import { Chain, GetContractReturnType, Hex, HttpTransport, PublicClient } from 'viem'; import { decodeEventLog, toFunctionSelector } from 'viem/utils'; import { publicDeployAccounts, setup } from './fixtures/utils.js'; @@ -44,8 +44,8 @@ describe('e2e_public_cross_chain_messaging', () => { let crossChainTestHarness: CrossChainTestHarness; let l2Token: TokenContract; let l2Bridge: TokenBridgeContract; - let inbox: any; - let outbox: any; + let inbox: GetContractReturnType>; + let outbox: GetContractReturnType>; beforeAll(async () => { ({ aztecNode, pxe, deployL1ContractsValues, wallets, accounts, logger, teardown } = await setup(2)); @@ -113,16 +113,25 @@ describe('e2e_public_cross_chain_messaging', () => { await user1Wallet.setPublicAuthWit(burnMessageHash, true).send().wait(); // 5. Withdraw owner's funds from L2 to L1 - const entryKey = await crossChainTestHarness.checkEntryIsNotInOutbox(withdrawAmount); - await crossChainTestHarness.withdrawPublicFromAztecToL1(withdrawAmount, nonce); + const l2ToL1Message = crossChainTestHarness.getL2ToL1MessageLeaf(withdrawAmount); + const l2TxReceipt = await crossChainTestHarness.withdrawPublicFromAztecToL1(withdrawAmount, nonce); await crossChainTestHarness.expectPublicBalanceOnL2(ownerAddress, afterBalance - withdrawAmount); // Check balance before and after exit. expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount); - await crossChainTestHarness.withdrawFundsFromBridgeOnL1(withdrawAmount, entryKey); - expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); - expect(await outbox.read.contains([entryKey.toString()])).toBeFalsy(); + const [l2ToL1MessageIndex, siblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( + l2TxReceipt.blockNumber!, + l2ToL1Message, + ); + + await crossChainTestHarness.withdrawFundsFromBridgeOnL1( + withdrawAmount, + l2TxReceipt.blockNumber!, + l2ToL1MessageIndex, + siblingPath, + ); + expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); }, 120_000); // docs:end:e2e_public_cross_chain @@ -225,14 +234,19 @@ describe('e2e_public_cross_chain_messaging', () => { const content = Fr.random(); const recipient = crossChainTestHarness.ethAccount; + let l2TxReceipt; + // We create the L2 -> L1 message using the test contract if (isPrivate) { - await testContract.methods + l2TxReceipt = await testContract.methods .create_l2_to_l1_message_arbitrary_recipient_private(content, recipient) .send() .wait(); } else { - await testContract.methods.create_l2_to_l1_message_arbitrary_recipient_public(content, recipient).send().wait(); + l2TxReceipt = await testContract.methods + .create_l2_to_l1_message_arbitrary_recipient_public(content, recipient) + .send() + .wait(); } const l2ToL1Message = { @@ -244,7 +258,33 @@ describe('e2e_public_cross_chain_messaging', () => { content: content.toString() as Hex, }; - const txHash = await outbox.write.consume([l2ToL1Message] as const, {} as any); + const leaf = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + testContract.address.toBuffer(), + new Fr(1).toBuffer(), // aztec version + recipient.toBuffer32(), + new Fr(crossChainTestHarness.publicClient.chain.id).toBuffer(), // chain id + content.toBuffer(), + ]), + ), + ); + + const [l2MessageIndex, siblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( + l2TxReceipt.blockNumber!, + leaf, + ); + + const txHash = await outbox.write.consume( + [ + l2ToL1Message, + BigInt(l2TxReceipt.blockNumber!), + BigInt(l2MessageIndex), + siblingPath.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + ], + {} as any, + ); + const txReceipt = await crossChainTestHarness.publicClient.waitForTransactionReceipt({ hash: txHash, }); @@ -258,12 +298,19 @@ describe('e2e_public_cross_chain_messaging', () => { abi: OutboxAbi, data: txLog.data, topics: txLog.topics, - }); + }) as { + eventName: 'MessageConsumed'; + args: { + l2BlockNumber: bigint; + root: `0x${string}`; + messageHash: `0x${string}`; + leafIndex: bigint; + }; + }; - // We check that MessageConsumed event was emitted with the expected recipient - // Note: For whatever reason, viem types "think" that there is no recipient on topics.args. I hack around this - // by casting the args to "any" - expect((topics.args as any).recipient).toBe(recipient.toChecksumString()); + // We check that MessageConsumed event was emitted with the expected message hash and leaf index + expect(topics.args.messageHash).toStrictEqual(leaf.toString()); + expect(topics.args.leafIndex).toStrictEqual(BigInt(0)); }, 60_000, ); @@ -284,11 +331,14 @@ describe('e2e_public_cross_chain_messaging', () => { ); // We inject the message to Inbox - const txHash = await inbox.write.sendL2Message([ - { actor: message.recipient.recipient.toString() as Hex, version: 1n }, - message.content.toString() as Hex, - message.secretHash.toString() as Hex, - ] as const); + const txHash = await inbox.write.sendL2Message( + [ + { actor: message.recipient.recipient.toString() as Hex, version: 1n }, + message.content.toString() as Hex, + message.secretHash.toString() as Hex, + ] as const, + {} as any, + ); // We check that the message was correctly injected by checking the emitted event const msgLeaf = message.hash(); diff --git a/yarn-project/end-to-end/src/integration_l1_publisher.test.ts b/yarn-project/end-to-end/src/integration_l1_publisher.test.ts index b98e3eb2759..45fa9176006 100644 --- a/yarn-project/end-to-end/src/integration_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/integration_l1_publisher.test.ts @@ -24,10 +24,11 @@ import { SideEffectLinkedToNoteHash, } from '@aztec/circuits.js'; import { fr, makeNewSideEffect, makeNewSideEffectLinkedToNoteHash, makeProof } from '@aztec/circuits.js/testing'; -import { createEthereumChain } from '@aztec/ethereum'; +import { L1ContractAddresses, createEthereumChain } from '@aztec/ethereum'; import { makeTuple, range } from '@aztec/foundation/array'; import { openTmpStore } from '@aztec/kv-store/utils'; import { AvailabilityOracleAbi, InboxAbi, OutboxAbi, RollupAbi } from '@aztec/l1-artifacts'; +import { SHA256, StandardTree } from '@aztec/merkle-tree'; import { EmptyRollupProver, L1Publisher, @@ -44,6 +45,7 @@ import { MerkleTreeOperations, MerkleTrees } from '@aztec/world-state'; import { beforeEach, describe, expect, it } from '@jest/globals'; import * as fs from 'fs'; import { + Account, Address, Chain, GetContractReturnType, @@ -72,6 +74,8 @@ const numberOfConsecutiveBlocks = 2; describe('L1Publisher integration', () => { let publicClient: PublicClient; + let walletClient: WalletClient; + let l1ContractAddresses: L1ContractAddresses; let deployerAccount: PrivateKeyAccount; let rollupAddress: Address; @@ -79,7 +83,7 @@ describe('L1Publisher integration', () => { let outboxAddress: Address; let rollup: GetContractReturnType>; - let inbox: GetContractReturnType>; + let inbox: GetContractReturnType>; let outbox: GetContractReturnType>; let publisher: L1Publisher; @@ -101,12 +105,11 @@ describe('L1Publisher integration', () => { beforeEach(async () => { deployerAccount = privateKeyToAccount(deployerPK); - const { - l1ContractAddresses, - walletClient, - publicClient: publicClient_, - } = await setupL1Contracts(config.rpcUrl, deployerAccount, logger); - publicClient = publicClient_; + ({ l1ContractAddresses, publicClient, walletClient } = await setupL1Contracts( + config.rpcUrl, + deployerAccount, + logger, + )); rollupAddress = getAddress(l1ContractAddresses.rollupAddress.toString()); inboxAddress = getAddress(l1ContractAddresses.inboxAddress.toString()); @@ -123,7 +126,6 @@ describe('L1Publisher integration', () => { abi: InboxAbi, client: walletClient, }); - outbox = getContract({ address: outboxAddress, abi: OutboxAbi, @@ -372,10 +374,10 @@ describe('L1Publisher integration', () => { const newL2ToL1MsgsArray = block.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs); - // check that values are not in the outbox - for (let j = 0; j < newL2ToL1MsgsArray.length; j++) { - expect(await outbox.read.contains([newL2ToL1MsgsArray[j].toString()])).toBeFalsy(); - } + const [emptyRoot] = await outbox.read.roots([block.header.globalVariables.blockNumber.toBigInt()]); + + // Check that we have not yet written a root to this blocknumber + expect(BigInt(emptyRoot)).toStrictEqual(0n); writeJson(`mixed_block_${i}`, block, l1ToL2Content, recipientAddress, deployerAccount.address); @@ -413,10 +415,16 @@ describe('L1Publisher integration', () => { expect(newToConsume).toEqual(toConsume + 1n); toConsume = newToConsume; + const treeHeight = Math.ceil(Math.log2(newL2ToL1MsgsArray.length)); + + const tree = new StandardTree(openTmpStore(true), new SHA256(), 'temp_outhash_sibling_path', treeHeight); + await tree.appendLeaves(newL2ToL1MsgsArray.map(l2ToL1Msg => l2ToL1Msg.toBuffer())); + + const expectedRoot = tree.getRoot(true); + const [actualRoot] = await outbox.read.roots([block.header.globalVariables.blockNumber.toBigInt()]); + // check that values are inserted into the outbox - for (let j = 0; j < newL2ToL1MsgsArray.length; j++) { - expect(await outbox.read.contains([newL2ToL1MsgsArray[j].toString()])).toBeTruthy(); - } + expect(`0x${expectedRoot.toString('hex')}`).toEqual(actualRoot); // There is a 1 block lag between before messages get consumed from the inbox currentL1ToL2Messages = nextL1ToL2Messages; diff --git a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts index 70132899c72..12234db4b09 100644 --- a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts @@ -5,10 +5,13 @@ import { DebugLogger, EthAddress, ExtendedNote, + FieldsOf, Fr, Note, PXE, + SiblingPath, TxHash, + TxReceipt, Wallet, computeMessageSecretHash, deployL1Contract, @@ -26,7 +29,16 @@ import { import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { TokenBridgeContract } from '@aztec/noir-contracts.js/TokenBridge'; -import { Account, Chain, HttpTransport, PublicClient, WalletClient, getContract, toFunctionSelector } from 'viem'; +import { + Account, + Chain, + GetContractReturnType, + HttpTransport, + PublicClient, + WalletClient, + getContract, + toFunctionSelector, +} from 'viem'; // docs:start:deployAndInitializeTokenAndBridgeContracts /** @@ -126,7 +138,7 @@ export class CrossChainTestHarness { aztecNode: AztecNode, pxeService: PXE, publicClient: PublicClient, - walletClient: any, + walletClient: WalletClient, wallet: Wallet, logger: DebugLogger, underlyingERC20Address?: EthAddress, @@ -201,9 +213,9 @@ export class CrossChainTestHarness { /** Underlying token for portal tests. */ public underlyingERC20: any, /** Message Bridge Inbox. */ - public inbox: any, + public inbox: GetContractReturnType>, /** Message Bridge Outbox. */ - public outbox: any, + public outbox: GetContractReturnType>, /** Viem Public client instance. */ public publicClient: PublicClient, /** Viem Wallet Client instance. */ @@ -313,18 +325,22 @@ export class CrossChainTestHarness { await this.l2Bridge.methods.claim_public(this.ownerAddress, bridgeAmount, secret).send().wait(); } - async withdrawPrivateFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO) { - await this.l2Bridge.methods + async withdrawPrivateFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO): Promise> { + const withdrawReceipt = await this.l2Bridge.methods .exit_to_l1_private(this.l2Token.address, this.ethAccount, withdrawAmount, EthAddress.ZERO, nonce) .send() .wait(); + + return withdrawReceipt; } - async withdrawPublicFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO) { - await this.l2Bridge.methods + async withdrawPublicFromAztecToL1(withdrawAmount: bigint, nonce: Fr = Fr.ZERO): Promise> { + const withdrawReceipt = await this.l2Bridge.methods .exit_to_l1_public(this.ethAccount, withdrawAmount, EthAddress.ZERO, nonce) .send() .wait(); + + return withdrawReceipt; } async getL2PrivateBalanceOf(owner: AztecAddress) { @@ -346,9 +362,7 @@ export class CrossChainTestHarness { expect(balance).toBe(expectedBalance); } - async checkEntryIsNotInOutbox(withdrawAmount: bigint, callerOnL1: EthAddress = EthAddress.ZERO): Promise { - this.logger('Ensure that the entry is not in outbox yet'); - + getL2ToL1MessageLeaf(withdrawAmount: bigint, callerOnL1: EthAddress = EthAddress.ZERO): Fr { const content = Fr.fromBufferReduce( sha256( Buffer.concat([ @@ -359,7 +373,7 @@ export class CrossChainTestHarness { ]), ), ); - const entryKey = Fr.fromBufferReduce( + const leaf = Fr.fromBufferReduce( sha256( Buffer.concat([ this.l2Bridge.address.toBuffer(), @@ -370,25 +384,39 @@ export class CrossChainTestHarness { ]), ), ); - expect(await this.outbox.read.contains([entryKey.toString()])).toBeFalsy(); - return entryKey; + return leaf; } - async withdrawFundsFromBridgeOnL1(withdrawAmount: bigint, entryKey: Fr) { + async withdrawFundsFromBridgeOnL1( + withdrawAmount: bigint, + blockNumber: number, + messageIndex: number, + siblingPath: SiblingPath, + ) { this.logger('Send L1 tx to consume entry and withdraw funds'); // Call function on L1 contract to consume the message - const { request: withdrawRequest, result: withdrawEntryKey } = await this.tokenPortal.simulate.withdraw([ + const { request: withdrawRequest } = await this.tokenPortal.simulate.withdraw([ this.ethAccount.toString(), withdrawAmount, false, + BigInt(blockNumber), + BigInt(messageIndex), + siblingPath.toBufferArray().map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], ]); - expect(withdrawEntryKey).toBe(entryKey.toString()); - expect(await this.outbox.read.contains([withdrawEntryKey])).toBeTruthy(); + expect( + await this.outbox.read.hasMessageBeenConsumedAtBlockAndIndex([BigInt(blockNumber), BigInt(messageIndex)], {}), + ).toBe(false); await this.walletClient.writeContract(withdrawRequest); - return withdrawEntryKey; + await expect(async () => { + await this.walletClient.writeContract(withdrawRequest); + }).rejects.toThrow(); + + expect( + await this.outbox.read.hasMessageBeenConsumedAtBlockAndIndex([BigInt(blockNumber), BigInt(messageIndex)], {}), + ).toBe(true); } async shieldFundsOnL2(shieldAmount: bigint, secretHash: Fr) { diff --git a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts index 8b4f0c1f35d..585c5626856 100644 --- a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts @@ -3,7 +3,7 @@ import { GasPortalAbi, OutboxAbi, PortalERC20Abi } from '@aztec/l1-artifacts'; import { GasTokenContract } from '@aztec/noir-contracts.js'; import { getCanonicalGasToken, getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token'; -import { Account, Chain, HttpTransport, PublicClient, WalletClient, getContract } from 'viem'; +import { Account, Chain, GetContractReturnType, HttpTransport, PublicClient, WalletClient, getContract } from 'viem'; export interface IGasBridgingTestHarness { bridgeFromL1ToL2(l1TokenBalance: bigint, bridgeAmount: bigint, owner: AztecAddress): Promise; @@ -120,7 +120,7 @@ class GasBridgingTestHarness implements IGasBridgingTestHarness { /** Underlying token for portal tests. */ public underlyingERC20: any, /** Message Bridge Outbox. */ - public outbox: any, + public outbox: GetContractReturnType>, /** Viem Public client instance. */ public publicClient: PublicClient, /** Viem Wallet Client instance. */ diff --git a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts index d0bc6472b9b..41727806769 100644 --- a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts +++ b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts @@ -9,11 +9,23 @@ import { computeAuthWitMessageHash, } from '@aztec/aztec.js'; import { deployL1Contract } from '@aztec/ethereum'; +import { sha256 } from '@aztec/foundation/crypto'; import { InboxAbi, UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; import { UniswapContract } from '@aztec/noir-contracts.js/Uniswap'; import { jest } from '@jest/globals'; -import { Chain, HttpTransport, PublicClient, decodeEventLog, getContract, parseEther } from 'viem'; +import { + Account, + Chain, + GetContractReturnType, + HttpTransport, + PublicClient, + WalletClient, + decodeEventLog, + getContract, + parseEther, + toFunctionSelector, +} from 'viem'; import { publicDeployAccounts } from '../fixtures/utils.js'; import { CrossChainTestHarness } from './cross_chain_test_harness.js'; @@ -39,7 +51,7 @@ export type UniswapSetupContext = { /** Viem Public client instance. */ publicClient: PublicClient; /** Viem Wallet Client instance. */ - walletClient: any; + walletClient: WalletClient; /** The owner wallet. */ ownerWallet: AccountWallet; /** The sponsor wallet. */ @@ -63,7 +75,8 @@ export const uniswapL1L2TestSuite = ( let pxe: PXE; let logger: DebugLogger; - let walletClient: any; + let walletClient: WalletClient; + let publicClient: PublicClient; let ownerWallet: AccountWallet; let ownerAddress: AztecAddress; @@ -75,7 +88,7 @@ export const uniswapL1L2TestSuite = ( let daiCrossChainHarness: CrossChainTestHarness; let wethCrossChainHarness: CrossChainTestHarness; - let uniswapPortal: any; + let uniswapPortal: GetContractReturnType>; let uniswapPortalAddress: EthAddress; let uniswapL2Contract: UniswapContract; @@ -84,12 +97,8 @@ export const uniswapL1L2TestSuite = ( const minimumOutputAmount = 0n; beforeAll(async () => { - let publicClient: PublicClient; ({ aztecNode, pxe, logger, publicClient, walletClient, ownerWallet, sponsorWallet } = await setup()); - // walletClient = deployL1ContractsValues.walletClient; - // const publicClient = deployL1ContractsValues.publicClient; - if (Number(await publicClient.getBlockNumber()) < expectedForkBlockNumber) { throw new Error('This test must be run on a fork of mainnet with the expected fork block'); } @@ -129,6 +138,7 @@ export const uniswapL1L2TestSuite = ( UniswapPortalAbi, UniswapPortalBytecode, ); + uniswapPortal = getContract({ address: uniswapPortalAddress.toString(), abi: UniswapPortalAbi, @@ -140,6 +150,7 @@ export const uniswapL1L2TestSuite = ( .deployed(); const registryAddress = (await pxe.getNodeInfo()).l1ContractAddresses.registryAddress; + await uniswapPortal.write.initialize( [registryAddress.toString(), uniswapL2Contract.address.toString()], {} as any, @@ -219,7 +230,7 @@ export const uniswapL1L2TestSuite = ( daiCrossChainHarness.generateClaimSecret(); const [secretForRedeemingDai, secretHashForRedeemingDai] = daiCrossChainHarness.generateClaimSecret(); - await uniswapL2Contract.methods + const l2UniswapInteractionReceipt = await uniswapL2Contract.methods .swap_private( wethCrossChainHarness.l2Token.address, wethCrossChainHarness.l2Bridge.address, @@ -234,6 +245,63 @@ export const uniswapL1L2TestSuite = ( ) .send() .wait(); + + const swapPrivateContent = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + Buffer.from( + toFunctionSelector( + 'swap_private(address,uint256,uint24,address,uint256,bytes32,bytes32,address)', + ).substring(2), + 'hex', + ), + wethCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(wethAmountToBridge).toBuffer(), + new Fr(uniswapFeeTier).toBuffer(), + daiCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(minimumOutputAmount).toBuffer(), + secretHashForRedeemingDai.toBuffer(), + secretHashForDepositingSwappedDai.toBuffer(), + ownerEthAddress.toBuffer32(), + ]), + ), + ); + + const swapPrivateLeaf = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + uniswapL2Contract.address.toBuffer(), + new Fr(1).toBuffer(), // aztec version + EthAddress.fromString(uniswapPortal.address).toBuffer32(), + new Fr(publicClient.chain.id).toBuffer(), // chain id + swapPrivateContent.toBuffer(), + ]), + ), + ); + + const withdrawContent = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + Buffer.from(toFunctionSelector('withdraw(address,uint256,address)').substring(2), 'hex'), + uniswapPortalAddress.toBuffer32(), + new Fr(wethAmountToBridge).toBuffer(), + uniswapPortalAddress.toBuffer32(), + ]), + ), + ); + + const withdrawLeaf = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + wethCrossChainHarness.l2Bridge.address.toBuffer(), + new Fr(1).toBuffer(), // aztec version + wethCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(publicClient.chain.id).toBuffer(), // chain id + withdrawContent.toBuffer(), + ]), + ), + ); + // ensure that user's funds were burnt await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); // ensure that uniswap contract didn't eat the funds. @@ -247,15 +315,42 @@ export const uniswapL1L2TestSuite = ( const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( daiCrossChainHarness.tokenPortalAddress, ); + + const [swapPrivateL2MessageIndex, swapPrivateSiblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( + l2UniswapInteractionReceipt.blockNumber!, + swapPrivateLeaf, + ); + const [withdrawL2MessageIndex, withdrawSiblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( + l2UniswapInteractionReceipt.blockNumber!, + withdrawLeaf, + ); + + const withdrawMessageMetadata = { + _l2BlockNumber: BigInt(l2UniswapInteractionReceipt.blockNumber!), + _leafIndex: BigInt(withdrawL2MessageIndex), + _path: withdrawSiblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + }; + + const swapPrivateMessageMetadata = { + _l2BlockNumber: BigInt(l2UniswapInteractionReceipt.blockNumber!), + _leafIndex: BigInt(swapPrivateL2MessageIndex), + _path: swapPrivateSiblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + }; + const swapArgs = [ wethCrossChainHarness.tokenPortalAddress.toString(), wethAmountToBridge, - uniswapFeeTier, + Number(uniswapFeeTier), daiCrossChainHarness.tokenPortalAddress.toString(), minimumOutputAmount, secretHashForRedeemingDai.toString(), secretHashForDepositingSwappedDai.toString(), true, + [withdrawMessageMetadata, swapPrivateMessageMetadata], ] as const; // this should also insert a message into the inbox. @@ -378,7 +473,63 @@ export const uniswapL1L2TestSuite = ( await ownerWallet.setPublicAuthWit(swapMessageHash, true).send().wait(); // 4.2 Call swap_public from user2 on behalf of owner - await action.send().wait(); + const uniswapL2Interaction = await action.send().wait(); + + const swapPublicContent = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + Buffer.from( + toFunctionSelector( + 'swap_public(address,uint256,uint24,address,uint256,bytes32,bytes32,address)', + ).substring(2), + 'hex', + ), + wethCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(wethAmountToBridge).toBuffer(), + new Fr(uniswapFeeTier).toBuffer(), + daiCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(minimumOutputAmount).toBuffer(), + ownerAddress.toBuffer(), + secretHashForDepositingSwappedDai.toBuffer(), + ownerEthAddress.toBuffer32(), + ]), + ), + ); + + const swapPublicLeaf = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + uniswapL2Contract.address.toBuffer(), + new Fr(1).toBuffer(), // aztec version + EthAddress.fromString(uniswapPortal.address).toBuffer32(), + new Fr(publicClient.chain.id).toBuffer(), // chain id + swapPublicContent.toBuffer(), + ]), + ), + ); + + const withdrawContent = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + Buffer.from(toFunctionSelector('withdraw(address,uint256,address)').substring(2), 'hex'), + uniswapPortalAddress.toBuffer32(), + new Fr(wethAmountToBridge).toBuffer(), + uniswapPortalAddress.toBuffer32(), + ]), + ), + ); + + const withdrawLeaf = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + wethCrossChainHarness.l2Bridge.address.toBuffer(), + new Fr(1).toBuffer(), // aztec version + wethCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(publicClient.chain.id).toBuffer(), // chain id + withdrawContent.toBuffer(), + ]), + ), + ); // check weth balance of owner on L2 (we first bridged `wethAmountToBridge` into L2 and now withdrew it!) await wethCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); @@ -392,15 +543,42 @@ export const uniswapL1L2TestSuite = ( const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( daiCrossChainHarness.tokenPortalAddress, ); + + const [swapPrivateL2MessageIndex, swapPrivateSiblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( + uniswapL2Interaction.blockNumber!, + swapPublicLeaf, + ); + const [withdrawL2MessageIndex, withdrawSiblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( + uniswapL2Interaction.blockNumber!, + withdrawLeaf, + ); + + const withdrawMessageMetadata = { + _l2BlockNumber: BigInt(uniswapL2Interaction.blockNumber!), + _leafIndex: BigInt(withdrawL2MessageIndex), + _path: withdrawSiblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + }; + + const swapPrivateMessageMetadata = { + _l2BlockNumber: BigInt(uniswapL2Interaction.blockNumber!), + _leafIndex: BigInt(swapPrivateL2MessageIndex), + _path: swapPrivateSiblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + }; + const swapArgs = [ wethCrossChainHarness.tokenPortalAddress.toString(), wethAmountToBridge, - uniswapFeeTier, + Number(uniswapFeeTier), daiCrossChainHarness.tokenPortalAddress.toString(), minimumOutputAmount, ownerAddress.toString(), secretHashForDepositingSwappedDai.toString(), true, + [withdrawMessageMetadata, swapPrivateMessageMetadata], ] as const; // this should also insert a message into the inbox. @@ -645,9 +823,10 @@ export const uniswapL1L2TestSuite = ( // Swap logger('Withdrawing weth to L1 and sending message to swap to dai'); - const secretHashForDepositingSwappedDai = Fr.random(); + const [, secretHashForRedeemingDai] = daiCrossChainHarness.generateClaimSecret(); - await uniswapL2Contract.methods + const [, secretHashForDepositingSwappedDai] = daiCrossChainHarness.generateClaimSecret(); + const withdrawReceipt = await uniswapL2Contract.methods .swap_private( wethCrossChainHarness.l2Token.address, wethCrossChainHarness.l2Bridge.address, @@ -656,12 +835,94 @@ export const uniswapL1L2TestSuite = ( nonceForWETHUnshieldApproval, uniswapFeeTier, minimumOutputAmount, - Fr.random(), + secretHashForRedeemingDai, secretHashForDepositingSwappedDai, ownerEthAddress, ) .send() .wait(); + + const swapPrivateContent = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + Buffer.from( + toFunctionSelector( + 'swap_private(address,uint256,uint24,address,uint256,bytes32,bytes32,address)', + ).substring(2), + 'hex', + ), + wethCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(wethAmountToBridge).toBuffer(), + new Fr(uniswapFeeTier).toBuffer(), + daiCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(minimumOutputAmount).toBuffer(), + secretHashForRedeemingDai.toBuffer(), + secretHashForDepositingSwappedDai.toBuffer(), + ownerEthAddress.toBuffer32(), + ]), + ), + ); + + const swapPrivateLeaf = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + uniswapL2Contract.address.toBuffer(), + new Fr(1).toBuffer(), // aztec version + EthAddress.fromString(uniswapPortal.address).toBuffer32(), + new Fr(publicClient.chain.id).toBuffer(), // chain id + swapPrivateContent.toBuffer(), + ]), + ), + ); + + const withdrawContent = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + Buffer.from(toFunctionSelector('withdraw(address,uint256,address)').substring(2), 'hex'), + uniswapPortalAddress.toBuffer32(), + new Fr(wethAmountToBridge).toBuffer(), + uniswapPortalAddress.toBuffer32(), + ]), + ), + ); + + const withdrawLeaf = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + wethCrossChainHarness.l2Bridge.address.toBuffer(), + new Fr(1).toBuffer(), // aztec version + wethCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(publicClient.chain.id).toBuffer(), // chain id + withdrawContent.toBuffer(), + ]), + ), + ); + + const [swapPrivateL2MessageIndex, swapPrivateSiblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( + withdrawReceipt.blockNumber!, + swapPrivateLeaf, + ); + const [withdrawL2MessageIndex, withdrawSiblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( + withdrawReceipt.blockNumber!, + withdrawLeaf, + ); + + const withdrawMessageMetadata = { + _l2BlockNumber: BigInt(withdrawReceipt.blockNumber!), + _leafIndex: BigInt(withdrawL2MessageIndex), + _path: withdrawSiblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + }; + + const swapPrivateMessageMetadata = { + _l2BlockNumber: BigInt(withdrawReceipt.blockNumber!), + _leafIndex: BigInt(swapPrivateL2MessageIndex), + _path: swapPrivateSiblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + }; + // ensure that user's funds were burnt await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); @@ -670,12 +931,13 @@ export const uniswapL1L2TestSuite = ( const swapArgs = [ wethCrossChainHarness.tokenPortalAddress.toString(), wethAmountToBridge, - uniswapFeeTier, + Number(uniswapFeeTier), daiCrossChainHarness.tokenPortalAddress.toString(), minimumOutputAmount, ownerAddress.toString(), secretHashForDepositingSwappedDai.toString(), true, + [withdrawMessageMetadata, swapPrivateMessageMetadata], ] as const; await expect( uniswapPortal.simulate.swapPublic(swapArgs, { @@ -700,7 +962,7 @@ export const uniswapL1L2TestSuite = ( // Call swap_public on L2 const secretHashForDepositingSwappedDai = Fr.random(); - await uniswapL2Contract.methods + const withdrawReceipt = await uniswapL2Contract.methods .swap_public( ownerAddress, wethCrossChainHarness.l2Bridge.address, @@ -716,22 +978,107 @@ export const uniswapL1L2TestSuite = ( ) .send() .wait(); + + const swapPublicContent = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + Buffer.from( + toFunctionSelector( + 'swap_public(address,uint256,uint24,address,uint256,bytes32,bytes32,address)', + ).substring(2), + 'hex', + ), + wethCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(wethAmountToBridge).toBuffer(), + new Fr(uniswapFeeTier).toBuffer(), + daiCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(minimumOutputAmount).toBuffer(), + ownerAddress.toBuffer(), + secretHashForDepositingSwappedDai.toBuffer(), + ownerEthAddress.toBuffer32(), + ]), + ), + ); + + const swapPublicLeaf = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + uniswapL2Contract.address.toBuffer(), + new Fr(1).toBuffer(), // aztec version + EthAddress.fromString(uniswapPortal.address).toBuffer32(), + new Fr(publicClient.chain.id).toBuffer(), // chain id + swapPublicContent.toBuffer(), + ]), + ), + ); + + const withdrawContent = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + Buffer.from(toFunctionSelector('withdraw(address,uint256,address)').substring(2), 'hex'), + uniswapPortalAddress.toBuffer32(), + new Fr(wethAmountToBridge).toBuffer(), + uniswapPortalAddress.toBuffer32(), + ]), + ), + ); + + const withdrawLeaf = Fr.fromBufferReduce( + sha256( + Buffer.concat([ + wethCrossChainHarness.l2Bridge.address.toBuffer(), + new Fr(1).toBuffer(), // aztec version + wethCrossChainHarness.tokenPortalAddress.toBuffer32(), + new Fr(publicClient.chain.id).toBuffer(), // chain id + withdrawContent.toBuffer(), + ]), + ), + ); + + const [swapPublicL2MessageIndex, swapPublicSiblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( + withdrawReceipt.blockNumber!, + swapPublicLeaf, + ); + const [withdrawL2MessageIndex, withdrawSiblingPath] = await aztecNode.getL2ToL1MessageIndexAndSiblingPath( + withdrawReceipt.blockNumber!, + withdrawLeaf, + ); + + const withdrawMessageMetadata = { + _l2BlockNumber: BigInt(withdrawReceipt.blockNumber!), + _leafIndex: BigInt(withdrawL2MessageIndex), + _path: withdrawSiblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + }; + + const swapPublicMessageMetadata = { + _l2BlockNumber: BigInt(withdrawReceipt.blockNumber!), + _leafIndex: BigInt(swapPublicL2MessageIndex), + _path: swapPublicSiblingPath + .toBufferArray() + .map((buf: Buffer) => `0x${buf.toString('hex')}`) as readonly `0x${string}`[], + }; + // check weth balance of owner on L2 (we first bridged `wethAmountToBridge` into L2 and now withdrew it!) await wethCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, 0n); // Call swap_private on L1 const secretHashForRedeemingDai = Fr.random(); // creating my own secret hash logger('Execute withdraw and swap on the uniswapPortal!'); + const swapArgs = [ wethCrossChainHarness.tokenPortalAddress.toString(), wethAmountToBridge, - uniswapFeeTier, + Number(uniswapFeeTier), daiCrossChainHarness.tokenPortalAddress.toString(), minimumOutputAmount, secretHashForRedeemingDai.toString(), secretHashForDepositingSwappedDai.toString(), true, + [withdrawMessageMetadata, swapPublicMessageMetadata], ] as const; + await expect( uniswapPortal.simulate.swapPrivate(swapArgs, { account: ownerEthAddress.toString(), diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index d48a8d25d72..7fb7939056e 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -31,7 +31,6 @@ export type DeployL1Contracts = { * Public Client Type. */ publicClient: PublicClient; - /** * The currently deployed l1 contract addresses */ @@ -122,15 +121,6 @@ export const deployL1Contracts = async ( ); logger(`Deployed Registry at ${registryAddress}`); - const outboxAddress = await deployL1Contract( - walletClient, - publicClient, - contractsToDeploy.outbox.contractAbi, - contractsToDeploy.outbox.contractBytecode, - [getAddress(registryAddress.toString())], - ); - logger(`Deployed Outbox at ${outboxAddress}`); - const availabilityOracleAddress = await deployL1Contract( walletClient, publicClient, @@ -148,7 +138,7 @@ export const deployL1Contracts = async ( ); logger(`Deployed Rollup at ${rollupAddress}`); - // Inbox is immutable and is deployed from Rollup's constructor so we just fetch it from the contract. + // Inbox and Outbox are immutable and are deployed from Rollup's constructor so we just fetch them from the contract. let inboxAddress!: EthAddress; { const rollup = getContract({ @@ -158,6 +148,18 @@ export const deployL1Contracts = async ( }); inboxAddress = EthAddress.fromString((await rollup.read.INBOX([])) as any); } + logger(`Inbox available at ${inboxAddress}`); + + let outboxAddress!: EthAddress; + { + const rollup = getContract({ + address: getAddress(rollupAddress.toString()), + abi: contractsToDeploy.rollup.contractAbi, + client: publicClient, + }); + outboxAddress = EthAddress.fromString((await rollup.read.OUTBOX([])) as any); + } + logger(`Outbox available at ${outboxAddress}`); // We need to call a function on the registry to set the various contract addresses. const registryContract = getContract({