Unconstrained Public Input Remediation
How to fix unconstrained public inputs by adding constraints that bind them to the circuit's computation.
Unconstrained Public Input Remediation
Overview
Related Detector: Unconstrained Public Input
A public input that appears only in unconstrained assignments (<--) but never in a constraint (=== or <==) is invisible to the verifier. The prover can claim any value for it. The fix is to include the public input in at least one constraint equation that the verifier checks.
Recommended Fix
Before (Vulnerable)
template UnsafeMerkle() {
signal input root; // public input -- the claimed tree root
signal input leaf; // public input
signal input path[10]; // private witness
signal output valid;
signal computed;
computed <-- leaf;
for (var i = 0; i < 10; i++) {
computed <-- hash2(computed, path[i]);
}
// root is never constrained -- prover can claim any root
valid <== 1;
}
After (Fixed)
template SafeMerkle() {
signal input root;
signal input leaf;
signal input path[10];
signal output valid;
component hashers[10];
signal nodes[11];
nodes[0] <== leaf; // leaf is now constrained via <==
for (var i = 0; i < 10; i++) {
hashers[i] = Poseidon(2);
hashers[i].inputs[0] <== nodes[i];
hashers[i].inputs[1] <== path[i];
nodes[i + 1] <== hashers[i].out;
}
// root is now constrained -- verifier checks this
root === nodes[10];
valid <== 1;
}
The constraint root === nodes[10] forces the prover to provide a root value that matches the Merkle path computation. The verifier checks this equation, so any forged root is rejected.
Alternative Mitigations
1. Constrain via Constraining Assignment RHS
If the public input appears on the RHS of a <==, it is constrained:
signal input public_val;
signal output result;
result <== public_val * public_val; // public_val is constrained (RHS of <==)
2. Constrain via Component Wiring
Wiring a public input to a component’s input via <== constrains it through the component’s internal constraints:
signal input public_val;
component checker = RangeCheck(32);
checker.in <== public_val; // public_val is constrained by RangeCheck
3. Direct Equality Constraint
When the public input should match a computed value:
signal input expected_hash; // public
signal computed_hash;
computed_hash <== Poseidon(preimage);
expected_hash === computed_hash; // binds public input to computation
Common Mistakes
Constraining an intermediate instead of the public input: if tmp <-- public_input * 2 and then result <== tmp, only tmp is constrained. The public input itself is not referenced in any constraint. The prover can set public_input to any value, compute tmp from it, and the proof still verifies — but the claimed public_input is not bound to the computation.
// WRONG: public_input is NOT constrained
signal input public_input;
signal tmp;
tmp <-- public_input * 2;
result <== tmp * tmp; // tmp is constrained, public_input is not
// CORRECT: public_input IS constrained
signal input public_input;
signal tmp;
tmp <== public_input * 2; // public_input appears in RHS of <==
result <== tmp * tmp;
Confusing “used” with “constrained”: a public input can be referenced in witness generation code (inside <--) and appear to influence the circuit, but the verifier only checks constraints. Usage in <-- provides no security guarantee.
Assuming all inputs are public: in Circom, whether an input is public or private depends on the main component declaration. However, this detector flags all input signals within templates, since any input could be public depending on how the template is used.
References
- Circom Documentation: Public and Private Signals
- Circom Documentation: Constraint Generation
- Pailoor, S. et al. “Automated Detection of Under-Constrained Circuits in Zero-Knowledge Proofs.” IEEE S&P 2023.