added tests

This commit is contained in:
erhant
2023-06-29 16:56:21 +03:00
parent 3858f4b666
commit 00e646979e
16 changed files with 675 additions and 562 deletions

View File

@@ -7,25 +7,6 @@ on:
jobs:
build:
name: Build checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Node.js
uses: actions/setup-node@v1
with:
node-version: 18.x
- name: Install dependencies
run: yarn
- name: Build everything
run: yarn build
style:
name: Styling checks
runs-on: ubuntu-latest
steps:
@@ -44,3 +25,6 @@ jobs:
- name: Lint code
run: yarn lint
- name: Build everything
run: yarn build

View File

@@ -1,10 +1,9 @@
name: test
name: tests
on:
push:
branches:
- main
- erhant/tests
jobs:
test:
@@ -22,6 +21,13 @@ jobs:
cd circom
cargo build --release
cargo install --path circom
cd ..
- name: Print help
- name: Print Circom version
run: circom --help
- name: Install dependencies
run: yarn
- name: Run tests
run: yarn test

9
.gitignore vendored
View File

@@ -107,19 +107,12 @@ dist
build
dist
# circuit-specific powers of tau are ignored
*.ptau
# universal ptaus not ignored
!ptau/*
# temporary ptaus are ignored
tmp.ptau
# is this still a thing lol
.DS_Store
# ignore auto generated test circuits
circuits/test
ptau
# for init tests
tmptest
inputs/multiplier_3/test-input.json

View File

@@ -15,6 +15,9 @@
<a href="./.github/workflows/build.yml" target="_blank">
<img alt="Workflow: Build" src="https://github.com/erhant/circomkit/actions/workflows/build.yml/badge.svg?branch=main">
</a>
<a href="./.github/workflows/tests.yml" target="_blank">
<img alt="Workflow: Tests" src="https://github.com/erhant/circomkit/actions/workflows/tests.yml/badge.svg?branch=main">
</a>
<a href="https://github.com/iden3/snarkjs" target="_blank">
<img alt="GitHub: SnarkJS" src="https://img.shields.io/badge/github-snarkjs-lightgray?logo=github">
</a>
@@ -126,7 +129,7 @@ npx circomkit calldata circuit input
Circomkit with its default configuration follows an _opinionated file structure_, abstracting away the pathing and orientation behind the scenes. All of these can be customized by overriding the respective settings in `circomkit.json`.
Here is an example structure, where we have a generic Sudoku proof-of-solution circuit, and we instantiate it for a 9x9 board:
An example structure is shown below. Suppose there is a generic circuit for a Sudoku solution knowledge proof written under `circuits` folder. When instantiated, a `main` component for a 9x9 board is created under `circuits/main`. The solution along with it's puzzle is stored as a JSON object under `inputs/sudoku_9x9`. You can see the respective artifacts under `build` directory. In particular, we see `groth16` prefix on some files, indicating that Groth16 protocol was used to create them.
```sh
circomkit
@@ -159,8 +162,10 @@ circomkit
│── sudoku_9x9.r1cs
│── sudoku_9x9.sym
── prover_key.zkey
── verifier_key.json
── groth16_pkey.zkey
│── groth16_vkey.json
└── groth16_verifier.sol
```
@@ -172,6 +177,8 @@ Run all tests via:
yarn test
```
You can also use the CLI in the repo by `yarn cli` as if you are using `npx circomkit`. This is useful for hands-on testing stuff.
## Styling
Circomkit uses [Google TypeScript Style Guide](https://google.github.io/styleguide/tsguide.html).

View File

@@ -37,27 +37,24 @@
"devDependencies": {
"@types/chai": "^4.3.4",
"@types/mocha": "^10.0.1",
"@types/mocha-each": "^2.0.0",
"@types/node": "^18.11.18",
"gts": "^3.1.1",
"mocha": "^10.2.0",
"mocha-each": "^2.0.1",
"rimraf": "^5.0.1",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
},
"optionalDependencies": {
"circomlib": "^2.0.5"
},
"dependencies": {
"chai": "^4.3.7",
"circom_tester": "^0.0.19",
"loglevel": "^1.8.1",
"snarkjs": "^0.6.0"
"snarkjs": "^0.7.0"
},
"keywords": [
"circom",
"zk",
"zero knowledge",
"zero-knowledge",
"snarkjs",
"typescript",
"cli",

Binary file not shown.

View File

@@ -1,58 +1,11 @@
#!/usr/bin/env node
import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'fs';
import {Circomkit} from '../circomkit';
import {initFiles, postInitString} from '../utils/initFiles';
import {initFiles, postInitString, usageString} from '../utils';
import {prettyStringify} from '../utils';
const CONFIG_PATH = './circomkit.json';
const DEFAULT_INPUT = 'default';
const USAGE = `Usage:
Compile the circuit.
> compile circuit
Create main component.
> instantiate circuit
Print circuit information.
> info circuit
Clean build artifacts & main component.
> clean circuit
Export Solidity verifier.
> contract circuit
Export calldata for a verifier contract.
> calldata circuit input
Export JSON for a chosen file.
> json r1cs circuit
> json zkey circuit
> json wtns circuit input
Commence circuit-specific setup.
> setup circuit
> setup circuit ptau-path
Download the PTAU file needed for the circuit.
> ptau circuit
Generate a proof.
> prove circuit input
Verify a proof.
> verify circuit input
Generate a witness.
> witness circuit input
Initialize a Circomkit project.
> init # initializes in current folder
> init project-name # initializes in a new folder
Print configurations to console.
> config
`;
async function cli(): Promise<number> {
// read user configs & override if there are any
@@ -91,12 +44,12 @@ async function cli(): Promise<number> {
case 'json': {
titleLog('Exporting JSON file');
const path = await circomkit.json(
const {json, path} = await circomkit.json(
process.argv[3] as 'r1cs' | 'zkey' | 'wtns',
process.argv[4],
process.argv[5],
true
process.argv[5]
);
writeFileSync(path, prettyStringify(json));
circomkit.log('Exported at: ' + path, 'success');
break;
}
@@ -163,8 +116,8 @@ async function cli(): Promise<number> {
case 'setup': {
titleLog('Circuit-specific setup');
const paths = await circomkit.setup(process.argv[3]);
circomkit.log('Prover key created: ' + paths.proverKey, 'success');
circomkit.log('Verifier key created: ' + paths.verifierKey, 'success');
circomkit.log('Prover key created: ' + paths.proverKeyPath, 'success');
circomkit.log('Verifier key created: ' + paths.verifierKeyPath, 'success');
break;
}
@@ -196,7 +149,7 @@ async function cli(): Promise<number> {
}
default:
console.log(USAGE);
console.log(usageString);
return 1;
}

View File

@@ -4,7 +4,7 @@ import {writeFileSync, readFileSync, existsSync, mkdirSync, rmSync, renameSync}
import {readFile, rm, writeFile} from 'fs/promises';
import {instantiate} from './utils/instantiate';
import {downloadPtau, getPtauName} from './utils/ptau';
import type {CircuitConfig, R1CSInfoType} from './types/circuit';
import type {CircuitConfig, CircuitSignals, R1CSInfoType} from './types/circuit';
import {Logger, getLogger} from 'loglevel';
import type {
CircomkitConfig,
@@ -16,7 +16,7 @@ import {randomBytes} from 'crypto';
import {CircomWasmTester} from './types/circom_tester';
import WasmTester from './testers/wasmTester';
import ProofTester from './testers/proofTester';
import {primeToCurveName} from './utils/curves';
import {prettyStringify, primeToCurveName} from './utils';
import {defaultConfig, colors, CURVES, PROTOCOLS} from './utils/config';
/**
@@ -29,7 +29,7 @@ import {defaultConfig, colors, CURVES, PROTOCOLS} from './utils/config';
* const circomkit = new Circomkit()
* ```
*
* It also provides a WasmTester and a ProofTester module which uses Chai assertions within.
* It also provides a **WasmTester** and a **ProofTester** module which uses Chai assertions within.
*
* ```ts
* const wasmTester = await circomkit.WasmTester(circuitName, circuitConfig)
@@ -65,11 +65,6 @@ export class Circomkit {
}
}
/** Pretty-print for JSON stringify. */
private prettyStringify(obj: unknown): string {
return JSON.stringify(obj, undefined, 2);
}
/** Parse circuit config from `circuits.json` */
private readCircuitConfig(circuit: string): CircuitConfig {
const circuits = JSON.parse(readFileSync(this.config.circuits, 'utf-8'));
@@ -92,18 +87,18 @@ export class Circomkit {
return dir;
case 'target':
return `${this.config.dirCircuits}/main/${circuit}.circom`;
case 'pkey':
return `${dir}/prover_key.zkey`;
case 'vkey':
return `${dir}/verifier_key.json`;
case 'r1cs':
return `${dir}/${circuit}.r1cs`;
case 'sym':
return `${dir}/${circuit}.sym`;
case 'sol':
return `${dir}/Verifier_${this.config.protocol}.sol`;
case 'wasm':
return `${dir}/${circuit}_js/${circuit}.wasm`;
case 'pkey':
return `${dir}/${this.config.protocol}_pkey.zkey`;
case 'vkey':
return `${dir}/${this.config.protocol}_vkey.json`;
case 'sol':
return `${dir}/${this.config.protocol}_verifier.sol`;
default:
throw new Error('Invalid type: ' + type);
}
@@ -166,7 +161,7 @@ export class Circomkit {
/** Information about circuit. */
async info(circuit: string): Promise<R1CSInfoType> {
// we do not pass this._logger here in purpose
// we do not pass this._logger here on purpose
const r1csinfo = await snarkjs.r1cs.info(this.path(circuit, 'r1cs'), undefined);
return {
variables: r1csinfo.nVars,
@@ -250,7 +245,11 @@ export class Circomkit {
/** Export calldata to console.
* @returns calldata as a string
*/
async calldata(circuit: string, input: string) {
async calldata(circuit: string, input: string): Promise<string> {
// fflonk gives error (tested at snarkjs v0.7.0)
if (this.config.protocol === 'fflonk') {
throw new Error('Exporting calldata is not supported for fflonk yet.');
}
const [pubs, proof] = (
await Promise.all(
(['pubs', 'proof'] as const)
@@ -307,8 +306,8 @@ export class Circomkit {
const dir = this.pathWithInput(circuit, input, 'dir');
mkdirSync(dir, {recursive: true});
await Promise.all([
writeFile(this.pathWithInput(circuit, input, 'pubs'), this.prettyStringify(fullProof.publicSignals)),
writeFile(this.pathWithInput(circuit, input, 'proof'), this.prettyStringify(fullProof.proof)),
writeFile(this.pathWithInput(circuit, input, 'pubs'), prettyStringify(fullProof.publicSignals)),
writeFile(this.pathWithInput(circuit, input, 'proof'), prettyStringify(fullProof.proof)),
]);
return dir;
}
@@ -316,7 +315,7 @@ export class Circomkit {
/** Commence a circuit-specific setup.
* @returns path to verifier key and prover key
*/
async setup(circuit: string, ptauPath?: string): Promise<{proverKey: string; verifierKey: string}> {
async setup(circuit: string, ptauPath?: string): Promise<{proverKeyPath: string; verifierKeyPath: string}> {
const r1csPath = this.path(circuit, 'r1cs');
const pkeyPath = this.path(circuit, 'pkey');
const vkeyPath = this.path(circuit, 'vkey');
@@ -377,8 +376,8 @@ export class Circomkit {
// export verification key
const vkey = await snarkjs.zKey.exportVerificationKey(pkeyPath, this._logger);
writeFileSync(vkeyPath, this.prettyStringify(vkey));
return {verifierKey: vkeyPath, proverKey: pkeyPath};
writeFileSync(vkeyPath, prettyStringify(vkey));
return {verifierKeyPath: vkeyPath, proverKeyPath: pkeyPath};
}
/** Verify a proof for some public signals.
@@ -412,20 +411,27 @@ export class Circomkit {
return wtnsPath;
}
/** Exports a JSON input file for some circuit with the given object.
* This is useful for testing real circuits, or creating an input programmatically.
* Overwrites an existing input.
* @returns path to created input file
*/
input(circuit: string, input: string, data: CircuitSignals): string {
const inputPath = this.pathWithInput(circuit, input, 'in');
writeFileSync(inputPath, prettyStringify(data));
return inputPath;
}
/**
* Export a circuit artifact in JSON format. If the last argument `write` is true, it will write to
* file with the appropriate path, and return the path. Otheriwse, returns the JSON obejct.
* @param type type of file to export
* @returns a JSON object or the path that it would be exported to.
*/
async json(
type: 'r1cs' | 'zkey' | 'wtns',
circuit: string,
input?: string,
write?: boolean
): Promise<object | string> {
async json(type: 'r1cs' | 'zkey' | 'wtns', circuit: string, input?: string): Promise<{json: object; path: string}> {
let json: object;
let path: string;
switch (type) {
// R1CS
case 'r1cs': {
@@ -435,6 +441,11 @@ export class Circomkit {
}
// Prover key
case 'zkey': {
// must be groth16, others give error (tested at snarkjs v0.7.0)
if (this.config.protocol !== 'groth16') {
throw new Error('Exporting zKey to JSON only supported for groth16 at the moment.');
}
path = this.path(circuit, 'pkey');
json = await snarkjs.zKey.exportJson(path, undefined); // does not take logger
break;
@@ -450,13 +461,10 @@ export class Circomkit {
throw new Error('Unknown export target: ' + type);
}
if (write) {
path += '.json';
writeFileSync(path, this.prettyStringify(json));
return path;
} else {
return json;
}
return {
json,
path: path + '.json',
};
}
/**

View File

@@ -96,13 +96,7 @@ export default class WasmTester<IN extends readonly string[] = [], OUT extends r
console.log(`# constraints: ${numConstraints}`);
if (expected !== undefined) {
if (numConstraints < expected) {
console.log(`\x1b[0;31mx expectation: ${expected}\x1b[0m`);
} else if (numConstraints > expected) {
console.log(`\x1b[0;33m! expectation: ${expected}\x1b[0m`);
} else {
console.log(`\x1b[0;32m✔\x1b[2;37m expectation: ${expected}\x1b[0m`);
}
expect(numConstraints).to.be.greaterThanOrEqual(expected, 'Circuit is under-constrained!');
}
}

View File

@@ -1,8 +0,0 @@
import type {CircomkitConfig} from '../types/circomkit';
/** A mapping from prime (decimals) to curve name. */
export const primeToCurveName: {[key: `${number}`]: CircomkitConfig['curve']} = {
'21888242871839275222246405745257275088548364400416034343698204186575808495617': 'bn128',
'52435875175126190479447740508185965837690552500527637822603658699938581184513': 'bls12381',
'18446744069414584321': 'goldilocks',
} as const;

View File

@@ -1,3 +1,17 @@
import type {CircomkitConfig} from '../types/circomkit';
/** A mapping from prime (decimals) to curve name. */
export const primeToCurveName: {[key: `${number}`]: CircomkitConfig['curve']} = {
'21888242871839275222246405745257275088548364400416034343698204186575808495617': 'bn128',
'52435875175126190479447740508185965837690552500527637822603658699938581184513': 'bls12381',
'18446744069414584321': 'goldilocks',
} as const;
/** JSON Stringify with a prettier format. */
export function prettyStringify(obj: unknown): string {
return JSON.stringify(obj, undefined, 2);
}
/**
* Initial files for Cirocmkit development environment.
* This is most likely to be used by the CLI via `npx circomkit init`.
@@ -101,3 +115,52 @@ You should also install the following packages:
npm install --save-dev ts-node typescript mocha @types/mocha
`;
export const usageString = `Usage:
Compile the circuit.
> compile circuit
Create main component.
> instantiate circuit
Print circuit information.
> info circuit
Clean build artifacts & main component.
> clean circuit
Export Solidity verifier.
> contract circuit
Export calldata for a verifier contract.
> calldata circuit input
Export JSON for a chosen file.
> json r1cs circuit
> json zkey circuit
> json wtns circuit input
Commence circuit-specific setup.
> setup circuit
> setup circuit ptau-path
Download the PTAU file needed for the circuit.
> ptau circuit
Generate a proof.
> prove circuit input
Verify a proof.
> verify circuit input
Generate a witness.
> witness circuit input
Initialize a Circomkit project.
> init # initializes in current folder
> init project-name # initializes in a new folder
Print configurations to console.
> config
`;

101
tests/circomkit.test.ts Normal file
View File

@@ -0,0 +1,101 @@
import forEach from 'mocha-each';
import {PROTOCOLS} from '../src/utils/config';
import {Circomkit} from '../src';
import {expect} from 'chai';
import {existsSync} from 'fs';
import {CIRCUIT_CONFIG, CIRCUIT_NAME, INPUT, INPUT_NAME, PTAU_PATH} from './common';
// we are not testing all curves because PTAU is only available for bn128
forEach(PROTOCOLS).describe('protocol: %s', (protocol: (typeof PROTOCOLS)[number]) => {
let circomkit: Circomkit;
before(() => {
circomkit = new Circomkit({
protocol,
verbose: false,
logLevel: 'silent',
});
});
it('should instantiate circuit', () => {
const path = circomkit.instantiate(CIRCUIT_NAME, CIRCUIT_CONFIG);
expect(existsSync(path)).to.be.true;
});
it('should compile circuit', async () => {
await circomkit.compile(CIRCUIT_NAME);
});
it('should give correct circuit info', async () => {
const info = await circomkit.info(CIRCUIT_NAME);
expect(info.curve).to.eq('bn128');
expect(info.constraints).to.eq(3); // three constraints for 3 numbers
expect(info.privateInputs).to.eq(3); // input is 3 numbers, all private
expect(info.publicInputs).to.eq(0); // there are no public inputs
expect(info.outputs).to.eq(1); // there is only 1 output, the product
expect(info.labels).to.eq(7); // 3 inputs + 2 inner signals + 1 output + 1 constant
expect(info.variables).to.eq(7); // 3 inputs + 2 inner signals + 1 output + 1 constant
});
it('should setup circuit', async () => {
await circomkit.setup(CIRCUIT_NAME, PTAU_PATH);
});
it('should create an input', async () => {
const path = circomkit.input(CIRCUIT_NAME, INPUT_NAME, INPUT);
expect(existsSync(path)).to.be.true;
});
it('should create a witness', async () => {
const path = await circomkit.witness(CIRCUIT_NAME, INPUT_NAME);
expect(existsSync(path)).to.be.true;
});
it('should create a proof', async () => {
const path = await circomkit.prove(CIRCUIT_NAME, INPUT_NAME);
expect(existsSync(path)).to.be.true;
});
it('should verify the proof', async () => {
const isVerified = await circomkit.verify(CIRCUIT_NAME, INPUT_NAME);
expect(isVerified).to.be.true;
});
it('should export verifier contract', async () => {
const path = await circomkit.contract(CIRCUIT_NAME);
expect(existsSync(path)).to.be.true;
});
it('should export contract calldata', async () => {
try {
await circomkit.calldata(CIRCUIT_NAME, INPUT_NAME);
// fflonk should fail for `calldata`
if (protocol === 'fflonk') {
throw new Error('Should have thrown an error before this.');
}
} catch (err) {
expect((err as Error).message).to.eq('Exporting calldata is not supported for fflonk yet.');
}
});
it('should export JSON files', async () => {
await circomkit.json('r1cs', CIRCUIT_NAME);
await circomkit.json('wtns', CIRCUIT_NAME, INPUT_NAME);
try {
await circomkit.json('zkey', CIRCUIT_NAME);
// only groth16 is allowed to export zkey
if (protocol !== 'groth16') {
throw new Error('Should have thrown an error before this.');
}
} catch (err) {
expect((err as Error).message).to.eq('Exporting zKey to JSON only supported for groth16 at the moment.');
}
});
it('should clean artifacts', async () => {
await circomkit.clean(CIRCUIT_NAME);
});
});

21
tests/common/index.ts Normal file
View File

@@ -0,0 +1,21 @@
// create N random numbers for the multiplier circuit and find its product
export const N = 3;
const numbers = Array.from({length: N}, () => Math.floor(Math.random() * 100 * N));
const product = numbers.reduce((prev, acc) => acc * prev);
export const CIRCUIT_NAME = `multiplier_${N}`;
export const CIRCUIT_CONFIG = {
file: 'multiplier',
template: 'Multiplier',
params: [N],
};
export const INPUT_NAME = 'test-input';
export const PTAU_PATH = './ptau/powersOfTau28_hez_final_08.ptau';
export const INPUT = {
in: numbers,
};
export const OUTPUT = {
out: product,
};

View File

@@ -1,5 +0,0 @@
import {expect} from 'chai';
import {Circomkit} from '../src';
// try and compile with all protocol + curve combinations
describe('compilation tests', () => {});

View File

@@ -1,32 +1,18 @@
import {Circomkit, FullProof, ProofTester, WasmTester} from '../src';
import {expect} from 'chai';
import {CIRCUIT_CONFIG, CIRCUIT_NAME, INPUT, N, OUTPUT} from './common';
describe('testers with multiplier circuit', () => {
describe('testers', () => {
const circomkit = new Circomkit({
verbose: false,
logLevel: 'silent',
});
const N = 3;
const circuitName = `multiplier_${N}`;
const numbers = Array.from({length: N}, () => Math.floor(Math.random() * 100 * N));
const product = numbers.reduce((prev, acc) => acc * prev);
const INPUT = {
in: numbers,
};
const OUTPUT = {
out: product,
};
describe('wasm tester', () => {
let circuit: WasmTester<['in'], ['out']>;
before(async () => {
circuit = await circomkit.WasmTester(circuitName, {
file: 'multiplier',
template: 'Multiplier',
params: [N],
});
circuit = await circomkit.WasmTester(CIRCUIT_NAME, CIRCUIT_CONFIG);
await circuit.checkConstraintCount(N);
});
@@ -47,7 +33,7 @@ describe('testers with multiplier circuit', () => {
let fullProof: FullProof;
before(async () => {
circuit = await circomkit.ProofTester(circuitName);
circuit = await circomkit.ProofTester(CIRCUIT_NAME);
fullProof = await circuit.prove(INPUT);
});
@@ -56,8 +42,7 @@ describe('testers with multiplier circuit', () => {
});
it('should NOT verify', async () => {
// just give a prime number as the output, assuming none of the inputs are 1
await circuit.expectFail(fullProof.proof, ['13']);
await circuit.expectFail(fullProof.proof, ['1']);
});
});
});

800
yarn.lock

File diff suppressed because it is too large Load Diff