ソースを参照

Process and verify merkle proofs (and multiproof) with custom hash function (#4887)

Co-authored-by: ernestognw <ernestognw@gmail.com>
Hadrien Croubois 1 年間 前
コミット
b73bcb231f

+ 5 - 0
.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.

+ 62 - 0
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);
+    }
+}

+ 297 - 33
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];

+ 4 - 0
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 {

+ 7 - 62
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": {

+ 1 - 1
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",

+ 1 - 0
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',

+ 178 - 0
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(),
+  '}',
+);

+ 11 - 0
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 };

+ 206 - 166
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');
+        });
+      });
     });
-  });
+  }
 });