Circom: Circuit Language for Zero-Knowledge Proofs

Circom is a domain-specific language (DSL) for defining arithmetic circuits used in zero-knowledge proofs. Developers write circuits in a Rust-like syntax that compile to R1CS constraints, which can then be used with zk-SNARK proving systems like Groth16 or PLONK.

Circom: Circuit Language for Zero-Knowledge Proofs

Circom is a domain-specific language (DSL) for defining arithmetic circuits used in zero-knowledge proofs. Developed by iden3, it allows developers to write circuits in a Rust-like syntax that compile to Rank-1 Constraint Systems (R1CS), which can then be used with zk-SNARK proving systems like Groth16 or PLONK. Circom is the most widely used circuit language for zk-SNARK development, powering projects such as Semaphore (ZK identity), zkEVM (Polygon Hermez), Tornado Cash (privacy mixer), and MACI (anti-collusion voting).

To understand Circom properly, it helps to be familiar with zk-SNARKs, linear algebra, and elliptic curve cryptography.

Circom workflow overview:
┌─────────────────────────────────────────────────────────────────────────┐
│                          Circom Workflow                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   Circuit Definition                  Compilation                        │
│   ┌─────────────────────┐             ┌─────────────────────────────┐   │
│   │  template Example() │             │  circom circuit.circom       │   │
│   │  {                  │  ────────→   │  ↓                           │   │
│   │    signal input a;  │             │  circuit.r1cs (constraints)  │   │
│   │    signal output b; │             │  circuit.sym (symbols)       │   │
│   │    b <== a * a;     │             │  circuit.wasm (witness gen)  │   │
│   │  }                  │             │                               │   │
│   └─────────────────────┘             └─────────────────────────────┘   │
│                                                    │                     │
│                                                    ▼                     │
│   Trusted Setup                          Proving & Verification         │
│   ┌─────────────────────┐             ┌─────────────────────────────┐   │
│   │ snarkjs powersoftau │             │ snarkjs groth16 prove       │   │
│   │ ↓                   │             │ ↓                           │   │
│   │ zkey (proving key)  │             │ proof.json (zk-SNARK proof) │   │
│   │ vkey (verifying key)│             │ public.json (public inputs) │   │
│   └─────────────────────┘             └─────────────────────────────┘   │
│                                                                          │
│   Final: Verification with snarkjs verify public.json proof.json vkey   │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

What Is Circom?

Circom is a language for writing arithmetic circuits that can be used to generate zero-knowledge proofs. A circuit in Circom describes constraints that inputs must satisfy. For example: "I know a number x such that SHA256(x) equals a given hash." The constraint system captures the verification logic. Provers demonstrate they know a valid input (witness) without revealing it.

  • Arithmetic Circuits: Based on finite field arithmetic (usually prime field of elliptic curve). Operations include addition, multiplication, and equality (constraints). No division, branches, or loops (without unrolling).
  • R1CS (Rank-1 Constraint System): Each constraint is of form A·s * B·s = C·s (dot products). Circom compiles high-level code to R1CS constraints.
  • Witness: Secret inputs that satisfy the circuit (the prover knows). Combined with public inputs for verification.
  • Templates: Reusable circuit components with parameters (like generic functions).
  • Signals: Variables representing values in the field (the wires of the circuit). Tagged as input, output, or intermediate.

Why Circom Matters

Writing circuits directly as R1CS constraints is infeasible for complex applications. Circom provides a high-level language for zk-SNARK development.

  • Productivity: Write circuits in Rust-like syntax (familiar to developers). Automatic constraint generation (no manual R1CS). Templates and libraries for reuse.
  • Ecosystem: Large library of standard circuits (SHA256, MiMC, Poseidon, ECDSA verification). Circomlib (standard library) has audited components. Active community, many examples.
  • Tooling Integration: Snarkjs (JavaScript) for proving/verification. Hardhat-circom plugin for Ethereum integration. Remix IDE support (experimental).
  • Security: Formal verification of some standard circuits (Poseidon). Audited implementations for critical primitives. Community-reviewed circuits.
Circom vs other ZK DSLs:
DSL             Approach                Backend               Ecosystem
─────────────────────────────────────────────────────────────────────────────
Circom          Low-level constraints    Groth16, PLONK        Largest
ZoKrates        High-level (Python-like) Groth16               Moderate
Leo (Aleo)      Rust-like (advanced)     Aleo's own            Growing
Cairo (Starknet)STARK circuit           StarkWare             Growing
Noir (Aztec)    Rust-like               PLONKish              Early

