Creating a ZK-Snark with Circom 2.0

Tehseen Dahya
9 min readJan 7, 2023

--

Before we dive in, check out this article I wrote for an introduction to all things ZK.

What is Circom 2.0?

Circom is a compiler written in the Rust programming language (similar to C++) for compiling arithmetic circuits that compute different Zero-Knowledge Proofs. We use Circom along with a package called SnarkJS to generate and validate these ZK proofs. You can check out the workflow below. I will explain the terminal commands they use later.

Circom was originally released in 2018 by iden3 in JavaScript and used in some of the more popular applications of ZK technology like Tornado Cash and Dark Forest. Version 2.0, in Rust, comes with compiling speeds that are almost 10x the original version. It also fixed some minor formatting limitations enhancing security and ease of use. If you want to learn more about the specific differences between Circom and Circom 2.0, watch this video from 31:30 onward.

This article is going to assume a basic understanding of the basics of Zero-Knowledge. Give this article a read to get caught up.

How do zk-proofs become circuits?

One of the applications of a Zero-Knowledge Proof is that the prover can prove they know the private key that corresponds to a specific public key, without revealing what that private key is.

Because there is a series of inputs and outputs, a circuit can be created. The diagram below represents a simple example. Given the circuit, the prover can input public and private inputs and generate a proof for the verifier. Then, given the proof, the public input, and the output, the verifier can ensure the prover knows the private input (without actually knowing the private input) and therefore execute the circuit. In this way, the three factors of a zkp are met and the circuit indicates a complete proof was made.

The specific proofs I am discussing here are ZK-Snarks, which specifically require being converted to arithmetic circuits. This circuit takes in some input signals and performs arithmetic operations (addition and multiplication to represent AND and OR circuits) on them. The output of each of these addition or multiplication gates is an intermediate signal (d) unless it is the final gate of the circuit, which is then called the output signal (out).

This is an F_7 circuit where a*b + c = out

In this example above, there are 5 signals: a, b, and c are the input, d is the intermediate, and out is the output signal. To represent this circuit in a way that can be computed, we create a system of equations to relate variables with each logic gate. Each equation used is called a constraint, which is just a condition the proof must satisfy. These constraints are linear, quadratic, or constant equations and there is typically one constraint per multiplicative gate. We call these systems of constraints the rank-1 constraint system (r1cs) — this term will come up later as I explain the code for the proof. These equations can get pretty complicated, but fortunately, when we design our circuits with Circom, the compiler will output the r1cs we need for the proof.

So far, the example we are using sounds very trivial. Given the input signals for a circuit, it is very easy to find the intermediate and output signals, so where does the Zero-Knowledge complexity come in? The beauty of this protocol is that the circuits will be complied and validated with the verifier never knowing the private inputs. For example, if we make a our private input, and the other signals public, we can prove that for certain values of b, c, and output, the constraints hold and the statement is valid. A set of valid values for the signals is called a witness.

**First, check out this walkthrough of the project

Now let’s get into some code.

If you want to follow along, you can learn how to download the relevant dependencies here and get started.

In this program, we are going to be proving a very simple three-digit multiplication operation using Circom. The goal is to generate a valid proof that we (the prover) know three factors of 20 but never reveal the three factors we choose to the verifier. We are using small numbers for simplicity, but the same algorithm could be applied to very large numbers with any number of factors.

I will now walk you through the steps of how I conducted the proof.

This Circom file contains the signals and constraints to be proven. The first line simply defines the version of the compiler we are using. If you are a Solidity user, you will understand this is just standard practice.

We then create a template, which defines the “shape” of the circuit we are going to create. Although they look similar, don’t confuse templates with functions. These templates are solely to create generic circuits; they could be used later to create larger, more complex circuits. Within the template, you must define the input and output signals to be used in the constraint. The final line in the template is the very simple constraint we will use. You can see the assignment operator used here is the ==> operator.

In Circom, there are three different assignment operators.

  1. “ — ->” is used to define a witness value (assign values to signals, but not a constraint).
  2. “= = =” is used to define constraints in the form of a*b+c = 0 and act as an assert statement for values of both sides.
  3. “==>” is used as a combination of the above two and implies a constraint declaration.

Here is the template for the multiplication with the third value. This block of code is just under the template for the multiplication of two values.

