From 40e5c034ed2fe122cd3a9d6979ba4d559949fd12 Mon Sep 17 00:00:00 2001 From: Erhan Tezcan Date: Sun, 2 Apr 2023 17:47:15 +0300 Subject: [PATCH] sudok & fp-add mostly done --- README.md | 7 + circuit.config.cjs | 11 +- circuit.config.d.ts | 32 ----- circuits/float_add.circom | 19 ++- circuits/main/multiplier3.circom | 6 - circuits/main/sudoku_4x4.circom | 6 + circuits/sudoku.circom | 58 ++++---- tests/fibonacci.test.ts | 33 +++-- tests/float_add.test.ts | 238 +++++++++++++++++++++++++++++-- tests/multiplier.proofs.test.ts | 5 +- tests/sudoku.test.ts | 128 ++++++++++------- types/circuit.ts | 34 +++++ utils/wasmTester.ts | 41 ++++-- 13 files changed, 445 insertions(+), 173 deletions(-) delete mode 100644 circuit.config.d.ts delete mode 100644 circuits/main/multiplier3.circom create mode 100644 circuits/main/sudoku_4x4.circom diff --git a/README.md b/README.md index 2ba646d..3ebb94f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ > 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: + +- **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. + ## Usage Clone the repository or create a new one with this as the template! You need [Circom](https://docs.circom.io/getting-started/installation/) to compile circuits. Other than that, just `yarn` or `npm install` to get started. It will also install [Circomlib](https://github.com/iden3/circomlib/tree/master/circuits) which has many utility circuits. diff --git a/circuit.config.cjs b/circuit.config.cjs index 4e1d8f0..b483c91 100644 --- a/circuit.config.cjs +++ b/circuit.config.cjs @@ -1,9 +1,9 @@ /** - * @type {import("./circuit.config").Config} + * @type {import("./types/circuit").Config} */ const config = { // multiplication of 3 numbers - multiplier3: { + multiplier_3: { file: 'multiplier', template: 'Multiplier', publicInputs: [], @@ -16,6 +16,13 @@ const config = { publicInputs: ['puzzle'], templateParams: [Math.sqrt(9)], }, + // A 4x4 sudoku board + sudoku_4x4: { + file: 'sudoku', + template: 'Sudoku', + publicInputs: ['puzzle'], + templateParams: [Math.sqrt(4)], + }, // 64-bit floating point, 11-bit exponent and 52-bit mantissa fp64: { file: 'float_add', diff --git a/circuit.config.d.ts b/circuit.config.d.ts deleted file mode 100644 index d77afd3..0000000 --- a/circuit.config.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Configuration file for your circuits. - */ -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; - }; -}; diff --git a/circuits/float_add.circom b/circuits/float_add.circom index ebf4bdf..bcf7b1a 100644 --- a/circuits/float_add.circom +++ b/circuits/float_add.circom @@ -101,12 +101,12 @@ template RightShift(b, shift) { // do the shifting signal y_bits[b-shift]; for (var i = 0; i < b-shift; i++) { - y_bits[i] <== x_bits.bits[shift+i]; + y_bits[i] <== x_bits.out[shift+i]; } // convert shifted bits to number component y_num = Bits2Num(b-shift); - y_num.bits <== y_bits; + y_num.in <== y_bits; y <== y_num.out; } @@ -159,15 +159,15 @@ template RoundAndCheck(k, p, P) { template Num2BitsWithSkipChecks(b) { signal input in; signal input skip_checks; - signal output bits[b]; + signal output out[b]; for (var i = 0; i < b; i++) { - bits[i] <-- (in >> i) & 1; - bits[i] * (1 - bits[i]) === 0; + out[i] <-- (in >> i) & 1; + out[i] * (1 - out[i]) === 0; } var sum_of_bits = 0; for (var i = 0; i < b; i++) { - sum_of_bits += (2 ** i) * bits[i]; + sum_of_bits += (2 ** i) * out[i]; } // is always true if skip_checks is 1 @@ -182,9 +182,8 @@ template LessThanWithSkipChecks(n) { component n2b = Num2BitsWithSkipChecks(n+1); n2b.in <== in[0] + (1< { +describe('fibonacci_11', () => { const INPUT: CircuitSignals = { in: [1, 1], }; @@ -24,7 +9,7 @@ describe(CIRCUIT_NAME, () => { let circuit: Awaited>; before(async () => { - circuit = await createWasmTester(CIRCUIT_NAME); + circuit = await createWasmTester('fibonacci_11'); }); it('should compute correctly', async () => { @@ -41,3 +26,17 @@ describe(CIRCUIT_NAME, () => { await circuit.assertOut(witness, output); }); }); + +// simple fibonacci with 2 variables +function fibonacci(init: [number, number], n: number): number { + if (n < 0) { + throw new Error('N must be positive'); + } + + let [a, b] = init; + for (let i = 2; i <= n; i++) { + b = a + b; + a = b - a; + } + return n == 0 ? a : b; +} diff --git a/tests/float_add.test.ts b/tests/float_add.test.ts index 06aa14c..918232e 100644 --- a/tests/float_add.test.ts +++ b/tests/float_add.test.ts @@ -1,27 +1,245 @@ -import {createWasmTester} from '../utils/wasmTester'; +import {createWasmTester, printConstraintCount} from '../utils/wasmTester'; import {ProofTester} from '../utils/proofTester'; import type {CircuitSignals, FullProof} from '../types/circuit'; import {assert, expect} from 'chai'; -// TODO: write tests -const CIRCUIT_NAME = 'cbl_32'; -describe('utils', () => { +describe('fp32', () => { let circuit: Awaited>; before(async () => { - circuit = await createWasmTester(CIRCUIT_NAME, 'test'); + circuit = await createWasmTester('fp32'); + await circuit.loadConstraints(); + await printConstraintCount(circuit, 401); }); - it('should compute correctly', async () => { + it('case I test', async () => { const witness = await circuit.calculateWitness( { - in: 3, + e: ['43', '5'], + m: ['11672136', '10566265'], }, true ); await circuit.checkConstraints(witness); - await circuit.assertOut(witness, { - out: 1, - }); + await circuit.assertOut(witness, {e_out: '43', m_out: '11672136'}); + }); + + it('case II test 1', async () => { + const witness = await circuit.calculateWitness( + { + e: ['104', '106'], + m: ['12444445', '14159003'], + }, + true + ); + 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( + { + e: ['176', '152'], + m: ['16777215', '16777215'], + }, + true + ); + 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( + { + e: ['142', '142'], + m: ['13291872', '13291872'], + }, + true + ); + 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( + { + e: ['0', '43'], + m: ['0', '10566265'], + }, + true + ); + 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( + { + e: ['0', '0'], + m: ['0', '0'], + }, + true + ); + 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.') + ); + }); + + 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.') + ); + }); + + 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.') + ); + }); + + 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.') + ); }); }); + +// describe('FP64Add', () => { +// var circ_file = path.join(__dirname, 'circuits', 'fp64_add.circom'); +// var circ, num_constraints; + +// before(async () => { +// circ = await wasm_tester(circ_file); +// await circuit.loadConstraints(); +// num_constraints = circuit.constraints.length; +// console.log('Float64 Add #Constraints:', num_constraints, 'Expected:', 819); +// }); + +// it('case I test', async () => { +// const input = { +// e: ['1122', '1024'], +// m: ['7807742059002284', '7045130465601185'], +// }; +// const witness = await circuit.calculateWitness(input, 1); +// await circuit.checkConstraints(witness); +// await circuit.assertOut(witness, {e_out: '1122', m_out: '7807742059002284'}); +// }); + +// it('case II test 1', async () => { +// const input = { +// e: ['1056', '1053'], +// m: ['8879495032259305', '5030141535601637'], +// }; +// const witness = await circuit.calculateWitness(input); +// await circuit.checkConstraints(witness); +// await circuit.assertOut(witness, {e_out: '1057', m_out: '4754131362104755'}); +// }); + +// it('case II test 2', async () => { +// const input = { +// e: ['1035', '982'], +// m: ['4804509148660890', '8505192799372177'], +// }; +// const witness = await circuit.calculateWitness(input); +// await circuit.checkConstraints(witness); +// await circuit.assertOut(witness, {e_out: '1035', m_out: '4804509148660891'}); +// }); + +// it('case II test 3', async () => { +// const input = { +// e: ['982', '982'], +// m: ['8505192799372177', '8505192799372177'], +// }; +// const witness = await circuit.calculateWitness(input); +// await circuit.checkConstraints(witness); +// await circuit.assertOut(witness, {e_out: '983', m_out: '8505192799372177'}); +// }); + +// it('one input zero test', async () => { +// const input = { +// e: ['0', '982'], +// m: ['0', '8505192799372177'], +// }; +// const witness = await circuit.calculateWitness(input); +// await circuit.checkConstraints(witness); +// await circuit.assertOut(witness, {e_out: '982', m_out: '8505192799372177'}); +// }); + +// it('both inputs zero test', async () => { +// const input = { +// e: ['0', '0'], +// m: ['0', '0'], +// }; +// const witness = await circuit.calculateWitness(input); +// await circuit.checkConstraints(witness); +// await circuit.assertOut(witness, {e_out: '0', m_out: '0'}); +// }); + +// it('should fail - exponent zero but mantissa non-zero', async () => { +// const input = { +// e: ['0', '0'], +// m: ['0', '8505192799372177'], +// }; +// try { +// const witness = await circuit.calculateWitness(input); +// } catch (e) { +// return 0; +// } +// assert.fail('should have thrown an error'); +// }); + +// it('should fail - mantissa < 2^{p}', async () => { +// const input = { +// e: ['0', '43'], +// m: ['0', '16777216'], +// }; +// try { +// const witness = await circuit.calculateWitness(input); +// } catch (e) { +// return 0; +// } +// assert.fail('should have thrown an error'); +// }); + +// }); diff --git a/tests/multiplier.proofs.test.ts b/tests/multiplier.proofs.test.ts index 6fe9439..60b14a6 100644 --- a/tests/multiplier.proofs.test.ts +++ b/tests/multiplier.proofs.test.ts @@ -4,12 +4,11 @@ import {assert, expect} from 'chai'; // read inputs from file import input80 from '../inputs/multiplier3/80.json'; -const CIRCUIT_NAME = 'multiplier3'; -describe(CIRCUIT_NAME + ' (proofs)', () => { +describe('multiplier3 (proofs)', () => { const INPUT: CircuitSignals = input80; let fullProof: FullProof; - const circuit = new ProofTester(CIRCUIT_NAME); + const circuit = new ProofTester('multiplier3'); before(async () => { fullProof = await circuit.prove(INPUT); diff --git a/tests/sudoku.test.ts b/tests/sudoku.test.ts index a9e3790..88740c1 100644 --- a/tests/sudoku.test.ts +++ b/tests/sudoku.test.ts @@ -1,9 +1,8 @@ import {createWasmTester} from '../utils/wasmTester'; import {assert, expect} from 'chai'; -const CIRCUIT_NAME = 'sudoku_9x9'; -describe(CIRCUIT_NAME, () => { - const INPUT = { +const INPUTS = { + sudoku_9x9: { solution: [ [1, 9, 4, 8, 6, 5, 2, 3, 7], [7, 3, 5, 4, 1, 2, 9, 6, 8], @@ -26,63 +25,90 @@ describe(CIRCUIT_NAME, () => { [0, 4, 0, 1, 0, 9, 0, 8, 0], [5, 0, 7, 0, 8, 0, 0, 9, 4], ], - }; + }, + sudoku_4x4: { + solution: [ + [4, 1, 3, 2], + [3, 2, 4, 1], + [2, 4, 1, 3], + [1, 3, 2, 4], + ], + puzzle: [ + [0, 1, 0, 2], + [3, 2, 0, 0], + [0, 0, 1, 0], + [1, 0, 0, 0], + ], + }, +}; - let circuit: Awaited>; +['sudoku_9x9', 'sudoku_4x4'].map(circuitName => + describe(circuitName, () => { + // @ts-ignore + const INPUT = INPUTS[circuitName]; - before(async () => { - circuit = await createWasmTester(CIRCUIT_NAME); - }); + let circuit: Awaited>; - it('should compute correctly', async () => { - // compute witness - const witness = await circuit.calculateWitness(INPUT, true); + before(async () => { + circuit = await createWasmTester(circuitName); + }); - // witness should have valid constraints - await circuit.checkConstraints(witness); - }); + it('should compute correctly', async () => { + // compute witness + const witness = await circuit.calculateWitness(INPUT, true); - it('should NOT accept non-distinct rows', async () => { - const badInput = JSON.parse(JSON.stringify(INPUT)); + // witness should have valid constraints + await circuit.checkConstraints(witness); + }); - badInput.solution[0][0] = badInput.solution[0][1]; - console.log(badInput.solution[0], badInput.solution[1]); - await circuit.calculateWitness(INPUT, true).then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); - }); + it('should NOT accept non-distinct rows', async () => { + const badInput = JSON.parse(JSON.stringify(INPUT)); - it('should NOT accept non-distinct columns', 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.') + ); + }); - badInput.solution[0][0] = badInput.solution[1][0]; - console.log(badInput.solution[0], badInput.solution[1]); - await circuit.calculateWitness(INPUT, true).then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); - }); + it('should NOT accept non-distinct columns', async () => { + const badInput = JSON.parse(JSON.stringify(INPUT)); - it('should NOT accept non-distinct square', async () => { - const badInput: typeof INPUT = 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.') + ); + }); - badInput.solution[0][0] = badInput.solution[1][1]; - console.log(badInput.solution[0], badInput.solution[1]); - await circuit.calculateWitness(INPUT, true).then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); - }); + it('should NOT accept non-distinct square', async () => { + const badInput: typeof INPUT = JSON.parse(JSON.stringify(INPUT)); - it('should NOT accept empty value in solution', async () => { - 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.') + ); + }); - badInput.solution[0][0] = 0; - console.log(badInput.solution[0], badInput.solution[1]); - await circuit.calculateWitness(badInput, true).then( - () => assert.fail(), - err => expect(err.message.slice(0, 21)).to.eq('Error: Assert Failed.') - ); - }); -}); + 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.') + ); + }); + + 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.') + ); + }); + }) +); diff --git a/types/circuit.ts b/types/circuit.ts index 1ccd644..0c7851d 100644 --- a/types/circuit.ts +++ b/types/circuit.ts @@ -17,3 +17,37 @@ export type FullProof = { proof: object; publicSignals: string[]; }; + +/** + * Configuration file 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; + }; +}; diff --git a/utils/wasmTester.ts b/utils/wasmTester.ts index 6d839df..98a138e 100644 --- a/utils/wasmTester.ts +++ b/utils/wasmTester.ts @@ -28,7 +28,7 @@ type WasmTester = { /** * Compute witness given the input signals. * @param input all signals, private and public. - * @param sanityCheck ? + * @param sanityCheck check if input signals are sanitized */ calculateWitness: (input: CircuitSignals, sanityCheck: boolean) => Promise; @@ -71,19 +71,32 @@ type WasmTester = { * @param showNumConstraints print number of constraints, defualts to `false` * @returns a `wasm_tester` object */ -export async function createWasmTester( - circuitName: string, - dir: string = 'main', - showNumConstraints: boolean = false -): Promise { - const circuit = await wasm_tester(`./circuits/${dir}/${circuitName}.circom`, { +export async function createWasmTester(circuitName: string, dir: string = 'main'): Promise { + return wasm_tester(`./circuits/${dir}/${circuitName}.circom`, { include: 'node_modules', // will link circomlib circuits }); - - if (showNumConstraints) { - await circuit.loadConstraints(); - console.log(' number of constraints:', circuit.constraints!.length); - } - - return circuit; +} + +/** + * 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) { + 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); }