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
- 0xparc Circom Learning Resource - Interactive tutorials and deep-dive explanations
- Circom Documentation - Official language reference
Libraries & Tools
- circomlib - Standard circuit library (Poseidon, Merkle trees, etc.)
- circomlibjs - JavaScript bindings for witness generation
Examples
- zk-assethub-demo Circuits - Circuit examples
Previous: Shielded Pools | Next: Deployment Guide