Signal as Array Index Remediation
How to fix unconstrained signals used as array indices by using constrained multiplexer circuits.
Signal as Array Index Remediation
Overview
Related Detector: Signal as Array Index
When an unconstrained signal is used to index into an array during witness generation (result <-- arr[index]), the prover controls which element is selected. The fix is to replace direct array indexing with a constrained multiplexer component (such as Mux1, QuinSelector, or a custom selector) that enforces the index-to-output relationship through constraints.
Recommended Fix
Before (Vulnerable)
template UnsafeLookup(n) {
signal input values[n];
signal input index;
signal output out;
// Prover controls which element is selected
out <-- values[index];
}
After (Fixed — QuinSelector Multiplexer)
template SafeLookup(n) {
signal input values[n];
signal input index;
signal output out;
component mux = QuinSelector(n);
for (var i = 0; i < n; i++) {
mux.in[i] <== values[i];
}
mux.index <== index; // Constrained index
out <== mux.out; // Constrained selection
}
After (Fixed — Manual Equality-Based Selector)
For small arrays, a manual selector using equality checks works well:
template SafeSelect(n) {
signal input values[n];
signal input index;
signal output out;
// Create equality checks for each position
component eq[n];
signal terms[n];
for (var i = 0; i < n; i++) {
eq[i] = IsEqual();
eq[i].in[0] <== index;
eq[i].in[1] <== i;
terms[i] <== eq[i].out * values[i];
}
// Sum all terms -- only one eq[i].out is 1
signal acc[n];
acc[0] <== terms[0];
for (var i = 1; i < n; i++) {
acc[i] <== acc[i - 1] + terms[i];
}
out <== acc[n - 1];
}
Alternative Mitigations
1. Range-Check the Index
If you must compute the index in witness generation, constrain it to a valid range and then use a constrained multiplexer:
signal index;
index <-- computeIndex();
// Range check: index must be less than array size
component lt = LessThan(8);
lt.in[0] <== index;
lt.in[1] <== arraySize;
lt.out === 1;
// Then use constrained mux with the range-checked index
2. Precompute Selection in Template Parameters
If the index is known at compile time, use a template parameter instead of a signal:
template StaticSelect(n, idx) {
signal input values[n];
signal output out;
out <== values[idx]; // Compile-time constant, fully constrained
}
Common Mistakes
Range-checking but not using a multiplexer: Constraining index < n ensures a valid range, but out <-- values[index] is still unconstrained. The prover can return any array element regardless of the index. You need a multiplexer to constrain the index-to-value mapping.
Using a multiplexer with unconstrained index input: Wiring the index to the multiplexer via mux.index <-- index defeats the purpose. Use <== for all multiplexer wiring.
Forgetting that signal array access is not constrained: In Circom, arr[signal_index] during witness generation is a runtime lookup. It generates no constraint linking the index to the selected value.