Missing Range Check Remediation
How to fix missing range constraints on signals used in size-sensitive operations.
Missing Range Check Remediation
Overview
Related Detector: Missing Range Check
Signals in Circom are field elements (up to ~254 bits). Operations like comparisons, bit decomposition, and array indexing assume inputs fit within a specific number of bits. Without a range check, values exceeding the expected bit width produce incorrect results. The fix is to constrain signals to a valid range using Num2Bits or equivalent before using them in size-sensitive operations.
Recommended Fix
Before (Vulnerable)
template UnsafeTransfer() {
signal input amount;
signal input balance;
// LessThan(64) only works correctly for values < 2^64
component lt = LessThan(64);
lt.in[0] <== amount; // No range check -- amount could be > 2^64
lt.in[1] <== balance;
lt.out === 1;
}
After (Fixed — Range Check Before Comparison)
template SafeTransfer() {
signal input amount;
signal input balance;
// Constrain both inputs to 64-bit range
component amountCheck = Num2Bits(64);
amountCheck.in <== amount;
component balanceCheck = Num2Bits(64);
balanceCheck.in <== balance;
// Comparison is now valid -- inputs are guaranteed < 2^64
component lt = LessThan(64);
lt.in[0] <== amount;
lt.in[1] <== balance;
lt.out === 1;
}
After (Fixed — Range Check for Array Index)
template SafeLookup(arraySize) {
signal input index;
signal input values[arraySize];
signal output out;
// Constrain index to valid range
var bits = 0;
var temp = arraySize - 1;
while (temp > 0) { bits++; temp >>= 1; }
component rangeCheck = Num2Bits(bits);
rangeCheck.in <== index;
// Also constrain index < arraySize
component lt = LessThan(bits + 1);
lt.in[0] <== index;
lt.in[1] <== arraySize;
lt.out === 1;
// Now safe to use index in multiplexer
component mux = QuinSelector(arraySize);
for (var i = 0; i < arraySize; i++) {
mux.in[i] <== values[i];
}
mux.index <== index;
out <== mux.out;
}
Alternative Mitigations
1. Implicit Range Check via Bit Decomposition
If you already decompose a signal into bits for other purposes, the decomposition itself serves as a range check:
component bits = Num2Bits(n);
bits.in <== signal;
// signal is now guaranteed to be in [0, 2^n)
// You can use the individual bits for other logic too
2. Assert Non-Negativity for Subtraction Results
When computing differences, range-check the result to prevent underflow wrapping:
signal diff;
diff <== balance - amount;
component check = Num2Bits(64);
check.in <== diff; // Fails if balance < amount (diff wraps to large field element)
Common Mistakes
Range-checking the wrong bit width: Using Num2Bits(64) but then comparing with LessThan(128) does not protect against overflow in the comparison. The range check bit width must match or be smaller than the comparison bit width.
Range-checking outputs but not inputs: If an input signal feeds into multiple operations, range-check it once at the point of entry, not at each usage site.
Assuming public inputs are automatically range-checked: Public inputs are field elements. The verifier does not enforce any range constraint unless the circuit explicitly constrains the input via Num2Bits or equivalent.