top of page

Circom language tutorial with circomlib walkthrough

Updated: Dec 1, 2023

This tutorial introduces the Circom language and how to use it, along with common pitfalls. We will also explain a significant portion of the circomlib library in order to introduce common design patterns.


A note about production use

Circom is a fantastic tool for learning zk-snarks. However, because it is quite low-level, there are more opportunities to accidentally add subtle bugs. In real applications, programmers should consider using higher level zero knowledge programming languages. You should always get an audit before deploying a smart contract that holds user funds, but this is especially true for zk circuits, as the attack vectors are less well known.


Prerequisites

Although it is possible to program in Circom without understanding what a Rank 1 Constraint System is, you will have a far easier time developing a mental model of the language if you do. Circom is essentially an ergonomic wrapper for creating Rank 1 Constraint Systems.


It is highly recommended you learn that first, otherwise you won’t have a full context of the term “underconstrained.” We also recommend that you read our article on zk circuits to understand the motivation for why they exist.


Installation

Follow the steps here to install circom

You will also need to install snarkjs

npm install -g snarkjs@latest


Hello World

The hello world of zk-circuits is doing a multiplication, so let’s start with that.

pragma circom 2.1.6;

template Multiply() {
  signal input a;
  signal input b;
  signal output out;

  out <== a * b;
}

component main = Multiply();

Save the above code as multiply.circom and run

$ circom multiply.circom
template instances: 1
Everything went okay, circom safe

to ensure it compiles.


Generating an R1CS file

To convert the circuit to an R1CS, run the following command:

circom multiply.circom --r1cs --sym

The --r1cs flag tells circom to generate an R1CS file and the --sym flag means “save the variable names.” That will become clear shortly.


Two new files are created:

multiply.r1cs
multiply.sym

If you open multiply.r1cs, you will see a bunch of binary data, but inside the .sym file, you will see the names of the variables.


To read the R1CS file, we use snarkjs as follows:

snarkjs r1cs print multiply.r1cs

and we will get the following output

[INFO]  snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.a ] * [ main.b ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.out ] = 0

Remember, everything is done in a finite field when dealing with arithmetic circuits, so the massive number you see is essentially “-1”. The R1CS equation is equivalent to

-1 * a * b - (-1*out) = 0;

With a little algebra, we can see this is equivalent to a * b = out, which is our original circuit.


The steps of the algebra are as follows:

-1 * a * b - (-1*out) = 0;
-1 * a * b = -1*out;
a * b = out;


Non quadratic constraints are not allowed!

A valid R1CS must have exactly one multiplication per constraint (row in R1CS, <== in Circom). If we try to do two (or more) multiplications, this will fail. All constraints with more than one multiplication need to split into two constraints. Consider the following (non-compiling) example

pragma circom 2.1.6;

template Multiply() {
  signal input a;
  signal input b;
  signal input c;
  signal output out;

  out <== a * b * c;
}

component main = Multiply();

When we run circom multiply.circom we will get the following error:

error[T3001]: Non quadratic constraints are not allowed!
  ┌─ "multiply.circom":9:3

9 │   out <== a * b * c;
  │   ^^^^^^^^^^^^^^^^^ found here

  = call trace:
    ->Multiply

previous errors were found

A constraint in Circom is represented by the <== operator. For this particular constraint, we have two multiplications for one constraint. To solve this, we need to create a separate constraint so that each constraint has only one multiplication.


Breaking up non-quadratic constraints

Fixing the problem above is straightforward. We introduce an intermediate signal s1 and have it constrain the first multiplication, then combine the output of s1 with the third input. Now we have two constraints and two multiplications, as Circom expects it.

pragma circom 2.1.6;

template Multiply() {
  signal input a;
  signal input b;
  signal input c;
  signal s1;
  signal output out;
  
  s1 <== a * b;
  out <== s1 * c;
}

component main = Multiply();

When we regenerate and print the R1CS, we get the following

[INFO]  snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.a ] * [ main.b ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.s1 ] = 0
[INFO]  snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.s1 ] * [ main.c ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.out ] = 0

With a little algebra, this translates to

a * b  = s1
s1 * c = out

And we can see this is encoding the same computation from our circuit.


Computing the witness

Run the following command to create code to generate the witness vector

