
Programming in Circom comes with its fair share of challenges. After reviewing numerous Circom codebases, we’ve identified certain anti-patterns that occur frequently.
In this series, we’ll provide a comprehensive overview of these issues to help you avoid the most common pitfalls.
Of course, this won’t be a complete list of every mistake possible (Circom has plenty of ways to trip you up). But the footguns we’ll cover are the ones that tend to catch developers off guard the most. So, let’s break them down and, more importantly, learn how to dodge them.
Assertions Do Not Add Constraints
Circom’s assert
does not add constraints to the circuit’s underlying R1CS. Therefore, you should never use assert
as a replacement for ===
!
The sole purpose of Circom assertions is to add safety checks to ensure templates are not instantiated with undesirable compile-time parameters. For example, circomlib uses assert
to ensure that developers can only use the LessThan(n)
template with n <= 252
as instantiating LessThan(n)
with n = 253
could lead to alias bugs. (If you’ve never heard of alias bugs before, don’t worry! We’ll cover these in a bit.)
template LessThan(n) {
// Ensure developers can't compile this template with n > 252
assert(n <= 252);
signal input in[2];
signal output out;
component n2b = Num2Bits(n+1);
n2b.in <== in[0]+ (1<<n) - in[1];
out <== 1-n2b.out[n];
}
Part of the reason people use assert
incorrectly might be that even relatively new AI models still get it wrong, too:

o3-mini-high when prompted “What is a practical use case for using assert
in Circom?”
This advice is snake oil. Circom’s assert
should only be applied to template parameters, not to signals.
Confusingly enough, however, you actually can apply assert
to signals in Circom. And to make matters even worse, the outcome of applying assert
to signals looks very much like the outcome of applying ===
! For example, consider the following circuit in zkrepl.dev:
pragma circom 2.1.6;
template TwoBools () {
signal input a;
signal input b;
(a - 1) * a === 0;
(b - 1) * b === 0;
}
component main = TwoBools();
/* INPUT = {
// A valid boolean
"a": "1",
// Not a valid boolean
"b": "42"
} */
Running this code will produce the following error, as setting the input for b
to 42
violates the second constraint.

Okay, no surprises thus far. However, what happens if we replace (b - 1) * b === 0
with an assertion?
pragma circom 2.1.6;
template TwoBools () {
signal input a;
signal input b;
(a - 1) * a === 0;
// Use `assert` instead of `===`
assert((b - 1) * b == 0);
}
component main = TwoBools();
/* INPUT = {
"a": "1",
"b": "42"
} */
Running this code will actually produce the same error!
So, at first sight, it looks like there’s no harm in using assert
and ===
interchangeably. There’s a subtle difference, though: our circuit’s R1CS will now only have one constraint instead of two!

