#4 Circom: Intermediate signals

Topics covered: intermediate signals, variables vs signals, gotcha on var as signal

Intermediate Signals

In the previous article, we reviewed quadratic constraints. In this article, we will examine how to breakdown non-quadratic constraints into acceptable quadratic ones.

templage Mul3() {

		signal input a;
		signal input b;
		signal input c;
		signal input res;

		// not allowed
		res === a * b * c;
} 

We will look to convert res === a * b * c; into an acceptable form.

Given we previously discussed variables, you might think you can get around the quadratic constraint requirement by using a variable to break up the multiplication. Like so:

template Mul3() {

		signal input a;
		signal input b;
		signal input c;
		signal input res;

		var prod;
		
		// seemingly quadratic expressions
		prod = a * b;
		prod = prod * c;
		
		res === prod;
}

// this circuit will not compile 
component main = Mul3();
  • Declare a variable prod and use it to calculate a * b * c.

  • Constraint res against the the value of prod.

  • However, this circuit will not compile.

It may seem like prod stores the final result; instead prod holds the full expression a * b * c.

This results in the non-quadratic constraint: res === a * b * c, which is not allowed.

This would hold true for other non-linear operations like division as well.

Usage of variables like in the manner above to flatten non-quadratic expressions will not work. The reader might be confused, given the prior explanation of variables in article 2. Let us clarify.

Cannot use variables to flatten non-quadratic signal expressions

When variables are used in signal operations, like the one above, they do not "flatten" the signal operations. Instead, they will hold the entire expression.

  • variables which involve signal operations, will hold the entire expression.

  • variables which do not involve signal operations can be treated normally.

So how do we reduce non-quadratic constraints into quadratic ones? By using intermediate signals as explained in the next section.

signal and <-- operator

An intermediate signal is declared using the signal keyword:

template Mul3() {
		// input signals
		signal input a;
		signal input b;
		signal input c;
		signal input res;
		
		// intermediate signal: declaration
		signal prod;
		
		// intermediate signal: assignment
		prod <-- a * b;
		
		// quadratic constraints
		prod === a * b;		
		 res === prod * c;
}
  • res === a * b * c is transformed into 2 quadratic constraints

  • prod as an intermediate signal allows for flattening of constraint expressions

We can draw a parallel between prod as an intermediate signal and the intermediate variables we employ during the arithmetization stage of R1CS.

In arithmetization, we flatten non-quadratic constraints into quadratic ones by introducing intermediate variables, which are specified within the witness vector.

This form should not be unfamiliar to the reader: [1 , out , x , y , v1 , v2], where v1 and v2 are intermediate variables.

Therefore, intermediate signals are simply the intermediate variables that we used during the pen and paper approach.

Takeaway: Intermediate signals are used to flatten constraints; not variables.

Signal assignment operator: <--

  • After declaring prod, a value must be assigned to it, before it can be used in a constraint definition.

  • That is the purpose of the <-- operator, it assigns prod the resulting value of a * b.

  • Subsequently, prod is constrained to a * b.

Why constraint prod === a * b?

The reader will be concerned as to why we constrain prod against the very thing we assigned it to.

This is to ensure that during proof generation, the witness values (actual inputs) obey this relationship. Without the constraint, there’s no guarantee that prod will equal a * b during proof generation.

It is important to understand that the proof is based on the constraints, not the assignments.

In the next article, we will explore how Circom circuits are actualized from proof generation to verification, which will clearly illustrate the need for constraining an intermediate signal when assigning it.


Exercise

We will review the process of circuit compilation and witness generation again, in the context of intermediate signals and variables.

We will use our Mul3 circuit from earlier in this walkthrough.

mul3.circom
pragma circom 2.1.8;

template Mul3() {
		// input signals
		signal input a;
		signal input b;
		signal input c;
		signal input res;
		
		// intermediate signal: declaration
		signal prod;
		
		// intermediate signal: assignment
		prod <-- a * b;
		
		// quadratic constraints
		prod === a * b;		
		 res === prod * c;
}

component main = Mul3();

Compile circuit

  • circom mul3.circom --r1cs --sym --wasm

  • Sanity check: terminal output should reflect that non-linear constraints: 2

Examine the R1CS file

  • snarkjs r1cs print mul3.r1cs

  • there should be only 2 constraints, as dictated by our circuit

[ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.a ] * [ main.b ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.prod ] = 0
[ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.prod ] * [ main.c ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.res ] = 0

// simplified:

   [ -1 * main.a ] * [ main.b ] - [ -1 * main.prod ] = 0
[ -1 * main.prod ] * [ main.c ] - [ -1 * main.res ] = 0

// and again:

    [ main.a ] * [ main.b ] = [ main.prod ]		
 [ main.prod ] * [ main.c ] = [ main.res ] 

It should be obvious to the reader that the constraints match our circuit.

Generate the witness

  • create inputs.json file in ./mul3_js directory

  • values passed will be as below

{"a": "1","b": "2","c": "3", "res": "6"}

This makes sense as the product of a, b, c is 6, which we have assigned to res.

  • run: node generate_witness.js **mul3**.wasm inputs.json witness.wtns.

  • export to json: snarkjs wtns export json witness.wtns

  • cat witness.json, should output:

[
 "1",  // 1
 "1",  // a
 "2",  // b
 "3",  // c
 "6",  // res
 **"2"   // prod**
]

The reader will observe in the layout of the witness, that intermediate signals are included, and come after input signals.

Last updated

Was this helpful?