Circom Circuits Guide

Circom is a domain-specific language for designing arithmetic circuits used in zero-knowledge proofs. This guide covers writing circuits for Kusama ZK applications.

What is Circom?

Circom (Circuit Compiler) is a Rust-based language for:

  • Arithmetic Circuits: Define constraints over finite fields
  • ZK Proof Generation: Compatible with Groth16, PLONK, FFLONK
  • Witness Calculation: Generate proof inputs

Installation

# Install circom (version 2.1.6+)
git clone https://github.com/iden3/circom.git
cd circom
cargo build --release
cargo install --path circom

# Verify installation
circom --version

Basic Circuit Structure

Simple Addition Circuit

pragma circom 2.0.0;

template Adder() {
    // Input signals
    signal input a;
    signal input b;
    
    // Output signal
    signal output out;
    
    // Constraint
    out <== a + b;
}

// Main component with public inputs
component main {public [a, b]} = Adder();

Compile the Circuit

# Generate R1CS constraint system
circom circuit.circom --r1cs --wasm --sym

# Output files:
# - circuit.r1cs  (constraint system)
# - circuit.wasm  (witness calculator)
# - circuit.sym   (symbol table)

Signals and Constraints

Signal Types

// Input signal (private by default)
signal input x;

// Output signal
signal output y;

// Intermediate signal
signal temp;

// Public input (specified at compile time)
component main {public [x]} = MyCircuit();

// Array of signals
signal inputs[3];

// Multi-dimensional arrays
signal matrix[2][2];

Constraint Operators

// Assignment with constraint (<==)
out <== a * b;

// Constraint only (===)
a * b === c;

// Non-linear constraint (must use <==)
out <== a * b + c;

// Linear constraint (can use ===)
a + b === c;

Constraint Rules

Valid:

out <== a * b;           // Non-linear assignment
a + b === c;             // Linear constraint
out <== a + b + c;       // Linear assignment

Invalid:

a * b === c * d;         // Non-linear constraint (not assignment)
out === a * b;           // Must use <== for non-linear

Templates and Components

Template with Parameters

pragma circom 2.0.0;

template Multiplier(n) {
    signal input in[n];
    signal output out;
    
    signal products[n];
    
    for (var i = 0; i < n; i++) {
        products[i] <== in[i] * 2;
    }
    
    out <== products[0];
    for (var i = 1; i < n; i++) {
        out <== out + products[i];
    }
}

component main = Multiplier(5);

Component Instantiation

template Inner() {
    signal input x;
    signal output y;
    y <== x * x;
}

template Outer() {
    signal input a;
    signal output b;
    
    component sq = Inner();
    sq.x <== a;
    b <== sq.y;
}

Using circomlib

Poseidon Hash

pragma circom 2.0.0;

include "circomlib/poseidon.circom";

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

component main {public [a, b]} = PoseidonExample();

Merkle Tree Verification

pragma circom 2.0.0;

include "circomlib/merkle_tree.circom";

template MerkleVerifier(depth) {
    signal input leaf;
    signal input root;
    signal input siblings[depth];
    signal input indices[depth];  // 0 or 1 for each level
    
    component verifier = MerkleTreeCheck(depth);
    verifier.f <== leaf;
    verifier.root <== root;
    
    for (var i = 0; i < depth; i++) {
        verifier.siblings[i] <== siblings[i];
        verifier.index[i] <== indices[i];
    }
}

component main {public [root]} = MerkleVerifier(20);

SHA-256 (for non-ZK-friendly hashing)

include "circomlib/sha256.circom";

template ShaExample() {
    signal input data[16];  // 512 bits
    signal output hash[256];
    
    component sha = Sha256(16);
    for (var i = 0; i < 16; i++) {
        sha.data[i] <== data[i];
    }
    
    for (var i = 0; i < 256; i++) {
        hash[i] <== sha.out[i];
    }
}

Advanced Patterns

Bit Decomposition

include "circomlib/mimc.circom";
include "circomlib/bitify.circom";

template BitsExample() {
    signal input value;
    signal output bits[32];
    
    // Decompose to bits
    component decomposer = Num2Bits(32);
    decomposer.in <== value;
    
    for (var i = 0; i < 32; i++) {
        bits[i] <== decomposer.out[i];
    }
}

Less Than Comparison

include "circomlib/comparators.circom";

template LessThanExample() {
    signal input a;
    signal input b;
    signal output lt;  // 1 if a < b, 0 otherwise
    
    component ltcmp = LessThan(256);
    ltcmp.in[0] <== a;
    ltcmp.in[1] <== b;
    lt <== ltcmp.out;
}

Multiplexer (Conditional Selection)

template Multiplexer() {
    signal input condition;  // 0 or 1
    signal input a;
    signal input b;
    signal output out;
    
    // out = condition ? a : b
    out <== condition * a + (1 - condition) * b;
}

Complete Example: Private Withdrawal

pragma circom 2.0.0;

include "circomlib/poseidon.circom";
include "circomlib/merkle_tree.circom";

/**
 * Withdraw circuit for shielded pool
 * 
 * Proves:
 * 1. Knowledge of a leaf in the Merkle tree
 * 2. Correct nullifier hash computation
 * 3. Valid withdrawal amount
 */
