What are Zero Knowledge Proofs: A Beginner's Guide to Creating Your First ZK Proof with Circom and SnarkJS on Scroll Network

Azan AdnanAzan Adnan
18 min read

Zero-knowledge proofs are getting popular, but it's not easy for beginners to find the right info to start coding with them. As the digital landscape evolves and concerns about privacy and security escalate, understanding the intricacies and potential applications of ZKPs becomes increasingly crucial I've been learning about them myself and thought I'd share what I've figured out. This article is for beginners like me, so if you see anything wrong, let me know!

In this article, I'll explain what zero-knowledge proofs are, how developers use them, and talk about Circom SnarkJS on Scroll Networks. We won't dive into the complex math stuff; instead, we'll view zero-knowledge proofs like a mystery tool that does cool crypto stuff.

What are Zero Knowledge Proofs?

Wikipedia defines Zero Knowledge Proofs as:

In cryptography, a zero knowledge proof or zero-knowledge protocol is a method by which one party (the prover) can prove to another party (the verifier) that a given statement is true, while avoiding conveying to the verifier any information beyond the mere fact of the statement's truth. The intuition underlying zero-knowledge proofs is that it is trivial to prove the possession of certain information by simply revealing it; the challenge is to prove this possession without revealing the information, or any aspect of it whatsoever.

For instance, picture this scenario: You need to prove to a friend that you own a specific valuable item, but you don't want to disclose what it is. Using a ZKP-based ownership verification system, you could demonstrate ownership without revealing the identity of the item itself.

This idea started when Goldwasser, Micali, and Rackoff did important work in the 1980s. Since then, many people have studied it and found ways to use it in different areas.

The main components of a ZKP are:

  1. Prover: The party that wants to convince the verifier of the truth of a statement without revealing the statement itself.

  2. Verifier: The party that checks the proof provided by the prover.

  3. Statement: The mathematical expression that the prover intends to demonstrate to the verifier.

  4. Proof: The evidence offered by the prover to persuade the verifier that the statement is true.

Zero-Knowledge Proofs (ZKPs) can be categorized as interactive, where a prover convinces a specific verifier, but must repeat this process for each individual verifier, or non-interactive, where a prover generates a proof that can be verified by anyone using the same proof.

The core principles of ZKP can be defined by the following three attributes:

  • Completeness: If a statement is true, an honest prover can always pass the verifier’s test.

  • Soundness: If the statement is false, no cheating prover can convince an honest verifier that it is true, except with some small probability.

  • Zero-knowledge: If the statement is true, no verifier learns anything other than the fact that the statement is true.

Applications and Use Cases

Zero-knowledge proofs (ZKPs) are a groundbreaking advancement in cryptography. They enable verifying information's authenticity without disclosing it, paving the way for enhanced privacy, security, and efficiency in various industries, including Web3, supply chains, and the Internet of Things.

Following are some of the key use cases of Zero-Knowledge Proofs (ZKPs) in the Web3 landscape:

  • Privacy Enhancement: ZKPs can be used to conceal transaction details, ensuring confidentiality and facilitating private interactions This is particularly important in blockchain scenarios where ZKPs like Zero-Knowledge Succinct Non-Interactive Arguments (ZK-SNARKs) verify blocks without exposing details, enhancing efficiency.

  • Scalability Improvement: Scalability is a common use case for ZKPs in Web3. For instance, solutions that combine the functions of expansion and privacy, such as the development of private Layer 2 public chains like Aztec and StarkNet, leverage ZKPs. Scroll also leverages Zero-Knowledge Proofs (ZKPs) to enhance scalability

  • Regulatory Compliance Assurance: ZKPs can help ensure regulatory compliance in the Web3 landscape. They allow for secure and private data verification without revealing transaction details, which is ideal for various applications that require privacy, security, and compliance.

  • Secure Authentication: ZKPs can be used to authenticate users without revealing their login credentials or personal information. This is particularly useful for online systems in the Web3 landscape that require secure authentication.

I recommend reading these two articles Understanding Zero-Knowledge Proofs Through the Source Code of Tornado CashandHow to Leverage ZKPs as a Web3 Builder

What is a ZK-SNARK?

Zero-Knowledge Succinct Non-Interactive Argument of Knowledge, or zk-SNARK, represents a significant advancement in cryptographic techniques, particularly in the realm of privacy-preserving computation. At its core, zk-SNARKs enable parties to prove knowledge of a statement without revealing any underlying information, making them invaluable tools for enhancing privacy and security in various applications, particularly within blockchain technology.

The key distinguishing feature of zk-SNARKs lies in their succinctness and non-interactivity. Unlike traditional zero-knowledge proofs, which may involve multiple interactions between the prover and verifier, zk-SNARKs allow for a compact proof that can be efficiently verified without the need for additional interactions. This efficiency is crucial for applications where computational resources are limited, such as blockchain networks.

The math behind zk-SNARKs is advanced, relying on a combination of advanced cryptographic primitives, including elliptic curve pairings and algebraic geometry. Without delving too deeply into the technical intricacies, the general idea is to construct a proof that demonstrates knowledge of a solution to a computational problem, while ensuring that the proof itself reveals nothing about the solution beyond its validity.

ZK-SNARKs consist of three main components:

The Prover, the Verifier, and the Succinct Proof. The Prover aims to persuade the Verifier of a statement's truthfulness without disclosing the statement itself. The Succinct Proof enables this process to occur rapidly and effectively.

ZK-SNARK Processes Involves:

  • Key generation : This algorithm, run by a trusted setup, takes a program and secret parameter as input, and generates two public keys: a proving key and a verification key. These keys are specific to the program and used for all proofs related to it.

  • Proof generation: The prover uses their secret witness and the proving key to create a concise mathematical proof. This proof doesn't reveal the witness but allows the verifier to confirm its validity.

  • Verification: The verifier takes the proof, the verification key, and the public statement as input and runs a quick verification algorithm. This confirms if the proof is valid without needing to know the witness or run the complex program itself.

How ZK-SNARKs work:

To create a zk-SNARK, the Prover constructs a 'proof' using polynomial equations, much like cryptographic puzzles. These equations form the foundation of zk-SNARKs, establishing a secure method for conveying truth without revealing it. Randomness is integral to this process.

The Prover introduces randomness into the equations, generating a unique signature for each proof. This randomness acts as a cryptographic shield, preventing the original statement from being reverse-engineered.

In essence, these polynomial equations can only be solved by the Prover but can be verified by anyone. They represent a puzzle with a hidden solution, accessible only to the Prover, yet confirmable by anyone without knowledge of the solution.

ZK-SNARKs with SnarkJS and Circom:

Circom:

is a programming language and a compiler for designing arithmetic circuits that are compiled to R1CS. Most practical ZK systems require the computation to be expressed as an arithmetic circuit that is encoded as a set of equations called rank-1 constraint system (R1CS). Circom can be complemented with Snarkjs, a library for generating and validating ZK proofs from R1CS.

SnarkJS:

serves as an implementation of the zk-SNARK protocol, developed entirely in JavaScript. Snarkjs is a JavaScript and Pure Web Assembly implementation of zkSNARK and PLONK schemes. It uses the Groth16 Protocol, PLONK, and FFLONK. This library includes all the tools required to perform trusted setup multi-party ceremonies. Circom is specifically designed to integrate smoothly with SnarkJS. Essentially, any circuit designed within Circom can be easily utilized within SnarkJS.

SnarkJS and Circom have several benefits:

  • User-Friendly Framework: Circom simplifies the creation of arithmetic circuits with its easy-to-use interface. It hides the complexity of proving mechanisms, making it simple for developers to build and confirm ZKP systems.

  • Efficiency and Security: Circom excels at crafting circuits involving intricate math computations. Developers can express these computations conveniently and intuitively in their code.

  • Community and Resources: Developers diving into SnarkJS and Circom have plenty of support available. There are extensive documentation, tutorials, and a vibrant community of fellow developers ready to offer assistance and guidance.

A Real-world Use Case:

ZKP for Supply Chain Provenance:

In the realm of supply chain management, ZKPs offer a compelling solution for ensuring product authenticity while preserving sensitive information. By leveraging cryptographic techniques, manufacturers can demonstrate the legitimacy of their products without disclosing confidential production processes or trade secrets. This fosters trust and transparency throughout the supply chain, benefiting businesses and consumers alike.

Benefits of ZKPs in Supply Chains:

  • Enhanced Trust and Transparency: Consumers can gain confidence in product authenticity without manufacturers needing to disclose confidential production processes.

  • Improved Efficiency: Verifying product legitimacy on-chain using ZKPs is significantly faster and cheaper than traditional methods.

  • Scalability: ZKPs allow for efficient verification of large amounts of data without overloading the blockchain.

Verifying Product Origin:

Imagine a scenario where a clothing manufacturer sources organic cotton from specific farms. To establish authenticity for consumers, they can leverage ZKPs powered by SnarkJS & Circom:

  1. Data Representation:

    • Creating a circuit in Circom to represent the supply chain stages: cotton farming, yarn production, fabric weaving, garment manufacturing, and distribution.

    • Encoding sensitive data (e.g., farm locations, processing techniques) into private inputs using techniques like range proofs or Merkle trees.

  2. Proof Generation:

    • The manufacturer runs the circuit with private inputs, generating a SNARK proof (a succinct, non-revealing proof). This proof mathematically assures the authenticity claim without disclosing the underlying data.
  3. Proof Verification:

    • Anyone can verify the proof using the public circuit and verification key, confirming the product's origin without learning the confidential details.

Leveraging SnarkJS Circom on Scroll

What is Scroll?

Scroll is a Layer 2 solution for Ethereum, designed to enhance scalability and performance of Ethereum-based applications. It uses advanced zero-knowledge proof technology and is EVM-compatible, meaning it can run Ethereum smart contracts as-is.

Features of Scroll:

Scalability: Scroll extends Ethereum’s capabilities, enabling apps to scale without any surprises.

EVMCompatibility: Thanks to bytecode-level compatibility, existing Ethereum apps can migrate onto Scroll as-is, and at a significantly reduced cost.

Security: Scroll uses advanced zero knowledge proof technology, battle-tested EVM models, and rigorous audits to ensure security and reliability.

What is a circuit?

In the context of Zero-Knowledge Proofs (ZKPs), a circuit is a mathematical construct that represents a computation or a program. The term “circuit” in ZKP is analogous to a circuit in computer science, which is a combination of gates (operations) and wires (inputs and outputs) that perform a specific computation

Setting Up Dependencies and Environment:

Find the code repository referenced in this guide at the following link: Repo.

To follow this guide, you'll need to install the following dependencies:

yay -S nlohmann-json
sudo pacman -S gmp
sudo pacman -S nasm

Next, create an empty project. You can name it anything you like. Then, initialize a Node.js project:

npm init

Install the circomlib package:

npm i circomlib

Additionally, install the latest version of snarkjs:

npm install -g snarkjs@latest

Create a directory named "circuits" and navigate into it:

mkdir circuits

Inside the "circuits" directory, create a file named "verifyOrigin.circom" for your circuit.

Finally, create an empty directory named "build" within the "circuits" folder:

mkdir circuits/build

These steps will set up the necessary environment for your project.

In the "verifyOrigin.circom" file copy the following code:

pragma circom 2.1.3;
template VerifyOrigin() {
    // Private input signals
    signal input originIdentifier;
    // Public input signals
    signal input expectedIdentifier;
    // Output signal (public)
    signal output out;
    // Create a constraint here saying that our input signals must equal each other.
    out <== originIdentifier - expectedIdentifier;
}
component main = VerifyOrigin();

In the above circuit, originIdentifier is the unique identifier for the product origin, which is a private input. expectedIdentifier is the expected identifier for the product origin, which is a public input. The circuit simply checks if these two identifiers are equal.

Compiling our circuit

cd circuits 
circom verifyOrigin.circom --r1cs --wasm --sym -o build

Output:

template instances: 1
non-linear constraints: 0
linear constraints: 0
public inputs: 0
private inputs: 2 (none belong to witness)
public outputs: 1
wires: 2
labels: 4
Written successfully: build/verifyOrigin.r1cs
Written successfully: build/verifyOrigin.sym
Written successfully: build/verifyOrigin_js/verifyOrigin.wasm
Everything went okay

The generated wasm and r1cs files are available in the build folder. To generate the proof, we need a proving key file, and to generate this file, we need a ptau file.

We need a trusted setup for zk-SNARKs, and the Powers of Tau is one such setup.

The trusted setup is a crucial part of zk-SNARKs. It generates a common reference string (CRS) that is used in the proof generation and verification processes. The security of zk-SNARKs relies on the fact that the CRS is generated correctly and that certain secret values used during its generation are properly discarded.

The Powers of Tau is a protocol to generate this CRS in a multi-party computation (MPC) manner, which means that as long as at least one participant is honest and discards their secret values, the resulting CRS is secure.

The size of the Powers of Tau file you need depends on the number of constraints in your circuit. we are using the smallest one (powersOfTau28_hez_final_08.ptau), which supports up to 256 constraints, should be sufficient.

The trusted setup improves the efficiency of the zk-SNARKs system and is necessary for its security, it is also one of the most controversial aspects of zk-SNARKs due to the need to trust that the setup was performed honestly. This is why the Powers of Tau protocol is designed to be a multi-party computation, to distribute the trust among multiple parties.

It is not recommended to use this zkey file for production, but for testing, it will be good for us.

In circuits folder make a folder named ptau and paste this file powersOfTau28.ptau in it.

Generate proving key

It is not recommended to use this zkey file for production, but for testing, it will be good for us. For more info please check the snarkjs documentation .

Now, from the circuits directory, we’ll run the command to generate the proving key that we will use to generate the proof using the R1CS and ptau files:

snarkjs plonk setup build/verifyOrigin.r1cs ptau/powersOfTau28_hez_final_08.ptau build/proving_key.zkey

Output:

[INFO]  snarkJS: Reading r1cs
[INFO]  snarkJS: Plonk constraints: 1
[INFO]  snarkJS: Setup Finished

Foundry Setup

Create a new directory in the project root directory called contracts.

cd contracts and then use the following command to create a new foundry project:

forge init --no-commit

Delete the contents of script, src, and test from the contracts folder and make a .env file by using touch.env and enter your private key in it.

In the src folder make a file name "PlonkVerifier.sol"

Access the "PlonkVerifier.sol" file here: PlonkVerifier.sol

snarkjs zkey export solidityverifier circuits/build/proving_key.zkey contracts/src/PlonkVerifier.sol

Output:

[INFO]  snarkJS: EXPORT VERIFICATION KEY STARTED
[INFO]  snarkJS: > Detected protocol: plonk
[INFO]  snarkJS: EXPORT VERIFICATION KEY FINISHED

Writing smart contract

We’ll write the following contract that utilizes the PlonkVerifier.sol file that we exported above. The contract outputs a Boolean true or false.

Make a contract file and name it VerifyOrigin.sol and copy the following code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface IPlonkVerifier {
    function verifyProof(bytes memory proof, uint[] memory pubSignals) external view returns (bool);
}

contract VerifyOrigin {
    address public s_plonkVerifierAddress;

    event OriginVerified(bool result);

    constructor(address plonkVerifierAddress) {
        s_plonkVerifierAddress = plonkVerifierAddress;
    }

    function submitProof(bytes memory proof, uint256[] memory pubSignals) public returns (bool) {
        bool result = IPlonkVerifier(s_plonkVerifierAddress).verifyProof(proof, pubSignals);
        emit OriginVerified(result);
        return result;
    }
}
  1. Interface IPlonkVerifier: This interface declares a function verifyProof that takes a proof and pubSignals as inputs and returns a boolean value.

  2. Contract VerifyOrigin: This is the main contract that interacts with the IPlonkVerifier interface.

    • s_plonkVerifierAddress: This is a public state variable that holds the address of the IPlonkVerifier contract.

    • Event OriginVerified: This event is emitted when a proof is verified. It returns a boolean result indicating the outcome of the verification.

    • Constructor: The constructor is a special function that is executed at the time of contract deployment. It takes the address of the IPlonkVerifier contract as an argument and assigns it to s_plonkVerifierAddress.

    • Function submitProof: This function takes a proof and pubSignals as inputs, calls the verifyProof function of the IPlonkVerifier contract, emits the OriginVerified event with the result of the verification, and returns the result.

