Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
6b079ee
Add a (complete) MerkleTree structure
Amxx Aug 14, 2022
42c695b
optimize array access & remove depth/length constrains
Amxx Aug 14, 2022
ca83cde
gas optimization
Amxx Aug 14, 2022
f4f46ca
limit tree depth to 255 to avoid issues (255 is enough for any realis…
Amxx Aug 14, 2022
af7cb9c
fix lint
Amxx Aug 14, 2022
45eae37
reason
Amxx Aug 14, 2022
ac648c6
coverage
Amxx Aug 14, 2022
1b184c9
comments
Amxx Aug 14, 2022
4863418
documentation & changelog entry
Amxx Aug 16, 2022
3c19dcf
Merge branch 'master' into structure/merkletree
Amxx Dec 13, 2023
9fc7f31
update
Amxx Dec 13, 2023
652c8a1
add changeset
Amxx Dec 13, 2023
c74ab55
fix lint
Amxx Dec 14, 2023
a9932c9
fix lint
Amxx Dec 14, 2023
d8bdfd0
fix codespell
Amxx Dec 14, 2023
4d0ed52
Merge branch 'master' into structure/merkletree
Amxx Jan 4, 2024
b131354
update @openzeppelin/merkle-tree dependency
Amxx Jan 28, 2024
6422af6
fix lint
Amxx Jan 28, 2024
3ab0d21
Merge branch 'master' into structure/merkletree
Amxx Feb 5, 2024
5639d7c
up
Amxx Feb 5, 2024
24c829a
minimize changes
Amxx Feb 5, 2024
f954a98
Panic with RESOURCE_ERROR when inserting in a full tree
Amxx Feb 5, 2024
acdc6a9
Merge branch 'master' into structure/merkletree
Amxx Feb 6, 2024
cebdc2a
improve coverage
Amxx Feb 6, 2024
d4ced94
test looparound property of memory arrays
Amxx Feb 6, 2024
a3a813c
rename initialize → setUp
Amxx Feb 7, 2024
ec05d19
Update contracts/utils/structs/MerkleTree.sol
Amxx Feb 7, 2024
8ecc790
Merge branch 'master' into structure/merkletree
Amxx Feb 12, 2024
ec3d96b
fix lint
Amxx Feb 12, 2024
bcc0667
cleanup
Amxx Feb 12, 2024
b50ebee
Merge branch 'master' into structure/merkletree
Amxx Feb 16, 2024
5b15205
remove root history from the MerkleTree structure
Amxx Feb 19, 2024
b390790
Add Hashes.sol
Amxx Feb 19, 2024
e331674
fix-lint
Amxx Feb 19, 2024
91f7057
rename to reflect removal of history
Amxx Feb 19, 2024
088fa8c
rename setUp → setup
Amxx Feb 19, 2024
2d869b7
doc
Amxx Feb 20, 2024
567cd3e
Update contracts/utils/structs/MerkleTree.sol
Amxx Feb 20, 2024
a13237a
Update MerkleTree.sol
Amxx Feb 20, 2024
03bea3e
Update changesets and fix some comments
ernestognw Feb 21, 2024
c475bad
Simplify
ernestognw Feb 21, 2024
6a9e873
Add Merkle Tree to the docs
ernestognw Feb 21, 2024
1e59539
Remove merkletree.test.js
ernestognw Feb 21, 2024
051107b
Recover MerkleTree.test.js
ernestognw Feb 21, 2024
a1dd158
prefix variables with underscore to mark them as private (similar do …
Amxx Feb 21, 2024
7a21c4e
test reseting the tree using setup
Amxx Feb 21, 2024
01c2879
rename hashing functions
Amxx Feb 21, 2024
2494680
return index and root when inserting a leaf
Amxx Feb 21, 2024
0a2bfce
rename structure and functions
Amxx Feb 21, 2024
08c9a3c
Apply PR suggestions
ernestognw Mar 5, 2024
31712fb
Update contracts/utils/cryptography/Hashes.sol
Amxx Mar 5, 2024
55853be
rename the standard node hash
Amxx Mar 6, 2024
eca27fc
fix lint
Amxx Mar 6, 2024
eca9085
Fix NatSpec weird error
ernestognw Mar 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Simplify
  • Loading branch information
ernestognw committed Feb 21, 2024
commit c475badb8f602d5887a9a07c73f7cf72f013eb25
4 changes: 2 additions & 2 deletions contracts/mocks/MerkleTreeMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ contract MerkleTreeMock {

MerkleTree.Bytes32MerkleTree private _tree;

constructor(uint256 _depth, bytes32 _zero) {
constructor(uint8 _depth, bytes32 _zero) {
_tree.setup(_depth, _zero);
}

Expand All @@ -22,7 +22,7 @@ contract MerkleTreeMock {
}

function getRoot() public view returns (bytes32) {
return _tree.getRoot();
return _tree.root;
}

// internal state
Expand Down
87 changes: 35 additions & 52 deletions contracts/utils/structs/MerkleTree.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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;
}
}
12 changes: 2 additions & 10 deletions test/utils/structs/Merkletree.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,19 @@ const makeTree = (leafs = [ethers.ZeroHash]) =>
{ sortLeaves: false },
);

const MAX_DEPTH = 255n;
const DEPTH = 4n; // 16 slots
const ZERO = makeTree().leafHash([ethers.ZeroHash]);

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);
Expand Down