Circom is the most popular language for writing ZK circuits. This hands-on tutorial takes you from installation to your first working proof.
Installation
# Install Rust (if needed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install Circom
git clone https://github.com/iden3/circom.git
cd circom
cargo build --release
cargo install --path circom
# Install snarkjs
npm install -g snarkjs
Your First Circuit
Create a simple circuit that proves knowledge of factors:
// multiplier.circom
pragma circom 2.0.0;
template Multiplier() {
// Private inputs (witness)
signal private input a;
signal private input b;
// Public output
signal output c;
// Constraint: a * b must equal c
c <== a * b;
}
component main = Multiplier();
Compiling the Circuit
# Compile to R1CS
circom multiplier.circom --r1cs --wasm --sym
# This generates:
# - multiplier.r1cs (constraint system)
# - multiplier_js/ (WASM for witness generation)
# - multiplier.sym (symbol file for debugging)
Trusted Setup
# Powers of tau ceremony (can reuse for circuits up to 2^n constraints)
snarkjs powersoftau new bn128 12 pot12_0000.ptau
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau
# Circuit-specific setup
snarkjs groth16 setup multiplier.r1cs pot12_final.ptau multiplier_0000.zkey
snarkjs zkey contribute multiplier_0000.zkey multiplier_final.zkey
snarkjs zkey export verificationkey multiplier_final.zkey verification_key.json
Generating a Proof
// Create input file: input.json
{ "a": 3, "b": 5 }
// Generate witness
node multiplier_js/generate_witness.js multiplier_js/multiplier.wasm input.json witness.wtns
// Generate proof
snarkjs groth16 prove multiplier_final.zkey witness.wtns proof.json public.json
Verifying the Proof
# Verify
snarkjs groth16 verify verification_key.json public.json proof.json
# Output: [INFO] OK!
What Was Proven
The prover demonstrated knowledge of a=3 and b=5 such that 3×5=15.
The verifier sees only that c=15 and that the proof is valid.
The values of a and b remain hidden.
More Complex Example: Age Verification
pragma circom 2.0.0;
include "circomlib/comparators.circom";
template AgeCheck() {
signal private input birthYear;
signal input currentYear;
signal input minimumAge;
signal output isOldEnough;
component gte = GreaterEqThan(16);
gte.in[0] <== currentYear - birthYear;
gte.in[1] <== minimumAge;
isOldEnough <== gte.out;
}
component main = AgeCheck();
Best Practices
- Use circomlib for common operations
- Minimize constraints for performance
- Test with various inputs
- Consider constraint count vs proving time trade-offs
Circom is the gateway to practical ZK development. Start simple, understand the workflow, then build more complex proofs.
Ready to Go Quantum-Secure?
Start protecting your users with post-quantum authentication today. 1,000 free auths, no credit card required.
Get Free API Key →