From 936486e73f58fb3534fcf0e3236a9f0dfbaffb7c Mon Sep 17 00:00:00 2001 From: cedoor Date: Thu, 5 Oct 2023 13:30:04 +0200 Subject: [PATCH] refactor(imt.sol): update quinary imt name and tests --- .../contracts/{QuinIMT.sol => QuinaryIMT.sol} | 46 +-- packages/imt.sol/contracts/README.md | 2 +- .../{QuinIMTTest.sol => QuinaryIMTTest.sol} | 14 +- packages/imt.sol/tasks/deploy-imt-test.ts | 16 +- packages/imt.sol/test/QuinIMT.ts | 309 ------------------ packages/imt.sol/test/QuinaryIMT.ts | 275 ++++++++++++++++ 6 files changed, 315 insertions(+), 347 deletions(-) rename packages/imt.sol/contracts/{QuinIMT.sol => QuinaryIMT.sol} (80%) rename packages/imt.sol/contracts/test/{QuinIMTTest.sol => QuinaryIMTTest.sol} (60%) delete mode 100644 packages/imt.sol/test/QuinIMT.ts create mode 100644 packages/imt.sol/test/QuinaryIMT.ts diff --git a/packages/imt.sol/contracts/QuinIMT.sol b/packages/imt.sol/contracts/QuinaryIMT.sol similarity index 80% rename from packages/imt.sol/contracts/QuinIMT.sol rename to packages/imt.sol/contracts/QuinaryIMT.sol index 8e6e4f94a..9aba9c3e1 100644 --- a/packages/imt.sol/contracts/QuinIMT.sol +++ b/packages/imt.sol/contracts/QuinaryIMT.sol @@ -5,7 +5,7 @@ import {PoseidonT6} from "poseidon-solidity/PoseidonT6.sol"; // Each incremental tree has certain properties and data that will // be used to add new leaves. -struct QuinIMTData { +struct QuinaryIMTData { uint256 depth; // Depth of the tree (levels - 1). uint256 root; // Root hash of the tree. uint256 numberOfLeaves; // Number of leaves of the tree. @@ -14,12 +14,12 @@ struct QuinIMTData { mapping(uint256 => uint256[5]) lastSubtrees; // Caching these values is essential to efficient appends. } -/// @title Incremental quin Merkle tree. +/// @title Incremental quinary Merkle tree. /// @dev The incremental tree allows to calculate the root hash each time a leaf is added, ensuring /// the integrity of the tree. -library QuinIMT { - uint8 internal constant MAX_DEPTH = 32; - uint256 internal constant SNARK_SCALAR_FIELD = +library QuinaryIMT { + uint8 public constant MAX_DEPTH = 32; + uint256 public constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; /// @dev Initializes a tree. @@ -27,12 +27,12 @@ library QuinIMT { /// @param depth: Depth of the tree. /// @param zero: Zero value to be used. function init( - QuinIMTData storage self, + QuinaryIMTData storage self, uint256 depth, uint256 zero ) public { - require(zero < SNARK_SCALAR_FIELD, "QuinIMT: leaf must be < SNARK_SCALAR_FIELD"); - require(depth > 0 && depth <= MAX_DEPTH, "QuinIMT: tree depth must be between 1 and 32"); + require(zero < SNARK_SCALAR_FIELD, "QuinaryIMT: leaf must be < SNARK_SCALAR_FIELD"); + require(depth > 0 && depth <= MAX_DEPTH, "QuinaryIMT: tree depth must be between 1 and 32"); self.depth = depth; @@ -60,11 +60,11 @@ library QuinIMT { /// @dev Inserts a leaf in the tree. /// @param self: Tree data. /// @param leaf: Leaf to be inserted. - function insert(QuinIMTData storage self, uint256 leaf) public { + function insert(QuinaryIMTData storage self, uint256 leaf) public { uint256 depth = self.depth; - require(leaf < SNARK_SCALAR_FIELD, "QuinIMT: leaf must be < SNARK_SCALAR_FIELD"); - require(self.numberOfLeaves < 5**depth, "QuinIMT: tree is full"); + require(leaf < SNARK_SCALAR_FIELD, "QuinaryIMT: leaf must be < SNARK_SCALAR_FIELD"); + require(self.numberOfLeaves < 5**depth, "QuinaryIMT: tree is full"); uint256 index = self.numberOfLeaves; uint256 hash = leaf; @@ -102,15 +102,15 @@ library QuinIMT { /// @param proofSiblings: Array of the sibling nodes of the proof of membership. /// @param proofPathIndices: Path of the proof of membership. function update( - QuinIMTData storage self, + QuinaryIMTData storage self, uint256 leaf, uint256 newLeaf, uint256[4][] calldata proofSiblings, uint8[] calldata proofPathIndices ) public { - require(newLeaf != leaf, "QuinIMT: new leaf cannot be the same as the old one"); - require(newLeaf < SNARK_SCALAR_FIELD, "QuinIMT: new leaf must be < SNARK_SCALAR_FIELD"); - require(verify(self, leaf, proofSiblings, proofPathIndices), "QuinIMT: leaf is not part of the tree"); + require(newLeaf != leaf, "QuinaryIMT: new leaf cannot be the same as the old one"); + require(newLeaf < SNARK_SCALAR_FIELD, "QuinaryIMT: new leaf must be < SNARK_SCALAR_FIELD"); + require(verify(self, leaf, proofSiblings, proofPathIndices), "QuinaryIMT: leaf is not part of the tree"); uint256 depth = self.depth; uint256 hash = newLeaf; @@ -143,7 +143,7 @@ library QuinIMT { ++i; } } - require(updateIndex < self.numberOfLeaves, "QuinIMT: leaf index out of range"); + require(updateIndex < self.numberOfLeaves, "QuinaryIMT: leaf index out of range"); self.root = hash; } @@ -154,7 +154,7 @@ library QuinIMT { /// @param proofSiblings: Array of the sibling nodes of the proof of membership. /// @param proofPathIndices: Path of the proof of membership. function remove( - QuinIMTData storage self, + QuinaryIMTData storage self, uint256 leaf, uint256[4][] calldata proofSiblings, uint8[] calldata proofPathIndices @@ -169,16 +169,16 @@ library QuinIMT { /// @param proofPathIndices: Path of the proof of membership. /// @return True or false. function verify( - QuinIMTData storage self, + QuinaryIMTData storage self, uint256 leaf, uint256[4][] calldata proofSiblings, uint8[] calldata proofPathIndices ) private view returns (bool) { - require(leaf < SNARK_SCALAR_FIELD, "QuinIMT: leaf must be < SNARK_SCALAR_FIELD"); + require(leaf < SNARK_SCALAR_FIELD, "QuinaryIMT: leaf must be < SNARK_SCALAR_FIELD"); uint256 depth = self.depth; require( proofPathIndices.length == depth && proofSiblings.length == depth, - "QuinIMT: length of path is not correct" + "QuinaryIMT: length of path is not correct" ); uint256 hash = leaf; @@ -186,13 +186,13 @@ library QuinIMT { for (uint8 i = 0; i < depth; ) { uint256[5] memory nodes; - require(proofPathIndices[i] >= 0 && proofPathIndices[i] < 5, "QuinIMT: path index is not between 0 and 4"); + require(proofPathIndices[i] >= 0 && proofPathIndices[i] < 5, "QuinaryIMT: path index is not between 0 and 4"); for (uint8 j = 0; j < 5; ) { if (j < proofPathIndices[i]) { require( proofSiblings[i][j] < SNARK_SCALAR_FIELD, - "QuinIMT: sibling node must be < SNARK_SCALAR_FIELD" + "QuinaryIMT: sibling node must be < SNARK_SCALAR_FIELD" ); nodes[j] = proofSiblings[i][j]; @@ -201,7 +201,7 @@ library QuinIMT { } else { require( proofSiblings[i][j - 1] < SNARK_SCALAR_FIELD, - "QuinIMT: sibling node must be < SNARK_SCALAR_FIELD" + "QuinaryIMT: sibling node must be < SNARK_SCALAR_FIELD" ); nodes[j] = proofSiblings[i][j - 1]; diff --git a/packages/imt.sol/contracts/README.md b/packages/imt.sol/contracts/README.md index 1727d5814..9210633ac 100644 --- a/packages/imt.sol/contracts/README.md +++ b/packages/imt.sol/contracts/README.md @@ -37,7 +37,7 @@ ## Libraries ✔️ [BinaryIMT](https://github.com/privacy-scaling-explorations/zk-kit/blob/main/packages/imt.sol/contracts/BinaryIMT.sol) (Poseidon)\ -✔️ [QuinIMT](https://github.com/privacy-scaling-explorations/zk-kit/blob/main/packages/imt.sol/contracts/QuinIMT.sol) (Poseidon)\ +✔️ [QuinaryIMT](https://github.com/privacy-scaling-explorations/zk-kit/blob/main/packages/imt.sol/contracts/QuinaryIMT.sol) (Poseidon)\ ✔️ [LazyIMT](https://github.com/privacy-scaling-explorations/zk-kit/blob/main/packages/imt.sol/contracts/LazyIMT.sol) (Poseidon) --- diff --git a/packages/imt.sol/contracts/test/QuinIMTTest.sol b/packages/imt.sol/contracts/test/QuinaryIMTTest.sol similarity index 60% rename from packages/imt.sol/contracts/test/QuinIMTTest.sol rename to packages/imt.sol/contracts/test/QuinaryIMTTest.sol index 9bf7824d5..690fa1f1f 100644 --- a/packages/imt.sol/contracts/test/QuinIMTTest.sol +++ b/packages/imt.sol/contracts/test/QuinaryIMTTest.sol @@ -2,17 +2,17 @@ pragma solidity ^0.8.0; -import "../QuinIMT.sol"; +import "../QuinaryIMT.sol"; -contract QuinIMTTest { - QuinIMTData public data; +contract QuinaryIMTTest { + QuinaryIMTData public data; function init(uint256 depth) external { - QuinIMT.init(data, depth, 0); + QuinaryIMT.init(data, depth, 0); } function insert(uint256 leaf) external { - QuinIMT.insert(data, leaf); + QuinaryIMT.insert(data, leaf); } function update( @@ -21,7 +21,7 @@ contract QuinIMTTest { uint256[4][] calldata proofSiblings, uint8[] calldata proofPathIndices ) external { - QuinIMT.update(data, leaf, newLeaf, proofSiblings, proofPathIndices); + QuinaryIMT.update(data, leaf, newLeaf, proofSiblings, proofPathIndices); } function remove( @@ -29,6 +29,6 @@ contract QuinIMTTest { uint256[4][] calldata proofSiblings, uint8[] calldata proofPathIndices ) external { - QuinIMT.remove(data, leaf, proofSiblings, proofPathIndices); + QuinaryIMT.remove(data, leaf, proofSiblings, proofPathIndices); } } diff --git a/packages/imt.sol/tasks/deploy-imt-test.ts b/packages/imt.sol/tasks/deploy-imt-test.ts index 4cd1a991c..b9f3e8404 100644 --- a/packages/imt.sol/tasks/deploy-imt-test.ts +++ b/packages/imt.sol/tasks/deploy-imt-test.ts @@ -1,12 +1,14 @@ import { task, types } from "hardhat/config" -import { PoseidonT3, proxy } from "poseidon-solidity" +import { PoseidonT3, PoseidonT6, proxy } from "poseidon-solidity" task("deploy:imt-test", "Deploy an IMT contract for testing a library") .addParam("library", "The name of the library", undefined, types.string) .addOptionalParam("logs", "Print the logs", true, types.boolean) - .setAction(async ({ logs, library: libraryName }, { ethers }): Promise => { - // Deterministically deploy PoseidonT3. + .addOptionalParam("arity", "The arity of the tree", 2, types.int) + .setAction(async ({ logs, library: libraryName, arity }, { ethers }): Promise => { + // Deterministically deploy Poseidon. const signer = await ethers.getSigner() + const Poseidon = arity === 5 ? PoseidonT6 : PoseidonT3 if ((await ethers.provider.getCode(proxy.address)) === "0x") { await signer.sendTransaction({ @@ -16,20 +18,20 @@ task("deploy:imt-test", "Deploy an IMT contract for testing a library") await ethers.provider.sendTransaction(proxy.tx) } - if ((await ethers.provider.getCode(PoseidonT3.address)) === "0x") { + if ((await ethers.provider.getCode(Poseidon.address)) === "0x") { await signer.sendTransaction({ to: proxy.address, - data: PoseidonT3.data + data: Poseidon.data }) } if (logs) { - console.info(`PoseidonT3 library has been deployed to: ${PoseidonT3.address}`) + console.info(`Poseidon library has been deployed to: ${Poseidon.address}`) } const LibraryFactory = await ethers.getContractFactory(libraryName, { libraries: { - PoseidonT3: PoseidonT3.address + [`PoseidonT${arity + 1}`]: Poseidon.address } }) const library = await LibraryFactory.deploy() diff --git a/packages/imt.sol/test/QuinIMT.ts b/packages/imt.sol/test/QuinIMT.ts deleted file mode 100644 index be149cb09..000000000 --- a/packages/imt.sol/test/QuinIMT.ts +++ /dev/null @@ -1,309 +0,0 @@ -//import { expect } from "chai" -//import { Contract } from "ethers" -//import { ethers, run } from "hardhat" -//import { createTree } from "./utils" - -//[> eslint-disable jest/valid-expect <] -//describe("IncrementalQuinTreeTest", () => { -//let contract: Contract - -//const treeId = ethers.utils.formatBytes32String("treeId") -//const leaf = BigInt(1) -//const depth = 8 - -//before(async () => { -//contract = await run("deploy:iqt-test", { logs: false }) -//}) - -//it("Should not create a tree with a depth > 32", async () => { -//const transaction = contract.createTree(treeId, 33) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTree: tree depth must be between 1 and 32") -//}) - -//it("Should create a tree", async () => { -//const transaction = contract.createTree(treeId, depth) - -//await expect(transaction).to.emit(contract, "TreeCreated").withArgs(treeId, depth) -//}) - -//it("Should not create a tree with an existing id", async () => { -//const transaction = contract.createTree(treeId, depth) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTreeTest: tree already exists") -//}) - -//it("Should not insert a leaf if the tree does not exist", async () => { -//const treeId = ethers.utils.formatBytes32String("treeId2") - -//const transaction = contract.insertLeaf(treeId, leaf) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTreeTest: tree does not exist") -//}) - -//it("Should not insert a leaf if its value is > SNARK_SCALAR_FIELD", async () => { -//const leaf = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495618") - -//const transaction = contract.insertLeaf(treeId, leaf) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTree: leaf must be < SNARK_SCALAR_FIELD") -//}) - -//it("Should insert a leaf in a tree", async () => { -//const tree = createTree(depth, 1, 5) -//const transaction = contract.insertLeaf(treeId, leaf) - -//await expect(transaction).to.emit(contract, "LeafInserted").withArgs(treeId, leaf, tree.root) -//}) - -//it("Should insert 6 leaves in a tree", async () => { -//const treeId = ethers.utils.formatBytes32String("tree2") -//await contract.createTree(treeId, depth) -//const tree = createTree(depth, 0, 5) - -//for (let i = 0; i < 6; i += 1) { -//tree.insert(BigInt(i + 1)) -//const transaction = contract.insertLeaf(treeId, BigInt(i + 1)) - -//await expect(transaction) -//.to.emit(contract, "LeafInserted") -//.withArgs(treeId, BigInt(i + 1), tree.root) -//} -//}) - -//it("Should not insert a leaf if the tree is full", async () => { -//const treeId = ethers.utils.formatBytes32String("tinyTree") - -//await contract.createTree(treeId, 1) -//await contract.insertLeaf(treeId, leaf) -//await contract.insertLeaf(treeId, leaf) -//await contract.insertLeaf(treeId, leaf) -//await contract.insertLeaf(treeId, leaf) -//await contract.insertLeaf(treeId, leaf) - -//const transaction = contract.insertLeaf(treeId, leaf) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTree: tree is full") -//}) - -//it("Should not update a leaf if the tree does not exist", async () => { -//const treeId = ethers.utils.formatBytes32String("none") - -//const transaction = contract.updateLeaf(treeId, leaf, leaf, [[0, 1, 2, 3]], [0]) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTreeTest: tree does not exist") -//}) - -//it("Should not update a leaf if the new value is the same as the old one", async () => { -//const leaf = BigInt(3) - -//const transaction = contract.updateLeaf(treeId, leaf, leaf, [[0, 1, 2, 3]], [0]) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTree: new leaf cannot be the same as the old one") -//}) - -//it("Should not update a leaf if its new value is > SNARK_SCALAR_FIELD", async () => { -//const leaf = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495618") - -//const transaction = contract.updateLeaf(treeId, BigInt(3), leaf, [[0, 1, 2, 3]], [0]) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTree: new leaf must be < SNARK_SCALAR_FIELD") -//}) - -//it("Should not update a leaf if its original value is > SNARK_SCALAR_FIELD", async () => { -//const leaf = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495618") - -//const transaction = contract.updateLeaf(treeId, leaf, BigInt(4), [[0, 1, 2, 3]], [0]) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTree: leaf must be < SNARK_SCALAR_FIELD") -//}) - -//it("Should not update a leaf if the path indices are wrong", async () => { -//const treeId = ethers.utils.formatBytes32String("tree2") -//const tree = createTree(depth, 0, 5) - -//for (let i = 0; i < 6; i += 1) { -//tree.insert(BigInt(i + 1)) -//} - -//const leaf = BigInt(1337) - -//tree.update(2, leaf) - -//const { pathIndices, siblings } = tree.createProof(2) - -//pathIndices[3] = 6 - -//const transaction = contract.updateLeaf(treeId, BigInt(3), leaf, siblings, pathIndices) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTree: path index is not between 0 and 4") -//}) - -//it("Should not update a leaf if the wrong current leaf is given", async () => { -//const treeId = ethers.utils.formatBytes32String("tree2") -//const tree = createTree(depth, 0, 5) - -//for (let i = 0; i < 6; i += 1) { -//tree.insert(BigInt(i + 1)) -//} - -//const leaf = BigInt(1337) - -//tree.update(2, leaf) - -//const { pathIndices, siblings } = tree.createProof(2) -//const transaction = contract.updateLeaf(treeId, BigInt(4), leaf, siblings, pathIndices) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTree: leaf is not part of the tree") -//}) - -//it("Should update a leaf", async () => { -//const treeId = ethers.utils.formatBytes32String("tree2") -//const tree = createTree(depth, 0, 5) - -//for (let i = 0; i < 6; i += 1) { -//tree.insert(BigInt(i + 1)) -//} - -//const leaf = BigInt(1337) - -//tree.update(2, leaf) - -//const { pathIndices, siblings, root } = tree.createProof(2) -//const transaction = contract.updateLeaf(treeId, BigInt(3), leaf, siblings, pathIndices) - -//await expect(transaction).to.emit(contract, "LeafUpdated").withArgs(treeId, leaf, root) -//}) - -//it("Should not remove a leaf if the tree does not exist", async () => { -//const treeId = ethers.utils.formatBytes32String("none") - -//const transaction = contract.removeLeaf(treeId, leaf, [[0, 1, 2, 3]], [0]) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTreeTest: tree does not exist") -//}) - -//it("Should not remove a leaf if its value is > SNARK_SCALAR_FIELD", async () => { -//const leaf = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495618") - -//const transaction = contract.removeLeaf(treeId, leaf, [[0, 1, 2, 3]], [0]) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTree: leaf must be < SNARK_SCALAR_FIELD") -//}) - -//it("Should remove a leaf", async () => { -//const treeId = ethers.utils.formatBytes32String("hello") -//const tree = createTree(depth, 3, 5) - -//tree.delete(0) - -//await contract.createTree(treeId, depth) -//await contract.insertLeaf(treeId, BigInt(1)) -//await contract.insertLeaf(treeId, BigInt(2)) -//await contract.insertLeaf(treeId, BigInt(3)) - -//const { siblings, pathIndices, root } = tree.createProof(0) -//const transaction = contract.removeLeaf(treeId, BigInt(1), siblings, pathIndices) - -//await expect(transaction).to.emit(contract, "LeafRemoved").withArgs(treeId, BigInt(1), root) -//}) - -//it("Should remove another leaf", async () => { -//const treeId = ethers.utils.formatBytes32String("hello") -//const tree = createTree(depth, 3, 5) - -//tree.delete(0) -//tree.delete(1) - -//const { siblings, pathIndices, root } = tree.createProof(1) - -//const transaction = contract.removeLeaf(treeId, BigInt(2), siblings, pathIndices) - -//await expect(transaction).to.emit(contract, "LeafRemoved").withArgs(treeId, BigInt(2), root) -//}) - -//it("Should not update a leaf that hasn't been inserted yet", async () => { -//// deploy a new, empty tree -//const treeId = ethers.utils.formatBytes32String("brokenTree") -//contract.createTree(treeId, depth) -//const tree = createTree(depth, 0, 5) - -//// insert 4 leaves into the tree -//for (let i = 0; i < 4; i += 1) { -//tree.insert(BigInt(i + 1)) -//await contract.insertLeaf(treeId, BigInt(i + 1)) -//} - -//// we're going to try to update leaf 7, despite there only being 4 leaves in the tree -//const leaf = BigInt(42069) - -//// note that we can insert zeros into the js library tree and the root won't change! -//// that's because we use the zeros optimization to calculate the roots efficiently. -//// technically speaking, there isn't an "empty" tree, there is only a tree that is -//// entirely full of the zero value at every index. therefore inserting the zero value -//// at any point into an incremental merkle tree doesn't change it's root, because -//// that is already the data the root was calculated from previously. in principle, -//// we can update any leaf that hasn't been inserted yet using this method -//const rootBeforeZeros = tree.root -//tree.insert(0) -//tree.insert(0) -//tree.insert(0) -//// the root doesn't change because the tree started full with 0s! -//expect(tree.root).to.be.equal(rootBeforeZeros) - -//// now we can make a merkle proof of zero being included at the uninitialized index -//const { pathIndices, siblings } = tree.createProof(6) - -//const transaction = contract.updateLeaf(treeId, BigInt(0), leaf, siblings, pathIndices) -//await expect(transaction).to.be.revertedWith("IncrementalQuinTree: leaf index out of range") -//}) - -//it("Should not remove a leaf that does not exist", async () => { -//const treeId = ethers.utils.formatBytes32String("hello") -//const tree = createTree(depth, 3, 5) - -//tree.delete(0) -//tree.delete(1) - -//const { siblings, pathIndices } = tree.createProof(0) - -//const transaction = contract.removeLeaf(treeId, BigInt(4), siblings, pathIndices) - -//await expect(transaction).to.be.revertedWith("IncrementalQuinTree: leaf is not part of the tree") -//}) - -//it("Should insert a leaf in a tree after a removal", async () => { -//const treeId = ethers.utils.formatBytes32String("hello") -//const tree = createTree(depth, 4, 5) - -//tree.delete(0) -//tree.delete(1) - -//const transaction = contract.insertLeaf(treeId, BigInt(4)) - -//await expect(transaction).to.emit(contract, "LeafInserted").withArgs(treeId, BigInt(4), tree.root) -//}) - -//it("Should insert 4 leaves and remove them all", async () => { -//const treeId = ethers.utils.formatBytes32String("complex") -//const tree = createTree(depth, 4, 5) - -//await contract.createTree(treeId, depth) - -//for (let i = 0; i < 4; i += 1) { -//await contract.insertLeaf(treeId, BigInt(i + 1)) -//} - -//for (let i = 0; i < 4; i += 1) { -//tree.delete(i) - -//const { siblings, pathIndices } = tree.createProof(i) - -//await contract.removeLeaf(treeId, BigInt(i + 1), siblings, pathIndices) -//} - -//const { root } = await contract.trees(treeId) - -//expect(root).to.equal(tree.root) -//}) -//}) diff --git a/packages/imt.sol/test/QuinaryIMT.ts b/packages/imt.sol/test/QuinaryIMT.ts new file mode 100644 index 000000000..7e2538c17 --- /dev/null +++ b/packages/imt.sol/test/QuinaryIMT.ts @@ -0,0 +1,275 @@ +import { IMT } from "@zk-kit/imt" +import { expect } from "chai" +import { run } from "hardhat" +import { QuinaryIMT, QuinaryIMTTest } from "../typechain-types" +import { createTree } from "./utils" + +/* eslint-disable jest/valid-expect */ +describe("QuinaryIMT", () => { + let quinaryIMTTest: QuinaryIMTTest + let quinaryIMT: QuinaryIMT + let imt: IMT + + beforeEach(async () => { + const { library, contract } = await run("deploy:imt-test", { library: "QuinaryIMT", arity: 5, logs: false }) + + quinaryIMTTest = contract + quinaryIMT = library + imt = createTree(16, undefined, 5) + }) + + it("Should not create a tree with a depth > 32", async () => { + const transaction = quinaryIMTTest.init(33) + + await expect(transaction).to.be.revertedWith("QuinaryIMT: tree depth must be between 1 and 32") + }) + + it("Should create a tree", async () => { + await quinaryIMTTest.init(imt.depth) + + const { depth } = await quinaryIMTTest.data() + + expect(depth).to.equal(imt.depth) + }) + + it("Should not insert a leaf if its value is > SNARK_SCALAR_FIELD", async () => { + const leaf = await quinaryIMT.SNARK_SCALAR_FIELD() + + const transaction = quinaryIMTTest.insert(leaf) + + await expect(transaction).to.be.revertedWith("QuinaryIMT: leaf must be < SNARK_SCALAR_FIELD") + }) + + it("Should insert a leaf in a tree", async () => { + await quinaryIMTTest.init(imt.depth) + + imt.insert(1) + + await quinaryIMTTest.insert(1) + + const { root } = await quinaryIMTTest.data() + + expect(root).to.equal(imt.root) + }) + + it("Should insert 6 leaves in a tree", async () => { + await quinaryIMTTest.init(imt.depth) + + for (let i = 0; i < 6; i += 1) { + const leaf = i + 1 + + imt.insert(leaf) + await quinaryIMTTest.insert(leaf) + + const { root } = await quinaryIMTTest.data() + + expect(root).to.equal(imt.root) + } + }) + + it("Should not insert a leaf if the tree is full", async () => { + await quinaryIMTTest.init(1) + + for (let i = 0; i < 5; i += 1) { + await quinaryIMTTest.insert(i + 1) + } + + const transaction = quinaryIMTTest.insert(3) + + await expect(transaction).to.be.revertedWith("QuinaryIMT: tree is full") + }) + + it("Should not update a leaf if the new value is the same as the old one", async () => { + await quinaryIMTTest.init(imt.depth) + await quinaryIMTTest.insert(1) + + const transaction = quinaryIMTTest.update(1, 1, [[0, 1, 2, 3]], [0]) + + await expect(transaction).to.be.revertedWith("QuinaryIMT: new leaf cannot be the same as the old one") + }) + + it("Should not update a leaf if its new value is > SNARK_SCALAR_FIELD", async () => { + await quinaryIMTTest.init(imt.depth) + await quinaryIMTTest.insert(1) + + const newLeaf = await quinaryIMT.SNARK_SCALAR_FIELD() + + const transaction = quinaryIMTTest.update(1, newLeaf, [[0, 1, 2, 3]], [0]) + + await expect(transaction).to.be.revertedWith("QuinaryIMT: new leaf must be < SNARK_SCALAR_FIELD") + }) + + it("Should not update a leaf if its original value is > SNARK_SCALAR_FIELD", async () => { + await quinaryIMTTest.init(imt.depth) + await quinaryIMTTest.insert(1) + + const oldLeaf = await quinaryIMT.SNARK_SCALAR_FIELD() + + const transaction = quinaryIMTTest.update(oldLeaf, 2, [[0, 1, 2, 3]], [0]) + + await expect(transaction).to.be.revertedWith("QuinaryIMT: leaf must be < SNARK_SCALAR_FIELD") + }) + + it("Should not update a leaf if the path indices are wrong", async () => { + await quinaryIMTTest.init(imt.depth) + await quinaryIMTTest.insert(1) + + imt.insert(1) + imt.update(0, 2) + + const { pathIndices, siblings } = imt.createProof(0) + + pathIndices[0] = 5 + + const transaction = quinaryIMTTest.update(1, 2, siblings, pathIndices) + + await expect(transaction).to.be.revertedWith("QuinaryIMT: path index is not between 0 and 4") + }) + + it("Should not update a leaf if the old leaf is wrong", async () => { + await quinaryIMTTest.init(imt.depth) + await quinaryIMTTest.insert(1) + + imt.insert(1) + imt.update(0, 2) + + const { pathIndices, siblings } = imt.createProof(0) + + const transaction = quinaryIMTTest.update(2, 3, siblings, pathIndices) + + await expect(transaction).to.be.revertedWith("QuinaryIMT: leaf is not part of the tree") + }) + + it("Should update a leaf", async () => { + await quinaryIMTTest.init(imt.depth) + await quinaryIMTTest.insert(1) + + imt.insert(1) + imt.update(0, 2) + + const { pathIndices, siblings } = imt.createProof(0) + + await quinaryIMTTest.update(1, 2, siblings, pathIndices) + + const { root } = await quinaryIMTTest.data() + + expect(root).to.equal(imt.root) + }) + + it("Should not remove a leaf if its value is > SNARK_SCALAR_FIELD", async () => { + const leaf = await quinaryIMT.SNARK_SCALAR_FIELD() + + const transaction = quinaryIMTTest.remove(leaf, [[0, 1, 2, 3]], [0]) + + await expect(transaction).to.be.revertedWith("QuinaryIMT: leaf must be < SNARK_SCALAR_FIELD") + }) + + it("Should not remove a leaf that does not exist", async () => { + await quinaryIMTTest.init(imt.depth) + await quinaryIMTTest.insert(1) + + imt.insert(1) + imt.delete(0) + + const { siblings, pathIndices } = imt.createProof(0) + + const transaction = quinaryIMTTest.remove(2, siblings, pathIndices) + + await expect(transaction).to.be.revertedWith("QuinaryIMT: leaf is not part of the tree") + }) + + it("Should remove a leaf", async () => { + await quinaryIMTTest.init(imt.depth) + await quinaryIMTTest.insert(1) + + imt.insert(1) + imt.delete(0) + + const { pathIndices, siblings } = imt.createProof(0) + + await quinaryIMTTest.remove(1, siblings, pathIndices) + + const { root } = await quinaryIMTTest.data() + + expect(root).to.equal(imt.root) + }) + + it("Should not update a leaf that hasn't been inserted yet", async () => { + quinaryIMTTest.init(imt.depth) + + for (let i = 0; i < 4; i += 1) { + const leaf = i + 1 + + imt.insert(leaf) + await quinaryIMTTest.insert(leaf) + } + + // We're going to try to update leaf 7, despite there only being 4 leaves in the tree. + const leaf = 42069 + + // Note that we can insert zeros into the js library tree and the root won't change! + // that's because we use the zeros optimization to calculate the roots efficiently. + // technically speaking, there isn't an "empty" tree, there is only a tree that is + // entirely full of the zero value at every index. Therefore inserting the zero value + // at any point into an incremental merkle tree doesn't change it's root, because + // that is already the data the root was calculated from previously. In principle, + // we can update any leaf that hasn't been inserted yet using this method. + const rootBeforeZeros = imt.root + + imt.insert(0) + imt.insert(0) + imt.insert(0) + + // The root doesn't change because the tree started full with 0s! + expect(imt.root).to.be.equal(rootBeforeZeros) + + // Now we can make a merkle proof of zero being included at the uninitialized index. + const { pathIndices, siblings } = imt.createProof(6) + + const transaction = quinaryIMTTest.update(0, leaf, siblings, pathIndices) + + await expect(transaction).to.be.revertedWith("QuinaryIMT: leaf index out of range") + }) + + it("Should insert a leaf in a tree after a removal", async () => { + await quinaryIMTTest.init(imt.depth) + await quinaryIMTTest.insert(1) + + imt.insert(1) + imt.delete(0) + + const { pathIndices, siblings } = imt.createProof(0) + + await quinaryIMTTest.remove(1, siblings, pathIndices) + + imt.insert(2) + await quinaryIMTTest.insert(2) + + const { root } = await quinaryIMTTest.data() + + expect(root).to.equal(imt.root) + }) + + it("Should insert 4 leaves and remove them all", async () => { + await quinaryIMTTest.init(imt.depth) + + for (let i = 0; i < 4; i += 1) { + const leaf = i + 1 + + imt.insert(leaf) + await quinaryIMTTest.insert(leaf) + } + + for (let i = 0; i < 4; i += 1) { + imt.delete(i) + + const { siblings, pathIndices } = imt.createProof(i) + + await quinaryIMTTest.remove(i + 1, siblings, pathIndices) + } + + const { root } = await quinaryIMTTest.data() + + expect(root).to.equal(imt.root) + }) +})