For the initialization of the signals, we use the same format as with two input but now use the keyword component to instantiate the template use of Multiplier2. The statements block of code essentially computes in1*in2*in3 and assigns it to out.

After creating this circuit, we are going to follow the workflow from the image above and run some commands in the terminal.

Everything under the first line is output. Only refer to the first line for the input

Using this command, we compile our circuit and generate three different files:

  1. r1cs: As I explained above, this file contains some very complex equations that we never even see. The compiler creates the equations for us and we never have to touch them.
  2. wasm: This command is used to create more files we can’t read, but works to create the witness needed for the proof
  3. sym: This command is needed to create files for debugging and printing the system of constraints
This is what you see when you try and open the r1cs and wasm files

The terminal output also explains that from the circuit, we have two templates (for each Multiplication operation), two non-linear constraints (for each multiplication operation), one public output (the product of the three inputs), three private inputs, six wires, and 11 labels (these values have to do with some modular arithmetic that stems from the size of the problem).

For the sake of simplicity in this example, we are simply going to use our input file to use three small integers as our private inputs. The module will execute the circuit and calculate the intermediate and output signals.

Use strings because JS doesn’t work well with larger numbers

After which, we can compute the witness by entering this into the terminal. The wasm (WebAssembly) calculator does the work here. A few non-readable files will appear in the directory for this calculation.

It is now time for the final steps… proving the circuit. We will use SnarkJS to generate and validate the proof here — proving that we know three factors of 20.

We are going to use a very specific SNARK protocol called Groth16. This circuit-specific protocol is the most popular current SNARK protocol because of its quick verifying time and constant size. The downside of the protocol is that it requires a trusted setup phase which could be a problem with malicious parties that could forge fraudulent proofs (one of the main reasons people are working on ZK-STARKS).

If you are interested in the setup ceremonies process of SNARKS, give this a read

As a part of the trusted setup phase of the protocol, we must undergo a Powers of Tau ceremony that is split into two parts:

  1. Phase 1: To produce a generic setup parameter for all circuits using this protocol (up to a certain size). Everyone must contribute to the protocol in this phase.
  2. Phase 2: Converting the output of the generic parameter to a specific CRS (common reference string) for proving and verifying the proof in this circuit.

The Powers of Tau ceremony for the trusted setup phase is a superior protocol to previous trusted setup “ceremonies” as it can handle many more participants at a time due to its random beacon that selects public values to enable a continuous ceremony without participants needing to be online.

To start this Powers of Tau ceremony (phase 1), we must input a few general commands into the terminal as well as contribute to the protocol so the protocol can generate this CRS. These are the general commands I referenced above that anyone using the protocol would have to enter.

Initiates Phase 1
My contribution to the protocol: hola

Now we enter the second phase of the trusted setup that generates the specific CRS for this circuit.

Here we initiate Phase 2
Generates prover and verifier keys

This command creates a .zkey file to store the prover and verifier keys, but like the r1cs and wasm files, it is not readable.

Another contribution to the protocol: SNARKER
Export our verification key

Now that the trusted setup process is done, we can finally create our proof!

This command generates our groth16 proof using our proof.json file (containing the actual proof in JS) and the public.json file (containing the values of the public inputs and outputs — just 20 in our case).

And Finally… the verification.

OK!

Because our public output is the product of our private inputs, our proof is OK!

If I were to change the public output in the public.json file to 21 instead of 20, for example, our proof would return:

Not good

This proof essentially proves to the validator that we (the prover) know the set of signals to satisfy the circuit, but it doesn’t reveal what our signals are.

This super simple example of finding three factors of 20 is just the tip of the iceberg for ZK technology. Think about doing this same procedure with any other mathematical operation. Think about using private wallet keys and block hashes as private inputs. That is the real potential of ZKs on the Blockchain. While there is a dire use case for ZK on the Blockchain, this versatile technology can be expanded in any cyber-security-related field. I am super excited to continue to build with this tech!

I hope you enjoyed this run-through of a very simple application of ZKPs. Leave a comment on some other Web3, Blockchain, or ZK projects/concepts I should dive into. Feel free to reach out to my on Linkedin and subscribe to my monthly newsletter to stay up to date on my progress and keep learning about ZK and Web3.0

--

--