diff --git a/l1-contracts/foundry.toml b/l1-contracts/foundry.toml index 31c93e28280..ff36d4ac7bf 100644 --- a/l1-contracts/foundry.toml +++ b/l1-contracts/foundry.toml @@ -2,7 +2,7 @@ src = 'src' out = 'out' libs = ['lib'] -solc = "0.8.21" +solc = "0.8.23" remappings = [ "@oz/=lib/openzeppelin-contracts/contracts/" diff --git a/l1-contracts/slither_output.md b/l1-contracts/slither_output.md index 8ff58c77191..32bf9f3cf12 100644 --- a/l1-contracts/slither_output.md +++ b/l1-contracts/slither_output.md @@ -3,17 +3,17 @@ Summary - [uninitialized-local](#uninitialized-local) (2 results) (Medium) - [unused-return](#unused-return) (1 results) (Medium) - [pess-dubious-typecast](#pess-dubious-typecast) (8 results) (Medium) - - [missing-zero-check](#missing-zero-check) (1 results) (Low) + - [missing-zero-check](#missing-zero-check) (2 results) (Low) - [reentrancy-events](#reentrancy-events) (2 results) (Low) - [timestamp](#timestamp) (4 results) (Low) - - [pess-public-vs-external](#pess-public-vs-external) (6 results) (Low) + - [pess-public-vs-external](#pess-public-vs-external) (7 results) (Low) - [assembly](#assembly) (2 results) (Informational) - [dead-code](#dead-code) (5 results) (Informational) - [solc-version](#solc-version) (1 results) (Informational) - [low-level-calls](#low-level-calls) (1 results) (Informational) - [similar-names](#similar-names) (3 results) (Informational) - [constable-states](#constable-states) (1 results) (Optimization) - - [pess-multiple-storage-read](#pess-multiple-storage-read) (5 results) (Optimization) + - [pess-multiple-storage-read](#pess-multiple-storage-read) (6 results) (Optimization) ## pess-unprotected-setter Impact: High Confidence: Medium @@ -136,10 +136,17 @@ Confidence: Medium src/core/messagebridge/NewInbox.sol#L41 + - [ ] ID-13 +[NewOutbox.constructor(address)._rollup](src/core/messagebridge/NewOutbox.sol#L30) lacks a zero-check on : + - [ROLLUP_CONTRACT = _rollup](src/core/messagebridge/NewOutbox.sol#L31) + +src/core/messagebridge/NewOutbox.sol#L30 + + ## reentrancy-events Impact: Low Confidence: Medium - - [ ] ID-13 + - [ ] ID-14 Reentrancy in [NewInbox.sendL2Message(DataStructures.L2Actor,bytes32,bytes32)](src/core/messagebridge/NewInbox.sol#L62-L99): External calls: - [index = currentTree.insertLeaf(leaf)](src/core/messagebridge/NewInbox.sol#L95) @@ -149,7 +156,7 @@ Reentrancy in [NewInbox.sendL2Message(DataStructures.L2Actor,bytes32,bytes32)](s src/core/messagebridge/NewInbox.sol#L62-L99 - - [ ] ID-14 + - [ ] ID-15 Reentrancy in [Rollup.process(bytes,bytes32,bytes,bytes)](src/core/Rollup.sol#L58-L101): External calls: - [inbox.batchConsume(l1ToL2Msgs,msg.sender)](src/core/Rollup.sol#L90) @@ -164,7 +171,7 @@ src/core/Rollup.sol#L58-L101 ## timestamp Impact: Low Confidence: Medium - - [ ] ID-15 + - [ ] ID-16 [Inbox.batchConsume(bytes32[],address)](src/core/messagebridge/Inbox.sol#L122-L143) uses timestamp for comparisons Dangerous comparisons: - [block.timestamp > entry.deadline](src/core/messagebridge/Inbox.sol#L136) @@ -172,7 +179,7 @@ Confidence: Medium src/core/messagebridge/Inbox.sol#L122-L143 - - [ ] ID-16 + - [ ] ID-17 [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) @@ -180,7 +187,7 @@ src/core/messagebridge/Inbox.sol#L122-L143 src/core/libraries/HeaderLib.sol#L106-L136 - - [ ] ID-17 + - [ ] ID-18 [Inbox.sendL2Message(DataStructures.L2Actor,uint32,bytes32,bytes32)](src/core/messagebridge/Inbox.sol#L45-L91) uses timestamp for comparisons Dangerous comparisons: - [_deadline <= block.timestamp](src/core/messagebridge/Inbox.sol#L54) @@ -188,7 +195,7 @@ src/core/libraries/HeaderLib.sol#L106-L136 src/core/messagebridge/Inbox.sol#L45-L91 - - [ ] ID-18 + - [ ] ID-19 [Inbox.cancelL2Message(DataStructures.L1ToL2Msg,address)](src/core/messagebridge/Inbox.sol#L102-L113) uses timestamp for comparisons Dangerous comparisons: - [block.timestamp <= _message.deadline](src/core/messagebridge/Inbox.sol#L108) @@ -199,28 +206,28 @@ src/core/messagebridge/Inbox.sol#L102-L113 ## pess-public-vs-external Impact: Low Confidence: Medium - - [ ] ID-19 + - [ ] ID-20 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-20 + - [ ] ID-21 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-21 + - [ ] ID-22 The following public functions could be turned into external in [Rollup](src/core/Rollup.sol#L30-L110) contract: [Rollup.constructor(IRegistry,IAvailabilityOracle)](src/core/Rollup.sol#L43-L49) src/core/Rollup.sol#L30-L110 - - [ ] ID-22 + - [ ] ID-23 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) @@ -229,7 +236,7 @@ The following public functions could be turned into external in [Outbox](src/cor src/core/messagebridge/Outbox.sol#L21-L148 - - [ ] ID-23 + - [ ] ID-24 The following public functions could be turned into external in [Inbox](src/core/messagebridge/Inbox.sol#L21-L231) contract: [Inbox.constructor(address)](src/core/messagebridge/Inbox.sol#L30-L32) [Inbox.contains(bytes32)](src/core/messagebridge/Inbox.sol#L174-L176) @@ -237,7 +244,14 @@ The following public functions could be turned into external in [Inbox](src/core src/core/messagebridge/Inbox.sol#L21-L231 - - [ ] ID-24 + - [ ] ID-25 +The following public functions could be turned into external in [NewOutbox](src/core/messagebridge/NewOutbox.sol#L18-L131) contract: + [NewOutbox.constructor(address)](src/core/messagebridge/NewOutbox.sol#L30-L32) + +src/core/messagebridge/NewOutbox.sol#L18-L131 + + + - [ ] ID-26 The following public functions could be turned into external in [NewInbox](src/core/messagebridge/NewInbox.sol#L25-L128) contract: [NewInbox.constructor(address,uint256)](src/core/messagebridge/NewInbox.sol#L41-L52) @@ -247,7 +261,7 @@ src/core/messagebridge/NewInbox.sol#L25-L128 ## assembly Impact: Informational Confidence: High - - [ ] ID-25 + - [ ] ID-27 [MessagesDecoder.decode(bytes)](src/core/libraries/decoders/MessagesDecoder.sol#L60-L142) uses assembly - [INLINE ASM](src/core/libraries/decoders/MessagesDecoder.sol#L79-L81) - [INLINE ASM](src/core/libraries/decoders/MessagesDecoder.sol#L112-L118) @@ -255,7 +269,7 @@ Confidence: High src/core/libraries/decoders/MessagesDecoder.sol#L60-L142 - - [ ] ID-26 + - [ ] ID-28 [TxsDecoder.computeRoot(bytes32[])](src/core/libraries/decoders/TxsDecoder.sol#L256-L275) uses assembly - [INLINE ASM](src/core/libraries/decoders/TxsDecoder.sol#L263-L265) @@ -265,31 +279,31 @@ src/core/libraries/decoders/TxsDecoder.sol#L256-L275 ## dead-code Impact: Informational Confidence: Medium - - [ ] ID-27 + - [ ] ID-29 [Inbox._errIncompatibleEntryArguments(bytes32,uint64,uint64,uint32,uint32,uint32,uint32)](src/core/messagebridge/Inbox.sol#L212-L230) is never used and should be removed src/core/messagebridge/Inbox.sol#L212-L230 - - [ ] ID-28 + - [ ] ID-30 [Outbox._errNothingToConsume(bytes32)](src/core/messagebridge/Outbox.sol#L114-L116) is never used and should be removed src/core/messagebridge/Outbox.sol#L114-L116 - - [ ] ID-29 + - [ ] ID-31 [Hash.sha256ToField(bytes32)](src/core/libraries/Hash.sol#L59-L61) is never used and should be removed src/core/libraries/Hash.sol#L59-L61 - - [ ] ID-30 + - [ ] ID-32 [Inbox._errNothingToConsume(bytes32)](src/core/messagebridge/Inbox.sol#L197-L199) is never used and should be removed src/core/messagebridge/Inbox.sol#L197-L199 - - [ ] ID-31 + - [ ] ID-33 [Outbox._errIncompatibleEntryArguments(bytes32,uint64,uint64,uint32,uint32,uint32,uint32)](src/core/messagebridge/Outbox.sol#L129-L147) is never used and should be removed src/core/messagebridge/Outbox.sol#L129-L147 @@ -298,13 +312,13 @@ src/core/messagebridge/Outbox.sol#L129-L147 ## solc-version Impact: Informational Confidence: High - - [ ] ID-32 -solc-0.8.21 is not recommended for deployment + - [ ] ID-34 +solc-0.8.23 is not recommended for deployment ## low-level-calls Impact: Informational Confidence: High - - [ ] ID-33 + - [ ] ID-35 Low level call in [Inbox.withdrawFees()](src/core/messagebridge/Inbox.sol#L148-L153): - [(success) = msg.sender.call{value: balance}()](src/core/messagebridge/Inbox.sol#L151) @@ -314,19 +328,19 @@ src/core/messagebridge/Inbox.sol#L148-L153 ## similar-names Impact: Informational Confidence: Medium - - [ ] ID-34 + - [ ] ID-36 Variable [Constants.LOGS_HASHES_NUM_BYTES_PER_BASE_ROLLUP](src/core/libraries/ConstantsGen.sol#L132) is too similar to [Constants.NOTE_HASHES_NUM_BYTES_PER_BASE_ROLLUP](src/core/libraries/ConstantsGen.sol#L125) src/core/libraries/ConstantsGen.sol#L132 - - [ ] ID-35 + - [ ] ID-37 Variable [Constants.L1_TO_L2_MESSAGE_LENGTH](src/core/libraries/ConstantsGen.sol#L112) is too similar to [Constants.L2_TO_L1_MESSAGE_LENGTH](src/core/libraries/ConstantsGen.sol#L113) src/core/libraries/ConstantsGen.sol#L112 - - [ ] ID-36 + - [ ] ID-38 Variable [Rollup.AVAILABILITY_ORACLE](src/core/Rollup.sol#L33) is too similar to [Rollup.constructor(IRegistry,IAvailabilityOracle)._availabilityOracle](src/core/Rollup.sol#L43) src/core/Rollup.sol#L33 @@ -335,7 +349,7 @@ src/core/Rollup.sol#L33 ## constable-states Impact: Optimization Confidence: High - - [ ] ID-37 + - [ ] ID-39 [Rollup.lastWarpedBlockTs](src/core/Rollup.sol#L41) should be constant src/core/Rollup.sol#L41 @@ -344,31 +358,37 @@ src/core/Rollup.sol#L41 ## pess-multiple-storage-read Impact: Optimization Confidence: High - - [ ] ID-38 + - [ ] ID-40 +In a function [NewOutbox.insert(uint256,bytes32,uint256)](src/core/messagebridge/NewOutbox.sol#L43-L63) variable [NewOutbox.roots](src/core/messagebridge/NewOutbox.sol#L28) is read multiple times + +src/core/messagebridge/NewOutbox.sol#L43-L63 + + + - [ ] ID-41 In a function [NewInbox.sendL2Message(DataStructures.L2Actor,bytes32,bytes32)](src/core/messagebridge/NewInbox.sol#L62-L99) variable [NewInbox.inProgress](src/core/messagebridge/NewInbox.sol#L37) is read multiple times src/core/messagebridge/NewInbox.sol#L62-L99 - - [ ] ID-39 + - [ ] ID-42 In a function [FrontierMerkle.root()](src/core/messagebridge/frontier_tree/Frontier.sol#L43-L76) variable [FrontierMerkle.HEIGHT](src/core/messagebridge/frontier_tree/Frontier.sol#L8) is read multiple times src/core/messagebridge/frontier_tree/Frontier.sol#L43-L76 - - [ ] ID-40 + - [ ] ID-43 In a function [NewInbox.consume()](src/core/messagebridge/NewInbox.sol#L108-L127) variable [NewInbox.inProgress](src/core/messagebridge/NewInbox.sol#L37) is read multiple times src/core/messagebridge/NewInbox.sol#L108-L127 - - [ ] ID-41 + - [ ] ID-44 In a function [NewInbox.consume()](src/core/messagebridge/NewInbox.sol#L108-L127) variable [NewInbox.toConsume](src/core/messagebridge/NewInbox.sol#L35) is read multiple times src/core/messagebridge/NewInbox.sol#L108-L127 - - [ ] ID-42 + - [ ] ID-45 In a function [FrontierMerkle.root()](src/core/messagebridge/frontier_tree/Frontier.sol#L43-L76) variable [FrontierMerkle.frontier](src/core/messagebridge/frontier_tree/Frontier.sol#L13) is read multiple times src/core/messagebridge/frontier_tree/Frontier.sol#L43-L76 diff --git a/l1-contracts/src/core/interfaces/messagebridge/INewOutbox.sol b/l1-contracts/src/core/interfaces/messagebridge/INewOutbox.sol new file mode 100644 index 00000000000..3a7c9e03af0 --- /dev/null +++ b/l1-contracts/src/core/interfaces/messagebridge/INewOutbox.sol @@ -0,0 +1,62 @@ +// 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/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 6cc41709fbd..422bbfafff6 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -46,6 +46,12 @@ library Errors { uint32 storedDeadline, uint32 deadlinePassed ); // 0x5e789f34 + error Outbox__InvalidPathLength(uint256 expected, uint256 actual); // 0x481bcd9c + error Outbox__InsertingInvalidRoot(); // 0x73c2daca + error Outbox__RootAlreadySetAtBlock(uint256 l2BlockNumber); // 0x3eccfd3e + error Outbox__InvalidRecipient(address expected, address actual); // 0x57aad581 + error Outbox__AlreadyNullified(uint256 l2BlockNumber, uint256 leafIndex); // 0xfd71c2d4 + error Outbox__NothingToConsumeAtBlock(uint256 l2BlockNumber); // 0xa4508f22 // Rollup error Rollup__InvalidArchive(bytes32 expected, bytes32 actual); // 0xb682a40e @@ -63,4 +69,7 @@ library Errors { // HeaderLib error HeaderLib__InvalidHeaderSize(uint256 expected, uint256 actual); // 0xf3ccb247 + + // MerkleLib + error MerkleLib__InvalidRoot(bytes32 expected, bytes32 actual); // 0xb77e99 } diff --git a/l1-contracts/src/core/libraries/MerkleLib.sol b/l1-contracts/src/core/libraries/MerkleLib.sol new file mode 100644 index 00000000000..bd7c5fbb8bd --- /dev/null +++ b/l1-contracts/src/core/libraries/MerkleLib.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +import {Errors} from "../libraries/Errors.sol"; + +/** + * @title Merkle Library + * @author Aztec Labs + * @notice Library that contains functions useful when interacting with Merkle Trees + */ +library MerkleLib { + /** + * @notice Verifies the membership of a leaf and path against an expected root. + * @dev In the case of a mismatched root, and subsequent inability to verify membership, this function throws. + * @param _path - The sibling path of the message as a leaf, used to prove message inclusion + * @param _leaf - The hash of the message we are trying to prove inclusion for + * @param _index - The index of the message inside the L2 to L1 message tree + * @param _expectedRoot - The expected root to check the validity of the message and sibling path with. + * @notice - + * E.g. A sibling path for a leaf at index 3 (L) in a tree of depth 3 (between 5 and 8 leafs) consists of the 3 elements denoted as *'s + * d0: [ root ] + * d1: [ ] [*] + * d2: [*] [ ] [ ] [ ] + * d3: [ ] [ ] [*] [L] [ ] [ ] [ ] [ ]. + * And the elements would be ordered as: [ d3_index_2, d2_index_0, d1_index_1 ]. + */ + function verifyMembership( + bytes32[] calldata _path, + bytes32 _leaf, + uint256 _index, + bytes32 _expectedRoot + ) internal pure { + bytes32 subtreeRoot = _leaf; + /// @notice - We use the indexAtHeight to see whether our child of the next subtree is at the left or the right side + uint256 indexAtHeight = _index; + + for (uint256 height = 0; height < _path.length; height++) { + /// @notice - This affects the way we concatenate our two children to then hash and calculate the root, as any odd indexes (index bit-masked with least significant bit) are right-sided children. + bool isRight = (indexAtHeight & 1) == 1; + + subtreeRoot = isRight + ? sha256(bytes.concat(_path[height], subtreeRoot)) + : sha256(bytes.concat(subtreeRoot, _path[height])); + /// @notice - We divide by two here to get the index of the parent of the current subtreeRoot in its own layer + indexAtHeight >>= 1; + } + + if (subtreeRoot != _expectedRoot) { + revert Errors.MerkleLib__InvalidRoot(_expectedRoot, subtreeRoot); + } + } +} diff --git a/l1-contracts/src/core/messagebridge/NewOutbox.sol b/l1-contracts/src/core/messagebridge/NewOutbox.sol new file mode 100644 index 00000000000..b172904e5d0 --- /dev/null +++ b/l1-contracts/src/core/messagebridge/NewOutbox.sol @@ -0,0 +1,131 @@ +// 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 { + 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/test/NewOutbox.t.sol b/l1-contracts/test/NewOutbox.t.sol new file mode 100644 index 00000000000..2a22f78115d --- /dev/null +++ b/l1-contracts/test/NewOutbox.t.sol @@ -0,0 +1,254 @@ +// 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/merkle/Merkle.t.sol b/l1-contracts/test/Parity.t.sol similarity index 81% rename from l1-contracts/test/merkle/Merkle.t.sol rename to l1-contracts/test/Parity.t.sol index 60721c97254..de14ded13f8 100644 --- a/l1-contracts/test/merkle/Merkle.t.sol +++ b/l1-contracts/test/Parity.t.sol @@ -1,31 +1,15 @@ // 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 {NaiveMerkle} from "./Naive.sol"; -import {FrontierMerkle} from "./../../src/core/messagebridge/frontier_tree/Frontier.sol"; -import {Constants} from "../../src/core/libraries/ConstantsGen.sol"; +import {FrontierMerkle} from "./../src/core/messagebridge/frontier_tree/Frontier.sol"; +import {Constants} from "../src/core/libraries/ConstantsGen.sol"; -contract MerkleTest is Test { +contract ParityTest is Test { function setUp() public {} - function testFrontier() public { - uint256 depth = 10; - - NaiveMerkle merkle = new NaiveMerkle(depth); - FrontierMerkle frontier = new FrontierMerkle(depth); - - uint256 upper = frontier.SIZE(); - for (uint256 i = 0; i < upper; i++) { - bytes32 leaf = sha256(abi.encode(i + 1)); - merkle.insertLeaf(leaf); - frontier.insertLeaf(leaf); - assertEq(merkle.computeRoot(), frontier.root(), "Frontier Roots should be equal"); - } - } - // Checks whether sha root matches output of base parity circuit function testRootMatchesBaseParity() public { uint256[4] memory msgs = [ diff --git a/l1-contracts/test/merkle/Frontier.t.sol b/l1-contracts/test/merkle/Frontier.t.sol new file mode 100644 index 00000000000..fa4cb85956e --- /dev/null +++ b/l1-contracts/test/merkle/Frontier.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +import {Test} from "forge-std/Test.sol"; + +import {NaiveMerkle} from "./Naive.sol"; +import {FrontierMerkle} from "./../../src/core/messagebridge/frontier_tree/Frontier.sol"; + +contract FrontierTest is Test { + function setUp() public {} + + function testFrontier() public { + uint256 depth = 10; + + NaiveMerkle merkle = new NaiveMerkle(depth); + FrontierMerkle frontier = new FrontierMerkle(depth); + + uint256 upper = frontier.SIZE(); + for (uint256 i = 0; i < upper; i++) { + bytes32 leaf = sha256(abi.encode(i + 1)); + merkle.insertLeaf(leaf); + frontier.insertLeaf(leaf); + assertEq(merkle.computeRoot(), frontier.root(), "Frontier Roots should be equal"); + } + } +} diff --git a/l1-contracts/test/merkle/MerkleLib.t.sol b/l1-contracts/test/merkle/MerkleLib.t.sol new file mode 100644 index 00000000000..b296fae8947 --- /dev/null +++ b/l1-contracts/test/merkle/MerkleLib.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +import {Test} from "forge-std/Test.sol"; + +import {NaiveMerkle} from "./Naive.sol"; +import {MerkleLibHelper} from "./helpers/MerkleLibHelper.sol"; + +contract MerkleLibTest is Test { + MerkleLibHelper internal merkleLibHelper; + NaiveMerkle internal merkle; + uint256 public constant DEPTH = 10; + + function setUp() public { + merkleLibHelper = new MerkleLibHelper(); + + merkle = new NaiveMerkle(DEPTH); + uint256 treeSize = merkle.SIZE(); + for (uint256 i = 0; i < treeSize; i++) { + bytes32 generatedLeaf = sha256(abi.encode(i + 1)); + merkle.insertLeaf(generatedLeaf); + } + } + + function testVerifyMembership(uint256 _idx) public view { + uint256 leafIndex = bound(_idx, 0, merkle.SIZE() - 1); + + (bytes32[] memory path, bytes32 leaf) = merkle.computeSiblingPath(leafIndex); + + bytes32 expectedRoot = merkle.computeRoot(); + + merkleLibHelper.verifyMembership(path, leaf, leafIndex, expectedRoot); + } + + function testVerifyMembershipWithBadInput(uint256 _idx) public { + uint256 leafIndex = bound(_idx, 0, merkle.SIZE() - 1); + bytes32 expectedRoot = merkle.computeRoot(); + + // Tests garbled path + (bytes32[] memory path1, bytes32 leaf) = merkle.computeSiblingPath(leafIndex); + bytes32 temp1 = path1[0]; + path1[0] = path1[path1.length - 1]; + path1[path1.length - 1] = temp1; + vm.expectRevert(); + merkleLibHelper.verifyMembership(path1, leaf, leafIndex, expectedRoot); + + // Tests truncated path + (bytes32[] memory path2,) = merkle.computeSiblingPath(leafIndex); + bytes32[] memory truncatedPath = new bytes32[](path2.length - 1); + for (uint256 i = 0; i < truncatedPath.length; i++) { + truncatedPath[i] = path2[i]; + } + + vm.expectRevert(); + merkleLibHelper.verifyMembership(truncatedPath, leaf, leafIndex, expectedRoot); + + // Tests empty path + bytes32[] memory emptyPath = new bytes32[](0); + vm.expectRevert(); + merkleLibHelper.verifyMembership(emptyPath, leaf, leafIndex, expectedRoot); + } + + function testVerifyMembershipWithRandomSiblingPaths( + uint256 _idx, + bytes32[DEPTH] memory _siblingPath + ) public { + uint256 leafIndex = _idx % (2 ** DEPTH); + bytes32 expectedRoot = merkle.computeRoot(); + + bytes32[] memory siblingPath = new bytes32[](DEPTH); + for (uint256 i = 0; i < _siblingPath.length; i++) { + siblingPath[i] = _siblingPath[i]; + } + + bytes32 leaf = sha256(abi.encode(leafIndex + 1)); + + vm.expectRevert(); + merkleLibHelper.verifyMembership(siblingPath, leaf, leafIndex, expectedRoot); + } +} diff --git a/l1-contracts/test/merkle/Naive.sol b/l1-contracts/test/merkle/Naive.sol index 1e6bc9c5584..6ab6c896a48 100644 --- a/l1-contracts/test/merkle/Naive.sol +++ b/l1-contracts/test/merkle/Naive.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2023 Aztec Labs. +// Copyright 2024 Aztec Labs. pragma solidity >=0.8.18; contract NaiveMerkle { @@ -34,4 +34,34 @@ contract NaiveMerkle { } return nodes[0]; } + + function computeSiblingPath(uint256 _index) public view returns (bytes32[] memory, bytes32) { + bytes32[] memory nodes = new bytes32[](SIZE / 2); + bytes32[] memory path = new bytes32[](DEPTH); + + uint256 idx = _index; + + uint256 size = SIZE; + for (uint256 i = 0; i < DEPTH; i++) { + bool isRight = (idx & 1) == 1; + if (i > 0) { + path[i] = isRight ? nodes[idx - 1] : nodes[idx + 1]; + } else { + path[i] = isRight ? leafs[idx - 1] : leafs[idx + 1]; + } + + for (uint256 j = 0; j < size; j += 2) { + if (i == 0) { + nodes[j / 2] = sha256(bytes.concat(leafs[j], leafs[j + 1])); + } else { + nodes[j / 2] = sha256(bytes.concat(nodes[j], nodes[j + 1])); + } + } + + idx /= 2; + size /= 2; + } + + return (path, leafs[_index]); + } } diff --git a/l1-contracts/test/merkle/Naive.t.sol b/l1-contracts/test/merkle/Naive.t.sol new file mode 100644 index 00000000000..7b6dee2130e --- /dev/null +++ b/l1-contracts/test/merkle/Naive.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +import {Test} from "forge-std/Test.sol"; + +import {NaiveMerkle} from "./Naive.sol"; + +contract NaiveTest is Test { + function setUp() public {} + + function testComputeSiblingPathManuallyLeftChild() public { + /// Creates a merkle tree with depth 3 and size 8, with leafs from 1 - 8 + NaiveMerkle tree = new NaiveMerkle(3); + for (uint256 i = 1; i <= 8; i++) { + bytes32 generatedLeaf = bytes32(abi.encode(i)); + tree.insertLeaf(generatedLeaf); + } + + /** + * We manually make a path; this is the sibling path of the leaf with the value of 1. + * This path, from leaf to root, consists a, b, and c; which correspond to the value of 2, then the hash of 3 and 4, + * and finally, the hash of 5 and 6 concatenated with the hash of 7 and 8; + * d0: [ root ] + * d1: [ ] [c] + * d2: [ ] [b] [ ] [ ] + * d3: [1] [a] [3] [4] [5] [6] [7] [8]. + */ + bytes32[3] memory expectedPath = [ + bytes32(abi.encode(2)), + sha256(bytes.concat(bytes32(abi.encode(3)), bytes32(abi.encode(4)))), + sha256( + bytes.concat( + sha256(bytes.concat(bytes32(abi.encode(5)), bytes32(abi.encode(6)))), + sha256(bytes.concat(bytes32(abi.encode(7)), bytes32(abi.encode(8)))) + ) + ) + ]; + + /// We then compute the sibling path using the tree and expect that our manual calculation should equal the computed one + (bytes32[] memory path, bytes32 leaf) = tree.computeSiblingPath(0); + assertEq(leaf, bytes32(abi.encode(1))); + assertEq(path[0], expectedPath[0]); + assertEq(path[1], expectedPath[1]); + assertEq(path[2], expectedPath[2]); + } + + function testComputeSiblingPathManuallyRightChild() public { + /// Creates a merkle tree with depth 3 and size 8, with leafs from 1 - 8 + NaiveMerkle tree = new NaiveMerkle(3); + for (uint256 i = 1; i <= 8; i++) { + bytes32 generatedLeaf = bytes32(abi.encode(i)); + tree.insertLeaf(generatedLeaf); + } + + /** + * We manually make a path; this is the sibling path of the leaf with the value of 8. + * This path, from leaf to root, consists of c a, b, and c; which correspond to the value of 7, then the hash of 5 and 6, + * and finally, the hash of 1 and 2 concatenated with the hash of 3 and 4; + * d0: [ root ] + * d1: [c] [ ] + * d2: [ ] [b] [b] [ ] + * d3: [1] [2] [3] [4] [5] [6] [a] [8]. + */ + bytes32[3] memory expectedPath = [ + bytes32(abi.encode(7)), + sha256(bytes.concat(bytes32(abi.encode(5)), bytes32(abi.encode(6)))), + sha256( + bytes.concat( + sha256(bytes.concat(bytes32(abi.encode(1)), bytes32(abi.encode(2)))), + sha256(bytes.concat(bytes32(abi.encode(3)), bytes32(abi.encode(4)))) + ) + ) + ]; + + /// We then compute the sibling path using the tree and expect that our manual calculation should equal the computed one + (bytes32[] memory path, bytes32 leaf) = tree.computeSiblingPath(7); + assertEq(leaf, bytes32(abi.encode(8))); + assertEq(path[0], expectedPath[0]); + assertEq(path[1], expectedPath[1]); + assertEq(path[2], expectedPath[2]); + } +} diff --git a/l1-contracts/test/merkle/TestUtil.sol b/l1-contracts/test/merkle/TestUtil.sol new file mode 100644 index 00000000000..4ca20511fcd --- /dev/null +++ b/l1-contracts/test/merkle/TestUtil.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +import {Test} from "forge-std/Test.sol"; + +contract MerkleTestUtil is Test { + /* + * @notice Calculates a tree height from the amount of elements in the tree + * @param _size - The amount of elements in the tree + */ + function calculateTreeHeightFromSize(uint256 _size) public pure returns (uint256) { + /// The code / formula that works below has one edge case at _size = 1, which we handle here + if (_size == 1) { + return 1; + } + + /// We need to store the original numer to check at the end if we are a power of two + uint256 originalNumber = _size; + + /// We need the height of the tree that will contain all of our leaves, + /// hence the next highest power of two from the amount of leaves - Math.ceil(Math.log2(x)) + uint256 height = 0; + + /// While size > 1, we divide by two, and count how many times we do this; producing a rudimentary way of calculating Math.Floor(Math.log2(x)) + while (_size > 1) { + _size >>= 1; + height++; + } + + /// @notice - We check if 2 ** height does not equal our original number. If so, this means that our size is not a power of two, + /// and hence we've rounded down (Math.floor) and have obtained the next lowest power of two instead of rounding up (Math.ceil) to obtain the next highest power of two and therefore we need to increment height before returning it. + /// If 2 ** height equals our original number, it means that we have a perfect power of two and Math.floor(Math.log2(x)) = Math.ceil(Math.log2(x)) and we can return height as-is + return (2 ** height) != originalNumber ? ++height : height; + } + + function testCalculateTreeHeightFromSize() external { + assertEq(calculateTreeHeightFromSize(0), 1); + assertEq(calculateTreeHeightFromSize(1), 1); + assertEq(calculateTreeHeightFromSize(2), 1); + assertEq(calculateTreeHeightFromSize(3), 2); + assertEq(calculateTreeHeightFromSize(4), 2); + assertEq(calculateTreeHeightFromSize(5), 3); + assertEq(calculateTreeHeightFromSize(6), 3); + assertEq(calculateTreeHeightFromSize(7), 3); + assertEq(calculateTreeHeightFromSize(8), 3); + assertEq(calculateTreeHeightFromSize(9), 4); + } +} diff --git a/l1-contracts/test/merkle/helpers/MerkleLibHelper.sol b/l1-contracts/test/merkle/helpers/MerkleLibHelper.sol new file mode 100644 index 00000000000..be8234ea023 --- /dev/null +++ b/l1-contracts/test/merkle/helpers/MerkleLibHelper.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +import {MerkleLib} from "../../../src/core/libraries/MerkleLib.sol"; + +// A wrapper used to be able to "call" library functions, instead of "jumping" to them, allowing forge to catch the reverts +contract MerkleLibHelper { + function verifyMembership( + bytes32[] calldata _path, + bytes32 _leaf, + uint256 _index, + bytes32 _expectedRoot + ) external pure { + MerkleLib.verifyMembership(_path, _leaf, _index, _expectedRoot); + } +}