diff --git a/.changeset/spotty-queens-own.md b/.changeset/spotty-queens-own.md new file mode 100644 index 00000000000..98fb2fbc0ed --- /dev/null +++ b/.changeset/spotty-queens-own.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`MerkleProof`: Add variations of `verify`, `processProof`, `multiProofVerify` and `processMultiProof` (and equivalent calldata version) with support for custom hashing functions. diff --git a/contracts/mocks/MerkleProofCustomHashMock.sol b/contracts/mocks/MerkleProofCustomHashMock.sol new file mode 100644 index 00000000000..1039af32fb5 --- /dev/null +++ b/contracts/mocks/MerkleProofCustomHashMock.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {MerkleProof} from "../utils/cryptography/MerkleProof.sol"; + +// This could be a library, but then we would have to add it to the Stateless.sol mock for upgradeable tests +abstract contract MerkleProofCustomHashMock { + function customHash(bytes32 a, bytes32 b) internal pure returns (bytes32) { + return a < b ? sha256(abi.encode(a, b)) : sha256(abi.encode(b, a)); + } + + function verify(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal view returns (bool) { + return MerkleProof.verify(proof, root, leaf, customHash); + } + + function processProof(bytes32[] calldata proof, bytes32 leaf) internal view returns (bytes32) { + return MerkleProof.processProof(proof, leaf, customHash); + } + + function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal view returns (bool) { + return MerkleProof.verifyCalldata(proof, root, leaf, customHash); + } + + function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal view returns (bytes32) { + return MerkleProof.processProofCalldata(proof, leaf, customHash); + } + + function multiProofVerify( + bytes32[] calldata proof, + bool[] calldata proofFlags, + bytes32 root, + bytes32[] calldata leaves + ) internal view returns (bool) { + return MerkleProof.multiProofVerify(proof, proofFlags, root, leaves, customHash); + } + + function processMultiProof( + bytes32[] calldata proof, + bool[] calldata proofFlags, + bytes32[] calldata leaves + ) internal view returns (bytes32) { + return MerkleProof.processMultiProof(proof, proofFlags, leaves, customHash); + } + + function multiProofVerifyCalldata( + bytes32[] calldata proof, + bool[] calldata proofFlags, + bytes32 root, + bytes32[] calldata leaves + ) internal view returns (bool) { + return MerkleProof.multiProofVerifyCalldata(proof, proofFlags, root, leaves, customHash); + } + + function processMultiProofCalldata( + bytes32[] calldata proof, + bool[] calldata proofFlags, + bytes32[] calldata leaves + ) internal view returns (bytes32) { + return MerkleProof.processMultiProofCalldata(proof, proofFlags, leaves, customHash); + } +} diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index dce05a0dc05..be1910062bb 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MerkleProof.sol) +// This file was procedurally generated from scripts/generate/templates/MerkleProof.js. pragma solidity ^0.8.20; @@ -18,6 +19,11 @@ import {Hashes} from "./Hashes.sol"; * the Merkle tree could be reinterpreted as a leaf value. * OpenZeppelin's JavaScript library generates Merkle trees that are safe * against this attack out of the box. + * + * NOTE: This library supports proof verification for merkle trees built using + * custom _commutative_ hashing functions (i.e. `H(a, b) == H(b, a)`). Proving + * leaf inclusion in trees built using non-commutative hashing functions requires + * additional logic that is not supported by this library. */ library MerkleProof { /** @@ -30,16 +36,44 @@ library MerkleProof { * defined by `root`. For this, a `proof` must be provided, containing * sibling hashes on the branch from the leaf to the root of the tree. Each * pair of leaves and each pair of pre-images are assumed to be sorted. + * + * This version handles proofs in memory with the default hashing function. */ function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) { return processProof(proof, leaf) == root; } /** - * @dev Calldata version of {verify} + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. When processing the proof, the pairs + * of leafs & pre-images are assumed to be sorted. + * + * This version handles proofs in memory with the default hashing function. */ - function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) { - return processProofCalldata(proof, leaf) == root; + function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { + bytes32 computedHash = leaf; + for (uint256 i = 0; i < proof.length; i++) { + computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]); + } + return computedHash; + } + + /** + * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree + * defined by `root`. For this, a `proof` must be provided, containing + * sibling hashes on the branch from the leaf to the root of the tree. Each + * pair of leaves and each pair of pre-images are assumed to be sorted. + * + * This version handles proofs in memory with a custom hashing function. + */ + function verify( + bytes32[] memory proof, + bytes32 root, + bytes32 leaf, + function(bytes32, bytes32) view returns (bytes32) hasher + ) internal view returns (bool) { + return processProof(proof, leaf, hasher) == root; } /** @@ -47,17 +81,40 @@ library MerkleProof { * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt * hash matches the root of the tree. When processing the proof, the pairs * of leafs & pre-images are assumed to be sorted. + * + * This version handles proofs in memory with a custom hashing function. */ - function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { + function processProof( + bytes32[] memory proof, + bytes32 leaf, + function(bytes32, bytes32) view returns (bytes32) hasher + ) internal view returns (bytes32) { bytes32 computedHash = leaf; for (uint256 i = 0; i < proof.length; i++) { - computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]); + computedHash = hasher(computedHash, proof[i]); } return computedHash; } /** - * @dev Calldata version of {processProof} + * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree + * defined by `root`. For this, a `proof` must be provided, containing + * sibling hashes on the branch from the leaf to the root of the tree. Each + * pair of leaves and each pair of pre-images are assumed to be sorted. + * + * This version handles proofs in calldata with the default hashing function. + */ + function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) { + return processProof(proof, leaf) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. When processing the proof, the pairs + * of leafs & pre-images are assumed to be sorted. + * + * This version handles proofs in calldata with the default hashing function. */ function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) { bytes32 computedHash = leaf; @@ -67,10 +124,49 @@ library MerkleProof { return computedHash; } + /** + * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree + * defined by `root`. For this, a `proof` must be provided, containing + * sibling hashes on the branch from the leaf to the root of the tree. Each + * pair of leaves and each pair of pre-images are assumed to be sorted. + * + * This version handles proofs in calldata with a custom hashing function. + */ + function verifyCalldata( + bytes32[] calldata proof, + bytes32 root, + bytes32 leaf, + function(bytes32, bytes32) view returns (bytes32) hasher + ) internal view returns (bool) { + return processProof(proof, leaf, hasher) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. When processing the proof, the pairs + * of leafs & pre-images are assumed to be sorted. + * + * This version handles proofs in calldata with a custom hashing function. + */ + function processProofCalldata( + bytes32[] calldata proof, + bytes32 leaf, + function(bytes32, bytes32) view returns (bytes32) hasher + ) internal view returns (bytes32) { + bytes32 computedHash = leaf; + for (uint256 i = 0; i < proof.length; i++) { + computedHash = hasher(computedHash, proof[i]); + } + return computedHash; + } + /** * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}. * + * This version handles multiproofs in memory with the default hashing function. + * * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details. */ function multiProofVerify( @@ -83,7 +179,151 @@ library MerkleProof { } /** - * @dev Calldata version of {multiProofVerify} + * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction + * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another + * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false + * respectively. + * + * This version handles multiproofs in memory with the default hashing function. + * + * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree + * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the + * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer). + */ + function processMultiProof( + bytes32[] memory proof, + bool[] memory proofFlags, + bytes32[] memory leaves + ) internal pure returns (bytes32 merkleRoot) { + // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by + // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the + // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of + // the Merkle tree. + uint256 leavesLen = leaves.length; + + // Check proof validity. + if (leavesLen + proof.length != proofFlags.length + 1) { + revert MerkleProofInvalidMultiproof(); + } + + // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using + // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop". + bytes32[] memory hashes = new bytes32[](proofFlags.length); + uint256 leafPos = 0; + uint256 hashPos = 0; + uint256 proofPos = 0; + // At each step, we compute the next hash using two values: + // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we + // get the next hash. + // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the + // `proof` array. + for (uint256 i = 0; i < proofFlags.length; i++) { + bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]; + bytes32 b = proofFlags[i] + ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) + : proof[proofPos++]; + hashes[i] = Hashes.commutativeKeccak256(a, b); + } + + if (proofFlags.length > 0) { + if (proofPos != proof.length) { + revert MerkleProofInvalidMultiproof(); + } + unchecked { + return hashes[proofFlags.length - 1]; + } + } else if (leavesLen > 0) { + return leaves[0]; + } else { + return proof[0]; + } + } + + /** + * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by + * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}. + * + * This version handles multiproofs in memory with a custom hashing function. + * + * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details. + */ + function multiProofVerify( + bytes32[] memory proof, + bool[] memory proofFlags, + bytes32 root, + bytes32[] memory leaves, + function(bytes32, bytes32) view returns (bytes32) hasher + ) internal view returns (bool) { + return processMultiProof(proof, proofFlags, leaves, hasher) == root; + } + + /** + * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction + * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another + * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false + * respectively. + * + * This version handles multiproofs in memory with a custom hashing function. + * + * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree + * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the + * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer). + */ + function processMultiProof( + bytes32[] memory proof, + bool[] memory proofFlags, + bytes32[] memory leaves, + function(bytes32, bytes32) view returns (bytes32) hasher + ) internal view returns (bytes32 merkleRoot) { + // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by + // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the + // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of + // the Merkle tree. + uint256 leavesLen = leaves.length; + + // Check proof validity. + if (leavesLen + proof.length != proofFlags.length + 1) { + revert MerkleProofInvalidMultiproof(); + } + + // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using + // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop". + bytes32[] memory hashes = new bytes32[](proofFlags.length); + uint256 leafPos = 0; + uint256 hashPos = 0; + uint256 proofPos = 0; + // At each step, we compute the next hash using two values: + // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we + // get the next hash. + // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the + // `proof` array. + for (uint256 i = 0; i < proofFlags.length; i++) { + bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]; + bytes32 b = proofFlags[i] + ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) + : proof[proofPos++]; + hashes[i] = hasher(a, b); + } + + if (proofFlags.length > 0) { + if (proofPos != proof.length) { + revert MerkleProofInvalidMultiproof(); + } + unchecked { + return hashes[proofFlags.length - 1]; + } + } else if (leavesLen > 0) { + return leaves[0]; + } else { + return proof[0]; + } + } + + /** + * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by + * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}. + * + * This version handles multiproofs in calldata with the default hashing function. * * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details. */ @@ -91,9 +331,9 @@ library MerkleProof { bytes32[] calldata proof, bool[] calldata proofFlags, bytes32 root, - bytes32[] memory leaves + bytes32[] calldata leaves ) internal pure returns (bool) { - return processMultiProofCalldata(proof, proofFlags, leaves) == root; + return processMultiProof(proof, proofFlags, leaves) == root; } /** @@ -102,31 +342,31 @@ library MerkleProof { * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false * respectively. * + * This version handles multiproofs in calldata with the default hashing function. + * * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer). */ - function processMultiProof( - bytes32[] memory proof, - bool[] memory proofFlags, - bytes32[] memory leaves + function processMultiProofCalldata( + bytes32[] calldata proof, + bool[] calldata proofFlags, + bytes32[] calldata leaves ) internal pure returns (bytes32 merkleRoot) { // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of // the Merkle tree. uint256 leavesLen = leaves.length; - uint256 proofLen = proof.length; - uint256 totalHashes = proofFlags.length; // Check proof validity. - if (leavesLen + proofLen != totalHashes + 1) { + if (leavesLen + proof.length != proofFlags.length + 1) { revert MerkleProofInvalidMultiproof(); } // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop". - bytes32[] memory hashes = new bytes32[](totalHashes); + bytes32[] memory hashes = new bytes32[](proofFlags.length); uint256 leafPos = 0; uint256 hashPos = 0; uint256 proofPos = 0; @@ -135,7 +375,7 @@ library MerkleProof { // get the next hash. // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the // `proof` array. - for (uint256 i = 0; i < totalHashes; i++) { + for (uint256 i = 0; i < proofFlags.length; i++) { bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]; bytes32 b = proofFlags[i] ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) @@ -143,12 +383,12 @@ library MerkleProof { hashes[i] = Hashes.commutativeKeccak256(a, b); } - if (totalHashes > 0) { - if (proofPos != proofLen) { + if (proofFlags.length > 0) { + if (proofPos != proof.length) { revert MerkleProofInvalidMultiproof(); } unchecked { - return hashes[totalHashes - 1]; + return hashes[proofFlags.length - 1]; } } else if (leavesLen > 0) { return leaves[0]; @@ -158,31 +398,55 @@ library MerkleProof { } /** - * @dev Calldata version of {processMultiProof}. + * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by + * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}. + * + * This version handles multiproofs in calldata with a custom hashing function. * * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details. */ + function multiProofVerifyCalldata( + bytes32[] calldata proof, + bool[] calldata proofFlags, + bytes32 root, + bytes32[] calldata leaves, + function(bytes32, bytes32) view returns (bytes32) hasher + ) internal view returns (bool) { + return processMultiProof(proof, proofFlags, leaves, hasher) == root; + } + + /** + * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction + * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another + * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false + * respectively. + * + * This version handles multiproofs in calldata with a custom hashing function. + * + * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree + * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the + * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer). + */ function processMultiProofCalldata( bytes32[] calldata proof, bool[] calldata proofFlags, - bytes32[] memory leaves - ) internal pure returns (bytes32 merkleRoot) { + bytes32[] calldata leaves, + function(bytes32, bytes32) view returns (bytes32) hasher + ) internal view returns (bytes32 merkleRoot) { // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of // the Merkle tree. uint256 leavesLen = leaves.length; - uint256 proofLen = proof.length; - uint256 totalHashes = proofFlags.length; // Check proof validity. - if (leavesLen + proofLen != totalHashes + 1) { + if (leavesLen + proof.length != proofFlags.length + 1) { revert MerkleProofInvalidMultiproof(); } // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop". - bytes32[] memory hashes = new bytes32[](totalHashes); + bytes32[] memory hashes = new bytes32[](proofFlags.length); uint256 leafPos = 0; uint256 hashPos = 0; uint256 proofPos = 0; @@ -191,20 +455,20 @@ library MerkleProof { // get the next hash. // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the // `proof` array. - for (uint256 i = 0; i < totalHashes; i++) { + for (uint256 i = 0; i < proofFlags.length; i++) { bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]; bytes32 b = proofFlags[i] ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) : proof[proofPos++]; - hashes[i] = Hashes.commutativeKeccak256(a, b); + hashes[i] = hasher(a, b); } - if (totalHashes > 0) { - if (proofPos != proofLen) { + if (proofFlags.length > 0) { + if (proofPos != proof.length) { revert MerkleProofInvalidMultiproof(); } unchecked { - return hashes[totalHashes - 1]; + return hashes[proofFlags.length - 1]; } } else if (leavesLen > 0) { return leaves[0]; diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 2fb99402594..93f59ace585 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -19,6 +19,10 @@ import {Panic} from "../Panic.sol"; * * Zero value: The value that represents an empty leaf. Used to avoid regular zero values to be part of the tree. * * Hashing function: A cryptographic hash function used to produce internal nodes. Defaults to {Hashes-commutativeKeccak256}. * + * NOTE: Building trees using non-commutative hashing functions (i.e. `H(a, b) != H(b, a)`) is supported. However, + * proving the inclusion of a leaf in such trees is not possible with the {MerkleProof} library since it only supports + * _commutative_ hashing functions. + * * _Available since v5.1._ */ library MerkleTree { diff --git a/package-lock.json b/package-lock.json index db7e9efe085..8b2cbc1737d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@nomicfoundation/hardhat-ethers": "^3.0.4", "@nomicfoundation/hardhat-network-helpers": "^1.0.3", "@openzeppelin/docs-utils": "^0.1.5", - "@openzeppelin/merkle-tree": "^1.0.6", + "@openzeppelin/merkle-tree": "^1.0.7", "@openzeppelin/upgrade-safe-transpiler": "^0.3.32", "@openzeppelin/upgrades-core": "^1.20.6", "chai": "^4.2.0", @@ -1990,70 +1990,15 @@ } }, "node_modules/@openzeppelin/merkle-tree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@openzeppelin/merkle-tree/-/merkle-tree-1.0.6.tgz", - "integrity": "sha512-cGWOb2WBWbJhqvupzxjnKAwGLxxAEYPg51sk76yZ5nVe5D03mw7Vx5yo8llaIEqYhP5O39M8QlrNWclgLfKVrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@openzeppelin/merkle-tree/-/merkle-tree-1.0.7.tgz", + "integrity": "sha512-i93t0YYv6ZxTCYU3CdO5Q+DXK0JH10A4dCBOMlzYbX+ujTXm+k1lXiEyVqmf94t3sqmv8sm/XT5zTa0+efnPgQ==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.7.0", - "ethereum-cryptography": "^1.1.2" - } - }, - "node_modules/@openzeppelin/merkle-tree/node_modules/@noble/hashes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", - "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, - "node_modules/@openzeppelin/merkle-tree/node_modules/@scure/bip32": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", - "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "@noble/hashes": "~1.2.0", - "@noble/secp256k1": "~1.7.0", - "@scure/base": "~1.1.0" - } - }, - "node_modules/@openzeppelin/merkle-tree/node_modules/@scure/bip39": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", - "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "@noble/hashes": "~1.2.0", - "@scure/base": "~1.1.0" - } - }, - "node_modules/@openzeppelin/merkle-tree/node_modules/ethereum-cryptography": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", - "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", - "dev": true, - "dependencies": { - "@noble/hashes": "1.2.0", - "@noble/secp256k1": "1.7.1", - "@scure/bip32": "1.1.5", - "@scure/bip39": "1.1.1" + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0" } }, "node_modules/@openzeppelin/upgrade-safe-transpiler": { diff --git a/package.json b/package.json index f5ab048374c..d55d1a45ef0 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@nomicfoundation/hardhat-ethers": "^3.0.4", "@nomicfoundation/hardhat-network-helpers": "^1.0.3", "@openzeppelin/docs-utils": "^0.1.5", - "@openzeppelin/merkle-tree": "^1.0.6", + "@openzeppelin/merkle-tree": "^1.0.7", "@openzeppelin/upgrade-safe-transpiler": "^0.3.32", "@openzeppelin/upgrades-core": "^1.20.6", "chai": "^4.2.0", diff --git a/scripts/generate/run.js b/scripts/generate/run.js index f8ec17606ac..b3215ad1133 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -32,6 +32,7 @@ function generateFromTemplate(file, template, outputPrefix = '') { // Contracts for (const [file, template] of Object.entries({ + 'utils/cryptography/MerkleProof.sol': './templates/MerkleProof.js', 'utils/math/SafeCast.sol': './templates/SafeCast.js', 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', diff --git a/scripts/generate/templates/MerkleProof.js b/scripts/generate/templates/MerkleProof.js new file mode 100644 index 00000000000..768a981ee6b --- /dev/null +++ b/scripts/generate/templates/MerkleProof.js @@ -0,0 +1,178 @@ +const format = require('../format-lines'); +const { OPTS } = require('./MerkleProof.opts'); + +const DEFAULT_HASH = 'Hashes.commutativeKeccak256'; + +const formatArgsSingleLine = (...args) => args.filter(Boolean).join(', '); +const formatArgsMultiline = (...args) => '\n' + format(args.filter(Boolean).join(',\0').split('\0')); + +// TEMPLATE +const header = `\ +pragma solidity ^0.8.20; + +import {Hashes} from "./Hashes.sol"; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the Merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates Merkle trees that are safe + * against this attack out of the box. + * + * NOTE: This library supports proof verification for merkle trees built using + * custom _commutative_ hashing functions (i.e. \`H(a, b) == H(b, a)\`). Proving + * leaf inclusion in trees built using non-commutative hashing functions requires + * additional logic that is not supported by this library. + */ +`; + +const errors = `\ +/** + *@dev The multiproof provided is not valid. + */ +error MerkleProofInvalidMultiproof(); +`; + +/* eslint-disable max-len */ +const templateProof = ({ suffix, location, visibility, hash }) => `\ +/** + * @dev Returns true if a \`leaf\` can be proved to be a part of a Merkle tree + * defined by \`root\`. For this, a \`proof\` must be provided, containing + * sibling hashes on the branch from the leaf to the root of the tree. Each + * pair of leaves and each pair of pre-images are assumed to be sorted. + * + * This version handles proofs in ${location} with ${hash ? 'a custom' : 'the default'} hashing function. + */ +function verify${suffix}(${(hash ? formatArgsMultiline : formatArgsSingleLine)( + `bytes32[] ${location} proof`, + 'bytes32 root', + 'bytes32 leaf', + hash && `function(bytes32, bytes32) view returns (bytes32) ${hash}`, +)}) internal ${visibility} returns (bool) { + return processProof(proof, leaf${hash ? `, ${hash}` : ''}) == root; +} + +/** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from \`leaf\` using \`proof\`. A \`proof\` is valid if and only if the rebuilt + * hash matches the root of the tree. When processing the proof, the pairs + * of leafs & pre-images are assumed to be sorted. + * + * This version handles proofs in ${location} with ${hash ? 'a custom' : 'the default'} hashing function. + */ +function processProof${suffix}(${(hash ? formatArgsMultiline : formatArgsSingleLine)( + `bytes32[] ${location} proof`, + 'bytes32 leaf', + hash && `function(bytes32, bytes32) view returns (bytes32) ${hash}`, +)}) internal ${visibility} returns (bytes32) { + bytes32 computedHash = leaf; + for (uint256 i = 0; i < proof.length; i++) { + computedHash = ${hash ?? DEFAULT_HASH}(computedHash, proof[i]); + } + return computedHash; +} +`; + +const templateMultiProof = ({ suffix, location, visibility, hash }) => `\ +/** + * @dev Returns true if the \`leaves\` can be simultaneously proven to be a part of a Merkle tree defined by + * \`root\`, according to \`proof\` and \`proofFlags\` as described in {processMultiProof}. + * + * This version handles multiproofs in ${location} with ${hash ? 'a custom' : 'the default'} hashing function. + * + * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details. + */ +function multiProofVerify${suffix}(${formatArgsMultiline( + `bytes32[] ${location} proof`, + `bool[] ${location} proofFlags`, + 'bytes32 root', + `bytes32[] ${location} leaves`, + hash && `function(bytes32, bytes32) view returns (bytes32) ${hash}`, +)}) internal ${visibility} returns (bool) { + return processMultiProof(proof, proofFlags, leaves${hash ? `, ${hash}` : ''}) == root; +} + +/** + * @dev Returns the root of a tree reconstructed from \`leaves\` and sibling nodes in \`proof\`. The reconstruction + * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another + * leaf/inner node or a proof sibling node, depending on whether each \`proofFlags\` item is true or false + * respectively. + * + * This version handles multiproofs in ${location} with ${hash ? 'a custom' : 'the default'} hashing function. + * + * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree + * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the + * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer). + */ +function processMultiProof${suffix}(${formatArgsMultiline( + `bytes32[] ${location} proof`, + `bool[] ${location} proofFlags`, + `bytes32[] ${location} leaves`, + hash && `function(bytes32, bytes32) view returns (bytes32) ${hash}`, +)}) internal ${visibility} returns (bytes32 merkleRoot) { + // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by + // consuming and producing values on a queue. The queue starts with the \`leaves\` array, then goes onto the + // \`hashes\` array. At the end of the process, the last hash in the \`hashes\` array should contain the root of + // the Merkle tree. + uint256 leavesLen = leaves.length; + + // Check proof validity. + if (leavesLen + proof.length != proofFlags.length + 1) { + revert MerkleProofInvalidMultiproof(); + } + + // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using + // \`xxx[xxxPos++]\`, which return the current value and increment the pointer, thus mimicking a queue's "pop". + bytes32[] memory hashes = new bytes32[](proofFlags.length); + uint256 leafPos = 0; + uint256 hashPos = 0; + uint256 proofPos = 0; + // At each step, we compute the next hash using two values: + // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we + // get the next hash. + // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the + // \`proof\` array. + for (uint256 i = 0; i < proofFlags.length; i++) { + bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]; + bytes32 b = proofFlags[i] + ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) + : proof[proofPos++]; + hashes[i] = ${hash ?? DEFAULT_HASH}(a, b); + } + + if (proofFlags.length > 0) { + if (proofPos != proof.length) { + revert MerkleProofInvalidMultiproof(); + } + unchecked { + return hashes[proofFlags.length - 1]; + } + } else if (leavesLen > 0) { + return leaves[0]; + } else { + return proof[0]; + } +} +`; +/* eslint-enable max-len */ + +// GENERATE +module.exports = format( + header.trimEnd(), + 'library MerkleProof {', + format( + [].concat( + errors, + OPTS.flatMap(opts => templateProof(opts)), + OPTS.flatMap(opts => templateMultiProof(opts)), + ), + ).trimEnd(), + '}', +); diff --git a/scripts/generate/templates/MerkleProof.opts.js b/scripts/generate/templates/MerkleProof.opts.js new file mode 100644 index 00000000000..911f2392257 --- /dev/null +++ b/scripts/generate/templates/MerkleProof.opts.js @@ -0,0 +1,11 @@ +const { product } = require('../../helpers'); + +const OPTS = product( + [ + { suffix: '', location: 'memory' }, + { suffix: 'Calldata', location: 'calldata' }, + ], + [{ visibility: 'pure' }, { visibility: 'view', hash: 'hasher' }], +).map(objs => Object.assign({}, ...objs)); + +module.exports = { OPTS }; diff --git a/test/utils/cryptography/MerkleProof.test.js b/test/utils/cryptography/MerkleProof.test.js index 9d5502d5981..93ee964a860 100644 --- a/test/utils/cryptography/MerkleProof.test.js +++ b/test/utils/cryptography/MerkleProof.test.js @@ -1,173 +1,213 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { StandardMerkleTree } = require('@openzeppelin/merkle-tree'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); +const { SimpleMerkleTree } = require('@openzeppelin/merkle-tree'); -const toElements = str => str.split('').map(e => [e]); -const hashPair = (a, b) => ethers.keccak256(Buffer.concat([a, b].sort(Buffer.compare))); - -async function fixture() { - const mock = await ethers.deployContract('$MerkleProof'); - return { mock }; -} +// generate bytes32 leaves from a string +const toLeaves = (str, separator = '') => str.split(separator).map(e => ethers.keccak256(ethers.toUtf8Bytes(e))); +// internal node hashes +const concatSorted = (...elements) => Buffer.concat(elements.map(ethers.getBytes).sort(Buffer.compare)); +const defaultHash = (a, b) => ethers.keccak256(concatSorted(a, b)); +const customHash = (a, b) => ethers.sha256(concatSorted(a, b)); describe('MerkleProof', function () { - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - describe('verify', function () { - it('returns true for a valid Merkle proof', async function () { - const merkleTree = StandardMerkleTree.of( - toElements('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='), - ['string'], - ); - - const root = merkleTree.root; - const hash = merkleTree.leafHash(['A']); - const proof = merkleTree.getProof(['A']); - - expect(await this.mock.$verify(proof, root, hash)).to.be.true; - expect(await this.mock.$verifyCalldata(proof, root, hash)).to.be.true; - - // For demonstration, it is also possible to create valid proofs for certain 64-byte values *not* in elements: - const noSuchLeaf = hashPair( - ethers.toBeArray(merkleTree.leafHash(['A'])), - ethers.toBeArray(merkleTree.leafHash(['B'])), - ); - expect(await this.mock.$verify(proof.slice(1), root, noSuchLeaf)).to.be.true; - expect(await this.mock.$verifyCalldata(proof.slice(1), root, noSuchLeaf)).to.be.true; - }); - - it('returns false for an invalid Merkle proof', async function () { - const correctMerkleTree = StandardMerkleTree.of(toElements('abc'), ['string']); - const otherMerkleTree = StandardMerkleTree.of(toElements('def'), ['string']); - - const root = correctMerkleTree.root; - const hash = correctMerkleTree.leafHash(['a']); - const proof = otherMerkleTree.getProof(['d']); - - expect(await this.mock.$verify(proof, root, hash)).to.be.false; - expect(await this.mock.$verifyCalldata(proof, root, hash)).to.be.false; - }); - - it('returns false for a Merkle proof of invalid length', async function () { - const merkleTree = StandardMerkleTree.of(toElements('abc'), ['string']); - - const root = merkleTree.root; - const hash = merkleTree.leafHash(['a']); - const proof = merkleTree.getProof(['a']); - const badProof = proof.slice(0, -1); - - expect(await this.mock.$verify(badProof, root, hash)).to.be.false; - expect(await this.mock.$verifyCalldata(badProof, root, hash)).to.be.false; - }); - }); - - describe('multiProofVerify', function () { - it('returns true for a valid Merkle multi proof', async function () { - const merkleTree = StandardMerkleTree.of(toElements('abcdef'), ['string']); - - const root = merkleTree.root; - const { proof, proofFlags, leaves } = merkleTree.getMultiProof(toElements('bdf')); - const hashes = leaves.map(e => merkleTree.leafHash(e)); - - expect(await this.mock.$multiProofVerify(proof, proofFlags, root, hashes)).to.be.true; - expect(await this.mock.$multiProofVerifyCalldata(proof, proofFlags, root, hashes)).to.be.true; - }); - - it('returns false for an invalid Merkle multi proof', async function () { - const merkleTree = StandardMerkleTree.of(toElements('abcdef'), ['string']); - const otherMerkleTree = StandardMerkleTree.of(toElements('ghi'), ['string']); - - const root = merkleTree.root; - const { proof, proofFlags, leaves } = otherMerkleTree.getMultiProof(toElements('ghi')); - const hashes = leaves.map(e => merkleTree.leafHash(e)); - - expect(await this.mock.$multiProofVerify(proof, proofFlags, root, hashes)).to.be.false; - expect(await this.mock.$multiProofVerifyCalldata(proof, proofFlags, root, hashes)).to.be.false; - }); - - it('revert with invalid multi proof #1', async function () { - const merkleTree = StandardMerkleTree.of(toElements('abcd'), ['string']); - - const root = merkleTree.root; - const hashA = merkleTree.leafHash(['a']); - const hashB = merkleTree.leafHash(['b']); - const hashCD = hashPair( - ethers.toBeArray(merkleTree.leafHash(['c'])), - ethers.toBeArray(merkleTree.leafHash(['d'])), - ); - const hashE = merkleTree.leafHash(['e']); // incorrect (not part of the tree) - const fill = ethers.randomBytes(32); - - await expect( - this.mock.$multiProofVerify([hashB, fill, hashCD], [false, false, false], root, [hashA, hashE]), - ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); - - await expect( - this.mock.$multiProofVerifyCalldata([hashB, fill, hashCD], [false, false, false], root, [hashA, hashE]), - ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); - }); - - it('revert with invalid multi proof #2', async function () { - const merkleTree = StandardMerkleTree.of(toElements('abcd'), ['string']); - - const root = merkleTree.root; - const hashA = merkleTree.leafHash(['a']); - const hashB = merkleTree.leafHash(['b']); - const hashCD = hashPair( - ethers.toBeArray(merkleTree.leafHash(['c'])), - ethers.toBeArray(merkleTree.leafHash(['d'])), - ); - const hashE = merkleTree.leafHash(['e']); // incorrect (not part of the tree) - const fill = ethers.randomBytes(32); - - await expect( - this.mock.$multiProofVerify([hashB, fill, hashCD], [false, false, false, false], root, [hashE, hashA]), - ).to.be.revertedWithPanic(0x32); - - await expect( - this.mock.$multiProofVerifyCalldata([hashB, fill, hashCD], [false, false, false, false], root, [hashE, hashA]), - ).to.be.revertedWithPanic(0x32); - }); - - it('limit case: works for tree containing a single leaf', async function () { - const merkleTree = StandardMerkleTree.of(toElements('a'), ['string']); - - const root = merkleTree.root; - const { proof, proofFlags, leaves } = merkleTree.getMultiProof(toElements('a')); - const hashes = leaves.map(e => merkleTree.leafHash(e)); - - expect(await this.mock.$multiProofVerify(proof, proofFlags, root, hashes)).to.be.true; - expect(await this.mock.$multiProofVerifyCalldata(proof, proofFlags, root, hashes)).to.be.true; - }); - - it('limit case: can prove empty leaves', async function () { - const merkleTree = StandardMerkleTree.of(toElements('abcd'), ['string']); - - const root = merkleTree.root; - expect(await this.mock.$multiProofVerify([root], [], root, [])).to.be.true; - expect(await this.mock.$multiProofVerifyCalldata([root], [], root, [])).to.be.true; - }); - - it('reverts processing manipulated proofs with a zero-value node at depth 1', async function () { - // Create a merkle tree that contains a zero leaf at depth 1 - const leave = ethers.id('real leaf'); - const root = hashPair(ethers.toBeArray(leave), Buffer.alloc(32, 0)); - - // Now we can pass any **malicious** fake leaves as valid! - const maliciousLeaves = ['malicious', 'leaves'].map(ethers.id).map(ethers.toBeArray).sort(Buffer.compare); - const maliciousProof = [leave, leave]; - const maliciousProofFlags = [true, true, false]; - - await expect( - this.mock.$multiProofVerify(maliciousProof, maliciousProofFlags, root, maliciousLeaves), - ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); - - await expect( - this.mock.$multiProofVerifyCalldata(maliciousProof, maliciousProofFlags, root, maliciousLeaves), - ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); + for (const { title, contractName, nodeHash } of [ + { title: 'default hash', contractName: '$MerkleProof', nodeHash: defaultHash }, + { title: 'custom hash', contractName: '$MerkleProofCustomHashMock', nodeHash: customHash }, + ]) { + describe(title, function () { + // stateless: no need for a fixture, just use before + before(async function () { + this.mock = await ethers.deployContract(contractName); + this.makeTree = str => SimpleMerkleTree.of(toLeaves(str), { nodeHash }); + }); + + describe('verify', function () { + it('returns true for a valid Merkle proof', async function () { + const merkleTree = this.makeTree('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='); + + const root = merkleTree.root; + const hash = merkleTree.at(0); + const proof = merkleTree.getProof(0); + + expect(await this.mock.$processProof(proof, hash)).to.equal(root); + expect(await this.mock.$processProofCalldata(proof, hash)).to.equal(root); + expect(await this.mock.$verify(proof, root, hash)).to.be.true; + expect(await this.mock.$verifyCalldata(proof, root, hash)).to.be.true; + + // For demonstration, it is also possible to create valid proofs for certain 64-byte values *not* in elements: + const noSuchLeaf = nodeHash(hash, proof.at(0)); + + expect(await this.mock.$processProof(proof.slice(1), noSuchLeaf)).to.equal(root); + expect(await this.mock.$processProofCalldata(proof.slice(1), noSuchLeaf)).to.equal(root); + expect(await this.mock.$verify(proof.slice(1), root, noSuchLeaf)).to.be.true; + expect(await this.mock.$verifyCalldata(proof.slice(1), root, noSuchLeaf)).to.be.true; + }); + + it('returns false for an invalid Merkle proof', async function () { + const correctMerkleTree = this.makeTree('abc'); + const otherMerkleTree = this.makeTree('def'); + + const root = correctMerkleTree.root; + const hash = correctMerkleTree.at(0); + const proof = otherMerkleTree.getProof(0); + + expect(await this.mock.$processProof(proof, hash)).to.not.equal(root); + expect(await this.mock.$processProofCalldata(proof, hash)).to.not.equal(root); + expect(await this.mock.$verify(proof, root, hash)).to.be.false; + expect(await this.mock.$verifyCalldata(proof, root, hash)).to.be.false; + }); + + it('returns false for a Merkle proof of invalid length', async function () { + const merkleTree = this.makeTree('abc'); + + const root = merkleTree.root; + const hash = merkleTree.at(0); + const proof = merkleTree.getProof(0); + const badProof = proof.slice(0, -1); + + expect(await this.mock.$processProof(badProof, hash)).to.not.equal(root); + expect(await this.mock.$processProofCalldata(badProof, hash)).to.not.equal(root); + expect(await this.mock.$verify(badProof, root, hash)).to.be.false; + expect(await this.mock.$verifyCalldata(badProof, root, hash)).to.be.false; + }); + }); + + describe('multiProofVerify', function () { + it('returns true for a valid Merkle multi proof', async function () { + const merkleTree = this.makeTree('abcdef'); + + const root = merkleTree.root; + const { proof, proofFlags, leaves } = merkleTree.getMultiProof(toLeaves('bdf')); + const hashes = leaves.map(e => merkleTree.leafHash(e)); + + expect(await this.mock.$processMultiProof(proof, proofFlags, hashes)).to.equal(root); + expect(await this.mock.$processMultiProofCalldata(proof, proofFlags, hashes)).to.equal(root); + expect(await this.mock.$multiProofVerify(proof, proofFlags, root, hashes)).to.be.true; + expect(await this.mock.$multiProofVerifyCalldata(proof, proofFlags, root, hashes)).to.be.true; + }); + + it('returns false for an invalid Merkle multi proof', async function () { + const merkleTree = this.makeTree('abcdef'); + const otherMerkleTree = this.makeTree('ghi'); + + const root = merkleTree.root; + const { proof, proofFlags, leaves } = otherMerkleTree.getMultiProof(toLeaves('ghi')); + const hashes = leaves.map(e => merkleTree.leafHash(e)); + + expect(await this.mock.$processMultiProof(proof, proofFlags, hashes)).to.not.equal(root); + expect(await this.mock.$processMultiProofCalldata(proof, proofFlags, hashes)).to.not.equal(root); + expect(await this.mock.$multiProofVerify(proof, proofFlags, root, hashes)).to.be.false; + expect(await this.mock.$multiProofVerifyCalldata(proof, proofFlags, root, hashes)).to.be.false; + }); + + it('revert with invalid multi proof #1', async function () { + const merkleTree = this.makeTree('abcd'); + + const root = merkleTree.root; + const hashA = merkleTree.at(0); + const hashB = merkleTree.at(1); + const hashCD = nodeHash(merkleTree.at(2), merkleTree.at(3)); + const hashE = ethers.randomBytes(32); // incorrect (not part of the tree) + const fill = ethers.randomBytes(32); + + await expect( + this.mock.$processMultiProof([hashB, fill, hashCD], [false, false, false], [hashA, hashE]), + ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); + + await expect( + this.mock.$processMultiProofCalldata([hashB, fill, hashCD], [false, false, false], [hashA, hashE]), + ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); + + await expect( + this.mock.$multiProofVerify([hashB, fill, hashCD], [false, false, false], root, [hashA, hashE]), + ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); + + await expect( + this.mock.$multiProofVerifyCalldata([hashB, fill, hashCD], [false, false, false], root, [hashA, hashE]), + ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); + }); + + it('revert with invalid multi proof #2', async function () { + const merkleTree = this.makeTree('abcd'); + + const root = merkleTree.root; + const hashA = merkleTree.at(0); + const hashB = merkleTree.at(1); + const hashCD = nodeHash(merkleTree.at(2), merkleTree.at(3)); + const hashE = ethers.randomBytes(32); // incorrect (not part of the tree) + const fill = ethers.randomBytes(32); + + await expect( + this.mock.$processMultiProof([hashB, fill, hashCD], [false, false, false, false], [hashE, hashA]), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + + await expect( + this.mock.$processMultiProofCalldata([hashB, fill, hashCD], [false, false, false, false], [hashE, hashA]), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + + await expect( + this.mock.$multiProofVerify([hashB, fill, hashCD], [false, false, false, false], root, [hashE, hashA]), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + + await expect( + this.mock.$multiProofVerifyCalldata([hashB, fill, hashCD], [false, false, false, false], root, [ + hashE, + hashA, + ]), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + + it('limit case: works for tree containing a single leaf', async function () { + const merkleTree = this.makeTree('a'); + + const root = merkleTree.root; + const { proof, proofFlags, leaves } = merkleTree.getMultiProof(toLeaves('a')); + const hashes = leaves.map(e => merkleTree.leafHash(e)); + + expect(await this.mock.$processMultiProof(proof, proofFlags, hashes)).to.equal(root); + expect(await this.mock.$processMultiProofCalldata(proof, proofFlags, hashes)).to.equal(root); + expect(await this.mock.$multiProofVerify(proof, proofFlags, root, hashes)).to.be.true; + expect(await this.mock.$multiProofVerifyCalldata(proof, proofFlags, root, hashes)).to.be.true; + }); + + it('limit case: can prove empty leaves', async function () { + const merkleTree = this.makeTree('abcd'); + + const root = merkleTree.root; + expect(await this.mock.$processMultiProof([root], [], [])).to.equal(root); + expect(await this.mock.$processMultiProofCalldata([root], [], [])).to.equal(root); + expect(await this.mock.$multiProofVerify([root], [], root, [])).to.be.true; + expect(await this.mock.$multiProofVerifyCalldata([root], [], root, [])).to.be.true; + }); + + it('reverts processing manipulated proofs with a zero-value node at depth 1', async function () { + // Create a merkle tree that contains a zero leaf at depth 1 + const leave = ethers.id('real leaf'); + const root = nodeHash(leave, ethers.ZeroHash); + + // Now we can pass any **malicious** fake leaves as valid! + const maliciousLeaves = ['malicious', 'leaves'].map(ethers.id).map(ethers.toBeArray).sort(Buffer.compare); + const maliciousProof = [leave, leave]; + const maliciousProofFlags = [true, true, false]; + + await expect( + this.mock.$processMultiProof(maliciousProof, maliciousProofFlags, maliciousLeaves), + ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); + + await expect( + this.mock.$processMultiProofCalldata(maliciousProof, maliciousProofFlags, maliciousLeaves), + ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); + + await expect( + this.mock.$multiProofVerify(maliciousProof, maliciousProofFlags, root, maliciousLeaves), + ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); + + await expect( + this.mock.$multiProofVerifyCalldata(maliciousProof, maliciousProofFlags, root, maliciousLeaves), + ).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof'); + }); + }); }); - }); + } });