diff --git a/README.md b/README.md index 3ebb94f..a40b6b3 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,10 @@ > An opinionated Circom circuit development environment. -You can develop & test Circom circuits with ease using this repository. We have several example circuits to help guide you: +You can develop & test Circom circuits with ease using this repository. -- **Multiplier**: Proves that you know the factors of a number. -- **Floating Point Addition**: A floating-point addition circuit, as written in [Berkeley ZKP MOOC 2023- Lab 1](https://github.com/rdi-berkeley/zkp-mooc-lab). -- **Fibonacci**: Calculate N'th Fibonacci number, has both recursive & iterative implementations. -- **Sudoku**: Prove that you know the solution to a sudoku puzzle where the board size is a perfect square. +- [x] CLI wrapper for Cirom and SnarkJS +- [x] Generic & type-safe circuit tester ## Usage @@ -66,11 +64,11 @@ Use the [CLI](./scripts/cli.sh), or its wrapper scripts in [package.json](./pack ```bash # first argument is ALWAYS the circuit name -yarn compile circuit-name -yarn clean circuit-name -yarn ptau circuit-name -n num-contribs -p phase1-ptau-path +yarn compile circuit-name [-d directory-name (default: main)] +yarn ptau circuit-name -p phase1-ptau-path [-n num-contribs (default: 1)] yarn prove circuit-name -i input-name yarn verify circuit-name -i input-name +yarn clean circuit-name yarn test circuit-name yarn test:all ``` @@ -93,6 +91,15 @@ Within each test, there are two sub-tests: - **Witness Computation** will test whether witness computations are matching the expectations & the constraints hold. - **Proof Validation** will test whether proof generation & verification works correctly. This requires the **WASM file**, **prover key**, and **verification key** to be calculated beforehand. +## Examples + +We have several example circuits to help guide you: + +- **Multiplier**: Prove that you know the factors of a number. +- **Floating Point Addition**: A floating-point addition circuit, as written in [Berkeley ZKP MOOC 2023 - Lab 1](https://github.com/rdi-berkeley/zkp-mooc-lab). +- **Fibonacci**: Calculate N'th Fibonacci number, has both recursive & iterative implementations. +- **Sudoku**: Prove that you know the solution to a sudoku puzzle where the board size is a perfect square. + ## Styling The code uses Google TypeScript Style guide. It also has some folder & file icon overrides for several Material UI icons to make things look better in VSCode. diff --git a/circuit.config.cjs b/circuit.config.ts similarity index 87% rename from circuit.config.cjs rename to circuit.config.ts index b483c91..e069085 100644 --- a/circuit.config.cjs +++ b/circuit.config.ts @@ -1,7 +1,8 @@ +import {Config} from './types/circuit'; /** - * @type {import("./types/circuit").Config} + * A configuration object for circuit `main` components. */ -const config = { +const config: Config = { // multiplication of 3 numbers multiplier_3: { file: 'multiplier', @@ -53,4 +54,4 @@ const config = { }, }; -module.exports = config; +export default config as Readonly; diff --git a/circuits/fibonacci.circom b/circuits/fibonacci.circom index 2b2f6e4..2eacba3 100644 --- a/circuits/fibonacci.circom +++ b/circuits/fibonacci.circom @@ -6,7 +6,6 @@ template Fibonacci(n) { signal input in[2]; signal output out; - // compute the sequence signal fib[n+1]; fib[0] <== in[0]; fib[1] <== in[1]; diff --git a/circuits/main/multiplier_3.circom b/circuits/main/multiplier_3.circom new file mode 100644 index 0000000..a8559aa --- /dev/null +++ b/circuits/main/multiplier_3.circom @@ -0,0 +1,6 @@ +// auto-generated by instantiate.js +pragma circom 2.0.0; + +include "../multiplier.circom"; + +component main = Multiplier(3); diff --git a/circuits/test/multiplier_3.circom b/circuits/test/multiplier_3.circom new file mode 100644 index 0000000..a8559aa --- /dev/null +++ b/circuits/test/multiplier_3.circom @@ -0,0 +1,6 @@ +// auto-generated by instantiate.js +pragma circom 2.0.0; + +include "../multiplier.circom"; + +component main = Multiplier(3); diff --git a/constants/index.ts b/constants/index.ts index 071f2ff..3399e15 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -1,8 +1,3 @@ -/** - * A label to be used in `console.time` - */ -export const WITNESS_COMP_LABEL = 'Witness computation'; - /** * Order of the finite field used in Ethereum (BN_254) * If you have a number larger than this, you should take the modulus. diff --git a/inputs/multiplier3/80.json b/inputs/multiplier_3/80.json similarity index 100% rename from inputs/multiplier3/80.json rename to inputs/multiplier_3/80.json diff --git a/package.json b/package.json index 1736a91..611410e 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "test:all": "npx mocha", "test": "npx mocha --grep", "type": "./scripts/cli.sh -f type -c", + "instantiate": "./scripts/cli.sh -f instantiate -c", "compile": "./scripts/cli.sh -f compile -c", "clean": "./scripts/cli.sh -f clean -c", "ptau": "./scripts/cli.sh -f ptau -c", @@ -17,6 +18,7 @@ "author": "erhant", "devDependencies": { "@types/chai": "^4.3.4", + "@types/ejs": "^3.1.2", "@types/mocha": "^10.0.1", "@types/node": "^18.11.18", "chai": "^4.3.7", diff --git a/scripts/cli.sh b/scripts/cli.sh index b841f5a..4657d46 100755 --- a/scripts/cli.sh +++ b/scripts/cli.sh @@ -14,6 +14,7 @@ source ./scripts/functions/prove.sh source ./scripts/functions/verify.sh source ./scripts/functions/witness.sh source ./scripts/functions/compile.sh +source ./scripts/functions/instantiate.sh # get arguments NUM_CONTRIBS=1 # default value @@ -68,6 +69,9 @@ case $FUNC in compile) compile $CIRCUIT $COMPILE_DIR ;; + instantiate) + instantiate $CIRCUIT $COMPILE_DIR + ;; type) type $CIRCUIT ;; @@ -86,13 +90,14 @@ case $FUNC in *) echo "Usage:" echo " -f " - echo " clean Cleans the build artifacts" - echo " compile Compile the circuit" - echo " type Generate types for TypeScript" - echo " ptau Phase-2 setup for the circuit" - echo " witness Generate witness from an input" - echo " prove Prove an input" - echo " verify Verify a proof & public signals" + echo " clean Cleans the build artifacts" + echo " compile Compile the circuit" + echo " instantiate Instantiate the main component" + echo " type Generate types for TypeScript" + echo " ptau Phase-2 setup for the circuit" + echo " witness Generate witness from an input" + echo " prove Prove an input" + echo " verify Verify a proof & public signals" echo " -c " echo " -d " echo " -n (default: 1)" diff --git a/scripts/functions/compile.sh b/scripts/functions/compile.sh index 75f3c91..73f7f21 100755 --- a/scripts/functions/compile.sh +++ b/scripts/functions/compile.sh @@ -5,10 +5,6 @@ compile() { local DIR=$2 local CIRCOM_IN=./circuits/$DIR/$CIRCUIT.circom local CIRCOM_OUT=./build/$CIRCUIT - - # generate the circuit main component - mkdir -p ./circuits/$DIR - node ./scripts/instantiate.js $CIRCUIT $DIR # create build dir if not exists already mkdir -p $CIRCOM_OUT diff --git a/scripts/functions/instantiate.sh b/scripts/functions/instantiate.sh new file mode 100755 index 0000000..efe20d9 --- /dev/null +++ b/scripts/functions/instantiate.sh @@ -0,0 +1,12 @@ +## Instantiate the main component +instantiate() { + echo -e "\n${CLIENV_COLOR_TITLE}=== Creating main component ===${CLIENV_COLOR_RESET}" + local CIRCUIT=$1 + local DIR=$2 + + # generate the circuit main component + mkdir -p ./circuits/$DIR + npx ts-node ./utils/instantiate.ts $CIRCUIT $DIR + + echo -e "${CLIENV_COLOR_LOG}Done!${CLIENV_COLOR_RESET}" +} diff --git a/scripts/instantiate.js b/scripts/instantiate.js deleted file mode 100644 index 0d5b0b0..0000000 --- a/scripts/instantiate.js +++ /dev/null @@ -1,19 +0,0 @@ -const ejs = require('ejs'); -const {writeFileSync, readFileSync} = require('fs'); -const config = require('../circuit.config.cjs'); - -// read circuit from config -const target = process.argv[2]; -const dir = process.argv[3]; -if (!(target in config)) { - throw new Error(`Target ${target} not found in config.`); -} - -// generate the main component code -const ejsPath = './circuits/ejs/template.circom'; -let circuit = ejs.render(readFileSync(ejsPath).toString(), config[target]); - -// output to file -const targetPath = `./circuits/${dir}/${target}.circom`; -writeFileSync(targetPath, circuit); -console.log(`Main component created at: ${targetPath}\n`); diff --git a/tests/fibonacci.test.ts b/tests/fibonacci.test.ts index 3d5e6c9..a99afae 100644 --- a/tests/fibonacci.test.ts +++ b/tests/fibonacci.test.ts @@ -1,24 +1,22 @@ import {createWasmTester} from '../utils/wasmTester'; -import type {CircuitSignals} from '../types/circuit'; describe('fibonacci_11', () => { - const INPUT: CircuitSignals = { - in: [1, 1], - }; - let circuit: Awaited>; before(async () => { circuit = await createWasmTester('fibonacci_11'); + await circuit.printConstraintCount(); }); it('should compute correctly', async () => { - const witness = await circuit.calculateWitness(INPUT, true); - await circuit.checkConstraints(witness); - const output = { - out: fibonacci(INPUT.in, 11), - }; - await circuit.assertOut(witness, output); + await circuit.expectCorrectAssert( + { + in: [1, 1], + }, + { + out: fibonacci([1, 1], 11), + } + ); }); }); diff --git a/tests/float_add.test.ts b/tests/float_add.test.ts index 81283b7..8032622 100644 --- a/tests/float_add.test.ts +++ b/tests/float_add.test.ts @@ -1,147 +1,100 @@ -import {createWasmTester, printConstraintCount} from '../utils/wasmTester'; -import {assert, expect} from 'chai'; +import {createWasmTester} from '../utils/wasmTester'; // tests adapted from https://github.com/rdi-berkeley/zkp-mooc-lab - describe('fp32', () => { let circuit: Awaited>; before(async () => { circuit = await createWasmTester('fp32'); - await circuit.loadConstraints(); - await printConstraintCount(circuit, 401); + await circuit.printConstraintCount(401); }); it('case I test', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['43', '5'], m: ['11672136', '10566265'], }, - true + {e_out: '43', m_out: '11672136'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '43', m_out: '11672136'}); }); it('case II test 1', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['104', '106'], m: ['12444445', '14159003'], }, - true + {e_out: '107', m_out: '8635057'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '107', m_out: '8635057'}); }); it('case II test 2', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['176', '152'], m: ['16777215', '16777215'], }, - true + {e_out: '177', m_out: '8388608'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '177', m_out: '8388608'}); }); it('case II test 3', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['142', '142'], m: ['13291872', '13291872'], }, - true + {e_out: '143', m_out: '13291872'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '143', m_out: '13291872'}); }); it('one input zero test', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['0', '43'], m: ['0', '10566265'], }, - true + {e_out: '43', m_out: '10566265'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '43', m_out: '10566265'}); }); it('both inputs zero test', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['0', '0'], m: ['0', '0'], }, - true + {e_out: '0', m_out: '0'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '0', m_out: '0'}); }); it('should fail - exponent zero but mantissa non-zero', async () => { - await circuit - .calculateWitness( - { - e: ['0', '0'], - m: ['0', '10566265'], - }, - true - ) - .then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); + await circuit.expectFailedAssert({ + e: ['0', '0'], + m: ['0', '10566265'], + }); }); it('should fail - mantissa >= 2^{p+1}', async () => { - await circuit - .calculateWitness( - { - e: ['0', '43'], - m: ['0', '16777216'], - }, - true - ) - .then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); + await circuit.expectFailedAssert({ + e: ['0', '43'], + m: ['0', '16777216'], + }); }); it('should fail - mantissa < 2^{p}', async () => { - await circuit - .calculateWitness( - { - e: ['0', '43'], - m: ['0', '6777216'], - }, - true - ) - .then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); + await circuit.expectFailedAssert({ + e: ['0', '43'], + m: ['0', '6777216'], + }); }); it('should fail - exponent >= 2^k', async () => { - await circuit - .calculateWitness( - { - e: ['0', '256'], - m: ['0', '10566265'], - }, - true - ) - .then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); + await circuit.expectFailedAssert({ + e: ['0', '256'], + m: ['0', '10566265'], + }); }); }); @@ -150,109 +103,80 @@ describe('fp64', () => { before(async () => { circuit = await createWasmTester('fp64'); - await circuit.loadConstraints(); - await printConstraintCount(circuit, 819); + await circuit.printConstraintCount(819); }); it('case I test', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['1122', '1024'], m: ['7807742059002284', '7045130465601185'], }, - true + {e_out: '1122', m_out: '7807742059002284'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '1122', m_out: '7807742059002284'}); }); it('case II test 1', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['1056', '1053'], m: ['8879495032259305', '5030141535601637'], }, - true + {e_out: '1057', m_out: '4754131362104755'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '1057', m_out: '4754131362104755'}); }); it('case II test 2', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['1035', '982'], m: ['4804509148660890', '8505192799372177'], }, - true + {e_out: '1035', m_out: '4804509148660891'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '1035', m_out: '4804509148660891'}); }); it('case II test 3', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['982', '982'], m: ['8505192799372177', '8505192799372177'], }, - true + {e_out: '983', m_out: '8505192799372177'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '983', m_out: '8505192799372177'}); }); it('one input zero test', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['0', '982'], m: ['0', '8505192799372177'], }, - true + {e_out: '982', m_out: '8505192799372177'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '982', m_out: '8505192799372177'}); }); it('both inputs zero test', async () => { - const witness = await circuit.calculateWitness( + await circuit.expectCorrectAssert( { e: ['0', '0'], m: ['0', '0'], }, - true + {e_out: '0', m_out: '0'} ); - await circuit.checkConstraints(witness); - await circuit.assertOut(witness, {e_out: '0', m_out: '0'}); }); it('should fail - exponent zero but mantissa non-zero', async () => { - await circuit - .calculateWitness( - { - e: ['0', '0'], - m: ['0', '8505192799372177'], - }, - true - ) - .then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); + await circuit.expectFailedAssert({ + e: ['0', '0'], + m: ['0', '8505192799372177'], + }); }); it('should fail - mantissa < 2^{p}', async () => { - await circuit - .calculateWitness( - { - e: ['0', '43'], - m: ['0', '16777216'], - }, - true - ) - .then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); + await circuit.expectFailedAssert({ + e: ['0', '43'], + m: ['0', '16777216'], + }); }); }); diff --git a/tests/multiplier.test.ts b/tests/multiplier.test.ts index 7c1e675..01d2630 100644 --- a/tests/multiplier.test.ts +++ b/tests/multiplier.test.ts @@ -2,31 +2,34 @@ import {createWasmTester} from '../utils/wasmTester'; import {ProofTester} from '../utils/proofTester'; import type {CircuitSignals, FullProof} from '../types/circuit'; import {assert, expect} from 'chai'; +import {instantiate, clearInstance} from '../utils/instantiate'; // read inputs from file -import input80 from '../inputs/multiplier3/80.json'; +import input80 from '../inputs/multiplier_3/80.json'; -const CIRCUIT_NAME = 'multiplier3'; -describe(CIRCUIT_NAME, () => { +describe('multiplier_3', () => { const INPUT: CircuitSignals = input80; let circuit: Awaited>; before(async () => { - circuit = await createWasmTester(CIRCUIT_NAME); + instantiate('multiplier_3', 'test', { + file: 'multiplier', + template: 'Multiplier', + publicInputs: [], + templateParams: [3], + }); + circuit = await createWasmTester('multiplier_3', 'test'); + await circuit.printConstraintCount(2); // N - 1 + }); + + after(() => { + clearInstance('multiplier_3', 'test'); }); it('should compute correctly', async () => { - // compute witness - const witness = await circuit.calculateWitness(INPUT, true); - - // witness should have valid constraints - await circuit.checkConstraints(witness); - - // witness should have correct output - const output = { + await circuit.expectCorrectAssert(INPUT, { out: BigInt(INPUT.in.reduce((prev: bigint, acc: bigint) => acc * prev)), - }; - await circuit.assertOut(witness, output); + }); }); it('should NOT compute with wrong number of inputs', async () => { @@ -45,13 +48,14 @@ describe(CIRCUIT_NAME, () => { }); // you can also test prover & verifier functions using the actual build files! -describe('multiplier3 (proofs)', () => { +describe.skip('multiplier_3 (proofs)', () => { const INPUT: CircuitSignals = input80; let fullProof: FullProof; - const circuit = new ProofTester('multiplier3'); + let circuit: ProofTester; before(async () => { + circuit = new ProofTester('multiplier_3'); fullProof = await circuit.prove(INPUT); }); diff --git a/tests/sudoku.test.ts b/tests/sudoku.test.ts index c733d49..61e4208 100644 --- a/tests/sudoku.test.ts +++ b/tests/sudoku.test.ts @@ -1,5 +1,4 @@ import {createWasmTester} from '../utils/wasmTester'; -import {assert, expect} from 'chai'; const INPUTS = { sudoku_9x9: { @@ -46,7 +45,6 @@ const INPUTS = { describe(circuitName, () => { // @ts-ignore const INPUT = INPUTS[circuitName]; - let circuit: Awaited>; before(async () => { @@ -54,58 +52,37 @@ const INPUTS = { }); it('should compute correctly', async () => { - const witness = await circuit.calculateWitness(INPUT, true); - await circuit.checkConstraints(witness); + await circuit.expectCorrectAssert(INPUT); }); it('should NOT accept non-distinct rows', async () => { const badInput = JSON.parse(JSON.stringify(INPUT)); - badInput.solution[0][0] = badInput.solution[0][1]; - await circuit.calculateWitness(badInput, true).then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); + await circuit.expectFailedAssert(badInput); }); it('should NOT accept non-distinct columns', async () => { const badInput = JSON.parse(JSON.stringify(INPUT)); - badInput.solution[0][0] = badInput.solution[1][0]; - await circuit.calculateWitness(badInput, true).then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); + await circuit.expectFailedAssert(badInput); }); it('should NOT accept non-distinct square', async () => { - const badInput: typeof INPUT = JSON.parse(JSON.stringify(INPUT)); - + const badInput = JSON.parse(JSON.stringify(INPUT)); badInput.solution[0][0] = badInput.solution[1][1]; - await circuit.calculateWitness(badInput, true).then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); + await circuit.expectFailedAssert(badInput); }); it('should NOT accept empty value in solution', async () => { const badInput = JSON.parse(JSON.stringify(INPUT)); - badInput.solution[0][0] = 0; - await circuit.calculateWitness(badInput, true).then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); + await circuit.expectFailedAssert(badInput); }); it('should NOT accept out-of-range values', async () => { const badInput = JSON.parse(JSON.stringify(INPUT)); - badInput.solution[0][0] = 99999; - await circuit.calculateWitness(badInput, true).then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); + await circuit.expectFailedAssert(badInput); }); }) ); diff --git a/tsconfig.json b/tsconfig.json index f6ece09..bc20961 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "strict": true, "resolveJsonModule": true, "esModuleInterop": true, + "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "node", diff --git a/types/circuit.ts b/types/circuit.ts index 0c7851d..6faed0c 100644 --- a/types/circuit.ts +++ b/types/circuit.ts @@ -19,35 +19,40 @@ export type FullProof = { }; /** - * Configuration file for your circuits. + * A configuration object for circuit main components. + */ +export type CircuitConfig = { + /** + * File to read the template from + */ + file: string; + + /** + * The template name to instantiate + */ + template: string; + + /** + * An array of public input signal names + */ + publicInputs: string[]; + + /** + * An array of template parameters + */ + templateParams: (number | bigint)[]; + + /** + * Directory to output under `circuits`, defaults to `main` + * @depracated work in progress, use `main` for now (leave empty) + */ + dir?: string; +}; + +/** + * Configurations for your circuits. * @see `circuit.config.cjs` in the project root. */ export type Config = { - [circuitName: string]: { - /** - * File to read the template from - */ - file: string; - - /** - * The template name to instantiate - */ - template: string; - - /** - * An array of public input signal names - */ - publicInputs: string[]; - - /** - * An array of template parameters - */ - templateParams: (number | bigint)[]; - - /** - * Directory to output under `circuits`, defaults to `main` - * @depracated work in progress, use `main` for now (leave empty) - */ - dir?: string; - }; + [circuitName: string]: CircuitConfig; }; diff --git a/types/wasmTester.ts b/types/wasmTester.ts new file mode 100644 index 0000000..394d545 --- /dev/null +++ b/types/wasmTester.ts @@ -0,0 +1,18 @@ +import {WitnessType, CircuitSignals} from './circuit'; + +/** + * A simple type-wrapper for `circom_tester` WASM tester class. + * Not all functions may exist here, some are omitted. + * @see https://github.com/iden3/circom_tester/blob/main/wasm/tester.js + */ +export type CircomWasmTester = { + checkConstraints: (witness: WitnessType) => Promise; + release: () => Promise; + assertOut: (actualOut: CircuitSignals, expectedOut: CircuitSignals) => Promise; + calculateWitness: (input: CircuitSignals, sanityCheck: boolean) => Promise; + loadConstraints: () => Promise; + constraints: any[] | undefined; + loadSymbols: () => Promise; + symbols: object | undefined; + getDecoratedOutput: (witness: WitnessType) => Promise; +}; diff --git a/utils/instantiate.ts b/utils/instantiate.ts new file mode 100644 index 0000000..79050f5 --- /dev/null +++ b/utils/instantiate.ts @@ -0,0 +1,36 @@ +import ejs from 'ejs'; +import {writeFileSync, readFileSync} from 'fs'; +import config from '../circuit.config'; +import {CircuitConfig} from '../types/circuit'; + +/** + * Programmatically generate the `main` component + */ +export function instantiate(name: string, directory: string, circuitConfig?: CircuitConfig) { + if (circuitConfig == undefined) { + if (!(name in config)) { + throw new Error(`Target ${name} not found in circuit.config.cjs`); + } + circuitConfig = config[name]; + } + + // generate the main component code + const ejsPath = './circuits/ejs/template.circom'; + let circuit = ejs.render(readFileSync(ejsPath).toString(), circuitConfig); + + // output to file + const targetDir = directory || 'main'; + const targetPath = `./circuits/${targetDir}/${name}.circom`; + writeFileSync(targetPath, circuit); + console.log(`Main component created at: ${targetPath}\n`); +} + +export function clearInstance(name: string, directory: string) { + // TODO: remove the file +} + +if (require.main === module) { + const name = process.argv[2]; + const directory = process.argv[3]; + instantiate(name, directory); +} diff --git a/utils/wasmTester.ts b/utils/wasmTester.ts index 3a719f6..0ea03ef 100644 --- a/utils/wasmTester.ts +++ b/utils/wasmTester.ts @@ -1,46 +1,65 @@ const wasm_tester = require('circom_tester').wasm; import {WitnessType, CircuitSignals} from '../types/circuit'; +import {CircomWasmTester} from '../types/wasmTester'; +import {assert, expect} from 'chai'; /** - * Custom types added with respect to `circomlibjs.wasm`. Not all functions exist here, some are omitted. - * @see https://github.com/iden3/circom_tester/blob/main/wasm/tester.js + A utility class to test your circuits. + - Use `expectFailedAssert` and `expectCorrectAssert` to test out evaluations */ -type WasmTester = { +class WasmTester { + /** + * The underlying `circom_tester` object + */ + readonly circomWasmTester: CircomWasmTester; + + /** + * A dictionary of symbols + */ + symbols: object | undefined; + + /** + * List of constraints, must call `loadConstraints` before accessing this key + */ + constraints: any[] | undefined; + + constructor(circomWasmTester: CircomWasmTester) { + this.circomWasmTester = circomWasmTester; + } + /** * Assert that constraints are valid. * @param witness witness */ - checkConstraints: (witness: WitnessType) => Promise; - - /** - * Cleanup directory, should probably be called upon test completion - * @deprecated this is buggy right now - */ - release(): Promise; + checkConstraints(witness: WitnessType): Promise { + return this.circomWasmTester.checkConstraints(witness); + } /** * Assert the output of a given witness. * @param actualOut expected output signals * @param expectedOut computed output signals */ - assertOut: (actualOut: CircuitSignals, expectedOut: CircuitSignals) => Promise; + assertOut(actualOut: CircuitSignals, expectedOut: CircuitSignals): Promise { + return this.circomWasmTester.assertOut(actualOut, expectedOut); + } /** * Compute witness given the input signals. * @param input all signals, private and public. * @param sanityCheck check if input signals are sanitized */ - calculateWitness: (input: CircuitSignals, sanityCheck: boolean) => Promise; + calculateWitness(input: CircuitSignals, sanityCheck: boolean): Promise { + return this.circomWasmTester.calculateWitness(input, sanityCheck); + } /** * Loads the list of R1CS constraints to `this.constraints` */ - loadConstraints(): Promise; - - /** - * List of constraints, must call `loadConstraints` before accessing this key - */ - constraints: any[] | undefined; + async loadConstraints(): Promise { + await this.circomWasmTester.loadConstraints(); + this.constraints = this.circomWasmTester.constraints; + } /** * Loads the symbols in a dictionary at `this.symbols` @@ -50,19 +69,80 @@ type WasmTester = { * 1: variable index * 2: component index */ - loadSymbols(): Promise; - - /** - * A dictionary of symbols - */ - symbols: object; + async loadSymbols(): Promise { + await this.circomWasmTester.loadSymbols(); + this.symbols = this.circomWasmTester.symbols; + } /** * @deprecated this is buggy right now * @param witness witness */ - getDecoratedOutput(witness: WitnessType): Promise; -}; + getDecoratedOutput(witness: WitnessType): Promise { + return this.circomWasmTester.getDecoratedOutput(witness); + } + + /** + * Cleanup directory, should probably be called upon test completion + * @deprecated this is buggy right now + */ + release(): Promise { + return this.circomWasmTester.release(); + } + + //////// CUSTOM ADDITIONS ///////// + + /** + * Prints the number of constraints of the circuit. + * If expected count is provided, will also include that in the log. + * @param circuit WasmTester circuit + * @param expected expected number of constraints + */ + async printConstraintCount(expected?: number) { + // load constraints + if (this.constraints == undefined) { + await this.loadConstraints(); + } + const numConstraints = this.constraints!.length; + let expectionMessage = ''; + if (expected !== undefined) { + let alertType = ''; + if (numConstraints < expected) { + alertType = '🔴'; + } else if (numConstraints > expected) { + alertType = '🟡'; + } else { + alertType = '🟢'; + } + expectionMessage = ` (${alertType} expected ${expected})`; + } + console.log(`#constraints: ${numConstraints}` + expectionMessage); + } + + /** + * Expect an input to fail an assertion in the circuit. + * @param input bad input + */ + async expectFailedAssert(input: CircuitSignals) { + await this.calculateWitness(input, true).then( + () => assert.fail(), + err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') + ); + } + + /** + * Expect an input to pass assertions and match the output. + * @param input correct input + * @param output expected output, if `undefined` it will only check constraints + */ + async expectCorrectAssert(input: CircuitSignals, output?: CircuitSignals) { + const witness = await this.calculateWitness(input, true); + await this.checkConstraints(witness); + if (output) { + await this.assertOut(witness, output); + } + } +} /** * Compiles and reutrns a circuit via `circom_tester`'s `wasm_tester`. @@ -72,34 +152,8 @@ type WasmTester = { * @returns a `wasm_tester` object */ export async function createWasmTester(circuitName: string, dir: string = 'main'): Promise { - return wasm_tester(`./circuits/${dir}/${circuitName}.circom`, { + const circomWasmTester: CircomWasmTester = await wasm_tester(`./circuits/${dir}/${circuitName}.circom`, { include: 'node_modules', // will link circomlib circuits }); -} - -/** - * Prints the number of constraints of the circuit. - * If expected count is provided, will also include that in the log. - * @param circuit WasmTester circuit - * @param expected expected number of constraints - */ -export async function printConstraintCount(circuit: WasmTester, expected?: number) { - // load constraints - if (!circuit.constraints) { - await circuit.loadConstraints(); - } - const numConstraints = circuit.constraints!.length; - let expectionMessage = ''; - if (expected !== undefined) { - let alertType = ''; - if (numConstraints < expected) { - alertType = '🔴'; - } else if (numConstraints > expected) { - alertType = '🟡'; - } else { - alertType = '🟢'; - } - expectionMessage = ` (${alertType} expected ${expected})`; - } - console.log(`#constraints: ${numConstraints}` + expectionMessage); + return new WasmTester(circomWasmTester); } diff --git a/yarn.lock b/yarn.lock index c09b6ce..0430871 100644 --- a/yarn.lock +++ b/yarn.lock @@ -485,6 +485,11 @@ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4" integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== +"@types/ejs@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.2.tgz#75d277b030bc11b3be38c807e10071f45ebc78d9" + integrity sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g== + "@types/json-schema@^7.0.7": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"