Feedback Loop
Detects circular signal dependencies in circuit constraints that prevent deterministic witness generation.
Feedback Loop
Overview
Remediation Guide: How to Fix Feedback Loops
The feedback loop detector identifies circular signal dependencies inside a template. A cycle exists when a signal participates, directly or transitively, in the computation of its own value. The simplest case is a <-- f(a); more elaborate cases involve a chain such as b <-- g(a); c <-- h(b); a <-- k(c).
Cyclic dependencies prevent the witness generator from picking a unique value for the involved signals. Either no assignment satisfies all constraints simultaneously, or many do. In either situation, the circuit no longer expresses a function from inputs to outputs.
Why This Is an Issue
Witness generation in Circom proceeds by topological order: each signal must be computable from signals already known. A cycle breaks this order. When the compiler encounters a cycle, behavior depends on the constraint type. For pure constraints (===), the cycle may admit multiple solutions, and the prover gets to choose. For witness assignments (<--), the cycle may produce undefined behavior depending on evaluation order — and any value the prover finds will satisfy the structural recurrence even if it disagrees with the intended semantics.
In both cases, the circuit’s claim no longer reduces to a single mathematical statement, which undermines the soundness guarantee of the proving system.
How to Resolve
Break the cycle by introducing a fresh signal for each “iteration” of the recurrence. The pattern is to materialize the dependency chain as a sequence of distinct signals, so that the witness generator can compute each in turn.
// Vulnerable: a depends on itself through a one-step cycle
template Accumulate() {
signal input start;
signal a;
signal output result;
a <-- start;
a <-- a + 1; // self-reference — cycle on `a`
result <== a;
}
// Fixed: each step uses a distinct signal
template Accumulate() {
signal input start;
signal a0;
signal a1;
signal output result;
a0 <== start;
a1 <== a0 + 1;
result <== a1;
}
For loops that legitimately need a recurrence, declare an array of intermediate signals indexed by iteration:
template Sum(N) {
signal input xs[N];
signal acc[N + 1];
signal output total;
acc[0] <== 0;
for (var i = 0; i < N; i++) {
acc[i + 1] <== acc[i] + xs[i];
}
total <== acc[N];
}
Sample Sigvex Output
{
"detector": "feedback-loop",
"severity": "high",
"confidence": 0.95,
"title": "Cyclic signal dependency in template `Accumulate`",
"template": "Accumulate",
"description": "Signal `a` depends on itself: a -> a. Witness generation cannot proceed deterministically when a signal participates in its own computation.",
"recommendation": "Replace the self-referential assignment with a sequence of distinct signals (one per iteration) and constrain each step explicitly."
}
Detection Methodology
For each template, the detector builds a directed graph in which nodes are signals and edges go from each signal on the right-hand side of an assignment to the signal on the left-hand side. For pure constraints (===), edges are inserted in both directions, since the relationship is symmetric. The graph is then traversed with depth-first search using visited and on-stack marks; any back edge corresponds to a cycle. Each distinct cycle is reported once.
Limitations
- Cycles that pass through component boundaries (where the loop closes through a sub-component’s input and output) are detected only if the parent template wires both ends.
- Cycles induced through array indexing with non-literal indices are conservatively over-approximated, which may produce false positives when the indices are statically known to differ.
- The detector reports the existence of a cycle, not the minimum cycle length — long cycles are reported with the same severity as one-step self-loops.
Related Detectors
- Signal Mutation — repeated assignments to the same signal
- Nondeterministic Control — prover-controlled execution paths
- Witness Complexity — costly or fragile witness computation