circom multiply.circom --r1cs --sym --wasm

This will regenerate the R1CS and symbol file, but also create a folder called multiply_js/. Cd to that folder.


Next, we need to create an input.json file in that folder. This is a map from the names of the signals designated input to the value that the prover will supply to them. Let’s set our input.json to have the following values:

{"a": "2","b": "3","c": "5"}

If we didn’t create the .sym file from earlier, it would not be possible for Circom to know what input signals we were trying to map to. Because s1 and out are not input signals, we don’t create key value pairs for those. The “a”, “b”, and “c” here correspond to the names of the input signals.

signal input a;
signal input b;
signal input c;

Now we calculate and export the witness with the following command:

node generate_witness.js multiply.wasm input.json witness.wtns

snarkjs wtns export json witness.wtns

cat witness.json
[
 "1",
 "30",
 "2",
 "3",
 "5",
 "6"
]

The computed witness has values [1, 30, 2, 3, 5, 6]. 2, 3, and 5 are the inputs, and 6 is the intermediate signal s1 which is the product of 2 and 3.


This follows the expected layout of the R1CS variables in the form [1, out, a, b, c, s1].


Public inputs

What if we want to make some of the inputs public? In nullifier schemes for example, we hash the concatenation of two numbers and reveal one of them later.


Motivation: nullifier schemes

As a quick aside, a nullifier scheme works by concatenating two numbers and then hashing them. They are used in the context where we have a set of hashes, and we want to prove we know the preimage to one of them without revealing which one it is.


If the hashes were simply the hash of one number, then revealing that number would reveal which hash we know the preimage of. However, if we don’t reveal any information, we could repeat the action of proving we know the preimage of one of the hashes several times. This would be very problematic if this action involved withdrawing money from a smart contract!


By revealing one of the two hashed numbers, we cannot re-use the preimage, but we also don’t reveal which hash we know the preimage of because revealing only one of the two numbers isn’t sufficient.


Here’s how Circom accomplishes public inputs:

template SomePublic() {

    signal input a;
    signal input b;
    signal input c;
    signal v;
    signal output out;
    
    v <== a * b; 
    out <== c * v;
}

component main {public [a, c]} = SomePublic();

In the example above, a and c are public inputs, but b remains hidden. Note the public keyword when the main component is instantiated.


Arrays in Circom

Let’s create a component that will compute n powers of the input.

It would be rather annoying to have to manually write signal input n times, so Circom provides an array type of signals to do this. Here is the code

pragma circom 2.1.6;

template Powers(n) {
    signal input a;
    signal output powers[n];
    
    powers[0] <== a;
    for (var i = 1; i < n; i++) {
        powers[i] <==  powers[i - 1] * a;
    }
}
component main = Powers(6);

A few new syntactic features are introduced with this example: template arguments and variables.


Template Arguments

In the example above, we see the template is parameterized with n, i.e. Powers(n).

A Rank 1 Constraint System must be fixed and immutable, this means we cannot change the number of rows or columns once defined, and we cannot change the values of the matrices or the witness. That is why the final line has hard-coded argument Powers(6), this size must be fixed.


However, if we wanted to re-use this code later to support a different size circuit, then it is more ergonomic to have the template be able to change its size on the fly. Therefore, components can take arguments to parameterize control flows and data structures, but this must be fixed per circuit.


Variables

The example above is equivalent to the following

pragma circom 2.1.6;

template Powers() {
    signal input a;
    signal output powers[6];
   
    powers[0] <== a;
    powers[1] <== powers[0] * a;
    powers[2] <== powers[1] * a;
    powers[3] <== powers[2] * a;
    powers[4] <== powers[3] * a;
    powers[5] <== powers[4] * a;
    
}
component main = Powers();

Although we get an identical R1CS, that code is ugly. However, this example usefully illustrates that any circuit that uses variables in its computation can be rewritten to not include variables.


Variables build helper code that exists outside the R1CS. They help define the circuit, but they are not part of the circuit.


The variable var i was simply bookkeeping to track the loop iteration while constructing the circuit, it isn’t part of the constraints.


Signal vs variable

A signal is immutable and intended to be one of the columns of the R1CS. A variable is not part of the R1CS. It is intended for computing values outside the R1CS to help define the R1CS.