Circom Syntax and Concepts

Basic Circuit Structure

Each circuit is defined as a template. Templates can be instantiated and connected together.

Example: Simple multiplier circuit:
template Multiplier() {
    // Input signals
    signal input a;
    signal input b;
    
    // Output signal
    signal output c;
    
    // Constraint: c === a * b
    c <== a * b;
}

template Multiplier2() {
    signal input a;
    signal input b;
    signal output d;
    
    // Instantiate sub-circuit
    component mul = Multiplier();
    mul.a <== a;
    mul.b <== b;
    d <== mul.c;
}

Signals and Variables

  • Signals: Represent values in the finite field. Cannot be reassigned (immutable). Tagged with input, output, or none (intermediate).
  • Variables: Temporary values used during witness generation (not part of constraints). Can be reassigned using `var`. Not constrained unless used in a signal assignment.
  • Assignment Operators: `<==` assign AND constrain (recommended). `==` constrain only (no assignment). `<--` assign without constraint (dangerous, for witness computation only). Use sparingly with proper constraints.

Control Flow in Circom

Circom has limited control flow. Loops must be unrolled at compile time (known bounds).

  • for loops: Unrolled at compile time (constant bounds). Example: `for (var i = 0; i < N; i++) { ... }`.
  • if statements: Conditional execution of constraints not allowed. Use arithmetic selection instead: `out = (cond) ? val1 : val2` (needs constraints).
  • No recursion. No dynamic loops.

Arrays and Components

Array of signals example:
template Sum(n) {
    signal input in[n];    // array of n input signals
    signal output out;
    
    var total = 0;
    for (var i = 0; i < n; i++) {
        total += in[i];
    }
    out <== total;
}

template ReuseExample() {
    signal input in1;
    signal input in2;
    signal output out;
    
    component sum = Sum(2);
    sum.in[0] <== in1;
    sum.in[1] <== in2;
    out <== sum.out;
}

Circom Standard Library (Circomlib)

Circomlib is the official standard library of reusable circuits, extensively audited and widely used.

Category Components Description
Bit Operations Bitify, Num2Bits, Bits2Num Convert between field elements and bits
Hash Functions Sha256, MiMC, Poseidon Cryptographic hashes in circuits
Comparisons LessThan, GreaterThan, IsZero, CompConstant Compare numbers (with constraints)
Merkle Trees MerkleTree (path verification) Verify inclusion proofs
ECDSA Verification Validate secp256k1 signatures Verify Ethereum signatures in circuit
Poseidon hash in Circom:
include "circomlib/poseidon.circom";

template PoseidonExample() {
    signal input preimage[2];
    signal output hash;
    
    component poseidon = Poseidon(2);  // 2-input Poseidon
    poseidon.inputs[0] <== preimage[0];
    poseidon.inputs[1] <== preimage[1];
    hash <== poseidon.out;
}

// Poseidon is ZK-friendly (much fewer constraints than SHA256)
// Used in many ZK projects for efficient hashing

Circom Development Workflow

Complete workflow using snarkjs:
# 1. Compile circuit
circom circuit.circom --r1cs --wasm --sym

# 2. Generate witness (input.json)
node circuit_js/generate_witness.js circuit_js/circuit.wasm input.json witness.wtns

# 3. Trusted setup (Power of Tau)
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

# 4. Generate proving key
snarkjs groth16 setup circuit.r1cs pot12_final.ptau circuit_0000.zkey

# 5. Export verification key
snarkjs zkey export verificationkey circuit_0000.zkey verification_key.json

# 6. Generate proof
snarkjs groth16 prove circuit_0000.zkey witness.wtns proof.json public.json

# 7. Verify proof
snarkjs groth16 verify verification_key.json public.json proof.json

Common Circom Anti-Patterns

  • Using `<--` Without Constraints: `<--` assigns value without creating constraint. Used for witness computation only, but must be followed by `===` constraint, otherwise prover can assign any value. Always add constraint after `<--`.
  • Not Considering Constraint Count: Circuits with many constraints (millions) are expensive to prove. Each multiplication adds constraint; additions are free. Optimize for fewer constraints.
  • Unbounded Loops: Loop bound must be known at compile-time. Use constant parameters e.g., `for (i=0; i<N; i++)` where N is template parameter.
  • Using SHA256 When Poseidon Works: SHA256 generates thousands of constraints, Poseidon generates linear constraints. Use simpler hash functions when security allows (ZK-friendly).
  • Inefficient Numeric Comparisons: Naive `a < b` requires bit decomposition; use Circomlib's LessThan template (optimized).
