#1 Circom: Introduction
Introduction
In the ZK Book, we explored the conceptual workings of arithmetic circuits. Now, we’ll use Circom to create these circuits, translating R1CS constraints into Circom code. The reader is expected to have understood concepts within the ZK book.
Installing Circom: https://docs.circom.io/getting-started/installation/#installing-dependencies
Example 1
Assume we are trying to create ZK proofs to assess if someone knows the product of two arbitrary numbers: c = a * b
Put another way, for some a
and b
, we are looking to verify that the user has computed the correct value for c
.
Programmatically speaking:
Consequently, our R1CS system of constraints would comprise of only 1 constraint, like so:
This is how it would look like in Circom:
Given inputs
a
,b
,c
, the circuit verifies thata * b
is indeed equal toc
.The circuit serves to verify, not compute, hence the outcome of the operation (
c
) is a required input.The
===
operator serves to define the constraint as previously expressed in R1CS form.===
behaves like an assertion, so the circuit will not be satisfied if an invalid inputs are supplied.
template
, component
, main
template
, component
, main
Templates define a blueprint for circuits, like a class defines the structure for objects in OOP (Object Oriented Programming).
A component is an instantiation of a template, similar to how an object is an instance of a class in OOP.
component main = SomeCircuit()
, is needed because Circom requires a single top-level component, main
, to define the circuit structure that will be compiled.
signal input
signal input
Signal inputs are values that will be provided from outside the component.
There are immutable and cannot be altered.
By declaring a signal, you're defining an interface for your component that specifies what data it expects to receive.
The finite field of Circom
Circom performs arithmetic in an finite field with an order of 21888242871839275222246405745257275088548364400416034343698204186575808495617
, which we will simply call p
.
The following should be obvious to the reader:
p
undermod p
is congruent to0
;p-1
is the largest integer undermod p
This is important to note, as passing values that are larger than p-1
, will result in overflow.
Example 2
Let’s look at a final example to conclude this article.
Consider a circuit that verifies values passed to it are binary, i.e.: 0
or 1
.
Given the inputs are x
and y
, the R1CS system of constraints would be:
Recall that R1CS constraints requires that each constraint to have only 1 multiplication operation.
Why x * (x - 1)
*x * (x - 1) = 0
→ x(x - 1) = 0
*
There are only 2 roots for this polynomial expression, which happen to be real.
I.e., x = 0 or x = 1.
Expressed in Circom
Alternative expression: using arrays
It is more conventional in Circom to group all the inputs into an array of signals called in
. Following convention, we will represent the earlier circuit, like so:
Arrays are zero-indexed as you would normally expect
In the next article we will cover loops, variables and template parameters.
Exercise
This section introduces common Circom commands. We assume the reader has already installed Circom and required dependencies.
In an empty directory, create a file called SomeCircuit.circom
. Copy the code below into the file:
This is a simple circuit with only 1 constraint.
1. Compiling circuits
In terminal execute the following command to compile:
circom **somecircuit**.circom --r1cs --sym --wasm
interchange the name of circuit to be compiled as needed
This is the expected output:
Observe that non-linear constraints are listed as 1, indicative of
a * b === c
The compiler creates the following:
somecircuit.r1cs
filesomecircuit.sym
filesomecircuit_js
directory
.r1cs file
Circuit's R1CS system of constraints in binary format.
Can be used with different tool stacks to construct proving/verifying statements (e.g. snarkjs, libsnark)
To read the R1CS file, you'll need a SNARK generator like snarkjs. Running cat somecircuit.r1cs
will output gibberish.
Running snarkjs r1cs print somecircuit.r1cs
, we get the following human-readable output:
In Circom, arithmetic operations are conducted within a finite field, so 21888242871839275222246405745257275088548364400416034343698204186575808495616
is actually representative of -1
.
We can confirm this by checking -1 mod p
, on Wolfram: https://www.wolframalpha.com/input?i=-1+mod+21888242871839275222246405745257275088548364400416034343698204186575808495617, where p
is the order of Circom’s finite field.
Hence, we can simplify the output to:
Again, observe that this matches with the constraint(a * b === c
) described in SomeCircuit.circom
.
.sym file
The somecircuit.sym
file generated is a symbols file required for debugging and printing the constraint system in an annotated mode.
somecircuit_js directory
somecircuit_js
directory contains artifacts for witness generation:
somecircuit.wasm
generate_witness.js
witness_calculator.js
By supplying input values for the circuit, these artifacts will calculate the necessary intermediate values and create a witness that can be submitted to a prover.
This is helpful in complex circuits, where there are few independent signals and many dependent ones, saving us from manually calculating all downstream values.
2. Calculating the witness
We start by providing the values for which the witness has to be generated. We do this by creating inputs.json
file in the somecircuit_js
directory.
Say we want to create a witness for input values a=1
, b=2
, c=2
. The json file would be like so:
Circom expects strings instead of numbers because JavaScript does not work accurately with integers larger than 253.
Run this command in somecircuit_js
directory: node generate_witness.js **somecircuit**.wasm inputs.json witness.wtns
.
outputs the computed witness as file
witness.wtns
.
If we had passed values that did not honor the constraint,
a*b === c
, e.g.a=1
,b=2
,c=3
, witness_calculator.js would throw an error.
Examine computed witness: witness.wtns
witness.wtns
If you run cat witness.wtns
, the output is gibberish.
This is because witness.wtns
is a binary file in a format accepted by snarkjs.
To get the human-readable form, we export it to json via: snarkjs wtns export json witness.wtns
snarkjs ingests
witness.wtns
file to outputwitness.json
.first 1 is the constant portion of the witness, which is always 1.
we have that a = 1, b = 2, and c = 2 since our input json was
{"a": "1","b": "2","c": "2"}
.computed witness adheres to the R1CS layout of the witness vector:
[1, a, b, c]
The reader might wonder why we are missing out
, (which typically follows 1
in R1CS form), recall that while the witness vector always begins with 1, out
is not mandatory.
Example: IsBinary.circom
Let’s run though a less trivial example, using IsBinary.circom
. The form of the constraints should be familiar to the reader given the explanation offered in example 2.
Compile circuit
circom isbinary.circom --r1cs --sym --wasm
Sanity check on terminal output:
non-linear constraints: 2
This makes sense, as our circuit contains two assertions that contain a multiplication of signals.
Next we examine the R1CS file: snarkjs r1cs print isbinary.r1cs
Notice that this large number is slightly different from the -1 mod p coefficient highlighted earlier
I.e.:
21888242871839275222246405745257275088548364400416034343698204186575808495616
Observe the additional digit,
1
at the end21888242871839275222246405745257275088548364400416034343698204186575808495616
21888242871839275222246405745257275088548364400416034343698204186575808495616
1
The reason there is a 1 at the end is due to a flaw in how snarkjs formats the output. It is “trying” to say
-1 * 1
but has no space between them.
The correct way to interpret it is as follows:
Visually, the output looks like a single number. However, it ought to be treated as the negative coefficient to
1
.
Generate the witness
create
inputs.json
file in./isbinary_js
directorywe will opt to pass values
in[0] = 1
,in[1] = 0
Generate
witness.wtns
:node generate_witness.js isbinary.wasm inputs.json witness.wtns
Now that
witness.wtns
has been created, export it to json, so we can examine it:snarkjs wtns export json witness.wtns
We would get the following as output, on executing
cat witness.json
:
The computed signal match the R1CS layout of the witness vector:
[1, in[0], in[1]]
, as do their respective values.
Last updated
Was this helpful?