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
Next Next commit
Add a (complete) MerkleTree structure
  • Loading branch information
Amxx committed Aug 14, 2022
commit 6b079eeb7b0b4d9b98a4abc71379e862eeca7458
56 changes: 56 additions & 0 deletions contracts/mocks/MerkleTreeMock.sol
Original file line number Diff line number Diff line change
@@ -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];
}
}
152 changes: 152 additions & 0 deletions contracts/utils/structs/MerkleTree.sol
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
79 changes: 79 additions & 0 deletions test/utils/structs/Merkletree.test.js
Original file line number Diff line number Diff line change
@@ -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()',
);
});
});
});