erhant.eth a06ebb4771 Merge pull request #10 from erhant/erhant/more-packaging
Circomkit is now a package, not a template repo!
2023-06-03 20:58:15 +03:00
2023-03-31 00:10:49 +03:00
2023-06-02 00:21:35 +03:00
2023-06-03 20:55:23 +03:00
2023-06-03 20:41:49 +03:00
2023-02-15 21:45:11 +03:00
2023-05-31 21:46:47 +03:00
2023-06-03 20:41:49 +03:00
2023-06-03 19:06:12 +03:00
2023-06-03 19:06:12 +03:00
2023-02-15 21:45:11 +03:00
2023-06-03 20:41:49 +03:00
2023-02-15 21:45:11 +03:00
2023-06-03 20:56:29 +03:00
2023-06-03 20:41:49 +03:00
2023-06-02 00:21:35 +03:00
2023-06-03 20:41:49 +03:00

Circomkit

A simple-to-use & opinionated circuit development & testing toolkit.

NPM Workflow: Styles Workflow: Build GitHub: SnarkJS GitHub: Circom

  • Programmable Circuits: The main component is created & compiled programmatically.
  • Simple CLI: A straightforward CLI is provided as a wrapper around SnarkJS commands, exposed via NPM scripts!
  • Easily Configurable: A single circomkit.env file stores the general configuration settings.
  • Constraint Testing: You can test computations & assertions for every template in a circuit, with minimal code-repetition.
  • Proof Testing: With prover & verification keys and the WASM circuit, you can test proof generation & verification.
  • Witness Manipulation: You can parse the output from a witness, and furthermore create fake witnesses to try and fool the verifier.
  • Type-safe: Witness & proof testers, as well as circuit signal inputs & outputs are all type-safe via generics.
  • Solidity Exports: Export a verifier contract in Solidity, or export a calldata for your proofs & public signals.

Usage

Using Circomkit is easy:

  1. Install Circom.
  2. Clone this repo (or use it as a template) and install packages (yarn or npm install).
  3. Write your circuit templates under the circuits folder. Your circuit code itself should be templates only; Circomkit programmatically generates the main component
  4. Write your tests under the tests folder.
  5. Once you are ready, write the circuit configurations at circuits.json.
  6. Use NPM scripts (yarn <script> or npm run <script>) to compile your circuit, build keys, generate & verify proofs and much more!

A circuit config looks like this:

// the key is <circuit-name>
sudoku_4x4: {
  file:      'sudoku',       // file name (circuits/sudoku.circom)
  template: 'Sudoku',       // template name
  pubs:     ['puzzle'],     // public signals
  params:   [Math.sqrt(4)], // template parameters
},

You can omit pubs and params options, they default to []. Afterwards, you can use the following commands:

# Compile the circuit (generates the main component & compiles it)
npx circomkit compile circuit-name

# Circuit setup
npx circomkit setup circuit-name -p phase1-ptau-path

# Create a Solidity verifier contract
npx circomkit contract circuit-name

# Clean circuit artifacts
npx circomkit clean circuit-name

# Generate the `main` component without compiling it afterwards
npx circomkit instantiate circuit-name

You can change some general settings such as the configured proof system or the prime field under circomkit.env.

Working with Input Signals

Some actions such as generating a witness, generating a proof and verifying a proof require JSON inputs to provide the signal values. For that, we specifically create our input files under the inputs folder, and under the target circuit name there. For example, an input named foobar for some circuit named circ would be at inputs/circ/foobar.json.

# Generate a witness for some input
npx circomkit witness circuit-name [-i input-name (default: "default")]

# Generate a proof for some input
npx circomkit prove circuit-name [-i input-name (default: "default")]

# Verify a proof for some input (public signals only)
npx circomkit verify circuit-name [-i input-name (default: "default")]

# Debug a witness of some input
npx circomkit debug circuit-name input-name

# Export calldata to call your Solidity verifier contract
npx circomkit calldata circuit-name [-i input-name (default: "default")]

Example Circuits

We have several example circuits that you can check out. With them, you can prove the following statements:

  • Multiplier: "I know n factors that make up some number".
  • Fibonacci: "I know the n'th Fibonacci number".
  • SHA256: "I know the n-byte preimage of some SHA256 digest".
  • Sudoku: "I know the solution to some (n^2)x(n^2) Sudoku puzzle".
  • Floating-Point Addition: "I know two floating-point numbers that make up some number with e exponent and m mantissa bits." (adapted from Berkeley ZKP MOOC 2023 - Lab 1).

Witness Calculation

Witness calculation tests check whether your circuit computes the correct result based on your inputs, and makes sure that assertions are correct. We provide very useful utility functions to help write these tests.

import WasmTester from '../utils/wasmTester';

const N = 3;
describe('multiplier', () => {
  // type-safe signal names ✔
  let circuit: WasmTester<['in'], ['out']>;

  before(async () => {
    circuit = await WasmTester.new(`multiplier_${N}`, {
      file: 'multiplier',
      template: 'Multiplier',
      params: [N], // template parameters ✔
      pubs: [], // public signals ✔
    });
    // constraint count checks ✔
    await circuit.checkConstraintCount(N - 1);
  });

  it('should compute correctly', async () => {
    const randomNumbers = Array.from({length: N}, () => Math.floor(Math.random() * 100 * N));
    await circuit.expectPass({in: randomNumbers}, {out: randomNumbers.reduce((prev, acc) => acc * prev)});
  });
});

