Skip to main content
Sigvex

Nondeterministic Control Flow Remediation

How to fix ternary operators in unconstrained assignments where the prover controls which branch executes.

Nondeterministic Control Flow Remediation

Overview

Related Detector: Nondeterministic Control Flow

Ternary operators (condition ? a : b) in unconstrained assignments give the prover control over which branch executes. Without constraints that verify both the condition and the result, the prover can choose either value freely. The fix is to add constraints that bind the ternary result to the condition, or to replace the ternary with a constrained multiplexer.

Before (Vulnerable)

template UnsafeMax() {
    signal input a;
    signal input b;
    signal output out;

    // Prover chooses which branch -- no constraint verifies the condition
    out <-- a > b ? a : b;
}

After (Fixed – Constrained Multiplexer)

template SafeMax() {
    signal input a;
    signal input b;
    signal output out;

    // Constrained comparison via circomlib
    component lt = LessThan(252);
    lt.in[0] <== b;
    lt.in[1] <== a;

    // Constrained selection: out = lt.out * a + (1 - lt.out) * b
    signal diff;
    diff <== a - b;
    out <== b + lt.out * diff;
}

Safe Pattern: IsZero with Full Constraints

The classic IsZero pattern uses a ternary but is safe because accompanying constraints fully bind the result:

template IsZero() {
    signal input in;
    signal output out;

    signal inv;
    inv <-- in != 0 ? 1 / in : 0;  // Nondeterministic but safe:
    out <== -in * inv + 1;          // Constrains relationship
    in * out === 0;                 // Ensures IsZero semantics
}

Alternative Mitigations

1. Use circomlib Mux Components

For selecting between values based on a condition, use Mux1:

component mux = Mux1();
mux.c[0] <== valueIfFalse;
mux.c[1] <== valueIfTrue;
mux.s <== constrainedCondition;  // Must be 0 or 1
signal result;
result <== mux.out;

2. Algebraic Selection

Express the selection as an algebraic constraint without a ternary:

// result = condition * a + (1 - condition) * b
// where condition is constrained to be 0 or 1
signal result;
signal condDiff;
condDiff <== condition * (a - b);
result <== b + condDiff;

Common Mistakes

Constraining the result but not the condition: If out <== a when the condition is true, this constrains out but not the condition itself. The prover can still choose which branch executes. Both the condition and the result must be constrained.

Using boolean checks without bit constraints: Writing condition * (condition - 1) === 0 ensures the condition is 0 or 1, but you must also constrain the condition to the correct comparison result (e.g., via LessThan).

Assuming all ternaries are dangerous: Ternaries in <-- are safe when accompanied by constraints that fully determine the result regardless of which branch the prover picks. The IsZero pattern is the canonical example.

References