Skip to main content
Sigvex

Signal as Array Index Remediation

How to fix unconstrained signals used as array indices by using constrained multiplexer circuits.

Signal as Array Index Remediation

Overview

Related Detector: Signal as Array Index

When an unconstrained signal is used to index into an array during witness generation (result <-- arr[index]), the prover controls which element is selected. The fix is to replace direct array indexing with a constrained multiplexer component (such as Mux1, QuinSelector, or a custom selector) that enforces the index-to-output relationship through constraints.

Before (Vulnerable)

template UnsafeLookup(n) {
    signal input values[n];
    signal input index;
    signal output out;

    // Prover controls which element is selected
    out <-- values[index];
}

After (Fixed – QuinSelector Multiplexer)

template SafeLookup(n) {
    signal input values[n];
    signal input index;
    signal output out;

    component mux = QuinSelector(n);
    for (var i = 0; i < n; i++) {
        mux.in[i] <== values[i];
    }
    mux.index <== index;  // Constrained index
    out <== mux.out;      // Constrained selection
}

After (Fixed – Manual Equality-Based Selector)

For small arrays, a manual selector using equality checks works well:

template SafeSelect(n) {
    signal input values[n];
    signal input index;
    signal output out;

    // Create equality checks for each position
    component eq[n];
    signal terms[n];
    for (var i = 0; i < n; i++) {
        eq[i] = IsEqual();
        eq[i].in[0] <== index;
        eq[i].in[1] <== i;
        terms[i] <== eq[i].out * values[i];
    }

    // Sum all terms -- only one eq[i].out is 1
    signal acc[n];
    acc[0] <== terms[0];
    for (var i = 1; i < n; i++) {
        acc[i] <== acc[i - 1] + terms[i];
    }
    out <== acc[n - 1];
}

Alternative Mitigations

1. Range-Check the Index

If you must compute the index in witness generation, constrain it to a valid range and then use a constrained multiplexer:

signal index;
index <-- computeIndex();

// Range check: index must be less than array size
component lt = LessThan(8);
lt.in[0] <== index;
lt.in[1] <== arraySize;
lt.out === 1;

// Then use constrained mux with the range-checked index

2. Precompute Selection in Template Parameters

If the index is known at compile time, use a template parameter instead of a signal:

template StaticSelect(n, idx) {
    signal input values[n];
    signal output out;
    out <== values[idx];  // Compile-time constant, fully constrained
}

Common Mistakes

Range-checking but not using a multiplexer: Constraining index < n ensures a valid range, but out <-- values[index] is still unconstrained. The prover can return any array element regardless of the index. You need a multiplexer to constrain the index-to-value mapping.

Using a multiplexer with unconstrained index input: Wiring the index to the multiplexer via mux.index <-- index defeats the purpose. Use <== for all multiplexer wiring.

Forgetting that signal array access is not constrained: In Circom, arr[signal_index] during witness generation is a runtime lookup. It generates no constraint linking the index to the selected value.

References