It would not be accurate to think of signals as “immutable variables” and variables as “mutable variables” the way some languages assign them. The reason signals are immutable is because the witness entries in an R1CS have a fixed value. A solution vector in an R1CS that changes value does not make sense, as you cannot create a proof for it.


The <--, <==, and === operators are used with signals, not variables. We will explain the unfamiliar operators shortly.


When working with variables, Circom behaves like a normal c-like language. The operators =, ==, >=, <=, and !=, ++, and -- behave the way you expect them to. This is why the loop example looks familiar.


The following examples are not allowed:

signal a;
a = 2; // using a variable assignment for a signal

var v;
v <-- a + b; // using a signal assignment for a variable is not allowed


=== vs <==

The following circuits are equivalent:

pragma circom 2.1.6;

template Multiply() {
    signal input a;
    signal input b;
    signal output c;
    
    c <-- a * b;
    c === a * b;
}

template MultiplySame() {
    signal input a;
    signal input b;
    signal output c;
    
    c <== a * b;
}

The <== operator computes, then assigns, then adds a constraint. If you only want to constrain, use the ===.


Typically, you’ll use the === operator when you are trying to enforce that the <-- assigned the correct value. You’ll see this in action when we look at the IsZero template.


But before we get to that, let’s look at a real example. Suppose we want the prover to supply both the inputs and the output. This is how we would do it using the === operator.

pragma circom 2.1.6;

template Multiply() {
    signal input a;
    signal input b;
    signal input c;

    c === a * b;
}

component main {public [c]} = Multiply();

Circom does not require an output signal to exist, as that is merely syntatic sugar for a public input. Remember, an “input” is merely an entry to the witness vector, so everything is an input from a zero knowledge proof perspective. In the above example, there is no output signal, but this is a perfectly valid circuit with proper constraints.


Wiring templates together

Circom templates are reusable and composeable as the following example illustrates. Here, Square is a template used by SumOfSquares. Note how inputs a and b are “wired” to the component Square().

pragma circom 2.1.6;

template Square() {

    signal input in;
    signal output out;

    out <== in * in;
}

template SumOfSquares() {
    signal input a;
    signal input b;
    signal output out;

    component sq1 = Square();
    component sq2 = Square();

    // wiring the components together
    sq1.in <== a;
    sq2.in <== b;

    out <== sq1.out + sq2.out;
}

component main = SumOfSquares();

You can think of the <== operator as “wiring” the components together by referencing their inputs or outputs.


Multiple inputs to a component

The example Square above takes a single signal as an input, but if a component takes multiple inputs, it is conventional to specify this as an array in[n]. The following component takes two numbers and returns their product:

template Mul {

    signal input in[2]; // takes two inputs
    signal output out; // single output
    
    out <== in[0] * in[1];
}

Knowing how to wire templates together is a prerequisite for when we bring in a library of circuits, which we will show in an upcoming section.


Signal naming conventions

It is conventional to name the input signals in or as an array in[] and output signal(s) to be out or out[].


Unsafe Powers, beware of <--

When running into the issue of quadratic constraints, it can be tempting to use the <-- operator to silence the compiler. The following code compiles, and seems to accomplish the same as our earlier powers example.

pragma circom 2.1.6;

template Powers {
    signal input a;
    signal output powers[6];
   
    powers[0] <== a;
    powers[1] <== a * a;
    powers[2] <-- a ** 3;
    powers[3] <-- a ** 4;
    powers[4] <-- a ** 5;
    powers[5] <-- a ** 6;
    
}
component main = Powers();

However, when we create the R1CS, we only have on constraint!

(base) ➜  hello-circom circom bad-powers.circom --r1cs
template instances: 1
non-linear constraints: 1 ### only one constraint ###
linear constraints: 0
public inputs: 0
public outputs: 6
private inputs: 1
private outputs: 0
wires: 7
labels: 8
Written successfully: ./powers.r1cs
Everything went okay, circom safe

With only one constraint, the prover only has to set the first element in the array correctly, but can put whatever value they like for the other 5! You cannot trust proofs that come out of a circuit like this!


Underconstraints are a major source source of security bugs in zero knowledge applications, so triple check that the constraints are actually generated in the R1CS the way you expect them to!


