Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(circuits): add fuzz tests for incremental quinary tree #1520

Merged
merged 1 commit into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading