[VerifyPreimage Circuit] Circom and SnarkJS

[VerifyPreimage Circuit] Circom and SnarkJS
none 0.0 0

Introduction

Zero-knowledge proofs (ZKPs) enable privacy-preserving verification of information, and Circom is a popular tool for building ZK circuits. In this tutorial, we’ll create a zk circuit and the goal is to prove that a user knows a “preimage” (a secret input) that hashes to a specific value, without revealing the preimage itself. Additionally, it generates a nullifier to prevent double usage of the preimage, which is essential in applications like privacy-preserving payments, identity verification, and anonymous voting.

Revisiting Key Concepts

  1. Prover and Verifier: The prover generates a proof that the verifier can use to confirm a statement’s validity without learning any details.
  2. Proof: A piece of cryptographic evidence that convinces the verifier.
  3. Witness: Hidden input data used to construct the proof.
  4. Circuit: A set of logical steps used to create a proof. It defines how the inputs relate to the outputs.
  5. Constraint System: Rules combining inputs using mathematical and cryptographic operations.
  6. Trusted Setup: A one-time process to generate cryptographic keys for the prover and verifier to use.

The combination of these elements allows zero-knowledge proofs to provide privacy, security, and verification without revealing sensitive information.

To learn more, read this introduction

Introduction to Zero-Knowledge Proofs

Tools and Environment Setups.

To build and run ZKPs, you’ll need the following tools installed on your system:
Note: you can use zkREPL as a faster and easier web based editor with built in circom compiler.

  1. Node.js: JavaScript runtime to run SnarkJS scripts.
  2. Rust: Used to compile Circom.
  3. Circom: A specialized programming language for ZK circuits.
  4. SnarkJS: A JavaScript library for generating and verifying ZKP proofs.

Step 1: Install Prerequisites

For installation and project setup, check out Part1 of this section in the series.

Building the Circuit

Create a file called verifypreimage in the circuits/ folder.

verifypreimage.circom

pragma circom 2.1.6;

include "circomlib/poseidon.circom";
// include "https://github.com/iden3/circomlib/blob/master/circuits/poseidon.circom";

template VerifyPreimage () {

    //public inputs
    signal input hash;
    signal input address;

     //private inputs
     signal input preimage;

     // output
     signal output nullifier;


    // hash the preimage and check if the result matches the hash.
    component hasher = Poseidon(1);
    hasher.inputs[0] <== preimage;

    hasher.out === hash;

    // the contract should keep track of seen nullifiers so as to prevent double usage.

    component nullifierHasher = Poseidon(2);
    nullifierHasher.inputs[0] <== address;
    nullifierHasher.inputs[1] <== preimage;

    nullifier <== nullifierHasher.out;  
}

component main = VerifyPreimage();

/* INPUT = {
    "hash": "",
} */

Code Walkthrough

Imports

include "circomlib/poseidon.circom";
  • This imports the Poseidon hash function from the circomlib, a library of cryptographic primitives commonly used in zk-SNARK circuits.

Signals Declaration

// Public inputs
signal input hash;        // Public hash to be verified
signal input address;     // Public address (used to compute the nullifier)

// Private inputs
signal input preimage;    // Private preimage (user's secret input)

// Output
signal output nullifier;  // Public output that allows tracking preimage usage
  • Public Inputs:
    • hash: This is the known hash value that we want to verify. It will be used to check if it matches the hash of the secret preimage.
    • address: A public address (could be an Ethereum address or a unique identifier) that gets combined with the preimage to generate a nullifier. This ensures that users cannot reuse the same preimage twice for the same address.
  • Private Input:
    • preimage: This is the secret value the user knows, and it will be hashed to check if it matches the public hash.
  • Output:
    • nullifier: A unique identifier that can be publicly tracked to prevent double usage of the same preimage for the same address.

Hashing the Preimage

component hasher = Poseidon(1);
hasher.inputs[0] <== preimage;

hasher.out === hash;
  • Hashing Process:
    • This creates a Poseidon hash component (hasher) that takes 1 input (the preimage).
    • The preimage is passed into the Poseidon hashing function.
    • The output of the Poseidon hash is compared to the public hash input using hasher.out === hash;.
    • This constraint ensures that the preimage provided by the prover produces the exact hash that matches the public input. If this constraint fails, the proof is invalid.

Generating the Nullifier

component nullifierHasher = Poseidon(2);
nullifierHasher.inputs[0] <== address;
nullifierHasher.inputs[1] <== preimage;

nullifier <== nullifierHasher.out;
  • Nullifier Generation:
    • A second Poseidon hash is created to produce a nullifier.
    • This hash takes 2 inputs:
      1. Address (user’s public identifier)
      2. Preimage (user’s secret)
    • The output of this hash becomes the nullifier.
    • This nullifier ensures that for a given (address, preimage) pair, a unique identifier is produced.
    • By keeping track of these nullifiers on-chain, the smart contract can detect and prevent reuse of the same preimage with the same address.