Now Build the contract by running this command in the contracts folder:

forge build

Output:

[⠊] Compiling...
[⠆] Installing Solc version 0.8.24
[⠰] Successfully installed Solc 0.8.24
[⠑] Compiling 2 files with 0.8.24
[⠘] Solc 0.8.24 finished in 174.12ms
Compiler run successful

Deploying on Scroll Sepolia Testnet:

Access the faucet for the Scroll Sepolia Network here: Faucet Link

Deployment Script:

Make a contract file and name it "VerifyOrigin.s.sol" and copy the following code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "../lib/forge-std/src/Script.sol";
import "../src/PlonkVerifier.sol";
import "../src/VerifyOrigin.sol";

contract VerifyOriginScript is Script {
    function setUp() public {}

    function run() public {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        PlonkVerifier pv = new PlonkVerifier();
        VerifyOrigin vo = new VerifyOrigin(address(pv));

        vm.stopBroadcast();
    }
}

Use the following command to deploy the script on scroll sepolia:

forge script contracts/script/VerifyOrigin.s.sol VerifyOriginScript  --broadcast --rpc-url https://sepolia-rpc.scroll.io/

You’ll see two contracts deployed. The first one is the PlonkVerifier contract, and the second is the VerifyOrigin contract. We only need the address of the VerifyOrigin contract

[⠆] Compiling...
No files changed, compilation skipped
Script ran successfully.
EIP-3855 is not supported in one or more of the RPCs used.
Unsupported Chain IDs: 534351.
Contracts deployed with a Solidity version equal or higher than 0.8.20 might not work properly.
For more information, please see https://eips.ethereum.org/EIPS/eip-3855

## Setting up 1 EVM.
==========================
Chain 534351
Estimated gas price: 4.75167 gwei
Estimated total gas used for script: 1748854
Estimated amount required: 0.00830997708618 ETH
==========================

