diff --git a/packages/hardhat/test/maci-tests/AccQueue.test.ts b/packages/hardhat/test/maci-tests/AccQueue.test.ts new file mode 100644 index 0000000..9e038bd --- /dev/null +++ b/packages/hardhat/test/maci-tests/AccQueue.test.ts @@ -0,0 +1,490 @@ +import { expect } from "chai"; +import { AccQueue, hashLeftRight, NOTHING_UP_MY_SLEEVE } from "../../maci-ts/crypto"; + +import { AccQueue as AccQueueContract } from "../../typechain-types"; + +import { + deployTestAccQueues, + fillGasLimit, + enqueueGasLimit, + testEmptySubtree, + testEmptyUponDeployment, + testEnqueue, + testEnqueueAndInsertSubTree, + testFillForAllIncompletes, + testIncompleteSubtree, + testInsertSubTrees, + testMerge, + testMergeAgain, +} from "./utils"; + +describe("AccQueues", () => { + describe("Binary AccQueue enqueues", () => { + const SUB_DEPTH = 2; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + let aqContract: AccQueueContract; + + before(async () => { + const r = await deployTestAccQueues("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aqContract = r.aqContract as AccQueueContract; + }); + + it("should be empty upon deployment", async () => { + await testEmptyUponDeployment(aqContract); + }); + + it("should not be able to get a subroot that does not exist", async () => { + await expect(aqContract.getSubRoot(5)).to.be.revertedWithCustomError(aqContract, "InvalidIndex"); + }); + + it("should enqueue leaves", async () => { + await testEnqueue(aqContract, HASH_LENGTH, SUB_DEPTH, ZERO); + }); + }); + + describe("Quinary AccQueue enqueues", () => { + const SUB_DEPTH = 2; + const HASH_LENGTH = 5; + const ZERO = BigInt(0); + let aqContract: AccQueueContract; + + before(async () => { + const r = await deployTestAccQueues("AccQueueQuinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aqContract = r.aqContract as AccQueueContract; + }); + + it("should be empty upon deployment", async () => { + await testEmptyUponDeployment(aqContract); + }); + + it("should not be able to get a subroot that does not exist", async () => { + await expect(aqContract.getSubRoot(5)).to.be.revertedWithCustomError(aqContract, "InvalidIndex"); + }); + + it("should enqueue leaves", async () => { + await testEnqueue(aqContract, HASH_LENGTH, SUB_DEPTH, ZERO); + }); + }); + + describe("Binary AccQueue0 fills", () => { + const SUB_DEPTH = 2; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + let aq: AccQueue; + let aqContract: AccQueueContract; + + before(async () => { + const r = await deployTestAccQueues("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aq = r.aq; + aqContract = r.aqContract as AccQueueContract; + }); + + it("should fill an empty subtree", async () => { + await testEmptySubtree(aq, aqContract, 0); + }); + + it("should fill an incomplete subtree", async () => { + await testIncompleteSubtree(aq, aqContract); + }); + + it("Filling an empty subtree again should create the correct subroot", async () => { + await testEmptySubtree(aq, aqContract, 2); + }); + + it("fill() should be correct for every number of leaves in an incomplete subtree", async () => { + await testFillForAllIncompletes(aq, aqContract, HASH_LENGTH); + }); + }); + + describe("Quinary AccQueue0 fills", () => { + const SUB_DEPTH = 2; + const HASH_LENGTH = 5; + const ZERO = BigInt(0); + let aq: AccQueue; + let aqContract: AccQueueContract; + + before(async () => { + const r = await deployTestAccQueues("AccQueueQuinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aq = r.aq; + aqContract = r.aqContract as AccQueueContract; + }); + + it("should fill an empty subtree", async () => { + await testEmptySubtree(aq, aqContract, 0); + }); + + it("should fill an incomplete subtree", async () => { + await testIncompleteSubtree(aq, aqContract); + }); + + it("Filling an empty subtree again should create the correct subroot", async () => { + await testEmptySubtree(aq, aqContract, 2); + }); + + it("fill() should be correct for every number of leaves in an incomplete subtree", async () => { + await testFillForAllIncompletes(aq, aqContract, HASH_LENGTH); + }); + }); + + describe("Binary AccQueueMaci fills", () => { + const SUB_DEPTH = 2; + const HASH_LENGTH = 2; + const ZERO = NOTHING_UP_MY_SLEEVE; + let aq: AccQueue; + let aqContract: AccQueueContract; + + before(async () => { + const r = await deployTestAccQueues("AccQueueBinaryMaci", SUB_DEPTH, HASH_LENGTH, ZERO); + aq = r.aq; + aqContract = r.aqContract as AccQueueContract; + }); + + it("should fill an empty subtree", async () => { + await testEmptySubtree(aq, aqContract, 0); + }); + + it("should fill an incomplete subtree", async () => { + await testIncompleteSubtree(aq, aqContract); + }); + + it("Filling an empty subtree again should create the correct subroot", async () => { + await testEmptySubtree(aq, aqContract, 2); + }); + + it("fill() should be correct for every number of leaves in an incomplete subtree", async () => { + await testFillForAllIncompletes(aq, aqContract, HASH_LENGTH); + }); + }); + + describe("Quinary AccQueueMaci fills", () => { + const SUB_DEPTH = 2; + const HASH_LENGTH = 5; + const ZERO = NOTHING_UP_MY_SLEEVE; + let aq: AccQueue; + let aqContract: AccQueueContract; + + before(async () => { + const r = await deployTestAccQueues("AccQueueQuinaryMaci", SUB_DEPTH, HASH_LENGTH, ZERO); + aq = r.aq; + aqContract = r.aqContract as AccQueueContract; + }); + + it("should fill an empty subtree", async () => { + await testEmptySubtree(aq, aqContract, 0); + }); + + it("should fill an incomplete subtree", async () => { + await testIncompleteSubtree(aq, aqContract); + }); + + it("Filling an empty subtree again should create the correct subroot", async () => { + await testEmptySubtree(aq, aqContract, 2); + }); + + it("fill() should be correct for every number of leaves in an incomplete subtree", async () => { + await testFillForAllIncompletes(aq, aqContract, HASH_LENGTH); + }); + }); + + describe("Merge after enqueuing more leaves", () => { + const SUB_DEPTH = 2; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + const MAIN_DEPTH = 3; + + it("should produce the correct main roots", async () => { + const r = await deployTestAccQueues("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + await testMergeAgain(r.aq, r.aqContract as AccQueueContract, MAIN_DEPTH); + }); + }); + + describe("Edge cases", () => { + const SUB_DEPTH = 2; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + + it("should not be possible to merge into a tree of depth 0", async () => { + const r = await deployTestAccQueues("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + + const aqContract = r.aqContract as AccQueueContract; + await aqContract.enqueue(1).then(tx => tx.wait()); + await aqContract.mergeSubRoots(0, { gasLimit: 1000000 }).then(tx => tx.wait()); + await expect(aqContract.merge(0, { gasLimit: 1000000 })).to.revertedWithCustomError( + aqContract, + "DepthCannotBeZero", + ); + }); + + it("A small SRT of depth 1 should just have 2 leaves", async () => { + const r = await deployTestAccQueues("AccQueueBinary0", 1, HASH_LENGTH, ZERO); + + const aqContract = r.aqContract as AccQueueContract; + await aqContract.enqueue(0, enqueueGasLimit).then(tx => tx.wait()); + await aqContract.mergeSubRoots(0, { gasLimit: 1000000 }).then(tx => tx.wait()); + const srtRoot = await aqContract.getSmallSRTroot(); + const expectedRoot = hashLeftRight(BigInt(0), BigInt(0)); + expect(srtRoot.toString()).to.eq(expectedRoot.toString()); + }); + + it("should not be possible to merge subroots into a tree shorter than the SRT depth", async () => { + const r = await deployTestAccQueues("AccQueueBinary0", 1, HASH_LENGTH, ZERO); + const aqContract = r.aqContract as AccQueueContract; + for (let i = 0; i < 4; i += 1) { + // eslint-disable-next-line no-await-in-loop + await aqContract.fill(fillGasLimit).then(tx => tx.wait()); + } + + await aqContract.mergeSubRoots(0, { gasLimit: 1000000 }).then(tx => tx.wait()); + + await expect(aqContract.merge(1, { gasLimit: 1000000 })).to.be.revertedWithCustomError( + aqContract, + "DepthTooSmall", + ); + }); + + it("Merging without enqueuing new data should not change the root", async () => { + const MAIN_DEPTH = 5; + + const r = await deployTestAccQueues("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + + const { aq } = r; + const aqContract = r.aqContract as AccQueueContract; + // Merge once + await testMerge(aq, aqContract, 1, MAIN_DEPTH); + // Get the root + const expectedMainRoot = (await aqContract.getMainRoot(MAIN_DEPTH)).toString(); + // Merge again + await aqContract.merge(MAIN_DEPTH, { gasLimit: 8000000 }).then(tx => tx.wait()); + // Get the root again + const root = (await aqContract.getMainRoot(MAIN_DEPTH)).toString(); + // Check that the roots match + expect(root).to.eq(expectedMainRoot); + }); + }); + + describe("Binary AccQueue0 one-shot merges", () => { + const SUB_DEPTH = 2; + const MAIN_DEPTH = 5; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + + const testParams = [1, 2, 3, 4]; + testParams.forEach(testParam => { + it(`should merge ${testParam} subtrees`, async () => { + const r = await deployTestAccQueues("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + const { aq } = r; + const aqContract = r.aqContract as AccQueueContract; + await testMerge(aq, aqContract, testParam, MAIN_DEPTH); + }); + }); + }); + + describe("Quinary AccQueue0 one-shot merges", () => { + const SUB_DEPTH = 2; + const MAIN_DEPTH = 6; + const HASH_LENGTH = 5; + const ZERO = BigInt(0); + + const testParams = [1, 5, 26]; + testParams.forEach(testParam => { + it(`should merge ${testParam} subtrees`, async () => { + const r = await deployTestAccQueues("AccQueueQuinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + const { aq } = r; + const aqContract = r.aqContract as AccQueueContract; + await testMerge(aq, aqContract, testParam, MAIN_DEPTH); + }); + }); + }); + + describe("Binary AccQueue0 subtree insertions", () => { + const SUB_DEPTH = 2; + const MAIN_DEPTH = 6; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + + it("Enqueued leaves and inserted subtrees should be in the right order", async () => { + const r = await deployTestAccQueues("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + const { aq } = r; + const aqContract = r.aqContract as AccQueueContract; + await testEnqueueAndInsertSubTree(aq, aqContract); + }); + + const testParams = [1, 2, 3, 9]; + testParams.forEach(testParam => { + it(`should insert ${testParam} subtrees`, async () => { + const r = await deployTestAccQueues("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + const { aq } = r; + const aqContract = r.aqContract as AccQueueContract; + await testInsertSubTrees(aq, aqContract, testParam, MAIN_DEPTH); + }); + }); + }); + + describe("Quinary AccQueue0 subtree insertions", () => { + const SUB_DEPTH = 2; + const MAIN_DEPTH = 6; + const HASH_LENGTH = 5; + const ZERO = BigInt(0); + + it("Enqueued leaves and inserted subtrees should be in the right order", async () => { + const r = await deployTestAccQueues("AccQueueQuinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + const { aq } = r; + const aqContract = r.aqContract as AccQueueContract; + await testEnqueueAndInsertSubTree(aq, aqContract); + }); + + const testParams = [1, 4, 9, 26]; + testParams.forEach(testParam => { + it(`should insert ${testParam} subtrees`, async () => { + const r = await deployTestAccQueues("AccQueueQuinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + const { aq } = r; + const aqContract = r.aqContract as AccQueueContract; + await testInsertSubTrees(aq, aqContract, testParam, MAIN_DEPTH); + }); + }); + }); + + describe("Binary AccQueue0 progressive merges", () => { + const SUB_DEPTH = 2; + const MAIN_DEPTH = 5; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + const NUM_SUBTREES = 5; + let aq: AccQueue; + let aqContract: AccQueueContract; + + before(async () => { + const r = await deployTestAccQueues("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aq = r.aq; + aqContract = r.aqContract as AccQueueContract; + }); + + it(`should progressively merge ${NUM_SUBTREES} subtrees`, async () => { + for (let i = 0; i < NUM_SUBTREES; i += 1) { + const leaf = BigInt(123); + // eslint-disable-next-line no-await-in-loop + await aqContract.enqueue(leaf.toString(), enqueueGasLimit).then(tx => tx.wait()); + aq.enqueue(leaf); + + aq.fill(); + // eslint-disable-next-line no-await-in-loop + await aqContract.fill(fillGasLimit).then(tx => tx.wait()); + } + + aq.mergeSubRoots(0); + const expectedSmallSRTroot = aq.getSmallSRTroot(); + + await expect(aqContract.getSmallSRTroot()).to.be.revertedWithCustomError(aqContract, "SubTreesNotMerged"); + + await aqContract.mergeSubRoots(2).then(tx => tx.wait()); + await aqContract.mergeSubRoots(2).then(tx => tx.wait()); + await aqContract.mergeSubRoots(1).then(tx => tx.wait()); + + const contractSmallSRTroot = await aqContract.getSmallSRTroot(); + expect(expectedSmallSRTroot.toString()).to.eq(contractSmallSRTroot.toString()); + + aq.merge(MAIN_DEPTH); + await (await aqContract.merge(MAIN_DEPTH)).wait(); + + const expectedMainRoot = aq.getMainRoots()[MAIN_DEPTH]; + const contractMainRoot = await aqContract.getMainRoot(MAIN_DEPTH); + + expect(expectedMainRoot.toString()).to.eq(contractMainRoot.toString()); + }); + }); + + describe("Quinary AccQueue0 progressive merges", () => { + const SUB_DEPTH = 2; + const MAIN_DEPTH = 5; + const HASH_LENGTH = 5; + const ZERO = BigInt(0); + const NUM_SUBTREES = 6; + let aq: AccQueue; + let aqContract: AccQueueContract; + + before(async () => { + const r = await deployTestAccQueues("AccQueueQuinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aq = r.aq; + aqContract = r.aqContract as AccQueueContract; + }); + + it(`should progressively merge ${NUM_SUBTREES} subtrees`, async () => { + for (let i = 0; i < NUM_SUBTREES; i += 1) { + const leaf = BigInt(123); + // eslint-disable-next-line no-await-in-loop + await aqContract.enqueue(leaf.toString(), enqueueGasLimit).then(tx => tx.wait()); + aq.enqueue(leaf); + + aq.fill(); + // eslint-disable-next-line no-await-in-loop + await aqContract.fill(fillGasLimit).then(tx => tx.wait()); + } + + aq.mergeSubRoots(0); + const expectedSmallSRTroot = aq.getSmallSRTroot(); + + await expect(aqContract.getSmallSRTroot()).to.be.revertedWithCustomError(aqContract, "SubTreesNotMerged"); + + await (await aqContract.mergeSubRoots(2)).wait(); + await (await aqContract.mergeSubRoots(2)).wait(); + await (await aqContract.mergeSubRoots(2)).wait(); + + const contractSmallSRTroot = await aqContract.getSmallSRTroot(); + expect(expectedSmallSRTroot.toString()).to.eq(contractSmallSRTroot.toString()); + + aq.merge(MAIN_DEPTH); + await (await aqContract.merge(MAIN_DEPTH)).wait(); + + const expectedMainRoot = aq.getMainRoots()[MAIN_DEPTH]; + const contractMainRoot = await aqContract.getMainRoot(MAIN_DEPTH); + + expect(expectedMainRoot.toString()).to.eq(contractMainRoot.toString()); + }); + }); + + describe("Conditions that cause merge() to revert", () => { + const SUB_DEPTH = 2; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + const NUM_SUBTREES = 1; + let aqContract: AccQueueContract; + + before(async () => { + const r = await deployTestAccQueues("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aqContract = r.aqContract as AccQueueContract; + }); + + it("mergeSubRoots() should fail on an empty AccQueue", async () => { + await expect(aqContract.mergeSubRoots(0, { gasLimit: 1000000 })).to.be.revertedWithCustomError( + aqContract, + "NothingToMerge", + ); + }); + + it("merge() should revert on an empty AccQueue", async () => { + await expect(aqContract.merge(1, { gasLimit: 1000000 })).to.be.revertedWithCustomError( + aqContract, + "SubTreesNotMerged", + ); + }); + + it(`merge() should revert if there are unmerged subtrees`, async () => { + for (let i = 0; i < NUM_SUBTREES; i += 1) { + // eslint-disable-next-line no-await-in-loop + await aqContract.fill(fillGasLimit).then(tx => tx.wait()); + } + + await expect(aqContract.merge(1)).to.be.revertedWithCustomError(aqContract, "SubTreesNotMerged"); + }); + + it(`merge() should revert if the desired depth is invalid`, async () => { + await aqContract.mergeSubRoots(0, { gasLimit: 1000000 }).then(tx => tx.wait()); + + await expect(aqContract.merge(0, { gasLimit: 1000000 })).to.be.revertedWithCustomError( + aqContract, + "DepthCannotBeZero", + ); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/AccQueueBenchmark.test.ts b/packages/hardhat/test/maci-tests/AccQueueBenchmark.test.ts new file mode 100644 index 0000000..6f13581 --- /dev/null +++ b/packages/hardhat/test/maci-tests/AccQueueBenchmark.test.ts @@ -0,0 +1,276 @@ +import { expect } from "chai"; +import { AccQueue, NOTHING_UP_MY_SLEEVE } from "../../maci-ts/crypto"; + +import { deployPoseidonContracts, linkPoseidonLibraries } from "../../maci-ts/ts/deploy"; +import { getDefaultSigner } from "../../maci-ts/ts/utils"; +import { AccQueue as AccQueueContract } from "../../typechain-types"; + +let aqContract: AccQueueContract; + +const deploy = async (contractName: string, SUB_DEPTH: number, HASH_LENGTH: number, ZERO: bigint) => { + const { PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract } = + await deployPoseidonContracts(await getDefaultSigner(), {}, true); + const [poseidonT3ContractAddress, poseidonT4ContractAddress, poseidonT5ContractAddress, poseidonT6ContractAddress] = + await Promise.all([ + PoseidonT3Contract.getAddress(), + PoseidonT4Contract.getAddress(), + PoseidonT5Contract.getAddress(), + PoseidonT6Contract.getAddress(), + ]); + + // Link Poseidon contracts + const AccQueueFactory = await linkPoseidonLibraries( + contractName, + poseidonT3ContractAddress, + poseidonT4ContractAddress, + poseidonT5ContractAddress, + poseidonT6ContractAddress, + await getDefaultSigner(), + true, + ); + + aqContract = (await AccQueueFactory.deploy(SUB_DEPTH)) as typeof aqContract; + + await aqContract.deploymentTransaction()?.wait(); + + const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO); + return { aq, aqContract }; +}; + +const testMerge = async ( + aq: AccQueue, + contract: AccQueueContract, + NUM_SUBTREES: number, + MAIN_DEPTH: number, + NUM_MERGES: number, +) => { + for (let i = 0; i < NUM_SUBTREES; i += 1) { + const leaf = BigInt(123); + // eslint-disable-next-line no-await-in-loop + await contract.enqueue(leaf.toString(), { gasLimit: 200000 }).then(tx => tx.wait()); + + aq.enqueue(leaf); + aq.fill(); + + // eslint-disable-next-line no-await-in-loop + await contract.fill({ gasLimit: 2000000 }).then(t => t.wait()); + } + + if (NUM_MERGES === 0) { + aq.mergeSubRoots(NUM_MERGES); + const tx = await contract.mergeSubRoots(NUM_MERGES, { gasLimit: 8000000 }); + const receipt = await tx.wait(); + + expect(receipt).to.not.eq(null); + expect(receipt?.gasUsed.toString()).to.not.eq(""); + expect(receipt?.gasUsed.toString()).to.not.eq("0"); + } else { + for (let i = 0; i < NUM_MERGES; i += 1) { + const n = NUM_SUBTREES / NUM_MERGES; + aq.mergeSubRoots(n); + // eslint-disable-next-line no-await-in-loop + const receipt = await contract.mergeSubRoots(n, { gasLimit: 8000000 }).then(tx => tx.wait()); + + expect(receipt).to.not.eq(null); + expect(receipt?.gasUsed.toString()).to.not.eq(""); + expect(receipt?.gasUsed.toString()).to.not.eq("0"); + } + } + + const expectedSmallSRTroot = aq.getSmallSRTroot(); + + const contractSmallSRTroot = await contract.getSmallSRTroot(); + + expect(expectedSmallSRTroot.toString()).to.eq(contractSmallSRTroot.toString()); + + aq.merge(MAIN_DEPTH); + const receipt = await contract.merge(MAIN_DEPTH, { gasLimit: 8000000 }).then(tx => tx.wait()); + + expect(receipt).to.not.eq(null); + expect(receipt?.gasUsed.toString()).to.not.eq(""); + expect(receipt?.gasUsed.toString()).to.not.eq("0"); + + const expectedMainRoot = aq.getMainRoots()[MAIN_DEPTH]; + const contractMainRoot = await contract.getMainRoot(MAIN_DEPTH); + + expect(expectedMainRoot.toString()).to.eq(contractMainRoot.toString()); +}; + +const testOneShot = async (aq: AccQueue, contract: AccQueueContract, NUM_SUBTREES: number, MAIN_DEPTH: number) => { + await testMerge(aq, contract, NUM_SUBTREES, MAIN_DEPTH, 0); +}; + +const testMultiShot = async ( + aq: AccQueue, + contract: AccQueueContract, + NUM_SUBTREES: number, + MAIN_DEPTH: number, + NUM_MERGES: number, +) => { + await testMerge(aq, contract, NUM_SUBTREES, MAIN_DEPTH, NUM_MERGES); +}; + +describe("AccQueue gas benchmarks", () => { + describe("Binary enqueues", () => { + const SUB_DEPTH = 3; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + before(async () => { + const r = await deploy("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aqContract = r.aqContract; + }); + + it(`should enqueue to a subtree of depth ${SUB_DEPTH}`, async () => { + for (let i = 0; i < HASH_LENGTH ** SUB_DEPTH; i += 1) { + // eslint-disable-next-line no-await-in-loop + const receipt = await aqContract.enqueue(i, { gasLimit: 400000 }).then(tx => tx.wait()); + + expect(receipt).to.not.eq(null); + expect(receipt?.gasUsed.toString()).to.not.eq(""); + expect(receipt?.gasUsed.toString()).to.not.eq("0"); + } + }); + }); + + describe("Binary fills", () => { + const SUB_DEPTH = 3; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + before(async () => { + const r = await deploy("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aqContract = r.aqContract; + }); + + it(`should fill to a subtree of depth ${SUB_DEPTH}`, async () => { + for (let i = 0; i < 2; i += 1) { + // eslint-disable-next-line no-await-in-loop + await aqContract.enqueue(i, { gasLimit: 800000 }).then(tx => tx.wait()); + // eslint-disable-next-line no-await-in-loop + const receipt = await aqContract.fill({ gasLimit: 800000 }).then(tx => tx.wait()); + + expect(receipt).to.not.eq(null); + expect(receipt?.gasUsed.toString()).to.not.eq(""); + expect(receipt?.gasUsed.toString()).to.not.eq("0"); + } + }); + }); + + describe("Quinary enqueues", () => { + const SUB_DEPTH = 2; + const HASH_LENGTH = 5; + const ZERO = BigInt(0); + before(async () => { + const r = await deploy("AccQueueQuinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aqContract = r.aqContract; + }); + + it(`should enqueue to a subtree of depth ${SUB_DEPTH}`, async () => { + for (let i = 0; i < HASH_LENGTH ** SUB_DEPTH; i += 1) { + // eslint-disable-next-line no-await-in-loop + const receipt = await aqContract.enqueue(i, { gasLimit: 800000 }).then(tx => tx.wait()); + + expect(receipt).to.not.eq(null); + expect(receipt?.gasUsed.toString()).to.not.eq(""); + expect(receipt?.gasUsed.toString()).to.not.eq("0"); + } + }); + }); + + describe("Quinary fills", () => { + const SUB_DEPTH = 2; + const HASH_LENGTH = 5; + const ZERO = NOTHING_UP_MY_SLEEVE; + before(async () => { + const r = await deploy("AccQueueQuinaryMaci", SUB_DEPTH, HASH_LENGTH, ZERO); + aqContract = r.aqContract; + }); + + it(`should fill a subtree of depth ${SUB_DEPTH}`, async () => { + for (let i = 0; i < 2; i += 1) { + // eslint-disable-next-line no-await-in-loop + await aqContract.enqueue(i, { gasLimit: 800000 }).then(tx => tx.wait()); + // eslint-disable-next-line no-await-in-loop + const receipt = await aqContract.fill({ gasLimit: 800000 }).then(tx => tx.wait()); + + expect(receipt).to.not.eq(null); + expect(receipt?.gasUsed.toString()).to.not.eq(""); + expect(receipt?.gasUsed.toString()).to.not.eq("0"); + } + }); + }); + + describe("Binary AccQueue0 one-shot merge", () => { + const SUB_DEPTH = 4; + const MAIN_DEPTH = 32; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + const NUM_SUBTREES = 32; + let aq: AccQueue; + before(async () => { + const r = await deploy("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aq = r.aq; + aqContract = r.aqContract; + }); + + it(`should merge ${NUM_SUBTREES} subtrees`, async () => { + await testOneShot(aq, aqContract, NUM_SUBTREES, MAIN_DEPTH); + }); + }); + + describe("Binary AccQueue0 multi-shot merge", () => { + const SUB_DEPTH = 4; + const MAIN_DEPTH = 32; + const HASH_LENGTH = 2; + const ZERO = BigInt(0); + const NUM_SUBTREES = 32; + const NUM_MERGES = 4; + let aq: AccQueue; + before(async () => { + const r = await deploy("AccQueueBinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aq = r.aq; + aqContract = r.aqContract; + }); + + it(`should merge ${NUM_SUBTREES} subtrees in ${NUM_MERGES}`, async () => { + await testMultiShot(aq, aqContract, NUM_SUBTREES, MAIN_DEPTH, NUM_MERGES); + }); + }); + + describe("Quinary AccQueue0 one-shot merge", () => { + const SUB_DEPTH = 2; + const MAIN_DEPTH = 32; + const HASH_LENGTH = 5; + const ZERO = BigInt(0); + const NUM_SUBTREES = 25; + let aq: AccQueue; + before(async () => { + const r = await deploy("AccQueueQuinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aq = r.aq; + aqContract = r.aqContract; + }); + + it(`should merge ${NUM_SUBTREES} subtrees`, async () => { + await testOneShot(aq, aqContract, NUM_SUBTREES, MAIN_DEPTH); + }); + }); + + describe("Quinary AccQueue0 multi-shot merge", () => { + const SUB_DEPTH = 2; + const MAIN_DEPTH = 32; + const HASH_LENGTH = 5; + const ZERO = BigInt(0); + const NUM_SUBTREES = 20; + const NUM_MERGES = 4; + let aq: AccQueue; + + before(async () => { + const r = await deploy("AccQueueQuinary0", SUB_DEPTH, HASH_LENGTH, ZERO); + aq = r.aq; + aqContract = r.aqContract; + }); + + it(`should merge ${NUM_SUBTREES} subtrees in ${NUM_MERGES}`, async () => { + await testMultiShot(aq, aqContract, NUM_SUBTREES, MAIN_DEPTH, NUM_MERGES); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/EASGatekeeper.test.ts b/packages/hardhat/test/maci-tests/EASGatekeeper.test.ts new file mode 100644 index 0000000..2c402be --- /dev/null +++ b/packages/hardhat/test/maci-tests/EASGatekeeper.test.ts @@ -0,0 +1,199 @@ +import { expect } from "chai"; +import dotenv from "dotenv"; +import { AbiCoder, Signer, ZeroAddress, toBeArray } from "ethers"; +import { ethers, network } from "hardhat"; +import { Keypair } from "../../maci-ts/domainobjs"; + +import { deployContract } from "../../maci-ts/ts/deploy"; +import { getDefaultSigner, getSigners, sleep } from "../../maci-ts/ts/utils"; +import { EASGatekeeper, MACI } from "../../typechain-types"; + +import { STATE_TREE_DEPTH, initialVoiceCreditBalance } from "./constants"; +import { deployTestContracts } from "./utils"; + +dotenv.config(); + +describe("EAS Gatekeeper", () => { + let easGatekeeper: EASGatekeeper; + let signer: Signer; + let signerAddress: string; + + const schema = "0xfdcfdad2dbe7489e0ce56b260348b7f14e8365a8a325aef9834818c00d46b31b"; + const easAddress = "0x4200000000000000000000000000000000000021"; + const attestation = "0xe9d4e5a14ec840656d9def34075d9523d1536176d5f0a7d574f2e93bea641b66"; + const revokedAttestation = "0x10207e0381318820574f8c99efde13b9dfe0b24114c9ec5337109d2435a8f13b"; + const invalidAttesterAttestation = "0x5f66cb6eaebece82ec3a47918afee718afa7dca838faab1ee156df3a6187cb9c"; + const attestationOwner = "0x849151d7D0bF1F34b70d5caD5149D28CC2308bf1"; + const trustedAttester = "0x621477dBA416E12df7FF0d48E14c4D20DC85D7D9"; + // random gitcoin passport attestation + const wrongAttestation = "0x8c60cf319b553194519098c7ecaad38fc0e818c538b939730c0b55bb1eeedaae"; + + const user = new Keypair(); + + before(async () => { + // fork the optimism mainnet network + if (network.name === "hardhat") { + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: process.env.OP_RPC_URL || "https://optimism.drpc.org", + }, + }, + ], + }); + } + signer = await getDefaultSigner(); + signerAddress = await signer.getAddress(); + easGatekeeper = await deployContract("EASGatekeeper", signer, true, easAddress, trustedAttester, toBeArray(schema)); + }); + + after(async () => { + // we reset + if (network.name === "hardhat") { + await network.provider.request({ + method: "hardhat_reset", + params: [], + }); + } + }); + + // add some sleep to ensure we don't have problems with the fork + // as one might use a free RPC plan + afterEach(async () => { + await sleep(3000); + }); + + describe("Deployment", () => { + it("The gatekeeper should be deployed correctly", () => { + expect(easGatekeeper).to.not.eq(undefined); + }); + + it("should fail to deploy when the eas contract address is not valid", async () => { + await expect( + deployContract("EASGatekeeper", signer, true, ZeroAddress, trustedAttester, toBeArray(schema)), + ).to.be.revertedWithCustomError(easGatekeeper, "ZeroAddress"); + }); + + it("should fail to deploy when the attester address is not valid", async () => { + await expect( + deployContract("EASGatekeeper", signer, true, easAddress, ZeroAddress, toBeArray(schema)), + ).to.be.revertedWithCustomError(easGatekeeper, "ZeroAddress"); + }); + }); + + describe("EASGatekeeper", () => { + let maciContract: MACI; + + before(async () => { + const r = await deployTestContracts( + initialVoiceCreditBalance, + STATE_TREE_DEPTH, + signer, + true, + true, + easGatekeeper, + ); + + maciContract = r.maciContract; + }); + + it("sets MACI instance correctly", async () => { + const maciAddress = await maciContract.getAddress(); + await easGatekeeper.setMaciInstance(maciAddress).then(tx => tx.wait()); + + expect(await easGatekeeper.maci()).to.eq(maciAddress); + }); + + it("should fail to set MACI instance when the caller is not the owner", async () => { + const [, secondSigner] = await getSigners(); + await expect(easGatekeeper.connect(secondSigner).setMaciInstance(signerAddress)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("should fail to set MACI instance when the MACI instance is not valid", async () => { + await expect(easGatekeeper.setMaciInstance(ZeroAddress)).to.be.revertedWithCustomError( + easGatekeeper, + "ZeroAddress", + ); + }); + + it("should throw when the attestation is not owned by the caller (mocking maci.signUp call)", async () => { + await easGatekeeper.setMaciInstance(signerAddress).then(tx => tx.wait()); + + await expect(easGatekeeper.register(signerAddress, toBeArray(attestation))).to.be.revertedWithCustomError( + easGatekeeper, + "NotYourAttestation", + ); + }); + + it("should throw when the attestation has been revoked", async () => { + await expect(easGatekeeper.register(signerAddress, toBeArray(revokedAttestation))).to.be.revertedWithCustomError( + easGatekeeper, + "AttestationRevoked", + ); + }); + + it("should throw when the attestation schema is not the one expected by the gatekeeper", async () => { + await easGatekeeper.setMaciInstance(signerAddress).then(tx => tx.wait()); + await expect(easGatekeeper.register(signerAddress, toBeArray(wrongAttestation))).to.be.revertedWithCustomError( + easGatekeeper, + "InvalidSchema", + ); + }); + + it("should throw when the attestation is not signed by the attestation owner", async () => { + await easGatekeeper.setMaciInstance(signerAddress).then(tx => tx.wait()); + await expect( + easGatekeeper.register(signerAddress, toBeArray(invalidAttesterAttestation)), + ).to.be.revertedWithCustomError(easGatekeeper, "AttesterNotTrusted"); + }); + + it("should register a user if the register function is called with the valid data", async () => { + // impersonate a user that owns the attestation + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [attestationOwner], + }); + + const userSigner = await ethers.getSigner(attestationOwner); + + await easGatekeeper.setMaciInstance(await maciContract.getAddress()).then(tx => tx.wait()); + + // signup via MACI + const tx = await maciContract + .connect(userSigner) + .signUp( + user.pubKey.asContractParam(), + toBeArray(attestation), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ); + + const receipt = await tx.wait(); + + expect(receipt?.status).to.eq(1); + }); + + it("should prevent signing up twice", async () => { + // impersonate a user that owns the attestation + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [attestationOwner], + }); + + const userSigner = await ethers.getSigner(attestationOwner); + + await expect( + maciContract + .connect(userSigner) + .signUp( + user.pubKey.asContractParam(), + toBeArray(attestation), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ), + ).to.be.revertedWithCustomError(easGatekeeper, "AlreadyRegistered"); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/Hasher.test.ts b/packages/hardhat/test/maci-tests/Hasher.test.ts new file mode 100644 index 0000000..c36fa16 --- /dev/null +++ b/packages/hardhat/test/maci-tests/Hasher.test.ts @@ -0,0 +1,94 @@ +import { expect } from "chai"; +import { BigNumberish } from "ethers"; +import { sha256Hash, hashLeftRight, hash3, hash4, hash5, genRandomSalt } from "../../maci-ts/crypto"; + +import { deployPoseidonContracts, linkPoseidonLibraries } from "../../maci-ts/ts/deploy"; +import { getDefaultSigner } from "../../maci-ts/ts/utils"; +import { Hasher } from "../../typechain-types"; + +describe("Hasher", () => { + let hasherContract: Hasher; + + before(async () => { + const { PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract } = + await deployPoseidonContracts(await getDefaultSigner(), {}, true); + const [poseidonT3ContractAddress, poseidonT4ContractAddress, poseidonT5ContractAddress, poseidonT6ContractAddress] = + await Promise.all([ + PoseidonT3Contract.getAddress(), + PoseidonT4Contract.getAddress(), + PoseidonT5Contract.getAddress(), + PoseidonT6Contract.getAddress(), + ]); + // Link Poseidon contracts + const hasherContractFactory = await linkPoseidonLibraries( + "Hasher", + poseidonT3ContractAddress, + poseidonT4ContractAddress, + poseidonT5ContractAddress, + poseidonT6ContractAddress, + await getDefaultSigner(), + true, + ); + + hasherContract = (await hasherContractFactory.deploy()) as Hasher; + await hasherContract.deploymentTransaction()?.wait(); + }); + + it("maci-crypto.sha256Hash should match hasher.sha256Hash", async () => { + const values: string[] = []; + for (let i = 0; i < 5; i += 1) { + values.push(genRandomSalt().toString()); + const hashed = sha256Hash(values.map(BigInt)); + + // eslint-disable-next-line no-await-in-loop + const onChainHash = await hasherContract.sha256Hash(values); + expect(onChainHash.toString()).to.eq(hashed.toString()); + } + }); + + it("maci-crypto.hashLeftRight should match hasher.hashLeftRight", async () => { + const left = genRandomSalt(); + const right = genRandomSalt(); + const hashed = hashLeftRight(left, right); + + const onChainHash = await hasherContract.hashLeftRight(left.toString(), right.toString()); + expect(onChainHash.toString()).to.eq(hashed.toString()); + }); + + it("maci-crypto.hash3 should match hasher.hash3", async () => { + const values: BigNumberish[] = []; + for (let i = 0; i < 3; i += 1) { + values.push(genRandomSalt().toString()); + } + const hashed = hash3(values.map(BigInt)); + + const onChainHash = await hasherContract.hash3(values as [BigNumberish, BigNumberish, BigNumberish]); + expect(onChainHash.toString()).to.eq(hashed.toString()); + }); + + it("maci-crypto.hash4 should match hasher.hash4", async () => { + const values: BigNumberish[] = []; + + for (let i = 0; i < 4; i += 1) { + values.push(genRandomSalt().toString()); + } + const hashed = hash4(values.map(BigInt)); + + const onChainHash = await hasherContract.hash4(values as [BigNumberish, BigNumberish, BigNumberish, BigNumberish]); + expect(onChainHash.toString()).to.eq(hashed.toString()); + }); + + it("maci-crypto.hash5 should match hasher.hash5", async () => { + const values: BigNumberish[] = []; + + for (let i = 0; i < 5; i += 1) { + values.push(genRandomSalt().toString()); + } + const hashed = hash5(values.map(BigInt)); + + const onChainHash = await hasherContract.hash5( + values as [BigNumberish, BigNumberish, BigNumberish, BigNumberish, BigNumberish], + ); + expect(onChainHash.toString()).to.eq(hashed.toString()); + }); +}); diff --git a/packages/hardhat/test/maci-tests/HasherBenchmarks.test.ts b/packages/hardhat/test/maci-tests/HasherBenchmarks.test.ts new file mode 100644 index 0000000..a6d99cc --- /dev/null +++ b/packages/hardhat/test/maci-tests/HasherBenchmarks.test.ts @@ -0,0 +1,60 @@ +import { expect } from "chai"; +import { BigNumberish } from "ethers"; +import { genRandomSalt } from "../../maci-ts/crypto"; + +import { deployPoseidonContracts, linkPoseidonLibraries } from "../../maci-ts/ts/deploy"; +import { getDefaultSigner } from "../../maci-ts/ts/utils"; +import { HasherBenchmarks } from "../../typechain-types"; + +describe("Hasher", () => { + let hasherContract: HasherBenchmarks; + before(async () => { + const { PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract } = + await deployPoseidonContracts(await getDefaultSigner(), {}, true); + const [poseidonT3ContractAddress, poseidonT4ContractAddress, poseidonT5ContractAddress, poseidonT6ContractAddress] = + await Promise.all([ + PoseidonT3Contract.getAddress(), + PoseidonT4Contract.getAddress(), + PoseidonT5Contract.getAddress(), + PoseidonT6Contract.getAddress(), + ]); + // Link Poseidon contracts + const hasherContractFactory = await linkPoseidonLibraries( + "HasherBenchmarks", + poseidonT3ContractAddress, + poseidonT4ContractAddress, + poseidonT5ContractAddress, + poseidonT6ContractAddress, + await getDefaultSigner(), + true, + ); + + hasherContract = (await hasherContractFactory.deploy()) as HasherBenchmarks; + await hasherContract.deploymentTransaction()?.wait(); + }); + + it("hashLeftRight", async () => { + const left = genRandomSalt(); + const right = genRandomSalt(); + + const result = await hasherContract + .hashLeftRightBenchmark(left.toString(), right.toString()) + .then(res => Number(res)); + + expect(result).to.not.eq(null); + }); + + it("hash5", async () => { + const values = []; + + for (let i = 0; i < 5; i += 1) { + values.push(genRandomSalt().toString()); + } + + const result = await hasherContract + .hash5Benchmark(values as [BigNumberish, BigNumberish, BigNumberish, BigNumberish, BigNumberish]) + .then(res => Number(res)); + + expect(result).to.not.eq(null); + }); +}); diff --git a/packages/hardhat/test/maci-tests/HatsGatekeeper.test.ts b/packages/hardhat/test/maci-tests/HatsGatekeeper.test.ts new file mode 100644 index 0000000..1ad7910 --- /dev/null +++ b/packages/hardhat/test/maci-tests/HatsGatekeeper.test.ts @@ -0,0 +1,337 @@ +import { expect } from "chai"; +import dotenv from "dotenv"; +import { AbiCoder, Signer, ZeroAddress } from "ethers"; +import { network } from "hardhat"; +import { Keypair } from "../../maci-ts/domainobjs"; + +import { deployContract } from "../../maci-ts/ts/deploy"; +import { getSigners, sleep } from "../../maci-ts/ts/utils"; +import { HatsGatekeeperMultiple, HatsGatekeeperSingle, MACI, MockHatsProtocol } from "../../typechain-types"; + +import { STATE_TREE_DEPTH, initialVoiceCreditBalance } from "./constants"; +import { deployTestContracts } from "./utils"; + +dotenv.config(); + +describe("HatsProtocol Gatekeeper", () => { + let maciContract: MACI; + + let hatsGatekeeperSingle: HatsGatekeeperSingle; + let hatsGatekeeperMultiple: HatsGatekeeperMultiple; + + let signer: Signer; + let voter: Signer; + let signerAddress: string; + let voterAddress: string; + + let mockHats: MockHatsProtocol; + let mockHatsAddress: string; + const hatsContractOP = "0x3bc1A0Ad72417f2d411118085256fC53CBdDd137"; + + const user = new Keypair(); + const oneAddress = "0x0000000000000000000000000000000000000001"; + + let topHat: bigint; + let hatId: bigint; + let secondHatId: bigint; + let thirdHatId: bigint; + + before(async () => { + // fork the optimism mainnet network + if (network.name === "hardhat") { + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: process.env.OP_RPC_URL || "https://optimism.drpc.org", + }, + }, + ], + }); + } + + [signer, voter] = await getSigners(); + signerAddress = await signer.getAddress(); + voterAddress = await voter.getAddress(); + + // deploy the wrapper around HatsProtocol + mockHats = await deployContract("MockHatsProtocol", signer, true, hatsContractOP); + mockHatsAddress = await mockHats.getAddress(); + + // create a new topHat + await mockHats + .connect(signer) + .mintTopHat(mockHatsAddress, "MACITOPHAT", "") + .then(tx => tx.wait()); + topHat = await mockHats.lastTopHat(); + + // create a new hat + await mockHats.createHat(topHat, "MACI HAT", 2, signerAddress, signerAddress, false, "").then(tx => tx.wait()); + hatId = await mockHats.lastHat(); + + // mint the hat + await mockHats.mintHat(hatId, signerAddress).then(tx => tx.wait()); + + // create a second hat + await mockHats.createHat(topHat, "MACI HAT 2", 2, signerAddress, signerAddress, true, "").then(tx => tx.wait()); + secondHatId = await mockHats.lastHat(); + + // mint the hat + await mockHats.mintHat(secondHatId, voterAddress).then(tx => tx.wait()); + + // create a third hat + await mockHats.createHat(topHat, "MACI HAT 3", 2, signerAddress, signerAddress, true, "").then(tx => tx.wait()); + thirdHatId = await mockHats.lastHat(); + + // mint the hat + await mockHats.mintHat(thirdHatId, signerAddress).then(tx => tx.wait()); + await mockHats.mintHat(thirdHatId, voterAddress).then(tx => tx.wait()); + + // deploy gatekeepers + hatsGatekeeperSingle = await deployContract("HatsGatekeeperSingle", signer, true, hatsContractOP, hatId); + hatsGatekeeperMultiple = await deployContract("HatsGatekeeperMultiple", signer, true, hatsContractOP, [ + hatId, + secondHatId, + ]); + }); + + after(async () => { + // we reset + if (network.name === "hardhat") { + await network.provider.request({ + method: "hardhat_reset", + params: [], + }); + } + }); + + // add some sleep to ensure we don't have problems with the fork + // as one might use a free RPC plan + afterEach(async () => { + await sleep(3000); + }); + + describe("hatsGatekeeperSingle", () => { + before(async () => { + const r = await deployTestContracts( + initialVoiceCreditBalance, + STATE_TREE_DEPTH, + signer, + true, + true, + hatsGatekeeperSingle, + ); + + maciContract = r.maciContract; + }); + + describe("Deployment", () => { + it("should be deployed correctly", async () => { + expect(hatsGatekeeperSingle).to.not.eq(undefined); + expect(await hatsGatekeeperSingle.criterionHat()).to.eq(hatId); + expect(await hatsGatekeeperSingle.maci()).to.eq(ZeroAddress); + expect(await hatsGatekeeperSingle.hats()).to.eq(hatsContractOP); + }); + }); + + describe("setMaci", () => { + it("should set the MACI instance correctly", async () => { + const maciAddress = await maciContract.getAddress(); + await hatsGatekeeperSingle.setMaciInstance(maciAddress).then(tx => tx.wait()); + + expect(await hatsGatekeeperSingle.maci()).to.eq(maciAddress); + }); + + it("should fail to set MACI instance to the zero address", async () => { + await expect(hatsGatekeeperSingle.setMaciInstance(ZeroAddress)).to.be.revertedWithCustomError( + hatsGatekeeperSingle, + "ZeroAddress", + ); + }); + + it("should fail to set MACI instance when the caller is not the owner", async () => { + await expect(hatsGatekeeperSingle.connect(voter).setMaciInstance(signerAddress)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + }); + + describe("register", () => { + it("should not allow to call from a non-registered MACI contract", async () => { + await hatsGatekeeperSingle.setMaciInstance(oneAddress).then(tx => tx.wait()); + await expect( + maciContract + .connect(signer) + .signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ), + ).to.be.revertedWithCustomError(hatsGatekeeperSingle, "OnlyMACI"); + }); + + it("should register a user if the register function is called with the valid data", async () => { + await hatsGatekeeperSingle.setMaciInstance(await maciContract.getAddress()).then(tx => tx.wait()); + + // signup via MACI + const tx = await maciContract + .connect(signer) + .signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ); + + const receipt = await tx.wait(); + + expect(receipt?.status).to.eq(1); + }); + + it("should fail to register a user if they do not own the criterion hat", async () => { + await expect( + maciContract + .connect(voter) + .signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ), + ).to.be.revertedWithCustomError(hatsGatekeeperSingle, "NotWearingCriterionHat"); + }); + + it("should prevent signing up twice", async () => { + await expect( + maciContract + .connect(signer) + .signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ), + ).to.be.revertedWithCustomError(hatsGatekeeperSingle, "AlreadyRegistered"); + }); + }); + }); + + describe("HatsGatekeeperMultiple", () => { + before(async () => { + const r = await deployTestContracts( + initialVoiceCreditBalance, + STATE_TREE_DEPTH, + signer, + true, + true, + hatsGatekeeperMultiple, + ); + + maciContract = r.maciContract; + }); + + describe("Deployment", () => { + it("should be deployed correctly", async () => { + expect(hatsGatekeeperMultiple).to.not.eq(undefined); + expect(await hatsGatekeeperMultiple.maci()).to.eq(ZeroAddress); + expect(await hatsGatekeeperMultiple.hats()).to.eq(hatsContractOP); + expect(await hatsGatekeeperMultiple.criterionHat(hatId)).to.eq(true); + expect(await hatsGatekeeperMultiple.criterionHat(secondHatId)).to.eq(true); + }); + }); + + describe("setMaci", () => { + it("should set the MACI instance correctly", async () => { + const maciAddress = await maciContract.getAddress(); + await hatsGatekeeperMultiple.setMaciInstance(maciAddress).then(tx => tx.wait()); + + expect(await hatsGatekeeperMultiple.maci()).to.eq(maciAddress); + }); + + it("should fail to set MACI instance to the zero address", async () => { + await expect(hatsGatekeeperMultiple.setMaciInstance(ZeroAddress)).to.be.revertedWithCustomError( + hatsGatekeeperMultiple, + "ZeroAddress", + ); + }); + + it("should fail to set MACI instance when the caller is not the owner", async () => { + await expect(hatsGatekeeperMultiple.connect(voter).setMaciInstance(signerAddress)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + }); + + describe("register", () => { + it("should not allow to call from a non-registered MACI contract", async () => { + await hatsGatekeeperMultiple + .connect(signer) + .setMaciInstance(oneAddress) + .then(tx => tx.wait()); + await expect( + maciContract + .connect(signer) + .signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ), + ).to.be.revertedWithCustomError(hatsGatekeeperMultiple, "OnlyMACI"); + }); + + it("should register a user if the register function is called with the valid data", async () => { + await hatsGatekeeperMultiple.connect(signer).setMaciInstance(await maciContract.getAddress()); + + // signup via MACI + const tx = await maciContract + .connect(signer) + .signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [hatId]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ); + + const receipt = await tx.wait(); + + expect(receipt?.status).to.eq(1); + }); + + it("should fail to register a user if they pass a non-criterion hat", async () => { + await expect( + maciContract + .connect(voter) + .signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [thirdHatId]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ), + ).to.be.revertedWithCustomError(hatsGatekeeperMultiple, "NotCriterionHat"); + }); + + it("should fail to register a user if they do not own a criterion hat", async () => { + // get another signer + const [, , another] = await getSigners(); + + await expect( + maciContract + .connect(another) + .signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [hatId]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ), + ).to.be.revertedWithCustomError(hatsGatekeeperMultiple, "NotWearingCriterionHat"); + }); + + it("should prevent signing up twice", async () => { + await expect( + maciContract + .connect(signer) + .signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [hatId]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ), + ).to.be.revertedWithCustomError(hatsGatekeeperMultiple, "AlreadyRegistered"); + }); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/MACI.test.ts b/packages/hardhat/test/maci-tests/MACI.test.ts new file mode 100644 index 0000000..32d74a9 --- /dev/null +++ b/packages/hardhat/test/maci-tests/MACI.test.ts @@ -0,0 +1,330 @@ +/* eslint-disable no-underscore-dangle */ +import { expect } from "chai"; +import { AbiCoder, BaseContract, BigNumberish, Contract, Signer } from "ethers"; +import { EthereumProvider } from "hardhat/types"; +import { MaciState } from "../../maci-ts/core"; +import { NOTHING_UP_MY_SLEEVE } from "../../maci-ts/crypto"; +import { Keypair, PubKey, Message } from "../../maci-ts/domainobjs"; + +import { parseArtifact } from "../../maci-ts/ts/abi"; +import { getDefaultSigner, getSigners } from "../../maci-ts/ts/utils"; +import { AccQueueQuinaryMaci, MACI, Poll as PollContract, Verifier, VkRegistry } from "../../typechain-types"; + +import { + STATE_TREE_DEPTH, + duration, + initialVoiceCreditBalance, + maxValues, + messageBatchSize, + treeDepths, +} from "./constants"; +import { timeTravel, deployTestContracts } from "./utils"; + +describe("MACI", () => { + let maciContract: MACI; + let stateAqContract: AccQueueQuinaryMaci; + let vkRegistryContract: VkRegistry; + let verifierContract: Verifier; + let pollId: bigint; + + const coordinator = new Keypair(); + const [pollAbi] = parseArtifact("Poll"); + const users = [new Keypair(), new Keypair(), new Keypair()]; + + let signer: Signer; + + const maciState = new MaciState(STATE_TREE_DEPTH); + const signUpTxOpts = { gasLimit: 400000 }; + + describe("Deployment", () => { + before(async () => { + signer = await getDefaultSigner(); + const r = await deployTestContracts(initialVoiceCreditBalance, STATE_TREE_DEPTH, signer, true); + + maciContract = r.maciContract; + stateAqContract = r.stateAqContract; + vkRegistryContract = r.vkRegistryContract; + verifierContract = r.mockVerifierContract as Verifier; + }); + + it("should have set the correct stateTreeDepth", async () => { + const std = await maciContract.stateTreeDepth(); + expect(std.toString()).to.eq(STATE_TREE_DEPTH.toString()); + }); + + it("should be the owner of the stateAqContract", async () => { + const stateAqAddr = await maciContract.stateAq(); + const stateAq = new Contract(stateAqAddr, parseArtifact("AccQueueQuinaryBlankSl")[0], signer); + + expect(await stateAq.owner()).to.eq(await maciContract.getAddress()); + }); + + it("should be possible to deploy Maci contracts with different state tree depth values", async () => { + const checkStateTreeDepth = async (stateTreeDepthTest: number): Promise => { + const { maciContract: testMaci } = await deployTestContracts( + initialVoiceCreditBalance, + stateTreeDepthTest, + signer, + true, + ); + expect(await testMaci.stateTreeDepth()).to.eq(stateTreeDepthTest); + }; + + await checkStateTreeDepth(1); + await checkStateTreeDepth(2); + await checkStateTreeDepth(3); + }); + }); + + describe("Signups", () => { + it("should sign up multiple users", async () => { + const iface = maciContract.interface; + + for (let index = 0; index < users.length; index += 1) { + const user = users[index]; + + // eslint-disable-next-line no-await-in-loop + const tx = await maciContract.signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + signUpTxOpts, + ); + // eslint-disable-next-line no-await-in-loop + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + + // Store the state index + const log = receipt!.logs[receipt!.logs.length - 1]; + const event = iface.parseLog(log as unknown as { topics: string[]; data: string }) as unknown as { + args: { + _stateIndex: BigNumberish; + _voiceCreditBalance: BigNumberish; + _timestamp: BigNumberish; + }; + }; + + expect(event.args._stateIndex.toString()).to.eq((index + 1).toString()); + + maciState.signUp( + user.pubKey, + BigInt(event.args._voiceCreditBalance.toString()), + BigInt(event.args._timestamp.toString()), + ); + } + }); + + it("should fail when given an invalid pubkey", async () => { + await expect( + maciContract.signUp( + { + x: "21888242871839275222246405745257275088548364400416034343698204186575808495617", + y: "0", + }, + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + signUpTxOpts, + ), + ).to.be.revertedWithCustomError(maciContract, "MaciPubKeyLargerThanSnarkFieldSize"); + }); + + it("should not allow to sign up more than the supported amount of users (5 ** stateTreeDepth)", async () => { + const stateTreeDepthTest = 1; + const maxUsers = 5 ** stateTreeDepthTest; + const maci = (await deployTestContracts(initialVoiceCreditBalance, stateTreeDepthTest, signer, true)) + .maciContract; + const keypair = new Keypair(); + // start from one as we already have one signup (blank state leaf) + for (let i = 1; i < maxUsers; i += 1) { + // eslint-disable-next-line no-await-in-loop + await maci.signUp( + keypair.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + ); + } + + // the next signup should fail + await expect( + maci.signUp( + keypair.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + ), + ).to.be.revertedWithCustomError(maci, "TooManySignups"); + }); + }); + + describe("Deploy a Poll", () => { + let deployTime: number | undefined; + + it("should deploy a poll", async () => { + // Create the poll and get the poll ID from the tx event logs + const tx = await maciContract.deployPoll( + duration, + treeDepths, + coordinator.pubKey.asContractParam() as { x: BigNumberish; y: BigNumberish }, + verifierContract, + vkRegistryContract, + false, + { gasLimit: 10000000 }, + ); + const receipt = await tx.wait(); + + const block = await signer.provider!.getBlock(receipt!.blockHash); + deployTime = block!.timestamp; + + expect(receipt?.status).to.eq(1); + const iface = maciContract.interface; + const logs = receipt!.logs[receipt!.logs.length - 1]; + const event = iface.parseLog(logs as unknown as { topics: string[]; data: string }) as unknown as { + args: { _pollId: bigint }; + }; + pollId = event.args._pollId; + + const p = maciState.deployPoll( + BigInt(deployTime + duration), + maxValues, + treeDepths, + messageBatchSize, + coordinator, + ); + expect(p.toString()).to.eq(pollId.toString()); + + // publish the NOTHING_UP_MY_SLEEVE message + const messageData = [NOTHING_UP_MY_SLEEVE]; + for (let i = 1; i < 10; i += 1) { + messageData.push(BigInt(0)); + } + const message = new Message(BigInt(1), messageData); + const padKey = new PubKey([ + BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"), + BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"), + ]); + maciState.polls.get(pollId)?.publishMessage(message, padKey); + }); + + it("should prevent deploying a second poll before the first has finished", async () => { + await expect( + maciContract.deployPoll( + duration, + treeDepths, + coordinator.pubKey.asContractParam(), + verifierContract, + vkRegistryContract, + false, + { + gasLimit: 10000000, + }, + ), + ) + .to.be.revertedWithCustomError(maciContract, "PreviousPollNotCompleted") + .withArgs(1); + }); + }); + + describe("Merge sign-ups", () => { + let pollContract: PollContract; + + before(async () => { + const pollContractAddress = await maciContract.getPoll(pollId); + pollContract = new BaseContract(pollContractAddress, pollAbi, signer) as PollContract; + }); + + it("should not allow the coordinator to merge the signUp AccQueue", async () => { + await expect(maciContract.mergeStateAqSubRoots(0, 0, { gasLimit: 3000000 })).to.be.revertedWithCustomError( + maciContract, + "CallerMustBePoll", + ); + + await expect(maciContract.mergeStateAq(0, { gasLimit: 3000000 })).to.be.revertedWithCustomError( + maciContract, + "CallerMustBePoll", + ); + }); + + it("should not allow a user to merge the signUp AccQueue", async () => { + const nonAdminUser = (await getSigners())[1]; + await expect( + maciContract.connect(nonAdminUser).mergeStateAqSubRoots(0, 0, { gasLimit: 3000000 }), + ).to.be.revertedWithCustomError(maciContract, "CallerMustBePoll"); + }); + + it("should prevent a new user from signin up after the accQueue subtrees have been merged", async () => { + await timeTravel(signer.provider as unknown as EthereumProvider, Number(duration) + 1); + + const tx = await pollContract.mergeMaciStateAqSubRoots(0, pollId, { + gasLimit: 3000000, + }); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + + await expect( + maciContract.signUp( + users[0].pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + signUpTxOpts, + ), + ).to.be.revertedWithCustomError(maciContract, "SignupTemporaryBlocked"); + }); + + it("should allow a Poll contract to merge the signUp AccQueue", async () => { + const tx = await pollContract.mergeMaciStateAq(pollId, { + gasLimit: 3000000, + }); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + }); + + it("should have the correct state root on chain after merging the acc queue", async () => { + const onChainStateRoot = await stateAqContract.getMainRoot(STATE_TREE_DEPTH); + maciState.polls.get(pollId)?.updatePoll(await pollContract.numSignups()); + expect(onChainStateRoot.toString()).to.eq(maciState.polls.get(pollId)?.stateTree?.root.toString()); + }); + + it("should get the correct state root with getStateAqRoot", async () => { + const onChainStateRoot = await maciContract.getStateAqRoot(); + expect(onChainStateRoot.toString()).to.eq(maciState.polls.get(pollId)?.stateTree?.root.toString()); + }); + + it("should allow a user to signup after the signUp AccQueue was merged", async () => { + const tx = await maciContract.signUp( + users[0].pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + signUpTxOpts, + ); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + + const iface = maciContract.interface; + + // Store the state index + const log = receipt!.logs[receipt!.logs.length - 1]; + const event = iface.parseLog(log as unknown as { topics: string[]; data: string }) as unknown as { + args: { + _stateIndex: BigNumberish; + _voiceCreditBalance: BigNumberish; + _timestamp: BigNumberish; + }; + }; + + maciState.signUp( + users[0].pubKey, + BigInt(event.args._voiceCreditBalance.toString()), + BigInt(event.args._timestamp.toString()), + ); + }); + }); + + describe("getPoll", () => { + it("should return an address for a valid id", async () => { + expect(await maciContract.getPoll(pollId)).to.not.eq(0); + }); + + it("should throw when given an invalid poll id", async () => { + await expect(maciContract.getPoll(5)).to.be.revertedWithCustomError(maciContract, "PollDoesNotExist").withArgs(5); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/MessageProcessor.test.ts b/packages/hardhat/test/maci-tests/MessageProcessor.test.ts new file mode 100644 index 0000000..a6ac56e --- /dev/null +++ b/packages/hardhat/test/maci-tests/MessageProcessor.test.ts @@ -0,0 +1,190 @@ +/* eslint-disable no-underscore-dangle */ +import { expect } from "chai"; +import { BaseContract, Signer } from "ethers"; +import { EthereumProvider } from "hardhat/types"; +import { MaciState, Poll, packProcessMessageSmallVals, IProcessMessagesCircuitInputs } from "../../maci-ts/core"; +import { NOTHING_UP_MY_SLEEVE } from "../../maci-ts/crypto"; +import { Keypair, Message, PubKey } from "../../maci-ts/domainobjs"; + +import { parseArtifact } from "../../maci-ts/ts/abi"; +import { IVerifyingKeyStruct } from "../../maci-ts/ts/types"; +import { getDefaultSigner } from "../../maci-ts/ts/utils"; +import { MACI, MessageProcessor, Poll as PollContract, Verifier, VkRegistry } from "../../typechain-types"; + +import { + STATE_TREE_DEPTH, + duration, + initialVoiceCreditBalance, + maxValues, + messageBatchSize, + testProcessVk, + testTallyVk, + treeDepths, +} from "./constants"; +import { timeTravel, deployTestContracts } from "./utils"; + +describe("MessageProcessor", () => { + // contracts + let maciContract: MACI; + let pollContract: PollContract; + let verifierContract: Verifier; + let vkRegistryContract: VkRegistry; + let mpContract: MessageProcessor; + + const [pollAbi] = parseArtifact("Poll"); + const [mpAbi] = parseArtifact("MessageProcessor"); + + let pollId: bigint; + + // local poll and maci state + let poll: Poll; + const maciState = new MaciState(STATE_TREE_DEPTH); + + let signer: Signer; + let generatedInputs: IProcessMessagesCircuitInputs; + const coordinator = new Keypair(); + + const users = [new Keypair(), new Keypair()]; + + before(async () => { + signer = await getDefaultSigner(); + // deploy test contracts + const r = await deployTestContracts(initialVoiceCreditBalance, STATE_TREE_DEPTH, signer, true); + maciContract = r.maciContract; + signer = await getDefaultSigner(); + verifierContract = r.mockVerifierContract as Verifier; + vkRegistryContract = r.vkRegistryContract; + + // deploy on chain poll + const tx = await maciContract.deployPoll( + duration, + treeDepths, + coordinator.pubKey.asContractParam(), + verifierContract, + vkRegistryContract, + false, + { + gasLimit: 10000000, + }, + ); + let receipt = await tx.wait(); + + // extract poll id + expect(receipt?.status).to.eq(1); + const iface = maciContract.interface; + const logs = receipt!.logs[receipt!.logs.length - 1]; + const event = iface.parseLog(logs as unknown as { topics: string[]; data: string }) as unknown as { + args: { + _pollId: bigint; + pollAddr: { + poll: string; + messageProcessor: string; + tally: string; + }; + }; + }; + pollId = event.args._pollId; + + const pollContractAddress = await maciContract.getPoll(pollId); + pollContract = new BaseContract(pollContractAddress, pollAbi, signer) as PollContract; + + mpContract = new BaseContract(event.args.pollAddr.messageProcessor, mpAbi, signer) as MessageProcessor; + + const block = await signer.provider!.getBlock(receipt!.blockHash); + const deployTime = block!.timestamp; + + // deploy local poll + const p = maciState.deployPoll(BigInt(deployTime + duration), maxValues, treeDepths, messageBatchSize, coordinator); + expect(p.toString()).to.eq(pollId.toString()); + + // publish the NOTHING_UP_MY_SLEEVE message + const messageData = [NOTHING_UP_MY_SLEEVE]; + for (let i = 1; i < 10; i += 1) { + messageData.push(BigInt(0)); + } + const message = new Message(BigInt(1), messageData); + const padKey = new PubKey([ + BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"), + BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"), + ]); + + poll = maciState.polls.get(pollId)!; + + poll.publishMessage(message, padKey); + + // update the poll state + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + generatedInputs = poll.processMessages(pollId); + + // set the verification keys on the vk smart contract + vkRegistryContract.setVerifyingKeys( + STATE_TREE_DEPTH, + treeDepths.intStateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + testTallyVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + }); + + describe("before merging acc queues", () => { + before(async () => { + await timeTravel(signer.provider! as unknown as EthereumProvider, duration + 1); + }); + + it("processMessages() should fail if the state AQ has not been merged", async () => { + await expect(mpContract.processMessages(0, [0, 0, 0, 0, 0, 0, 0, 0])).to.be.revertedWithCustomError( + mpContract, + "StateAqNotMerged", + ); + }); + }); + + describe("after merging acc queues", () => { + before(async () => { + await pollContract.mergeMaciStateAqSubRoots(0, pollId); + await pollContract.mergeMaciStateAq(0); + + await pollContract.mergeMessageAqSubRoots(0); + await pollContract.mergeMessageAq(); + }); + + it("genProcessMessagesPackedVals() should generate the correct value", async () => { + const packedVals = packProcessMessageSmallVals( + BigInt(maxValues.maxVoteOptions), + BigInt(users.length), + 0, + poll.messages.length, + ); + const onChainPackedVals = BigInt( + await mpContract.genProcessMessagesPackedVals( + 0, + users.length, + poll.messages.length, + treeDepths.messageTreeSubDepth, + treeDepths.voteOptionTreeDepth, + ), + ); + expect(packedVals.toString()).to.eq(onChainPackedVals.toString()); + }); + + it("processMessages() should update the state and ballot root commitment", async () => { + // Submit the proof + const tx = await mpContract.processMessages(generatedInputs.newSbCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + + const processingComplete = await mpContract.processingComplete(); + expect(processingComplete).to.eq(true); + + const onChainNewSbCommitment = await mpContract.sbCommitment(); + expect(generatedInputs.newSbCommitment).to.eq(onChainNewSbCommitment.toString()); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/Poll.test.ts b/packages/hardhat/test/maci-tests/Poll.test.ts new file mode 100644 index 0000000..4dde3e9 --- /dev/null +++ b/packages/hardhat/test/maci-tests/Poll.test.ts @@ -0,0 +1,304 @@ +/* eslint-disable no-underscore-dangle */ +import { expect } from "chai"; +import { BaseContract, Signer } from "ethers"; +import { EthereumProvider } from "hardhat/types"; +import { MaciState } from "../../maci-ts/core"; +import { NOTHING_UP_MY_SLEEVE } from "../../maci-ts/crypto"; +import { Keypair, Message, PCommand, PubKey } from "../../maci-ts/domainobjs"; + +import { parseArtifact } from "../../maci-ts/ts/abi"; +import { getDefaultSigner, getSigners } from "../../maci-ts/ts/utils"; +import { AccQueue, MACI, Poll as PollContract, TopupCredit, Verifier, VkRegistry } from "../../typechain-types"; + +import { + MESSAGE_TREE_DEPTH, + STATE_TREE_DEPTH, + duration, + initialVoiceCreditBalance, + maxValues, + messageBatchSize, + treeDepths, +} from "./constants"; +import { timeTravel, deployTestContracts } from "./utils"; + +describe("Poll", () => { + let maciContract: MACI; + let pollId: bigint; + let pollContract: PollContract; + let verifierContract: Verifier; + let vkRegistryContract: VkRegistry; + let topupCreditContract: TopupCredit; + let signer: Signer; + let deployTime: number; + const coordinator = new Keypair(); + const [pollAbi] = parseArtifact("Poll"); + const [accQueueQuinaryMaciAbi] = parseArtifact("AccQueueQuinaryMaci"); + + const maciState = new MaciState(STATE_TREE_DEPTH); + + const keypair = new Keypair(); + + describe("deployment", () => { + before(async () => { + signer = await getDefaultSigner(); + const r = await deployTestContracts(initialVoiceCreditBalance, STATE_TREE_DEPTH, signer, true); + maciContract = r.maciContract; + verifierContract = r.mockVerifierContract as Verifier; + vkRegistryContract = r.vkRegistryContract; + topupCreditContract = r.topupCreditContract; + + // deploy on chain poll + const tx = await maciContract.deployPoll( + duration, + treeDepths, + coordinator.pubKey.asContractParam(), + verifierContract, + vkRegistryContract, + false, + { + gasLimit: 10000000, + }, + ); + const receipt = await tx.wait(); + + const block = await signer.provider!.getBlock(receipt!.blockHash); + deployTime = block!.timestamp; + + expect(receipt?.status).to.eq(1); + const iface = maciContract.interface; + const logs = receipt!.logs[receipt!.logs.length - 1]; + const event = iface.parseLog(logs as unknown as { topics: string[]; data: string }) as unknown as { + args: { _pollId: bigint }; + }; + pollId = event.args._pollId; + + const pollContractAddress = await maciContract.getPoll(pollId); + pollContract = new BaseContract(pollContractAddress, pollAbi, signer) as PollContract; + + // deploy local poll + const p = maciState.deployPoll( + BigInt(deployTime + duration), + maxValues, + treeDepths, + messageBatchSize, + coordinator, + ); + expect(p.toString()).to.eq(pollId.toString()); + // publish the NOTHING_UP_MY_SLEEVE message + const messageData = [NOTHING_UP_MY_SLEEVE]; + for (let i = 1; i < 10; i += 1) { + messageData.push(BigInt(0)); + } + const message = new Message(BigInt(1), messageData); + const padKey = new PubKey([ + BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"), + BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"), + ]); + maciState.polls.get(pollId)?.publishMessage(message, padKey); + }); + + it("should not be possible to init the Poll contract twice", async () => { + await expect(pollContract.init()).to.be.revertedWithCustomError(pollContract, "PollAlreadyInit"); + }); + + it("should have the correct coordinator public key set", async () => { + const coordinatorPubKey = await pollContract.coordinatorPubKey(); + expect(coordinatorPubKey[0].toString()).to.eq(coordinator.pubKey.rawPubKey[0].toString()); + expect(coordinatorPubKey[1].toString()).to.eq(coordinator.pubKey.rawPubKey[1].toString()); + + const coordinatorPubKeyHash = await pollContract.coordinatorPubKeyHash(); + expect(coordinatorPubKeyHash.toString()).to.eq(coordinator.pubKey.hash().toString()); + }); + + it("should have the correct duration set", async () => { + const dd = await pollContract.getDeployTimeAndDuration(); + expect(dd[1].toString()).to.eq(duration.toString()); + }); + + it("should have the correct max values set", async () => { + const mv = await pollContract.maxValues(); + expect(mv[0].toString()).to.eq(maxValues.maxMessages.toString()); + expect(mv[1].toString()).to.eq(maxValues.maxVoteOptions.toString()); + }); + + it("should have the correct tree depths set", async () => { + const td = await pollContract.treeDepths(); + expect(td[0].toString()).to.eq(treeDepths.intStateTreeDepth.toString()); + expect(td[1].toString()).to.eq(treeDepths.messageTreeSubDepth.toString()); + expect(td[2].toString()).to.eq(treeDepths.messageTreeDepth.toString()); + expect(td[3].toString()).to.eq(treeDepths.voteOptionTreeDepth.toString()); + }); + + it("should have numMessages set to 1 (blank message)", async () => { + const numMessages = await pollContract.numMessages(); + expect(numMessages.toString()).to.eq("1"); + }); + }); + + describe("topup", () => { + let voter: Signer; + + before(async () => { + // transfer tokens to a user and pre-approve + [, voter] = await getSigners(); + await topupCreditContract.airdropTo(voter.getAddress(), 200n); + await topupCreditContract.connect(voter).approve(await pollContract.getAddress(), 1000n); + }); + + it("should allow to publish a topup message when the caller has enough topup tokens", async () => { + const tx = await pollContract.connect(voter).topup(1n, 50n); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + + // publish on local maci state + maciState.polls.get(pollId)?.topupMessage(new Message(2n, [1n, 50n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n])); + }); + + it("should throw when the user does not have enough tokens", async () => { + await expect(pollContract.connect(signer).topup(1n, 50n)).to.be.revertedWith("ERC20: insufficient allowance"); + }); + + it("should emit an event when publishing a topup message", async () => { + expect(await pollContract.connect(voter).topup(1n, 50n)).to.emit(pollContract, "TopupMessage"); + // publish on local maci state + maciState.polls.get(pollId)?.topupMessage(new Message(2n, [1n, 50n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n])); + }); + + it("should successfully increase the number of messages", async () => { + const numMessages = await pollContract.numMessages(); + await pollContract.connect(voter).topup(1n, 50n); + const newNumMessages = await pollContract.numMessages(); + expect(newNumMessages.toString()).to.eq((numMessages + 1n).toString()); + + // publish on local maci state + maciState.polls.get(pollId)?.topupMessage(new Message(2n, [1n, 50n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n])); + }); + }); + + describe("publishMessage", () => { + it("should publish a message to the Poll contract", async () => { + const command = new PCommand(1n, keypair.pubKey, 0n, 9n, 1n, pollId, 0n); + + const signature = command.sign(keypair.privKey); + const sharedKey = Keypair.genEcdhSharedKey(keypair.privKey, coordinator.pubKey); + const message = command.encrypt(signature, sharedKey); + const tx = await pollContract.publishMessage(message.asContractParam(), keypair.pubKey.asContractParam()); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + + maciState.polls.get(pollId)?.publishMessage(message, keypair.pubKey); + }); + + it("should emit an event when publishing a message", async () => { + const command = new PCommand(1n, keypair.pubKey, 0n, 9n, 1n, pollId, 0n); + + const signature = command.sign(keypair.privKey); + const sharedKey = Keypair.genEcdhSharedKey(keypair.privKey, coordinator.pubKey); + const message = command.encrypt(signature, sharedKey); + expect(await pollContract.publishMessage(message.asContractParam(), keypair.pubKey.asContractParam())) + .to.emit(pollContract, "PublishMessage") + .withArgs(message.asContractParam(), keypair.pubKey.asContractParam()); + + maciState.polls.get(pollId)?.publishMessage(message, keypair.pubKey); + }); + + it("should allow to publish a message batch", async () => { + const messages: [Message, PubKey][] = []; + for (let i = 0; i < 2; i += 1) { + const command = new PCommand(1n, keypair.pubKey, 0n, 9n, 1n, pollId, BigInt(i)); + const signature = command.sign(keypair.privKey); + const sharedKey = Keypair.genEcdhSharedKey(keypair.privKey, coordinator.pubKey); + const message = command.encrypt(signature, sharedKey); + messages.push([message, keypair.pubKey]); + } + + const tx = await pollContract.publishMessageBatch( + messages.map(([m]) => m.asContractParam()), + messages.map(([, k]) => k.asContractParam()), + ); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + + messages.forEach(([message, key]) => { + maciState.polls.get(pollId)?.publishMessage(message, key); + }); + }); + + it("should throw when the message batch has messages length != encPubKeys length", async () => { + const command = new PCommand(1n, keypair.pubKey, 0n, 9n, 1n, pollId, 0n); + const signature = command.sign(keypair.privKey); + const sharedKey = Keypair.genEcdhSharedKey(keypair.privKey, coordinator.pubKey); + const message = command.encrypt(signature, sharedKey); + await expect( + pollContract.publishMessageBatch( + [message.asContractParam(), message.asContractParam()], + [keypair.pubKey.asContractParam()], + ), + ).to.be.revertedWithCustomError(pollContract, "InvalidBatchLength"); + }); + + it("should not allow to publish a message after the voting period ends", async () => { + const dd = await pollContract.getDeployTimeAndDuration(); + await timeTravel(signer.provider as unknown as EthereumProvider, Number(dd[0]) + 1); + + const command = new PCommand(1n, keypair.pubKey, 0n, 9n, 1n, pollId, 0n); + + const signature = command.sign(keypair.privKey); + const sharedKey = Keypair.genEcdhSharedKey(keypair.privKey, coordinator.pubKey); + const message = command.encrypt(signature, sharedKey); + + await expect( + pollContract.publishMessage(message.asContractParam(), keypair.pubKey.asContractParam(), { gasLimit: 300000 }), + ).to.be.revertedWithCustomError(pollContract, "VotingPeriodOver"); + }); + + it("should not allow to publish a message batch after the voting period ends", async () => { + const command = new PCommand(1n, keypair.pubKey, 0n, 9n, 1n, pollId, 0n); + const signature = command.sign(keypair.privKey); + const sharedKey = Keypair.genEcdhSharedKey(keypair.privKey, coordinator.pubKey); + const message = command.encrypt(signature, sharedKey); + await expect( + pollContract.publishMessageBatch([message.asContractParam()], [keypair.pubKey.asContractParam()], { + gasLimit: 300000, + }), + ).to.be.revertedWithCustomError(pollContract, "VotingPeriodOver"); + }); + }); + + describe("Merge messages", () => { + let messageAqContract: AccQueue; + + beforeEach(async () => { + const extContracts = await pollContract.extContracts(); + + const messageAqAddress = extContracts.messageAq; + messageAqContract = new BaseContract(messageAqAddress, accQueueQuinaryMaciAbi, signer) as AccQueue; + }); + + it("should revert if the subtrees are not merged for StateAq", async () => { + await expect(pollContract.mergeMaciStateAq(0, { gasLimit: 4000000 })).to.be.revertedWithCustomError( + pollContract, + "StateAqSubtreesNeedMerge", + ); + }); + + it("should allow the coordinator to merge the message AccQueue", async () => { + let tx = await pollContract.mergeMessageAqSubRoots(0, { + gasLimit: 3000000, + }); + let receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + + tx = await pollContract.mergeMessageAq({ gasLimit: 4000000 }); + receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + }); + + it("should have the correct message root set", async () => { + const onChainMessageRoot = await messageAqContract.getMainRoot(MESSAGE_TREE_DEPTH); + const offChainMessageRoot = maciState.polls.get(pollId)!.messageTree.root; + + expect(onChainMessageRoot.toString()).to.eq(offChainMessageRoot.toString()); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/PollFactory.test.ts b/packages/hardhat/test/maci-tests/PollFactory.test.ts new file mode 100644 index 0000000..3d79ae0 --- /dev/null +++ b/packages/hardhat/test/maci-tests/PollFactory.test.ts @@ -0,0 +1,53 @@ +import { expect } from "chai"; +import { BaseContract, Signer, ZeroAddress } from "ethers"; +import { Keypair } from "../../maci-ts/domainobjs"; + +import { deployPollFactory, getDefaultSigner } from "../../maci-ts/ts"; +import { PollFactory } from "../../typechain-types"; + +import { maxValues, treeDepths } from "./constants"; + +describe("pollFactory", () => { + let pollFactory: PollFactory; + let signer: Signer; + + const { pubKey: coordinatorPubKey } = new Keypair(); + + before(async () => { + signer = await getDefaultSigner(); + pollFactory = (await deployPollFactory(signer, true)) as BaseContract as PollFactory; + }); + + describe("deployment", () => { + it("should allow anyone to deploy a new poll", async () => { + const tx = await pollFactory.deploy( + "100", + maxValues, + treeDepths, + coordinatorPubKey.asContractParam(), + ZeroAddress, + ZeroAddress, + await signer.getAddress(), + ); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + }); + + it("should revert when called with an invalid param for max values", async () => { + await expect( + pollFactory.deploy( + "100", + { + maxMessages: maxValues.maxMessages, + maxVoteOptions: 2 ** 50, + }, + treeDepths, + coordinatorPubKey.asContractParam(), + ZeroAddress, + ZeroAddress, + await signer.getAddress(), + ), + ).to.be.revertedWithCustomError(pollFactory, "InvalidMaxValues"); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/SignUpGatekeeper.test.ts b/packages/hardhat/test/maci-tests/SignUpGatekeeper.test.ts new file mode 100644 index 0000000..e24fc8e --- /dev/null +++ b/packages/hardhat/test/maci-tests/SignUpGatekeeper.test.ts @@ -0,0 +1,107 @@ +import { expect } from "chai"; +import { AbiCoder, ZeroAddress, Signer } from "ethers"; +import { Keypair } from "../../maci-ts/domainobjs"; + +import { + deploySignupToken, + deploySignupTokenGatekeeper, + deployFreeForAllSignUpGatekeeper, +} from "../../maci-ts/ts/deploy"; +import { getDefaultSigner } from "../../maci-ts/ts/utils"; +import { FreeForAllGatekeeper, MACI, SignUpToken, SignUpTokenGatekeeper } from "../../typechain-types"; + +import { STATE_TREE_DEPTH, initialVoiceCreditBalance } from "./constants"; +import { deployTestContracts } from "./utils"; + +describe("SignUpGatekeeper", () => { + let signUpToken: SignUpToken; + let freeForAllContract: FreeForAllGatekeeper; + let signUpTokenGatekeeperContract: SignUpTokenGatekeeper; + let signer: Signer; + + before(async () => { + signer = await getDefaultSigner(); + freeForAllContract = await deployFreeForAllSignUpGatekeeper(signer, true); + signUpToken = await deploySignupToken(signer, true); + signUpTokenGatekeeperContract = await deploySignupTokenGatekeeper(await signUpToken.getAddress(), signer, true); + }); + + describe("Deployment", () => { + it("Gatekeepers should be deployed correctly", () => { + expect(freeForAllContract).to.not.eq(undefined); + expect(signUpToken).to.not.eq(undefined); + expect(signUpTokenGatekeeperContract).to.not.eq(undefined); + }); + + it("SignUpTokenGatekeeper has token address set", async () => { + expect(await signUpTokenGatekeeperContract.token()).to.eq(await signUpToken.getAddress()); + }); + }); + + describe("SignUpTokenGatekeeper", () => { + let maciContract: MACI; + + beforeEach(async () => { + signUpToken = await deploySignupToken(signer, true); + signUpTokenGatekeeperContract = await deploySignupTokenGatekeeper(await signUpToken.getAddress(), signer, true); + + const r = await deployTestContracts( + initialVoiceCreditBalance, + STATE_TREE_DEPTH, + signer, + true, + true, + signUpTokenGatekeeperContract, + ); + + maciContract = r.maciContract; + }); + + it("sets MACI instance correctly", async () => { + const maciAddress = await maciContract.getAddress(); + const tx = await signUpTokenGatekeeperContract.setMaciInstance(maciAddress); + await tx.wait(); + + expect(await signUpTokenGatekeeperContract.maci()).to.eq(maciAddress); + }); + + it("it should revert if the register function is called by a non registered maci instance", async () => { + const user = new Keypair(); + + await signUpToken.giveToken(await signer.getAddress(), 0); + + await expect( + maciContract.signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + ), + ).to.be.revertedWithCustomError(signUpTokenGatekeeperContract, "OnlyMACI"); + }); + + it("should register a user if the register function is called by a registered maci instance", async () => { + const user = new Keypair(); + + await signUpToken.giveToken(await signer.getAddress(), 0); + + await signUpTokenGatekeeperContract.setMaciInstance(await maciContract.getAddress()); + + const tx = await maciContract.signUp( + user.pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ); + const receipt = await tx.wait(); + + expect(receipt?.status).to.eq(1); + }); + }); + + describe("FreeForAllSignUpGatekeeper", () => { + it("should always complete successfully", async () => { + const tx = await freeForAllContract.register(ZeroAddress, AbiCoder.defaultAbiCoder().encode(["uint256"], [1])); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/Subsidy.test.ts b/packages/hardhat/test/maci-tests/Subsidy.test.ts new file mode 100644 index 0000000..12296cb --- /dev/null +++ b/packages/hardhat/test/maci-tests/Subsidy.test.ts @@ -0,0 +1,213 @@ +/* eslint-disable no-underscore-dangle */ +import { expect } from "chai"; +import { BaseContract, Signer } from "ethers"; +import { EthereumProvider } from "hardhat/types"; +import { + MaciState, + Poll, + packSubsidySmallVals, + IProcessMessagesCircuitInputs, + ISubsidyCircuitInputs, +} from "../../maci-ts/core"; +import { NOTHING_UP_MY_SLEEVE } from "../../maci-ts/crypto"; +import { Keypair, Message, PubKey } from "../../maci-ts/domainobjs"; + +import { parseArtifact } from "../../maci-ts/ts/abi"; +import { IVerifyingKeyStruct } from "../../maci-ts/ts/types"; +import { getDefaultSigner } from "../../maci-ts/ts/utils"; +import { MACI, Poll as PollContract, MessageProcessor, Subsidy, Verifier, VkRegistry } from "../../typechain-types"; + +import { + STATE_TREE_DEPTH, + duration, + maxValues, + messageBatchSize, + testProcessVk, + testTallyVk, + treeDepths, +} from "./constants"; +import { timeTravel, deployTestContracts } from "./utils"; + +describe("Subsidy", () => { + let signer: Signer; + let maciContract: MACI; + let pollContract: PollContract; + let subsidyContract: Subsidy; + let mpContract: MessageProcessor; + let verifierContract: Verifier; + let vkRegistryContract: VkRegistry; + + const coordinator = new Keypair(); + const maciState = new MaciState(STATE_TREE_DEPTH); + + const [pollAbi] = parseArtifact("Poll"); + const [mpAbi] = parseArtifact("MessageProcessor"); + const [subsidyAbi] = parseArtifact("Subsidy"); + + let pollId: bigint; + let poll: Poll; + + let generatedInputs: IProcessMessagesCircuitInputs; + + before(async () => { + signer = await getDefaultSigner(); + + const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, true); + maciContract = r.maciContract; + verifierContract = r.mockVerifierContract as Verifier; + vkRegistryContract = r.vkRegistryContract; + + // deploy a poll + // deploy on chain poll + const tx = await maciContract.deployPoll( + duration, + treeDepths, + coordinator.pubKey.asContractParam(), + verifierContract, + vkRegistryContract, + true, + { + gasLimit: 10000000, + }, + ); + const receipt = await tx.wait(); + + const block = await signer.provider!.getBlock(receipt!.blockHash); + const deployTime = block!.timestamp; + + expect(receipt?.status).to.eq(1); + const iface = maciContract.interface; + + // parse DeployPoll log + const logMPTally = receipt!.logs[receipt!.logs.length - 1]; + const MPTallyEvent = iface.parseLog(logMPTally as unknown as { topics: string[]; data: string }) as unknown as { + args: { + _pollId: bigint; + pollAddr: { + poll: string; + messageProcessor: string; + tally: string; + subsidy: string; + }; + }; + name: string; + }; + expect(MPTallyEvent.name).to.eq("DeployPoll"); + + pollId = MPTallyEvent.args._pollId; + + const pollContractAddress = await maciContract.getPoll(pollId); + pollContract = new BaseContract(pollContractAddress, pollAbi, signer) as PollContract; + const mpContractAddress = MPTallyEvent.args.pollAddr.messageProcessor; + mpContract = new BaseContract(mpContractAddress, mpAbi, signer) as MessageProcessor; + const subsidyContractAddress = MPTallyEvent.args.pollAddr.subsidy; + subsidyContract = new BaseContract(subsidyContractAddress, subsidyAbi, signer) as Subsidy; + + // deploy local poll + const p = maciState.deployPoll(BigInt(deployTime + duration), maxValues, treeDepths, messageBatchSize, coordinator); + expect(p.toString()).to.eq(pollId.toString()); + // publish the NOTHING_UP_MY_SLEEVE message + const messageData = [NOTHING_UP_MY_SLEEVE, BigInt(0)]; + for (let i = 2; i < 10; i += 1) { + messageData.push(BigInt(0)); + } + const message = new Message(BigInt(1), messageData); + const padKey = new PubKey([ + BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"), + BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"), + ]); + + // save the poll + poll = maciState.polls.get(pollId)!; + + poll.publishMessage(message, padKey); + + // update the poll state + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + // process messages locally + generatedInputs = poll.processMessages(pollId); + + // set the verification keys on the vk smart contract + await vkRegistryContract.setVerifyingKeys( + STATE_TREE_DEPTH, + treeDepths.intStateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + testTallyVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + await vkRegistryContract.setSubsidyKeys( + STATE_TREE_DEPTH, + treeDepths.intStateTreeDepth, + treeDepths.voteOptionTreeDepth, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + }); + + it("should not be possible to update subsidy before the poll has ended", async () => { + await expect(subsidyContract.updateSubsidy(0, [0, 0, 0, 0, 0, 0, 0, 0])).to.be.revertedWithCustomError( + subsidyContract, + "VotingPeriodNotPassed", + ); + }); + + it("genSubsidyPackedVals() should generate the correct value", async () => { + const onChainPackedVals = BigInt(await subsidyContract.genSubsidyPackedVals(0)); + const packedVals = packSubsidySmallVals(0, 0, 0); + expect(onChainPackedVals.toString()).to.eq(packedVals.toString()); + }); + + it("updateSbCommitment() should revert when the messages have not been processed yet", async () => { + // go forward in time + await timeTravel(signer.provider! as unknown as EthereumProvider, duration + 1); + + await expect(subsidyContract.updateSbCommitment()).to.be.revertedWithCustomError( + subsidyContract, + "ProcessingNotComplete", + ); + }); + + it("updateSubsidy() should fail as the messages have not been processed yet", async () => { + await expect(subsidyContract.updateSubsidy(0, [0, 0, 0, 0, 0, 0, 0, 0])).to.be.revertedWithCustomError( + subsidyContract, + "ProcessingNotComplete", + ); + }); + + describe("after merging acc queues", () => { + let subsidyGeneratedInputs: ISubsidyCircuitInputs; + before(async () => { + await pollContract.mergeMaciStateAqSubRoots(0, pollId); + await pollContract.mergeMaciStateAq(0); + + await pollContract.mergeMessageAqSubRoots(0); + await pollContract.mergeMessageAq(); + subsidyGeneratedInputs = poll.subsidyPerBatch(); + }); + it("updateSubsidy() should update the tally commitment", async () => { + // do the processing on the message processor contract + await mpContract.processMessages(generatedInputs.newSbCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + + const tx = await subsidyContract.updateSubsidy( + subsidyGeneratedInputs.newSubsidyCommitment, + [0, 0, 0, 0, 0, 0, 0, 0], + ); + + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + + const onChainNewTallyCommitment = await subsidyContract.subsidyCommitment(); + + expect(subsidyGeneratedInputs.newSubsidyCommitment).to.eq(onChainNewTallyCommitment.toString()); + }); + it("updateSubsidy() should revert when votes have already been tallied", async () => { + await expect( + subsidyContract.updateSubsidy(subsidyGeneratedInputs.newSubsidyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]), + ).to.be.revertedWithCustomError(subsidyContract, "AllSubsidyCalculated"); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/Tally.test.ts b/packages/hardhat/test/maci-tests/Tally.test.ts new file mode 100644 index 0000000..14739af --- /dev/null +++ b/packages/hardhat/test/maci-tests/Tally.test.ts @@ -0,0 +1,505 @@ +/* eslint-disable no-underscore-dangle */ +import { expect } from "chai"; +import { AbiCoder, BaseContract, Signer } from "ethers"; +import { EthereumProvider } from "hardhat/types"; +import { + MaciState, + Poll, + packTallyVotesSmallVals, + IProcessMessagesCircuitInputs, + ITallyCircuitInputs, +} from "../../maci-ts/core"; +import { NOTHING_UP_MY_SLEEVE } from "../../maci-ts/crypto"; +import { Keypair, Message, PubKey } from "../../maci-ts/domainobjs"; + +import { parseArtifact } from "../../maci-ts/ts/abi"; +import { IVerifyingKeyStruct } from "../../maci-ts/ts/types"; +import { getDefaultSigner } from "../../maci-ts/ts/utils"; +import { Tally, MACI, Poll as PollContract, MessageProcessor, Verifier, VkRegistry } from "../../typechain-types"; + +import { + STATE_TREE_DEPTH, + duration, + initialVoiceCreditBalance, + maxValues, + messageBatchSize, + tallyBatchSize, + testProcessVk, + testTallyVk, + treeDepths, +} from "./constants"; +import { timeTravel, deployTestContracts } from "./utils"; + +describe("TallyVotes", () => { + let signer: Signer; + let maciContract: MACI; + let pollContract: PollContract; + let tallyContract: Tally; + let mpContract: MessageProcessor; + let verifierContract: Verifier; + let vkRegistryContract: VkRegistry; + + const coordinator = new Keypair(); + let users: Keypair[]; + let maciState: MaciState; + + const [pollAbi] = parseArtifact("Poll"); + const [mpAbi] = parseArtifact("MessageProcessor"); + const [tallyAbi] = parseArtifact("Tally"); + + let pollId: bigint; + let poll: Poll; + + let generatedInputs: IProcessMessagesCircuitInputs; + + before(async () => { + maciState = new MaciState(STATE_TREE_DEPTH); + + users = [new Keypair(), new Keypair()]; + + signer = await getDefaultSigner(); + + const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, true, true); + maciContract = r.maciContract; + verifierContract = r.mockVerifierContract as Verifier; + vkRegistryContract = r.vkRegistryContract; + + // deploy a poll + // deploy on chain poll + const tx = await maciContract.deployPoll( + duration, + treeDepths, + coordinator.pubKey.asContractParam(), + verifierContract, + vkRegistryContract, + false, + { + gasLimit: 10000000, + }, + ); + const receipt = await tx.wait(); + + const block = await signer.provider!.getBlock(receipt!.blockHash); + const deployTime = block!.timestamp; + + expect(receipt?.status).to.eq(1); + const iface = maciContract.interface; + const logs = receipt!.logs[receipt!.logs.length - 1]; + const event = iface.parseLog(logs as unknown as { topics: string[]; data: string }) as unknown as { + args: { + _pollId: bigint; + pollAddr: { + poll: string; + messageProcessor: string; + tally: string; + }; + }; + name: string; + }; + expect(event.name).to.eq("DeployPoll"); + + pollId = event.args._pollId; + + const pollContractAddress = await maciContract.getPoll(pollId); + pollContract = new BaseContract(pollContractAddress, pollAbi, signer) as PollContract; + mpContract = new BaseContract(event.args.pollAddr.messageProcessor, mpAbi, signer) as MessageProcessor; + tallyContract = new BaseContract(event.args.pollAddr.tally, tallyAbi, signer) as Tally; + + // deploy local poll + const p = maciState.deployPoll(BigInt(deployTime + duration), maxValues, treeDepths, messageBatchSize, coordinator); + expect(p.toString()).to.eq(pollId.toString()); + // publish the NOTHING_UP_MY_SLEEVE message + const messageData = [NOTHING_UP_MY_SLEEVE]; + for (let i = 1; i < 10; i += 1) { + messageData.push(BigInt(0)); + } + const message = new Message(BigInt(1), messageData); + const padKey = new PubKey([ + BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"), + BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"), + ]); + + // save the poll + poll = maciState.polls.get(pollId)!; + + poll.publishMessage(message, padKey); + + // update the poll state + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + // process messages locally + generatedInputs = poll.processMessages(pollId); + + // set the verification keys on the vk smart contract + await vkRegistryContract.setVerifyingKeys( + STATE_TREE_DEPTH, + treeDepths.intStateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + testTallyVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + }); + + it("should not be possible to tally votes before the poll has ended", async () => { + await expect(tallyContract.tallyVotes(0, [0, 0, 0, 0, 0, 0, 0, 0])).to.be.revertedWithCustomError( + tallyContract, + "VotingPeriodNotPassed", + ); + }); + + it("genTallyVotesPackedVals() should generate the correct value", async () => { + const onChainPackedVals = BigInt(await tallyContract.genTallyVotesPackedVals(users.length, 0, tallyBatchSize)); + const packedVals = packTallyVotesSmallVals(0, tallyBatchSize, users.length); + expect(onChainPackedVals.toString()).to.eq(packedVals.toString()); + }); + + it("updateSbCommitment() should revert when the messages have not been processed yet", async () => { + // go forward in time + await timeTravel(signer.provider! as unknown as EthereumProvider, duration + 1); + + await expect(tallyContract.updateSbCommitment()).to.be.revertedWithCustomError( + tallyContract, + "ProcessingNotComplete", + ); + }); + + it("tallyVotes() should fail as the messages have not been processed yet", async () => { + await expect(tallyContract.tallyVotes(0, [0, 0, 0, 0, 0, 0, 0, 0])).to.be.revertedWithCustomError( + tallyContract, + "ProcessingNotComplete", + ); + }); + + describe("after merging acc queues", () => { + let tallyGeneratedInputs: ITallyCircuitInputs; + before(async () => { + await pollContract.mergeMaciStateAqSubRoots(0, pollId); + await pollContract.mergeMaciStateAq(0); + + await pollContract.mergeMessageAqSubRoots(0); + await pollContract.mergeMessageAq(); + tallyGeneratedInputs = poll.tallyVotes(); + }); + + it("isTallied should return false", async () => { + const isTallied = await tallyContract.isTallied(); + expect(isTallied).to.eq(false); + }); + + it("tallyVotes() should update the tally commitment", async () => { + // do the processing on the message processor contract + await mpContract.processMessages(generatedInputs.newSbCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + + await tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + + const onChainNewTallyCommitment = await tallyContract.tallyCommitment(); + expect(tallyGeneratedInputs.newTallyCommitment).to.eq(onChainNewTallyCommitment.toString()); + }); + + it("isTallied should return true", async () => { + const isTallied = await tallyContract.isTallied(); + expect(isTallied).to.eq(true); + }); + + it("tallyVotes() should revert when votes have already been tallied", async () => { + await expect( + tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]), + ).to.be.revertedWithCustomError(tallyContract, "AllBallotsTallied"); + }); + }); + + describe("ballots === tallyBatchSize", () => { + before(async () => { + // create 24 users (total 25 - 24 + 1 nothing up my sleeve) + users = Array.from({ length: 24 }, () => new Keypair()); + maciState = new MaciState(STATE_TREE_DEPTH); + + const updatedDuration = 5000000; + + const intStateTreeDepth = 2; + + const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, true, true); + maciContract = r.maciContract; + verifierContract = r.mockVerifierContract as Verifier; + vkRegistryContract = r.vkRegistryContract; + + // signup all users + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < users.length; i += 1) { + const timestamp = Math.floor(Date.now() / 1000); + // signup locally + maciState.signUp(users[i].pubKey, BigInt(initialVoiceCreditBalance), BigInt(timestamp)); + // signup on chain + + // eslint-disable-next-line no-await-in-loop + await maciContract.signUp( + users[i].pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + ); + } + + // deploy a poll + // deploy on chain poll + const tx = await maciContract.deployPoll( + updatedDuration, + { + ...treeDepths, + intStateTreeDepth, + }, + coordinator.pubKey.asContractParam(), + verifierContract, + vkRegistryContract, + false, + { + gasLimit: 10000000, + }, + ); + const receipt = await tx.wait(); + + const block = await signer.provider!.getBlock(receipt!.blockHash); + const deployTime = block!.timestamp; + + expect(receipt?.status).to.eq(1); + const iface = maciContract.interface; + const logs = receipt!.logs[receipt!.logs.length - 1]; + const event = iface.parseLog(logs as unknown as { topics: string[]; data: string }) as unknown as { + args: { + _pollId: bigint; + pollAddr: { + poll: string; + messageProcessor: string; + tally: string; + }; + }; + name: string; + }; + expect(event.name).to.eq("DeployPoll"); + + pollId = event.args._pollId; + + const pollContractAddress = await maciContract.getPoll(pollId); + pollContract = new BaseContract(pollContractAddress, pollAbi, signer) as PollContract; + mpContract = new BaseContract(event.args.pollAddr.messageProcessor, mpAbi, signer) as MessageProcessor; + tallyContract = new BaseContract(event.args.pollAddr.tally, tallyAbi, signer) as Tally; + + // deploy local poll + const p = maciState.deployPoll( + BigInt(deployTime + updatedDuration), + maxValues, + { + ...treeDepths, + intStateTreeDepth, + }, + messageBatchSize, + coordinator, + ); + expect(p.toString()).to.eq(pollId.toString()); + // publish the NOTHING_UP_MY_SLEEVE message + const messageData = [NOTHING_UP_MY_SLEEVE]; + for (let i = 1; i < 10; i += 1) { + messageData.push(BigInt(0)); + } + const message = new Message(BigInt(1), messageData); + const padKey = new PubKey([ + BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"), + BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"), + ]); + + // save the poll + poll = maciState.polls.get(pollId)!; + + poll.publishMessage(message, padKey); + + // update the poll state + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + // set the verification keys on the vk smart contract + await vkRegistryContract.setVerifyingKeys( + STATE_TREE_DEPTH, + intStateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + testTallyVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + + await timeTravel(signer.provider! as unknown as EthereumProvider, updatedDuration); + await pollContract.mergeMaciStateAqSubRoots(0, pollId); + await pollContract.mergeMaciStateAq(0); + + await pollContract.mergeMessageAqSubRoots(0); + await pollContract.mergeMessageAq(); + + const processMessagesInputs = poll.processMessages(pollId); + await mpContract.processMessages(processMessagesInputs.newSbCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + }); + + it("should tally votes correctly", async () => { + const tallyGeneratedInputs = poll.tallyVotes(); + await tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + + const onChainNewTallyCommitment = await tallyContract.tallyCommitment(); + expect(tallyGeneratedInputs.newTallyCommitment).to.eq(onChainNewTallyCommitment.toString()); + await expect( + tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]), + ).to.be.revertedWithCustomError(tallyContract, "AllBallotsTallied"); + }); + }); + + describe("ballots > tallyBatchSize", () => { + before(async () => { + // create 25 users (and thus 26 ballots) (total 26 - 25 + 1 nothing up my sleeve) + users = Array.from({ length: 25 }, () => new Keypair()); + maciState = new MaciState(STATE_TREE_DEPTH); + + const updatedDuration = 5000000; + + const intStateTreeDepth = 2; + + const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, true, true); + maciContract = r.maciContract; + verifierContract = r.mockVerifierContract as Verifier; + vkRegistryContract = r.vkRegistryContract; + + // signup all users + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < users.length; i += 1) { + const timestamp = Math.floor(Date.now() / 1000); + // signup locally + maciState.signUp(users[i].pubKey, BigInt(initialVoiceCreditBalance), BigInt(timestamp)); + // signup on chain + + // eslint-disable-next-line no-await-in-loop + await maciContract.signUp( + users[i].pubKey.asContractParam(), + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + ); + } + + // deploy a poll + // deploy on chain poll + const tx = await maciContract.deployPoll( + updatedDuration, + { + ...treeDepths, + intStateTreeDepth, + }, + coordinator.pubKey.asContractParam(), + verifierContract, + vkRegistryContract, + false, + { + gasLimit: 10000000, + }, + ); + const receipt = await tx.wait(); + + const block = await signer.provider!.getBlock(receipt!.blockHash); + const deployTime = block!.timestamp; + + expect(receipt?.status).to.eq(1); + const iface = maciContract.interface; + const logs = receipt!.logs[receipt!.logs.length - 1]; + const event = iface.parseLog(logs as unknown as { topics: string[]; data: string }) as unknown as { + args: { + _pollId: bigint; + pollAddr: { + poll: string; + messageProcessor: string; + tally: string; + }; + }; + name: string; + }; + expect(event.name).to.eq("DeployPoll"); + + pollId = event.args._pollId; + + const pollContractAddress = await maciContract.getPoll(pollId); + pollContract = new BaseContract(pollContractAddress, pollAbi, signer) as PollContract; + mpContract = new BaseContract(event.args.pollAddr.messageProcessor, mpAbi, signer) as MessageProcessor; + tallyContract = new BaseContract(event.args.pollAddr.tally, tallyAbi, signer) as Tally; + + // deploy local poll + const p = maciState.deployPoll( + BigInt(deployTime + updatedDuration), + maxValues, + { + ...treeDepths, + intStateTreeDepth, + }, + messageBatchSize, + coordinator, + ); + expect(p.toString()).to.eq(pollId.toString()); + // publish the NOTHING_UP_MY_SLEEVE message + const messageData = [NOTHING_UP_MY_SLEEVE]; + for (let i = 1; i < 10; i += 1) { + messageData.push(BigInt(0)); + } + const message = new Message(BigInt(1), messageData); + const padKey = new PubKey([ + BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"), + BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"), + ]); + + // save the poll + poll = maciState.polls.get(pollId)!; + + poll.publishMessage(message, padKey); + + // update the poll state + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + // set the verification keys on the vk smart contract + await vkRegistryContract.setVerifyingKeys( + STATE_TREE_DEPTH, + intStateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + testTallyVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + + await timeTravel(signer.provider! as unknown as EthereumProvider, updatedDuration); + await pollContract.mergeMaciStateAqSubRoots(0, pollId); + await pollContract.mergeMaciStateAq(0); + + await pollContract.mergeMessageAqSubRoots(0); + await pollContract.mergeMessageAq(); + + const processMessagesInputs = poll.processMessages(pollId); + await mpContract.processMessages(processMessagesInputs.newSbCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + }); + + it("should tally votes correctly", async () => { + // tally first batch + let tallyGeneratedInputs = poll.tallyVotes(); + + await tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + + // check commitment + const onChainNewTallyCommitment = await tallyContract.tallyCommitment(); + expect(tallyGeneratedInputs.newTallyCommitment).to.eq(onChainNewTallyCommitment.toString()); + + // tally second batch + tallyGeneratedInputs = poll.tallyVotes(); + + await tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + + // check that it fails to tally again + await expect( + tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]), + ).to.be.revertedWithCustomError(tallyContract, "AllBallotsTallied"); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/TallyNonQv.test.ts b/packages/hardhat/test/maci-tests/TallyNonQv.test.ts new file mode 100644 index 0000000..8c6b64f --- /dev/null +++ b/packages/hardhat/test/maci-tests/TallyNonQv.test.ts @@ -0,0 +1,213 @@ +/* eslint-disable no-underscore-dangle */ +import { expect } from "chai"; +import { BaseContract, Signer } from "ethers"; +import { EthereumProvider } from "hardhat/types"; +import { + MaciState, + Poll, + packTallyVotesSmallVals, + IProcessMessagesCircuitInputs, + ITallyCircuitInputs, +} from "../../maci-ts/core"; +import { NOTHING_UP_MY_SLEEVE } from "../../maci-ts/crypto"; +import { Keypair, Message, PubKey } from "../../maci-ts/domainobjs"; + +import type { Tally, MACI, Poll as PollContract, MessageProcessor, Verifier, VkRegistry } from "../../typechain-types"; + +import { parseArtifact } from "../../maci-ts/ts/abi"; +import { IVerifyingKeyStruct } from "../../maci-ts/ts/types"; +import { getDefaultSigner } from "../../maci-ts/ts/utils"; + +import { + STATE_TREE_DEPTH, + duration, + maxValues, + messageBatchSize, + tallyBatchSize, + testProcessVk, + testTallyVk, + treeDepths, +} from "./constants"; +import { timeTravel, deployTestContracts } from "./utils"; + +describe("TallyVotesNonQv", () => { + let signer: Signer; + let maciContract: MACI; + let pollContract: PollContract; + let tallyContract: Tally; + let mpContract: MessageProcessor; + let verifierContract: Verifier; + let vkRegistryContract: VkRegistry; + + const coordinator = new Keypair(); + let users: Keypair[]; + let maciState: MaciState; + + const [pollAbi] = parseArtifact("Poll"); + const [mpAbi] = parseArtifact("MessageProcessor"); + const [tallyAbi] = parseArtifact("TallyNonQv"); + + let pollId: bigint; + let poll: Poll; + + let generatedInputs: IProcessMessagesCircuitInputs; + + before(async () => { + maciState = new MaciState(STATE_TREE_DEPTH); + + users = [new Keypair(), new Keypair()]; + + signer = await getDefaultSigner(); + + const r = await deployTestContracts(100, STATE_TREE_DEPTH, signer, false, true); + maciContract = r.maciContract; + verifierContract = r.mockVerifierContract as Verifier; + vkRegistryContract = r.vkRegistryContract; + + // deploy a poll + // deploy on chain poll + const tx = await maciContract.deployPoll( + duration, + treeDepths, + coordinator.pubKey.asContractParam(), + verifierContract, + vkRegistryContract, + false, + { + gasLimit: 10000000, + }, + ); + const receipt = await tx.wait(); + + const block = await signer.provider!.getBlock(receipt!.blockHash); + const deployTime = block!.timestamp; + + expect(receipt?.status).to.eq(1); + const iface = maciContract.interface; + const logs = receipt!.logs[receipt!.logs.length - 1]; + const event = iface.parseLog(logs as unknown as { topics: string[]; data: string }) as unknown as { + args: { + _pollId: bigint; + pollAddr: { + poll: string; + messageProcessor: string; + tally: string; + }; + }; + name: string; + }; + expect(event.name).to.eq("DeployPoll"); + + pollId = event.args._pollId; + + const pollContractAddress = await maciContract.getPoll(pollId); + pollContract = new BaseContract(pollContractAddress, pollAbi, signer) as PollContract; + mpContract = new BaseContract(event.args.pollAddr.messageProcessor, mpAbi, signer) as MessageProcessor; + tallyContract = new BaseContract(event.args.pollAddr.tally, tallyAbi, signer) as Tally; + + // deploy local poll + const p = maciState.deployPoll(BigInt(deployTime + duration), maxValues, treeDepths, messageBatchSize, coordinator); + expect(p.toString()).to.eq(pollId.toString()); + // publish the NOTHING_UP_MY_SLEEVE message + const messageData = [NOTHING_UP_MY_SLEEVE]; + for (let i = 1; i < 10; i += 1) { + messageData.push(BigInt(0)); + } + const message = new Message(BigInt(1), messageData); + const padKey = new PubKey([ + BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"), + BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"), + ]); + + // save the poll + poll = maciState.polls.get(pollId)!; + + poll.publishMessage(message, padKey); + + // update the poll state + poll.updatePoll(BigInt(maciState.stateLeaves.length)); + + // process messages locally + generatedInputs = poll.processMessages(pollId, false); + + // set the verification keys on the vk smart contract + await vkRegistryContract.setVerifyingKeys( + STATE_TREE_DEPTH, + treeDepths.intStateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + testTallyVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + }); + + it("should not be possible to tally votes before the poll has ended", async () => { + await expect(tallyContract.tallyVotes(0, [0, 0, 0, 0, 0, 0, 0, 0])).to.be.revertedWithCustomError( + tallyContract, + "VotingPeriodNotPassed", + ); + }); + + it("genTallyVotesPackedVals() should generate the correct value", async () => { + const onChainPackedVals = BigInt(await tallyContract.genTallyVotesPackedVals(users.length, 0, tallyBatchSize)); + const packedVals = packTallyVotesSmallVals(0, tallyBatchSize, users.length); + expect(onChainPackedVals.toString()).to.eq(packedVals.toString()); + }); + + it("updateSbCommitment() should revert when the messages have not been processed yet", async () => { + // go forward in time + await timeTravel(signer.provider! as unknown as EthereumProvider, duration + 1); + + await expect(tallyContract.updateSbCommitment()).to.be.revertedWithCustomError( + tallyContract, + "ProcessingNotComplete", + ); + }); + + it("tallyVotes() should fail as the messages have not been processed yet", async () => { + await expect(tallyContract.tallyVotes(0, [0, 0, 0, 0, 0, 0, 0, 0])).to.be.revertedWithCustomError( + tallyContract, + "ProcessingNotComplete", + ); + }); + + describe("after merging acc queues", () => { + let tallyGeneratedInputs: ITallyCircuitInputs; + before(async () => { + await pollContract.mergeMaciStateAqSubRoots(0, pollId); + await pollContract.mergeMaciStateAq(0); + + await pollContract.mergeMessageAqSubRoots(0); + await pollContract.mergeMessageAq(); + tallyGeneratedInputs = poll.tallyVotes(); + }); + + it("isTallied should return false", async () => { + const isTallied = await tallyContract.isTallied(); + expect(isTallied).to.eq(false); + }); + + it("tallyVotes() should update the tally commitment", async () => { + // do the processing on the message processor contract + await mpContract.processMessages(generatedInputs.newSbCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + + await tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]); + + const onChainNewTallyCommitment = await tallyContract.tallyCommitment(); + expect(tallyGeneratedInputs.newTallyCommitment).to.eq(onChainNewTallyCommitment.toString()); + }); + + it("isTallied should return true", async () => { + const isTallied = await tallyContract.isTallied(); + expect(isTallied).to.eq(true); + }); + + it("tallyVotes() should revert when votes have already been tallied", async () => { + await expect( + tallyContract.tallyVotes(tallyGeneratedInputs.newTallyCommitment, [0, 0, 0, 0, 0, 0, 0, 0]), + ).to.be.revertedWithCustomError(tallyContract, "AllBallotsTallied"); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/Utilities.test.ts b/packages/hardhat/test/maci-tests/Utilities.test.ts new file mode 100644 index 0000000..a53c263 --- /dev/null +++ b/packages/hardhat/test/maci-tests/Utilities.test.ts @@ -0,0 +1,86 @@ +import { expect } from "chai"; +import { BigNumberish, ZeroAddress } from "ethers"; +import { StateLeaf, Keypair } from "../../maci-ts/domainobjs"; + +import { deployPoseidonContracts, linkPoseidonLibraries } from "../../maci-ts/ts/deploy"; +import { getDefaultSigner } from "../../maci-ts/ts/utils"; +import { Utilities } from "../../typechain-types"; + +describe("Utilities", () => { + let utilitiesContract: Utilities; + + describe("Deployment", () => { + before(async () => { + const { PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract } = + await deployPoseidonContracts(await getDefaultSigner(), {}, true); + + const [ + poseidonT3ContractAddress, + poseidonT4ContractAddress, + poseidonT5ContractAddress, + poseidonT6ContractAddress, + ] = await Promise.all([ + PoseidonT3Contract.getAddress(), + PoseidonT4Contract.getAddress(), + PoseidonT5Contract.getAddress(), + PoseidonT6Contract.getAddress(), + ]); + + // Link Poseidon contracts + const utilitiesContractFactory = await linkPoseidonLibraries( + "Utilities", + poseidonT3ContractAddress, + poseidonT4ContractAddress, + poseidonT5ContractAddress, + poseidonT6ContractAddress, + await getDefaultSigner(), + true, + ); + + utilitiesContract = (await utilitiesContractFactory.deploy()) as Utilities; + await utilitiesContract.deploymentTransaction()?.wait(); + }); + + it("should have been deployed", async () => { + expect(utilitiesContract).to.not.eq(undefined); + expect(await utilitiesContract.getAddress()).to.not.eq(ZeroAddress); + }); + }); + + describe("hashStateLeaf", () => { + it("should correctly hash a StateLeaf", async () => { + const keypair = new Keypair(); + const voiceCreditBalance = BigInt(1234); + const stateLeaf = new StateLeaf(keypair.pubKey, voiceCreditBalance, BigInt(456546345)); + const onChainHash = await utilitiesContract.hashStateLeaf(stateLeaf.asContractParam()); + const expectedHash = stateLeaf.hash(); + + expect(onChainHash.toString()).to.eq(expectedHash.toString()); + }); + }); + + describe("padAndHashMessage", () => { + it("should correctly pad and hash a message", async () => { + const dataToPad: [BigNumberish, BigNumberish] = [1234, 1234]; + const msgType = BigInt(2) as BigNumberish; + + // Call the padAndHashMessage function + const { message, padKey } = await utilitiesContract.padAndHashMessage(dataToPad, msgType); + + // Validate the returned message + expect(message.msgType.toString()).to.eq(msgType.toString()); + expect(message.data.slice(0, 2)).to.deep.eq(dataToPad); + for (let i = 2; i < 10; i += 1) { + expect(message.data[i]).to.eq(0); + } + + // Validate the returned padKey + expect(padKey.x.toString()).to.eq( + "10457101036533406547632367118273992217979173478358440826365724437999023779287", + ); + expect(padKey.y.toString()).to.eq( + "19824078218392094440610104313265183977899662750282163392862422243483260492317", + ); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/Verifier.test.ts b/packages/hardhat/test/maci-tests/Verifier.test.ts new file mode 100644 index 0000000..76717d7 --- /dev/null +++ b/packages/hardhat/test/maci-tests/Verifier.test.ts @@ -0,0 +1,106 @@ +import { expect } from "chai"; +import { G1Point, G2Point } from "../../maci-ts/crypto"; +import { VerifyingKey } from "../../maci-ts/domainobjs"; + +import type { IVerifyingKeyStruct } from "../../maci-ts/ts/types"; +import type { BigNumberish } from "ethers"; + +import { deployVerifier } from "../../maci-ts/ts/deploy"; +import { getDefaultSigner } from "../../maci-ts/ts/utils"; +import { Verifier } from "../../typechain-types"; + +describe("DomainObjs", () => { + const vk = new VerifyingKey( + new G1Point( + BigInt("20491192805390485299153009773594534940189261866228447918068658471970481763042"), + BigInt("9383485363053290200918347156157836566562967994039712273449902621266178545958"), + ), + new G2Point( + [ + BigInt("4252822878758300859123897981450591353533073413197771768651442665752259397132"), + BigInt("6375614351688725206403948262868962793625744043794305715222011528459656738731"), + ], + [ + BigInt("21847035105528745403288232691147584728191162732299865338377159692350059136679"), + BigInt("10505242626370262277552901082094356697409835680220590971873171140371331206856"), + ], + ), + new G2Point( + [ + BigInt("11559732032986387107991004021392285783925812861821192530917403151452391805634"), + BigInt("10857046999023057135944570762232829481370756359578518086990519993285655852781"), + ], + [ + BigInt("4082367875863433681332203403145435568316851327593401208105741076214120093531"), + BigInt("8495653923123431417604973247489272438418190587263600148770280649306958101930"), + ], + ), + new G2Point( + [ + BigInt("11700261708411360112482712242528551130212577267248363110777096731569359533937"), + BigInt("19316071393769631071739466808924557575370046223156790236472688098546713485164"), + ], + [ + BigInt("8314809347259847850803251217663255270167988731493310587391546796826904220459"), + BigInt("19027224119116513453619472056165183919393637553270616301189593772848351986009"), + ], + ), + [ + new G1Point( + BigInt("8475939680648083280638846051497134319487781451783634569144849229381887869470"), + BigInt("15777387922383777864128245075158682837173769163333646572506201314277694741524"), + ), + new G1Point( + BigInt("6307974476057044946223853054915497058693993784049217695740696374670315278450"), + BigInt("19541766564091333476121980691242907000813131822237920987048117031710761017707"), + ), + ], + ); + + const proof: BigNumberish[] = [ + "1165825367733124312792381812275119057681245770152620921258630875255505370924", + "1326527658843314194011957286833609405197138989276710702270523657454496479584", + + "7737027768984365020868604289323857674854735856726312758475237268839850113180", + "20950373595246980797046559551868305313847958836379415962375381761472018077992", + + "13877265106716680864869634040774025553232681727839817029074568384172308524666", + "15414074355891062201145392604892692653071670599659589357921635169192446560614", + + "18990315920454525475289309807669145530304447815324475374776788804237092237703", + "14054172703456858179866637245926478995167764402990898943219235085496257747260", + ]; + + const publicInputs: BigNumberish[] = [ + "17771946183498688010237928397719449956849198402702324449167227661291280245514", + ]; + + let verifierContract: Verifier; + + describe("Deployment", () => { + before(async () => { + verifierContract = await deployVerifier(await getDefaultSigner(), true); + }); + + it("should correctly verify a proof", async () => { + const isValid = await verifierContract.verify( + proof, + vk.asContractParam() as IVerifyingKeyStruct, + publicInputs[0], + { gasLimit: 1000000 }, + ); + + expect(isValid).to.eq(true); + }); + it("should return false for a proof that is not valid", async () => { + const isValid = await verifierContract.verify( + proof, + vk.asContractParam() as IVerifyingKeyStruct, + BigInt(publicInputs[0]) + BigInt(1), + { gasLimit: 1000000 }, + ); + + expect(isValid).to.eq(false); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/VkRegistry.test.ts b/packages/hardhat/test/maci-tests/VkRegistry.test.ts new file mode 100644 index 0000000..2a401d6 --- /dev/null +++ b/packages/hardhat/test/maci-tests/VkRegistry.test.ts @@ -0,0 +1,189 @@ +import { expect } from "chai"; +import { Signer } from "ethers"; + +import { IVerifyingKeyStruct, VkRegistry, deployVkRegistry, getDefaultSigner } from "../../maci-ts/ts"; + +import { messageBatchSize, testProcessVk, testTallyVk, treeDepths } from "./constants"; +import { compareVks } from "./utils"; + +describe("VkRegistry", () => { + let signer: Signer; + let vkRegistryContract: VkRegistry; + + const stateTreeDepth = 10; + + describe("deployment", () => { + before(async () => { + signer = await getDefaultSigner(); + vkRegistryContract = await deployVkRegistry(signer, true); + }); + it("should have set the correct owner", async () => { + expect(await vkRegistryContract.owner()).to.eq(await signer.getAddress()); + }); + }); + + describe("setVerifyingKeys", () => { + it("should set the process and tally vks", async () => { + const tx = await vkRegistryContract.setVerifyingKeys( + stateTreeDepth, + treeDepths.intStateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + testTallyVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + }); + + it("should throw when trying to set another vk for the same params", async () => { + await expect( + vkRegistryContract.setVerifyingKeys( + stateTreeDepth, + treeDepths.intStateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + testTallyVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ), + ).to.be.revertedWithCustomError(vkRegistryContract, "ProcessVkAlreadySet"); + }); + + it("should allow to set vks for different params", async () => { + const tx = await vkRegistryContract.setVerifyingKeys( + stateTreeDepth + 1, + treeDepths.intStateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + testTallyVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + }); + + it("should allow to set the subsidy vks", async () => { + const tx = await vkRegistryContract.setSubsidyKeys( + stateTreeDepth, + treeDepths.intStateTreeDepth, + treeDepths.voteOptionTreeDepth, + testProcessVk.asContractParam() as IVerifyingKeyStruct, + { gasLimit: 1000000 }, + ); + const receipt = await tx.wait(); + expect(receipt?.status).to.eq(1); + }); + }); + + describe("hasVks", () => { + describe("hasProcessVk", () => { + it("should return true for the process vk", async () => { + expect( + await vkRegistryContract.hasProcessVk( + stateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + ), + ).to.eq(true); + }); + it("should return false for a non-existing vk", async () => { + expect( + await vkRegistryContract.hasProcessVk( + stateTreeDepth + 2, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + ), + ).to.eq(false); + }); + }); + + describe("hasTallyVk", () => { + it("should return true for the tally vk", async () => { + expect( + await vkRegistryContract.hasTallyVk( + stateTreeDepth, + treeDepths.intStateTreeDepth, + treeDepths.voteOptionTreeDepth, + ), + ).to.eq(true); + }); + it("should return false for a non-existing vk", async () => { + expect( + await vkRegistryContract.hasTallyVk( + stateTreeDepth + 2, + treeDepths.intStateTreeDepth, + treeDepths.voteOptionTreeDepth, + ), + ).to.eq(false); + }); + }); + + describe("hasSubsidyVk", () => { + it("should return true for the subsidy vk", async () => { + expect( + await vkRegistryContract.hasSubsidyVk( + stateTreeDepth, + treeDepths.intStateTreeDepth, + treeDepths.voteOptionTreeDepth, + ), + ).to.eq(true); + }); + it("should return false for a non-existing vk", async () => { + expect( + await vkRegistryContract.hasSubsidyVk( + stateTreeDepth + 2, + treeDepths.intStateTreeDepth, + treeDepths.voteOptionTreeDepth, + ), + ).to.eq(false); + }); + }); + }); + + describe("genSignatures", () => { + describe("genProcessVkSig", () => { + it("should generate a valid signature", async () => { + const sig = await vkRegistryContract.genProcessVkSig( + stateTreeDepth, + treeDepths.messageTreeDepth, + treeDepths.voteOptionTreeDepth, + messageBatchSize, + ); + const vk = await vkRegistryContract.getProcessVkBySig(sig); + compareVks(testProcessVk, vk); + }); + }); + + describe("genTallyVkSig", () => { + it("should generate a valid signature", async () => { + const sig = await vkRegistryContract.genTallyVkSig( + stateTreeDepth, + treeDepths.intStateTreeDepth, + treeDepths.voteOptionTreeDepth, + ); + const vk = await vkRegistryContract.getTallyVkBySig(sig); + compareVks(testTallyVk, vk); + }); + }); + + describe("genSubsidyVkSig", () => { + it("should generate a valid signature", async () => { + const sig = await vkRegistryContract.genSubsidyVkSig( + stateTreeDepth, + treeDepths.intStateTreeDepth, + treeDepths.voteOptionTreeDepth, + ); + const vk = await vkRegistryContract.getSubsidyVkBySig(sig); + compareVks(testProcessVk, vk); + }); + }); + }); +}); diff --git a/packages/hardhat/test/maci-tests/constants.ts b/packages/hardhat/test/maci-tests/constants.ts new file mode 100644 index 0000000..ea65248 --- /dev/null +++ b/packages/hardhat/test/maci-tests/constants.ts @@ -0,0 +1,42 @@ +import { MaxValues, TreeDepths } from "../../maci-ts/core"; +import { G1Point, G2Point } from "../../maci-ts/crypto"; +import { VerifyingKey } from "../../maci-ts/domainobjs"; + +export const duration = 2_000; + +export const STATE_TREE_DEPTH = 10; +export const STATE_TREE_ARITY = 5; +export const MESSAGE_TREE_DEPTH = 2; +export const MESSAGE_TREE_SUBDEPTH = 1; +export const messageBatchSize = STATE_TREE_ARITY ** MESSAGE_TREE_SUBDEPTH; + +export const testProcessVk = new VerifyingKey( + new G1Point(BigInt(0), BigInt(1)), + new G2Point([BigInt(2), BigInt(3)], [BigInt(4), BigInt(5)]), + new G2Point([BigInt(6), BigInt(7)], [BigInt(8), BigInt(9)]), + new G2Point([BigInt(10), BigInt(11)], [BigInt(12), BigInt(13)]), + [new G1Point(BigInt(14), BigInt(15)), new G1Point(BigInt(16), BigInt(17))], +); + +export const testTallyVk = new VerifyingKey( + new G1Point(BigInt(0), BigInt(1)), + new G2Point([BigInt(2), BigInt(3)], [BigInt(4), BigInt(5)]), + new G2Point([BigInt(6), BigInt(7)], [BigInt(8), BigInt(9)]), + new G2Point([BigInt(10), BigInt(11)], [BigInt(12), BigInt(13)]), + [new G1Point(BigInt(14), BigInt(15)), new G1Point(BigInt(16), BigInt(17))], +); + +export const initialVoiceCreditBalance = 100; +export const maxValues: MaxValues = { + maxMessages: STATE_TREE_ARITY ** MESSAGE_TREE_DEPTH, + maxVoteOptions: 25, +}; + +export const treeDepths: TreeDepths = { + intStateTreeDepth: 1, + messageTreeDepth: MESSAGE_TREE_DEPTH, + messageTreeSubDepth: MESSAGE_TREE_SUBDEPTH, + voteOptionTreeDepth: 2, +}; + +export const tallyBatchSize = STATE_TREE_ARITY ** treeDepths.intStateTreeDepth; diff --git a/packages/hardhat/test/maci-tests/utils.ts b/packages/hardhat/test/maci-tests/utils.ts new file mode 100644 index 0000000..b1cc250 --- /dev/null +++ b/packages/hardhat/test/maci-tests/utils.ts @@ -0,0 +1,536 @@ +import { expect } from "chai"; +import { BaseContract, Signer } from "ethers"; +import { IncrementalQuinTree, AccQueue, calcDepthFromNumLeaves, hash2, hash5 } from "../../maci-ts/crypto"; +import { IVkContractParams, VerifyingKey } from "../../maci-ts/domainobjs"; + +import type { EthereumProvider } from "hardhat/types"; + +import { getDefaultSigner } from "../../maci-ts/ts"; +import { + deployConstantInitialVoiceCreditProxy, + deployFreeForAllSignUpGatekeeper, + deployMaci, + deployMockVerifier, + deployPoseidonContracts, + deployTopupCredit, + deployVkRegistry, + linkPoseidonLibraries, +} from "../../maci-ts/ts/deploy"; +import { IDeployedTestContracts } from "../../maci-ts/ts/types"; +import { AccQueue as AccQueueContract, FreeForAllGatekeeper } from "../../typechain-types"; + +export const insertSubTreeGasLimit = { gasLimit: 300000 }; +export const enqueueGasLimit = { gasLimit: 500000 }; +export const fillGasLimit = { gasLimit: 4000000 }; + +/** + * Travel in time in a local blockchain node + * @param provider the provider to use + * @param seconds the number of seconds to travel for + */ +export async function timeTravel(provider: EthereumProvider, seconds: number): Promise { + await provider.send("evm_increaseTime", [Number(seconds)]); + await provider.send("evm_mine", []); +} + +/** + * Compare two verifying keys + * @param vk - the off chain vk + * @param vkOnChain - the on chain vk + */ +export const compareVks = (vk: VerifyingKey, vkOnChain: IVkContractParams): void => { + expect(vk.ic.length).to.eq(vkOnChain.ic.length); + for (let i = 0; i < vk.ic.length; i += 1) { + expect(vk.ic[i].x.toString()).to.eq(vkOnChain.ic[i].x.toString()); + expect(vk.ic[i].y.toString()).to.eq(vkOnChain.ic[i].y.toString()); + } + expect(vk.alpha1.x.toString()).to.eq(vkOnChain.alpha1.x.toString()); + expect(vk.alpha1.y.toString()).to.eq(vkOnChain.alpha1.y.toString()); + expect(vk.beta2.x[0].toString()).to.eq(vkOnChain.beta2.x[0].toString()); + expect(vk.beta2.x[1].toString()).to.eq(vkOnChain.beta2.x[1].toString()); + expect(vk.beta2.y[0].toString()).to.eq(vkOnChain.beta2.y[0].toString()); + expect(vk.beta2.y[1].toString()).to.eq(vkOnChain.beta2.y[1].toString()); + expect(vk.delta2.x[0].toString()).to.eq(vkOnChain.delta2.x[0].toString()); + expect(vk.delta2.x[1].toString()).to.eq(vkOnChain.delta2.x[1].toString()); + expect(vk.delta2.y[0].toString()).to.eq(vkOnChain.delta2.y[0].toString()); + expect(vk.delta2.y[1].toString()).to.eq(vkOnChain.delta2.y[1].toString()); + expect(vk.gamma2.x[0].toString()).to.eq(vkOnChain.gamma2.x[0].toString()); + expect(vk.gamma2.x[1].toString()).to.eq(vkOnChain.gamma2.x[1].toString()); + expect(vk.gamma2.y[0].toString()).to.eq(vkOnChain.gamma2.y[0].toString()); + expect(vk.gamma2.y[1].toString()).to.eq(vkOnChain.gamma2.y[1].toString()); +}; + +/** + * Deploy an AccQueue contract and setup a local TS instance of an AccQueue class + * @param contractName - the name of the contract to deploy + * @param SUB_DEPTH - the depth of the subtrees + * @param HASH_LENGTH - the number of leaves in each subtree + * @param ZERO - the zero value to be used as leaf + * @returns the AccQueue class instance and the AccQueue contract + */ +export const deployTestAccQueues = async ( + contractName: string, + SUB_DEPTH: number, + HASH_LENGTH: number, + ZERO: bigint, +): Promise<{ aq: AccQueue; aqContract: BaseContract }> => { + const { PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract } = + await deployPoseidonContracts(await getDefaultSigner(), {}, true); + + const [poseidonT3ContractAddress, poseidonT4ContractAddress, poseidonT5ContractAddress, poseidonT6ContractAddress] = + await Promise.all([ + PoseidonT3Contract.getAddress(), + PoseidonT4Contract.getAddress(), + PoseidonT5Contract.getAddress(), + PoseidonT6Contract.getAddress(), + ]); + // Link Poseidon contracts + const AccQueueFactory = await linkPoseidonLibraries( + contractName, + poseidonT3ContractAddress, + poseidonT4ContractAddress, + poseidonT5ContractAddress, + poseidonT6ContractAddress, + await getDefaultSigner(), + true, + ); + + const aqContract = await AccQueueFactory.deploy(SUB_DEPTH); + + await aqContract.deploymentTransaction()?.wait(); + + const aq = new AccQueue(SUB_DEPTH, HASH_LENGTH, ZERO); + + return { aq, aqContract }; +}; + +/** + * Test whether fill() works for an empty subtree + * @param aq - the AccQueue class instance + * @param aqContract - the AccQueue contract + * @param index - the index of the subtree + */ +export const testEmptySubtree = async (aq: AccQueue, aqContract: AccQueueContract, index: number): Promise => { + aq.fill(); + const tx = await aqContract.fill(fillGasLimit); + await tx.wait(); + const subRoot = await aqContract.getSubRoot(index); + expect(subRoot.toString()).to.equal(aq.getSubRoot(index).toString()); +}; + +/** + * Insert one leaf and compute the subroot + * @param aq - the AccQueue class instance + * @param aqContract - the AccQueue contract + */ +export const testIncompleteSubtree = async (aq: AccQueue, aqContract: AccQueueContract): Promise => { + const leaf = BigInt(1); + + aq.enqueue(leaf); + await aqContract.enqueue(leaf.toString(), enqueueGasLimit).then(tx => tx.wait()); + + aq.fill(); + await aqContract.fill(fillGasLimit).then(tx => tx.wait()); + + const subRoot = await aqContract.getSubRoot(1); + expect(subRoot.toString()).to.equal(aq.getSubRoot(1).toString()); +}; + +/** + * Test whether fill() works for every number of leaves in an incomplete subtree + * @param aq - the AccQueue class instance + * @param aqContract - the AccQueue contract + * @param HASH_LENGTH - the number of leaves in each subtree + */ +export const testFillForAllIncompletes = async ( + aq: AccQueue, + aqContract: AccQueueContract, + HASH_LENGTH: number, +): Promise => { + for (let i = 0; i < HASH_LENGTH; i += 1) { + for (let j = 0; j < i; j += 1) { + const leaf = BigInt(i + 1); + aq.enqueue(leaf); + // eslint-disable-next-line no-await-in-loop + await aqContract.enqueue(leaf.toString(), enqueueGasLimit).then(tx => tx.wait()); + } + aq.fill(); + // eslint-disable-next-line no-await-in-loop + await aqContract.fill(fillGasLimit).then(tx => tx.wait()); + + // eslint-disable-next-line no-await-in-loop + const subRoot = await aqContract.getSubRoot(3 + i); + expect(subRoot.toString()).to.equal(aq.getSubRoot(3 + i).toString()); + } +}; + +/** + * Test whether the AccQueue is empty upon deployment + * @param aqContract - the AccQueue contract + */ +export const testEmptyUponDeployment = async (aqContract: AccQueueContract): Promise => { + const numLeaves = await aqContract.numLeaves(); + expect(numLeaves.toString()).to.equal("0"); + + await expect(aqContract.getSubRoot(0)).to.be.revertedWithCustomError(aqContract, "InvalidIndex"); +}; + +/** + * Enqueue leaves and check their subroots + * @param aqContract - the AccQueue contract + * @param HASH_LENGTH - the number of leaves in each subtree + * @param SUB_DEPTH - the depth of the subtrees + * @param ZERO - the zero value to be used as leaf + */ +export const testEnqueue = async ( + aqContract: AccQueueContract, + HASH_LENGTH: number, + SUB_DEPTH: number, + ZERO: bigint, +): Promise => { + const hashFunc = HASH_LENGTH === 5 ? hash5 : hash2; + const tree0 = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, hashFunc); + const subtreeCapacity = HASH_LENGTH ** SUB_DEPTH; + + // Insert up to a subtree + for (let i = 0; i < subtreeCapacity; i += 1) { + const leaf = BigInt(i + 1); + tree0.insert(leaf); + + // eslint-disable-next-line no-await-in-loop + await aqContract.enqueue(leaf.toString(), enqueueGasLimit).then(tx => tx.wait()); + } + + let numLeaves = await aqContract.numLeaves(); + expect(numLeaves.toString()).to.eq(subtreeCapacity.toString()); + + const r = await aqContract.getSubRoot(0); + expect(r.toString()).to.eq(tree0.root.toString()); + + const tree1 = new IncrementalQuinTree(SUB_DEPTH, ZERO, HASH_LENGTH, hashFunc); + + // Insert the other subtree + for (let i = 0; i < subtreeCapacity; i += 1) { + const leaf = BigInt(i + 2); + tree1.insert(leaf); + + // eslint-disable-next-line no-await-in-loop + await aqContract.enqueue(leaf.toString(), enqueueGasLimit).then(tx => tx.wait()); + } + + numLeaves = await aqContract.numLeaves(); + expect(numLeaves.toString()).to.eq((subtreeCapacity * 2).toString()); + + const subroot1 = await aqContract.getSubRoot(1); + expect(subroot1.toString()).to.eq(tree1.root.toString()); +}; + +/** + * Insert subtrees directly + * @param aq - the AccQueue class instance + * @param aqContract - the AccQueue contract + * @param NUM_SUBTREES - the number of subtrees to insert + */ +export const testInsertSubTrees = async ( + aq: AccQueue, + aqContract: AccQueueContract, + NUM_SUBTREES: number, + MAIN_DEPTH: number, +): Promise => { + const leaves: bigint[] = []; + for (let i = 0; i < NUM_SUBTREES; i += 1) { + const subTree = new IncrementalQuinTree(aq.getSubDepth(), aq.getZeros()[0], aq.getHashLength(), aq.hashFunc); + const leaf = BigInt(i); + subTree.insert(leaf); + leaves.push(leaf); + + // insert the subtree root + aq.insertSubTree(subTree.root); + // eslint-disable-next-line no-await-in-loop + await aqContract.insertSubTree(subTree.root.toString(), insertSubTreeGasLimit).then(tx => tx.wait()); + } + + let correctRoot: string; + if (NUM_SUBTREES === 1) { + correctRoot = aq.getSubRoots()[0].toString(); + } else { + const depth = calcDepthFromNumLeaves(aq.getHashLength(), aq.getSubRoots().length); + const tree = new IncrementalQuinTree(depth, aq.getZeros()[aq.getSubDepth()], aq.getHashLength(), aq.hashFunc); + + aq.getSubRoots().forEach(subRoot => { + tree.insert(subRoot); + }); + + correctRoot = tree.root.toString(); + } + + // Check whether mergeSubRoots() works + aq.mergeSubRoots(0); + await aqContract.mergeSubRoots(0, { gasLimit: 8000000 }).then(tx => tx.wait()); + + const expectedSmallSRTroot = aq.getSmallSRTroot().toString(); + + expect(correctRoot).to.eq(expectedSmallSRTroot); + + const contractSmallSRTroot = await aqContract.getSmallSRTroot(); + expect(expectedSmallSRTroot.toString()).to.eq(contractSmallSRTroot.toString()); + + // Check whether merge() works + aq.merge(MAIN_DEPTH); + await aqContract.merge(MAIN_DEPTH, { gasLimit: 8000000 }).then(tx => tx.wait()); + + const expectedMainRoot = aq.getMainRoots()[MAIN_DEPTH]; + const contractMainRoot = await aqContract.getMainRoot(MAIN_DEPTH); + + expect(expectedMainRoot.toString()).to.eq(contractMainRoot.toString()); +}; + +/** + * The order of leaves when using enqueue() and insertSubTree() should be correct. + * @param aq - the AccQueue class instance + * @param aqContract - the AccQueue contract + */ +export const testEnqueueAndInsertSubTree = async (aq: AccQueue, aqContract: AccQueueContract): Promise => { + const [z] = aq.getZeros(); + const n = BigInt(1); + + const leaves: bigint[] = []; + + const subTree = new IncrementalQuinTree(aq.getSubDepth(), z, aq.getHashLength(), aq.hashFunc); + + for (let i = 0; i < aq.getHashLength() ** aq.getSubDepth(); i += 1) { + leaves.push(z); + } + + leaves.push(n); + // leaves is now [z, z, z, z..., n] + + const depth = calcDepthFromNumLeaves(aq.getHashLength(), leaves.length); + const tree = new IncrementalQuinTree(depth, z, aq.getHashLength(), aq.hashFunc); + + leaves.forEach(leaf => { + tree.insert(leaf); + }); + + const expectedRoot = tree.root.toString(); + + aq.enqueue(n); + await aqContract.enqueue(n.toString(), enqueueGasLimit).then(tx => tx.wait()); + + aq.insertSubTree(subTree.root); + await aqContract.insertSubTree(subTree.root.toString(), insertSubTreeGasLimit).then(tx => tx.wait()); + + aq.fill(); + await aqContract.fill(fillGasLimit).then(tx => tx.wait()); + + aq.mergeSubRoots(0); + await aqContract.mergeSubRoots(0, { gasLimit: 8000000 }).then(tx => tx.wait()); + + expect(expectedRoot).to.eq(aq.getSmallSRTroot().toString()); + + const contractSmallSRTroot = await aqContract.getSmallSRTroot(); + expect(expectedRoot).to.eq(contractSmallSRTroot.toString()); +}; + +/** + * Insert a number of subtrees and merge them all into a main tree + * @param aq - the AccQueue class instance + * @param aqContract - the AccQueue contract + */ +export const testMerge = async ( + aq: AccQueue, + aqContract: AccQueueContract, + NUM_SUBTREES: number, + MAIN_DEPTH: number, +): Promise => { + // The raw leaves of the main tree + const leaves: bigint[] = []; + for (let i = 0; i < NUM_SUBTREES; i += 1) { + const leaf = BigInt(i); + + aq.enqueue(leaf); + aq.fill(); + // eslint-disable-next-line no-await-in-loop + await aqContract.enqueue(leaf.toString(), enqueueGasLimit).then(tx => tx.wait()); + // eslint-disable-next-line no-await-in-loop + await aqContract.fill(fillGasLimit).then(tx => tx.wait()); + + leaves.push(leaf); + + for (let j = 1; j < aq.getHashLength() ** aq.getSubDepth(); j += 1) { + leaves.push(aq.getZeros()[0]); + } + } + + // Insert leaves into a main tree + const tree = new IncrementalQuinTree(MAIN_DEPTH, aq.getZeros()[0], aq.getHashLength(), aq.hashFunc); + + leaves.forEach(leaf => { + tree.insert(leaf); + }); + + // minHeight should be the small SRT height + const minHeight = await aqContract.calcMinHeight(); + const c = calcDepthFromNumLeaves(aq.getHashLength(), NUM_SUBTREES); + expect(minHeight.toString()).to.eq(c.toString()); + + // Check whether mergeSubRoots() works + aq.mergeSubRoots(0); + await (await aqContract.mergeSubRoots(0, { gasLimit: 8000000 })).wait(); + + const expectedSmallSRTroot = aq.getSmallSRTroot().toString(); + const contractSmallSRTroot = (await aqContract.getSmallSRTroot()).toString(); + + expect(expectedSmallSRTroot).to.eq(contractSmallSRTroot); + + if (NUM_SUBTREES === 1) { + expect(expectedSmallSRTroot).to.eq(aq.getSubRoots()[0].toString()); + } else { + // Check whether the small SRT root is correct + const srtHeight = calcDepthFromNumLeaves(aq.getHashLength(), NUM_SUBTREES); + const smallTree = new IncrementalQuinTree( + srtHeight, + aq.getZeros()[aq.getSubDepth()], + aq.getHashLength(), + aq.hashFunc, + ); + + aq.getSubRoots().forEach(subRoot => { + smallTree.insert(subRoot); + }); + + expect(expectedSmallSRTroot).to.eq(smallTree.root.toString()); + } + + // Check whether mergeDirect() works + const aq2 = aq.copy(); + + aq2.mergeDirect(MAIN_DEPTH); + const directlyMergedRoot = aq2.getMainRoots()[MAIN_DEPTH].toString(); + expect(directlyMergedRoot.toString()).to.eq(tree.root.toString()); + + // Check whether off-chain merge() works + aq.merge(MAIN_DEPTH); + + const expectedMainRoot = aq.getMainRoots()[MAIN_DEPTH].toString(); + + expect(expectedMainRoot).to.eq(directlyMergedRoot); + + // Check whether on-chain merge() works + await (await aqContract.merge(MAIN_DEPTH, { gasLimit: 8000000 })).wait(); + const contractMainRoot = (await aqContract.getMainRoot(MAIN_DEPTH)).toString(); + expect(expectedMainRoot).to.eq(contractMainRoot); +}; + +/** + * Enqueue, merge, enqueue, and merge again + * @param aq - the AccQueue class instance + * @param aqContract - the AccQueue contract + */ +export const testMergeAgain = async (aq: AccQueue, aqContract: AccQueueContract, MAIN_DEPTH: number): Promise => { + const tree = new IncrementalQuinTree(MAIN_DEPTH, aq.getZeros()[0], aq.getHashLength(), aq.hashFunc); + const leaf = BigInt(123); + + // Enqueue + aq.enqueue(leaf); + await aqContract.enqueue(leaf.toString()).then(tx => tx.wait()); + tree.insert(leaf); + + // Merge + aq.mergeDirect(MAIN_DEPTH); + await aqContract.mergeSubRoots(0, { gasLimit: 8000000 }).then(tx => tx.wait()); + await aqContract.merge(MAIN_DEPTH, { gasLimit: 8000000 }).then(tx => tx.wait()); + + for (let i = 1; i < aq.getHashLength() ** aq.getSubDepth(); i += 1) { + tree.insert(aq.getZeros()[0]); + } + + const mainRoot = (await aqContract.getMainRoot(MAIN_DEPTH)).toString(); + const expectedMainRoot = aq.getMainRoots()[MAIN_DEPTH].toString(); + expect(expectedMainRoot).to.eq(mainRoot); + expect(expectedMainRoot).to.eq(tree.root.toString()); + + const leaf2 = BigInt(456); + + // Enqueue + aq.enqueue(leaf2); + await aqContract.enqueue(leaf2.toString()).then(tx => tx.wait()); + tree.insert(leaf2); + + // Merge + aq.mergeDirect(MAIN_DEPTH); + await aqContract.mergeSubRoots(0, { gasLimit: 8000000 }).then(tx => tx.wait()); + await aqContract.merge(MAIN_DEPTH, { gasLimit: 8000000 }).then(tx => tx.wait()); + + for (let i = 1; i < aq.getHashLength() ** aq.getSubDepth(); i += 1) { + tree.insert(aq.getZeros()[0]); + } + + const mainRoot2 = (await aqContract.getMainRoot(MAIN_DEPTH)).toString(); + const expectedMainRoot2 = aq.getMainRoots()[MAIN_DEPTH].toString(); + expect(expectedMainRoot2).to.eq(tree.root.toString()); + + expect(expectedMainRoot2).not.to.eq(expectedMainRoot); + expect(expectedMainRoot2).to.eq(mainRoot2); +}; + +/** + * Deploy a set of smart contracts that can be used for testing. + * @param initialVoiceCreditBalance - the initial voice credit balance for each user + * @param stateTreeDepth - the depth of the state tree + * @param signer - the signer to use + * @param quiet - whether to suppress console output + * @param gatekeeper - the gatekeeper contract to use + * @returns the deployed contracts + */ +export const deployTestContracts = async ( + initialVoiceCreditBalance: number, + stateTreeDepth: number, + signer?: Signer, + useQv = true, + quiet = true, + gatekeeper: FreeForAllGatekeeper | undefined = undefined, +): Promise => { + const mockVerifierContract = await deployMockVerifier(signer, true); + + let gatekeeperContract = gatekeeper; + if (!gatekeeperContract) { + gatekeeperContract = (await deployFreeForAllSignUpGatekeeper(signer, true)) as FreeForAllGatekeeper; + } + + const constantIntialVoiceCreditProxyContract = await deployConstantInitialVoiceCreditProxy( + initialVoiceCreditBalance, + signer, + true, + ); + + // VkRegistry + const vkRegistryContract = await deployVkRegistry(signer, true); + const topupCreditContract = await deployTopupCredit(signer, true); + const [gatekeeperContractAddress, constantIntialVoiceCreditProxyContractAddress, topupCreditContractAddress] = + await Promise.all([ + gatekeeperContract.getAddress(), + constantIntialVoiceCreditProxyContract.getAddress(), + topupCreditContract.getAddress(), + ]); + + const { maciContract, stateAqContract } = await deployMaci({ + signUpTokenGatekeeperContractAddress: gatekeeperContractAddress, + initialVoiceCreditBalanceAddress: constantIntialVoiceCreditProxyContractAddress, + topupCreditContractAddress, + signer, + stateTreeDepth, + useQv, + quiet, + }); + + return { + mockVerifierContract, + gatekeeperContract, + constantIntialVoiceCreditProxyContract, + maciContract, + stateAqContract, + vkRegistryContract, + topupCreditContract, + }; +};