Skip to content

Commit

Permalink
test: fuzz testing for circuits
Browse files Browse the repository at this point in the history
- [x] Add fast-check for circuits packages
- [x] Add fuzz testing test templates
- [x] Code style fixes for circuits
0xmad committed May 24, 2024
1 parent d0b5add commit 429bd12
Showing 8 changed files with 371 additions and 186 deletions.
2 changes: 1 addition & 1 deletion circuits/circom/utils/calculateTotal.circom
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ template CalculateTotal(n) {
signal sums[n];
sums[0] <== nums[0];

for (var i=1; i < n; i++) {
for (var i = 1; i < n; i++) {
sums[i] <== sums[i - 1] + nums[i];
}

8 changes: 8 additions & 0 deletions circuits/circom/utils/privToPubKey.circom
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ pragma circom 2.0.0;

// circomlib imports
include "./bitify.circom";
include "./comparators.circom";
include "./escalarmulfix.circom";

/**
@@ -15,9 +16,16 @@ template PrivToPubKey() {
16950150798460657717958625567821834550301663161624707787222815936182638968203
];

// Prime subgroup order 'l'.
var l = 2736030358979909402780800718157159386076813972158567259200215660948447373041;

signal input privKey;
signal output pubKey[2];

// Check if private key is in the prime subgroup order 'l'
var isLessThan = LessThan(251)([privKey, l]);
isLessThan === 1;

// Convert the private key to bits.
var computedPrivBits[253] = Num2Bits(253)(privKey);

30 changes: 17 additions & 13 deletions circuits/package.json
Original file line number Diff line number Diff line change
@@ -20,19 +20,21 @@
"circom:build": "NODE_OPTIONS=--max-old-space-size=4096 circomkit compile",
"circom:setup": "NODE_OPTIONS=--max-old-space-size=4096 circomkit setup",
"types": "tsc -p tsconfig.json --noEmit",
"test": "ts-mocha --exit ts/__tests__/*.test.ts",
"test:hasher": "ts-mocha --exit ts/__tests__/Hasher.test.ts",
"test:slAndBallotTransformer": "ts-mocha --exit ts/__tests__/StateLeafAndBallotTransformer.test.ts",
"test:messageToCommand": "ts-mocha --exit ts/__tests__/MessageToCommand.test.ts",
"test:messageValidator": "ts-mocha --exit ts/__tests__/MessageValidator.test.ts",
"test:verifySignature": "ts-mocha --exit ts/__tests__/VerifySignature.test.ts",
"test:splicer": "ts-mocha --exit ts/__tests__/Splicer.test.ts",
"test:privToPubKey": "ts-mocha --exit ts/__tests__/PrivToPubKey.test.ts",
"test:calculateTotal": "ts-mocha --exit ts/__tests__/CalculateTotal.test.ts",
"test:processMessages": "NODE_OPTIONS=--max-old-space-size=4096 ts-mocha --exit ts/__tests__/ProcessMessages.test.ts",
"test:tallyVotes": "NODE_OPTIONS=--max-old-space-size=4096 ts-mocha --exit ts/__tests__/TallyVotes.test.ts",
"test:ceremonyParams": "ts-mocha --exit ts/__tests__/CeremonyParams.test.ts",
"test:incrementalQuinaryTree": "ts-mocha --exit ts/__tests__/IncrementalQuinaryTree.test.ts"
"mocha-test": "NODE_OPTIONS=--max-old-space-size=4096 ts-mocha --exit -g '^(?!.*\\[fuzz\\]).*$'",
"test": "pnpm run mocha-test ts/__tests__/*.test.ts",
"test:fuzz": "NODE_OPTIONS=--max-old-space-size=4096 ts-mocha --exit -g '\\[fuzz\\]' ./ts/__tests__/*.test.ts",
"test:hasher": "pnpm run mocha-test ts/__tests__/Hasher.test.ts",
"test:slAndBallotTransformer": "pnpm run mocha-test ts/__tests__/StateLeafAndBallotTransformer.test.ts",
"test:messageToCommand": "pnpm run mocha-test ts/__tests__/MessageToCommand.test.ts",
"test:messageValidator": "pnpm run mocha-test ts/__tests__/MessageValidator.test.ts",
"test:verifySignature": "pnpm run mocha-test ts/__tests__/VerifySignature.test.ts",
"test:splicer": "pnpm run mocha-test ts/__tests__/Splicer.test.ts",
"test:privToPubKey": "pnpm run mocha-test ts/__tests__/PrivToPubKey.test.ts",
"test:calculateTotal": "pnpm run mocha-test ts/__tests__/CalculateTotal.test.ts",
"test:processMessages": "pnpm run mocha-test ts/__tests__/ProcessMessages.test.ts",
"test:tallyVotes": "pnpm run mocha-test ts/__tests__/TallyVotes.test.ts",
"test:ceremonyParams": "pnpm run mocha-test ts/__tests__/CeremonyParams.test.ts",
"test:incrementalQuinaryTree": "pnpm run mocha-test ts/__tests__/IncrementalQuinaryTree.test.ts"
},
"dependencies": {
"@zk-kit/circuits": "^0.4.0",
@@ -48,8 +50,10 @@
"@types/chai-as-promised": "^7.1.8",
"@types/mocha": "^10.0.6",
"@types/node": "^20.12.12",
"@zk-kit/baby-jubjub": "^1.0.1",
"chai": "^4.3.10",
"chai-as-promised": "^7.1.2",
"fast-check": "^3.18.0",
"mocha": "^10.4.0",
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.1",
49 changes: 47 additions & 2 deletions circuits/ts/__tests__/CalculateTotal.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { r } from "@zk-kit/baby-jubjub";
import { type WitnessTester } from "circomkit";
import fc from "fast-check";

import { circomkitInstance } from "./utils/utils";
import { circomkitInstance, getSignal } from "./utils/utils";

describe("CalculateTotal circuit", function test() {
this.timeout(900000);

describe("CalculateTotal circuit", () => {
let circuit: WitnessTester<["nums"], ["sum"]>;

before(async () => {
@@ -15,6 +19,7 @@ describe("CalculateTotal circuit", () => {

it("should correctly sum a list of values", async () => {
const nums: number[] = [];

for (let i = 0; i < 6; i += 1) {
nums.push(Math.floor(Math.random() * 100));
}
@@ -27,4 +32,44 @@ describe("CalculateTotal circuit", () => {

await circuit.expectPass(circuitInputs, { sum });
});

it("should sum max value and loop back", async () => {
const nums: bigint[] = [r, r, r, r, r, r];

await circuit.expectPass({ nums }, { sum: 0n });
});

it("should sum max negative value and loop back", async () => {
const nums: bigint[] = [-r, -r, -r, -r, -r, -r];

await circuit.expectPass({ nums }, { sum: 0n });
});

it("should sum max positive and negative values without looping", async () => {
const nums: bigint[] = [-r, r, -r, r, 1n, 2n];

await circuit.expectPass({ nums }, { sum: 3n });
});

it("should correctly sum a list of values [fuzz]", async () => {
await fc.assert(
fc.asyncProperty(fc.array(fc.bigInt({ min: 0n, max: r - 1n }), { minLength: 1 }), async (nums: bigint[]) => {
const sum = nums.reduce((a, b) => a + b, 0n);
fc.pre(sum <= r - 1n);

const testCircuit = await circomkitInstance.WitnessTester("calculateTotal", {
file: "./utils/calculateTotal",
template: "CalculateTotal",
params: [nums.length],
});

const witness = await testCircuit.calculateWitness({ nums });
await testCircuit.expectConstraintPass(witness);
const total = await getSignal(testCircuit, witness, "sum");

return total === sum;
}),
{ numRuns: 10_000 },
);
});
});
365 changes: 204 additions & 161 deletions circuits/ts/__tests__/Hasher.test.ts

Large diffs are not rendered by default.

88 changes: 82 additions & 6 deletions circuits/ts/__tests__/PrivToPubKey.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Base8, inCurve, mulPointEscalar, r } from "@zk-kit/baby-jubjub";
import { expect } from "chai";
import { type WitnessTester } from "circomkit";
import { SNARK_FIELD_SIZE } from "maci-crypto";
import { Keypair } from "maci-domainobjs";
import fc from "fast-check";
import { Keypair, PrivKey, PubKey } from "maci-domainobjs";

import { L } from "./utils/constants";
import { circomkitInstance, getSignal } from "./utils/utils";

describe("Public key derivation circuit", function test() {
this.timeout(90000);
describe.only("Public key derivation circuit", function test() {
this.timeout(900000);

let circuit: WitnessTester<["privKey"], ["pubKey"]>;

@@ -45,7 +47,81 @@ describe("Public key derivation circuit", function test() {

const derivedPubkey0 = await getSignal(circuit, witness, "pubKey[0]");
const derivedPubkey1 = await getSignal(circuit, witness, "pubKey[1]");
expect(derivedPubkey0 < SNARK_FIELD_SIZE).to.eq(true);
expect(derivedPubkey1 < SNARK_FIELD_SIZE).to.eq(true);
expect(inCurve([derivedPubkey0, derivedPubkey1])).to.eq(true);
});

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) => {
const error = await circuit.expectFail({ privKey });

return error.includes("Assert Failed");
}),
);
});

it("should correctly produce different public keys for the different private keys [fuzz]", async () => {
await fc.assert(
fc.asyncProperty(fc.bigInt({ min: 1n, max: L - 1n }), async (x: bigint) => {
const publicKeys = new Map<string, PubKey>();
const privateKeys: PrivKey[] = [];

let i = 0n;

while (x + i * r <= 2n ** 253n) {
privateKeys.push(new PrivKey(x + i * r));
i += 1n;
}

// eslint-disable-next-line no-restricted-syntax
for (const privateKey of privateKeys) {
const publicKey = mulPointEscalar(Base8, BigInt(privateKey.rawPrivKey));

// eslint-disable-next-line no-await-in-loop
const witness = await circuit.calculateWitness({
privKey: BigInt(privateKey.rawPrivKey),
});
// eslint-disable-next-line no-await-in-loop
await circuit.expectConstraintPass(witness);
// eslint-disable-next-line no-await-in-loop
const derivedPubkey0 = await getSignal(circuit, witness, "pubKey[0]");
// eslint-disable-next-line no-await-in-loop
const derivedPubkey1 = await getSignal(circuit, witness, "pubKey[1]");

expect(publicKey[0]).to.eq(derivedPubkey0);
expect(publicKey[1]).to.eq(derivedPubkey1);

publicKeys.set(privateKey.serialize(), new PubKey([derivedPubkey0, derivedPubkey1]));
}

const uniquePublicKeys = [...publicKeys.values()].filter(
(value, index, array) => array.findIndex((publicKey) => publicKey.equals(value)) === index,
);

return uniquePublicKeys.length === privateKeys.length && uniquePublicKeys.length === publicKeys.size;
}),
{ numRuns: 10_000 },
);
});

it("should correctly compute a public key [fuzz]", async () => {
await fc.assert(
fc.asyncProperty(fc.bigInt(), async (salt: bigint) => {
const { pubKey, privKey } = new Keypair(new PrivKey(salt));

const witness = await circuit.calculateWitness({ privKey: BigInt(privKey.asCircuitInputs()) });
await circuit.expectConstraintPass(witness);

const derivedPubkey0 = await getSignal(circuit, witness, "pubKey[0]");
const derivedPubkey1 = await getSignal(circuit, witness, "pubKey[1]");

return (
derivedPubkey0 === pubKey.rawPubKey[0] &&
derivedPubkey1 === pubKey.rawPubKey[1] &&
inCurve([derivedPubkey0, derivedPubkey1])
);
}),
{ numRuns: 10_000 },
);
});
});
2 changes: 2 additions & 0 deletions circuits/ts/__tests__/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -13,3 +13,5 @@ export const treeDepths = {
voteOptionTreeDepth: 2,
};
export const messageBatchSize = 5;

export const L = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
13 changes: 10 additions & 3 deletions pnpm-lock.yaml

0 comments on commit 429bd12

Please sign in to comment.