diff --git a/circuits/circom/trees/incrementalQuinaryTree.circom b/circuits/circom/trees/incrementalQuinaryTree.circom index 853ef5f245..377fe95c95 100644 --- a/circuits/circom/trees/incrementalQuinaryTree.circom +++ b/circuits/circom/trees/incrementalQuinaryTree.circom @@ -249,11 +249,11 @@ template QuinCheckRoot(levels) { // Initialize hashers for the leaves. for (var i = 0; i < numLeafHashers; i++) { computedHashers[i] = PoseidonHasher(5)([ - leaves[i*LEAVES_PER_NODE+0], - leaves[i*LEAVES_PER_NODE+1], - leaves[i*LEAVES_PER_NODE+2], - leaves[i*LEAVES_PER_NODE+3], - leaves[i*LEAVES_PER_NODE+4] + leaves[i * LEAVES_PER_NODE + 0], + leaves[i * LEAVES_PER_NODE + 1], + leaves[i * LEAVES_PER_NODE + 2], + leaves[i * LEAVES_PER_NODE + 3], + leaves[i * LEAVES_PER_NODE + 4] ]); } @@ -261,14 +261,14 @@ template QuinCheckRoot(levels) { var k = 0; for (var i = numLeafHashers; i < numHashers; i++) { computedHashers[i] = PoseidonHasher(5)([ - computedHashers[k*LEAVES_PER_NODE+0], - computedHashers[k*LEAVES_PER_NODE+1], - computedHashers[k*LEAVES_PER_NODE+2], - computedHashers[k*LEAVES_PER_NODE+3], - computedHashers[k*LEAVES_PER_NODE+4] + computedHashers[k * LEAVES_PER_NODE + 0], + computedHashers[k * LEAVES_PER_NODE + 1], + computedHashers[k * LEAVES_PER_NODE + 2], + computedHashers[k * LEAVES_PER_NODE + 3], + computedHashers[k * LEAVES_PER_NODE + 4] ]); k++; } - root <== computedHashers[numHashers-1]; + root <== computedHashers[numHashers - 1]; } \ No newline at end of file diff --git a/circuits/ts/__tests__/IncrementalQuinaryTree.test.ts b/circuits/ts/__tests__/IncrementalQuinaryTree.test.ts index ae26f6f8cf..932f091530 100644 --- a/circuits/ts/__tests__/IncrementalQuinaryTree.test.ts +++ b/circuits/ts/__tests__/IncrementalQuinaryTree.test.ts @@ -1,6 +1,8 @@ +import { r } from "@zk-kit/baby-jubjub"; import chai, { expect } from "chai"; import chaiAsPromised from "chai-as-promised"; import { type WitnessTester } from "circomkit"; +import fc, { type Arbitrary } from "fast-check"; import { IncrementalQuinTree, hash5 } from "maci-crypto"; import { getSignal, circomkitInstance } from "./utils/utils"; @@ -8,7 +10,7 @@ import { getSignal, circomkitInstance } from "./utils/utils"; chai.use(chaiAsPromised); describe("Incremental Quinary Tree (IQT)", function test() { - this.timeout(50000); + this.timeout(2250000); const leavesPerNode = 5; const treeDepth = 3; @@ -73,6 +75,65 @@ describe("Incremental Quinary Tree (IQT)", function test() { await expect(circuitQuinSelector.calculateWitness(circuitInputs)).to.be.rejectedWith("Assert Failed."); }); + + it("should check the correct value [fuzz]", async () => { + await fc.assert( + fc.asyncProperty( + fc.nat(), + fc.array(fc.bigInt({ min: 0n, max: r - 1n }), { minLength: leavesPerNode, maxLength: leavesPerNode }), + async (index: number, elements: bigint[]) => { + fc.pre(elements.length > index); + + const witness = await circuitQuinSelector.calculateWitness({ index: BigInt(index), in: elements }); + await circuitQuinSelector.expectConstraintPass(witness); + const out = await getSignal(circuitQuinSelector, witness, "out"); + + return out.toString() === elements[index].toString(); + }, + ), + ); + }); + + it("should loop the value if number is greater that r [fuzz]", async () => { + await fc.assert( + fc.asyncProperty( + fc.nat(), + fc.array(fc.bigInt({ min: r }), { minLength: leavesPerNode, maxLength: leavesPerNode }), + async (index: number, elements: bigint[]) => { + fc.pre(elements.length > index); + + const witness = await circuitQuinSelector.calculateWitness({ index: BigInt(index), in: elements }); + await circuitQuinSelector.expectConstraintPass(witness); + const out = await getSignal(circuitQuinSelector, witness, "out"); + + return out.toString() === (elements[index] % r).toString(); + }, + ), + ); + }); + + it("should throw error if index is out of bounds [fuzz]", async () => { + await fc.assert( + fc.asyncProperty( + fc.nat(), + fc.array(fc.bigInt({ min: 0n }), { minLength: 1 }), + async (index: number, elements: bigint[]) => { + fc.pre(index >= elements.length); + + const circuit = await circomkitInstance.WitnessTester("quinSelector", { + file: "./trees/incrementalQuinaryTree", + template: "QuinSelector", + params: [elements.length], + }); + + return circuit + .calculateWitness({ index: BigInt(index), in: elements }) + .then(() => false) + .catch((error: Error) => error.message.includes("Assert Failed")); + }, + ), + ); + }); }); describe("Splicer", () => { @@ -97,6 +158,60 @@ describe("Incremental Quinary Tree (IQT)", function test() { expect(out4.toString()).to.eq("20"); expect(out5.toString()).to.eq("44"); }); + + it("should check value insertion [fuzz]", async () => { + await fc.assert( + fc.asyncProperty( + fc.nat(), + fc.bigInt({ min: 0n, max: r - 1n }), + fc.array(fc.bigInt({ min: 0n, max: r - 1n }), { minLength: leavesPerNode - 1, maxLength: leavesPerNode - 1 }), + async (index: number, leaf: bigint, elements: bigint[]) => { + fc.pre(index < elements.length); + + const witness = await splicerCircuit.calculateWitness({ + in: elements, + leaf, + index: BigInt(index), + }); + await splicerCircuit.expectConstraintPass(witness); + + const out: bigint[] = []; + + for (let i = 0; i < elements.length + 1; i += 1) { + // eslint-disable-next-line no-await-in-loop + const value = await getSignal(splicerCircuit, witness, `out[${i}]`); + out.push(value); + } + + return out.toString() === [...elements.slice(0, index), leaf, ...elements.slice(index)].toString(); + }, + ), + ); + }); + + it("should throw error if index is out of bounds [fuzz]", async () => { + const maxAllowedIndex = 7; + + await fc.assert( + fc.asyncProperty( + fc.integer({ min: maxAllowedIndex + 1 }), + fc.bigInt({ min: 0n, max: r - 1n }), + fc.array(fc.bigInt({ min: 0n, max: r - 1n }), { minLength: leavesPerNode - 1, maxLength: leavesPerNode - 1 }), + async (index: number, leaf: bigint, elements: bigint[]) => { + fc.pre(index > elements.length); + + return splicerCircuit + .calculateWitness({ + in: elements, + leaf, + index: BigInt(index), + }) + .then(() => false) + .catch((error: Error) => error.message.includes("Assert Failed")); + }, + ), + ); + }); }); describe("QuinGeneratePathIndices", () => { @@ -118,6 +233,73 @@ describe("Incremental Quinary Tree (IQT)", function test() { expect(out3.toString()).to.be.eq("1"); expect(out4.toString()).to.be.eq("0"); }); + + it("should throw error if input is out of bounds [fuzz]", async () => { + const maxLevel = 1_000n; + + await fc.assert( + fc.asyncProperty( + fc.bigInt({ min: 1n, max: maxLevel }), + fc.bigInt({ min: 1n, max: r - 1n }), + async (levels: bigint, input: bigint) => { + fc.pre(BigInt(leavesPerNode) ** levels < input); + + const witness = await circomkitInstance.WitnessTester("quinGeneratePathIndices", { + file: "./trees/incrementalQuinaryTree", + template: "QuinGeneratePathIndices", + params: [levels], + }); + + return witness + .calculateWitness({ in: input }) + .then(() => false) + .catch((error: Error) => error.message.includes("Assert Failed")); + }, + ), + ); + }); + + it("should check generation of path indices [fuzz]", async () => { + const maxLevel = 100n; + + await fc.assert( + fc.asyncProperty( + fc.bigInt({ min: 1n, max: maxLevel }), + fc.bigInt({ min: 1n, max: r - 1n }), + async (levels: bigint, input: bigint) => { + fc.pre(BigInt(leavesPerNode) ** levels > input); + + const tree = new IncrementalQuinTree(Number(levels), 0n, 5, hash5); + + const circuit = await circomkitInstance.WitnessTester("quinGeneratePathIndices", { + file: "./trees/incrementalQuinaryTree", + template: "QuinGeneratePathIndices", + params: [levels], + }); + + const witness = await circuit.calculateWitness({ + in: input, + }); + await circuit.expectConstraintPass(witness); + + const values: bigint[] = []; + + for (let i = 0; i < levels; i += 1) { + // eslint-disable-next-line no-await-in-loop + const value = await getSignal(circuit, witness, `out[${i}]`); + tree.insert(value); + values.push(value); + } + + const { pathIndices } = tree.genProof(Number(input)); + + const isEqual = pathIndices.every((item, index) => item.toString() === values[index].toString()); + + return values.length === pathIndices.length && isEqual; + }, + ), + ); + }); }); describe("QuinLeafExists", () => { @@ -155,6 +337,45 @@ describe("Incremental Quinary Tree (IQT)", function test() { await expect(circuitLeafExists.calculateWitness(circuitInputs)).to.be.rejectedWith("Assert Failed."); }); + + it("should check the correct leaf [fuzz]", async () => { + // TODO: seems js implementation doesn't work with levels more than 22 + const maxLevel = 22; + + await fc.assert( + fc.asyncProperty( + fc.integer({ min: 1, max: maxLevel }), + fc.nat({ max: leavesPerNode - 1 }), + fc.array(fc.bigInt({ min: 0n, max: r - 1n }), { minLength: leavesPerNode, maxLength: leavesPerNode }), + async (levels: number, index: number, leaves: bigint[]) => { + const circuit = await circomkitInstance.WitnessTester("quinLeafExists", { + file: "./trees/incrementalQuinaryTree", + template: "QuinLeafExists", + params: [levels], + }); + + const tree = new IncrementalQuinTree(levels, 0n, leavesPerNode, hash5); + leaves.forEach((value) => { + tree.insert(value); + }); + + const proof = tree.genProof(index); + + const witness = await circuit.calculateWitness({ + root: tree.root, + leaf: leaves[index], + path_elements: proof.pathElements, + path_index: proof.pathIndices, + }); + + return circuit + .expectConstraintPass(witness) + .then(() => true) + .catch(() => false); + }, + ), + ); + }); }); describe("QuinCheckRoot", () => { @@ -188,5 +409,43 @@ describe("Incremental Quinary Tree (IQT)", function test() { "Not enough values for input signal leaves", ); }); + + describe("fuzz checks", () => { + // Bigger values cause out of memory error due to number of elements (5 ** level) + const maxLevel = 4; + + const generateLeaves = (levels: number): Arbitrary => + fc.array(fc.bigInt({ min: 0n, max: r - 1n }), { + minLength: leavesPerNode ** levels, + maxLength: leavesPerNode ** levels, + }); + + const quinCheckRootTest = async (leaves: bigint[]): Promise => { + const levels = Math.floor(Math.log(leaves.length) / Math.log(leavesPerNode)); + const circuit = await circomkitInstance.WitnessTester("quinCheckRoot", { + file: "./trees/incrementalQuinaryTree", + template: "QuinCheckRoot", + params: [levels], + }); + + const tree = new IncrementalQuinTree(levels, 0n, leavesPerNode, hash5); + leaves.forEach((value) => { + tree.insert(value); + }); + + return circuit + .expectPass({ leaves }, { root: tree.root }) + .then(() => true) + .catch(() => false); + }; + + for (let level = 0; level < maxLevel; level += 1) { + it.only(`should check the computation of correct merkle root (level ${level + 1}) [fuzz]`, async () => { + await fc.assert( + fc.asyncProperty(generateLeaves(level + 1), async (leaves: bigint[]) => quinCheckRootTest(leaves)), + ); + }); + } + }); }); }); diff --git a/circuits/ts/__tests__/PrivToPubKey.test.ts b/circuits/ts/__tests__/PrivToPubKey.test.ts index 955212d8fe..3d76ed2ab9 100644 --- a/circuits/ts/__tests__/PrivToPubKey.test.ts +++ b/circuits/ts/__tests__/PrivToPubKey.test.ts @@ -52,7 +52,7 @@ describe("Public key derivation circuit", function test() { it("should throw error if private key is not in the prime subgroup l", async () => { await fc.assert( - fc.asyncProperty(fc.bigInt({ min: L, max: r }), async (privKey: bigint) => { + fc.asyncProperty(fc.bigInt({ min: L, max: r - 1n }), async (privKey: bigint) => { const error = await circuit.expectFail({ privKey }); return error.includes("Assert Failed");