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 will implement a Group Signature verification scheme. The goal of this circuit is to allow a prover to prove membership in a group of public keys without revealing which specific key (or secret key) they possess.
This concept is often used in anonymity-preserving cryptography (like ring signatures or group signatures) to prove that a user belongs to a group without revealing their specific identity. It’s similar to how Zcash’s zk-SNARKs work, enabling privacy in transactions.
Key Concepts:
Group Signatures
A group signature allows a member of a group to sign a message anonymously on behalf of the group. The verifier can confirm that a valid signature was made by someone from the group but won’t know which member signed it.
This concept is used in:
- Privacy-preserving payments (like Zcash or Tornado Cash)
- Anonymous voting in decentralized governance
- Proof of Membership for access control in zero-knowledge systems
How This Circuit Works
- Key Generation:
- Each member of the group generates a public key (pk) from a secret key (sk).
- Public keys are computed using a hash function (Mimc Sponge) applied to
sk
.
- Membership Proof:
- The prover provides a secret key (sk).
- The circuit computes the corresponding public key (pk).
- The circuit checks if this pk matches one of the provided group public keys (pk1, pk2, pk3).
- If the public key matches one of the known public keys, the proof is valid, proving that the prover is a member of the group.
ZK Concepts
- Prover and Verifier: The prover generates a proof that the verifier can use to confirm a statement’s validity without learning any details.
- Proof: A piece of cryptographic evidence that convinces the verifier.
- Witness: Hidden input data used to construct the proof.
- Circuit: A set of logical steps used to create a proof. It defines how the inputs relate to the outputs.
- Constraint System: Rules combining inputs using mathematical and cryptographic operations.
- 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.
- Node.js: JavaScript runtime to run SnarkJS scripts.
- Rust: Used to compile Circom.
- Circom: A specialized programming language for ZK circuits.
- 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 groupsignature in the circuits/ folder.
groupsignature.circom
pragma circom 2.1.6;
include "circomlib/mimcsponge.circom";
template GroupSig () {
signal input sk;
signal input pk1;
signal input pk2;
signal input pk3;
component pkGen = MiMCSponge(1, 220, 1); // has one input signal `in`, and one input signal `out`
pkGen.ins[0] <== sk;
pkGen.k <== 0;
signal pk;
pk <== pkGen.outs[0];
signal intern;
intern <== (pk - pk1)*(pk - pk2);
intern * (pk - pk3) === 0;
}
component main { public [ pk1,pk2, pk3] } = GroupSig();
/* INPUT = {
"sk": "32",
"pk1": "55",
"pk2": " 3cb6bc651b03d994974629eacef9696311d85c2ca9e3d08cbdf01e1833c2a405",
"pk3": "50",
} */
Code Walkthrough
Imports
include "circomlib/mimcsponge.circom";
- The circuit imports MiMCSponge, a cryptographic hash function optimized for zero-knowledge proofs.
- MiMCSponge allows you to generate cryptographic hashes using an internal sponge construction.
Signals Declaration
signal input sk; // The prover's secret key (private input)
signal input pk1; // Public key 1 (known member of the group)
signal input pk2; // Public key 2 (known member of the group)
signal input pk3; // Public key 3 (known member of the group)
- Inputs:
- sk: The prover’s private key. This is a secret input that only the prover knows.
- pk1, pk2, pk3: The public keys of known members in the group. These are the “known members” the prover wants to prove they belong to.
Public Inputs:
component main { public [ pk1, pk2, pk3 ] } = GroupSig();
- These three public keys are available for the verifier to see, and they define the “group” of users to which the prover must prove membership.
Key Generation
component pkGen = MiMCSponge(1, 220, 1);
pkGen.ins[0] <== sk; // The secret key `sk` is input to the sponge
pkGen.k <== 0; // The constant k (sometimes used as salt) is set to 0
- MiMCSponge is used to compute the public key
pk
from the secret keysk
. - The sponge takes 1 input (the secret key) and generates 1 output (the public key).
- Why use MiMCSponge? It’s zk-friendly and allows hashing of the secret key to generate a public key, similar to how Ethereum addresses are generated from private keys.
Public Key Check
signal pk;
pk <== pkGen.outs[0]; // Output from MiMCSponge is the public key (pk)
- The output of the MiMCSponge is assigned to
pk
, which is the prover’s computed public key.
Membership Proof
signal intern;
intern <== (pk - pk1) * (pk - pk2);
intern * (pk - pk3) === 0;
- The prover proves that their pk is equal to pk1, pk2, or pk3 using the following logic:
(pk - pk1) * (pk - pk2) * (pk - pk3) === 0
How it works
- If pk matches one of pk1, pk2, or pk3, then one of the terms becomes zero.
- For example, if
pk = pk2
, then:
(pk - pk1) * (0) * (pk - pk3) = 0
- This ensures that at least one of the terms is zero, making the whole product zero.
- If pk does not match any of the three public keys, the product will be non-zero.
How it Works (Example)
Let’s walk through an example of how this works.
Prover (User)
- sk = 32 (this is the user’s private key, secret to the prover).
- The circuit computes pk using MiMCSponge:
pk = MiMCSponge(sk)
Verifier (Group)
The verifier has 3 public keys:
pk1 = 55
pk2 = 3cb6bc651b03d994974629eacef9696311d85c2ca9e3d08cbdf01e1833c2a405
pk3 = 50
Proof Check
- The circuit computes:
(pk - pk1) * (pk - pk2) * (pk - pk3) === 0
- If pk matches one of these public keys, the result will be 0.
- If pk does not match any of them, the result will be non-zero, and the proof will fail.
Compiling the Circuit
Run the following command to compile the circuit:
circom circuits/groupsignature.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/:
-
groupsignature.r1cs: The constraint system file.
-
groupsignature.wasm: The WebAssembly file.
-
groupsignature.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/groupsignature.r1cs outputs/pot10.ptau outputs/groupsignature_final.zkey
Export the Verification Key
snarkjs zkey export verificationkey outputs/groupsignature_final.zkey outputs/verification_key.json
Creating Inputs
Create an input file input/input.json
with the following content:
{
"sk": "32",
"pk1": "55",
"pk2": " 3cb6bc651b03d994974629eacef9696311d85c2ca9e3d08cbdf01e1833c2a405",
"pk3": "50",
}
These Inputs will be used to generate the proof.
Generating the Proof
node outputs/groupsignature_js/generate_witness.js outputs/groupsignature_js/groupsignature.wasm inputs/input.json outputs/witness.wtns
snarkjs groth16 prove outputs/groupsignature_final.zkey outputs/witness.wtns outputs/proof.json outputs/public.json
snarkjs groth16 verify outputs/verification_key.json outputs/public.json outputs/proof.json
If everything is setup correctly, you should see:
snarkJS: OK!
Summary
-
Design the Circuit: Create the logic of the proof in a Circom file.
-
Compile the Circuit: Convert the logic to constraint systems and WebAssembly files.
-
Generate Trusted Setup: Produce keys for the prover and verifier.
-
Input Data: Provide the inputs for the computation.
-
Generate Witness: Produce intermediate computation results.
-
Generate Proof: Create a proof from the witness.
-
Verify Proof: Use the verifier to check the proof’s validity.
Use Cases
- Anonymous Voting: Prove you are a registered voter without revealing your identity.
- Anonymous Access Control: Prove you are on an access list without revealing which user you are.
- Ring Signatures: Sign a message as part of a group, but keep your identity private (like in Monero).
- Private Payments: Send payments from a known list of contributors while hiding the contributor’s identity.
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.