Building a Simple zK Circuit (Developers)

AJTECH0001 - Jul 14 - - Dev Community

Understanding Zero-Knowledge Proofs (ZKPs)
In this tutorial we will introduce you to the ZK tech-stack and the tools you can use to build ZK applications for the Celo blockchain.

**Learning Objectives
At the end of this chapter, you will:

  • Understand the zK tech stack

  • Build a simple zK Circuit

An Overview of the Celo zK Tech Stack

The Celo blockchain is a Layer 1 protocol and its light client is based on ZK features. However there are rumors and proposals that Celo may transition to a Layer-2 model with a zkEVM.

When looking at ZK applications in Celo at a high level, we can separate the techstack into two sections based on Prover and Verifier.

The Prover side is executed off-chain on a user machine while the Verification takes place on-chain. Now let's dive deeper into the different tools developers can use to develop ZK applications.

Image description

The zK Stack

The main individual components of this stack include the:
1) Celo EVM,
2) prover, and
3) domain-specific language (DSL).

Image description

  1. The Celo EVM

This is Celo’s Solidity execution environment where smart contract logic would be executed. When designing your ZK application you can write code in solidity that can verify ZK proofs on chain. This is a powerful ability because now your contracts can act based on off-chain proofs (we will expand on this shortly).

  1. The Prover

As discussed in previous chapters, a Prover is software that given some input can generate a Proof of some computation. Today there are two mainstream provers that are popular amongst the ZK community: Groth16 provers and plonk based provers.

  1. DSL

A DSL (Domain Specific Language) is a programming language (just like Javascript or C++) but is designed to allow developers to express ZK Circuits easily. A ZK Circuit is the component that allows a user to generate a proof (e.g., prove that you paid X-amount of USDC to a client).

A popular DSL for SnarkJS a Groth16 prover is Circom, while a popular DSL for Plonk provers is HALO2.

Let's have a look at an example showing how all these elements come together.

Going over the project
In this tutorial we will be using circom and snarkjs to build our circuits and generate our proofs. If you are using VS code there is a syntax highlighter for circom for VSCode.

Setting up your dependencies
Install nodejs (LTS should be good), and make sure you have nvm installed.
Clone this repo:
Clone the submodules: git submodule update --init --recursive
Install the packages with npm i

Circom and SnarkJS

Circom is the DSL we will be writing during this tutorial. SnarkJS is a program developed by IDEN3 which allows you to generate all files required to generate and verify your compiled Circom circuits. To understand the relation between Caricom and SnarkJS, let's review a common developer flow.

Image description

The diagram above shows the flow of how Snarkjs takes a circuit - a pre generated power of tau (ptau) file and public inputs - and is able to generate verifying and proving keys. These keys can then generate proofs and verify them. In this tutorial we will be using Groth16 which requires a trusted setup and thus for each circuit you will have to generate the setup.

  1. What are Ptau Files ?

The .ptau file is related to the "Powers of Tau" ceremony, which is a multi-party computation (MPC) protocol. The Powers of Tau ceremony is a one-time setup that generates a common reference string (CRS) needed to construct and verify zk-SNARK proofs. This CRS is also known as the "structured reference string" (SRS). During the ceremony, participants contribute randomness to the protocol, and the .ptau file accumulates these contributions. The purpose of this is to ensure that as long as one participant in the ceremony is honest and destroys their "toxic waste" (the randomness they used), the final SRS can be considered secure. It's a foundational step in preparing the environment for secure, succinct, and non-interactive proofs where no single party can have enough information to cheat the system.

  1. What is a Witness?

In zero-knowledge proofs, a witness refers to the set of values that satisfy the constraints of the arithmetic circuit (or R1CS in the case of zk-SNARKs) for a given instance. Essentially, it is the private input that, along with the public inputs, makes the statement being proven true. The witness demonstrates that the prover knows a solution to the problem without revealing the solution itself.

To generate a proof we first need private and public inputs. If you were to build a full ZK sudoku application, then you would probably receive this information from the user's moves inside the application’s UI.

Building a Simple Circuit

Let's begin with building a simple circuit to understand the core basics of writing Circom and compiling it with Snarkjs. In this example circuit, given a private input sideLength of a square proves that you know some square of size X without revealing the sideLength.

template SquareAreaProof() {

signal private input sideLength; // The side length of the square, a private input

signal input declaredArea;       // The declared area of the square, a public input

signal output isValid;           // Output signal to indicate if the proof is valid



// Calculate the actual area by squaring the side length

signal actualArea;

actualArea <== sideLength * sideLength;



// Check if the declared area matches the actual area

isValid <== actualArea === declaredArea;
Enter fullscreen mode Exit fullscreen mode

}

component main = SquareAreaProof();

Let’s define the following key terms: private input, input and output.

private input is a value which only the prover should know.

input is a public input, this input is a value known to the general public.

output is simply a boolean value describing if the proof has passed or not.

If we examine the logic of our circuit we perform a calculation based on the private input sideLength. The computation is simple:

signal actualArea;

actualArea <== sideLength * sideLength;
Enter fullscreen mode Exit fullscreen mode

We calculate the area based on the input. We then proceed to validate that the public input declaredArea is in fact the same as actualArea. These conditions generate constraints which generate a proof. I suggest that you read the official Circom language documentation to gain a more in depth understanding of the Circom language.

Build Another Simple Circuit

Challenge: Write a circuit that can verify the area of a triangle.

Image description

h - height from base

b - length of base

I suggest you approach this exercise in the following steps:

  1. Define which inputs are private and which inputs are public.

  2. Define the core logic

  3. Define the constraint to check if the proof is valid.

Using Dependencies
Every time we create a circuit we are creating a method, and similar to other programming languages, methods can be exported and reused.

template MyCircuit() {

This template (MyCircuit) can be imported by other circuits and used to encapsulate logic. A popular example is circomlib, a collection of common circuits ranging from bit operators to hash functions. Let's see how you can use circomlib.

First let's create a folder. Name it as you wish, and then enter that folder.

`mkdir

cd `

Second, initiate a new Node project with npm and install circomlib.

`npm init

npm i circomlib`

For this example let's use circomlibs poseidon hash.

Create a new circuit named PoseidonHasher.

`pragma circom 2.0.0;

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

template PoseidonHasher() {

signal input in;

signal output out;

component poseidon = Poseidon(1);

poseidon.inputs[0] <== in;

out <== poseidon.out;

}

component main = PoseidonHasher();`

In the second line we import poseidon.circom we are then able to use it easily, we just saved ourselves many hours of development by importing a ready made primitive.

. . . . . .