Here is the trap in miniature. This line assigns the output but adds no constraint:
c <-- a * b;
This line adds the constraint that makes the proof meaningful:
c === a * b;
Beginners often write the first and forget the second. The circuit compiles, the witness calculates, and the proof verifies. It also proves nothing about multiplication. Auditors look for this exact pattern.
Run the compiler:
circom multiplier.circom --r1cs --wasm --sym
You get three useful artifacts:
Sanity-check the constraint count with snarkjs r1cs info multiplier.r1cs. For this circuit you should see a very small number of constraints and one public output. SnarkJS also reports a constant signal named "1," which surprises people the first time they read the output.
If you add an extra signal and forget to constrain it, the count may still look fine. That is why constraint count is a useful smell test, not a proof of correctness. A circuit can be small and still be wrong.
Groth16, the proof system SnarkJS defaults to, needs a proving key derived from a ceremony. For local development you generate one yourself.
The number 12 is the exponent, and it caps the circuit size at roughly 4,096 constraints — plenty for this example.
snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="first" -v -e="some long random text"
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup multiplier.r1cs pot12_final.ptau multiplier_0000.zkey
snarkjs zkey contribute multiplier_0000.zkey multiplier_0001.zkey --name="first" -v -e="more random entropy"
snarkjs zkey export verificationkey multiplier_0001.zkey verification_key.json
For a real deployment, one contributor is not enough. Setup security depends on at least one participant genuinely destroying their randomness, which is why production circuits use multi-party ceremonies with published transcripts.
Think of the trusted setup as a necessary evil for Groth16. If you want to avoid it entirely, you can explore newer proof systems such as PLONK or STARKs, but the Circom and SnarkJS workflow remains a great introduction because it makes every moving part visible.
Write an input file, input.json:
{"a": "3", "b": "11"}
Field elements are strings, not JSON numbers, so the quotation marks matter. Compute the witness:
snarkjs wtns calculate multiplier_js/multiplier.wasm input.json witness.wtns
Then generate the proof:
snarkjs groth16 prove multiplier_0001.zkey witness.wtns proof.json public.json
public.json holds the public output, which should be 33. Verify it:
snarkjs groth16 verify verification_key.json public.json proof.json
A line reading [INFO] snarkjs: OK! means the proof checked out. Change a or b, regenerate the witness and proof, and the public value updates. Now edit public.json by hand and rerun the verify command. It fails, and that is the entire point.
You can also verify without trusting the prover by treating proof.json and public.json as separate inputs. The verifier only needs the verification key and the public statement. The secret inputs never enter the verification step.
Most failures at this stage are not cryptographic. They are operational. Here are the usual suspects:
When verification fails, resist the urge to regenerate everything immediately. First inspect the public output. If it differs from what you expect, the witness generation step is the problem. If it matches but verification still fails, the setup or proof files are mismatched.
A proof system does not care that your circuit is conceptually correct. It only cares that the witness satisfies the exact constraint system you compiled.
The .sym file is the most underrated artifact in the folder. It maps every signal name to its index in the witness. When a constraint fails, SnarkJS may report an index rather than a readable name. Open multiplier.sym, find that index, and you will know which signal is misbehaving.
For a one-line circuit this feels unnecessary. For a circuit with dozens of templates and hundreds of signals, it is the difference between a five-minute fix and an evening of guesswork.
You can also add intermediate signals to inspect values during development:
signal intermediate;
intermediate <== a * b;
c <== intermediate;
This changes nothing mathematically, but it gives you a named checkpoint. When the witness looks wrong, you can isolate whether the problem is upstream or downstream.
The toy circuit works with 3 and 11. Try 0 and 42. Try a very large number near the field modulus. Try negative values if your Circom version and field handling support them. Each case exercises a different part of the arithmetic.
In a real circuit, edge cases are where security bugs hide. A range check that fails for zero, a comparison that breaks at the modulus boundary, or a division that assumes a nonzero denominator can all produce proofs for statements you did not intend to allow.
Write a small shell script or a few commands that run several inputs and verify each proof. It is not a full test suite, but it catches the obvious mistakes before they become expensive.
SnarkJS can export a Solidity verifier:
snarkjs zkey export solidityverifier multiplier_0001.zkey verifier.sol
That contract exposes a verifyProof function. You pass it the proof and the public inputs, and it returns true or false. This is how many on-chain privacy and scaling applications work: the heavy computation happens off-chain, and the chain only verifies a short proof.
The gas cost depends on the proof system and the number of public inputs. Groth16 verification is relatively cheap, but the trusted setup requirement is a real trade-off. For a production system, you would also want to audit the circuit, the ceremony, and the integration code around the verifier.
The multiplier circuit is a teaching tool, not a product. The next step is usually one of three directions:
The pipeline stays recognizable. You still write a circuit, compile it, generate a witness, prove, and verify. The details change, but the mental model holds.
Build the multiplier, break it, fix it, and verify it again. That loop is the real tutorial. Everything else is scale.
Photo: Pexels / Pixabay