Circom Components in a Loop

Circom does not allow for components to be directly instantiated in a loop. For example, compiling the following code results in the error below.

include "./node_modules/circomlib/circuits/comparators.circom";

template IsSorted(n) {
  signal input in[n];

  for (var i = 0; i < n; i++) {
    component lt = LessEqThan(252); // error here
    lt.in[0] <== in[0];
    lt.in[1] <== in[1];
    lt.out === 1;
  }
}

component main = IsSorted(8);
Signal or component declaration inside While scope. Signal and component can only be defined in the initial scope or in If scopes with known condition

The workaround is to declare an array of the components but not specify the component type:

pragma circom 2.1.8;
include "./node_modules/circomlib/circuits/comparators.circom";

template IsSorted(n) {

  signal input in[n];

  // declare array of components
  // but do not specify the component type
  component lessThan[n];

  for (var i = 0; i < n - 1; i++) {
    lessThan[i] = LessEqThan(252); // specify type in the loop
    lessThan[i].in[0] <== in[i];
    lessThan[i].in[1] <== in[i+1];
    lessThan[i].out === 1;
  }
}

component main = IsSorted(8);

When components are declared in this manner, it is not possible to do a “one-line assignment” to a signal like shown below:

pragma circom 2.1.8;
include "./node_modules/circomlib/circuits/comparators.circom";

template IsSorted() {

  signal input in[4];
  signal leq1;  
  signal leq2;  
  signal leq3;  

  // one line assignment to the signal
  leq1 <== LessEqThan(252)([in[0], in[1]]);
  leq2 <== LessEqThan(252)([in[1], in[2]]);
  leq3 <== LessEqThan(252)([in[2], in[3]]);

  leq1 === 1;
  leq2 === 1;
  leq3 === 1;
}

component main = IsSorted();

Outside a loop, signals can be set on a single line. Inside a loop, however, we have to write out the assignment in more steps, like we did in lessThan[i] = LessEqThan(252); // specify type in the loop.

Example 1: max of an array

To illustrate a useful example of declaring components in a loop, we show how to prove k is the maximum of an array. To do this, we need to constrain that k is greater than or equal to every other element and that it is equal to at least one of the elements. To see why the equality check is necessary, consider that 18 is greater than or equal to all the elements in [7, 8, 15], but it is not the maximum of the array.

The following Circom code computes the maximum value of the array without generating constraints. Then, it runs n GreaterEqualThan components to constrain that the proposed max value is indeed the maximum value, and also checks that at least one of the elements is equal to k using an array of IsEqual components.

include "./node_modules/circomlib/circuits/comparators.circom";

template Max(n) {
  signal input in[n];
  signal output out;

  // no constraints here, just a computation    
  // to find the max    

  var max = 0;    
  for (var i = 0; i < n; i++) {        
    max = in[i] > max ? in[i] : max;   
  }    

  signal maxSignal;    
  maxSignal <-- max;

  // for each element in the array, assert that
  // max ≥ that element
  component GTE[n];
  component EQ[n];
  var acc;
  for (var i = 0; i < n; i++) {
    GTE[i] = GreaterEqThan(252);
    GTE[i].in[0] <== maxSignal;
    GTE[i].in[1] <== in[i];
    GTE[i].out === 1;

    // this is used in the
    // next code block to ensure
    // that maxSignal equals at
    // least one of the inputs
    EQ[i] = IsEqual();
    EQ[i].in[0] <== maxSignal;
    EQ[i].in[1] <== in[i];

    // acc is greater than zero
    // (acc != 0) if EQ[i].out
    // equals 1 at least one time
    acc += EQ[i].out;
  }

  // assert that maxSignal is 
  // equal to at least one of the
  // inputs. if acc = 0 then
  // none of the inputs equals
  // maxSignal
  signal allZero;
  allZero <== IsEqual()([0, acc]);
  allZero === 0;
  out <== max;
}

component main = Max(8);

Exercise: Create a circuit that does the same as above, but for the min.

Example 2: array Is sorted

We can assert that an array is sorted by checking that each element is less than or equal to the subsequent one. Unlike the previous example which needed n components, we need n - 1 components since we are comparing neighboring values to each other. Since we have n elements, we are going to do n - 1 comparisons.

Here is a template that constrains an input array in[n] to be sorted. Note that if an array only has one element, it is sorted by definition, and the circuit below is also compatible with that scenario:

pragma circom 2.1.6;

include "circomlib/comparators.circom";

template IsSorted(n) {
  signal input in[n];

  component lt[n - 1];

  // loop goes up to n - 1, not n
  for (var i = 0; i < n - 1; i++) {
    lt[i] = LessThan(252);
    lt[i].in[0] <== in[i];
    lt[i].in[1] <== in[i+1];
    lt[i].out === 1;
  }
}

component main = IsSorted(3);

Example 3: All items are unique

To check that all items in a list are unique, the most straightforward way is to use a hashmap — but hashmaps are not available in arithmetic circuits. The second most efficient way is to sort the list, but sorting inside a circuit is quite tricky, so we avoid that for now. This leaves us with the brute force solution of comparing every element to every other element. This requires a nested for-loop.

The computation we are doing can be illustrated as follows:

$$ \begin{array}{c|c|c|c|c|} &a_1&a_2&a_3&a_4\\ \hline a_1&&\neq&\neq&\neq\\ \hline a_2&&&\neq&\neq\\ \hline a_3&&&&\neq\\ \hline a_4\\ \hline \end{array} $$

In general, there will be

$$ \frac{n(n-1)}{2} $$

inequality checks, so we will need that many components.

We show how to accomplish this below:

pragma circom 2.1.8;

include "./node_modules/circomlib/comparators.circom";

template ForceNotEqual() {
  signal input in[2];

  component iseq = IsEqual();
  iseq.in[0] <== in[0];
  iseq.in[1] <== in[1];
  iseq.out === 0;
}

template AllUnique (n) {
  signal input in[n];

  // the nested loop below will run
  // n * (n - 1) / 2 times
  component Fneq[n * (n-1)/2];

  // loop from 0 to n - 1
  var index = 0;
  for (var i = 0; i < n - 1; i++) {
    // loop from i + 1 to n
    for (var j = i + 1; j < n; j++) {
      Fneq[index] = ForceNotEqual();
      Fneq[index].in[0] <== in[i];
      Fneq[index].in[1] <== in[j];
      index++;
    }
  }
}

component main = AllUnique(5);

Summary

To use Circom components inside a loop, we declare an array of components outside the loop without specifying the type.

Then inside the loop, we declare the components and constrain the inputs and outputs of the component.

Circle FFT — Part 1: Building the Circle Domain

Circle FFT — Part 1: Building the Circle Domain Circle STARKs is a new zk-STARK scheme that has been implemented in Stwo and Plonky3, and it has been adopted by several zkVM projects. Its key innovation lies in enabling the use of small 32-bit fields (Mersenne prime $2^{31}-1$) while still maintaining the mathematical properties needed for efficient FFT operations. […]

Multiplicative Subgroups and Primitive Elements

Multiplicative Subgroups and Primitive Elements Introduction This chapter continues our study of group theory by exploring subgroups and generators. The concept of a primitive element will be introduced at the end. We assume you are already familiar with the definition of a group. If you need a refresher, check out this article. To build intuition, […]

Square and Multiply Algorithm

Square and Multiply Algorithm The square and multiply algorithm computes integer exponents in $\mathcal{O}(\log n)$ (logarithmic time). The naive way to compute an exponent $x^n$ is to multiply $x$ by itself, $n$ times, which would require $\mathcal{O}(n)$ time to compute. Suppose we want to compute $x^{20}$. Instead of multiplying $x$ with itself 20 times, we […]

Computing the Current Tick Given sqrtPriceX96

Computing the Current Tick Given sqrtPriceX96 In the previous chapters, we saw that the protocol stores the square root of the price instead of the price itself. Therefore, it is necessary to relate ticks to the square root of the price in the fixed-point Q64.96 format. In this chapter, we will explore how to convert […]

Opportunities

Looking for an audit?

Leverage our extensive network of top security specialists.

Get A Quote
Security Engineer

Asymmetric Research is looking for someone to research and secure Solana/Rust-based smart contracts, build security tools, and lead audits.

Apply Now
Senior Rust Engineer

Irys is looking for a Senior Rust Engineer to lead protocol development, optimise core blockchain systems, and drive technical excellence.

Apply Now
Intermediate Rust Engineer

ZK Email is looking for a Rust engineer to build zero-knowledge tools, implement core features, and contribute to high-impact projects like zk-regex, zk-email, and partner integrations.

Apply Now