Signal Mutation in Loop Remediation
How to fix self-referential signal mutations in loops by using signal arrays with per-step constraints.
Signal Mutation in Loop Remediation
Overview
Related Detector: Signal Mutation in Loop
When a signal is reassigned inside a loop via <-- with the RHS referencing the same signal (e.g., acc <-- acc + value), the prover controls the final value. Circom signals are meant to be single-assignment; self-referential mutation violates this invariant. The fix is to use an array of signals with one element per iteration, constraining each step with <==.
Recommended Fix
Before (Vulnerable)
template UnsafeSum(n) {
signal input values[n];
signal output total;
signal acc;
acc <-- 0;
for (var i = 0; i < n; i++) {
acc <-- acc + values[i]; // Self-referential mutation
}
total <-- acc; // Prover controls final value
}
After (Fixed — Signal Array with Per-Step Constraints)
template SafeSum(n) {
signal input values[n];
signal output total;
signal acc[n + 1];
acc[0] <== 0;
for (var i = 0; i < n; i++) {
acc[i + 1] <== acc[i] + values[i]; // Each step constrained
}
total <== acc[n]; // Fully constrained final value
}
After (Fixed — Running Product)
The same pattern works for multiplication:
template SafeProduct(n) {
signal input values[n];
signal output product;
signal acc[n + 1];
acc[0] <== 1;
for (var i = 0; i < n; i++) {
acc[i + 1] <== acc[i] * values[i]; // Degree-2 constraint per step
}
product <== acc[n];
}
Alternative Mitigations
1. Component-Based Accumulation
Delegate the accumulation to a dedicated component that handles constraints internally:
template Accumulator(n) {
signal input values[n];
signal output sum;
signal partial[n + 1];
partial[0] <== 0;
for (var i = 0; i < n; i++) {
partial[i + 1] <== partial[i] + values[i];
}
sum <== partial[n];
}
2. Constrain Only the Final Value
If the intermediate steps are not security-critical, compute them in witness generation and constrain the final result:
signal total;
total <-- computeSum(values, n);
// Verify by constraining total against known relationship
// (only works if you have an independent way to verify)
This approach is weaker and should only be used when an independent verification constraint exists.
Common Mistakes
Using a single signal with multiple <-- assignments: Circom allows multiple <-- to the same signal (last write wins), but the constraint system sees only one value. The mutation semantics exist only during witness generation and are prover-controlled.
Constraining only the final accumulator value: Writing total === expected at the end does not constrain the intermediate steps. Each step must be constrained, or the prover can reach the correct total through incorrect intermediate values.
Forgetting that loop bounds must be compile-time constants: Circom unrolls loops at compile time, so the number of iterations must be a template parameter or constant. If the loop bound is a signal, it cannot be used in a for loop.