So, while using assert
”feels the same” as ===
during development, you should always assume that a malicious prover can eventually just copy the circuit code, delete the assert
, and create a proof. Notice that this malicious proof will satisfy all of your circuit’s constraints as your assert
didn’t add a proper constraint in the first place!
Bottomline: Only use assert
on template parameters to ensure your templates are not created with undesirable parameter values. Do not use assert
on signals and, in particular, do not use assert
as a replacement for ===
.
Hints Are Unconstrained
Circom’s <--
operator goes by many names:
- hint
- out-of-circuit computation
- assignment
- witness generation
- …
Regardless of what you call it, if you use this operator in your code, that should ring alarm bells! The reason?
Circom’s <--
does not add any constraints to the circuit!
Think of the <--
operator as “accepting untrusted user input,” which must always be sanitized by adding constraints.
A popular example is unconstrained division:
template Inverse() {
signal input in;
signal output out;
out <-- 1 / in;
}
Arguably, this is a very simple example, and even junior developers should be able to quickly identify that the above circuit lacks a constraint:
template Inverse() {
signal input in;
signal output out;
out <-- 1 / in;
// Don't forget to constrain your out-of-circuit computations!
out * in === 1;
}
Now you might go “Well, duh… why would anyone fall prey to this?!” But rest assured: real-world circuits are rarely that simple.
Let’s take things up a notch and consider the following circuit:
template IsZero() {
signal input in;
signal output out;
signal inv;
inv <-- in!=0 ? 1/in : 0;
out <-- -in*inv +1;
out === -in*inv +1;
}
Compared to our Inverse
example, identifying the issue here is already a bit more challenging, isn’t it? After all, our three signals are all appearing in the constraint, so we should be good, right? RIGHT?
Well… nope.
We must add a second constraint, in * out === 0
. Otherwise, an attacker can easily craft a malicious witness, e.g., in = 42
, inv = 0
, out = 1
, which the verifier will happily accept since it satisfies the circuit’s only constraint out === -in*inv +1
. However, 42
is obviously non-zero. In other words, the verifier will think that the user has a zero value when they actually don’t!
Using <--
instead of <==
for your Circom assignments can be tempting, especially as this will silence any “Non quadratic constraints are not allowed!” errors that the compiler might throw at you. Moreover, separating computation and constraints by using hints can often be more performant than modeling your computations with pure constraints alone. In particular, you want to use <--
in cases where computations are difficult to perform, but easy to check, with addition and multiplication. One example of such a circuit is the Inverse
template mentioned above.
Bottomline: If you use <--
in your Circom code, make absolutely sure that all required constraints are explicitly enforced elsewhere in the circuit!
Finite Field Arithmetic Enables Aliasing Attacks
Encoding a number via a bit array can lead to alias bugs if the number in question is greater than $p-1$, where $p$ denotes Circom’s field prime.
More specifically, an alias is when a binary array encodes a value $\ge p$ so that, under modulo $p$ arithmetic, it “aliases” to a different, smaller field element instead of its true integer value.
Example. Suppose we’re encoding field elements using 3-bit arrays, i.e., the maximum unsigned integer we can encode is $M=2^3-1=7$. Furthermore, suppose $p=5$. In that case, the (little-endian) array $[0,1,1]$ is an encoding of $a=6$ which, in the field $\mathbb{F}_5$, is an alias for $x=1$ (since $6 \text{ mod }5 = 1$). Notice that $0$ and $2$ also have exactly one alias (namely $5$ and $7$) while the remaining field elements, $3$ and $4$, do not have an alias. This is because $3+p \hspace{3pt}(= 8)$ and $4+p \hspace{3pt}(= 9)$ exceed our 3-bit encoding maximum $M=7$.
More generally, suppose we use an $n$-bit array to encode an input signal $x$. Furthermore, let $p \le M$ and $m := M \text{ mod } p$, where $M=2^{n}-1$ is the maximum unsigned integer representable with $n$ bits. Moreover, let’s assume $x\in [0,m]$ for now. You can think of $m$ as “the threshold above which the number of aliases decreases by one.” (Notice how this is consistent with the above example, where we had $m = 7 \text{ mod }5 = 2$. The elements $0$, $1$, $2$ had one alias each, whereas $3$ and $4$ had zero aliases.)
In this setup, $x$ will have
$$ N := \lfloor(M - x)/p\rfloor $$
aliases $a_i$. Given this number $N$, a field element $y\in (m, p-1]$ will then have $N-1$ aliases $\tilde{a}_j$.

