1.2. Getting started

This tutorial will guide you through the process of creating and validating your first zero-knowledge SNARK circuit using circom and snarkJS.

Installing the tools

First, you need to install Node.js. To get a better performance, it is important that you install version 10 or higher. This is because last versions of Node.js include big integer support and web assembly compilers that help run code faster.

Then, install circom and snarkJS. You can do it running the following commands:

npm install -g circom
npm install -g snarkjs

My first circuit

Let's create a circuit that will allow you to prove that you are able to factor a number.

  1. Create an empty directory called factor where you will put all the files.

mkdir factor
cd factor

In a real circuit, we recommend you to create a git repository with a circuits directory and a test directory with all your tests, and the needed scripts to build all the circuits.

2. Create a new file named circuit.circom with the following content:

template Multiplier() {
signal private input a;
signal private input b;
signal output c;
c <== a*b;
}
component main = Multiplier();

This circuit has two private input signals named a and b and one output named c. The circuit named Multiplier circuit forces the signal c to be the result of the multiplication a*b. After declaring the template Multiplier, we need to instantiate it with a component named main.

When compiling a circuit a component named main must always exist.

3. We are now ready to compile the circuit. Run the following command:

circom circuit.circom --r1cs --wasm --sym

With these options we generate three types of files:

  • --r1cs: it generates the file circuit.r1cs that contains the R1CS constraint system of the circuit in binary format.

  • --wasm: it generates the file circuit.wasm that contains the Wasm code to generate the witness.

  • --sym : it generates the file circuit.sym , a symbols file required for debugging or for printing the constraint system in an annotated mode.

My first zero-knowledge proof

Now that the circuit is compiled, we will use snarkJS to generate and validate zk-SNARK proofs. More specifically, we will prove that we are able to factor the number 33. That is, we will show that we know two integers a and b such that when we multiply them, it results in 33.

You can always access the help ofsnarkJS by typing the command

$ snarkjs --help

  1. With snarkJS, you can get general statistics of the circuit and print the constraints. Just run:

snarkjs info -c circuit.r1cs
snarkjs print -r circuit.r1cs -s circuit.sym

2. To generate and verify zk-SNARK proofs, it is required to have a setup.

The generation of the setup has two phases: the first step consists in the creation of some values known as powers of tau. For this tutorial we will keep a simple "powers of tau" file. Run the following commands:

// Start a new powers of tau ceremony and make a contribution
snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v

You will be prompted to enter some random text that will be used as source of entropy. Once you have entered you text, the commands will output a transcript named pot12_001.ptau. This first part of the setup is generic and useful to any circuit, so you can save it to reuse it in future projects.

The generation of the setup is necessary to obtain the proving and verification keys of zk-SNARK proofs. This step requires the generation of some random values that need to be eliminated. This elimination process is crucial: if these values are ever exposed, the security of the whole scheme is compromised.

To construct the setting, we use a multi-party computation (MPC) ceremony that allows multiple independent parties to collaboratively construct the parameters. With MPC, it is enough that one single participant deletes its secret counterpart of the contribution in order to keep the whole scheme secure.

The construction of the trusted setup has two phases: a general MPC ceremony that is valid for any circuit (known as powers of tau ceremony), and a second phase (phase 2) that is constructed for each specific circuit. Anyone can contribute with their randomness to the MPC ceremonies and typically, before getting the final parameters, a random beacon is applied.

For the sake of simplicity, we generated a setup with only one contribution without giving all the details. If you want to deepen into robust setups with multiple contributions:

  • You can download the file powersOfTau28_hez_final.ptau, which contains 54 contributions and a final random beacon, and use it to import and export challenges.

  • If you want to run your own MPC ceremonies or learn about generating trusted setups with snarkJS, we recommend you to follow this other tutorial.

The next step, which is called phase 2 of the setup, is circuit-specific. First, we have to prepare the phase running the command:

snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v

The generation of phase 2 is similar to what we did with powers of tau. In this case, we will generate a .zkey file that will contain the proving and verification keys together with all phase 2 contributions.

// Start a new zkey and make a contribution
snarkjs zkey new circuit.r1cs pot12_final.ptau circuit_0000.zkey
snarkjs zkey contribute circuit_0000.zkey circuit_final.zkey --name="1st Contributor Name" -v

As before, you will be prompted to enter some random text to provide a source of entropy. The output will be a file named circuit_final.zkey, which we will use to export the verification key.

snarkjs zkey export verificationkey circuit_final.zkey verification_key.json

Now, the verification key from circuit_final.zkey is exported into the file verification_key.json.

You can always verify that the computations of a .ptau or a .zkey file are correct:

snarkjs powersoftau verify pot12_final.ptau
snarkjs zkey verify circuit.r1cs pot12_final.ptau circuit_final.zkey

If everything checks out, you should see the following at the top of the output:

