Skip to main content

Circom

For users of the SnarkJS stack.

info

This tutorial corresponds to the circuit type Circom. In particular, follow the guidelines described below if your circuitry is defined via Circom language v2.0.0 or above.

Introduction

We will walk through all the steps to create a Groth16 prover that checks Sudoku solutions. All of the reference code can be found here and is based on web3-master's awesome repo.

The game of Sudoku is played on a 9x9 grid which is further divided into 3x3 squares. A player is presented with an initial grid which is mostly blank, but has a few entries ranging from 1 to 9. To win a player must fill the grid entirely using only numbers 1 through 9 such that:

  1. there are no duplicate entries in any row
  2. there are no duplicates in any column
  3. there are no duplicates within a 3x3 subgrid

It's easy to envision a competitive scenario where users want to claim they have a solution without revealing it. To this end, a circuit must check the conditions (1) through (3) described previously, but there are also many more implicit checks that need to be made. For example, are all of the inputs numbers? If so, are they all in the range [1,9]? Etc. With this preface, we are ready to dive in to the circuit code that you'll upload.

Circuit Configuration

Here is what the circuit directory would look like before you compress everything and upload.

Circuit Directory Structure
📦sudoku
┣ 📂circomlib
┃ ┣ 📜aliascheck.circom
┃ ┣ 📜binsum.circom
┃ ┣ 📜bitify.circom
┃ ┣ 📜comparators.circom
┃ ┣ 📜compconstant.circom
┃ ┗ 📜gates.circom
┣ 📜circuit.circom
┣ 📜utils.circom
┗ 📜sindri.json

The main circuit accepts an initial Sudoku puzzle and a user's private solution and will return whether the solution is valid. The real heavy lifting circuit templates are defined in utils.circom which makes use of basic functions in the circomlib.

caution

You should have exactly one file named circuit.circom in the top level of your directory which contains your main component definition, to make it easier for Sindri's API to determine which file to reference for circuit compilation.

The top level of your circuit upload must have a sindri.json file specifying whether your Circom language files should produce a witness generator from WASM or c++ code. Using c++ as a backend is usually necessary for large circuits. This file should also specify a choice of provingScheme and over which curveName the proof should take place. Currently only groth16 is supported for the proving scheme.

circuit_tutorials/circom/sudoku/circuit/sindri.json
loading...

Sindri API

We'll use Python to demonstrate the steps to upload your circuit to Sindri, compile it, generate proofs, and verify proofs. For this tutorial, each code block shown can be copy/pasted into an interactive Python shell in order to send the API commands and see the results. Alternatively, you can download the full compile_and_prove.py script and run it all at once with python3 compile_and_prove.py. To ensure that you have the scripts and necessary files, you should clone the sindri-resources repository and work within the circom/sudoku directory.

# Clone the sindri-resources repository and move into the `sudoku` tutorial.
git clone https://github.com/Sindri-Labs/sindri-resources.git
cd sindri-resources/circuit_tutorials/circom/sudoku/

You may need to install the python requests module before you continue.

# Installing the requests module for communication with the API
python -m pip install requests

Before uploading the circuit, you will need an API key which will be sent with each request (see API Authentication for instructions to obtain your key for the first time). The code block below loads an API key from the environment variable SINDRI_API_KEY and includes it within any header sent with an API request. You can either modify the script to include your API key directly, or set the SINDRI_API_KEY environment variable before running the script (e.g. by running SINDRI_API_KEY=<my-key> python3 compile_and_prove.py).

The first section of our compilation script pulls in all of the required imports, and prepares our request headers with content type and authentication information.

circuit_tutorials/circom/sudoku/compile_and_prove.py
loading...

Upload & Compile

This circuit will receive a unique identifier which we must include in each request involving our circuit. If you misplace your circuit_id, you can look up your full list of circuits via the circuit list endpoint. This code assumes that the circuit definition is located in a ./circuit/ subdirectory next to the script (this will be the case if you cloned the repository). We will create a tar archive of our circuit directory and upload it to the /circuit/{circuit_id}/create endpoint.

circuit_tutorials/circom/sudoku/compile_and_prove.py
loading...
Output - Create Circuit
{
"circuit_id": "02f803d4-8f4d-461b-a18e-2925d1d302de",
"circuit_type": "circom",
"circuit_name": "sudoku",
"date_created": "2023-12-02T03:58:52.961Z",
"status": "Queued",
"compute_time": null,
"compute_times": null,
"file_sizes": {
"code.tar.gz": 4613,
"total": 4613,
"total_mb": 0.004613,
"total_gb": 4.613e-6
},
"metadata": {
"api_version": "v1.5.15",
"prover_backend_version": "v0.3.0"
},
"worker_hardware": null,
"verification_key": null,
"error": null,
"curve": "bn254",
"degree": null,
"num_constraints": null,
"num_outputs": null,
"num_private_inputs": null,
"num_public_inputs": null,
"num_wires": null,
"proving_scheme": "groth16",
"trusted_setup_file": null,
"witness_compiler": "c++"
}

After Sindri's API has received the data, it will begin compiling the circuit. The compilation process for Circom will transform your raw language files into an executable version of the circuit which is used in any proof for witness generation. Additionally, compilation produces mock proving and verification keys if these files are not found within the uploaded data. For this reason, compilation can be a lengthy process, so we define a loop which will poll until the circuit status is Ready.

