MerkleTree.test.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
  5. const { StandardMerkleTree } = require('@openzeppelin/merkle-tree');
  6. const { generators } = require('../../helpers/random');
  7. const { range } = require('../../helpers/iterate');
  8. const DEPTH = 4; // 16 slots
  9. const makeTree = (leaves = [], length = 2 ** DEPTH, zero = ethers.ZeroHash) =>
  10. StandardMerkleTree.of(
  11. []
  12. .concat(
  13. leaves,
  14. Array.from({ length: length - leaves.length }, () => zero),
  15. )
  16. .map(leaf => [leaf]),
  17. ['bytes32'],
  18. { sortLeaves: false },
  19. );
  20. const ZERO = makeTree().leafHash([ethers.ZeroHash]);
  21. async function fixture() {
  22. const mock = await ethers.deployContract('MerkleTreeMock');
  23. await mock.setup(DEPTH, ZERO);
  24. return { mock };
  25. }
  26. describe('MerkleTree', function () {
  27. beforeEach(async function () {
  28. Object.assign(this, await loadFixture(fixture));
  29. });
  30. it('sets initial values at setup', async function () {
  31. const merkleTree = makeTree();
  32. await expect(this.mock.root()).to.eventually.equal(merkleTree.root);
  33. await expect(this.mock.depth()).to.eventually.equal(DEPTH);
  34. await expect(this.mock.nextLeafIndex()).to.eventually.equal(0n);
  35. });
  36. describe('push', function () {
  37. it('pushing correctly updates the tree', async function () {
  38. const leaves = [];
  39. // for each leaf slot
  40. for (const i in range(2 ** DEPTH)) {
  41. // generate random leaf
  42. leaves.push(generators.bytes32());
  43. // rebuild tree.
  44. const tree = makeTree(leaves);
  45. const hash = tree.leafHash(tree.at(i));
  46. // push value to tree
  47. await expect(this.mock.push(hash)).to.emit(this.mock, 'LeafInserted').withArgs(hash, i, tree.root);
  48. // check tree
  49. await expect(this.mock.root()).to.eventually.equal(tree.root);
  50. await expect(this.mock.nextLeafIndex()).to.eventually.equal(BigInt(i) + 1n);
  51. }
  52. });
  53. it('pushing to a full tree reverts', async function () {
  54. await Promise.all(Array.from({ length: 2 ** Number(DEPTH) }).map(() => this.mock.push(ethers.ZeroHash)));
  55. await expect(this.mock.push(ethers.ZeroHash)).to.be.revertedWithPanic(PANIC_CODES.TOO_MUCH_MEMORY_ALLOCATED);
  56. });
  57. });
  58. describe('update', function () {
  59. for (const { leafCount, leafIndex } of range(2 ** DEPTH + 1).flatMap(leafCount =>
  60. range(leafCount).map(leafIndex => ({ leafCount, leafIndex })),
  61. ))
  62. it(`updating a leaf correctly updates the tree (leaf #${leafIndex + 1}/${leafCount})`, async function () {
  63. // initial tree
  64. const leaves = Array.from({ length: leafCount }, generators.bytes32);
  65. const oldTree = makeTree(leaves);
  66. // fill tree and verify root
  67. for (const i in leaves) {
  68. await this.mock.push(oldTree.leafHash(oldTree.at(i)));
  69. }
  70. await expect(this.mock.root()).to.eventually.equal(oldTree.root);
  71. // create updated tree
  72. leaves[leafIndex] = generators.bytes32();
  73. const newTree = makeTree(leaves);
  74. const oldLeafHash = oldTree.leafHash(oldTree.at(leafIndex));
  75. const newLeafHash = newTree.leafHash(newTree.at(leafIndex));
  76. // perform update
  77. await expect(this.mock.update(leafIndex, oldLeafHash, newLeafHash, oldTree.getProof(leafIndex)))
  78. .to.emit(this.mock, 'LeafUpdated')
  79. .withArgs(oldLeafHash, newLeafHash, leafIndex, newTree.root);
  80. // verify updated root
  81. await expect(this.mock.root()).to.eventually.equal(newTree.root);
  82. // if there is still room in the tree, fill it
  83. for (const i of range(leafCount, 2 ** DEPTH)) {
  84. // push new value and rebuild tree
  85. leaves.push(generators.bytes32());
  86. const nextTree = makeTree(leaves);
  87. // push and verify root
  88. await this.mock.push(nextTree.leafHash(nextTree.at(i)));
  89. await expect(this.mock.root()).to.eventually.equal(nextTree.root);
  90. }
  91. });
  92. it('replacing a leaf that was not previously pushed reverts', async function () {
  93. // changing leaf 0 on an empty tree
  94. await expect(this.mock.update(1, ZERO, ZERO, []))
  95. .to.be.revertedWithCustomError(this.mock, 'MerkleTreeUpdateInvalidIndex')
  96. .withArgs(1, 0);
  97. });
  98. it('replacing a leaf using an invalid proof reverts', async function () {
  99. const leafCount = 4;
  100. const leafIndex = 2;
  101. const leaves = Array.from({ length: leafCount }, generators.bytes32);
  102. const tree = makeTree(leaves);
  103. // fill tree and verify root
  104. for (const i in leaves) {
  105. await this.mock.push(tree.leafHash(tree.at(i)));
  106. }
  107. await expect(this.mock.root()).to.eventually.equal(tree.root);
  108. const oldLeafHash = tree.leafHash(tree.at(leafIndex));
  109. const newLeafHash = generators.bytes32();
  110. const proof = tree.getProof(leafIndex);
  111. // invalid proof (tamper)
  112. proof[1] = generators.bytes32();
  113. await expect(this.mock.update(leafIndex, oldLeafHash, newLeafHash, proof)).to.be.revertedWithCustomError(
  114. this.mock,
  115. 'MerkleTreeUpdateInvalidProof',
  116. );
  117. });
  118. });
  119. it('reset', async function () {
  120. // empty tree
  121. const emptyTree = makeTree();
  122. // tree with one element
  123. const leaves = [generators.bytes32()];
  124. const tree = makeTree(leaves);
  125. const hash = tree.leafHash(tree.at(0));
  126. // root should be that of a zero tree
  127. expect(await this.mock.root()).to.equal(emptyTree.root);
  128. expect(await this.mock.nextLeafIndex()).to.equal(0n);
  129. // push leaf and check root
  130. await expect(this.mock.push(hash)).to.emit(this.mock, 'LeafInserted').withArgs(hash, 0, tree.root);
  131. expect(await this.mock.root()).to.equal(tree.root);
  132. expect(await this.mock.nextLeafIndex()).to.equal(1n);
  133. // reset tree
  134. await this.mock.setup(DEPTH, ZERO);
  135. expect(await this.mock.root()).to.equal(emptyTree.root);
  136. expect(await this.mock.nextLeafIndex()).to.equal(0n);
  137. // re-push leaf and check root
  138. await expect(this.mock.push(hash)).to.emit(this.mock, 'LeafInserted').withArgs(hash, 0, tree.root);
  139. expect(await this.mock.root()).to.equal(tree.root);
  140. expect(await this.mock.nextLeafIndex()).to.equal(1n);
  141. });
  142. });