Symbolic Variables in Circom

A symbolic variable in Circom is a variable that has been assigned values from a signal.

When a signal is assigned to a variable (thereby turning it into a symbolic variable), the variable becomes a container for that signal and for any arithmetic operations applied to it. A symbolic variable is declared using the var keyword, just like other variables.

For example, the following two circuits are equivalent, i.e. they produce the same underlying R1CS:

template ExampleA() {
    signal input a;
    signal input b;
    signal input c;

    a * b === c;
}

template ExampleB() {
    signal input a;
    signal input b;
    signal input c;

    // symbolic variable v "contains" a * b
    var v = a * b;

    // a * b === c under the hood
    v === c;
}

In ExampleB, the symbolic variable v is simply a placeholder for the expression a * b. Both ExampleA and ExampleB are compiled using the exact same R1CS, and there is zero functional difference between them.

Use cases of symbolic variables

Checking That $\sum\texttt{in}[i]=\texttt{sum}$

Symbolic variables are extremely handy if we want to sum up an array of signals in a loop. In fact, summing signals in a loop is their most common use case:

// assert sum of in === sum
template Sum(n) {
    signal input in[n];
    signal input sum;

    var accumulator;
    for (var i = 0; i < n; i++) {
        accumulator += in[i];
    }

    // in[0] + in[1] + in[2] + ... + in[n - 1] === sum
    accumulator === sum;
}

Checking That in Is a Valid Binary Representation of k

A more interesting example is proving that in[n] is the binary representation of k for a templated value of n. In the circuit below, we check that:

$$ \texttt{in[0]}+2\cdot\texttt{in[1]}+4\cdot\texttt{in[2]} +\dots2^{n-1}\cdot\texttt{in[n-1]} == k $$

If all the signals in in are constrained to be $\set{0,1}$, then that implies in[] is the binary representation of k:

template IsBinaryRepresentation(n) {

    signal input in[n];
    signal input k;

    // in is binary only
    for (var i = 0; i < n; i++) {
        in[i] * (in[i] - 1) === 0;
    }

    // in is the binary representation of k
    var acc; // symbolic variable
    var powersOf2 = 1; // regular variable
    for (var i = 0; i < n; i++) {
        acc += powersOf2 * in[i];
        powersOf2 *= 2;
    }

    acc === k;
}

Why symbolic variables are helpful

Consider the earlier example of proving that $\sum\texttt{in}[i]=\texttt{sum}$. Without symbolic variables, it’s very clumsy to express

sum === in[0] + in[1] + in[2] + ... + in[n-1];

if we don’t know what n is in advance. Even if n was fixed, say at 32, actually typing out 32 variables by hand would be annoying. Thus, symbolic variables enable us to incrementally construct in[0] + in[1] + in[2] + ... without explicitly writing out the signals.

Non quadratic Constraints With Symbolic Variables

Because symbolic variables can “contain” a multiplication between two signals, they can lead to embedding two multiplications into one constraint if we aren’t careful. Consider the following example, that will not compile:

template QViolation() {
    signal input a;
    signal input b;
    signal input c;
    signal input d;

    // v "contains" a * b
    var v = a * b;

    // error: there are two
    // multiplications
    // in this constraint
    v === c * d;
}

In the code above, the symbolic variable v has one multiplication in it and we declared v == a*b. So the constraint v === c * d; is equivalent to a * b = c * d;. Hence, the above code will not compile.

Arbitrary operators are allowed with non-symbolic variables

Doing operations like computing the modulo or bitshifting are allowed with (non-symbolic) variables. However, this means that the variable can no longer be used as part of a constraint:

// this has no constraints
// but it will compile
template Ok() {
    signal input a;
    signal input b;

    var v = a % b;
}

The above example will compile because v is not used in a constraint. However, if we use v in a constraint, the code will not compile. An example of this is shown below:

template NotOk() {
    signal input a;
    signal input b;
    signal input c;

    var v = a % b;

    // non-quadratic constraint
    c === v;
}

Symbolic variables cannot be used to determine the boundary of a loop or the condition

Similarly, only regular variables can be used to determine the boundary of a loop or the condition of an if statement. If a symbolic variable is used, then the code will not compile:

template NotOk() {
    signal input a;
    signal input b;
    signal input c;

    var v = a * b;

    // v is a symbolic variable
    // used in an if statement
    if (v == 0) {
        c === 0;
    } else {
        c === 1;
    }
}

Summary

Symbolic variables are variables that were assigned a value from a signal. They are most frequently used for adding a parameterizable number of signals together, as the sum can be accumulated in a for loop. They are effectively a “container” or “bucket” that holds either a single signal or a collection of signals that are added or multiplied together. If a variable is never assigned a value from a signal, then it is not a symbolic variable.

Since symbolic variables contain signals, care must be taken to avoid quadratic constraint violations when using them.

ZK Proof of Selection Sort

ZK Proof of Selection Sort Most computations of interest are generally “stateful” — that is, they need to go through a series of steps to produce the final result. Sometimes, we do not need to show we executed the computation but only show the result. For example, if A is a list, we can prove […]

How a ZKVM Works

How a ZKVM Works A Zero-Knowledge Virtual Machine (ZKVM) is a virtual machine that can create a ZK-proof that verifies it executed a set of machine instructions correctly. This allows us to take a program (a set of opcodes), a virtual machine specification (how the virtual machine behaves, what opcodes it uses, etc), and prove […]

The Permutation Argument

The Permutation Argument A permutation argument is a proof that two lists hold the same elements, but possibly in a different order. For example, [2,3,1] is a permutation of [1,2,3] and vice-versa. The permutation argument is useful for proving one list is a sorted version of another. That is, if list B has the same […]

ZK Friendly Hash Functions

ZK Friendly Hash Functions ZK-friendly hash functions are hash functions that require much fewer constraints to prove and verify than traditional cryptographic hash functions. Hash functions such as SHA256 or keccak256 make heavy use of bitwise operators such as XOR or bit rotation. Proving the correct execution of XOR or bit rotation requires representing the […]

Featured Jobs

RareSkills Researcher

As a RareSkills researcher, you will be contributing to the technical content we post on our website.

Apply Now
Rust/Solana Auditor

We’re looking for someone to design and implement security measures and defense-in-depth controls to prevent and limit vulnerabilities.

Apply Now
Full Stack Developer

We’re looking for a Senior Full-Stack Engineer to play a foundational role in working across the entire offchain stack of products.

Apply Now
Rust Developer

We are seeking a talented Rust Developer to build a robust, scalable blockchain indexers and analytic backend.

Apply Now