#3 Circom: Parametrizing circuits

Previously, we touched upon parametrizing circuits by introducing template parameters, loops and variables. This article explores parametrizing best practices and common pitfalls.

Known and unknown values

Before we get to parametrization, let's define what Circom considers "known" and "unknown" values:

  • Known values: values that are fixed and determined at compile time, such as constants and template parameters.

  • Unknown values: values that are not fixed at compile time; such as signals which can be assigned arbitrary values post-compilation.

Parametrizing and constraint generation

To enforce that constraint expressions cannot be non-quadratic, the compiler enforces restrictions on how unknown values (signals) are used.

The following example illustrates the downstream effects of using unknown and known variables:

template Example(n){ // known

   signal input in; // unknown

   var x = 0;  // known
   var y = n;  // known
   var z = in; // unknown
}

component main = Example(2);
  • since n is known → variable y is known

  • since in is unknown → variable z in unknown

Whether a variable is known or unknown will be determined by its assignment. The known-ness of a variable will dictate how and where it can be used for parametrization.

We examine this in the following sections.

Arrays

Arrays (of signals or variables) must have a fixed known size at compile time.

  • cannot use signals to define array size

  • can use template parameters to define array size

Negative Example


template ArraySize1(){
   
   signal input in;
   
   // array of size in
   var someArray[in];   
   
   // Error
}

component main = ArraySize1();

Since the input signal, in, is unknown, we cannot use it to define an array.

Compiler will return an error: Error: The length of every array must known during the constraint generation phase

Positive Example


template ArraySize2(n){
   
   signal input in;

   // n is a parameter of a template 
   var someArray[n+1];  
   
}

// instantiation
component main = ArraySize2(4);
  • n is a template parameter; can be used to define array

  • on compilation the array is compiled as: someArray[5]

Arrays and constraint generation

When using an array in a constraint, the array element (and therefore index) being accessed must be known at compile time.

  • index must be known → cannot use signals

  • can use template parameters as array index

Negative Example

template ArrayConstraint1(n){

   signal input in1;
   signal input in2;

	 // declaration and initialization
   var someArray[3] = [n, n+1, n+2];

	 // constraint 
   in2 === someArray[in1];
   
   // Error
}

component main = ArrayConstraint1(3);
  • someArray consists of 3 elements

  • in2 is constrained against an element of the array

  • that element is dependent on input signal in1

  • since in1 is unknown, the constraint cannot be well-defined

Compiler will return an error: Error: Non-quadratic constraint was detected statically, using unknown index will cause the constraint to be non-quadratic

Using a signal as an array index allows different elements to be selected, leading to arbitrarily defined constraints. This enables exploits based on favourable values; but the compiler prevents such vulnerabilities.

Positive Example

template ArrayConstraint2(n){

   signal input in1;
   signal input in2;

   var someArray[n];

	 // constraint in2
   in2 === someArray[n];
   
}

component main = ArrayConstraint2(3);
  • in2 remains constrained against an element of the array

  • however, here the array index passed is known, as the template parameter is used

  • circuit will compile

To the reader: It is understood that the array, someArray, has not been initialized; this is a trivial example that only serves to illustrates the use of template parameters.

Loops and constraint generation

Constraints can only be generated in control flows (if-else, for-loops) with known conditions.

  • cannot use signals to define loop iterations

  • can use template parameters to define loop iterations

Let’s look at for-loops first.

For loops

Negative Example

template ForLoop(){

   signal input in1;
   signal input in2;

   // Error       
   for (var i = 0; i < in1; i++){
       
       in2 === i;
   }
}

// all constraints must be known and defined
component main = ForLoop();
  • loop is expected to iterate from i = 0 to i = in1-1

  • but the value of input signal in1 is unknown

Since the number of iterations is unknown at compile time, the number of constraints is unknown.

Compiler will return an error: Error: There are constraints depending on the value of the condition and it can be unknown during the constraint generation phase

Positive Example

template ForLoop(n){

   signal input in1;
   signal input in2;
   
   for (var i = 0; i < n; i++){
       
       in2 === i;
   }
}

// all constraints must be known and defined
component main = ForLoop(5);
  • for loop has 5 iterations

  • 5 constraints are created

On compilation, all constraints must be known and defined; and in this example there are.

If-else

If the condition is unknown, the entire block of code inside the control flow statement is considered unknown, and no constraints can be generated inside it.

Negative Example

template IfLoop(){

   signal input in1;
	   
		if (in1 > 0) {
		  // some constraints
		} else {
		  // other constraints
		}
}

component main = IfLoop();
  • if condition is based on signal in1

  • the condition in1 > 0 is unknown at compile time.

The compiler can't predict if an if block will execute, making it uncertain which constraints to include; so it's not allowed to ensure the R1CS system remains static and known at compile time.

Positive Example

template IfLoop(n){

   signal input in1;
   
		if (n > 0) {
		  // some constraints
		} else {
		  // other constraints
		}
}

component main = IfLoop(2);

Conversely, when the condition is known at compile time, the compiler can generate constraints within the control flow statement.

In summary, constraints in control flow statements require known conditions at compile time. Unknown conditions render the entire code block indeterminate, preventing constraint generation within it. This ensures that all constraints are well-defined and predictable during circuit compilation.

Summary

  • Circom distinguishes between known values (constants and template parameters) and unknown values (signals).

  • Arrays must have a fixed, known size at compile time. Template parameters can be used to define array sizes, but signals cannot.

  • When using an array in a constraint, the array's accessed position must be known at compile time.

  • Constraints can only be generated in control flows (if-else, for-loops) with known conditions.

  • For loops must have a known number of iterations, typically defined by template parameters.

  • In if-else statements, the condition must be known at compile time for constraints to be generated within the block.

These rules ensure that all constraints are well-defined and predictable during circuit compilation, preventing potential vulnerabilities in the circuit design.

Last updated

Was this helpful?