circuit_tutorials/circom/sudoku/compile_and_prove.py
loading...
Output - Circuit Polling
Circuit poll exited after 43 seconds with status: Ready
Output - Circuit Detail
{
"circuit_id": "02f803d4-8f4d-461b-a18e-2925d1d302de",
"circuit_name": "sudoku",
"circuit_type": "Circom",
"compute_time": "P0DT00H00M32.123477S",
"compute_times": {
"clean_up": 0.0019759489223361015,
"compile_cpp": 8.080261475872248,
"create_cpp": 0.7626559878699481,
"create_r1cs": 0.6666127759963274,
"download_trusted_setup_file": 0.052195815835148096,
"export_verification_key": 2.2164228637702763,
"extract_code_tarfile": 0.0053771501407027245,
"file_setup": 0.3324133111163974,
"get_r1cs_info": 0.032404554076492786,
"groth16_setup": 18.586857828311622,
"queued": 28.778535,
"save_results": 1.3859535572119057,
"total": 32.12347655184567
},
"curve": "bn-128",
"date_created": "2023-10-30T21:10:13.223Z",
"degree": 14,
"error": null,
"num_constraints": 11906,
"num_outputs": 1,
"num_private_inputs": 81,
"num_public_inputs": 81,
"num_wires": 12150,
"status": "Ready",
"trusted_setup_file": "powersOfTau28_hez_final_14.ptau",
"verification_key": null,
"version": "v1.5.4",
"witness_executable": "c++"
}

Now that the circuit is compiled, we will move on to produce a proof of a sudoku solution's validity. While our tutorial python script performs a proof directly after compilation, you can perform circuit compilation arbitrarily far in advance of requesting any proofs.

Prove

We will use the following puzzle and solution as input to the prove step.

example_solution.json
{
"puzzle": [
0, 0, 2, 0, 0, 0, 9, 7, 0, 8, 0, 0, 0, 0, 5, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 8, 0, 2, 3, 0, 0, 0,
0, 0, 5, 0, 0, 1, 0, 0, 0, 0, 7, 0, 0, 9, 0, 4, 0, 0, 0, 0, 0, 0, 0, 7, 8, 0, 0, 3, 0, 0, 1, 0,
0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 9, 5, 2, 0, 0, 0
],
"solution": [
4, 5, 2, 3, 1, 8, 9, 7, 6, 8, 6, 3, 7, 9, 5, 4, 1, 2, 7, 9, 1, 4, 2, 6, 3, 8, 5, 2, 3, 7, 1, 8,
4, 6, 5, 9, 6, 1, 5, 2, 3, 9, 7, 4, 8, 9, 8, 4, 5, 6, 7, 1, 2, 3, 5, 7, 8, 6, 4, 3, 2, 9, 1, 3,
2, 9, 8, 7, 1, 5, 6, 4, 1, 4, 6, 9, 5, 2, 8, 3, 7
]
}

The file above is loaded into a JSON formatted string and sent to the /circuit/{circuit_id}/prove endpoint. Right after we send the request for a proof, we start polling. Details about the proof are requested every second until the proof status field indicates the proof is ready.

circuit_tutorials/circom/sudoku/compile_and_prove.py
loading...
Output - Proof Generation
Proof ID: 0b87604f-4e48-47f6-9429-464a8b11a88c
Proof poll exited after 14 seconds with status: Ready

The prove step creates both a result, or output from the circuit, and a proof. We can access both of these via the include_public and include_proof parameters within a proof detail request. Note that until the proof is successfully generated, both those parameters will be empty in the proof detail response. We save them all out to disk as JSON files in our script for use during local verification.

circuit_tutorials/circom/sudoku/compile_and_prove.py
loading...
Output - Proof Detail
{ "circuit_id": "02f803d4-8f4d-461b-a18e-2925d1d302de",
"circuit_name": "sudoku",
"circuit_type": "Circom",
"compute_time": "P0DT00H00M02.035859S",
"compute_times": {...},
"date_created": "2023-10-30T21:11:15.870Z",
"error": None,
"perform_verify": False,
"proof": {...},
"proof_id": "0b87604f-4e48-47f6-9429-464a8b11a88c",
"proof_input": None,
"prover_implementation": {...},
"public": [...],
"status": "Ready",
"verification_key": {...},
"version": "v1.5.4"}
[1, 0, 0, 2, 0, 0, 0, 9, 7, 0, 8, 0, 0, 0, 0, 5, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 8, 0, 2, 3, 0, 0, 0, 0, 0, 5, 0, 0, 1, 0, 0, 0, 0, 7, 0, 0, 9, 0, 4, 0, 0, 0, 0, 0, 0, 0, 7, 8, 0, 0, 3, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 9, 5, 2, 0, 0, 0]

The contents of proof_detail["public"] are all the outputs of the circuit followed by an public inputs. If you refer to the circuit definition, you can see that circuit.circom has one output indicating whether the solution is valid. The following 81 entries copy the public puzzle input. As an exercise, you could change any entry of example_solution.json and watch the first bit of public_output change from a 1 to a 0.

Verify

The code block above saved the proof, public variables, and verification key to JSON files which snarkjs can read in. The following verifier script can be invoked via python3 verify.py.

circuit_tutorials/circom/sudoku/verify.py
loading...
Output - Proof Verification
Verification successful
Verifier output: [INFO] snarkJS: OK!

As the output shows, our proof was verified successfully! As an exercise, you could change the first bit of public.json to 0 to claim the solution was invalid and observe that the output of the script indicates a failure to verify the proof.