Skip to main content
Sigvex

Private Input Unchecked Remediation

How to fix unchecked private inputs in Noir by adding assertions that constrain witness values.

Private Input Unchecked Remediation

Overview

Related Detector: Private Input Unchecked

Private inputs in Noir are witness values provided by the prover. Without assertions constraining them, the prover can set these values to anything and still generate a valid proof. The fix is to add assert or assert_eq statements that bind the private input to the circuit’s intended computation.

Before (Vulnerable)

fn main(public_hash: pub Field, preimage: Field) {
    let computed = std::hash::pedersen([preimage]);
    // BUG: no assertion -- preimage is unconstrained
    // The prover can claim to know any preimage
}

After (Fixed)

fn main(public_hash: pub Field, preimage: Field) {
    let computed = std::hash::pedersen([preimage]);
    assert(computed == public_hash);
    // preimage is now constrained: it must hash to public_hash
}

The assertion generates a constraint in the circuit. The verifier checks that the Pedersen hash of the prover’s preimage equals public_hash. If the prover uses a wrong preimage, the proof will not verify.

Alternative Mitigations

1. Range Assertions

When the private input must fall within a specific range:

fn main(amount: pub Field, balance: Field) {
    // Constrain: balance must be at least amount
    assert(balance as u64 >= amount as u64);
    // Constrain: balance is within valid range
    assert(balance as u64 <= 1000000);
}

2. Equality Assertions Against Public Inputs

When the private input must match a derivation from public data:

fn main(commitment: pub Field, value: Field, blinding: Field) {
    let computed_commitment = std::hash::pedersen([value, blinding]);
    assert_eq(computed_commitment, commitment);
    // Both value and blinding are now constrained
}

3. Use in Constrained Return

If the function returns a value that depends on the private input, and the return value is used in a parent constrained context, the private input may be transitively constrained. However, this is fragile – add an explicit assertion for clarity:

fn process(x: pub Field, witness: Field) -> Field {
    assert(witness != 0);  // explicit constraint
    let result = x / witness;
    result  // returned value carries the constraint
}

Common Mistakes

Assertions on unrelated values: adding assert(1 == 1) satisfies the “has an assertion” check syntactically, but does not constrain any private input. The assertion must reference the private parameter by name.

// WRONG: assertion does not reference `secret`
fn main(x: pub Field, secret: Field) {
    assert(x != 0);  // only constrains x, not secret
    let z = x + secret;
}

// CORRECT: assertion references `secret`
fn main(x: pub Field, secret: Field) {
    assert(secret != 0);
    let z = x + secret;
    assert(z == x + secret);  // redundant but shows intent
}

Aliasing without assertion: assigning a private input to a local variable and asserting on the variable works for the circuit constraints, but the detector uses name-based matching and may not recognize the alias. Keep assertions close to the parameter name for both clarity and detector accuracy.

// Less clear (and may trigger a false positive)
fn main(x: pub Field, secret: Field) {
    let s = secret;
    assert(s != 0);
}

// Clearer
fn main(x: pub Field, secret: Field) {
    assert(secret != 0);
}

Confusing unconstrained and constrained context: assertions inside unconstrained functions do not generate circuit constraints. They run only during witness generation and can be bypassed by a malicious prover. Place assertions in constrained functions:

// WRONG: assertion in unconstrained context is not enforced
unconstrained fn validate(x: Field) {
    assert(x != 0);  // this is NOT a circuit constraint
}

// CORRECT: assertion in constrained context
fn validate(x: Field) {
    assert(x != 0);  // this IS a circuit constraint
}

References