DEV Community

Tony Olendo
Tony Olendo

Posted on

The Complete Full-Stack Guide to Getting Started with Zero-Knowledge Proofs using Circom and ZK-Snarks - Part 2

Welcome to the second part of this series where I try to demystify zero-knowledge proofs (ZKPs) for the rest of non-math nerds . Since writing part 1, my thoughts on the subject have evolved. I’m more drawn to answering the following questions

  • Can anyone be a ZK developer?
  • Is there a simple path that we can define for anyone to get started in writing ZK circuits?

The simple answer to all the questions above is yes! Most assuredly, yes. How? Just learn Circom and you’ll be well on your way.

What is Circom? Circom is a circuit programming language and a compiler that allows programmers to design and create their own arithmetic circuits for Zero Knowledge Proofs. In other words, it makes it easier for any developer to get started writing ZKPs. Later we’ll look at some key Circom features and the most crucial piece of Circom, understanding polynomial constraints, but let’s start by understanding arithmetic circuits and why they are so important to Circom.

Arithmetic Circuits

Most programming languages today are based on a Turing Machine, which is the foundation of computer science today given their ability to compute any function. On the other hand, an arithmetic circuit is a set of gates with separate inputs for each number that must be processed. The gates are connected so as to carry out an arithmetic action and the outputs of the gate circuit are the digits of the result (addition, subtraction, multiplication, or division).

For ZK cryptographic processing, arithmetic circuits are preferred over Turing Machines for a number of reasons:

  • Efficiency: Circuits are generally more time and space efficient as compared to Turing machines because circuits are comprised of a fixed set of gates.
  • Verifiability: Circuits are better suited at proving a ZKPs’ computation is correct compared to Turing Machines.
  • Compactness: Circuits are better suited to representing ZKPs’ complex and large computations by using a relatively small number of gates with reduced space requirements as compared to Turing Machines.

Polynomial Constraints

A huge limitation I’ve seen most new Circom developers struggle with is how to express conditional statements as polynomial constraints. This is the most significant difference any programmer needs to understand when writing Circom circuits. But what are Polynomial constraints and how do they work?

In Part 1 of this series, we converted the expression

if (x < 5) y = 7 else: y = 9
Enter fullscreen mode Exit fullscreen mode

to

y = 7 * (x < 5) + 9 * (x >= 5);
Enter fullscreen mode Exit fullscreen mode

This is a simple expression of a conditional statement as a quadratic equation and this goes to the heart of a key feature of Circom. In order to write circuits, one needs to understand polynomial constraints. A polynomial constraint is a mathematical statement that involves polynomials and sets limits on the values that the variables in the polynomial can take. For example, the polynomial "3x^2 + 2x + 1" is made up of the variables x and x^2, the coefficients 3 and 2, and the constant term 1. A polynomial is an expression made up of variables and coefficients, arranged according to certain rules of exponents.

In a polynomial constraint, the variables in the polynomial are subject to certain conditions or limits. These conditions can be equalities (e.g., "x^2 + y^2 = 1") or inequalities (e.g., "x^2 + y^2 < 1"). Polynomial constraints are often used in optimization problems, where the goal is to find the values of the variables that maximize or minimize a given function subject to certain constraints. For example, consider the optimization problem of finding the minimum value of the polynomial "x^2 + y^2" subject to the constraint "x + y = 1."

In this case, the constraint limits the values that x and y can take, as they must add up to 1. The solution to this problem would be x=y=0.5, which is the point at which the polynomial "x^2 + y^2" is minimized.

We’ll delve deeper into constraint generation in Circom and how they function.

Key features of Circom

Circuits in general are composed of a couple of core features, inputs, wires, and an output. Circom requires developers to express their computations in the form of arithmetic circuits and a few Circom features parallel these traditional circuit setups:

  • Signals (inputs)
  • Constraints (wires)
  • Output

Let’s introduce a simple circuit that we can use to demonstrate some key Circom features


