Signal Assignment and Constraint Generation

Restricting circuits to achieve verifiable private computation

Signal Assignment

From the official Circom docs:

Signals can only be assigned using the operations <-- or <== (see Basic operators) with the signal on the left hand side and and --> or ==> (see Basic operators) with the signal on the right hand side. The safe options are <== and ==>, since they assign values and also generate constraints at the same time. Using <-- and --> is, in general, dangerous and should only be used when the assigned expression cannot be included in a constraint, like in the following example.

This is further elaborated:

out <== 1 - a*b;

is equivalent to:

out === 1 – a*b;
out <-- 1 - a*b;

Essentially, this means that

  • the <-- operator sets the value of a signal on the right based on the value of an expression, variable or signal on the left

  • the === operator forces the computation to prove the signals on the left and right of the operand are equal, or fail to generate a valid proof

  • the <== operator forces the signal on the left to be equal to the value of an expression, variable, or signal on the left

The difference between <== and <-- may be subtle, however it has massive implications on the function and safety of your ZK proof. With the use of <==, you can provably output a value (either as an intermediate signal in a component or as an output signal in the main component).

Using the <-- operator by itself does absolutely nothing to constrain the execution of the computation. Without additionally providing some sort of equality constraint using ===, the use of <-- is somewhere between pointless and dangerous.

There are however cases where using <== would cause non-quadratic constraints, in which case it may make sense to assign a signal in one statement and constrain its value in a separate one. An example of such a case can be found in the BattleZips board proof:

for (var i = 0; i < 100; i++) {
    toNumH.in[i] <-- boardH[i \ 10][i % 10]; // assign without constraint
    toNumV.in[i] <-- boardV[i \ 10][i % 10]; // assign without constraint
}
// mux boards to get next state
boardMux.c[0] <== toNumH.out; // apply constraint to assigned signal
boardMux.c[1] <== toNumV.out; // apply constraint to assigned signal
boardMux.s <== ship[2];
boardOut <== boardMux.out; // constrain output w/ unconstrained intermediate signals

Constraint Generation

There are four types of constraints that can be generated in Circom.

  • Constant values: only a constant value is allowed.

  • Linear expression: an expression where only addition is used. It can also be written using multiplication of variables by constants. For instance, the expression 2*x + 3*y + 2 is allowed, as it is equivalent to x + x + y + y + y + 2.

  • Quadratic expression: it is obtained by allowing a multiplication between two linear expressions and addition of a linear expression: A*B - C, where A, B and C are linear expressions. For instance, (2*x + 3*y + 2) * (x+y) + 6*x + y – 2.

  • Non quadratic expressions: any arithmetic expression which is not of the previous kind.

In layman's terms, this means that the computations one can perform in a single expression with Circom are bounded. In practice, you will need to take as many measures as possible to minimize the degree in the computations you make. See conditional statements for a specific example of how this might occur.

Note on assert()

You may be tempted to use the assert() function built into the Circom language to handle logical checks on variables or signals in Circom. While the IDE/ your scripts in unit tests will fail due to the Circom compile-time error, there will be absolutely no checks in the verification of the zero knowledge proof on the logic evaluated using assert(). For this reason, unless you know what you are doing, you should avoid the use of assert().

Last updated