###
Finding wallets for all the necessary addresses...
##
Sending transactions [0 - 1].
⠉ [00:00:00] [###############################################################] 2/2 txes (0.0s)
Transactions saved to: /home/Azan/Documents/scroll-zk-sample/broadcast/VerifyOrigin.s.sol/534351/run-latest.json
Sensitive values saved to: /home/Azan/Documents/scroll-zk-sample/cache/VerifyOrigin.s.sol/534351/run-latest.json

##
Waiting for receipts.
⠙ [00:00:06] [###########################################################] 2/2 receipts (0.0s)
##### scroll-sepolia
✅  [Success]Hash: 0x344a5ced9647b1d81e4f06d6065203ab7d80e48b65413eedc0f96a16abdd6bfd
Contract Address: 0xD3161FDc1aD8AeE97A5A20876beeb272F89f6C69
Block: 2951631
Paid: 0.00504447740874 ETH (1061622 gas * 4.75167 gwei)

##### scroll-sepolia
✅  [Success]Hash: 0xb050a872fc8b0f13f55d9419d4950cf60d9a5dc37fe971a3fbdcedd97ce9f4c2
Contract Address: 0xEC8445BFa8E9B40FdD2983A1591a2688c08e039D
Block: 2951631
Paid: 0.00134955505839 ETH (284017 gas * 4.75167 gwei)


Transactions saved to: /home/Azan/Documents/scroll-zk-sample/broadcast/VerifyOrigin.s.sol/534351/run-latest.json
Sensitive values saved to: /home/Azan/Documents/scroll-zk-sample/cache/VerifyOrigin.s.sol/534351/run-latest.json
==========================

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Total Paid: 0.00639403246713 ETH (1345639 gas * avg 4.75167 gwei)

Transactions saved to: /home/Azan/Documents/scroll-zk-sample/broadcast/VerifyOrigin.s.sol/534351/run-latest.json
Sensitive values saved to: /home/Azan/Documents/scroll-zk-sample/cache/VerifyOrigin.s.sol/534351/run-latest.json

You can view your transactions on the Scroll Sepolia on ScrollScan website.

What is a witness?

Before creating the 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 to do this job.

Let us start with the Wasm code. Using the generated Wasm binary and three JavaScript files, 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.

For big circuits, the C++ witness calculator is significantly faster than the WASM calculator, but in this guide, we are not using the C++ one.

Computing the witness:

We need to create a file named input.json containing the inputs written in the standard json format.

The generate_witness.js script is used to generate a witness from your input and the compiled circuit. It uses the witness_calculator.js module to do this. Here’s how you can use it:

  1. Prepare the Input file: Create a JSON file with your inputs in the verifyOrigin_js folder. For example, you can create an input.json file with the following content:
{
    "originIdentifier": 123,
    "expectedIdentifier": 123
}
  1. Generate the Witness: Run the generate_witness.js script with the compiled circuit (.wasm file), the input file, and the output file for the witness as arguments. Here’s an example command:
node generate_witness.js verifyOrigin.wasm input.json witness.wtns

Replace verifyOrigin.wasm with the path to your .wasm file, input.json with the path to your input file, and witness.wtns with the path where you want to save the witness.

  1. Generate the Proof: After generating the witness, you can use snarkjs to generate the proof using Plonk, which will generate two files proof.json and public.json:
snarkjs plonk prove proving_key.zkey witness.wtns proof.json public.json

Replace circuit_final.zkey with the path to your .zkey file.

Verifying from Smart Contract:

Below is a sample script showcasing the utilization of the submitProof function within your VerifyOrigin contract. It's imperative to acknowledge that this script remains untested. Your task now is to thoroughly test and validate it to ensure its functionality aligns with your requirements.

const Web3 = require('web3');
const fs = require('fs');

// Connect to the Scroll network
const web3 = new Web3('https://sepolia-rpc.scroll.io/');

// Load the contract ABI
const contractABI = JSON.parse(fs.readFileSync('VerifyOriginABI.json', 'utf8'));

// The address of your deployed contract
const contractAddress = '0xYourContractAddress';

// Create a new contract instance
const contract = new web3.eth.Contract(contractABI, contractAddress);

// Load the proof and public signals
const proof = JSON.parse(fs.readFileSync('proof.json', 'utf8'));
const publicSignals = JSON.parse(fs.readFileSync('public.json', 'utf8'));

// Prepare the proof for the solidity function
const proofFormatted = [
    [proof.pi_a[0], proof.pi_a[1]],
    [[proof.pi_b[0][1], proof.pi_b[0][0]], [proof.pi_b[1][1], proof.pi_b[1][0]]],
    [proof.pi_c[0], proof.pi_c[1]]
];

// Call the submitProof function
contract.methods.submitProof(proofFormatted, publicSignals).call({from: '0xYourAddress'}, function(error, result) {
    if (!error) {
        console.log('Proof verification result: ', result);
    } else {
        console.error(error);
    }
});

Replace '0xYourContractAddress' with your deployed contract address and '0xYourAddress' with the address you want to send the transaction from.

Conclusion

In conclusion, we've journeyed through the fascinating world of zero-knowledge proofs, delving into the intricacies of zk-SNARKs, and exploring the practical applications of snarkjs and circom. We've seen how these powerful tools can be harnessed to create robust, secure, and efficient systems.

The step-by-step guide provided in this article has hopefully demystified the process of coding and deploying on Scroll Sepolia, making it accessible to developers at all levels. As we continue to push the boundaries of what's possible with these technologies, it's exciting to imagine what the future holds.

The field of cryptography is vast and constantly evolving. Stay curious, keep experimenting, and continue to build on the knowledge you've gained here. Happy coding!

If you want to learn further in detail here is the link: ZK Whiteboard Sessions.
For more, follow me here on Twitter and on LinkedIn as well! Cheers!

2
Subscribe to my newsletter

Read articles from Azan Adnan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Azan Adnan
Azan Adnan