pragma circom 2.0.0; 

template Factor () {  
   signal input a;  
   signal input b;  
   signal output c;  

   // Constraints
   c <== a * b;  
}
Enter fullscreen mode Exit fullscreen mode

The circuit above multiples a and b and outputs the value as c. Let’s delve into a few of the features of the Circom language seen here.

Pragma

pragma circom 2.0.0
Enter fullscreen mode Exit fullscreen mode

The line above is the version pragma and it specifies what compiler version the circuit is compatible with. If you don’t specify a pragma version, then the latest compiler version will be used and a warning will be shown.

Template

template Factor () {
Enter fullscreen mode Exit fullscreen mode

A template is used to generate circuits, think of them as objects that receive inputs, process computations and produce outputs. They are defined by instantiation and can be reused.

A quick note on Templates, they are composable, meaning they can be reused to build bigger more complex circuits. A template is instantiated using the keyword component. So for instance, if we are to reuse our example circuit above, we would instantiate this as

component multiplier = Factor ()
Enter fullscreen mode Exit fullscreen mode

And we’d then pass signals using the in keyword and output the computation using the out keyword.

signal intermediary // holds a temporary value
component multiplier = Factor()
multiplier.a ⇐ 8;
multiplier.b ⇐ 32;
intermediary ⇐ multiplier.c;
Enter fullscreen mode Exit fullscreen mode

Here we introduce a temporary signal that will hold the value of the computation and we assign input and output values. Template reuse is a huge part of Circom and the Circom library continues to grow. I’d recommend reading through the various libraries to understand more about Circom.

Signals

signal input a;  
signal input b;  
signal output c;
Enter fullscreen mode Exit fullscreen mode

Going back to the analogy of electrical circuits, in Circom, signals can be thought of as the basic units of information that travel through a circuit. Signals power computations meaning that regular mathematical operations can be performed using signals.

Signals are private by default but they can hold input, output or intermediary values. Intermediary signals hold values inside of a circuit.

Constraints

So everything we’ve done so far has been “wax-on, wax-off” (am I dating myself using this Karate Kid reference? maybe), we can now bring all this together. In Circom, everything comes down to writing constraints. Constraints can only be quadratic equations. Simply put, they should take the form of X + Y * Z = 0. I’m deliberately not going to delve any deeper into this for now but we can demonstrate this further by looking at how this works.

Let’s take a look at the XOR gate template from CircomLib

pragma circom 2.0.0;


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

   out <== a + b - 2*a*b; 
}
Enter fullscreen mode Exit fullscreen mode

This XOR template evaluates to true if only one input is true, take not of how the out signal is assigned out <== a + b - 2*a*b which essentially constrains the circuit to a given quadratic computation.

In Circom, constraints can be applied using the === or the <== operator. The former is an equality operator. Using the ===operator in the form of a * 1 + b === 0 is sufficient for the compiler to generate constraints and for your circuit to work.

The <== allows you to combine signal assignment and constraint generation in one go, improving the readability of your code. A lot of Circom libraries use this convention.

Now, there is a <-- operator that can be used for signal assignment but (shameless name drop here) I work at Polygon and as such I get the chance to interface with Jordi Baylina who created Circom and his advice is to me, which I pass on to you, is to always use double equals. This ensures that you’re always writing quadratic equations and generating constraints. If you somehow manage to write a circuit without constraints, the compiler will not play nice.

So when in doubt, do what Jordi said, always use double equals unless you absolutely know why you’re using something else.

Conclusion

So we have reviewed a few features of Circom and the underlying paradigms required to write Zero-Knowledge Circuits in Circom. I think anyone can be a ZK developer using Circom and in the next part, we’ll delve into a sample circuit that highlights how to write polynomials constraints, the Rank 1 Constrain System, a Powers of Tau ceremony and eventually generate a Solidity file that you can deploy on-chain and build a full stack Dapp around. Exciting stuff!!

Top comments (0)