#2 Circom: Template parameters, Variables, Loops

Template parameters

Previously we looked at a circuit (IsBinary) that verified if the supplied inputs were indeed binary.

That circuit circuit was hardcoded to accept only 2 inputs.

What if we want to parametrize the number of inputs? We would modify the circuit, like so:

template IsBinary(n) {
    
    // array of n inputs
    signal input in[n];
    
    // 4 constraints each of them to be 0 or 1
    // only checks first 4 elements in array
    in[0] * (1 - in[0]) === 0;
    in[1] * (1 - in[1]) === 0;
    in[2] * (1 - in[2]) === 0;
    in[3] * (1 - in[3]) === 0;
}

// must specify value of n
component main = IsBinary(4);

Notice that the template declaration has changed to include n in the parenthesis

  • n here is known as a template parameter

  • n is used within the circuit to specify the size of the array in

  • on instantiating, we must specify the value of n

Circuits and constraints in Circom must have a fixed, known structure at instantiation; meaning they can't change after compilation. While templates can use parameters, once compiled, the circuit must be static and clearly defined. There is no support for "dynamic-length" circuits or constraints —everything must be fixed and well-defined from the start.

It is not possible to support variability in circuits and constraints post-compilation, as such variability would undermine the integrity of said circuits. Hence, it is crucial to ensure the scope of proving and verification is static and well-defined. Imagine having an R1CS system of constraints whose structure was mutable based on input signal values. Neither the prover nor the verifier could operate effectively as the number of constraints are not set in stone.

Although we have parametrized the array to be based on n inputs, the number of constraints remain unchanged - there are 4 constraints and they only verify for the first 4 elements of the array. In the next example, we will explore how to generalize the number of constraints to match our array, using loops.

For loop and Variables: for, var

Use a for loop to generate constraints programmatically to match the number of inputs, like so:

template IsBinary(n) {

    // array of n inputs
    signal input in[n];
		
    // n loops: n constraints
    for (var i = 0; i < n; i++) {
        in[i] * (1 - in[i]) === 0;
    }
}

// instantiated w/ 4 inputs & 4 constraints
component main = IsBinary(4);
  • both inputs and loop iterations are defined by n

  • for each input, a constraint is defined with the purpose of verifying that it is either 0 or 1

We have introduced two new keywords into the circuit: for and var

  • Usage of the for keyword should be no mystery to the reader.

  • The var keyword is used to declare a variable; i , as seen in the loop definition.

  • Variable assignment is made using the equal symbol =

Here, variable i is used to programmatically refer to different signals in the input array, while creating constraints for them.

Variables

Variables hold non-signal data and are mutable. Here is an example of a variable declaration outside of a loop:

component VariableExample(n) {

	var acc = 2;
	signal s;
}
  • Variables are not part of the R1CS system of constraints.

  • Variables are used to compute values outside the R1CS, to help define the R1CS.

  • When working with variables, Circom behaves like a normal programming language.

  • Operators =, ==, >=, <=, and !=, ++, and -- behave the way you expect them to with variables.

  • However, these operators do not work with signals (Circom will not compile such code), as they would not honour quadratic expressions.

Example

component VariableExample() {

	signal input s;
	var acc;

	// non-quadratic expression
	acc = (10 / 2) * 3;
}

component main = VariableExample();
  • non-quadratic expressions are allowed with variables

  • acc would be assigned the value of 15

Since we can use non-quadratic expressions with variables, they are very useful for side calculations, which can yield a value that can subsequently be used within a constraint.

component VariableExample() {

	signal input s;
	var acc;
		
	// non-quadratic expression
	acc = (10 / 2) * 3;
	
	s === acc;
}

component main = VariableExample();
  • input signal s is constrained to acc

  • the circuit serves to verify if the input signal is indeed 15.

Summary Example

Let’s combine all the above concepts into a less trivial example. The circuit below serves to verify if someone knows the decimal form of a binary number.

It expected both the binary and its supposed decimal equivalent to be provided for verification.

template BinaryToDecimal(n) {

    // Binary number as array
    signal input in[n];   // n-bit binary input
    signal input res;    

    var sum = 0;

    // Loop through each bit of the binary input
    for (var i = 0; i < n; i++) {
    
        // Add the binary value * 2^i to the sum
        // 1 << i is the same as 2^i
        sum += in[i] * (1 << i); 
    }

    // constraint the computed sum to the given decimal
    res === sum;
}

// Example with a 4-bit binary number
component main = BinaryToDecimal(4); 
  • circuit takes the binary number input as an array in[n] and the decimal result as res

  • the provided binary number is converted to decimal form and assigned it to sum

  • res is then constrained against sum

  • If the computed decimal value is not equal to the provided decimal value, verification fails.

This example showcases how a circuit can be created via parameters.

In the next article we will discuss in-depth where and how parameters can be used.

Last updated

Was this helpful?