-
Notifications
You must be signed in to change notification settings - Fork 12.4k
Process and verify merkle proofs (and multiproof) with custom hash function #4887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Amxx
merged 31 commits into
OpenZeppelin:master
from
Amxx:feature/cryptography/merkle-proof-custom-hash
Jul 15, 2024
Merged
Changes from 19 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
ffe08a4
add custm-hash versions of the MerkleProof functions
Amxx a45abe8
remove cache that causes stack too deep
Amxx 9bc06ea
fix lint
Amxx 2977574
more tests
Amxx 92399a3
natspec comments
Amxx f449987
procedurally generate MerkleProof
Amxx 725a75f
add changeset
Amxx d56dad6
Merge branch 'master' into feature/cryptography/merkle-proof-custom-hash
Amxx 408271a
codespell
Amxx d11d7f5
Merge branch 'master' into feature/cryptography/merkle-proof-custom-hash
ernestognw a7abb27
Fix procedural generation
ernestognw e49c0ac
Nits
ernestognw 55b390e
Merge branch 'master' into feature/cryptography/merkle-proof-custom-hash
ernestognw 2d87005
Merge branch 'master' into feature/cryptography/merkle-proof-custom-hash
Amxx 9c7ab66
linted generation
Amxx b32a118
up
Amxx ec01f3d
Fix tests
ernestognw c2ca934
remove one local variable to avoid stack too depth when calldata + cu…
Amxx 0b19ff7
remove one local variable to avoid stack too depth when calldata + cu…
Amxx 4ca51e9
add note about the use of non communative hashing functions
Amxx cc8d816
add note about the use of non communative hashing functions
Amxx c38e1aa
Merge branch 'master' into feature/cryptography/merkle-proof-custom-hash
ernestognw d437555
Improve coverage and address review comments
ernestognw a939a13
Nit
ernestognw 50f1ccb
update merkle-tree to @1.0.7
Amxx 640e2da
Merge branch 'feature/cryptography/merkle-proof-custom-hash-tests' in…
Amxx e3554ae
refactor tests
Amxx 75703a6
up
Amxx 0ea8d75
Apply suggestions from code review
Amxx f388e69
Update MerkleTree.sol
Amxx b7d56bb
Use sha256 for the MerkleProof custom hash tests
Amxx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 support verification of proof for merkle tree built using | ||
| * custom hashing functions. This is limited to commutative hashing functions. | ||
| * The verification of inclusion proofs for merkle tree build using non commutative | ||
| * hashing function require additional logic that this library does not provide. | ||
| */ | ||
| `; | ||
|
|
||
| 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(), | ||
| '}', | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }; |
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.