An alias bug in Circom (or any ZK circuit language) occurs when a binary array of signals encodes a number larger than the field element can hold. We will refer to signals and field elements interchangeably in this article. We refer to the characteristic of the field as p. Loosely speaking, p is the value the signal “overflows” at. It is the value of the implicit modulo in all of the arithmetic operations.

By default, Circom sets p to be 21888242871839275222246405745257275088548364400416034343698204186575808495617, which takes 254 bits to store. However, 2²⁵⁴ - 1 (~2.89×10⁷⁶)is larger than the default p (~2.18×10⁷⁶). That is, 254 bits can encode numbers larger than Circom signals can store.

Below we plot the number line showing these values on a number line, approximately to scale:

0 to p - 1 (the green line segment) is the interval a Circom field element can hold, and p to 2²⁵⁴ - 1 (the red segment) is the values a 254 bit binary value can hold, but a field element cannot.

The “danger zone” is 254 bit binary values larger than p - 1. These are the numbers in the interval [p, 2²⁵⁴ − 1]. In Circom’s (default) case, the range [p, 2²⁵⁴ − 1] is

[21888242871839275222246405745257275088548364400416034343698204186575808495617, 28948022309329048855892746252171976963317496166410141009864396001978282409983]

To get a sense of scale, if we divide p / 2²⁵⁴ we get 0.7561, which means a p can hold approximately 3/4ths of the numbers representable by a 254 bit number.

## Binary representation constraints silently fail when they overflow

To constrain a binary number b₀, b₁, ..., bₙ to equal a field element v, we write the following __arithmetic circuit__:

`b₀ + 2*b₁ + … + n*bₙ === v`

and also constrain each bᵢ to be 0 or 1.

However, the computation v = b₀ + 2*b₁ + … + n*bₙ is done modulo p. Therefore, if the computation b₀ + 2b₁ + … + nbₙ overflows p, then we could present a binary number b₀, b₁, ..., bₙ whose value is not v and create a false proof. For example, if our modulo is 11, then 2 and 13 are “equal” to each other because 13 mod 11 is 2.

### Small example

Suppose p = 11 and we are using four bits to represent a field element. The bits can encode numbers as large as 15. If we enocde 12 in binary as (1100) this will evaluate to 12 modulo 11 = 1. So we can claim that 1100 is the binary representation of 1.

Specifically:

8(1) + 4(1) + 2(0) + (0) == 1 (mod 11)

## Demonstration in Python

To see the values being used by the attacker, below we recreate the constraint used by Circomlib’s Bits2Num and Num2Bits in Python so that the values can be easily printed out:

The important thing here is that constrain_modulo_p accepts two binary representations for 3: the “correct” binary representation for 3 (11), and it’s alias 3 + p — which is encodable with as a 254 bit number.

## Bug prevention with AliasCheck and Num2Bits_strict and Bits2Num_strict

The “strict” versions of the bit conversion templates in __Circomlib’s bitify library__ prevent the alias bug by passing the binary array to the AliasCheck template.

The __AliasCheck template__ takes a binary array and asserts that the value encoded is less than the maximum value the field element can hold

AliasCheck uses -1 to refer to p - 1. compConstant takes a binary input (which could encode a value larger than the field element can hold) and returns 0 if it is less than or equal to a certain threshold and 1 if the binary value is greater than the threshold.

By constraining the output of __compConstant__ to be 0, and setting the constant for comparison to be -1, AliasCheck disallows binary numbers that are larger than p.

## If the binary array holds fewer bits than the field can encode, there is no danger of alias bugs

Applying Num2Bits to a field element also enforces a range check on that number to be less than 2ⁿ where n is the number of bits. For example, if n = 4 and p is the default value, and we set the input signal to be 17, the result will not silently overflow to binary 1 (0001) — the circuit will not be satisfied.

This is why Num2Bits_strict and Bits2Num_strict in Circomlib have the number of bits hardcoded to 254 — this is the value at which aliases can appear.

This is also why the LessThan template doesn’t allow the dev to build a comparator with more than 252 bits. This avoids a footgun with the alias bug.

**If you change the default ****p**** in the Circom compiler (the **__-p option__**), be sure to check that **Num2Bits_strict**, **Bits2Num_strict**, **AliasCheck**, and **CompConstant** still protect you from alias bugs because they are hardcoded to use 254 bits.**

## Spot the bug challenge

Here is a __CTF we posted on X__ (formerly Twitter) that has the bug described in this article:

The problem with the code above is that there are multiple witnesses that lead to the same hash, due to allowing 254 bits which leads to overflow.

Remember, **the input to an arithmetic circuit is not only the signals labelled **input** its ***every*** signal in the circuit**. Circom provides us a nice C-like programming language to “fill in” some of the signals based on the values provided in the input signals, but the code is not part of the final constraint system.

In the code above, the 254 bit binary array is held in the output signals of Num2Bits and the input signals of Bits2Num.

To inject the wrong values into the signals used to encode the binary array, we need to use the technique describe in our tutorial on __hacking underconstrained Circom circuits__. To generate a proof of concept for the code above, we take the following steps.

Generate a hash using a number u which is small enough to have an alias in the range, i.e. [p, 2²⁵⁴ − 1].

Generate malicious witness using the same number applied to input in but reassign the binary array to hold the value u + p.

The hash generated by both of these signal assignments will be the same, but the witness is different.

In summary, the code in the challenge is vulnerable to a second preimage attack via an alias bug.

It is the value of the implicit modulo in all of the arithmetic operations.

tunnel rush