template Withdraw(treeDepth) {
    // ─── Public Inputs ─────────────────────────────────────
    signal input root;              // Current Merkle root
    signal input withdrawnValue;    // Amount being withdrawn
    signal input context;           // Replay protection hash
    
    // ─── Private Inputs ────────────────────────────────────
    signal input nullifier;         // Unique spend identifier
    signal input secret;            // User's secret key
    signal input merkleProofPath[treeDepth];
    signal input merkleProofIndices[treeDepth];
    signal input newCommitmentSecret;
    
    // ─── Outputs ───────────────────────────────────────────
    signal output newCommitmentHash;
    signal output existingNullifierHash;
    
    // ─── Compute Nullifier Hash ────────────────────────────
    component nullifierHash = Poseidon(2);
    nullifierHash.inputs[0] <== nullifier;
    nullifierHash.inputs[1] <== secret;
    existingNullifierHash <== nullifierHash.out;
    
    // ─── Verify Merkle Membership ──────────────────────────
    component merkleCheck = MerkleTreeCheck(treeDepth);
    merkleCheck.f <== existingNullifierHash;
    merkleCheck.root <== root;
    
    for (var i = 0; i < treeDepth; i++) {
        merkleCheck.siblings[i] <== merkleProofPath[i];
        merkleCheck.index[i] <== merkleProofIndices[i];
    }
    
    // ─── Validate Withdrawal Amount ────────────────────────
    // Ensure value is positive and in range
    component valueBits = Num2Bits(64);
    valueBits.in <== withdrawnValue;
    
    // ─── Create New Commitment (UTXO Change) ───────────────
    component newCommitment = Poseidon(2);
    newCommitment.inputs[0] <== withdrawnValue;
    newCommitment.inputs[1] <== newCommitmentSecret;
    newCommitmentHash <== newCommitment.out;
    
    // ─── Context Binding ───────────────────────────────────
    // Context is verified in the smart contract:
    // context == keccak256(recipient, asset) mod p
}

component main {public [root, withdrawnValue, context]} = Withdraw(20);

Witness Generation

Generate Witness

# Compile circuit
circom withdraw.circom --r1cs --wasm --sym

# Create input file (input.json)
cat > input.json << EOF
{
    "nullifier": "123456789",
    "secret": "987654321",
    "merkleProofPath": ["...", "..."],
    "merkleProofIndices": [0, 1, 0, ...],
    "root": "111222333",
    "withdrawnValue": "100",
    "context": "444555666",
    "newCommitmentSecret": "555666777"
}
EOF

# Generate witness
node withdraw_js/generate_witness.js \
    withdraw_js/witness.wasm \
    input.json \
    witness.wtns

Trusted Setup

# Start powers of tau ceremony
snarkjs powersoftau new bn128 14 pot14_0000.ptau -v

# Contribute to ceremony
snarkjs powersoftau contribute pot14_0000.ptau pot14_0001.ptau \
    --name="First contribution" -v

# Apply circuit-specific phase
snarkjs powersoftau apply-file pot14_0001.ptau \
    circuit_final.ptau -v

# Generate zkey
snarkjs groth16 setup circuit.r1cs pot14_0001.ptau circuit_0000.zkey

# Contribute to zkey
snarkjs zkey contribute circuit_0000.zkey circuit_0001.zkey \
    -name="Contribution 1" -v

# Export verification key
snarkjs zkey export verificationkey circuit_0001.zkey vkey.json

# Export Solidity verifier
snarkjs zkey export solidityverifier circuit_0001.zkey verifier.sol

Generate Proof

snarkjs groth16 prove circuit_final.zkey witness.wtns proof.json public.json

# Verify proof
snarkjs groth16 verify vkey.json public.json proof.json

Debugging

Enable Verbose Output

circom circuit.circom --r1cs --wasm --sym --verbose

Check Constraints

# Show constraint statistics
circom circuit.circom --r1cs --json

# Analyze with snarkjs
snarkjs r1cs info circuit.r1cs

Common Errors

"Signal not connected":

// Wrong
signal out;

// Right
signal output out;
out <== expression;

"Non-quadratic constraint":

// Wrong
a * b * c === d;

// Right
signal temp;
temp <== a * b;
temp * c === d;

"Negative exponent":

// Wrong - Circom doesn't support division directly
out <== a / b;

// Right - Use modular inverse or redesign

Best Practices

1. Minimize Constraints

// Inefficient
signal temp1, temp2, temp3;
temp1 <== a * b;
temp2 <== temp1 * c;
temp3 <== temp2 * d;
out <== temp3;

// Efficient
out <== a * b * c * d;

2. Use Appropriate Bit Lengths

// For amounts (up to 2^64)
component bits = Num2Bits(64);

// For boolean (0 or 1)
component bool = Num2Bits(1);

3. Validate Inputs

// Ensure boolean is 0 or 1
component boolCheck = IsZero();
boolCheck.in <== condition * (1 - condition);

// Ensure value is in range
component rangeCheck = Num2Bits(64);
rangeCheck.in <== value;

4. Document Public Inputs

// Clearly mark public vs private
component main {public [root, amount]} = MyCircuit();
// root: Merkle root (public)
// amount: Withdrawal amount (public)
// Other inputs are private

Resources

Learning & Tutorials

Libraries & Tools

  • circomlib - Standard circuit library (Poseidon, Merkle trees, etc.)
  • circomlibjs - JavaScript bindings for witness generation

Examples


Previous: Shielded Pools | Next: Deployment Guide