Skip to main content
Sigvex

Unconstrained Output Relation in Noir Circuits

Flags Noir functions that return a value derived from private inputs without any assertion binding the output to those inputs.

Unconstrained Output Relation in Noir Circuits

Overview

A zero-knowledge proof is only meaningful when the circuit constrains the relationship it claims to prove. A Noir function that takes private witness inputs, computes something, and returns a result is asserting — implicitly — that the returned value is the correct function of those inputs. But that assertion only holds if the circuit actually contains constraints tying the output to the inputs. If it does not, the function proves nothing: the prover can return any value and the verifier will accept it.

This detector finds constrained functions that have private inputs, a return type, and real computation, yet emit no assertions at all. Such a function looks like it computes a result, but from the verifier’s point of view the output is free. The proof certifies that the prover ran some witness, not that the output is the intended result of the declared inputs.

Why This Is an Issue

The whole value of a proof is the binding between what is hidden and what is revealed. Consider a circuit meant to prove “I know a preimage whose hash is h” that returns the hash. If no assertion forces return == hash(preimage), the prover can submit any preimage and claim any hash. The proof verifies, but it certifies a relationship that was never enforced.

This failure is especially dangerous because the circuit appears complete. It has inputs, it has arithmetic, it returns a result — everything a reviewer expects. The missing piece is invisible: there is no assertion stating that the output is what it purports to be. Downstream systems that trust the returned value as “proven” inherit a value the prover chose freely.

How to Resolve

Add assertions that constrain the returned value in terms of the inputs. The output must appear on one side of an equality (or other constraint) whose other side is computed from the private inputs.

// Before: the output is never tied to the inputs
fn derive_commitment(secret: Field, salt: Field) -> Field {
    let blended = secret * salt + salt;
    blended // prover can return anything; nothing constrains this
}

// After: the returned value is bound to the inputs by an assertion
fn derive_commitment(secret: Field, salt: Field) -> pub Field {
    let blended = secret * salt + salt;
    let commitment = std::hash::pedersen_hash([secret, salt]);
    assert(commitment == blended + secret); // binds output to inputs
    commitment
}

If the function is genuinely a witness-generation helper with no soundness role, mark it unconstrained so its intent is explicit and it is excluded from constraint expectations.

Detection Methodology

The Noir source is parsed into a typed representation capturing each function’s visibility, parameters (including their public/private status), return type, operations, and assertions. The detector selects functions that are constrained (not unconstrained), declare a non-empty return type, take at least one private parameter, perform at least one operation, and contain no assertions whatsoever. A function meeting all of these conditions returns a value derived from witness data with nothing in the constraint system binding that value, so it is reported.

Limitations

  • The detector reports a function only when it has no assertions at all. A function with assertions that are present but insufficient to constrain the output is outside this check’s scope and may be better served by more targeted constraint-gap analysis.
  • Functions whose output is constrained by a caller rather than internally are still reported, since the analysis is per-function.
  • Helper functions intended purely for witness generation should be marked unconstrained; if they are left constrained, they will be flagged.

References

Sample Sigvex Output

{
  "detector": "unconstrained-output-relation",
  "severity": "critical",
  "confidence": 0.80,
  "title": "Function `derive_commitment` returns `Field` from private inputs without constraining the output",
  "signal": "derive_commitment",
  "line": 1,
  "description": "Function `derive_commitment` takes private witness inputs and returns `Field`, but contains no assertions. The verifier accepts any output the prover claims, since nothing constrains the relationship between the private inputs and the returned value. The prover can produce a valid proof for arbitrary results.",
  "recommendation": "Add assertions constraining the relationship between inputs and outputs. For example: `assert(output == f(input));`"
}