This is why we emphasized understanding Rank 1 Constraint Systems before learning the Circom language, otherwise there is an entire category of bugs that will be hard for you to detect!


When to use <--

If the <-- leads to underconstraints, why would the language include such a foot gun?

When describing algorithms using circuits rather than natural code, the mindset to adopt is “compute, then constrain.” Some operations are extremely difficult to model with pure constraints alone. Consider the following example.


Circomlib

Iden3 maintains a repository of useful circom templates called circomlib. At this point, we have enough requisite Circom knowledge to begin studying these templates, and now is a good time to introduce a simple but useful template that demonstrates the use of <--.


IsZero

The IsZero template returns 1 if the input signal is zero, and zero if the input signal is non-zero.


If you spend some time thinking about how to test if a number is zero using only multiplications, you will find yourself getting stuck; it will turn out to be a fiendishly difficult problem.


Let’s see how the circomlib component IsZero accomplishes this.


template IsZero() {
  signal input in;
  signal output out;

  signal inv;

  inv <-- in!=0 ? 1/in : 0;

  out <== -in*inv +1;
  in*out === 0;
}

In the example above, inv is an auxiliary input signal to make it easier to create a valid circuit. We compute inv to be either zero or the inverse of x outside the R1CS then force inv to be correct as part of the constraints. This is following the pattern of “compute, then constrain.”


But why make inv a signal instead of a variable? It appears to only be used ephemerally. If we set inv to be a var and replace <-- with = we will get the non-quadratic constraints error because we are multiplying the signal in with a variable, which was derived from some non-trivial computation – one that certainly involves more than one multiplication.


Also, the signal inv is still constrained.


As an exercise for the reader, draw a truth table showing the following possible combinations


out: {1,0} inv: {0, in^{-1}, invalid} in: {0, non-zero}


You will see it is only possible to satisfy the constraints by setting out to be 1 when in is 0, and out to 0 when in is non-zero. You cannot monkey around with inv, it must follow the rules. This illustrates the pattern of “compute, then constrain.”


The arrow creates a constraint and also assigns a value. In the code above, out is constrained to be equal -in*inv + 1. However, the final line is not assigning zero to in*out. Rather, it is enforcing that in*out does in fact equal zero.


Although you would normally use the ternary operator (or c-like things in general) with variables, you can use the traditional programming syntax with signals if you use the <-- operator.


Exercise for the reader: can you create a template that does the opposite of IsZero, without using the IsZero template?


Use the IsEqual template rather than === unless you are constraining a multiplication of signals

When comparing components that are equal, you can use the IsEqual template, which subtracts the inputs and runs them through the IsZero component – if they are equal, the output will be zero.


The motivation for this is shown in the following two sections.

template IsEqual() {
    signal input in[2];
    signal output out;

    component isz = IsZero();

    in[1] - in[0] ==> isz.in;

    isz.out ==> out;
}


=== and <== will not create a constraint if the operation is not quadratic

Here is another underconstraint foot gun to look out for

template FactorOfFiveFootgun() {

    signal input in;
    signal output out;
    
    out <== in * 5;
}

component main = FactorOfFiveFootgun();

Here we are saying we know x, such that 5x = out, where out is public. So if out is 100, we expect x to be 20 right? If we look at the R1CS, we see it is in fact empty, no constraints are created!


This is because although <== is a constraint, the constraint it generates is not a quadratic constraint, it is multiplication by a constant.


When we compile the circuit to R1CS, we see it is empty!

(base) ➜  hello-circom circom footgun.circom --r1cs
template instances: 1
non-linear constraints: 0 ### no constraints ###
linear constraints: 0
public inputs: 0
public outputs: 1
private inputs: 1
private outputs: 0
wires: 2
labels: 3
Written successfully: ./footgun.r1cs
Everything went okay, circom safe

The solution is to wire the IsEqual template in to enforce the equality. This is left as an exercise for the reader.


Example: taking the average

Suppose we wanted to compute the average value of an array of signals. Doing this in regular programming is easy, but using only quadratic constraints, this would be exceptionally challenging.


The solution is to compute the average using regular programming, then constrain the output to be correct.


The average of n signals is their sum divided by the number of signals. Division in finite fields is the same as multiplying by the inverse, so we need to add up the signals, then multiply it by the inverse of the length of the array.


