Unchecked Component
Detects component instances whose inputs or outputs are never wired into the enclosing R1CS, leaving the component's internal constraints disconnected from the proof.
Unchecked Component
Overview
The unchecked component detector finds template instances whose ports are disconnected from the enclosing template’s constraint graph. Declaring component c = SomeTemplate(); expands the sub-template’s internal R1CS into the global R1CS, but those constraints only influence the proof when the parent reads the sub-component’s outputs through <== or writes to its inputs through <==. An instance that is declared and then ignored, or one whose ports are wired only through <--, adds no enforcement to the parent circuit even though it inflates the constraint count.
The pattern appears in three flavours. First, the “decorative instantiation”: a range-check or hash component is created but no other signal in the parent ever reads its outputs. Second, the “hint-only wiring”: the parent assigns the sub-component’s output to a signal with <--, which snapshots the value in the witness but does not bind it in the R1CS. Third, the “partially wired component”: some inputs are bound with <== while others are left assigned with <-- or never assigned at all, so the sub-component’s constraint system does not have the values the parent believes it passed in.
Severity is High in general and escalates to Critical when the unchecked component is a hash function, range check, or signature verifier — that is, when the author almost certainly intended the component to enforce a security property.
Why This Is an Issue
Template instances are the main tool Circom authors use to reason about “this property is checked here”. If a Num2Bits(64) is instantiated next to a balance update, a reviewer sees the component and concludes that the balance is bounded. If that component’s output is never read and its input is wired with <--, the component has no effect on soundness whatsoever — the balance is still arbitrary, and the reviewer’s conclusion is wrong.
This is particularly dangerous during refactors. A working circuit is modified to introduce a new signal flow; the author rewires one edge but forgets the others; tests pass because the witness values still line up; the on-chain verifier accepts proofs that no longer enforce the original invariant.
How to Resolve
Wire every input and output of a security-relevant component with <== (or <-- followed immediately by an === that rebinds the value).
// Vulnerable: range check is instantiated but never observed
template Transfer() {
signal input amount;
component bound = Num2Bits(64);
bound.in <-- amount; // hint-only wiring
// bound.out[*] never read
}
// Fixed: input and at least one output are bound in the R1CS
template Transfer() {
signal input amount;
component bound = Num2Bits(64);
bound.in <== amount;
// Num2Bits' internal constraints now effectively enforce amount < 2^64
signal reconstructed;
var acc = 0;
for (var i = 0; i < 64; i++) {
acc += bound.out[i] * (1 << i);
}
reconstructed <== acc;
reconstructed === amount;
}
Examples
Sample Sigvex Output
{
"detector": "unchecked-component",
"severity": "critical",
"confidence": 0.92,
"title": "Range check `rangeCheck` is instantiated but disconnected",
"template": "Transfer",
"component": "rangeCheck",
"sub_template": "Num2Bits",
"line": 8,
"description": "Component `rangeCheck = Num2Bits(64)` is declared but its output array is never read and its input is wired through `<--`. The sub-component's constraints are not connected to any signal in `Transfer`, so the range check has no effect on the proof.",
"recommendation": "Wire `rangeCheck.in` with `<==` and consume at least one output port with a constrained assignment."
}
Detection Methodology
For every template, the detector enumerates component declarations and records, for each instance, its declared input and output ports. It then walks the parent’s assignment list and marks each port as constrained-written, hint-written, constrained-read, hint-read, or unused. A component is flagged when at least one of its inputs is not constrained-written or at least one of its outputs is never constrained-read.
The finding is upgraded to Critical severity when the sub-template’s name matches a known security primitive (Num2Bits, Bits2Num, Poseidon, MiMC, LessThan, EdDSAVerifier, ECDSAVerify, MerkleTreeChecker, etc.). Unknown sub-templates default to High severity with lower confidence.
Limitations
- Output ports read through array indexing with a signal-valued index are conservatively treated as read; genuinely unused entries in such arrays are not detected.
- Sub-templates that intentionally expose outputs the parent should ignore are not distinguished from bugs; suppressing them requires an annotation on the sub-template.
- Parameterised sub-templates whose port list depends on a compile-time parameter may be analysed for the wrong port set if the parameter is not a literal.
Related Detectors
- Cross-Template Constraint Gap — wiring between two components that does not carry a constraint
- Under-Constrained Signal — parent-level signals missing constraints
- Template Misuse — components used outside their documented contract
References
- Circom Documentation: Templates and Components
- iden3/circomlib
- Pailoor, S., Wang, Y., Wang, X., Dillig, I. “Automated Detection of Under-Constrained Circuits in Zero-Knowledge Proofs.” IACR ePrint 2023/512.