123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- 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 { generators } = require('../../helpers/random');
- const { range } = require('../../helpers/iterate');
- const DEPTH = 4; // 16 slots
- const makeTree = (leaves = [], length = 2 ** DEPTH, zero = ethers.ZeroHash) =>
- StandardMerkleTree.of(
- []
- .concat(
- leaves,
- Array.from({ length: length - leaves.length }, () => zero),
- )
- .map(leaf => [leaf]),
- ['bytes32'],
- { sortLeaves: false },
- );
- const ZERO = makeTree().leafHash([ethers.ZeroHash]);
- async function fixture() {
- const mock = await ethers.deployContract('MerkleTreeMock');
- await mock.setup(DEPTH, ZERO);
- return { mock };
- }
- describe('MerkleTree', function () {
- beforeEach(async function () {
- Object.assign(this, await loadFixture(fixture));
- });
- it('sets initial values at setup', async function () {
- const merkleTree = makeTree();
- await expect(this.mock.root()).to.eventually.equal(merkleTree.root);
- await expect(this.mock.depth()).to.eventually.equal(DEPTH);
- await expect(this.mock.nextLeafIndex()).to.eventually.equal(0n);
- });
- describe('push', function () {
- it('pushing correctly updates the tree', async function () {
- const leaves = [];
- // for each leaf slot
- for (const i in range(2 ** DEPTH)) {
- // generate random leaf
- leaves.push(generators.bytes32());
- // rebuild tree.
- const tree = makeTree(leaves);
- const hash = tree.leafHash(tree.at(i));
- // push value to tree
- await expect(this.mock.push(hash)).to.emit(this.mock, 'LeafInserted').withArgs(hash, i, tree.root);
- // check tree
- await expect(this.mock.root()).to.eventually.equal(tree.root);
- await expect(this.mock.nextLeafIndex()).to.eventually.equal(BigInt(i) + 1n);
- }
- });
- it('pushing to a full tree reverts', async function () {
- await Promise.all(Array.from({ length: 2 ** Number(DEPTH) }).map(() => this.mock.push(ethers.ZeroHash)));
- await expect(this.mock.push(ethers.ZeroHash)).to.be.revertedWithPanic(PANIC_CODES.TOO_MUCH_MEMORY_ALLOCATED);
- });
- });
- describe('update', function () {
- for (const { leafCount, leafIndex } of range(2 ** DEPTH + 1).flatMap(leafCount =>
- range(leafCount).map(leafIndex => ({ leafCount, leafIndex })),
- ))
- it(`updating a leaf correctly updates the tree (leaf #${leafIndex + 1}/${leafCount})`, async function () {
- // initial tree
- const leaves = Array.from({ length: leafCount }, generators.bytes32);
- const oldTree = makeTree(leaves);
- // fill tree and verify root
- for (const i in leaves) {
- await this.mock.push(oldTree.leafHash(oldTree.at(i)));
- }
- await expect(this.mock.root()).to.eventually.equal(oldTree.root);
- // create updated tree
- leaves[leafIndex] = generators.bytes32();
- const newTree = makeTree(leaves);
- const oldLeafHash = oldTree.leafHash(oldTree.at(leafIndex));
- const newLeafHash = newTree.leafHash(newTree.at(leafIndex));
- // perform update
- await expect(this.mock.update(leafIndex, oldLeafHash, newLeafHash, oldTree.getProof(leafIndex)))
- .to.emit(this.mock, 'LeafUpdated')
- .withArgs(oldLeafHash, newLeafHash, leafIndex, newTree.root);
- // verify updated root
- await expect(this.mock.root()).to.eventually.equal(newTree.root);
- // if there is still room in the tree, fill it
- for (const i of range(leafCount, 2 ** DEPTH)) {
- // push new value and rebuild tree
- leaves.push(generators.bytes32());
- const nextTree = makeTree(leaves);
- // push and verify root
- await this.mock.push(nextTree.leafHash(nextTree.at(i)));
- await expect(this.mock.root()).to.eventually.equal(nextTree.root);
- }
- });
- it('replacing a leaf that was not previously pushed reverts', async function () {
- // changing leaf 0 on an empty tree
- await expect(this.mock.update(1, ZERO, ZERO, []))
- .to.be.revertedWithCustomError(this.mock, 'MerkleTreeUpdateInvalidIndex')
- .withArgs(1, 0);
- });
- it('replacing a leaf using an invalid proof reverts', async function () {
- const leafCount = 4;
- const leafIndex = 2;
- const leaves = Array.from({ length: leafCount }, generators.bytes32);
- const tree = makeTree(leaves);
- // fill tree and verify root
- for (const i in leaves) {
- await this.mock.push(tree.leafHash(tree.at(i)));
- }
- await expect(this.mock.root()).to.eventually.equal(tree.root);
- const oldLeafHash = tree.leafHash(tree.at(leafIndex));
- const newLeafHash = generators.bytes32();
- const proof = tree.getProof(leafIndex);
- // invalid proof (tamper)
- proof[1] = generators.bytes32();
- await expect(this.mock.update(leafIndex, oldLeafHash, newLeafHash, proof)).to.be.revertedWithCustomError(
- this.mock,
- 'MerkleTreeUpdateInvalidProof',
- );
- });
- });
- it('reset', async function () {
- // empty tree
- const emptyTree = makeTree();
- // tree with one element
- const leaves = [generators.bytes32()];
- const tree = makeTree(leaves);
- const hash = tree.leafHash(tree.at(0));
- // root should be that of a zero tree
- expect(await this.mock.root()).to.equal(emptyTree.root);
- expect(await this.mock.nextLeafIndex()).to.equal(0n);
- // push leaf and check root
- await expect(this.mock.push(hash)).to.emit(this.mock, 'LeafInserted').withArgs(hash, 0, 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.root()).to.equal(emptyTree.root);
- expect(await this.mock.nextLeafIndex()).to.equal(0n);
- // re-push leaf and check root
- await expect(this.mock.push(hash)).to.emit(this.mock, 'LeafInserted').withArgs(hash, 0, tree.root);
- expect(await this.mock.root()).to.equal(tree.root);
- expect(await this.mock.nextLeafIndex()).to.equal(1n);
- });
- });
|