We need to prove that the inverse of the length is in fact the inverse of the length, since we are computing that without constraints.


The following looks correct but it does not achieve the desired behavior:

pragma circom 2.1.6;

include "node_modules/circomlib/circuits/comparators.circom";

template AverageWrong(n) {

    signal input in[n];
    signal denominator;
    signal output out;

    // sum up the inputsvar sum;
    for (var i = 0; i < n; i++) {
        sum += in[i];
    }

    // compute the denominator
    denominator_inv <-- 1 / n;
    
    // "force" the denominator to be equal to the inverse of n1 === denominator_inv * n; // this does not create a constraint!// sum / denominator
    out <== sum * denominator_inv;
}

component main  = AverageWrong(5);

When the above circuit is compiled, we get zero constraints!


When comparing a signal to a constant, use the IsEqual component, not ===


The following is the correct way to do it:

pragma circom 2.1.6;

include "node_modules/circomlib/circuits/comparators.circom";

template Average(n) {

    signal input in[n];
    signal denominator_inv;
    signal output out;

    var sum;
    for (var i = 0; i < n; i++) {
        sum += in[i];
    }

    denominator_inv <-- 1 / n;

    component eq = IsEqual();
    eq.in[0] <== 1;
    eq.in[1] <== denominator_inv * n;

    out <== sum * denominator_inv;

}

component main  = Average(5);

Now when we compute the R1CS, we actually see the sum is constrained to be the sum of the signals and the denominator is constrained to be the inverse of n.

Specifically, you can see the R1CS sums up the array of n and multiplies it by the denominator, then checks it is equal to out.


The following thee constraints ensure denominator really is calculated properly.

[INFO]  snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.in[0] +21888242871839275222246405745257275088548364400416034343698204186575808495616main.in[1] +21888242871839275222246405745257275088548364400416034343698204186575808495616main.in[2] +21888242871839275222246405745257275088548364400416034343698204186575808495616main.in[3] +21888242871839275222246405745257275088548364400416034343698204186575808495616main.in[4] ] * [ main.denominator ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.out ] = 0

[INFO]  snarkJS: [ 51 +21888242871839275222246405745257275088548364400416034343698204186575808495616main.denominator ] * [ main.eq.isz.inv ] - [ 1 +21888242871839275222246405745257275088548364400416034343698204186575808495616main.eq.out ] = 0

[INFO]  snarkJS: [ 51 +21888242871839275222246405745257275088548364400416034343698204186575808495616main.denominator ] * [ main.eq.out ] - [  ] = 0

The moral of the story is to always sanity check the R1CS constraints!


Functions in circom

If we are doing a lot of work with variables, we can block them off into their own function for code cleanliness. Consider the following example.

pragma circom 2.1.6;

include "node_modules/circomlib/circuits/comparators.circom";

function invert(x) {
    return 1 / x;
}

template Average(n) {

    signal input in[n];
    signal denominator;
    signal output out;

    var sum;
    for (var i = 0; i < n; i++) {
        sum += in[i];
    }

    denominator <-- invert(n);

    component eq = IsEqual();
    eq.in[0] <== denominator;
    eq.in[1] <== n;

    out <== sum * denominator;
}

component main  = Average(5);


There are constraints depending on the value of the condition and it can be unknown during the constraint generation phase

Here is another easy mistake to make when writing circom code: the signal cannot be an input to an if statement or a loop.

template IsOver21() {
    signal input age;
    signal output oldEnough;
    
    if (age >= 21) {
        oldEnough <== 1;
    } else {
        oldEnough <== 0;
    }
}

If you try to compile the above code, you will get the error in the header of this section. You will get a similar error if you use a signal to determine the number of times to execute a loop (try it!). The reason this is not allowed, is because we are making the number of constraints in the R1CS a function of one of the R1CS inputs, but this makes no sense.


So how do we check if someone is over 21 years old?


This is actually harder than it seems, since comparing numbers in a finite field is rather tricky.


The Pitfalls of comparing numbers in Circom

If 1 - 2 = 21888242871839275222246405745257275088548364400416034343698204186575808495616, then is that huge number actually larger than one or less than 1?

You cannot compare numbers in a finite field, as there is no meaning in saying one number is larger than another number.


