Contents
A zero-knowledge proof lets one party convince another that a statement is true without revealing anything beyond that fact. Circom is a language for writing the circuits behind such proofs. In this post I go from an empty file to a compiled circuit and point to where proving starts.
What is Circom?
Circom is a domain-specific language (DSL) designed to create and compile zk-SNARK circuits. zk-SNARKs (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge) are cryptographic proofs that allow one party to prove the possession of certain information without revealing it. You write circuits in Circom, and the compiler turns them into a constraint system plus a witness generator. A separate prover such as snarkjs produces and verifies the proofs. The circom documentation is the reference for everything below.
Why Circom?
Circom is fairly low-level: you describe constraints rather than a computation. Three things make it worth learning:
-
Familiar syntax: loops, conditionals and functions look like C or JavaScript. What is new is signals and the constraint operators such as
<==and===. -
Flexibility: Circom allows developers to create circuits of varying complexity, enabling a wide range of applications and use cases.
-
Control: you write the constraints yourself, so you can see what the circuit costs. The compiler only simplifies what you wrote (signal-to-signal and signal-to-constant substitution by default, full simplification with
--O2). The price is that nothing stops you from leaving a signal unconstrained (more on that below).
Installing circom
Prebuilt x86-64 binaries for Linux, macOS and Windows are on the circom releases page. To build from source instead, install Rust with rustup, then open a new terminal so cargo is on your PATH. You also need Node.js with npm for snarkjs. Then run:
git clone --branch v2.2.3 --depth 1 https://github.com/iden3/circom.git
cd circom
cargo build --release
cargo install --path circom
npm install -g snarkjs
cargo install puts the circom binary in $HOME/.cargo/bin. v2.2.3 was the latest release in October 2026, so check the releases page for a newer tag.
Creating a Circuit
A Circom circuit looks like a function, but what it declares is signals (values in a finite field) and the constraints between them. Every constraint has to reduce to the form A·B + C = 0, where A, B and C are linear combinations of signals. Here is a minimal example. Save it as multiplier.circom:
pragma circom 2.0.0;
template Multiplier() {
signal input a;
signal input b;
signal output c;
c <== a * b;
}
component main = Multiplier();
This circuit takes two input signals, a and b, multiplies them, and produces an output signal c. The <== statement assigns the product to c and adds the constraint that ensures the output is indeed the product of the inputs. The pragma line names the compiler version the circuit was written for; without it, circom 2 prints a warning.
Assignment without a constraint
Circom also has <--, which assigns a value without adding a constraint. Nothing in the constraint system then ties that signal to the expression, so unless other constraints pin it down, a dishonest prover can put a different value there and the proof still verifies. Swap <== for <-- in the example above and the compiler still reports "Everything went okay", with zero constraints. The docs put it plainly: using <-- "is, in general, dangerous and should only be used when the assigned expression cannot be included in a constraint". When you do need <--, add the missing relation yourself with === (circom docs: signals).
Compiling a Circuit
Compiling turns the circuit into a system of constraints, plus a program that computes the witness: every signal value, inputs included, that satisfies those constraints. snarkjs then uses the constraints and a computed witness to create the proof. To compile the example, run:
circom multiplier.circom --r1cs --wasm --sym
This writes multiplier.r1cs (the constraint system, in binary), multiplier.sym (a symbols file for debugging) and a directory multiplier_js/ with the WebAssembly witness generator multiplier.wasm and the JavaScript helpers that run it.
Next: generating a proof
Compiling is half the job. To get a proof, you compute a witness from an input file, run a trusted setup, and then prove and verify with snarkjs. The circom guide walks through each step: computing the witness, then proving circuits.