For $x \in [0, m]$, the aliases can be computed via:
$$ a_1 = x + 1 \cdot p $$ $$ a_2 = x + 2 \cdot p $$ $$ \vdots $$ $$ a_N = x+N\cdot p $$
Given the number $N$, the aliases for the remaining field elements $y\in (m, p-1]$ can then be computed via:
$$ \tilde{a}_{1} = y + 1 \cdot p $$ $$ \tilde{a}_{2} = y + 2 \cdot p $$ $$ \vdots $$ $$ \tilde{a}_{N-1} = y+(N-1)\cdot p $$
You should think of the interval $[p,M]$ as the “danger zone” in the sense that each element in $[p,M]$ will alias an element in $[0,p-1]$.
In other words, whenever we’re encoding binary arrays with $M\geq p$, we’re in danger of introducing alias bugs.
In practice, you’ll often find that $M$ is strictly between $p$ and $2p$ so that some, but not all, field elements may have two valid representations.
To make things more concrete, let’s examine an attack in actual Circom code. Suppose we want to prove that we know the 254-bit representation of the decimal number $42$. To do this, we could naively use the following circuit:
pragma circom 2.1.6;
template Bits2Num(n) {
signal input in[n];
signal output out;
var lc1=0;
var e2 = 1;
for (var i = 0; i<n; i++) {
lc1 += in[i] * e2;
e2 = e2 + e2;
}
lc1 ==> out;
}
template Alias (n) {
signal input in[n];
signal output out;
component b2n = Bits2Num(n);
for (var i=0; i<n; i++) {
b2n.in[i] <== in[i];
}
out <== b2n.out;
}
component main = Alias(254);
However, this circuit is not sound! To see this, first notice that
- $n=254$,
- $p$ is Circom’s default prime,
- and $x = 42$ is the field element for which we want to fake a binary encoding.
Note that this implies:
- $M=2^{254}-1$ and
- $m = M \text{ mod }p \approx 7.05 \cdot 10^{75}$
Thus, we have $x\in [0,m]$ and can therefore compute:
$$ N = \lfloor(M - x)/p\rfloor = 1 $$
In other words: In default Circom, the number $x = 42\in [0,p-1]$ has exactly one alias, namely $a = 42 + p\in [p,M]$. In particular, we can convince a verifier that the binary representation of $a$ is the binary representation of $x$ — which, in reality, it is not!
To see that the circuit is indeed unsound, you can run the following example in zkREPL:
pragma circom 2.1.6;
template Bits2Num(n) {
signal input in[n];
signal output out;
var lc1=0;
var e2 = 1;
for (var i = 0; i<n; i++) {
lc1 += in[i] * e2;
e2 = e2 + e2;
}
lc1 ==> out;
}
template Alias (n) {
signal input in[n];
signal output out;
component b2n = Bits2Num(n);
for (var i=0; i<n; i++) {
b2n.in[i] <== in[i];
}
out <== b2n.out;
}
component main = Alias(254);
// Use the (little-endian) binary representation of 42+p as the input 'in'
/* INPUT = {
"in": [1,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,1,0,0,1,1,0,1,0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,1,1,1,0,1,0,0,1,1,1,0,1,1,0,0,1,1,1,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,1,1,1,1,1,0,0,1,1,0,0,0,0,0,1,0,1,0,0,1,0,1,1,1,0,1,0,0,0,0,1,1,0,1,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,0,1,1,0,1,1,0,1,1,0,1,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,0,1,1,1,0,1,1,0,0,1,0,1,0,0,0,0,0,0,0,1,0,1,1,0,0,0,1,1,0,0,1,0,0,0,0,1,1,1,0,1,0,0,1,1,1,0,0,1,1,1,0,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,1,1]
} */
// 'out' will evaluate to 42 although 'in' is NOT the correct binary
// representation of 42 (which would be [0,1,0,1,0,1,0,0,0,...])
Okay, now that we have a better understanding of aliasing attacks, how do we prevent them? Elementary, my dear Watson…
For the default case where $p$ is Circom’s default prime and $n=254$, circomlib provides “strict” versions of Bits2Num
and Num2Bits
, which implement an AliasCheck
to “cut off” the danger zone $[p, M]$ from the interval $[0, M]$. In other words, these templates will ensure that the input is strictly smaller than the field prime so that alias inputs will always be rejected.
template AliasCheck() {
signal input in[254];
component compConstant = CompConstant(-1);
for (var i=0; i<254; i++) in[i] ==> compConstant.in[i];
compConstant.out === 0;
}
template Bits2Num_strict() {
signal input in[254];
signal output out;
component aliasCheck = AliasCheck();
component b2n = Bits2Num(254);
for (var i=0; i<254; i++) {
in[i] ==> b2n.in[i];
in[i] ==> aliasCheck.in[i];
}
b2n.out ==> out;
}
template Num2Bits_strict() {
signal input in;
signal output out[254];
component aliasCheck = AliasCheck();
component n2b = Num2Bits(254);
in ==> n2b.in;
for (var i=0; i<254; i++) {
n2b.out[i] ==> out[i];
n2b.out[i] ==> aliasCheck.in[i];
}
}
Note that the AliasCheck
template uses CompConstant(-1)
to enforce that the binary input array encodes a value less than or equal to $p-1\hspace{5pt}(= -1 \text{ mod }p)$.
Before wrapping things up, we would like to emphasize one more time that aliasing attacks can only occur if $M\ge p$. In other words, if the input’s bit-size allows for binary encodings of numbers greater than the field prime. This is precisely the reason why circomlib’s LessThan(n)
template enforces n <= 252
: a value greater than that would lead to an instantiation of LessThan
’s internal Num2Bits
that would be vulnerable to aliasing attacks.
template LessThan(n) {
assert(n <= 252);
signal input in[2];
signal output out;
// n > 252 would make Num2Bits(n+1) vulnerable to
// aliasing attacks
component n2b = Num2Bits(n+1);
n2b.in <== in[0]+ (1<<n) - in[1];
out <== 1-n2b.out[n];
}
As a last comment, notice that circomlib’s AliasCheck
, CompConstant
, Bits2Num_strict
, and Num2Bits_strict
all hardcode the bit-size to 254! This means that, should you ever decide to work with a field prime other than Circom’s default 254-bit prime, you may have to manually change these templates accordingly to prevent aliasing attacks.
For a real-world example of an alias vulnerability, check out the following (fixed) double-spending issue in Semaphore.
Bottomline: Using circomlib’s Bits2Num(n)
and Num2Bits(n)
templates can make your circuits vulnerable to aliasing attacks if n = 254
and $p$ is Circom’s default prime. To mitigate this issue, you can use circomlib’s _strict
versions of these templates instead.
Special thanks to Gregor Mitscha-Baude, Alex Babits, Giorgio Dell’Immagine, Nadir Khan, Jaehun Kim, and 0xAlexSR for spending their valuable time helping me polish this post.