However, we would still like to be able to compare numbers!


The first requirement before we do any comparisons, is that they must be smaller than the field size, and we will treat any numbers in the field as positive integers, and carefully guard against underflow and overflow.


If you think for a moment about how to represent the following statement


a > b


using a purely R1CS representation, you’ll get stuck. It’s impossible to do that.

As long as we force numbers to stay within the field order, then we can meaningfully compare them. It isn’t possible to translate the > operator into a set of quadratic constraints.


However, if we convert a field element to a binary representation of the number, then it is possible to make a comparison.


Now we can introduce two more circomlib templates: Num2Bits and LessThan, with the motivation being to compare integers.


Num2Bits

How could we compare two numbers that are 9,999 or less without using the normal < and > operators and only using one binary conversion? If we are allowed two binary conversions, this is easy, but it makes for a larger circuit.


It’s a tricky problem, but here’s how circomlib does it.


Let’s say we are comparing x and y.


Since 9,999 largest value our inputs can take, we add 10,000 to the x and subtract the y from the sum of x and y.

z = (10,000 + x) - y

No matter what, z will be positive, even if y is 9,999 and x is 0. Since we are dealing with field elements here, we do not want any underflows to happen, and in this case, an underflow cannot occur.


Here is the key part. If x ≥ y, then z will between 10,000 and 19,999 and if x < y, z will be in the range [1-9,999].


To know if a number is in the range [10,000-19,999], we only need to look at the digit in the 10,000ths place, i.e. 1x,xxx. If we have a 1 there, then we know z is in the range [10,000-19,999] and if we have something of the form 0x,xxx, then y must have originally been greater than x.


Circom just does the same thing in binary rather than decimal representation.

The following Circomlib Num2Bits template shows how circom transforms a signal into an array of signals that hold the binary representation.

template Num2Bits(n) {
    signal input in;
    signal output out[n];
    var lc1=0; 
    // this serves as an accumulator to "recompute" in bit-by-bit
    var e2=1;
    for (var i = 0; i<n; i++) {
        out[i] <-- (in >> i) & 1;
        out[i] * (out[i] -1 ) === 0; // force out[i] to be 1 or 0
        lc1 += out[i] * e2; //add to the accumulator if the bit is 1 
        e2 = e2+e2; // takes on values 1,2,4,8,...
    }

    lc1 === in;
}


LessThan

Now that we have Num2Bits under our belt, understanding the LessThan template ought to be straightforward

template LessThan(n) {
    assert(n <= 252);
    signal input in[2];
    signal output out;

    component n2b = Num2Bits(n+1);

    n2b.in <== in[0] + (1<<n) - in[1];

    out <== 1-n2b.out[n];
}

The code is parameterized by n which is the maximum size of the numbers in bits, though there is an enforced upper limit of 252 bits.


The numbers cannot be larger than 2^n, so adding 1<<n ensures the term in[0] + (1<<n) will always be larger than in[1]. The difference is turned into binary, and if the highest bit is still 1, then in[0] is greater than in[1]. The final term is 1 minus that bit, so this inverts whether or not the bit is present.


Thus, the component returns 1 if in[0] is less than in[1].


Over21 functional example

Now that we have the requisite toolbox, let’s make an age checker that actually works.


Circomlib provides a comparator called GreaterThan which is LessThan with a simple twist, so we will not explain it here. The interested reader can consult the source code directly.


To use the circomlib template, create an empty node_modules directory then run

npm install circomlib

Then create the following circuit

pragma circom 2.1.6;

include "node_modules/circomlib/circuits/comparators.circom";

template Over21() {

    signal input age;
    signal input ageLimit;
    signal output oldEnough;
    
    // 8 bits is plenty to store age
    component gt = GreaterThan(8);
    gt.in[0] <== age;
    gt.in[1] <== 21;
    
    oldEnough <== gt.out;
}

component main = Over21();

This is the safe way to verify age is is really over 21 (though in a real application, you’d need an authority to attest to age).


Comparators.circom

The comparators provided in this file are

  • IsZero

  • IsEqual (subtracts the two inputs and passes that to IsZero)

  • LessThan

  • LessEqThan (derived from LessThan)

  • GreaterThan (derived from LessThan)

  • GreaterEqThan (derived from LessThan)

  • ForceEqualIfEnabled

