From 6b079eeb7b0b4d9b98a4abc71379e862eeca7458 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 14 Aug 2022 15:48:27 +0200 Subject: [PATCH 01/48] Add a (complete) MerkleTree structure --- contracts/mocks/MerkleTreeMock.sol | 56 +++++++++ contracts/utils/structs/MerkleTree.sol | 152 +++++++++++++++++++++++++ test/utils/structs/Merkletree.test.js | 79 +++++++++++++ 3 files changed, 287 insertions(+) create mode 100644 contracts/mocks/MerkleTreeMock.sol create mode 100644 contracts/utils/structs/MerkleTree.sol create mode 100644 test/utils/structs/Merkletree.test.js diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol new file mode 100644 index 00000000000..f5db1d951f6 --- /dev/null +++ b/contracts/mocks/MerkleTreeMock.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/structs/MerkleTree.sol"; + +contract MerkleTreeMock { + using MerkleTree for MerkleTree.TreeWithHistory; + + MerkleTree.TreeWithHistory private tree; + + constructor(uint32 _depth, uint32 _length) { + tree.initialize(_depth, _length); + } + + function insert(bytes32 leaf) public returns (uint32) { + return tree.insert(leaf); + } + + function getLastRoot() public view returns (bytes32) { + return tree.getLastRoot(); + } + + function isKnownRoot(bytes32 root) public view returns (bool) { + return tree.isKnownRoot(root); + } + + // internal state + function depth() public view returns (uint32) { + return tree.depth; + } + + function length() public view returns (uint32) { + return tree.length; + } + + function currentRootIndex() public view returns (uint32) { + return tree.currentRootIndex; + } + + function nextLeafIndex() public view returns (uint32) { + return tree.nextLeafIndex; + } + + function filledSubtrees(uint256 i) public view returns (bytes32) { + return tree.filledSubtrees[i]; + } + + function zeros(uint256 i) public view returns (bytes32) { + return tree.zeros[i]; + } + + function roots(uint256 i) public view returns (bytes32) { + return tree.roots[i]; + } +} diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol new file mode 100644 index 00000000000..ae6145aa9dd --- /dev/null +++ b/contracts/utils/structs/MerkleTree.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +error Full(); + +library MerkleTree { + uint8 private constant MAX_DEPTH = 32; + + struct TreeWithHistory { + function(bytes32, bytes32) view returns (bytes32) fnHash; + uint32 depth; + uint32 length; + uint32 currentRootIndex; + uint32 nextLeafIndex; + bytes32[MAX_DEPTH] filledSubtrees; + bytes32[MAX_DEPTH] zeros; + bytes32[2**MAX_DEPTH] roots; + } + + /** + * @dev Initialize a new complete MerkleTree defined by: + * - Depth `depth` + * - All leaves are initialize to `zero` + * - Hashing function for a pair of leaves is fnHash + * and keep a root history of length `length` when leaves are inserted. + */ + function initialize( + TreeWithHistory storage self, + uint32 depth, + uint32 length, + bytes32 zero, + function(bytes32, bytes32) view returns (bytes32) fnHash + ) internal { + require(depth <= MAX_DEPTH); + + self.depth = depth; + self.length = length; + self.fnHash = fnHash; + + bytes32 currentZero = zero; + for (uint32 i = 0; i < depth; ++i) { + self.zeros[i] = self.filledSubtrees[i] = currentZero; + currentZero = fnHash(currentZero, currentZero); + } + + // Insert the first root + self.roots[0] = currentZero; + } + + /** + * @dev Insert a new leaf in the tree, compute the new root, and store that new root in the history. + * + * For depth < 32, reverts if the MerkleTree is already full. + * For depth = 32, reverts when trying to populate the last leaf (nextLeafIndex increment overflow). + * + * Said differently: + * `2 ** depth` entries can be inserted into trees with depth < 32. + * `2 ** depth - 1` entries can be inserted into trees with depth = 32. + */ + function insert(TreeWithHistory storage self, bytes32 leaf) internal returns (uint32) { + // cache read + uint32 depth = self.depth; + + // Get leaf index + uint32 leafIndex = self.nextLeafIndex++; + + // Check if tree is full. + if (leafIndex == 1 << depth) revert Full(); + + // Rebuild branch from leaf to root + uint32 currentIndex = leafIndex; + bytes32 currentLevelHash = leaf; + for (uint32 i = 0; i < depth; i++) { + // Reaching the parent node, is currentLevelHash the left child? + bool isLeft = currentIndex % 2 == 0; + + // If so, next time we will come from the right, so we need to save it + if (isLeft) { + self.filledSubtrees[i] = currentLevelHash; + } + + // Compute the node hash by hasing the current hash with either: + // - the last value for this level + // - the zero for this level + currentLevelHash = self.fnHash( + isLeft ? currentLevelHash : self.filledSubtrees[i], + isLeft ? self.zeros[i] : currentLevelHash + ); + + // update node index + currentIndex >>= 1; + } + + // Record new root + self.currentRootIndex = (self.currentRootIndex + 1) % self.length; + self.roots[self.currentRootIndex] = currentLevelHash; + + return leafIndex; + } + + /** + * @dev Return the current root of the tree. + */ + function getLastRoot(TreeWithHistory storage self) internal view returns (bytes32) { + return self.roots[self.currentRootIndex]; + } + + /** + * @dev Look in root history, + */ + function isKnownRoot(TreeWithHistory storage self, bytes32 root) internal view returns (bool) { + if (root == 0) { + return false; + } + + // cache as uint256 (avoid overflow) + uint256 currentRootIndex = self.currentRootIndex; + uint256 length = self.length; + + // search + for (uint256 i = length; i > 0; --i) { + if (root == self.roots[(currentRootIndex + i) % length]) { + return true; + } + } + + return false; + } + + // Default hash + function initialize( + TreeWithHistory storage self, + uint32 depth, + uint32 length + ) internal { + return initialize(self, depth, length, bytes32(0), _hashPair); + } + + function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { + return a < b ? _efficientHash(a, b) : _efficientHash(b, a); + } + + function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, a) + mstore(0x20, b) + value := keccak256(0x00, 0x40) + } + } +} diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js new file mode 100644 index 00000000000..f813c6d3e12 --- /dev/null +++ b/test/utils/structs/Merkletree.test.js @@ -0,0 +1,79 @@ +const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { MerkleTree } = require('merkletreejs'); +const keccak256 = require('keccak256'); +const { expect } = require('chai'); + +const MerkleTreeMock = artifacts.require('MerkleTreeMock'); + +describe('Merklee tree', function () { + const DEPTH = new BN(4); + const LENGTH = new BN(10); + + beforeEach(async function () { + this.contract = await MerkleTreeMock.new(DEPTH, LENGTH); + }); + + it('setup', async function () { + const leafs = Array(2 ** DEPTH).fill(constants.ZERO_BYTES32); + const merkleTree = new MerkleTree(leafs, keccak256, { sortPairs: true }); + + expect(await this.contract.depth()).to.be.bignumber.equal(DEPTH); + expect(await this.contract.length()).to.be.bignumber.equal(LENGTH); + expect(await this.contract.currentRootIndex()).to.be.bignumber.equal('0'); + expect(await this.contract.nextLeafIndex()).to.be.bignumber.equal('0'); + + expect(await this.contract.getLastRoot()).to.be.equal(merkleTree.getHexRoot()); + for (let i = 0; i < DEPTH; ++i) { + expect(await this.contract.zeros(i)).to.be.equal(merkleTree.getHexLayers()[i][0]); + expect(await this.contract.filledSubtrees(i)).to.be.equal(merkleTree.getHexLayers()[i][0]); + } + + for (let i = 0; i < LENGTH; ++i) { + expect(await this.contract.roots(i)).to.be.equal(i === 0 ? merkleTree.getHexRoot() : constants.ZERO_BYTES32); + } + + expect(await this.contract.isKnownRoot(merkleTree.getHexRoot())).to.be.equal(true); + expect(await this.contract.isKnownRoot(constants.ZERO_BYTES32)).to.be.equal(false); + }); + + describe('insert', function () { + it('tree is correctly updated', async function () { + const leafs = Array(2 ** DEPTH).fill(constants.ZERO_BYTES32); + const roots = []; + + // for each entry + for (const i of Object.keys(leafs).map(Number)) { + // generate random leaf + leafs[i] = web3.utils.randomHex(32); + const merkleTree = new MerkleTree(leafs, keccak256, { sortPairs: true }); + + // insert leaf + await this.contract.insert(leafs[i]); + + // check tree + expect(await this.contract.currentRootIndex()).to.be.bignumber.equal(((i + 1) % LENGTH).toString()); + expect(await this.contract.nextLeafIndex()).to.be.bignumber.equal((i + 1).toString()); + expect(await this.contract.getLastRoot()).to.be.equal(merkleTree.getHexRoot()); + + // check root history + roots.push(merkleTree.getHexRoot()); + for (const root of roots.slice(0, -LENGTH)) { + expect(await this.contract.isKnownRoot(root)).to.be.equal(false); + } + for (const root of roots.slice(-LENGTH)) { + expect(await this.contract.isKnownRoot(root)).to.be.equal(true); + } + } + }); + + it('revert when tree is full', async function () { + for (let i = 0; i < 2 ** DEPTH; ++i) { + await this.contract.insert(constants.ZERO_BYTES32); + } + await expectRevert( + this.contract.insert(constants.ZERO_BYTES32), + 'Full()', + ); + }); + }); +}); From 42c695b53e9de02835b62c1c713a1103ca90f7ea Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 14 Aug 2022 22:18:06 +0200 Subject: [PATCH 02/48] optimize array access & remove depth/length constrains --- contracts/mocks/MerkleTreeMock.sol | 24 +++--- contracts/utils/structs/MerkleTree.sol | 104 ++++++++++++++++--------- test/utils/structs/Merkletree.test.js | 4 +- 3 files changed, 83 insertions(+), 49 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index f5db1d951f6..f1c13075450 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -9,14 +9,22 @@ contract MerkleTreeMock { MerkleTree.TreeWithHistory private tree; - constructor(uint32 _depth, uint32 _length) { + constructor(uint256 _depth, uint256 _length) { tree.initialize(_depth, _length); } - function insert(bytes32 leaf) public returns (uint32) { + function insert(bytes32 leaf) public returns (uint256) { return tree.insert(leaf); } + function getDepth() public view returns (uint256) { + return tree.getDepth(); + } + + function getLength() public view returns (uint256) { + return tree.getLength(); + } + function getLastRoot() public view returns (bytes32) { return tree.getLastRoot(); } @@ -26,19 +34,11 @@ contract MerkleTreeMock { } // internal state - function depth() public view returns (uint32) { - return tree.depth; - } - - function length() public view returns (uint32) { - return tree.length; - } - - function currentRootIndex() public view returns (uint32) { + function currentRootIndex() public view returns (uint256) { return tree.currentRootIndex; } - function nextLeafIndex() public view returns (uint32) { + function nextLeafIndex() public view returns (uint256) { return tree.nextLeafIndex; } diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index ae6145aa9dd..81e1f465667 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -2,20 +2,21 @@ pragma solidity ^0.8.0; +// TODO: replace this import in favor or "../Array.sol" when #3589 is merged +import "../StorageSlot.sol"; + error Full(); library MerkleTree { - uint8 private constant MAX_DEPTH = 32; + uint256 private constant MAX_DEPTH = 256; struct TreeWithHistory { + uint256 currentRootIndex; + uint256 nextLeafIndex; + bytes32[] filledSubtrees; + bytes32[] zeros; + bytes32[] roots; function(bytes32, bytes32) view returns (bytes32) fnHash; - uint32 depth; - uint32 length; - uint32 currentRootIndex; - uint32 nextLeafIndex; - bytes32[MAX_DEPTH] filledSubtrees; - bytes32[MAX_DEPTH] zeros; - bytes32[2**MAX_DEPTH] roots; } /** @@ -27,49 +28,48 @@ library MerkleTree { */ function initialize( TreeWithHistory storage self, - uint32 depth, - uint32 length, + uint256 depth, + uint256 length, bytes32 zero, function(bytes32, bytes32) view returns (bytes32) fnHash ) internal { require(depth <= MAX_DEPTH); - self.depth = depth; - self.length = length; + setLength(self.filledSubtrees, depth); + setLength(self.zeros, depth); + setLength(self.roots, length); self.fnHash = fnHash; bytes32 currentZero = zero; for (uint32 i = 0; i < depth; ++i) { - self.zeros[i] = self.filledSubtrees[i] = currentZero; + unsafeAccess(self.zeros, i).value = currentZero; + unsafeAccess(self.filledSubtrees, i).value = currentZero; currentZero = fnHash(currentZero, currentZero); } // Insert the first root - self.roots[0] = currentZero; + unsafeAccess(self.roots, 0).value = currentZero; } /** * @dev Insert a new leaf in the tree, compute the new root, and store that new root in the history. * - * For depth < 32, reverts if the MerkleTree is already full. - * For depth = 32, reverts when trying to populate the last leaf (nextLeafIndex increment overflow). - * - * Said differently: - * `2 ** depth` entries can be inserted into trees with depth < 32. - * `2 ** depth - 1` entries can be inserted into trees with depth = 32. + * Note: because of the `nextLeafIndex` overflow, trees of depth 256 can only store 2**256-1 items. + * This should never be an issue in practice. */ - function insert(TreeWithHistory storage self, bytes32 leaf) internal returns (uint32) { + function insert(TreeWithHistory storage self, bytes32 leaf) internal returns (uint256) { // cache read - uint32 depth = self.depth; + uint256 depth = self.zeros.length; // Get leaf index - uint32 leafIndex = self.nextLeafIndex++; + uint256 leafIndex = self.nextLeafIndex++; - // Check if tree is full. + // Check if tree is full. This check will not work if depth == 256 and the tree is full, but realistically + // this will never happen. if (leafIndex == 1 << depth) revert Full(); // Rebuild branch from leaf to root - uint32 currentIndex = leafIndex; + uint256 currentIndex = leafIndex; bytes32 currentLevelHash = leaf; for (uint32 i = 0; i < depth; i++) { // Reaching the parent node, is currentLevelHash the left child? @@ -77,15 +77,15 @@ library MerkleTree { // If so, next time we will come from the right, so we need to save it if (isLeft) { - self.filledSubtrees[i] = currentLevelHash; + unsafeAccess(self.filledSubtrees, i).value = currentLevelHash; } // Compute the node hash by hasing the current hash with either: // - the last value for this level // - the zero for this level currentLevelHash = self.fnHash( - isLeft ? currentLevelHash : self.filledSubtrees[i], - isLeft ? self.zeros[i] : currentLevelHash + isLeft ? currentLevelHash : unsafeAccess(self.filledSubtrees, i).value, + isLeft ? unsafeAccess(self.zeros, i).value : currentLevelHash ); // update node index @@ -93,17 +93,31 @@ library MerkleTree { } // Record new root - self.currentRootIndex = (self.currentRootIndex + 1) % self.length; - self.roots[self.currentRootIndex] = currentLevelHash; + self.currentRootIndex = (self.currentRootIndex + 1) % self.roots.length; + unsafeAccess(self.roots, self.currentRootIndex).value = currentLevelHash; return leafIndex; } + /** + * @dev Tree's depth (set at initialization) + */ + function getDepth(TreeWithHistory storage self) internal view returns (uint256) { + return self.zeros.length; + } + + /** + * @dev History length (set at initialization) + */ + function getLength(TreeWithHistory storage self) internal view returns (uint256) { + return self.roots.length; + } + /** * @dev Return the current root of the tree. */ function getLastRoot(TreeWithHistory storage self) internal view returns (bytes32) { - return self.roots[self.currentRootIndex]; + return unsafeAccess(self.roots, self.currentRootIndex).value; } /** @@ -116,11 +130,11 @@ library MerkleTree { // cache as uint256 (avoid overflow) uint256 currentRootIndex = self.currentRootIndex; - uint256 length = self.length; + uint256 length = self.roots.length; // search for (uint256 i = length; i > 0; --i) { - if (root == self.roots[(currentRootIndex + i) % length]) { + if (root == unsafeAccess(self.roots, (currentRootIndex + i) % length).value) { return true; } } @@ -128,11 +142,31 @@ library MerkleTree { return false; } + /** + * @dev Helper to set the length of an dynamic array. Directly writting to `.length` is forbiden. + */ + function setLength(bytes32[] storage array, uint256 len) private { + assembly { + sstore(array.slot, len) + } + } + + // TODO: this is part of PR#3589 (in the Arrays library) + function unsafeAccess(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) { + bytes32 slot; + /// @solidity memory-safe-assembly + assembly { + mstore(0, arr.slot) + slot := add(keccak256(0, 0x20), pos) + } + return StorageSlot.getBytes32Slot(slot); + } + // Default hash function initialize( TreeWithHistory storage self, - uint32 depth, - uint32 length + uint256 depth, + uint256 length ) internal { return initialize(self, depth, length, bytes32(0), _hashPair); } diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js index f813c6d3e12..8041b4bc363 100644 --- a/test/utils/structs/Merkletree.test.js +++ b/test/utils/structs/Merkletree.test.js @@ -17,8 +17,8 @@ describe('Merklee tree', function () { const leafs = Array(2 ** DEPTH).fill(constants.ZERO_BYTES32); const merkleTree = new MerkleTree(leafs, keccak256, { sortPairs: true }); - expect(await this.contract.depth()).to.be.bignumber.equal(DEPTH); - expect(await this.contract.length()).to.be.bignumber.equal(LENGTH); + expect(await this.contract.getDepth()).to.be.bignumber.equal(DEPTH); + expect(await this.contract.getLength()).to.be.bignumber.equal(LENGTH); expect(await this.contract.currentRootIndex()).to.be.bignumber.equal('0'); expect(await this.contract.nextLeafIndex()).to.be.bignumber.equal('0'); From ca83cde1bae5255d945869a0e41fa46d426a9a4a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 14 Aug 2022 22:27:31 +0200 Subject: [PATCH 03/48] gas optimization --- contracts/mocks/MerkleTreeMock.sol | 4 ++-- contracts/utils/structs/MerkleTree.sol | 13 +++++++------ test/utils/structs/Merkletree.test.js | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index f1c13075450..8f4a2fa3c6a 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -42,8 +42,8 @@ contract MerkleTreeMock { return tree.nextLeafIndex; } - function filledSubtrees(uint256 i) public view returns (bytes32) { - return tree.filledSubtrees[i]; + function sides(uint256 i) public view returns (bytes32) { + return tree.sides[i]; } function zeros(uint256 i) public view returns (bytes32) { diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 81e1f465667..1ae68eb543f 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -13,7 +13,7 @@ library MerkleTree { struct TreeWithHistory { uint256 currentRootIndex; uint256 nextLeafIndex; - bytes32[] filledSubtrees; + bytes32[] sides; bytes32[] zeros; bytes32[] roots; function(bytes32, bytes32) view returns (bytes32) fnHash; @@ -35,15 +35,16 @@ library MerkleTree { ) internal { require(depth <= MAX_DEPTH); - setLength(self.filledSubtrees, depth); + // Store depth & length in the dynamic array + setLength(self.sides, depth); setLength(self.zeros, depth); setLength(self.roots, length); self.fnHash = fnHash; + // Build the different hashes in a zero-filled complete tree bytes32 currentZero = zero; for (uint32 i = 0; i < depth; ++i) { unsafeAccess(self.zeros, i).value = currentZero; - unsafeAccess(self.filledSubtrees, i).value = currentZero; currentZero = fnHash(currentZero, currentZero); } @@ -58,7 +59,7 @@ library MerkleTree { * This should never be an issue in practice. */ function insert(TreeWithHistory storage self, bytes32 leaf) internal returns (uint256) { - // cache read + // Cache read uint256 depth = self.zeros.length; // Get leaf index @@ -77,14 +78,14 @@ library MerkleTree { // If so, next time we will come from the right, so we need to save it if (isLeft) { - unsafeAccess(self.filledSubtrees, i).value = currentLevelHash; + unsafeAccess(self.sides, i).value = currentLevelHash; } // Compute the node hash by hasing the current hash with either: // - the last value for this level // - the zero for this level currentLevelHash = self.fnHash( - isLeft ? currentLevelHash : unsafeAccess(self.filledSubtrees, i).value, + isLeft ? currentLevelHash : unsafeAccess(self.sides, i).value, isLeft ? unsafeAccess(self.zeros, i).value : currentLevelHash ); diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js index 8041b4bc363..faae6c4c6ba 100644 --- a/test/utils/structs/Merkletree.test.js +++ b/test/utils/structs/Merkletree.test.js @@ -25,7 +25,7 @@ describe('Merklee tree', function () { expect(await this.contract.getLastRoot()).to.be.equal(merkleTree.getHexRoot()); for (let i = 0; i < DEPTH; ++i) { expect(await this.contract.zeros(i)).to.be.equal(merkleTree.getHexLayers()[i][0]); - expect(await this.contract.filledSubtrees(i)).to.be.equal(merkleTree.getHexLayers()[i][0]); + expect(await this.contract.sides(i)).to.be.equal(constants.ZERO_BYTES32); } for (let i = 0; i < LENGTH; ++i) { From f4f46caa1c254ec7419c534fe6318f8c937abfca Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 14 Aug 2022 22:35:48 +0200 Subject: [PATCH 04/48] limit tree depth to 255 to avoid issues (255 is enough for any realistic application) --- contracts/utils/structs/MerkleTree.sol | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 1ae68eb543f..9db58b2a609 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -8,7 +8,7 @@ import "../StorageSlot.sol"; error Full(); library MerkleTree { - uint256 private constant MAX_DEPTH = 256; + uint256 private constant MAX_DEPTH = 255; struct TreeWithHistory { uint256 currentRootIndex; @@ -54,9 +54,6 @@ library MerkleTree { /** * @dev Insert a new leaf in the tree, compute the new root, and store that new root in the history. - * - * Note: because of the `nextLeafIndex` overflow, trees of depth 256 can only store 2**256-1 items. - * This should never be an issue in practice. */ function insert(TreeWithHistory storage self, bytes32 leaf) internal returns (uint256) { // Cache read @@ -65,8 +62,7 @@ library MerkleTree { // Get leaf index uint256 leafIndex = self.nextLeafIndex++; - // Check if tree is full. This check will not work if depth == 256 and the tree is full, but realistically - // this will never happen. + // Check if tree is full. if (leafIndex == 1 << depth) revert Full(); // Rebuild branch from leaf to root From af7cb9c7b67d7ecb24a99d8e303122c91f9f40dc Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 14 Aug 2022 22:41:12 +0200 Subject: [PATCH 05/48] fix lint --- contracts/mocks/MerkleTreeMock.sol | 24 ++++++++++++------------ contracts/utils/structs/MerkleTree.sol | 12 ++++++------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index 8f4a2fa3c6a..664a8fec11c 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -7,50 +7,50 @@ import "../utils/structs/MerkleTree.sol"; contract MerkleTreeMock { using MerkleTree for MerkleTree.TreeWithHistory; - MerkleTree.TreeWithHistory private tree; + MerkleTree.TreeWithHistory private _tree; constructor(uint256 _depth, uint256 _length) { - tree.initialize(_depth, _length); + _tree.initialize(_depth, _length); } function insert(bytes32 leaf) public returns (uint256) { - return tree.insert(leaf); + return _tree.insert(leaf); } function getDepth() public view returns (uint256) { - return tree.getDepth(); + return _tree.getDepth(); } function getLength() public view returns (uint256) { - return tree.getLength(); + return _tree.getLength(); } function getLastRoot() public view returns (bytes32) { - return tree.getLastRoot(); + return _tree.getLastRoot(); } function isKnownRoot(bytes32 root) public view returns (bool) { - return tree.isKnownRoot(root); + return _tree.isKnownRoot(root); } // internal state function currentRootIndex() public view returns (uint256) { - return tree.currentRootIndex; + return _tree.currentRootIndex; } function nextLeafIndex() public view returns (uint256) { - return tree.nextLeafIndex; + return _tree.nextLeafIndex; } function sides(uint256 i) public view returns (bytes32) { - return tree.sides[i]; + return _tree.sides[i]; } function zeros(uint256 i) public view returns (bytes32) { - return tree.zeros[i]; + return _tree.zeros[i]; } function roots(uint256 i) public view returns (bytes32) { - return tree.roots[i]; + return _tree.roots[i]; } } diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 9db58b2a609..f82d14613d8 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -8,7 +8,7 @@ import "../StorageSlot.sol"; error Full(); library MerkleTree { - uint256 private constant MAX_DEPTH = 255; + uint256 private constant _MAX_DEPTH = 255; struct TreeWithHistory { uint256 currentRootIndex; @@ -33,12 +33,12 @@ library MerkleTree { bytes32 zero, function(bytes32, bytes32) view returns (bytes32) fnHash ) internal { - require(depth <= MAX_DEPTH); + require(depth <= _MAX_DEPTH); // Store depth & length in the dynamic array - setLength(self.sides, depth); - setLength(self.zeros, depth); - setLength(self.roots, length); + _unsafeSetLength(self.sides, depth); + _unsafeSetLength(self.zeros, depth); + _unsafeSetLength(self.roots, length); self.fnHash = fnHash; // Build the different hashes in a zero-filled complete tree @@ -142,7 +142,7 @@ library MerkleTree { /** * @dev Helper to set the length of an dynamic array. Directly writting to `.length` is forbiden. */ - function setLength(bytes32[] storage array, uint256 len) private { + function _unsafeSetLength(bytes32[] storage array, uint256 len) private { assembly { sstore(array.slot, len) } From 45eae371ed604718bf66121b031d5796e91f206c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 14 Aug 2022 22:44:39 +0200 Subject: [PATCH 06/48] reason --- contracts/utils/structs/MerkleTree.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index f82d14613d8..243e0f78cc5 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -33,7 +33,7 @@ library MerkleTree { bytes32 zero, function(bytes32, bytes32) view returns (bytes32) fnHash ) internal { - require(depth <= _MAX_DEPTH); + require(depth <= _MAX_DEPTH, "MerkleTree: invalid length"); // Store depth & length in the dynamic array _unsafeSetLength(self.sides, depth); From ac648c6cb0d39ec13d62d1d9f958d2e4bf04e63e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 14 Aug 2022 22:54:30 +0200 Subject: [PATCH 07/48] coverage --- test/utils/structs/Merkletree.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js index faae6c4c6ba..059310423fd 100644 --- a/test/utils/structs/Merkletree.test.js +++ b/test/utils/structs/Merkletree.test.js @@ -13,6 +13,13 @@ describe('Merklee tree', function () { this.contract = await MerkleTreeMock.new(DEPTH, LENGTH); }); + it('depth is limited', async function () { + await expectRevert( + MerkleTreeMock.new(256, LENGTH), + 'MerkleTree: invalid length', + ); + }); + it('setup', async function () { const leafs = Array(2 ** DEPTH).fill(constants.ZERO_BYTES32); const merkleTree = new MerkleTree(leafs, keccak256, { sortPairs: true }); From 1b184c9100465b7924661ad4fe161c67dc24e170 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 14 Aug 2022 23:25:54 +0200 Subject: [PATCH 08/48] comments --- contracts/utils/structs/MerkleTree.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 243e0f78cc5..44a3450f8bb 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -85,7 +85,7 @@ library MerkleTree { isLeft ? unsafeAccess(self.zeros, i).value : currentLevelHash ); - // update node index + // Update node index currentIndex >>= 1; } @@ -125,11 +125,11 @@ library MerkleTree { return false; } - // cache as uint256 (avoid overflow) + // Cache read uint256 currentRootIndex = self.currentRootIndex; uint256 length = self.roots.length; - // search + // Search (most recents first) for (uint256 i = length; i > 0; --i) { if (root == unsafeAccess(self.roots, (currentRootIndex + i) % length).value) { return true; From 486341870e8021074f8a1b615de459e40ee0bb74 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 16 Aug 2022 14:55:26 +0200 Subject: [PATCH 09/48] documentation & changelog entry --- CHANGELOG.md | 1 + contracts/utils/structs/MerkleTree.sol | 44 ++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5828a7b2193..95aabcbb02d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * `SafeCast`: optimize downcasting of signed integers. ([#3565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3565)) * `ERC20FlashMint`: add an internal `_flashFee` function for overriding. ([#3551](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3551)) * `VestingWallet`: remove unused library `Math.sol`. ([#3605](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3605)) + * `MerkleTree`: a new data structure that keep track of merkle roots as values are inserted into a merkle tree. ([#3617](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3617)) ### Compatibility Note diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 44a3450f8bb..bc1b24d6671 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -5,11 +5,48 @@ pragma solidity ^0.8.0; // TODO: replace this import in favor or "../Array.sol" when #3589 is merged import "../StorageSlot.sol"; -error Full(); - +/** + * @dev A complete binary tree with the ability to sequentially insert leaves, changing them from a zero to a non-zero + * value, while keeping a history of merkle roots. This structure allows inserting commitment (or other entrie) that + * are not stored, but can be proven to be part of the tree. + * + * The history of merkle roots allow inclusion proofs to remain valid even if leaves are inserted into the tree between + * the moment the proof is generated and the moment it's verified. + * + * Each tree can be customized to use specific + * - depth + * - length of the root history + * - zero values (for "empty" leaves) + * - hash function + * + * WARNING: + * + * By design, the tree include zero leaves. Customizing the "zero value" might be necessary to ensure that empty leaves + * being provably part of the tree is not a security issue. + * + * _Available since v4.x._ + */ library MerkleTree { + /** + * @dev Maximum supported depth. Beyond that, some checks will fail to properly work. + * This should be enough for any realistic usecase. + */ uint256 private constant _MAX_DEPTH = 255; + /** + * @dev Leaf cannot be inserted because the tree is full. + */ + error Full(); + + /** + * @dev The `sides` and `zero` arrays are set, at initialization, to have a length equal to the depth of the tree. + * No push/pop operations should be performed of these array, and their lengths should not be updated. + * + * The `roots` array stores the history of roots. Its length is set at initialization, and should not be updated. + * + * The hashing function used during initialization to compute the `zeros` values (value of a node at a given depth + * for which the subtree is full of zero leaves). This function is kept in the structure for handling insertions. + */ struct TreeWithHistory { uint256 currentRootIndex; uint256 nextLeafIndex; @@ -58,6 +95,7 @@ library MerkleTree { function insert(TreeWithHistory storage self, bytes32 leaf) internal returns (uint256) { // Cache read uint256 depth = self.zeros.length; + function(bytes32, bytes32) view returns (bytes32) fnHash = self.fnHash; // Get leaf index uint256 leafIndex = self.nextLeafIndex++; @@ -80,7 +118,7 @@ library MerkleTree { // Compute the node hash by hasing the current hash with either: // - the last value for this level // - the zero for this level - currentLevelHash = self.fnHash( + currentLevelHash = fnHash( isLeft ? currentLevelHash : unsafeAccess(self.sides, i).value, isLeft ? unsafeAccess(self.zeros, i).value : currentLevelHash ); From 9fc7f31b79d0e87a6fec23adea8fb3306979b910 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 13 Dec 2023 17:02:10 +0100 Subject: [PATCH 10/48] update --- contracts/mocks/MerkleTreeMock.sol | 4 +- contracts/utils/Arrays.sol | 48 +++++++++- contracts/utils/cryptography/MerkleProof.sol | 10 +- contracts/utils/structs/MerkleTree.sol | 97 ++++++++----------- test/utils/structs/Merkletree.test.js | 98 ++++++++++---------- 5 files changed, 139 insertions(+), 118 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index 664a8fec11c..d3ae4784eaf 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -9,8 +9,8 @@ contract MerkleTreeMock { MerkleTree.TreeWithHistory private _tree; - constructor(uint256 _depth, uint256 _length) { - _tree.initialize(_depth, _length); + constructor(uint256 _depth, uint256 _length, bytes32 _zero) { + _tree.initialize(_depth, _length, _zero); } function insert(bytes32 leaf) public returns (uint256) { diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index aaab3ce592b..4597ab596e0 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -108,7 +108,7 @@ library Arrays { * * WARNING: Only use if you are certain `pos` is lower than the array length. */ - function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) { + function unsafeMemoryAccess(address[] memory arr, uint256 pos) internal pure returns (address res) { assembly { res := mload(add(add(arr, 0x20), mul(pos, 0x20))) } @@ -119,9 +119,53 @@ library Arrays { * * WARNING: Only use if you are certain `pos` is lower than the array length. */ - function unsafeMemoryAccess(address[] memory arr, uint256 pos) internal pure returns (address res) { + function unsafeMemoryAccess(bytes32[] memory arr, uint256 pos) internal pure returns (bytes32 res) { + assembly { + res := mload(add(add(arr, 0x20), mul(pos, 0x20))) + } + } + + /** + * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. + * + * WARNING: Only use if you are certain `pos` is lower than the array length. + */ + function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) { assembly { res := mload(add(add(arr, 0x20), mul(pos, 0x20))) } } + + /** + * @dev Helper to set the length of an dynamic array. Directly writing to `.length` is forbidden. + * + * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased. + */ + function unsafeSetLength(address[] storage array, uint256 len) internal { + assembly { + sstore(array.slot, len) + } + } + + /** + * @dev Helper to set the length of an dynamic array. Directly writing to `.length` is forbidden. + * + * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased. + */ + function unsafeSetLength(bytes32[] storage array, uint256 len) internal { + assembly { + sstore(array.slot, len) + } + } + + /** + * @dev Helper to set the length of an dynamic array. Directly writing to `.length` is forbidden. + * + * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased. + */ + function unsafeSetLength(uint256[] storage array, uint256 len) internal { + assembly { + sstore(array.slot, len) + } + } } diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index 525f5da34bc..a52476bfc1a 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -49,7 +49,7 @@ library MerkleProof { function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { bytes32 computedHash = leaf; for (uint256 i = 0; i < proof.length; i++) { - computedHash = _hashPair(computedHash, proof[i]); + computedHash = hashPair(computedHash, proof[i]); } return computedHash; } @@ -60,7 +60,7 @@ library MerkleProof { function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) { bytes32 computedHash = leaf; for (uint256 i = 0; i < proof.length; i++) { - computedHash = _hashPair(computedHash, proof[i]); + computedHash = hashPair(computedHash, proof[i]); } return computedHash; } @@ -138,7 +138,7 @@ library MerkleProof { bytes32 b = proofFlags[i] ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) : proof[proofPos++]; - hashes[i] = _hashPair(a, b); + hashes[i] = hashPair(a, b); } if (totalHashes > 0) { @@ -194,7 +194,7 @@ library MerkleProof { bytes32 b = proofFlags[i] ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) : proof[proofPos++]; - hashes[i] = _hashPair(a, b); + hashes[i] = hashPair(a, b); } if (totalHashes > 0) { @@ -214,7 +214,7 @@ library MerkleProof { /** * @dev Sorts the pair (a, b) and hashes the result. */ - function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { + function hashPair(bytes32 a, bytes32 b) internal pure returns (bytes32) { return a < b ? _efficientHash(a, b) : _efficientHash(b, a); } diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index bc1b24d6671..7687c458082 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.0; -// TODO: replace this import in favor or "../Array.sol" when #3589 is merged -import "../StorageSlot.sol"; +import { MerkleProof } from "../cryptography/MerkleProof.sol"; +import { Arrays } from "../Arrays.sol"; /** * @dev A complete binary tree with the ability to sequentially insert leaves, changing them from a zero to a non-zero - * value, while keeping a history of merkle roots. This structure allows inserting commitment (or other entrie) that + * value, while keeping a history of merkle roots. This structure allows inserting commitment (or other entries) that * are not stored, but can be proven to be part of the tree. * * The history of merkle roots allow inclusion proofs to remain valid even if leaves are inserted into the tree between @@ -36,7 +36,12 @@ library MerkleTree { /** * @dev Leaf cannot be inserted because the tree is full. */ - error Full(); + error MerkleTreeFull(); + + /** + * @dev Merkle tree cannot be initialized because requrest depth is to large. + */ + error MerkleTreeInvalidDepth(uint256 depth, uint256 maxDepth); /** * @dev The `sides` and `zero` arrays are set, at initialization, to have a length equal to the depth of the tree. @@ -56,6 +61,18 @@ library MerkleTree { function(bytes32, bytes32) view returns (bytes32) fnHash; } + /** + * @dev Initialize using the default hash + */ + function initialize( + TreeWithHistory storage self, + uint256 depth, + uint256 length, + bytes32 zero + ) internal { + return initialize(self, depth, length, zero, MerkleProof.hashPair); + } + /** * @dev Initialize a new complete MerkleTree defined by: * - Depth `depth` @@ -70,23 +87,25 @@ library MerkleTree { bytes32 zero, function(bytes32, bytes32) view returns (bytes32) fnHash ) internal { - require(depth <= _MAX_DEPTH, "MerkleTree: invalid length"); + if (depth > _MAX_DEPTH) { + revert MerkleTreeInvalidDepth(depth, _MAX_DEPTH); + } // Store depth & length in the dynamic array - _unsafeSetLength(self.sides, depth); - _unsafeSetLength(self.zeros, depth); - _unsafeSetLength(self.roots, length); + Arrays.unsafeSetLength(self.sides, depth); + Arrays.unsafeSetLength(self.zeros, depth); + Arrays.unsafeSetLength(self.roots, length); self.fnHash = fnHash; // Build the different hashes in a zero-filled complete tree bytes32 currentZero = zero; for (uint32 i = 0; i < depth; ++i) { - unsafeAccess(self.zeros, i).value = currentZero; + Arrays.unsafeAccess(self.zeros, i).value = currentZero; currentZero = fnHash(currentZero, currentZero); } // Insert the first root - unsafeAccess(self.roots, 0).value = currentZero; + Arrays.unsafeAccess(self.roots, 0).value = currentZero; } /** @@ -101,7 +120,7 @@ library MerkleTree { uint256 leafIndex = self.nextLeafIndex++; // Check if tree is full. - if (leafIndex == 1 << depth) revert Full(); + if (leafIndex == 1 << depth) revert MerkleTreeFull(); // Rebuild branch from leaf to root uint256 currentIndex = leafIndex; @@ -112,15 +131,15 @@ library MerkleTree { // If so, next time we will come from the right, so we need to save it if (isLeft) { - unsafeAccess(self.sides, i).value = currentLevelHash; + Arrays.unsafeAccess(self.sides, i).value = currentLevelHash; } - // Compute the node hash by hasing the current hash with either: + // Compute the node hash by hashing the current hash with either: // - the last value for this level // - the zero for this level currentLevelHash = fnHash( - isLeft ? currentLevelHash : unsafeAccess(self.sides, i).value, - isLeft ? unsafeAccess(self.zeros, i).value : currentLevelHash + isLeft ? currentLevelHash : Arrays.unsafeAccess(self.sides, i).value, + isLeft ? Arrays.unsafeAccess(self.zeros, i).value : currentLevelHash ); // Update node index @@ -129,7 +148,7 @@ library MerkleTree { // Record new root self.currentRootIndex = (self.currentRootIndex + 1) % self.roots.length; - unsafeAccess(self.roots, self.currentRootIndex).value = currentLevelHash; + Arrays.unsafeAccess(self.roots, self.currentRootIndex).value = currentLevelHash; return leafIndex; } @@ -152,7 +171,7 @@ library MerkleTree { * @dev Return the current root of the tree. */ function getLastRoot(TreeWithHistory storage self) internal view returns (bytes32) { - return unsafeAccess(self.roots, self.currentRootIndex).value; + return Arrays.unsafeAccess(self.roots, self.currentRootIndex).value; } /** @@ -169,53 +188,11 @@ library MerkleTree { // Search (most recents first) for (uint256 i = length; i > 0; --i) { - if (root == unsafeAccess(self.roots, (currentRootIndex + i) % length).value) { + if (root == Arrays.unsafeAccess(self.roots, (currentRootIndex + i) % length).value) { return true; } } return false; } - - /** - * @dev Helper to set the length of an dynamic array. Directly writting to `.length` is forbiden. - */ - function _unsafeSetLength(bytes32[] storage array, uint256 len) private { - assembly { - sstore(array.slot, len) - } - } - - // TODO: this is part of PR#3589 (in the Arrays library) - function unsafeAccess(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) { - bytes32 slot; - /// @solidity memory-safe-assembly - assembly { - mstore(0, arr.slot) - slot := add(keccak256(0, 0x20), pos) - } - return StorageSlot.getBytes32Slot(slot); - } - - // Default hash - function initialize( - TreeWithHistory storage self, - uint256 depth, - uint256 length - ) internal { - return initialize(self, depth, length, bytes32(0), _hashPair); - } - - function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { - return a < b ? _efficientHash(a, b) : _efficientHash(b, a); - } - - function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { - /// @solidity memory-safe-assembly - assembly { - mstore(0x00, a) - mstore(0x20, b) - value := keccak256(0x00, 0x40) - } - } } diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js index 059310423fd..0c0d0f0df4b 100644 --- a/test/utils/structs/Merkletree.test.js +++ b/test/utils/structs/Merkletree.test.js @@ -1,86 +1,86 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); -const { MerkleTree } = require('merkletreejs'); -const keccak256 = require('keccak256'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { StandardMerkleTree } = require('@openzeppelin/merkle-tree'); -const MerkleTreeMock = artifacts.require('MerkleTreeMock'); +// TODO: when working with merkle tree construction, ordering of the leaves should be disabled. +const makeTree = (leafs = [ethers.ZeroHash]) => StandardMerkleTree.of(leafs.map(leaf => [leaf]), ['bytes32']); -describe('Merklee tree', function () { - const DEPTH = new BN(4); - const LENGTH = new BN(10); +const MAX_DEPTH = 255n; +const DEPTH = 4n; // 16 slots +const LENGTH = 8n; +const ZERO = makeTree().leafHash([ ethers.ZeroHash ]); + +async function fixture () { + return { mock: await ethers.deployContract('MerkleTreeMock', [ DEPTH, LENGTH, ZERO ]) }; +} +describe('Merklee tree', function () { beforeEach(async function () { - this.contract = await MerkleTreeMock.new(DEPTH, LENGTH); + Object.assign(this, await loadFixture(fixture)); }); it('depth is limited', async function () { - await expectRevert( - MerkleTreeMock.new(256, LENGTH), - 'MerkleTree: invalid length', - ); + await expect(ethers.deployContract('MerkleTreeMock', [256n, LENGTH, ZERO])) + .to.be.revertedWithCustomError({ interface: this.mock.interface }, 'MerkleTreeInvalidDepth') + .withArgs(256n, MAX_DEPTH); }); it('setup', async function () { - const leafs = Array(2 ** DEPTH).fill(constants.ZERO_BYTES32); - const merkleTree = new MerkleTree(leafs, keccak256, { sortPairs: true }); - - expect(await this.contract.getDepth()).to.be.bignumber.equal(DEPTH); - expect(await this.contract.getLength()).to.be.bignumber.equal(LENGTH); - expect(await this.contract.currentRootIndex()).to.be.bignumber.equal('0'); - expect(await this.contract.nextLeafIndex()).to.be.bignumber.equal('0'); - - expect(await this.contract.getLastRoot()).to.be.equal(merkleTree.getHexRoot()); - for (let i = 0; i < DEPTH; ++i) { - expect(await this.contract.zeros(i)).to.be.equal(merkleTree.getHexLayers()[i][0]); - expect(await this.contract.sides(i)).to.be.equal(constants.ZERO_BYTES32); - } + const merkleTree = makeTree(Array(2 ** Number(DEPTH)).fill(ethers.ZeroHash)); + + expect(await this.mock.getDepth()).to.equal(DEPTH); + expect(await this.mock.getLength()).to.equal(LENGTH); + expect(await this.mock.currentRootIndex()).to.equal(0n); + expect(await this.mock.nextLeafIndex()).to.equal(0n); + expect(await this.mock.getLastRoot()).to.equal(merkleTree.root); for (let i = 0; i < LENGTH; ++i) { - expect(await this.contract.roots(i)).to.be.equal(i === 0 ? merkleTree.getHexRoot() : constants.ZERO_BYTES32); + expect(await this.mock.roots(i)).to.equal(i === 0 ? merkleTree.root : ethers.ZeroHash); } - expect(await this.contract.isKnownRoot(merkleTree.getHexRoot())).to.be.equal(true); - expect(await this.contract.isKnownRoot(constants.ZERO_BYTES32)).to.be.equal(false); + expect(await this.mock.isKnownRoot(merkleTree.root)).to.be.true; + expect(await this.mock.isKnownRoot(ethers.ZeroHash)).to.be.false; }); describe('insert', function () { it('tree is correctly updated', async function () { - const leafs = Array(2 ** DEPTH).fill(constants.ZERO_BYTES32); + const leafs = Array(2 ** Number(DEPTH)).fill(ethers.ZeroHash); const roots = []; - // for each entry - for (const i of Object.keys(leafs).map(Number)) { + // for each leaf slot + for (const i in leafs) { // generate random leaf - leafs[i] = web3.utils.randomHex(32); - const merkleTree = new MerkleTree(leafs, keccak256, { sortPairs: true }); + leafs[i] = ethers.randomBytes(32); + + // update leaf list and rebuild tree. + const merkleTree = makeTree(leafs); - // insert leaf - await this.contract.insert(leafs[i]); + // insert value in tree + await this.mock.insert(merkleTree.leafHash([ leafs[i] ])); // check tree - expect(await this.contract.currentRootIndex()).to.be.bignumber.equal(((i + 1) % LENGTH).toString()); - expect(await this.contract.nextLeafIndex()).to.be.bignumber.equal((i + 1).toString()); - expect(await this.contract.getLastRoot()).to.be.equal(merkleTree.getHexRoot()); + expect(await this.mock.currentRootIndex()).to.equal((BigInt(i) + 1n) % LENGTH); + expect(await this.mock.nextLeafIndex()).to.equal(BigInt(i) + 1n); + expect(await this.mock.getLastRoot()).to.equal(merkleTree.root); // check root history - roots.push(merkleTree.getHexRoot()); - for (const root of roots.slice(0, -LENGTH)) { - expect(await this.contract.isKnownRoot(root)).to.be.equal(false); + roots.push(merkleTree.root); + for (const root of roots.slice(0, -Number(LENGTH))) { + expect(await this.mock.isKnownRoot(root)).to.be.false; } - for (const root of roots.slice(-LENGTH)) { - expect(await this.contract.isKnownRoot(root)).to.be.equal(true); + for (const root of roots.slice(-Number(LENGTH))) { + expect(await this.mock.isKnownRoot(root)).to.be.true; } } }); it('revert when tree is full', async function () { - for (let i = 0; i < 2 ** DEPTH; ++i) { - await this.contract.insert(constants.ZERO_BYTES32); + for (let i = 0; i < 2 ** Number(DEPTH); ++i) { + await this.mock.insert(ethers.ZeroHash); } - await expectRevert( - this.contract.insert(constants.ZERO_BYTES32), - 'Full()', - ); + await expect(this.mock.insert(ethers.ZeroHash)) + .to.be.revertedWithCustomError(this.mock, 'MerkleTreeFull'); }); }); }); From 652c8a1ae12c60399cd21ec667e2d1993aa98257 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 13 Dec 2023 17:08:52 +0100 Subject: [PATCH 11/48] add changeset --- .changeset/warm-sheep-cover.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/warm-sheep-cover.md diff --git a/.changeset/warm-sheep-cover.md b/.changeset/warm-sheep-cover.md new file mode 100644 index 00000000000..c0418a6f7e3 --- /dev/null +++ b/.changeset/warm-sheep-cover.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`MerkleTree`: a new data structure that keep track of merkle roots as values are inserted into a merkle tree. From c74ab55fea8fdd1f3c51abb7269517a207bec0d0 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 14 Dec 2023 15:22:13 +0100 Subject: [PATCH 12/48] fix lint --- contracts/utils/structs/MerkleTree.sol | 11 +++-------- test/utils/structs/Merkletree.test.js | 17 ++++++++++------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 7687c458082..7e3b4aced1d 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.0; -import { MerkleProof } from "../cryptography/MerkleProof.sol"; -import { Arrays } from "../Arrays.sol"; +import {MerkleProof} from "../cryptography/MerkleProof.sol"; +import {Arrays} from "../Arrays.sol"; /** * @dev A complete binary tree with the ability to sequentially insert leaves, changing them from a zero to a non-zero @@ -64,12 +64,7 @@ library MerkleTree { /** * @dev Initialize using the default hash */ - function initialize( - TreeWithHistory storage self, - uint256 depth, - uint256 length, - bytes32 zero - ) internal { + function initialize(TreeWithHistory storage self, uint256 depth, uint256 length, bytes32 zero) internal { return initialize(self, depth, length, zero, MerkleProof.hashPair); } diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js index 0c0d0f0df4b..98c77d575ec 100644 --- a/test/utils/structs/Merkletree.test.js +++ b/test/utils/structs/Merkletree.test.js @@ -4,15 +4,19 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { StandardMerkleTree } = require('@openzeppelin/merkle-tree'); // TODO: when working with merkle tree construction, ordering of the leaves should be disabled. -const makeTree = (leafs = [ethers.ZeroHash]) => StandardMerkleTree.of(leafs.map(leaf => [leaf]), ['bytes32']); +const makeTree = (leafs = [ethers.ZeroHash]) => + StandardMerkleTree.of( + leafs.map(leaf => [leaf]), + ['bytes32'], + ); const MAX_DEPTH = 255n; const DEPTH = 4n; // 16 slots const LENGTH = 8n; -const ZERO = makeTree().leafHash([ ethers.ZeroHash ]); +const ZERO = makeTree().leafHash([ethers.ZeroHash]); -async function fixture () { - return { mock: await ethers.deployContract('MerkleTreeMock', [ DEPTH, LENGTH, ZERO ]) }; +async function fixture() { + return { mock: await ethers.deployContract('MerkleTreeMock', [DEPTH, LENGTH, ZERO]) }; } describe('Merklee tree', function () { @@ -57,7 +61,7 @@ describe('Merklee tree', function () { const merkleTree = makeTree(leafs); // insert value in tree - await this.mock.insert(merkleTree.leafHash([ leafs[i] ])); + await this.mock.insert(merkleTree.leafHash([leafs[i]])); // check tree expect(await this.mock.currentRootIndex()).to.equal((BigInt(i) + 1n) % LENGTH); @@ -79,8 +83,7 @@ describe('Merklee tree', function () { for (let i = 0; i < 2 ** Number(DEPTH); ++i) { await this.mock.insert(ethers.ZeroHash); } - await expect(this.mock.insert(ethers.ZeroHash)) - .to.be.revertedWithCustomError(this.mock, 'MerkleTreeFull'); + await expect(this.mock.insert(ethers.ZeroHash)).to.be.revertedWithCustomError(this.mock, 'MerkleTreeFull'); }); }); }); From a9932c9a378087e24f781a772ccfe05ea83ab047 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 14 Dec 2023 16:37:23 +0100 Subject: [PATCH 13/48] fix lint --- contracts/mocks/MerkleTreeMock.sol | 2 +- contracts/utils/structs/MerkleTree.sol | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index d3ae4784eaf..2bef4983dbe 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import "../utils/structs/MerkleTree.sol"; +import {MerkleTree} from "../utils/structs/MerkleTree.sol"; contract MerkleTreeMock { using MerkleTree for MerkleTree.TreeWithHistory; diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 7e3b4aced1d..78cbd43bec3 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -31,7 +31,7 @@ library MerkleTree { * @dev Maximum supported depth. Beyond that, some checks will fail to properly work. * This should be enough for any realistic usecase. */ - uint256 private constant _MAX_DEPTH = 255; + uint256 private constant MAX_DEPTH = 255; /** * @dev Leaf cannot be inserted because the tree is full. @@ -82,8 +82,8 @@ library MerkleTree { bytes32 zero, function(bytes32, bytes32) view returns (bytes32) fnHash ) internal { - if (depth > _MAX_DEPTH) { - revert MerkleTreeInvalidDepth(depth, _MAX_DEPTH); + if (depth > MAX_DEPTH) { + revert MerkleTreeInvalidDepth(depth, MAX_DEPTH); } // Store depth & length in the dynamic array From d8bdfd02ccbdbeab43071b9a87a740fc547c0353 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 14 Dec 2023 16:38:04 +0100 Subject: [PATCH 14/48] fix codespell --- contracts/utils/structs/MerkleTree.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 78cbd43bec3..338f636ffcb 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -39,7 +39,7 @@ library MerkleTree { error MerkleTreeFull(); /** - * @dev Merkle tree cannot be initialized because requrest depth is to large. + * @dev Merkle tree cannot be initialized because requested depth is to large. */ error MerkleTreeInvalidDepth(uint256 depth, uint256 maxDepth); From b131354e7e128ff0dc0b897c3d9800edba9bdd07 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 28 Jan 2024 13:56:31 +0100 Subject: [PATCH 15/48] update @openzeppelin/merkle-tree dependency --- package-lock.json | 8 ++++---- package.json | 2 +- test/utils/structs/Merkletree.test.js | 7 ++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e538236941..40029d24a10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@nomicfoundation/hardhat-network-helpers": "^1.0.3", "@nomicfoundation/hardhat-toolbox": "^4.0.0", "@openzeppelin/docs-utils": "^0.1.5", - "@openzeppelin/merkle-tree": "^1.0.5", + "@openzeppelin/merkle-tree": "^1.0.6", "@openzeppelin/upgrade-safe-transpiler": "^0.3.32", "@openzeppelin/upgrades-core": "^1.20.6", "chai": "^4.2.0", @@ -2471,9 +2471,9 @@ } }, "node_modules/@openzeppelin/merkle-tree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@openzeppelin/merkle-tree/-/merkle-tree-1.0.5.tgz", - "integrity": "sha512-JkwG2ysdHeIphrScNxYagPy6jZeNONgDRyqU6lbFgE8HKCZFSkcP8r6AjZs+3HZk4uRNV0kNBBzuWhKQ3YV7Kw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@openzeppelin/merkle-tree/-/merkle-tree-1.0.6.tgz", + "integrity": "sha512-cGWOb2WBWbJhqvupzxjnKAwGLxxAEYPg51sk76yZ5nVe5D03mw7Vx5yo8llaIEqYhP5O39M8QlrNWclgLfKVrA==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.7.0", diff --git a/package.json b/package.json index f4076433b61..748a8682d95 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@nomicfoundation/hardhat-network-helpers": "^1.0.3", "@nomicfoundation/hardhat-toolbox": "^4.0.0", "@openzeppelin/docs-utils": "^0.1.5", - "@openzeppelin/merkle-tree": "^1.0.5", + "@openzeppelin/merkle-tree": "^1.0.6", "@openzeppelin/upgrade-safe-transpiler": "^0.3.32", "@openzeppelin/upgrades-core": "^1.20.6", "chai": "^4.2.0", diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js index 98c77d575ec..d509a7646c9 100644 --- a/test/utils/structs/Merkletree.test.js +++ b/test/utils/structs/Merkletree.test.js @@ -3,11 +3,11 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { StandardMerkleTree } = require('@openzeppelin/merkle-tree'); -// TODO: when working with merkle tree construction, ordering of the leaves should be disabled. const makeTree = (leafs = [ethers.ZeroHash]) => StandardMerkleTree.of( leafs.map(leaf => [leaf]), ['bytes32'], + { sortLeaves: false } ); const MAX_DEPTH = 255n; @@ -25,9 +25,10 @@ describe('Merklee tree', function () { }); it('depth is limited', async function () { - await expect(ethers.deployContract('MerkleTreeMock', [256n, LENGTH, ZERO])) + const invalidDepth = MAX_DEPTH + 1n; + await expect(ethers.deployContract('MerkleTreeMock', [invalidDepth, LENGTH, ZERO])) .to.be.revertedWithCustomError({ interface: this.mock.interface }, 'MerkleTreeInvalidDepth') - .withArgs(256n, MAX_DEPTH); + .withArgs(invalidDepth, MAX_DEPTH); }); it('setup', async function () { From 6422af67d838614da83e6b693206e3fa04cd1cfa Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 28 Jan 2024 14:04:43 +0100 Subject: [PATCH 16/48] fix lint --- test/utils/structs/Merkletree.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js index d509a7646c9..96c8fb35ff6 100644 --- a/test/utils/structs/Merkletree.test.js +++ b/test/utils/structs/Merkletree.test.js @@ -7,7 +7,7 @@ const makeTree = (leafs = [ethers.ZeroHash]) => StandardMerkleTree.of( leafs.map(leaf => [leaf]), ['bytes32'], - { sortLeaves: false } + { sortLeaves: false }, ); const MAX_DEPTH = 255n; From 5639d7c8cd37c5456c79e44d4634a2f38c0dff87 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 5 Feb 2024 17:40:12 +0100 Subject: [PATCH 17/48] up --- lib/forge-std | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forge-std b/lib/forge-std index ae570fec082..c2236853aad 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce +Subproject commit c2236853aadb8e2d9909bbecdc490099519b70a4 From 24c829a2c8d97c6311314757126de72d82c81f7b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 5 Feb 2024 17:45:19 +0100 Subject: [PATCH 18/48] minimize changes --- lib/forge-std | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forge-std b/lib/forge-std index c2236853aad..ae570fec082 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit c2236853aadb8e2d9909bbecdc490099519b70a4 +Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce From f954a9855b3f2e189d90cd0efbdb3d7e69853228 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 5 Feb 2024 17:57:00 +0100 Subject: [PATCH 19/48] Panic with RESOURCE_ERROR when inserting in a full tree --- contracts/utils/structs/MerkleTree.sol | 8 ++------ test/utils/structs/Merkletree.test.js | 12 ++++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 338f636ffcb..694d120b5f1 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import {MerkleProof} from "../cryptography/MerkleProof.sol"; import {Arrays} from "../Arrays.sol"; +import {Panic} from "../Panic.sol"; /** * @dev A complete binary tree with the ability to sequentially insert leaves, changing them from a zero to a non-zero @@ -33,11 +34,6 @@ library MerkleTree { */ uint256 private constant MAX_DEPTH = 255; - /** - * @dev Leaf cannot be inserted because the tree is full. - */ - error MerkleTreeFull(); - /** * @dev Merkle tree cannot be initialized because requested depth is to large. */ @@ -115,7 +111,7 @@ library MerkleTree { uint256 leafIndex = self.nextLeafIndex++; // Check if tree is full. - if (leafIndex == 1 << depth) revert MerkleTreeFull(); + if (leafIndex == 1 << depth) Panic.panic(Panic.RESOURCE_ERROR); // Rebuild branch from leaf to root uint256 currentIndex = leafIndex; diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js index 96c8fb35ff6..eb2ad76e161 100644 --- a/test/utils/structs/Merkletree.test.js +++ b/test/utils/structs/Merkletree.test.js @@ -1,6 +1,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); const { StandardMerkleTree } = require('@openzeppelin/merkle-tree'); const makeTree = (leafs = [ethers.ZeroHash]) => @@ -32,7 +33,7 @@ describe('Merklee tree', function () { }); it('setup', async function () { - const merkleTree = makeTree(Array(2 ** Number(DEPTH)).fill(ethers.ZeroHash)); + const merkleTree = makeTree(Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash)); expect(await this.mock.getDepth()).to.equal(DEPTH); expect(await this.mock.getLength()).to.equal(LENGTH); @@ -50,7 +51,7 @@ describe('Merklee tree', function () { describe('insert', function () { it('tree is correctly updated', async function () { - const leafs = Array(2 ** Number(DEPTH)).fill(ethers.ZeroHash); + const leafs = Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash); const roots = []; // for each leaf slot @@ -81,10 +82,9 @@ describe('Merklee tree', function () { }); it('revert when tree is full', async function () { - for (let i = 0; i < 2 ** Number(DEPTH); ++i) { - await this.mock.insert(ethers.ZeroHash); - } - await expect(this.mock.insert(ethers.ZeroHash)).to.be.revertedWithCustomError(this.mock, 'MerkleTreeFull'); + await Promise.all(Array.from({ length: 2 ** Number(DEPTH) }).map(() => this.mock.insert(ethers.ZeroHash))); + + await expect(this.mock.insert(ethers.ZeroHash)).to.be.revertedWithPanic(PANIC_CODES.TOO_MUCH_MEMORY_ALLOCATED); }); }); }); From cebdc2af3f91849d03edc106e64038be48c101d7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 6 Feb 2024 22:20:18 +0100 Subject: [PATCH 20/48] improve coverage --- contracts/mocks/ArraysMock.sol | 24 ++++++++++++++++++++++++ test/utils/Arrays.test.js | 10 +++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/contracts/mocks/ArraysMock.sol b/contracts/mocks/ArraysMock.sol index a2fbb6dea63..e7daa22d091 100644 --- a/contracts/mocks/ArraysMock.sol +++ b/contracts/mocks/ArraysMock.sol @@ -36,6 +36,14 @@ contract Uint256ArraysMock { function unsafeAccess(uint256 pos) external view returns (uint256) { return _array.unsafeAccess(pos).value; } + + function unsafeSetLength(uint256 newLength) external { + _array.unsafeSetLength(newLength); + } + + function length() external view returns (uint256) { + return _array.length; + } } contract AddressArraysMock { @@ -50,6 +58,14 @@ contract AddressArraysMock { function unsafeAccess(uint256 pos) external view returns (address) { return _array.unsafeAccess(pos).value; } + + function unsafeSetLength(uint256 newLength) external { + _array.unsafeSetLength(newLength); + } + + function length() external view returns (uint256) { + return _array.length; + } } contract Bytes32ArraysMock { @@ -64,4 +80,12 @@ contract Bytes32ArraysMock { function unsafeAccess(uint256 pos) external view returns (bytes32) { return _array.unsafeAccess(pos).value; } + + function unsafeSetLength(uint256 newLength) external { + _array.unsafeSetLength(newLength); + } + + function length() external view returns (uint256) { + return _array.length; + } } diff --git a/test/utils/Arrays.test.js b/test/utils/Arrays.test.js index ffe5d5a22fe..2e8ce23847e 100644 --- a/test/utils/Arrays.test.js +++ b/test/utils/Arrays.test.js @@ -154,7 +154,7 @@ describe('Arrays', function () { } }); - describe('unsafeAccess', function () { + describe('unsafe', function () { for (const [type, { artifact, elements }] of Object.entries({ address: { artifact: 'AddressArraysMock', elements: randomArray(generators.address, 10) }, bytes32: { artifact: 'Bytes32ArraysMock', elements: randomArray(generators.bytes32, 10) }, @@ -179,6 +179,14 @@ describe('Arrays', function () { it('unsafeAccess outside bounds', async function () { await expect(this.instance.unsafeAccess(elements.length)).to.not.be.rejected; }); + + it('unsafeSetLength changes the length or the array', async function () { + const newLength = generators.uint256(); + + expect(await this.instance.length()).to.equal(elements.length); + await expect(this.instance.unsafeSetLength(newLength)).to.not.be.rejected; + expect(await this.instance.length()).to.equal(newLength); + }); }); describe('memory', function () { From d4ced94ae85169333c2fff6d0d820ed2e77085d6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 6 Feb 2024 22:40:38 +0100 Subject: [PATCH 21/48] test looparound property of memory arrays --- test/utils/Arrays.test.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/test/utils/Arrays.test.js b/test/utils/Arrays.test.js index 2e8ce23847e..f101ef3840d 100644 --- a/test/utils/Arrays.test.js +++ b/test/utils/Arrays.test.js @@ -155,10 +155,22 @@ describe('Arrays', function () { }); describe('unsafe', function () { - for (const [type, { artifact, elements }] of Object.entries({ - address: { artifact: 'AddressArraysMock', elements: randomArray(generators.address, 10) }, - bytes32: { artifact: 'Bytes32ArraysMock', elements: randomArray(generators.bytes32, 10) }, - uint256: { artifact: 'Uint256ArraysMock', elements: randomArray(generators.uint256, 10) }, + for (const [type, { artifact, elements, format }] of Object.entries({ + address: { + artifact: 'AddressArraysMock', + elements: randomArray(generators.address, 10), + format: x => ethers.getAddress(ethers.toBeHex(x, 20)), + }, + bytes32: { + artifact: 'Bytes32ArraysMock', + elements: randomArray(generators.bytes32, 10), + format: x => ethers.toBeHex(x, 32), + }, + uint256: { + artifact: 'Uint256ArraysMock', + elements: randomArray(generators.uint256, 10), + format: x => ethers.toBigInt(x), + }, })) { describe(type, function () { describe('storage', function () { @@ -201,6 +213,14 @@ describe('Arrays', function () { it('unsafeMemoryAccess outside bounds', async function () { await expect(this.mock[fragment](elements, elements.length)).to.not.be.rejected; }); + + it('unsafeMemoryAccess loop around', async function () { + for (let i = 251n; i < 256n; ++i) { + expect(await this.mock[fragment](elements, 2n ** i - 1n)).to.equal(format(elements.length)); + expect(await this.mock[fragment](elements, 2n ** i + 0n)).to.equal(elements[0]); + expect(await this.mock[fragment](elements, 2n ** i + 1n)).to.equal(elements[1]); + } + }); }); }); } From a3a813c4f0207128f34c3255e1c48144a2082564 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 21:12:47 +0100 Subject: [PATCH 22/48] =?UTF-8?q?rename=20initialize=20=E2=86=92=20setUp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/mocks/MerkleTreeMock.sol | 2 +- contracts/utils/structs/MerkleTree.sol | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index 2bef4983dbe..ee9a6edfc88 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -10,7 +10,7 @@ contract MerkleTreeMock { MerkleTree.TreeWithHistory private _tree; constructor(uint256 _depth, uint256 _length, bytes32 _zero) { - _tree.initialize(_depth, _length, _zero); + _tree.setUp(_depth, _length, _zero); } function insert(bytes32 leaf) public returns (uint256) { diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 694d120b5f1..ea6ccf0fadd 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -20,12 +20,10 @@ import {Panic} from "../Panic.sol"; * - zero values (for "empty" leaves) * - hash function * - * WARNING: + * WARNING: By design, the tree include zero leaves. Customizing the "zero value" might be necessary to ensure that + * empty leaves being provably part of the tree is not a security issue. * - * By design, the tree include zero leaves. Customizing the "zero value" might be necessary to ensure that empty leaves - * being provably part of the tree is not a security issue. - * - * _Available since v4.x._ + * _Available since v5.1._ */ library MerkleTree { /** @@ -35,7 +33,7 @@ library MerkleTree { uint256 private constant MAX_DEPTH = 255; /** - * @dev Merkle tree cannot be initialized because requested depth is to large. + * @dev Merkle tree cannot be set-up because requested depth is to large. */ error MerkleTreeInvalidDepth(uint256 depth, uint256 maxDepth); @@ -60,8 +58,8 @@ library MerkleTree { /** * @dev Initialize using the default hash */ - function initialize(TreeWithHistory storage self, uint256 depth, uint256 length, bytes32 zero) internal { - return initialize(self, depth, length, zero, MerkleProof.hashPair); + function setUp(TreeWithHistory storage self, uint256 depth, uint256 length, bytes32 zero) internal { + return setUp(self, depth, length, zero, MerkleProof.hashPair); } /** @@ -71,7 +69,7 @@ library MerkleTree { * - Hashing function for a pair of leaves is fnHash * and keep a root history of length `length` when leaves are inserted. */ - function initialize( + function setUp( TreeWithHistory storage self, uint256 depth, uint256 length, From ec05d193b3e394577bd02b776ab5940a9ab3c14d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 21:13:33 +0100 Subject: [PATCH 23/48] Update contracts/utils/structs/MerkleTree.sol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto GarcĂ­a --- contracts/utils/structs/MerkleTree.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index ea6ccf0fadd..26f194b1962 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -109,7 +109,7 @@ library MerkleTree { uint256 leafIndex = self.nextLeafIndex++; // Check if tree is full. - if (leafIndex == 1 << depth) Panic.panic(Panic.RESOURCE_ERROR); + if (leafIndex >= 1 << depth) Panic.panic(Panic.RESOURCE_ERROR); // Rebuild branch from leaf to root uint256 currentIndex = leafIndex; From ec3d96b5af9c9b60a729a569dadd880ae589e6c1 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 12 Feb 2024 18:16:40 +0100 Subject: [PATCH 24/48] fix lint --- test/utils/Arrays.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/Arrays.test.js b/test/utils/Arrays.test.js index 1d4f6688d4b..0d088fa1c31 100644 --- a/test/utils/Arrays.test.js +++ b/test/utils/Arrays.test.js @@ -116,7 +116,7 @@ describe('Arrays', function () { } }); - for (const [type, { artifact, elements, comp }] of Object.entries({ + for (const [type, { artifact, elements, comp, format }] of Object.entries({ address: { artifact: 'AddressArraysMock', elements: Array.from({ length: 10 }, generators.address), From bcc06674ecadaeb9c623669f79db05c33b0a78d2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 12 Feb 2024 18:24:32 +0100 Subject: [PATCH 25/48] cleanup --- test/utils/Arrays.test.js | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/test/utils/Arrays.test.js b/test/utils/Arrays.test.js index 0d088fa1c31..30685199333 100644 --- a/test/utils/Arrays.test.js +++ b/test/utils/Arrays.test.js @@ -17,6 +17,7 @@ const upperBound = (array, value) => { }; const bigintSign = x => (x > 0n ? 1 : x < 0n ? -1 : 0); +const comparator = (a, b) => bigintSign(ethers.toBigInt(a) - ethers.toBigInt(b)); const hasDuplicates = array => array.some((v, i) => array.indexOf(v) != i); describe('Arrays', function () { @@ -116,26 +117,22 @@ describe('Arrays', function () { } }); - for (const [type, { artifact, elements, comp, format }] of Object.entries({ + for (const [type, { artifact, format }] of Object.entries({ address: { artifact: 'AddressArraysMock', - elements: Array.from({ length: 10 }, generators.address), - comp: (a, b) => bigintSign(ethers.toBigInt(a) - ethers.toBigInt(b)), format: x => ethers.getAddress(ethers.toBeHex(x, 20)), }, bytes32: { artifact: 'Bytes32ArraysMock', - elements: Array.from({ length: 10 }, generators.bytes32), - comp: (a, b) => bigintSign(ethers.toBigInt(a) - ethers.toBigInt(b)), format: x => ethers.toBeHex(x, 32), }, uint256: { artifact: 'Uint256ArraysMock', - elements: Array.from({ length: 10 }, generators.uint256), - comp: (a, b) => bigintSign(a - b), format: x => ethers.toBigInt(x), }, })) { + const elements = Array.from({ length: 10 }, generators[type]); + describe(type, function () { const fixture = async () => { return { instance: await ethers.deployContract(artifact, [elements]) }; @@ -149,14 +146,14 @@ describe('Arrays', function () { for (const length of [0, 1, 2, 8, 32, 128]) { describe(`${type}[] of length ${length}`, function () { beforeEach(async function () { - this.elements = Array.from({ length }, generators[type]); + this.array = Array.from({ length }, generators[type]); }); afterEach(async function () { - const expected = Array.from(this.elements).sort(comp); + const expected = Array.from(this.array).sort(comparator); const reversed = Array.from(expected).reverse(); - expect(await this.instance.sort(this.elements)).to.deep.equal(expected); - expect(await this.instance.sortReverse(this.elements)).to.deep.equal(reversed); + expect(await this.instance.sort(this.array)).to.deep.equal(expected); + expect(await this.instance.sortReverse(this.array)).to.deep.equal(reversed); }); it('sort array', async function () { @@ -166,23 +163,23 @@ describe('Arrays', function () { if (length > 1) { it('sort array for identical elements', async function () { // duplicate the first value to all elements - this.elements.fill(this.elements.at(0)); + this.array.fill(this.array.at(0)); }); it('sort already sorted array', async function () { // pre-sort the elements - this.elements.sort(comp); + this.array.sort(comparator); }); it('sort reversed array', async function () { // pre-sort in reverse order - this.elements.sort(comp).reverse(); + this.array.sort(comparator).reverse(); }); it('sort almost sorted array', async function () { // pre-sort + rotate (move the last element to the front) for an almost sorted effect - this.elements.sort(comp); - this.elements.unshift(this.elements.pop()); + this.array.sort(comparator); + this.array.unshift(this.array.pop()); }); } }); From 5b152051142e6c64c30f91263e9bdde114248c6a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 19 Feb 2024 19:36:34 +0100 Subject: [PATCH 26/48] remove root history from the MerkleTree structure --- contracts/mocks/MerkleTreeMock.sol | 24 ++-------- contracts/utils/structs/MerkleTree.sol | 64 +++++++------------------- test/utils/structs/Merkletree.test.js | 29 ++---------- 3 files changed, 24 insertions(+), 93 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index ee9a6edfc88..7c99e35b31e 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -9,8 +9,8 @@ contract MerkleTreeMock { MerkleTree.TreeWithHistory private _tree; - constructor(uint256 _depth, uint256 _length, bytes32 _zero) { - _tree.setUp(_depth, _length, _zero); + constructor(uint256 _depth, bytes32 _zero) { + _tree.setUp(_depth, _zero); } function insert(bytes32 leaf) public returns (uint256) { @@ -21,23 +21,11 @@ contract MerkleTreeMock { return _tree.getDepth(); } - function getLength() public view returns (uint256) { - return _tree.getLength(); - } - - function getLastRoot() public view returns (bytes32) { - return _tree.getLastRoot(); - } - - function isKnownRoot(bytes32 root) public view returns (bool) { - return _tree.isKnownRoot(root); + function getRoot() public view returns (bytes32) { + return _tree.getRoot(); } // internal state - function currentRootIndex() public view returns (uint256) { - return _tree.currentRootIndex; - } - function nextLeafIndex() public view returns (uint256) { return _tree.nextLeafIndex; } @@ -49,8 +37,4 @@ contract MerkleTreeMock { function zeros(uint256 i) public view returns (bytes32) { return _tree.zeros[i]; } - - function roots(uint256 i) public view returns (bytes32) { - return _tree.roots[i]; - } } diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 26f194b1962..c78d0eadca7 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -41,38 +41,36 @@ library MerkleTree { * @dev The `sides` and `zero` arrays are set, at initialization, to have a length equal to the depth of the tree. * No push/pop operations should be performed of these array, and their lengths should not be updated. * - * The `roots` array stores the history of roots. Its length is set at initialization, and should not be updated. - * * The hashing function used during initialization to compute the `zeros` values (value of a node at a given depth * for which the subtree is full of zero leaves). This function is kept in the structure for handling insertions. + * + * Developper using this structure may want to use a secondary structure to store a (partial) list of historical + * roots. */ struct TreeWithHistory { - uint256 currentRootIndex; + bytes32 root; uint256 nextLeafIndex; bytes32[] sides; bytes32[] zeros; - bytes32[] roots; function(bytes32, bytes32) view returns (bytes32) fnHash; } /** * @dev Initialize using the default hash */ - function setUp(TreeWithHistory storage self, uint256 depth, uint256 length, bytes32 zero) internal { - return setUp(self, depth, length, zero, MerkleProof.hashPair); + function setUp(TreeWithHistory storage self, uint256 depth, bytes32 zero) internal { + return setUp(self, depth, zero, MerkleProof.hashPair); } /** * @dev Initialize a new complete MerkleTree defined by: * - Depth `depth` * - All leaves are initialize to `zero` - * - Hashing function for a pair of leaves is fnHash - * and keep a root history of length `length` when leaves are inserted. + * - Hashing function for a pair of leaves is fnHash. */ function setUp( TreeWithHistory storage self, uint256 depth, - uint256 length, bytes32 zero, function(bytes32, bytes32) view returns (bytes32) fnHash ) internal { @@ -83,7 +81,6 @@ library MerkleTree { // Store depth & length in the dynamic array Arrays.unsafeSetLength(self.sides, depth); Arrays.unsafeSetLength(self.zeros, depth); - Arrays.unsafeSetLength(self.roots, length); self.fnHash = fnHash; // Build the different hashes in a zero-filled complete tree @@ -92,13 +89,12 @@ library MerkleTree { Arrays.unsafeAccess(self.zeros, i).value = currentZero; currentZero = fnHash(currentZero, currentZero); } - - // Insert the first root - Arrays.unsafeAccess(self.roots, 0).value = currentZero; + // Set the first root + self.root = currentZero; } /** - * @dev Insert a new leaf in the tree, compute the new root, and store that new root in the history. + * @dev Insert a new leaf in the tree, and compute the new root. */ function insert(TreeWithHistory storage self, bytes32 leaf) internal returns (uint256) { // Cache read @@ -109,7 +105,9 @@ library MerkleTree { uint256 leafIndex = self.nextLeafIndex++; // Check if tree is full. - if (leafIndex >= 1 << depth) Panic.panic(Panic.RESOURCE_ERROR); + if (leafIndex >= 1 << depth) { + Panic.panic(Panic.RESOURCE_ERROR); + } // Rebuild branch from leaf to root uint256 currentIndex = leafIndex; @@ -136,8 +134,7 @@ library MerkleTree { } // Record new root - self.currentRootIndex = (self.currentRootIndex + 1) % self.roots.length; - Arrays.unsafeAccess(self.roots, self.currentRootIndex).value = currentLevelHash; + self.root = currentLevelHash; return leafIndex; } @@ -149,39 +146,10 @@ library MerkleTree { return self.zeros.length; } - /** - * @dev History length (set at initialization) - */ - function getLength(TreeWithHistory storage self) internal view returns (uint256) { - return self.roots.length; - } - /** * @dev Return the current root of the tree. */ - function getLastRoot(TreeWithHistory storage self) internal view returns (bytes32) { - return Arrays.unsafeAccess(self.roots, self.currentRootIndex).value; - } - - /** - * @dev Look in root history, - */ - function isKnownRoot(TreeWithHistory storage self, bytes32 root) internal view returns (bool) { - if (root == 0) { - return false; - } - - // Cache read - uint256 currentRootIndex = self.currentRootIndex; - uint256 length = self.roots.length; - - // Search (most recents first) - for (uint256 i = length; i > 0; --i) { - if (root == Arrays.unsafeAccess(self.roots, (currentRootIndex + i) % length).value) { - return true; - } - } - - return false; + function getRoot(TreeWithHistory storage self) internal view returns (bytes32) { + return self.root; } } diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js index eb2ad76e161..1fd0d062c71 100644 --- a/test/utils/structs/Merkletree.test.js +++ b/test/utils/structs/Merkletree.test.js @@ -13,11 +13,10 @@ const makeTree = (leafs = [ethers.ZeroHash]) => const MAX_DEPTH = 255n; const DEPTH = 4n; // 16 slots -const LENGTH = 8n; const ZERO = makeTree().leafHash([ethers.ZeroHash]); async function fixture() { - return { mock: await ethers.deployContract('MerkleTreeMock', [DEPTH, LENGTH, ZERO]) }; + return { mock: await ethers.deployContract('MerkleTreeMock', [DEPTH, ZERO]) }; } describe('Merklee tree', function () { @@ -27,7 +26,7 @@ describe('Merklee tree', function () { it('depth is limited', async function () { const invalidDepth = MAX_DEPTH + 1n; - await expect(ethers.deployContract('MerkleTreeMock', [invalidDepth, LENGTH, ZERO])) + await expect(ethers.deployContract('MerkleTreeMock', [invalidDepth, ZERO])) .to.be.revertedWithCustomError({ interface: this.mock.interface }, 'MerkleTreeInvalidDepth') .withArgs(invalidDepth, MAX_DEPTH); }); @@ -36,23 +35,13 @@ describe('Merklee tree', function () { const merkleTree = makeTree(Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash)); expect(await this.mock.getDepth()).to.equal(DEPTH); - expect(await this.mock.getLength()).to.equal(LENGTH); - expect(await this.mock.currentRootIndex()).to.equal(0n); expect(await this.mock.nextLeafIndex()).to.equal(0n); - expect(await this.mock.getLastRoot()).to.equal(merkleTree.root); - - for (let i = 0; i < LENGTH; ++i) { - expect(await this.mock.roots(i)).to.equal(i === 0 ? merkleTree.root : ethers.ZeroHash); - } - - expect(await this.mock.isKnownRoot(merkleTree.root)).to.be.true; - expect(await this.mock.isKnownRoot(ethers.ZeroHash)).to.be.false; + expect(await this.mock.getRoot()).to.equal(merkleTree.root); }); describe('insert', function () { it('tree is correctly updated', async function () { const leafs = Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash); - const roots = []; // for each leaf slot for (const i in leafs) { @@ -66,18 +55,8 @@ describe('Merklee tree', function () { await this.mock.insert(merkleTree.leafHash([leafs[i]])); // check tree - expect(await this.mock.currentRootIndex()).to.equal((BigInt(i) + 1n) % LENGTH); + expect(await this.mock.getRoot()).to.equal(merkleTree.root); expect(await this.mock.nextLeafIndex()).to.equal(BigInt(i) + 1n); - expect(await this.mock.getLastRoot()).to.equal(merkleTree.root); - - // check root history - roots.push(merkleTree.root); - for (const root of roots.slice(0, -Number(LENGTH))) { - expect(await this.mock.isKnownRoot(root)).to.be.false; - } - for (const root of roots.slice(-Number(LENGTH))) { - expect(await this.mock.isKnownRoot(root)).to.be.true; - } } }); From b390790d336182ef9f8203a8c2359feebe3d3ce5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 19 Feb 2024 19:49:22 +0100 Subject: [PATCH 27/48] Add Hashes.sol --- .changeset/odd-files-protect.md | 5 ++++ contracts/utils/README.adoc | 7 +++-- contracts/utils/cryptography/Hashes.sol | 28 +++++++++++++++++++ contracts/utils/cryptography/MerkleProof.sol | 29 ++++---------------- contracts/utils/structs/MerkleTree.sol | 4 +-- test/utils/structs/Merkletree.test.js | 2 +- 6 files changed, 47 insertions(+), 28 deletions(-) create mode 100644 .changeset/odd-files-protect.md create mode 100644 contracts/utils/cryptography/Hashes.sol diff --git a/.changeset/odd-files-protect.md b/.changeset/odd-files-protect.md new file mode 100644 index 00000000000..78eb991b407 --- /dev/null +++ b/.changeset/odd-files-protect.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Hashes`: new library of commonly used hashes. diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 68e228321f7..e34572b9017 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -9,6 +9,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {SafeCast}: Checked downcasting functions to avoid silent truncation. * {ECDSA}, {MessageHashUtils}: Libraries for interacting with ECDSA signatures. * {SignatureChecker}: A library helper to support regular ECDSA from EOAs as well as ERC-1271 signatures for smart contracts. + * {Hashes}: Commonly used hash functions. * {MerkleProof}: Functions for verifying https://en.wikipedia.org/wiki/Merkle_tree[Merkle Tree] proofs. * {EIP712}: Contract with functions to allow processing signed typed structure data according to https://eips.ethereum.org/EIPS/eip-712[EIP-712]. * {ReentrancyGuard}: A modifier that can prevent reentrancy during certain functions. @@ -48,13 +49,15 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable {{ECDSA}} +{{EIP712}} + {{MessageHashUtils}} {{SignatureChecker}} -{{MerkleProof}} +{{Hashes}} -{{EIP712}} +{{MerkleProof}} == Security diff --git a/contracts/utils/cryptography/Hashes.sol b/contracts/utils/cryptography/Hashes.sol new file mode 100644 index 00000000000..436a61704cb --- /dev/null +++ b/contracts/utils/cryptography/Hashes.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + + +/** + * @dev Library of standard hash functions. + */ +library Hashes { + /** + * @dev Keccak256 hash of a sorted pair of bytes32. Frequently used when working with merkle proofs. + */ + function stdPairHash(bytes32 a, bytes32 b) internal pure returns (bytes32) { + return a < b ? _efficientHash(a, b) : _efficientHash(b, a); + } + + /** + * @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory. + */ + function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, a) + mstore(0x20, b) + value := keccak256(0x00, 0x40) + } + } +} \ No newline at end of file diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index a52476bfc1a..8b51f091482 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.20; +import { Hashes } from "./Hashes.sol"; + /** * @dev These functions deal with verification of Merkle Tree proofs. * @@ -49,7 +51,7 @@ library MerkleProof { function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { bytes32 computedHash = leaf; for (uint256 i = 0; i < proof.length; i++) { - computedHash = hashPair(computedHash, proof[i]); + computedHash = Hashes.stdPairHash(computedHash, proof[i]); } return computedHash; } @@ -60,7 +62,7 @@ library MerkleProof { function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) { bytes32 computedHash = leaf; for (uint256 i = 0; i < proof.length; i++) { - computedHash = hashPair(computedHash, proof[i]); + computedHash = Hashes.stdPairHash(computedHash, proof[i]); } return computedHash; } @@ -138,7 +140,7 @@ library MerkleProof { bytes32 b = proofFlags[i] ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) : proof[proofPos++]; - hashes[i] = hashPair(a, b); + hashes[i] = Hashes.stdPairHash(a, b); } if (totalHashes > 0) { @@ -194,7 +196,7 @@ library MerkleProof { bytes32 b = proofFlags[i] ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) : proof[proofPos++]; - hashes[i] = hashPair(a, b); + hashes[i] = Hashes.stdPairHash(a, b); } if (totalHashes > 0) { @@ -210,23 +212,4 @@ library MerkleProof { return proof[0]; } } - - /** - * @dev Sorts the pair (a, b) and hashes the result. - */ - function hashPair(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? _efficientHash(a, b) : _efficientHash(b, a); - } - - /** - * @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory. - */ - function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { - /// @solidity memory-safe-assembly - assembly { - mstore(0x00, a) - mstore(0x20, b) - value := keccak256(0x00, 0x40) - } - } } diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index c78d0eadca7..f938cb3d734 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import {MerkleProof} from "../cryptography/MerkleProof.sol"; +import {Hashes} from "../cryptography/Hashes.sol"; import {Arrays} from "../Arrays.sol"; import {Panic} from "../Panic.sol"; @@ -59,7 +59,7 @@ library MerkleTree { * @dev Initialize using the default hash */ function setUp(TreeWithHistory storage self, uint256 depth, bytes32 zero) internal { - return setUp(self, depth, zero, MerkleProof.hashPair); + return setUp(self, depth, zero, Hashes.stdPairHash); } /** diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js index 1fd0d062c71..2b79f4e2e12 100644 --- a/test/utils/structs/Merkletree.test.js +++ b/test/utils/structs/Merkletree.test.js @@ -34,9 +34,9 @@ describe('Merklee tree', function () { it('setup', async function () { const merkleTree = makeTree(Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash)); + expect(await this.mock.getRoot()).to.equal(merkleTree.root); expect(await this.mock.getDepth()).to.equal(DEPTH); expect(await this.mock.nextLeafIndex()).to.equal(0n); - expect(await this.mock.getRoot()).to.equal(merkleTree.root); }); describe('insert', function () { From e331674266073e3ebc02755814fc7218ec1357dc Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 19 Feb 2024 20:04:57 +0100 Subject: [PATCH 28/48] fix-lint --- contracts/utils/cryptography/Hashes.sol | 3 +-- contracts/utils/cryptography/MerkleProof.sol | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/utils/cryptography/Hashes.sol b/contracts/utils/cryptography/Hashes.sol index 436a61704cb..ed15ef5f060 100644 --- a/contracts/utils/cryptography/Hashes.sol +++ b/contracts/utils/cryptography/Hashes.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; - /** * @dev Library of standard hash functions. */ @@ -25,4 +24,4 @@ library Hashes { value := keccak256(0x00, 0x40) } } -} \ No newline at end of file +} diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index 8b51f091482..254a55613c2 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; -import { Hashes } from "./Hashes.sol"; +import {Hashes} from "./Hashes.sol"; /** * @dev These functions deal with verification of Merkle Tree proofs. From 91f7057989a92732d9aab63a6a6a971c3c3d7a9b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 19 Feb 2024 20:50:20 +0100 Subject: [PATCH 29/48] rename to reflect removal of history --- contracts/mocks/MerkleTreeMock.sol | 4 ++-- contracts/utils/structs/MerkleTree.sol | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index 7c99e35b31e..4ece559df54 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -5,9 +5,9 @@ pragma solidity ^0.8.0; import {MerkleTree} from "../utils/structs/MerkleTree.sol"; contract MerkleTreeMock { - using MerkleTree for MerkleTree.TreeWithHistory; + using MerkleTree for MerkleTree.Bytes32MerkleTree; - MerkleTree.TreeWithHistory private _tree; + MerkleTree.Bytes32MerkleTree private _tree; constructor(uint256 _depth, bytes32 _zero) { _tree.setUp(_depth, _zero); diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index f938cb3d734..35ce5413ead 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -47,7 +47,7 @@ library MerkleTree { * Developper using this structure may want to use a secondary structure to store a (partial) list of historical * roots. */ - struct TreeWithHistory { + struct Bytes32MerkleTree { bytes32 root; uint256 nextLeafIndex; bytes32[] sides; @@ -58,7 +58,7 @@ library MerkleTree { /** * @dev Initialize using the default hash */ - function setUp(TreeWithHistory storage self, uint256 depth, bytes32 zero) internal { + function setUp(Bytes32MerkleTree storage self, uint256 depth, bytes32 zero) internal { return setUp(self, depth, zero, Hashes.stdPairHash); } @@ -69,7 +69,7 @@ library MerkleTree { * - Hashing function for a pair of leaves is fnHash. */ function setUp( - TreeWithHistory storage self, + Bytes32MerkleTree storage self, uint256 depth, bytes32 zero, function(bytes32, bytes32) view returns (bytes32) fnHash @@ -96,7 +96,7 @@ library MerkleTree { /** * @dev Insert a new leaf in the tree, and compute the new root. */ - function insert(TreeWithHistory storage self, bytes32 leaf) internal returns (uint256) { + function insert(Bytes32MerkleTree storage self, bytes32 leaf) internal returns (uint256) { // Cache read uint256 depth = self.zeros.length; function(bytes32, bytes32) view returns (bytes32) fnHash = self.fnHash; @@ -142,14 +142,14 @@ library MerkleTree { /** * @dev Tree's depth (set at initialization) */ - function getDepth(TreeWithHistory storage self) internal view returns (uint256) { + function getDepth(Bytes32MerkleTree storage self) internal view returns (uint256) { return self.zeros.length; } /** * @dev Return the current root of the tree. */ - function getRoot(TreeWithHistory storage self) internal view returns (bytes32) { + function getRoot(Bytes32MerkleTree storage self) internal view returns (bytes32) { return self.root; } } From 088fa8c6036784214fe08af4d3091f9216879339 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 19 Feb 2024 20:52:24 +0100 Subject: [PATCH 30/48] =?UTF-8?q?rename=20setUp=20=E2=86=92=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/mocks/MerkleTreeMock.sol | 2 +- contracts/utils/structs/MerkleTree.sol | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index 4ece559df54..859ee1a0469 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -10,7 +10,7 @@ contract MerkleTreeMock { MerkleTree.Bytes32MerkleTree private _tree; constructor(uint256 _depth, bytes32 _zero) { - _tree.setUp(_depth, _zero); + _tree.setup(_depth, _zero); } function insert(bytes32 leaf) public returns (uint256) { diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 35ce5413ead..e0f4e2f85a6 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -58,8 +58,8 @@ library MerkleTree { /** * @dev Initialize using the default hash */ - function setUp(Bytes32MerkleTree storage self, uint256 depth, bytes32 zero) internal { - return setUp(self, depth, zero, Hashes.stdPairHash); + function setup(Bytes32MerkleTree storage self, uint256 depth, bytes32 zero) internal { + return setup(self, depth, zero, Hashes.stdPairHash); } /** @@ -68,7 +68,7 @@ library MerkleTree { * - All leaves are initialize to `zero` * - Hashing function for a pair of leaves is fnHash. */ - function setUp( + function setup( Bytes32MerkleTree storage self, uint256 depth, bytes32 zero, From 2d869b78be37aad50f72ab0d45803f7ddaebc2ee Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 20 Feb 2024 10:17:24 +0100 Subject: [PATCH 31/48] doc --- contracts/utils/structs/MerkleTree.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index e0f4e2f85a6..2c6d841569e 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -44,8 +44,10 @@ library MerkleTree { * The hashing function used during initialization to compute the `zeros` values (value of a node at a given depth * for which the subtree is full of zero leaves). This function is kept in the structure for handling insertions. * - * Developper using this structure may want to use a secondary structure to store a (partial) list of historical - * roots. + * Contracts using this structure may want to use a secondary structure to store a (partial) list of historical + * roots. This could be done using a cicular buffer (to keep the last N roots) or the {Checkpoints} library to + * keep a more complete history. Note that if using the Checkpoints.Trace224 structure for storing roots, you will + * be limited to keeping "only" 26 bytes out of the root's 32. This should not be a security issue. */ struct Bytes32MerkleTree { bytes32 root; From 567cd3e744d520fa3a6c250a26c8d2b5c1fbef02 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 20 Feb 2024 13:59:00 +0100 Subject: [PATCH 32/48] Update contracts/utils/structs/MerkleTree.sol --- contracts/utils/structs/MerkleTree.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 2c6d841569e..0eaa85a6bca 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -45,7 +45,7 @@ library MerkleTree { * for which the subtree is full of zero leaves). This function is kept in the structure for handling insertions. * * Contracts using this structure may want to use a secondary structure to store a (partial) list of historical - * roots. This could be done using a cicular buffer (to keep the last N roots) or the {Checkpoints} library to + * roots. This could be done using a circular buffer (to keep the last N roots) or the {Checkpoints} library to * keep a more complete history. Note that if using the Checkpoints.Trace224 structure for storing roots, you will * be limited to keeping "only" 26 bytes out of the root's 32. This should not be a security issue. */ From a13237aa282d240dcf3815714c9b3afaa2214ef8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 20 Feb 2024 14:06:07 +0100 Subject: [PATCH 33/48] Update MerkleTree.sol --- contracts/utils/structs/MerkleTree.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 0eaa85a6bca..1763ff96d1f 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -69,6 +69,8 @@ library MerkleTree { * - Depth `depth` * - All leaves are initialize to `zero` * - Hashing function for a pair of leaves is fnHash. + * + * If the MerkleTree was already setup and used, calling that function again will reset it to a blank state. */ function setup( Bytes32MerkleTree storage self, @@ -80,10 +82,9 @@ library MerkleTree { revert MerkleTreeInvalidDepth(depth, MAX_DEPTH); } - // Store depth & length in the dynamic array + // Store depth in the dynamic array Arrays.unsafeSetLength(self.sides, depth); Arrays.unsafeSetLength(self.zeros, depth); - self.fnHash = fnHash; // Build the different hashes in a zero-filled complete tree bytes32 currentZero = zero; @@ -93,6 +94,8 @@ library MerkleTree { } // Set the first root self.root = currentZero; + self.nextLeafIndex = 0; + self.fnHash = fnHash; } /** From 03bea3eebb5064de8502034a4662f253e22f9dd0 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 20 Feb 2024 19:04:55 -0600 Subject: [PATCH 34/48] Update changesets and fix some comments --- .changeset/odd-files-protect.md | 2 +- .changeset/warm-sheep-cover.md | 2 +- contracts/utils/structs/MerkleTree.sol | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.changeset/odd-files-protect.md b/.changeset/odd-files-protect.md index 78eb991b407..8b334acfd11 100644 --- a/.changeset/odd-files-protect.md +++ b/.changeset/odd-files-protect.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`Hashes`: new library of commonly used hashes. +`Hashes`: A library with commonly used hash functions. diff --git a/.changeset/warm-sheep-cover.md b/.changeset/warm-sheep-cover.md index c0418a6f7e3..f0a2ebaa256 100644 --- a/.changeset/warm-sheep-cover.md +++ b/.changeset/warm-sheep-cover.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`MerkleTree`: a new data structure that keep track of merkle roots as values are inserted into a merkle tree. +`MerkleTree`: A data structure that allows inserting elements into a merkle tree and updating its root hash. diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 1763ff96d1f..637104222f1 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -12,7 +12,7 @@ import {Panic} from "../Panic.sol"; * are not stored, but can be proven to be part of the tree. * * The history of merkle roots allow inclusion proofs to remain valid even if leaves are inserted into the tree between - * the moment the proof is generated and the moment it's verified. + * the moment the proof is generated and the moment it's verified. * * Each tree can be customized to use specific * - depth @@ -20,7 +20,7 @@ import {Panic} from "../Panic.sol"; * - zero values (for "empty" leaves) * - hash function * - * WARNING: By design, the tree include zero leaves. Customizing the "zero value" might be necessary to ensure that + * IMPORTANT: By design, the tree include zero leaves. Customizing the "zero value" might be necessary to ensure that * empty leaves being provably part of the tree is not a security issue. * * _Available since v5.1._ @@ -39,7 +39,7 @@ library MerkleTree { /** * @dev The `sides` and `zero` arrays are set, at initialization, to have a length equal to the depth of the tree. - * No push/pop operations should be performed of these array, and their lengths should not be updated. + * No push/pop operations should be performed of these arrays, and their lengths should not be updated. * * The hashing function used during initialization to compute the `zeros` values (value of a node at a given depth * for which the subtree is full of zero leaves). This function is kept in the structure for handling insertions. @@ -58,7 +58,7 @@ library MerkleTree { } /** - * @dev Initialize using the default hash + * @dev Initialize using {Hashes-stdPairHash} as the hashing function for a pair of leaves. */ function setup(Bytes32MerkleTree storage self, uint256 depth, bytes32 zero) internal { return setup(self, depth, zero, Hashes.stdPairHash); From c475badb8f602d5887a9a07c73f7cf72f013eb25 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 20 Feb 2024 21:59:21 -0600 Subject: [PATCH 35/48] Simplify --- contracts/mocks/MerkleTreeMock.sol | 4 +- contracts/utils/structs/MerkleTree.sol | 87 +++++++++++--------------- test/utils/structs/Merkletree.test.js | 12 +--- 3 files changed, 39 insertions(+), 64 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index 859ee1a0469..913cb8df9d9 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -9,7 +9,7 @@ contract MerkleTreeMock { MerkleTree.Bytes32MerkleTree private _tree; - constructor(uint256 _depth, bytes32 _zero) { + constructor(uint8 _depth, bytes32 _zero) { _tree.setup(_depth, _zero); } @@ -22,7 +22,7 @@ contract MerkleTreeMock { } function getRoot() public view returns (bytes32) { - return _tree.getRoot(); + return _tree.root; } // internal state diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 637104222f1..0b44c720dab 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -7,47 +7,34 @@ import {Arrays} from "../Arrays.sol"; import {Panic} from "../Panic.sol"; /** - * @dev A complete binary tree with the ability to sequentially insert leaves, changing them from a zero to a non-zero - * value, while keeping a history of merkle roots. This structure allows inserting commitment (or other entries) that - * are not stored, but can be proven to be part of the tree. + * @dev Library for managing https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] data structures. * - * The history of merkle roots allow inclusion proofs to remain valid even if leaves are inserted into the tree between - * the moment the proof is generated and the moment it's verified. + * Each tree is a complete binary tree with the ability to sequentially insert leaves, changing them from a zero to a + * non-zero value and updating its root. This structure allows inserting commitments (or other entries) that are not + * stored, but can be proven to be part of the tree at a later time. See {MerkleProof}. * - * Each tree can be customized to use specific - * - depth - * - length of the root history - * - zero values (for "empty" leaves) - * - hash function + * A tree is defined by the following parameters: * - * IMPORTANT: By design, the tree include zero leaves. Customizing the "zero value" might be necessary to ensure that - * empty leaves being provably part of the tree is not a security issue. + * * Depth: The number of levels in the tree, it also defines the maximum number of leaves as 2**depth. + * * 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 process pairs of leaves. * * _Available since v5.1._ */ library MerkleTree { /** - * @dev Maximum supported depth. Beyond that, some checks will fail to properly work. - * This should be enough for any realistic usecase. - */ - uint256 private constant MAX_DEPTH = 255; - - /** - * @dev Merkle tree cannot be set-up because requested depth is to large. - */ - error MerkleTreeInvalidDepth(uint256 depth, uint256 maxDepth); - - /** - * @dev The `sides` and `zero` arrays are set, at initialization, to have a length equal to the depth of the tree. - * No push/pop operations should be performed of these arrays, and their lengths should not be updated. + * @dev A complete `bytes32` Merkle tree. + * + * The `sides` and `zero` arrays are set to have a length equal to the depth of the tree during setup. * * The hashing function used during initialization to compute the `zeros` values (value of a node at a given depth * for which the subtree is full of zero leaves). This function is kept in the structure for handling insertions. * - * Contracts using this structure may want to use a secondary structure to store a (partial) list of historical - * roots. This could be done using a circular buffer (to keep the last N roots) or the {Checkpoints} library to - * keep a more complete history. Note that if using the Checkpoints.Trace224 structure for storing roots, you will - * be limited to keeping "only" 26 bytes out of the root's 32. This should not be a security issue. + * NOTE: The `root` is kept up to date after each insertion without keeping track of its history. Consider + * using a secondary structure to store a list of historical roots (e.g. a mapping, {BitMaps} or {Checkpoints} + * limited to 26 bytes if using {Checkpoints-Trace224}). + * + * WARNING: Updating any of the tree's parameters after the first insertion will result in a corrupted tree. */ struct Bytes32MerkleTree { bytes32 root; @@ -58,40 +45,41 @@ library MerkleTree { } /** - * @dev Initialize using {Hashes-stdPairHash} as the hashing function for a pair of leaves. + * @dev Initialize a {Bytes32MerkleTree} using {Hashes-stdPairHash} to hash pairs of leaves. + * The capacity of the tree (i.e. number of leaves) is set to `2**depth`. + * + * Calling this function on MerkleTree that was already setup and used will reset it to a blank state. + * + * IMPORTANT: The zero value should be carefully chosen since it will be stored in the tree representing + * empty leaves. It should be a value that is not expected to be part of the tree. */ - function setup(Bytes32MerkleTree storage self, uint256 depth, bytes32 zero) internal { + function setup(Bytes32MerkleTree storage self, uint8 depth, bytes32 zero) internal { return setup(self, depth, zero, Hashes.stdPairHash); } /** - * @dev Initialize a new complete MerkleTree defined by: - * - Depth `depth` - * - All leaves are initialize to `zero` - * - Hashing function for a pair of leaves is fnHash. + * @dev Same as {setup}, but allows to specify a custom hashing function. * - * If the MerkleTree was already setup and used, calling that function again will reset it to a blank state. + * IMPORTANT: Providing a custom hashing function is a security-sensitive operation since it may + * compromise the soundness of the tree. Consider using functions from {Hashes}. */ function setup( Bytes32MerkleTree storage self, - uint256 depth, + uint8 depth, bytes32 zero, function(bytes32, bytes32) view returns (bytes32) fnHash ) internal { - if (depth > MAX_DEPTH) { - revert MerkleTreeInvalidDepth(depth, MAX_DEPTH); - } - // Store depth in the dynamic array Arrays.unsafeSetLength(self.sides, depth); Arrays.unsafeSetLength(self.zeros, depth); - // Build the different hashes in a zero-filled complete tree + // Build each root of zero-filled subtrees bytes32 currentZero = zero; for (uint32 i = 0; i < depth; ++i) { Arrays.unsafeAccess(self.zeros, i).value = currentZero; currentZero = fnHash(currentZero, currentZero); } + // Set the first root self.root = currentZero; self.nextLeafIndex = 0; @@ -100,6 +88,9 @@ library MerkleTree { /** * @dev Insert a new leaf in the tree, and compute the new root. + * + * Hashing the leaf before calling this function is recommended as a protection against + * second pre-image attacks. */ function insert(Bytes32MerkleTree storage self, bytes32 leaf) internal returns (uint256) { // Cache read @@ -126,9 +117,8 @@ library MerkleTree { Arrays.unsafeAccess(self.sides, i).value = currentLevelHash; } - // Compute the node hash by hashing the current hash with either: - // - the last value for this level - // - the zero for this level + // Compute the current node hash by using the hash function + // with either the its sibling (side) or the zero value for that level. currentLevelHash = fnHash( isLeft ? currentLevelHash : Arrays.unsafeAccess(self.sides, i).value, isLeft ? Arrays.unsafeAccess(self.zeros, i).value : currentLevelHash @@ -150,11 +140,4 @@ library MerkleTree { function getDepth(Bytes32MerkleTree storage self) internal view returns (uint256) { return self.zeros.length; } - - /** - * @dev Return the current root of the tree. - */ - function getRoot(Bytes32MerkleTree storage self) internal view returns (bytes32) { - return self.root; - } } diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js index 2b79f4e2e12..8c948719513 100644 --- a/test/utils/structs/Merkletree.test.js +++ b/test/utils/structs/Merkletree.test.js @@ -11,7 +11,6 @@ const makeTree = (leafs = [ethers.ZeroHash]) => { sortLeaves: false }, ); -const MAX_DEPTH = 255n; const DEPTH = 4n; // 16 slots const ZERO = makeTree().leafHash([ethers.ZeroHash]); @@ -19,19 +18,12 @@ async function fixture() { return { mock: await ethers.deployContract('MerkleTreeMock', [DEPTH, ZERO]) }; } -describe('Merklee tree', function () { +describe('MerkleTree', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); - it('depth is limited', async function () { - const invalidDepth = MAX_DEPTH + 1n; - await expect(ethers.deployContract('MerkleTreeMock', [invalidDepth, ZERO])) - .to.be.revertedWithCustomError({ interface: this.mock.interface }, 'MerkleTreeInvalidDepth') - .withArgs(invalidDepth, MAX_DEPTH); - }); - - it('setup', async function () { + it('sets initial values at setup', async function () { const merkleTree = makeTree(Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash)); expect(await this.mock.getRoot()).to.equal(merkleTree.root); From 6a9e8732c64d4025fc40831a002c692359434d77 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 20 Feb 2024 22:03:49 -0600 Subject: [PATCH 36/48] Add Merkle Tree to the docs --- contracts/utils/README.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index e34572b9017..b7c1baf8db6 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -21,6 +21,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {EnumerableSet}: Like {EnumerableMap}, but for https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets]. Can be used to store privileged accounts, issued IDs, etc. * {DoubleEndedQueue}: An implementation of a https://en.wikipedia.org/wiki/Double-ended_queue[double ended queue] whose values can be removed added or remove from both sides. Useful for FIFO and LIFO structures. * {Checkpoints}: A data structure to store values mapped to an strictly increasing key. Can be used for storing and accessing values over time. + * {MerkleTree}: A library with https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] data structures and helper functions. * {Create2}: Wrapper around the https://blog.openzeppelin.com/getting-the-most-out-of-create2/[`CREATE2` EVM opcode] for safe use without having to deal with low-level assembly. * {Address}: Collection of functions for overloading Solidity's https://docs.soliditylang.org/en/latest/types.html#address[`address`] type. * {Arrays}: Collection of functions that operate on https://docs.soliditylang.org/en/latest/types.html#arrays[`arrays`]. @@ -91,6 +92,8 @@ Ethereum contracts have no native concept of an interface, so applications must {{Checkpoints}} +{{MerkleTree}} + == Libraries {{Create2}} From 1e59539bc2398a40ea498e27a29af048b6fe79f3 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 20 Feb 2024 22:09:13 -0600 Subject: [PATCH 37/48] Remove merkletree.test.js --- test/utils/structs/Merkletree.test.js | 61 --------------------------- 1 file changed, 61 deletions(-) delete mode 100644 test/utils/structs/Merkletree.test.js diff --git a/test/utils/structs/Merkletree.test.js b/test/utils/structs/Merkletree.test.js deleted file mode 100644 index 8c948719513..00000000000 --- a/test/utils/structs/Merkletree.test.js +++ /dev/null @@ -1,61 +0,0 @@ -const { ethers } = require('hardhat'); -const { expect } = require('chai'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); -const { StandardMerkleTree } = require('@openzeppelin/merkle-tree'); - -const makeTree = (leafs = [ethers.ZeroHash]) => - StandardMerkleTree.of( - leafs.map(leaf => [leaf]), - ['bytes32'], - { sortLeaves: false }, - ); - -const DEPTH = 4n; // 16 slots -const ZERO = makeTree().leafHash([ethers.ZeroHash]); - -async function fixture() { - return { mock: await ethers.deployContract('MerkleTreeMock', [DEPTH, ZERO]) }; -} - -describe('MerkleTree', function () { - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - it('sets initial values at setup', async function () { - const merkleTree = makeTree(Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash)); - - expect(await this.mock.getRoot()).to.equal(merkleTree.root); - expect(await this.mock.getDepth()).to.equal(DEPTH); - expect(await this.mock.nextLeafIndex()).to.equal(0n); - }); - - describe('insert', function () { - it('tree is correctly updated', async function () { - const leafs = Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash); - - // for each leaf slot - for (const i in leafs) { - // generate random leaf - leafs[i] = ethers.randomBytes(32); - - // update leaf list and rebuild tree. - const merkleTree = makeTree(leafs); - - // insert value in tree - await this.mock.insert(merkleTree.leafHash([leafs[i]])); - - // check tree - expect(await this.mock.getRoot()).to.equal(merkleTree.root); - expect(await this.mock.nextLeafIndex()).to.equal(BigInt(i) + 1n); - } - }); - - it('revert when tree is full', async function () { - await Promise.all(Array.from({ length: 2 ** Number(DEPTH) }).map(() => this.mock.insert(ethers.ZeroHash))); - - await expect(this.mock.insert(ethers.ZeroHash)).to.be.revertedWithPanic(PANIC_CODES.TOO_MUCH_MEMORY_ALLOCATED); - }); - }); -}); From 051107b6e5103c9f3fe69d5114c887c6ee62f5ee Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 20 Feb 2024 22:09:46 -0600 Subject: [PATCH 38/48] Recover MerkleTree.test.js --- test/utils/structs/MerkleTree.test.js | 61 +++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 test/utils/structs/MerkleTree.test.js diff --git a/test/utils/structs/MerkleTree.test.js b/test/utils/structs/MerkleTree.test.js new file mode 100644 index 00000000000..8c948719513 --- /dev/null +++ b/test/utils/structs/MerkleTree.test.js @@ -0,0 +1,61 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); +const { StandardMerkleTree } = require('@openzeppelin/merkle-tree'); + +const makeTree = (leafs = [ethers.ZeroHash]) => + StandardMerkleTree.of( + leafs.map(leaf => [leaf]), + ['bytes32'], + { sortLeaves: false }, + ); + +const DEPTH = 4n; // 16 slots +const ZERO = makeTree().leafHash([ethers.ZeroHash]); + +async function fixture() { + return { mock: await ethers.deployContract('MerkleTreeMock', [DEPTH, ZERO]) }; +} + +describe('MerkleTree', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + it('sets initial values at setup', async function () { + const merkleTree = makeTree(Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash)); + + expect(await this.mock.getRoot()).to.equal(merkleTree.root); + expect(await this.mock.getDepth()).to.equal(DEPTH); + expect(await this.mock.nextLeafIndex()).to.equal(0n); + }); + + describe('insert', function () { + it('tree is correctly updated', async function () { + const leafs = Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash); + + // for each leaf slot + for (const i in leafs) { + // generate random leaf + leafs[i] = ethers.randomBytes(32); + + // update leaf list and rebuild tree. + const merkleTree = makeTree(leafs); + + // insert value in tree + await this.mock.insert(merkleTree.leafHash([leafs[i]])); + + // check tree + expect(await this.mock.getRoot()).to.equal(merkleTree.root); + expect(await this.mock.nextLeafIndex()).to.equal(BigInt(i) + 1n); + } + }); + + it('revert when tree is full', async function () { + await Promise.all(Array.from({ length: 2 ** Number(DEPTH) }).map(() => this.mock.insert(ethers.ZeroHash))); + + await expect(this.mock.insert(ethers.ZeroHash)).to.be.revertedWithPanic(PANIC_CODES.TOO_MUCH_MEMORY_ALLOCATED); + }); + }); +}); From a1dd15841edd4b21cc0a13ac4ae0f1be14e5e367 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 21 Feb 2024 09:44:28 +0100 Subject: [PATCH 39/48] prefix variables with underscore to mark them as private (similar do DoubleEndedQueue) --- contracts/mocks/MerkleTreeMock.sol | 16 +++---- contracts/utils/structs/MerkleTree.sol | 60 +++++++++++++++----------- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index 913cb8df9d9..4bb497c00ea 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -13,28 +13,28 @@ contract MerkleTreeMock { _tree.setup(_depth, _zero); } - function insert(bytes32 leaf) public returns (uint256) { + function insert(bytes32 leaf) public returns (bytes32) { return _tree.insert(leaf); } - function getDepth() public view returns (uint256) { - return _tree.getDepth(); + function getRoot() public view returns (bytes32) { + return _tree.getRoot(); } - function getRoot() public view returns (bytes32) { - return _tree.root; + function getDepth() public view returns (uint256) { + return _tree.getDepth(); } // internal state function nextLeafIndex() public view returns (uint256) { - return _tree.nextLeafIndex; + return _tree._nextLeafIndex; } function sides(uint256 i) public view returns (bytes32) { - return _tree.sides[i]; + return _tree._sides[i]; } function zeros(uint256 i) public view returns (bytes32) { - return _tree.zeros[i]; + return _tree._zeros[i]; } } diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 0b44c720dab..5c93516aa52 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -17,7 +17,7 @@ import {Panic} from "../Panic.sol"; * * * Depth: The number of levels in the tree, it also defines the maximum number of leaves as 2**depth. * * 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 process pairs of leaves. + * * Hashing function: A cryptographic hash function used to produce internal nodes. * * _Available since v5.1._ */ @@ -30,22 +30,25 @@ library MerkleTree { * The hashing function used during initialization to compute the `zeros` values (value of a node at a given depth * for which the subtree is full of zero leaves). This function is kept in the structure for handling insertions. * + * Struct members have an underscore prefix indicating that they are "private" and should not be read or written to + * directly. Use the functions provided below instead. Modifying the struct manually may violate assumptions and + * lead to unexpected behavior. + * * NOTE: The `root` is kept up to date after each insertion without keeping track of its history. Consider - * using a secondary structure to store a list of historical roots (e.g. a mapping, {BitMaps} or {Checkpoints} - * limited to 26 bytes if using {Checkpoints-Trace224}). + * using a secondary structure to store a list of historical roots (e.g. a mapping, {BitMaps} or {Checkpoints}). * * WARNING: Updating any of the tree's parameters after the first insertion will result in a corrupted tree. */ struct Bytes32MerkleTree { - bytes32 root; - uint256 nextLeafIndex; - bytes32[] sides; - bytes32[] zeros; - function(bytes32, bytes32) view returns (bytes32) fnHash; + bytes32 _root; + uint256 _nextLeafIndex; + bytes32[] _sides; + bytes32[] _zeros; + function(bytes32, bytes32) view returns (bytes32) _fnHash; } /** - * @dev Initialize a {Bytes32MerkleTree} using {Hashes-stdPairHash} to hash pairs of leaves. + * @dev Initialize a {Bytes32MerkleTree} using {Hashes-stdPairHash} to hash internal nodes. * The capacity of the tree (i.e. number of leaves) is set to `2**depth`. * * Calling this function on MerkleTree that was already setup and used will reset it to a blank state. @@ -70,20 +73,20 @@ library MerkleTree { function(bytes32, bytes32) view returns (bytes32) fnHash ) internal { // Store depth in the dynamic array - Arrays.unsafeSetLength(self.sides, depth); - Arrays.unsafeSetLength(self.zeros, depth); + Arrays.unsafeSetLength(self._sides, depth); + Arrays.unsafeSetLength(self._zeros, depth); // Build each root of zero-filled subtrees bytes32 currentZero = zero; for (uint32 i = 0; i < depth; ++i) { - Arrays.unsafeAccess(self.zeros, i).value = currentZero; + Arrays.unsafeAccess(self._zeros, i).value = currentZero; currentZero = fnHash(currentZero, currentZero); } // Set the first root - self.root = currentZero; - self.nextLeafIndex = 0; - self.fnHash = fnHash; + self._root = currentZero; + self._nextLeafIndex = 0; + self._fnHash = fnHash; } /** @@ -92,13 +95,13 @@ library MerkleTree { * Hashing the leaf before calling this function is recommended as a protection against * second pre-image attacks. */ - function insert(Bytes32MerkleTree storage self, bytes32 leaf) internal returns (uint256) { + function insert(Bytes32MerkleTree storage self, bytes32 leaf) internal returns (bytes32) { // Cache read - uint256 depth = self.zeros.length; - function(bytes32, bytes32) view returns (bytes32) fnHash = self.fnHash; + uint256 depth = self._zeros.length; + function(bytes32, bytes32) view returns (bytes32) fnHash = self._fnHash; // Get leaf index - uint256 leafIndex = self.nextLeafIndex++; + uint256 leafIndex = self._nextLeafIndex++; // Check if tree is full. if (leafIndex >= 1 << depth) { @@ -114,14 +117,14 @@ library MerkleTree { // If so, next time we will come from the right, so we need to save it if (isLeft) { - Arrays.unsafeAccess(self.sides, i).value = currentLevelHash; + Arrays.unsafeAccess(self._sides, i).value = currentLevelHash; } // Compute the current node hash by using the hash function // with either the its sibling (side) or the zero value for that level. currentLevelHash = fnHash( - isLeft ? currentLevelHash : Arrays.unsafeAccess(self.sides, i).value, - isLeft ? Arrays.unsafeAccess(self.zeros, i).value : currentLevelHash + isLeft ? currentLevelHash : Arrays.unsafeAccess(self._sides, i).value, + isLeft ? Arrays.unsafeAccess(self._zeros, i).value : currentLevelHash ); // Update node index @@ -129,15 +132,22 @@ library MerkleTree { } // Record new root - self.root = currentLevelHash; + self._root = currentLevelHash; + + return currentLevelHash; + } - return leafIndex; + /** + * @dev Tree's current root + */ + function getRoot(Bytes32MerkleTree storage self) internal view returns (uint256) { + return self._root; } /** * @dev Tree's depth (set at initialization) */ function getDepth(Bytes32MerkleTree storage self) internal view returns (uint256) { - return self.zeros.length; + return self._zeros.length; } } From 7a21c4e344f237480f48690ffa2c089a8cbf4a59 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 21 Feb 2024 09:58:44 +0100 Subject: [PATCH 40/48] test reseting the tree using setup --- contracts/mocks/MerkleTreeMock.sol | 2 +- contracts/utils/structs/MerkleTree.sol | 2 +- test/utils/structs/MerkleTree.test.js | 37 ++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index 4bb497c00ea..3761e75468b 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -9,7 +9,7 @@ contract MerkleTreeMock { MerkleTree.Bytes32MerkleTree private _tree; - constructor(uint8 _depth, bytes32 _zero) { + function setup(uint8 _depth, bytes32 _zero) public { _tree.setup(_depth, _zero); } diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 5c93516aa52..7bf65e7b99e 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -140,7 +140,7 @@ library MerkleTree { /** * @dev Tree's current root */ - function getRoot(Bytes32MerkleTree storage self) internal view returns (uint256) { + function getRoot(Bytes32MerkleTree storage self) internal view returns (bytes32) { return self._root; } diff --git a/test/utils/structs/MerkleTree.test.js b/test/utils/structs/MerkleTree.test.js index 8c948719513..79d90f0f75b 100644 --- a/test/utils/structs/MerkleTree.test.js +++ b/test/utils/structs/MerkleTree.test.js @@ -4,6 +4,8 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); const { StandardMerkleTree } = require('@openzeppelin/merkle-tree'); +const { generators } = require('../../helpers/random'); + const makeTree = (leafs = [ethers.ZeroHash]) => StandardMerkleTree.of( leafs.map(leaf => [leaf]), @@ -15,7 +17,9 @@ const DEPTH = 4n; // 16 slots const ZERO = makeTree().leafHash([ethers.ZeroHash]); async function fixture() { - return { mock: await ethers.deployContract('MerkleTreeMock', [DEPTH, ZERO]) }; + const mock = await ethers.deployContract('MerkleTreeMock'); + await mock.setup(DEPTH, ZERO); + return { mock }; } describe('MerkleTree', function () { @@ -38,7 +42,7 @@ describe('MerkleTree', function () { // for each leaf slot for (const i in leafs) { // generate random leaf - leafs[i] = ethers.randomBytes(32); + leafs[i] = generators.bytes32(); // update leaf list and rebuild tree. const merkleTree = makeTree(leafs); @@ -58,4 +62,33 @@ describe('MerkleTree', function () { await expect(this.mock.insert(ethers.ZeroHash)).to.be.revertedWithPanic(PANIC_CODES.TOO_MUCH_MEMORY_ALLOCATED); }); }); + + it('reset', async function () { + // empty tree + const zeroLeafs = Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash); + const zeroTree = makeTree(zeroLeafs); + + // tree with one element + const leafs = Array.from({ length: 2 ** Number(DEPTH) }, (_, i) => i == 0 ? generators.bytes32() : ethers.ZeroHash); + const tree = makeTree(leafs); + + // root should that of zero tree + expect(await this.mock.getRoot()).to.equal(zeroTree.root); + expect(await this.mock.nextLeafIndex()).to.equal(0n); + + // insert leaf and check root + await this.mock.insert(tree.leafHash([leafs[0]])); + expect(await this.mock.getRoot()).to.equal(tree.root); + expect(await this.mock.nextLeafIndex()).to.equal(1n); + + // reset tree + await this.mock.setup(DEPTH, ZERO); + expect(await this.mock.getRoot()).to.equal(zeroTree.root); + expect(await this.mock.nextLeafIndex()).to.equal(0n); + + // re-insert leaf and check root + await this.mock.insert(tree.leafHash([leafs[0]])); + expect(await this.mock.getRoot()).to.equal(tree.root); + expect(await this.mock.nextLeafIndex()).to.equal(1n); + }); }); From 01c28793219cdca97471c935b179e6be0fae4665 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 21 Feb 2024 10:05:59 +0100 Subject: [PATCH 41/48] rename hashing functions --- contracts/utils/cryptography/Hashes.sol | 6 +++--- contracts/utils/cryptography/MerkleProof.sol | 8 ++++---- contracts/utils/structs/MerkleTree.sol | 4 ++-- test/utils/structs/MerkleTree.test.js | 4 +++- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/contracts/utils/cryptography/Hashes.sol b/contracts/utils/cryptography/Hashes.sol index ed15ef5f060..96859b6d5ce 100644 --- a/contracts/utils/cryptography/Hashes.sol +++ b/contracts/utils/cryptography/Hashes.sol @@ -9,14 +9,14 @@ library Hashes { /** * @dev Keccak256 hash of a sorted pair of bytes32. Frequently used when working with merkle proofs. */ - function stdPairHash(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? _efficientHash(a, b) : _efficientHash(b, a); + function sortedPairKeccak256(bytes32 a, bytes32 b) internal pure returns (bytes32) { + return a < b ? _efficientKeccak256(a, b) : _efficientKeccak256(b, a); } /** * @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory. */ - function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { + function _efficientKeccak256(bytes32 a, bytes32 b) private pure returns (bytes32 value) { /// @solidity memory-safe-assembly assembly { mstore(0x00, a) diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index 254a55613c2..851d5db722c 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -51,7 +51,7 @@ library MerkleProof { function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { bytes32 computedHash = leaf; for (uint256 i = 0; i < proof.length; i++) { - computedHash = Hashes.stdPairHash(computedHash, proof[i]); + computedHash = Hashes.sortedPairKeccak256(computedHash, proof[i]); } return computedHash; } @@ -62,7 +62,7 @@ library MerkleProof { function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) { bytes32 computedHash = leaf; for (uint256 i = 0; i < proof.length; i++) { - computedHash = Hashes.stdPairHash(computedHash, proof[i]); + computedHash = Hashes.sortedPairKeccak256(computedHash, proof[i]); } return computedHash; } @@ -140,7 +140,7 @@ library MerkleProof { bytes32 b = proofFlags[i] ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) : proof[proofPos++]; - hashes[i] = Hashes.stdPairHash(a, b); + hashes[i] = Hashes.sortedPairKeccak256(a, b); } if (totalHashes > 0) { @@ -196,7 +196,7 @@ library MerkleProof { bytes32 b = proofFlags[i] ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) : proof[proofPos++]; - hashes[i] = Hashes.stdPairHash(a, b); + hashes[i] = Hashes.sortedPairKeccak256(a, b); } if (totalHashes > 0) { diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 7bf65e7b99e..1d1d7857349 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -48,7 +48,7 @@ library MerkleTree { } /** - * @dev Initialize a {Bytes32MerkleTree} using {Hashes-stdPairHash} to hash internal nodes. + * @dev Initialize a {Bytes32MerkleTree} using {Hashes-sortedPairKeccak256} to hash internal nodes. * The capacity of the tree (i.e. number of leaves) is set to `2**depth`. * * Calling this function on MerkleTree that was already setup and used will reset it to a blank state. @@ -57,7 +57,7 @@ library MerkleTree { * empty leaves. It should be a value that is not expected to be part of the tree. */ function setup(Bytes32MerkleTree storage self, uint8 depth, bytes32 zero) internal { - return setup(self, depth, zero, Hashes.stdPairHash); + return setup(self, depth, zero, Hashes.sortedPairKeccak256); } /** diff --git a/test/utils/structs/MerkleTree.test.js b/test/utils/structs/MerkleTree.test.js index 79d90f0f75b..25388cca46f 100644 --- a/test/utils/structs/MerkleTree.test.js +++ b/test/utils/structs/MerkleTree.test.js @@ -69,7 +69,9 @@ describe('MerkleTree', function () { const zeroTree = makeTree(zeroLeafs); // tree with one element - const leafs = Array.from({ length: 2 ** Number(DEPTH) }, (_, i) => i == 0 ? generators.bytes32() : ethers.ZeroHash); + const leafs = Array.from({ length: 2 ** Number(DEPTH) }, (_, i) => + i == 0 ? generators.bytes32() : ethers.ZeroHash, + ); const tree = makeTree(leafs); // root should that of zero tree From 2494680254553db51b6fe32a13fa4045c5920075 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 21 Feb 2024 13:31:42 +0100 Subject: [PATCH 42/48] return index and root when inserting a leaf --- contracts/mocks/MerkleTreeMock.sol | 7 +++++-- contracts/utils/structs/MerkleTree.sol | 7 ++++--- test/utils/structs/MerkleTree.test.js | 28 ++++++++++++++++---------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index 3761e75468b..93734d32e74 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -9,12 +9,15 @@ contract MerkleTreeMock { MerkleTree.Bytes32MerkleTree private _tree; + event LeafInserted(bytes32 leaf, uint256 index, bytes32 root); + function setup(uint8 _depth, bytes32 _zero) public { _tree.setup(_depth, _zero); } - function insert(bytes32 leaf) public returns (bytes32) { - return _tree.insert(leaf); + function insert(bytes32 leaf) public { + (uint256 index, bytes32 root) = _tree.insert(leaf); + emit LeafInserted(leaf, index, root); } function getRoot() public view returns (bytes32) { diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 1d1d7857349..7f601797265 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -90,12 +90,13 @@ library MerkleTree { } /** - * @dev Insert a new leaf in the tree, and compute the new root. + * @dev Insert a new leaf in the tree, and compute the new root. Returns the position of the inserted leaf in the + * tree, and the resulting root. * * Hashing the leaf before calling this function is recommended as a protection against * second pre-image attacks. */ - function insert(Bytes32MerkleTree storage self, bytes32 leaf) internal returns (bytes32) { + function insert(Bytes32MerkleTree storage self, bytes32 leaf) internal returns (uint256, bytes32) { // Cache read uint256 depth = self._zeros.length; function(bytes32, bytes32) view returns (bytes32) fnHash = self._fnHash; @@ -134,7 +135,7 @@ library MerkleTree { // Record new root self._root = currentLevelHash; - return currentLevelHash; + return (leafIndex, currentLevelHash); } /** diff --git a/test/utils/structs/MerkleTree.test.js b/test/utils/structs/MerkleTree.test.js index 25388cca46f..3736c0110e0 100644 --- a/test/utils/structs/MerkleTree.test.js +++ b/test/utils/structs/MerkleTree.test.js @@ -13,8 +13,10 @@ const makeTree = (leafs = [ethers.ZeroHash]) => { sortLeaves: false }, ); +const hashLeaf = leaf => makeTree().leafHash([leaf]); + const DEPTH = 4n; // 16 slots -const ZERO = makeTree().leafHash([ethers.ZeroHash]); +const ZERO = hashLeaf(ethers.ZeroHash); async function fixture() { const mock = await ethers.deployContract('MerkleTreeMock'); @@ -41,17 +43,19 @@ describe('MerkleTree', function () { // for each leaf slot for (const i in leafs) { - // generate random leaf - leafs[i] = generators.bytes32(); + // generate random leaf and hash it + const hashedLeaf = hashLeaf((leafs[i] = generators.bytes32())); // update leaf list and rebuild tree. - const merkleTree = makeTree(leafs); + const tree = makeTree(leafs); // insert value in tree - await this.mock.insert(merkleTree.leafHash([leafs[i]])); + await expect(this.mock.insert(hashedLeaf)) + .to.emit(this.mock, 'LeafInserted') + .withArgs(hashedLeaf, i, tree.root); // check tree - expect(await this.mock.getRoot()).to.equal(merkleTree.root); + expect(await this.mock.getRoot()).to.equal(tree.root); expect(await this.mock.nextLeafIndex()).to.equal(BigInt(i) + 1n); } }); @@ -69,9 +73,8 @@ describe('MerkleTree', function () { const zeroTree = makeTree(zeroLeafs); // tree with one element - const leafs = Array.from({ length: 2 ** Number(DEPTH) }, (_, i) => - i == 0 ? generators.bytes32() : ethers.ZeroHash, - ); + const leafs = Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash); + const hashedLeaf = hashLeaf((leafs[0] = generators.bytes32())); // fill first leaf and hash it const tree = makeTree(leafs); // root should that of zero tree @@ -79,17 +82,20 @@ describe('MerkleTree', function () { expect(await this.mock.nextLeafIndex()).to.equal(0n); // insert leaf and check root - await this.mock.insert(tree.leafHash([leafs[0]])); + await expect(this.mock.insert(hashedLeaf)).to.emit(this.mock, 'LeafInserted').withArgs(hashedLeaf, 0, tree.root); + expect(await this.mock.getRoot()).to.equal(tree.root); expect(await this.mock.nextLeafIndex()).to.equal(1n); // reset tree await this.mock.setup(DEPTH, ZERO); + expect(await this.mock.getRoot()).to.equal(zeroTree.root); expect(await this.mock.nextLeafIndex()).to.equal(0n); // re-insert leaf and check root - await this.mock.insert(tree.leafHash([leafs[0]])); + await expect(this.mock.insert(hashedLeaf)).to.emit(this.mock, 'LeafInserted').withArgs(hashedLeaf, 0, tree.root); + expect(await this.mock.getRoot()).to.equal(tree.root); expect(await this.mock.nextLeafIndex()).to.equal(1n); }); From 0a2bfce792ba3c273ff62abc4528a616eba7834c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 21 Feb 2024 17:01:20 +0100 Subject: [PATCH 43/48] rename structure and functions --- contracts/mocks/MerkleTreeMock.sol | 18 +++++++-------- contracts/utils/structs/MerkleTree.sol | 32 +++++++++++++------------- test/utils/structs/MerkleTree.test.js | 32 +++++++++++++------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index 93734d32e74..9a50af5801f 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -5,9 +5,9 @@ pragma solidity ^0.8.0; import {MerkleTree} from "../utils/structs/MerkleTree.sol"; contract MerkleTreeMock { - using MerkleTree for MerkleTree.Bytes32MerkleTree; + using MerkleTree for MerkleTree.Bytes32PushTree; - MerkleTree.Bytes32MerkleTree private _tree; + MerkleTree.Bytes32PushTree private _tree; event LeafInserted(bytes32 leaf, uint256 index, bytes32 root); @@ -15,17 +15,17 @@ contract MerkleTreeMock { _tree.setup(_depth, _zero); } - function insert(bytes32 leaf) public { - (uint256 index, bytes32 root) = _tree.insert(leaf); - emit LeafInserted(leaf, index, root); + function push(bytes32 leaf) public { + (uint256 leafIndex, bytes32 currentRoot) = _tree.push(leaf); + emit LeafInserted(leaf, leafIndex, currentRoot); } - function getRoot() public view returns (bytes32) { - return _tree.getRoot(); + function root() public view returns (bytes32) { + return _tree.root(); } - function getDepth() public view returns (uint256) { - return _tree.getDepth(); + function depth() public view returns (uint256) { + return _tree.depth(); } // internal state diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 7f601797265..db6bf5fd96b 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -39,7 +39,7 @@ library MerkleTree { * * WARNING: Updating any of the tree's parameters after the first insertion will result in a corrupted tree. */ - struct Bytes32MerkleTree { + struct Bytes32PushTree { bytes32 _root; uint256 _nextLeafIndex; bytes32[] _sides; @@ -48,16 +48,16 @@ library MerkleTree { } /** - * @dev Initialize a {Bytes32MerkleTree} using {Hashes-sortedPairKeccak256} to hash internal nodes. - * The capacity of the tree (i.e. number of leaves) is set to `2**depth`. + * @dev Initialize a {Bytes32PushTree} using {Hashes-sortedPairKeccak256} to hash internal nodes. + * The capacity of the tree (i.e. number of leaves) is set to `2**levels`. * * Calling this function on MerkleTree that was already setup and used will reset it to a blank state. * * IMPORTANT: The zero value should be carefully chosen since it will be stored in the tree representing * empty leaves. It should be a value that is not expected to be part of the tree. */ - function setup(Bytes32MerkleTree storage self, uint8 depth, bytes32 zero) internal { - return setup(self, depth, zero, Hashes.sortedPairKeccak256); + function setup(Bytes32PushTree storage self, uint8 levels, bytes32 zero) internal { + return setup(self, levels, zero, Hashes.sortedPairKeccak256); } /** @@ -67,18 +67,18 @@ library MerkleTree { * compromise the soundness of the tree. Consider using functions from {Hashes}. */ function setup( - Bytes32MerkleTree storage self, - uint8 depth, + Bytes32PushTree storage self, + uint8 levels, bytes32 zero, function(bytes32, bytes32) view returns (bytes32) fnHash ) internal { // Store depth in the dynamic array - Arrays.unsafeSetLength(self._sides, depth); - Arrays.unsafeSetLength(self._zeros, depth); + Arrays.unsafeSetLength(self._sides, levels); + Arrays.unsafeSetLength(self._zeros, levels); // Build each root of zero-filled subtrees bytes32 currentZero = zero; - for (uint32 i = 0; i < depth; ++i) { + for (uint32 i = 0; i < levels; ++i) { Arrays.unsafeAccess(self._zeros, i).value = currentZero; currentZero = fnHash(currentZero, currentZero); } @@ -96,23 +96,23 @@ library MerkleTree { * Hashing the leaf before calling this function is recommended as a protection against * second pre-image attacks. */ - function insert(Bytes32MerkleTree storage self, bytes32 leaf) internal returns (uint256, bytes32) { + function push(Bytes32PushTree storage self, bytes32 leaf) internal returns (uint256, bytes32) { // Cache read - uint256 depth = self._zeros.length; + uint256 levels = self._zeros.length; function(bytes32, bytes32) view returns (bytes32) fnHash = self._fnHash; // Get leaf index uint256 leafIndex = self._nextLeafIndex++; // Check if tree is full. - if (leafIndex >= 1 << depth) { + if (leafIndex >= 1 << levels) { Panic.panic(Panic.RESOURCE_ERROR); } // Rebuild branch from leaf to root uint256 currentIndex = leafIndex; bytes32 currentLevelHash = leaf; - for (uint32 i = 0; i < depth; i++) { + for (uint32 i = 0; i < levels; i++) { // Reaching the parent node, is currentLevelHash the left child? bool isLeft = currentIndex % 2 == 0; @@ -141,14 +141,14 @@ library MerkleTree { /** * @dev Tree's current root */ - function getRoot(Bytes32MerkleTree storage self) internal view returns (bytes32) { + function root(Bytes32PushTree storage self) internal view returns (bytes32) { return self._root; } /** * @dev Tree's depth (set at initialization) */ - function getDepth(Bytes32MerkleTree storage self) internal view returns (uint256) { + function depth(Bytes32PushTree storage self) internal view returns (uint256) { return self._zeros.length; } } diff --git a/test/utils/structs/MerkleTree.test.js b/test/utils/structs/MerkleTree.test.js index 3736c0110e0..4776b54334e 100644 --- a/test/utils/structs/MerkleTree.test.js +++ b/test/utils/structs/MerkleTree.test.js @@ -32,12 +32,12 @@ describe('MerkleTree', function () { it('sets initial values at setup', async function () { const merkleTree = makeTree(Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash)); - expect(await this.mock.getRoot()).to.equal(merkleTree.root); - expect(await this.mock.getDepth()).to.equal(DEPTH); + expect(await this.mock.root()).to.equal(merkleTree.root); + expect(await this.mock.depth()).to.equal(DEPTH); expect(await this.mock.nextLeafIndex()).to.equal(0n); }); - describe('insert', function () { + describe('push', function () { it('tree is correctly updated', async function () { const leafs = Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash); @@ -49,21 +49,21 @@ describe('MerkleTree', function () { // update leaf list and rebuild tree. const tree = makeTree(leafs); - // insert value in tree - await expect(this.mock.insert(hashedLeaf)) + // push value to tree + await expect(this.mock.push(hashedLeaf)) .to.emit(this.mock, 'LeafInserted') .withArgs(hashedLeaf, i, tree.root); // check tree - expect(await this.mock.getRoot()).to.equal(tree.root); + expect(await this.mock.root()).to.equal(tree.root); expect(await this.mock.nextLeafIndex()).to.equal(BigInt(i) + 1n); } }); it('revert when tree is full', async function () { - await Promise.all(Array.from({ length: 2 ** Number(DEPTH) }).map(() => this.mock.insert(ethers.ZeroHash))); + await Promise.all(Array.from({ length: 2 ** Number(DEPTH) }).map(() => this.mock.push(ethers.ZeroHash))); - await expect(this.mock.insert(ethers.ZeroHash)).to.be.revertedWithPanic(PANIC_CODES.TOO_MUCH_MEMORY_ALLOCATED); + await expect(this.mock.push(ethers.ZeroHash)).to.be.revertedWithPanic(PANIC_CODES.TOO_MUCH_MEMORY_ALLOCATED); }); }); @@ -78,25 +78,25 @@ describe('MerkleTree', function () { const tree = makeTree(leafs); // root should that of zero tree - expect(await this.mock.getRoot()).to.equal(zeroTree.root); + expect(await this.mock.root()).to.equal(zeroTree.root); expect(await this.mock.nextLeafIndex()).to.equal(0n); - // insert leaf and check root - await expect(this.mock.insert(hashedLeaf)).to.emit(this.mock, 'LeafInserted').withArgs(hashedLeaf, 0, tree.root); + // push leaf and check root + await expect(this.mock.push(hashedLeaf)).to.emit(this.mock, 'LeafInserted').withArgs(hashedLeaf, 0, tree.root); - expect(await this.mock.getRoot()).to.equal(tree.root); + expect(await this.mock.root()).to.equal(tree.root); expect(await this.mock.nextLeafIndex()).to.equal(1n); // reset tree await this.mock.setup(DEPTH, ZERO); - expect(await this.mock.getRoot()).to.equal(zeroTree.root); + expect(await this.mock.root()).to.equal(zeroTree.root); expect(await this.mock.nextLeafIndex()).to.equal(0n); - // re-insert leaf and check root - await expect(this.mock.insert(hashedLeaf)).to.emit(this.mock, 'LeafInserted').withArgs(hashedLeaf, 0, tree.root); + // re-push leaf and check root + await expect(this.mock.push(hashedLeaf)).to.emit(this.mock, 'LeafInserted').withArgs(hashedLeaf, 0, tree.root); - expect(await this.mock.getRoot()).to.equal(tree.root); + expect(await this.mock.root()).to.equal(tree.root); expect(await this.mock.nextLeafIndex()).to.equal(1n); }); }); From 08c9a3c1c14e8e74d40e4383e6c83fad46031d77 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 4 Mar 2024 18:57:42 -0600 Subject: [PATCH 44/48] Apply PR suggestions --- contracts/utils/structs/MerkleTree.sol | 2 +- test/utils/structs/MerkleTree.test.js | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index db6bf5fd96b..2b8d6b33647 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -96,7 +96,7 @@ library MerkleTree { * Hashing the leaf before calling this function is recommended as a protection against * second pre-image attacks. */ - function push(Bytes32PushTree storage self, bytes32 leaf) internal returns (uint256, bytes32) { + function push(Bytes32PushTree storage self, bytes32 leaf) internal returns (uint256 index, bytes32 newRoot) { // Cache read uint256 levels = self._zeros.length; function(bytes32, bytes32) view returns (bytes32) fnHash = self._fnHash; diff --git a/test/utils/structs/MerkleTree.test.js b/test/utils/structs/MerkleTree.test.js index 4776b54334e..2603ac2897d 100644 --- a/test/utils/structs/MerkleTree.test.js +++ b/test/utils/structs/MerkleTree.test.js @@ -50,9 +50,7 @@ describe('MerkleTree', function () { const tree = makeTree(leafs); // push value to tree - await expect(this.mock.push(hashedLeaf)) - .to.emit(this.mock, 'LeafInserted') - .withArgs(hashedLeaf, i, tree.root); + await expect(this.mock.push(hashedLeaf)).to.emit(this.mock, 'LeafInserted').withArgs(hashedLeaf, i, tree.root); // check tree expect(await this.mock.root()).to.equal(tree.root); @@ -77,7 +75,7 @@ describe('MerkleTree', function () { const hashedLeaf = hashLeaf((leafs[0] = generators.bytes32())); // fill first leaf and hash it const tree = makeTree(leafs); - // root should that of zero tree + // root should be that of a zero tree expect(await this.mock.root()).to.equal(zeroTree.root); expect(await this.mock.nextLeafIndex()).to.equal(0n); From 31712fbfe4ec08e3ad5652fe9272ec3bd2925d2c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 5 Mar 2024 10:53:45 +0100 Subject: [PATCH 45/48] Update contracts/utils/cryptography/Hashes.sol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto GarcĂ­a --- contracts/utils/cryptography/Hashes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/cryptography/Hashes.sol b/contracts/utils/cryptography/Hashes.sol index 96859b6d5ce..b000bd3a804 100644 --- a/contracts/utils/cryptography/Hashes.sol +++ b/contracts/utils/cryptography/Hashes.sol @@ -7,7 +7,7 @@ pragma solidity ^0.8.0; */ library Hashes { /** - * @dev Keccak256 hash of a sorted pair of bytes32. Frequently used when working with merkle proofs. + * @dev Commutative Keccak256 hash of a sorted pair of bytes32. Frequently used when working with merkle proofs. */ function sortedPairKeccak256(bytes32 a, bytes32 b) internal pure returns (bytes32) { return a < b ? _efficientKeccak256(a, b) : _efficientKeccak256(b, a); From 55853bec0575305a3c2a940b1a615e360dcb4be4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 6 Mar 2024 17:05:30 +0100 Subject: [PATCH 46/48] rename the standard node hash --- contracts/utils/cryptography/Hashes.sol | 4 +++- contracts/utils/cryptography/MerkleProof.sol | 8 ++++---- contracts/utils/structs/MerkleTree.sol | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/contracts/utils/cryptography/Hashes.sol b/contracts/utils/cryptography/Hashes.sol index b000bd3a804..1eaab19eb7a 100644 --- a/contracts/utils/cryptography/Hashes.sol +++ b/contracts/utils/cryptography/Hashes.sol @@ -8,8 +8,10 @@ pragma solidity ^0.8.0; library Hashes { /** * @dev Commutative Keccak256 hash of a sorted pair of bytes32. Frequently used when working with merkle proofs. + * + * Note: this is equivalent to the `standardNodeHash` from the `@openzeppelin/merkle-tree` library. */ - function sortedPairKeccak256(bytes32 a, bytes32 b) internal pure returns (bytes32) { + function commutativeKeccak256(bytes32 a, bytes32 b) internal pure returns (bytes32) { return a < b ? _efficientKeccak256(a, b) : _efficientKeccak256(b, a); } diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index 851d5db722c..dce05a0dc05 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -51,7 +51,7 @@ library MerkleProof { function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { bytes32 computedHash = leaf; for (uint256 i = 0; i < proof.length; i++) { - computedHash = Hashes.sortedPairKeccak256(computedHash, proof[i]); + computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]); } return computedHash; } @@ -62,7 +62,7 @@ library MerkleProof { function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) { bytes32 computedHash = leaf; for (uint256 i = 0; i < proof.length; i++) { - computedHash = Hashes.sortedPairKeccak256(computedHash, proof[i]); + computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]); } return computedHash; } @@ -140,7 +140,7 @@ library MerkleProof { bytes32 b = proofFlags[i] ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) : proof[proofPos++]; - hashes[i] = Hashes.sortedPairKeccak256(a, b); + hashes[i] = Hashes.commutativeKeccak256(a, b); } if (totalHashes > 0) { @@ -196,7 +196,7 @@ library MerkleProof { bytes32 b = proofFlags[i] ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) : proof[proofPos++]; - hashes[i] = Hashes.sortedPairKeccak256(a, b); + hashes[i] = Hashes.commutativeKeccak256(a, b); } if (totalHashes > 0) { diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 2b8d6b33647..dd2cc0baeb0 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -48,7 +48,7 @@ library MerkleTree { } /** - * @dev Initialize a {Bytes32PushTree} using {Hashes-sortedPairKeccak256} to hash internal nodes. + * @dev Initialize a {Bytes32PushTree} using {Hashes-commutativeKeccak256} to hash internal nodes. * The capacity of the tree (i.e. number of leaves) is set to `2**levels`. * * Calling this function on MerkleTree that was already setup and used will reset it to a blank state. @@ -57,7 +57,7 @@ library MerkleTree { * empty leaves. It should be a value that is not expected to be part of the tree. */ function setup(Bytes32PushTree storage self, uint8 levels, bytes32 zero) internal { - return setup(self, levels, zero, Hashes.sortedPairKeccak256); + return setup(self, levels, zero, Hashes.commutativeKeccak256); } /** From eca27fc80048d9c09acb54707c844c1b9cd32a93 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 6 Mar 2024 21:44:08 +0100 Subject: [PATCH 47/48] fix lint --- contracts/utils/cryptography/Hashes.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/utils/cryptography/Hashes.sol b/contracts/utils/cryptography/Hashes.sol index 1eaab19eb7a..c773486b76b 100644 --- a/contracts/utils/cryptography/Hashes.sol +++ b/contracts/utils/cryptography/Hashes.sol @@ -8,8 +8,8 @@ pragma solidity ^0.8.0; library Hashes { /** * @dev Commutative Keccak256 hash of a sorted pair of bytes32. Frequently used when working with merkle proofs. - * - * Note: this is equivalent to the `standardNodeHash` from the `@openzeppelin/merkle-tree` library. + * + * Note: this is equivalent to the `standardNodeHash` from the `@openzeppelin/merkle-tree` library. */ function commutativeKeccak256(bytes32 a, bytes32 b) internal pure returns (bytes32) { return a < b ? _efficientKeccak256(a, b) : _efficientKeccak256(b, a); From eca9085689f5b6dcb03d2c66023064bef567d49e Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 7 Mar 2024 09:04:54 -0600 Subject: [PATCH 48/48] Fix NatSpec weird error --- contracts/utils/cryptography/Hashes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/cryptography/Hashes.sol b/contracts/utils/cryptography/Hashes.sol index c773486b76b..434a8494251 100644 --- a/contracts/utils/cryptography/Hashes.sol +++ b/contracts/utils/cryptography/Hashes.sol @@ -9,7 +9,7 @@ library Hashes { /** * @dev Commutative Keccak256 hash of a sorted pair of bytes32. Frequently used when working with merkle proofs. * - * Note: this is equivalent to the `standardNodeHash` from the `@openzeppelin/merkle-tree` library. + * NOTE: Equivalent to the `standardNodeHash` in our https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. */ function commutativeKeccak256(bytes32 a, bytes32 b) internal pure returns (bytes32) { return a < b ? _efficientKeccak256(a, b) : _efficientKeccak256(b, a);