Components and Templates

C++ has Classes. Solidity has Contracts. Circom has Components.

What are Components / Templates?

Templates are the definition of generic, indivisible circuits in Circom. Components are the instantiation of these templates inside of other circuits. Templates provide the mechanism for creating API's using Signals in Circom. That is, by defining logic within templates, components can be extended to quickly specify a set logical pattern/ action within your circuits.

Though we draw the analogy to classes/ contracts, templates do not provide any sort of object-oriented programming.

The Main Component

The main component defines the circuit you intend to construct your Zero Knowledge proofs with. The main component can contain arbitrarily many components within its circuit, however there can only be one main component at the top level.

As mentioned in the Signals and Variables section, Circom developers specify their public inputs when they declare a template is the main component. This is shown below, where we want inPub to be a public input and inPriv to be a shielded, private input:

pragma circom 2.0.4;

template Multiplier2(){
   //Declaration of signals
   signal input inPub;
   signal input inPriv;
   signal output out <== inPub * inPriv; // inline output signal declaration
}

component main {public [inPub]} = Multiplier2(); 

Note that your entire code base is not limited to one main component. Your app's intended functionality may rely on multiple different proofs, in which each proof has its own main component. In BattleZips, both the shot proof and the board proof are main components.

Template Parameters

Oftentimes you will find that your template's logic needs the Circom equivalent of a generic parameter. The values are still integers on the field, but during the execution of a component you can supply constant values that are parameterized at the template level.

Let's take the example of a num2bits decomposition component.

template Num2Bits(n) { // n defines # of bits in number being bitified
    signal input in;
    signal output out[n]; // provide n bits out
    var lc1=0;

    var e2=1;
    for (var i = 0; i<n; i++) { // iterate n times to get each bit
        out[i] <-- (in >> i) & 1;
        out[i] * (out[i] -1 ) === 0;
        lc1 += out[i] * e2;
        e2 = e2+e2;
    }

    lc1 === in;
}

In this case, we provide the template parameter n to define the number of bits in the number we are decomposing into a bit array. We want to build modular tool that can be extensively reused with few barriers; by using template parameters we can make generic circuits that can be molded for their specific application.

You can also use template parameters with the main component. This can be seen in the RollupNC code base:

template Main(balDepth, txDepth) {
    .
    .
    .
}

component main { public [txRoot, prevRoot, nextRoot] } = Main(4, 2);

This allows us to configure a specific constant to use in our Zero Knowledge proof computation while also maintaining a degree of modularity over our implementations. Practically, for the example of RollupNC, we use small account and transaction trees since we are using this code to run unit tests. However, we could very easily scale to production size by simply changing the Main component to hold millions of accounts with hundreds of transactions per batch by defining the main component with the parameters:

component main { public [txRoot, prevRoot, nextRoot] } = Main(24, 8);

Working with Components

In the case of num2bits shown above, we can see how it is employed in BattleZips (code abridged here, see full source)

pragma circom 2.0.3;

include "node_modules/circomlib/circuits/bitify.circom"; // import template

template PlaceShip() {
    signal input boardIn; // numerical representation of board bitmap
    component toBits = Num2Bits(100); // instantiate num2bits component with 100 bits
    toBits.in <== boardIn; // assign number to decompose to num2bits component
    var boardH[10][10]; 
    for (var i = 0; i < 100; i++) {
        boardH[i \ 10][i % 10] = toBits.out[i]; // utilise the output of num2bits
    }
    .
    .
    .
}

In the above case, we assign a variable with the component's output. However, you are just as able to use the <-- and <== operators to assign signals with a component's output.

When using a component, all inputs MUST be assigned before attempting to assign another signal using the output of the component. For instance, if in the above code we only assigned 99 of the 100 bits to toBits.in, then tried to access toBits.out, we would experience a compile-time error.

Anonymous Components

Circom 2.1 included the addition of anonymous components. Much like the concept of anonymous functions, the Circom docs define anonymous components as such:

An anonymous component allows in a single instruction 1) the implicit declaration of a new component, 2) the assignment of every input signal and, finally, 3) the assignment of every output signal.

In simple terms, there are times when we can treat a component like a throwaway function where in one shot we want to extract an output given an input. In previous versions of Circom, whether you want to repeatedly access a component or only need it once, you must declare the component as shown below:

template A(n){
   signal input a, b;
   signal output c;
   c <== a*b;
}
template B(n){
   signal input in[n];
   signal out;
   component temp_a = A(n);
   temp_a.a <== in[0]; 
   temp_a.b <== in[1];
   out <== temp_a.c;
}
component main = B(2);

In template B, we do not care about the component temp_a and do not need such a verbose API to access it. Anonymous templates mean that we do not need to declare temp_a at all, streamlining the code to look like:

template A(n){
   signal input a, b;
   signal output c;
   c <== a*b;
}
template B(n){
   signal input in[n];
   signal out <== A(n)(in[0],in[1]);
}
component main = B(2);

Last updated