What does the last one do?


ForceEqualIfEnabled

template ForceEqualIfEnabled() {
    signal input enabled;
    signal input in[2];

    component isz = IsZero();

    in[1] - in[0] ==> isz.in;

    (1 - isz.out)*enabled === 0;
}

The ForceEqualIfEnabled template behaves like an assert statement by including a constraint that the inputs must be equal if the enable flag is non-zero. Rather that returning true or false based on equality (there is no output signal here to return zero or one), it will cause the circuit to be unsatisfiable if the inputs are unequal.


Circom assert

Confusingly enough, Circom has an assert statement that doesn’t quite do what you would expect.


The assert statement does not add any constraints, it’s merely a sanity check on the inputs for a non-malicious prover.


For example, we might use it to ensure templates aren’t initialized with length zero arrays if that leads to the possibility of dividing by zero.


You should always assume that a malicious prover can just copy the circuit code, delete the assert statement, and create a proof. That proof will be compatible with the circuit, because the circuit doesn’t incorporate the assert statement.


Boolean operators

Hopefully you know by now that you cannot use the operators intended for variables on signals. The following code will not compile

template And() {

    signal input in[2];
    signal output c;

    c <== in[0] && in[1];
}

Remember, signals are field elements, and && is not a valid operator for field elements.


However, if you constrain a and b to be zero and 1, you can achieve the same effect using multiplication

template And() {
    signal input in[2];
    signal output c;
    
    // force inputs to be zero or one
    in[0] === in[0] * in[0];
    in[1] === in[1] * in[1];
    
    // c will be 1 iff in[0] and in[1] are 1
    c <== in[0] * in[1];
}

It is a useful exercise for the reader to think of how to accomplish the other boolean operations, not, or, nand, nor, xor, and xnor. Although every boolean gate can be constructed from a nand gate, this will make the circuit larger than necessary, so don’t overdo re-using gates.


The solutions are in circomlib’s gates.circom file. Note that they do not constrain the input to be zero or one, it is up to the circuit designer to do that.


ZK-Friendly hash functions

You can imagine that building up a hash function using the method the circuits produced will be huge. Let’s see how big a sha256 hasher that takes 256 bits will be:

pragma circom 2.0.0;
include "node_modules/circomlib/circuits/sha256/sha256.circom";

component main = Sha256(256);

This innocuous little circuit will produce almost 30,000 constraints:

(base) ➜  hello-circom circom Sha256-example.circom --r1cs
template instances: 99
non-linear constraints: 29380 ### WOW! ###
linear constraints: 0
public inputs: 0
public outputs: 256
private inputs: 256
private outputs: 0
wires: 29325
labels: 204521
Written successfully: ./Sha256-example.r1cs
Everything went okay, circom safe

This creates a significant amount of work for the prover. As a response, the research community has produced hash functions optimized for circuit representation. In addition to sha256 you will find:

  • Mimc

  • Pedersen

  • Poseidon

It is an exercise for the reader to compare the size of the output R1CS of the hash functions. Bear in mind some of the functions take the number of “rounds” as a parameter, which will naturally increase the size of the circuit.

Zk friendly hash functions are a sizable topic that are best suited to have their own discussion, so we defer this to a future article.


What about the rest?

The remainder of the templates are small enough to be self-explanatory, or are related to zero knowledge digital signature schemes and computing elliptic curves.

The motivation for computing elliptic curves inside a zk-circuit is that some zk-friendly hash functions rely on elliptic curves as a core primitive.

This is another large topic which we defer to another article.


Learning Circom from Real World Examples

Two of the classic yet approachable circuits to study are Tornado Cash and Semaphore.


Learn more with RareSkills

The best way to learn circom is to use it. Our Zero Knowledge Puzzles provide bite-sized challenges of increasing complexity for you to learn the language. A development environment is provided with unit tests to inform you if you have completed the puzzle successfully. Be aware it is possible to pass the tests with underconstrained circuits, as testing for underconstraint is extremely difficult to do in general. At the very least, you should check the number of constraints generated and make sure the number makes sense.


This material is part of our zero knowledge course. Please see the program to learn more.

5,161 views0 comments

Comentários


bottom of page