Skip to content

Commit

Permalink
test(circuits): add fuzz tests for incremental quinary tree (#1520)
Browse files Browse the repository at this point in the history
Co-authored-by: 0xmad <[email protected]>
  • Loading branch information
0xmad and 0xmad authored Jun 3, 2024
1 parent 951b1b3 commit 7a812ef
Show file tree
Hide file tree
Showing 3 changed files with 272 additions and 13 deletions.
22 changes: 11 additions & 11 deletions circuits/circom/trees/incrementalQuinaryTree.circom
Original file line number Diff line number Diff line change
Expand Up @@ -249,26 +249,26 @@ 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]
]);
}

// Initialize hashers for intermediate nodes and compute the root.
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];
}
261 changes: 260 additions & 1 deletion circuits/ts/__tests__/IncrementalQuinaryTree.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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";

chai.use(chaiAsPromised);

describe("Incremental Quinary Tree (IQT)", function test() {
this.timeout(50000);
this.timeout(2250000);

const leavesPerNode = 5;
const treeDepth = 3;
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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<bigint[]> =>
fc.array(fc.bigInt({ min: 0n, max: r - 1n }), {
minLength: leavesPerNode ** levels,
maxLength: leavesPerNode ** levels,
});

const quinCheckRootTest = async (leaves: bigint[]): Promise<boolean> => {
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)),
);
});
}
});
});
});
2 changes: 1 addition & 1 deletion circuits/ts/__tests__/PrivToPubKey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit 7a812ef

Please sign in to comment.