Circuit-specific setup optionally takes the path to a PTAU file as argument. If not provided, it will automatically decide the PTAU to use with respect to constraint count, and download that for you! This feature only works for bn128 prime, and has an upper-limit of at most $2^{28}$ constraints.
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 foo for some circuit named bar would be at inputs/bar/foo.json.
# Generate a witness
npx circomkit witness <circuit><input># Generate a proof
npx circomkit prove <circuit><input># Verify a proof with public signals
npx circomkit verify <circuit><input># Export Solidity calldata to console
npx circomkit calldata <circuit><input>
Circomkit Configurations
Everything used by Circomkit can be optionally overridden by providing the selected fields in its constructor. Circomkit CLI does this automatically by checking out circomkit.json and overriding the defaults with that. You can print the active configuration via the following command:
npx circomkit config
You can edit any of the fields there to fit your needs. Most importantly, you can change the protocol to be groth16, plonk or fflonk; and you can change the underlying prime field to bn128, bls12381 and goldilocks.
Note
Using a prime other than bn128 makes things a bit harder in circuit-specific setup, as you will have to find the PTAU files yourself, whereas in bn128 we can use Perpetual Powers of Tau.
Circuit Configurations
A circuit config within circuits.json looks like below, where the key is the circuit name to be used in commands, and the value is an object that describes the filename, template name, public signals and template parameters:
The pubs and params options can be omitted, in which case they will default to [].
Using Circomkit in Code
All CLI commands other than init can be used with the same name and arguments within Circomkit. Furthermore, you can provide configuration & inputs directly, instead of letting Circomkit read from circuits.json or from within the inputs folder.
import{Circomkit}from'circomkit';constcircomkit=newCircomkit({// custom configurationsprotocol: 'plonk',});// artifacts output at `build/multiplier_3` directoryawaitcircomkit.compile('multiplier_3',{file: 'multiplier',template: 'Multiplier',params: [3],});// proof & public signals at `build/multiplier_3/my_input` directoryawaitcircomkit.prove('multiplier_3','my_input',{in: [3,5,7]});// verify with proof & public signals at `build/multiplier_3/my_input`awaitcircomkit.verify('multiplier_3','my_input');
Writing Tests
Circomkit provides two tester utilities that use Chai assertions within, which may be used in a test suite such as Mocha. The key point of these utilities is to help reduce boilerplate code and let you simply worry about the inputs and outputs of a circuit.
Witness Tester
The Witness tester extends require('circom_tester').wasm tool with type-safety and few assertion functions. It provides a very simple interface:
expectPass(input) checks if constraints & assertions are passing for an input
expectPass(input, output) additionally checks if the outputs are matching
expectFail(input) checks if any constraints / assertions are failing
See an example below:
describe('witness tester',()=>{// input signals and output signals can be given as type parameters// this makes all functions type-safe!letcircuit: WitnessTester<['in'],['out']>;beforeAll(async()=>{constcircomkit=newCircomkit();circuit=awaitcircomkit.WitnessTester(CIRCUIT_NAME,CIRCUIT_CONFIG);});it('should pass on correct input & output',async()=>{awaitcircuit.expectPass(INPUT,OUTPUT);});it('should fail on wrong output',async()=>{awaitcircuit.expectFail(INPUT,WRONG_OUTPUT);});it('should fail on bad input',async()=>{awaitcircuit.expectFail(BAD_INPUT);});});
You can check if the number of constraints are correct using expectConstraintCount, as shown below:
it('should have correct number of constraints',async()=>{// expects at least N constraintsawaitcircuit.expectConstraintCount(N);// expects exactly N constraintsawaitcircuit.expectConstraintCount(N,true);});
If you want more control over the output signals, you can use the compute function. It takes in an input, and an array of output signal names used in the main component so that they can be extracted from the witness.
Finally, you can run tests on the witnesses too. This is most useful when you would like to check for soundness errors.
expectConstraintPass(witness) checks if constraints are passing for a witness
expectConstraintFail(witness) checks if constraints are failing
You can compute the witness via the calculateWitness(input) function. To test for soundness errors, you may edit the witness and see if constraints are failing.
Circomkit provides a nice utility for this purpose, called editWitness(witness, symbols). You simply provide a dictionary of symbols to their new values, and it will edit the witness accordingly. See the example below:
it('should pass on correct witness',async()=>{constwitness=awaitcircuit.calculateWitness(INPUT);awaitcircuit.expectConstraintPass(witness);});it('should fail on fake witness',async()=>{constwitness=awaitcircuit.calculateWitness(INPUT);constbadWitness=awaitcircuit.editWitness(witness,{'main.signal': BigInt(1234),'main.component.signal': BigInt('0xCAFE'),'main.foo.bar[0]': BigInt('0b0101'),});awaitcircuit.expectConstraintFail(badWitness);});
Proof Tester
As an alternative to simulate generating a proof and verifying it, you can use Proof Tester. The proof tester makes use of WASM file, prover key and verifier key in the background. It will use the underlying Circomkit configuration to look for those files, and it can generate them automatically if they do not exist. An example using Plonk protocol is given below. Notice how we create the necessary files before creating the tester, as they are required for proof generation and verification.
describe('proof tester',()=>{// input signals and output signals can be given as type parameters// this makes all functions type-safe!letcircuit: ProofTester<['in']>;constprotocol='plonk';beforeAll(async()=>{constcircomkit=newCircomkit({protocol});circomkit.instantiate(CIRCUIT_NAME,CIRCUIT_CONFIG);awaitcircomkit.setup(CIRCUIT_NAME,PTAU_PATH);circuit=awaitcircomkit.ProofTester(CIRCUIT_NAME,protocol);});it('should verify a proof correctly',async()=>{const{proof, publicSignals}=awaitcircuit.prove(INPUT);awaitcircuit.expectPass(proof,publicSignals);});it('should NOT verify a proof with invalid public signals',async()=>{const{proof}=awaitcircuit.prove(INPUT);awaitcircuit.expectFail(proof,BAD_PUBLIC_SIGNALS);});});
Type-Safety
You may notice that there are optional template parameters in both testers: WitnessTester<InputSignals, OutputSignals> and ProofTester<InputSignals>. These template parameters take in an array of strings corresponding to signal names. For example, if your circuit has two input signals in1, in2 and an output out, you may instantiate the tester as WitnessTester<['in1', 'in2'], ['out']>. In doing so, you will get type-checking on all inputs and outputs required by the tester.
File Structure
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.
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.
The ptau(s) that circomkit downloads are from the Hermez phase1 ceremony, which are ready to be used in a phase2 ceremony.
ps. the base url of the ptau files should be updated just to be safe - seems like they have moved to google storage
EDIT: issue breakdown
Remove these lines and use existing PTAU instead of thats being created ptau2init there
Update base URL for Perpetual Powers of Tau urls
Add an extract function that looks for the zKey (w.r.t circuit name and configured protocol) and generates the vKey
Add CLI wrapper that calls extract
No need to check if PTAUs for larger constrain counts exist as you mentioned above, but maybe we can add a comment TODO: check for performance gains when larger PTAUs are found instead of the target PTAU; I want to check that out sometime :)
If I specify a custom directory for my circuit templates (e.g. src) and run circomkit compile it still creates a circuits/main/circuit.circom folder for its component. It fails because the relative path generated within that circuit is wrong. "../circuit.circom" doesn't exist in that folder.
I think the right folder for components should be src/main/circuit.circom.
Currently, when compiling a circuit, it is not possible to generate the C witness calculator files. By adding a optional config entry, we can decide whether to generate those files or not.
Would you be happy to have this feature in?
ps. great work with circomkit, we are just now integrating it in maci, and having this option would make it much easier for us to integrate it in our workflows.
Given a sorted array, prove that you know how the array looked like before it was sorted. The permutation array would be a secret input, and the sorted array is public input.
For example, groth16_proof.json & groth16_public.json and such. The reason is, when you change protocols the previous results will be overwritten due to them using the same path.
@numtel I was able to reduce some number packages for hardhat, but I dont think it has to do with this one. Would you have any idea on why Hardhat might leave an open handle?
This command will list the built circuits under the configured build folder. It's just a quality-of-life command to see which circuits are built. It could even go beyond and see which inputs are exported, and which protocols are outputted for each.
When reading a witness value, if that witness got optimized due to Circom optimization parameters, their variable index will be -1. It is possible to keep track of this somewhere (--simplification_substitution option) but we may just want to give a more explicit error for these cases.
Currently, it returns undefined if you read an optimized symbol.