[INFO] snarkJS: Powers of Tau file OK!
[INFO] snarkJS: ZKey OK!

The command snarkjs zkey verify also checks that the .zkey file corresponds to the specific circuit.

3. Once the setup is ready, we compute a witness for the circuit.

Before creating any proof, we need to calculate all the signals of the circuit that match all the constraints of the circuit. For that, we will use the Wasm module generated bycircom that helps do this job. We simply need to provide a file with the inputs and the module will execute the circuit and calculate all the intermediate signals and the output. The set of inputs, intermediate signals and output is called witness.

In our case, we want to prove that we able to factor the number 33. So, we assign a = 3 and b = 11.

Note that we could assign the number 1 to one of the inputs and the number 33 to the other. So, our proof does not really show that we are able to factor the number 33. At the end of this section, we will add few modifications to the circuit to deal with this problem.

We need to create a file named input.json with the inputs.

{"a": 3, "b": 11}

Now, we calculate the witness and generate a file witness.wtns with the result.

snarkjs wtns calculate circuit.wasm input.json witness.wtns

4. Once the witness is computed, we can generate a zk-proof associated to the circuit and the witness.

snarkjs groth16 prove circuit_final.zkey witness.wtns proof.json public.json

This command generates a proof (Groth16 is a specific zk-SNARK protocol) and outputs two files:

  • proof.json: it contains the zk-SNARK proof.

  • public.json: it contains the values of the public inputs and outputs.

5. To verify the proof, run:

snarkjs groth16 verify verification_key.json public.json proof.json

This command uses the files verification_key.json we exported earlier,proof.json and public.json to check if the proof is valid. If the proof is valid, the command outputs an OK.

A valid proof not only proves that we know a set of signals that satisfy the circuit, but also that the public inputs and outputs that we use match the ones described in the public.json file.

👉 It is also possible to generate a solidity verifier that allows verifying proofs on Ethereum blockchain.

First, we need to generate the solidity code using the command:

snarkjs zkey export solidityverifier circuit_final.zkey verifier.sol

This command takes validation key circuit_final.zkey and outputs solidity code in a file named verifier.sol. You can take the code from this file and cut and paste it in Remix. You will see that the code contains two contracts: Pairing and Verifier. You only need to deploy the Verifier contract.

You may want to use first a testnet like Rinkeby, Kovan or Ropsten. You can also use the JavaScript VM, but in some browsers the verification takes long and the page may freeze.

The Verifier has a view function called verifyProof that returns TRUE if and only if the proof and the inputs are valid. To facilitate the call, you can use snarkJS to generate the parameters of the call by typing:

snarkjs generatecall

Cut and paste the output of the command to the parameters field of the verifyProof method in Remix. If everything works fine, this method should return TRUE. You can try to change just a single bit of the parameters, and you will see that the result is verifiable FALSE.

Bonus track

We have shown that it is possible to generate a proof that shows that we know two factors a and b such that a*b = 33. But this does not prove that we know how to factor the number 33, as we could have chosen a = 1 and b = 33 and in general, for any integer n, choose inputs a = 1, b = n.

In this section, we will modify the circuit so that it is not possible to assign the number 1 to any of the inputs. To do so, we will use the fact that 0 is the only number with no inverse.

👉 Our arithmetic circuits consist of signals that live in the finite field Fp\mathbb{F}_p, where pp is this prime number. In this field, the only element that has no inverse is the number 0.

For example, if we want to impose a != 1, we can impose, equivalently, that (a-1) != 0 , which is true if and only if (a-1) has no inverse. This condition can be translated in terms of constraints: if we define a new variable inva that is the inverse of a, then the condition (a-1)*inva = 1 can only be satisfied if a != 1. The inverse inva can easily be calculated doing 1/(a-1).

Let's modify the circuit according to this:

template Multiplier() {
signal private input a;
signal private input b;
signal output c;
signal inva;
signal invb;
inva <-- 1/(a-1);
(a-1)*inva === 1;
invb <-- 1/(b-1);
(b-1)*invb === 1;
c <== a*b;
}
component main = Multiplier();

Note that the calculation of the inverse 1/(a-1) is done using the simple arrow <-- instead of the double arrow <==. The reason for this, is that the double arrow gathers two actions together:

  • <-- : assign a value to a signal without creating any constraint.

  • === : add a constraint without assigning any value to any signal.

The operation 1/(a-1) does not have the form of a constraint (it is not linear nor quadratic), so we have to use <-- to assign a value and then manually add the constraint (a-1)*inva === 1 to ensure a constraint that captures the previous assignment.

You can read more about the difference between constraints and calculation assignments in the section Signals > Assignment to Signals.

Note: The circuit still has another problem. Since the operations work in Fp\mathbb{F}_p, we should guarantee that the multiplication does not overflow. This can be done by converting the signals to binary and checking that the values are in the range 0...(p-1). This fix is quite complex, so we keep it for further tutorials.