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.
The zK Stack
The main individual components of this stack include the:
1) Celo EVM,
2) prover, and
3) domain-specific language (DSL).
- 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).
- 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.
- 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.
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.
- 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.
- 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;
}
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;
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.
h - height from base
b - length of base
I suggest you approach this exercise in the following steps:
Define which inputs are private and which inputs are public.
Define the core logic
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.