With the circuit object, we can do the following:

  • circuit.expectPass(input, output) to test whether we get the expected output for some given input.
  • circuit.expectPass(input) to test whether the circuit assertions pass for some given input
  • circuit.expectFail(input) to test whether the circuit assertions pass for some given input

Witness

What if we would just like to see what the output is, instead of comparing it to some witness? Well, that would be a trouble because we would have to parse the witness array (which is huge for some circuits) with respect to which signals the output signals correspond to. Thankfully, Circomkit has a function for that:

const output = await circuit.compute(INPUT, ['foo', 'bar']);
/* {
  foo: [[1n, 2n], [3n, 4n]]
  bar: 42n
} */

Note that this operation requires parsing the symbols file (.sym) and reading the witness array, which may be costly for large circuits. Most of the time, you won't need this for testing; instead, you will likely use it to see what the circuit actually does for debugging.

On top of these, you can create a fake witness by overriding symbols in the witness. This is useful in case you think there is a soundness error and would like to try and generate an adversarial witness.

// correct witness
const witness = await circuit.calculateWitness(INPUT);
// faked witness
const fakeWitness = await circuit.fakeWitness(witness, {
  'symbol-names-here': 42n,
});

Multiple templates

You will often have multiple templates in your circuit code, and you might want to test them in the same test file of your main circuit too. Well, you can!

describe('multiplier utilities', () => {
  describe('multiplication gate', () => {
    let circuit: WasmTester<['in'], ['out']>;

    before(async () => {
      circuit = await WasmTester.new(circuitName, {
        file: 'multiplier',
        template: 'MultiplicationGate',
        dir: 'test/multiplier', // nested paths ✔
      });
    });

    it('should pass for in range', async () => {
      await circuit.expectPass({in: [7, 5]}, {out: 7 * 5});
    });
  });
});

Proof Verification

If you have created the prover key, verification key & the circuit WASM file (which is simply yarn keygen <circuit-name> -p <pptau-path>), you can also test proof generation & verification.

describe('multiplier proofs', () => {
  let fullProof: FullProof;
  let circuit: ProofTester<['in']>;

  before(async () => {
    circuit = new ProofTester(`multiplier_${N}`);
    fullProof = await circuit.prove({
      in: Array.from({length: N}, () => Math.floor(Math.random() * 100 * N)),
    });
  });

  it('should verify', async () => {
    await circuit.expectPass(fullProof.proof, fullProof.publicSignals);
  });

  it('should NOT verify', async () => {
    await circuit.expectFail(fullProof.proof, ['13']);
  });
});

The two utility functions provided here are:

  • circuit.expectPass(proof, publicSignals) that makes sure that the given proof is accepted by the verifier for the given public signals.
  • circuit.expectFail(proof, publicSignals) that makes sure that the given proof is rejected by the verifier for the given public signals.

File Structure

The repository follows an opinionated file structure shown below, abstracting away the pathing and orientation behind the scenes. Circomkit handles most of the work with respect to this structure.

circomkit
├── circuits.json # configs for circuit main components
├── circomkit.env # environment variables for cli
├── circuits # where you write templates
│   ├── main # auto-generated main components
│   │   │── sudoku_9x9.circom # e.g. a 9x9 sudoku board
│   │   └── ...
│   ├── test # auto-generated test components
│   │   └── ...
│   │── sudoku.circom # a generic sudoku circuit template
│   └── ...
├── inputs # where you write JSON inputs per circuit
│   ├── sudoku_9x9 # each main template has its own folder
│   │   ├── example-input.json # e.g. a solution & its puzzle
│   │   └── ...
│   └── ...
├── ptau # universal phase-1 setups
│   ├── powersOfTau28_hez_final_12.ptau
│   └── ...
└── build # build artifacts, these are .gitignore'd
    │── sudoku_9x9 # each main template has its own folder
    │   │── sudoku_9x9_js # artifacts of compilation
    │   │   │── generate_witness.js
    │   │   │── witness_calculator.js
    │   │   └── sudoku_9x9.wasm
    │   │── example-input # artifacts of an input
    │   │   │── proof.json # generated proof object
    │   │   │── public.json # public signals
    │   │   └── witness.wtns # witness file
    │   │── ... # folders for other inputs
    │   │── sudoku_9x9.r1cs
    │   │── sudoku_9x9.sym
    │   │── prover_key.zkey
    │   └── verification_key.json
    └── ...

Testing

To run tests do the following:

# test a specific circuit
yarn test <circuit-name>

# test all circuits
yarn test:all

You can test both witness calculations and proof generation & verification. We describe both in their respective sections, going over an example of "Multiplication" circuit.

Styling

We use Google TypeScript Style Guide for the TypeScript codes.

# check the formatting
yarn format

# lint everything
yarn lint

# do both at once
yarn style
Description
No description provided
Readme MIT 5.9 MiB
Languages
TypeScript 83.3%
JavaScript 12.1%
Circom 4.6%