Common mistakes checklist:
[X] Using <-- without corresponding === constraint
[X] Forgetting to unroll loops (dynamic bounds)
[X] Using SHA256 for large inputs (too many constraints)
[X] Not testing with invalid witnesses (circuit should reject)
[X] Mixing up signal and variable assignments

[✓] Use <== for assignment + constraint (preferred)
[✓] Use constant bounds for all loops
[✓] Use Poseidon for ZK-friendly hashing
[✓] Test with both valid and invalid witnesses
[✓] Understand signal (constrained) vs var (unconstrained)

Circom Best Practices

  • Prefer `<==` Over `<--`: `<==` both assigns and constrains (safer). Use `<-- + ===` only when value must be computed but not constrained directly. Avoid `<--` without following constraint.
  • Use Circomlib Templates: Standard library is audited and optimized. Reuse over reinventing. Examples: Poseidon, MiMC, LessThan, MerkleTree.
  • Minimize Constraints: Addition is free (no constraint). Multiplication adds constraints (minimize multiplications). Use bit decomposition only when needed, not for all comparisons. Profile constraint count with `--r1cs` and inspect with `snarkjs r1cs info`.
  • Test with Invalid Witnesses: Good circuits reject incorrect inputs. Add negative tests (inputs that violate constraints). Use `assert` in JavaScript witness generator for debugging.
  • Add Comments and Document Public Inputs: Circuits are harder to audit than normal code. Explain purpose of each template. Document which inputs are public vs private.
  • Use Constants: Parameters (circuit size, depth, rounds) as template parameters allow reuse. Use `param` for configuration (e.g., `template MerkleTree(nLevels)`.
Constraint optimization tips:
Operation               Cost (Constraints)      Notes
─────────────────────────────────────────────────────────────────────────────
Addition                Free                     a + b
Multiplication          1                        c = a * b
Division                >1 (invert)              compute inverse then multiply
Comparison              High (bit decomposition) Use Circomlib LessThan
SHA256                  27000+                   Use Poseidon (∼300)
Poseidon (2 inputs)     300                      ZK-friendly
Merkle Path (20 levels) 20 * hash_constraints   Reuse computed paths

Circom and Ethereum Integration

Circom circuits are commonly used with Ethereum for ZK applications: private transfers, ZK rollups, anonymous voting, ZK identity (Semaphore). The verifier contract generated by snarkjs can verify proofs on-chain.

Integrating with Solidity (snarkjs):
# Generate Solidity verifier contract
snarkjs zkey export solidityverifier circuit_0000.zkey verifier.sol

# Deploy verifier contract to Ethereum
# Users submit proof and public inputs
# Contract calls verifier.verifyProof(proof, publicSignals)

pragma solidity ^0.8.0;

interface IVerifier {
    function verifyProof(
        uint[2] memory a,
        uint[2][2] memory b,
        uint[2] memory c,
        uint[] memory input
    ) external view returns (bool);
}

Frequently Asked Questions

  1. What is the difference between Circom and ZoKrates?
    Circom is lower-level (explicit constraints), while ZoKrates provides higher-level abstractions (field-aware types). Circom has larger ecosystem and more libraries. Both compile to R1CS for Groth16. Choose Circom for complex circuits or existing standard library.
  2. What's the maximum circuit size Circom can handle?
    Limited by available RAM (constraint generation) and proving time. Circuits with 10-50 million constraints are possible on large machines (≥64GB RAM). Typical use cases: 100k-1M constraints.
  3. Does Circom support arrays of signals?
    Yes. Signals can be arrays `signal input in[n];`. Loop over array indices for constraints (must unroll). Length must be known at compile time (template parameter).
  4. How do I debug a Circom circuit?
    Use `log` statements in component (prints during witness generation). `assert` in witness generator JavaScript. Inspect intermediate signals with `snarkjs wtns debug`. Run small test inputs (trace constraints manually).
  5. What is the difference between Circom 1 and Circom 2?
    Circom 2 introduced custom gate support (PLONK). Better signal reuse (no multiple definitions). Improved type system and error messages. Circom 1 is legacy (still used). New projects should use Circom 2.
  6. What should I learn next after Circom?
    After mastering Circom, explore zk-SNARKs (Groth16, PLONK), snarkjs for proof generation, ZK rollups (zkSync, Polygon Hermez), Semaphore for anonymous signaling, and MACI for anti-collusion voting.