Main Component

component main = VerifyPreimage();
  • This creates the main component for the circuit. It instantiates the VerifyPreimage template, so when the circuit is compiled, the main logic runs.
  • The main component expects inputs for hash, address, and preimage and outputs the nullifier.

How it Works

  1. Prover’s Role:
  • The prover (user) knows a preimage and provides it as a private input.
  • They claim that this preimage hashes to a specific public hash value.
  • The prover also provides a public address (possibly linked to their wallet or account) that is used to generate a nullifier.
  1. Verifier’s Role:
  • The verifier checks the following conditions:
    • Preimage Verification: Does the Poseidon hash of the provided preimage equal the public hash? poseidon(preimage) === hash
    • Nullifier Calculation: Computes the nullifier for the (address, preimage) pair. poseidon(address, preimage)
    • This nullifier can be stored on-chain to prevent the prover from using the same preimage again.

Compiling the Circuit

Run the following command to compile the circuit:

circom circuits/verifypreimage.circom --r1cs --wasm --sym -o output/

Let’s look at each file:

  • –r1cs: Generates the Rank 1 Constraint System (R1CS) file.

  • –wasm: Outputs a WebAssembly file for computation.

  • –sym: Outputs a symbol file for debugging.

  • -o output/: Specifies the output directory.

Check for the following files in output/:

  • verifypreimage.r1cs: The constraint system file.

  • verifypreimage.wasm: The WebAssembly file.

  • verifypreimage.sym: The symbol file for debugging.

Generating Trusted Setup

Generate the keys for the prover and verifier:

wget https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_10.ptau -O outputs/pot10.ptau
snarkjs groth16 setup outputs/verifypreimage.r1cs outputs/pot10.ptau outputs/verifypreimage_final.zkey

Export the Verification Key

snarkjs zkey export verificationkey outputs/verifypreimage_final.zkey outputs/verification_key.json

Creating Inputs

Create an input file input/input.json with the following content:

{
 "hash": "3cb6bc651b03d994974629eacef9696311d85c2ca9e3d08cbdf01e1833c2a405",
"address": " 0x03C64d863cB2a39881f1702EBED5D46C314F66e2",
"preimage": "54112"
}

These Inputs will be used to generate the proof.

Generating the Proof

  1. Generate Witness

node outputs/verifypreimage_js/generate_witness.js outputs/verifypreimage_js/verifypreimage.wasm inputs/input.json outputs/witness.wtns
  1. Generate the Proof

snarkjs groth16 prove outputs/verifypreimage_final.zkey outputs/witness.wtns outputs/proof.json outputs/public.json

  1. Verify the Proof

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

If everything is setup correctly, you should see:

snarkJS: OK!

Summary

  1. Design the Circuit: Create the logic of the proof in a Circom file.

  2. Compile the Circuit: Convert the logic to constraint systems and WebAssembly files.

  3. Generate Trusted Setup: Produce keys for the prover and verifier.

  4. Input Data: Provide the inputs for the computation.

  5. Generate Witness: Produce intermediate computation results.

  6. Generate Proof: Create a proof from the witness.

  7. Verify Proof: Use the verifier to check the proof’s validity.

Use Cases

  1. Anonymous Payments (like Tornado Cash):

    • The user can deposit funds and then later withdraw them anonymously.
    • When they withdraw, they reveal the preimage, and the contract verifies it against the nullifier.
    • If the same preimage is used again, the contract will reject it since the nullifier would already exist.
  2. Anonymous Voting:

    • Voters submit their preimage as their “vote”, and their votes are hashed.
    • Once their vote is cast, the system tracks the nullifier to ensure that the voter cannot vote twice.
  3. Identity Proofs:

    • A user can prove they possess certain credentials (like a driver’s license or degree) without revealing it.
    • The user proves that the preimage hashes to a known hash value, and the system tracks the nullifier to ensure the user doesn’t reuse it for multiple claims.

Conclusion

Congratulations! You’ve completed a hands-on implementation of zero-knowledge proofs. This process may seem complex initially, but as you create more advanced circuits, you’ll see how useful ZKPs can be for privacy-preserving computation.

We encourage you to continue your exploration of topics on zk proofs. If you’re interested in diving deeper, you can follow up on the pathway here Zero-Knowledge Proofs on the Celo Blockchain: A Comprehensive Tutorial Series - Pathways - Celo Academy

Resources

Circom docs
SnarkJS
0xparc Circom Workshop

About the author

I’m Jonathan Iheme, A full stack block-chain Developer from Nigeria. With a great passion for Zero Knowledge Technology.

linkedIn
Twitter