ToolszkEVMSpecIndex

Modular Programs

One of the core features of _Polynomial Identity Language_ (PIL) is that it allows modular design of its programs. This document describes how PIL connects prog

One of the core features of Polynomial Identity Language (PIL) is that it allows modular design of its programs. This document describes how PIL connects programs.

Modular design

One of PIL's core features is that it allows modular design of its programs. By modular, we mean the ability to split the design of a program MM into multiple smaller programs, such that a proper combination of these small programs leads to MM.

Without this feature, one would need to combine the logic of multiple pieces in a single design, and as a consequence, a lot of redundant polynomials would be included in the design. Moreover, the resulting design would be too big and difficult to validate and test.

The following example is used throughout this section to illustrate PIL's modularity.

Example

Suppose that we want to design a program that verifies the operation aaˉa \cdot \bar{a}, where aa is a 4-bit integer and aˉ\bar{a} is defined as the integer obtained by negating each of the bits of aa. (i.e., aˉ\bar{a} is the bitwise negation of aa).

The difficulty here is that bitwise operations are difficult to describe when working directly with chunks of 4 bits.

Then, the idea is to split these integers into bits in another program, allowing us to check the operations in a trivial way.

The main program consists of 33 columns; a\texttt{a}, neg_a\mathtt{neg\_a}, and op\texttt{op}. In each row, the column a\texttt{a} contains the integer aa of the computation aaˉa \cdot \bar{a}. The columns, neg_a\mathtt{neg\_a} and op\texttt{op}, contain aˉ\bar{a} and aaˉa \cdot \bar{a}, respectively.

The below table represents a valid execution trace of the program that validates negated strings of bits.

rowaneg_aop  11101001000011010  20100101100101100  31111000000000000  41000011100111000 \begin{aligned} \begin{array}{|c|c|c|c|}\hline \texttt{row} & \mathtt{a} & \mathtt{neg\_a} & \texttt{op} \\ \hline \ \text{ 1} & \texttt{1101} & \texttt{0010} & \texttt{00011010} \\ \ \text{ 2} & \texttt{0100} & \texttt{1011} & \texttt{00101100} \\ \ \text{ 3} & \texttt{1111} & \texttt{0000} & \texttt{00000000} \\ \ \text{ 4} & \texttt{1000} & \texttt{0111} & \texttt{00111000} \\ \hline \end{array} \end{aligned}

First, there is a need to enforce that each of the inputs is a 4-bit integer. That is, an integer in the range [0,15][0, 15].

This can be enforced via an inclusion argument. Specifically, this argument enforces that all the values of a vector belong to a certain range (publicly known). For this reason, such a family of inclusion arguments is often referred to as range checks.

The PIL code for a range check of a column a\texttt{a} is as follows:

include "config.pil";

namespace Global(%N);
pol constant BITS4;

namespace Main(%N);
pol commit a, neg_a , op;

a in Global.BITS4;

Remarks about the above code

BITS4 is a polynomial containing each of the possible 4-bit integers. These 4-bit integers can be chosen in any order when constructing BITS4 because inclusion checks do not respect orderings.

Also, observe that BITS4 is called from a namespace which is different from where it is defined. The syntax Namespace.polynomial can be used to access polynomials of other namespaces.

The traditional procedure is to put different namespaces in separate files and then use the include keyword to “connect” them. For instance, two programs can be defined as follows.

// Filename: global.pil

include "config.pil";

namespace Global(%N); 
pol constant L1;
pol constant BITS4;
// Filename: main.pil

include "config.pil"; 
include "global.pil"

namespace Main(%N);
pol commit a, neg_a , op;

a in Global.BITS4;

As mentioned before, working directly with 44-bits would be difficult. Hence, a separate program that works bitwise is designed. This program contains a column called bits\texttt{bits} that stores the single bits that shape each of the integers present in a\texttt{a}, expressed in little-endian. Similarly, another column called nbits\texttt{nbits} containing the negation of each one of the 44-bit integers in a\texttt{a}.

See the below table for a concrete example of the execution trace.

rowbitsnbitsFACTOR110120123102241023501160127102280123\begin{aligned}\begin{array}{|c|c|c|c|}\hline \texttt{row} & \mathtt{bits} & \mathtt{nbits} & \mathtt{FACTOR} \\ \hline \text{1} & \texttt{1} & \texttt{0} & \texttt{1} \\ \text{2} & \texttt{0} & \texttt{1} & \texttt{2} \\ \text{3} & \texttt{1} & \texttt{0} & \mathtt{2^2} \\ \text{4} & \texttt{1} & \texttt{0} & \mathtt{2^3} \\ \hline \text{5} & \texttt{0} & \texttt{1} & \texttt{1} \\ \text{6} & \texttt{0} & \texttt{1} & \texttt{2} \\ \text{7} & \texttt{1} & \texttt{0} & \mathtt{2^2} \\ \text{8} & \texttt{0} & \texttt{1} & \mathtt{2^3} \\ \hline \vdots & \vdots & \vdots & \vdots \\ \hline \end{array} \end{aligned}

Since bits=nbits\overline{\mathtt{bits}} = \mathtt{nbits}, and bits\mathtt{bits}, nbits{0,1}\mathtt{nbits} \in \{ 0, 1 \}, we can express the relation between the columns bits\mathtt{bits} and nbits\mathtt{nbits} as:

bits+nbits=1.\mathtt{bits} + \mathtt{nbits} = 1.

The idea of connecting the Main\mathtt{Main} program with the Negation\mathtt{Negation} program is to construct aa and aˉ\bar{a} from the given bits of each free input integer aa. Therefore, the inclusion argument that verifies that the tuple (a,aˉ)(a,\bar{a}) from the Main\mathtt{Main} program is included in the Negation\mathtt{Negation} program also proves the fact that aˉ\bar{a} is correctly constructed. In order to achieve this, we need a constant polynomial called FACTOR\mathtt{FACTOR}. As depicted in the above table, FACTOR\mathtt{FACTOR} places the bits in their correct bit-position.

At the same time, a constant polynomial RESET\texttt{RESET} is necessary so as to allow resetting the generation of columns a\texttt{a} and neg_a\mathtt{neg\_a} from the bits of bits\mathtt{bits} and nbits\mathtt{nbits} respectively, after every 4 rows.

Observe that the cyclic behavior is ensured in this situation because 44 divides N=210\mathtt{N} = 2^{10}. Since N\mathtt{N} should be a power of 22, this requirement is always satisfied.

The following are the constraints that should be added to PIL in order to describe this generation:

a = FACTORbits + (1RESET)a(Eqn. 11a)\mathtt{a}'\ = \ \mathtt{FACTOR}' \cdot \mathtt{bits}' \ +\ (1 - \mathtt{RESET}) \cdot \mathtt{a} \qquad\qquad\qquad \tag{Eqn. 11a} neg_a = FACTORnbits + (1RESET)neg_a (Eqn. 11b)\mathtt{neg\_a}'\ =\ \mathtt{FACTOR}' \cdot \mathtt{nbits}'\ +\ (1 - \mathtt{RESET}) \cdot \mathtt{neg\_a} \quad\ \tag{Eqn. 11b}

The below table shows a complete example of what the execution trace of the Negation\mathtt{Negation} program looks like.

rowbitsnbitsFACTORaneg_aRESET110110020120110031022101010041023110100101501101060120011071022100011080123010010111\begin{aligned}\begin{array}{|c|c|c|c|c|c|c|}\hline \texttt{row} & \mathtt{bits} & \mathtt{nbits} & \mathtt{FACTOR} & \mathtt{a} & \mathtt{neg\_a} & \mathtt{RESET} \\ \hline \text{1} & \texttt{1} & \texttt{0} & \texttt{1} & \texttt{1} & \texttt{0} & \texttt{0} \\ \text{2} & \texttt{0} & \texttt{1} & \texttt{2} & \texttt{01} & \texttt{10} & \texttt{0} \\ \text{3} & \texttt{1} & \texttt{0} & \mathtt{2^2} & \texttt{101} & \texttt{010} & \texttt{0} \\ \text{4} & \texttt{1} & \texttt{0} & \mathtt{2^3} & \texttt{1101} & \texttt{0010} & \texttt{1} \\ \hline \text{5} & \texttt{0} & \texttt{1} & \texttt{1} & \texttt{0} & \texttt{1} & \texttt{0} \\ \text{6} & \texttt{0} & \texttt{1} & \texttt{2} & \texttt{00} & \texttt{11} & \texttt{0} \\ \text{7} & \texttt{1} & \texttt{0} & \mathtt{2^2} & \texttt{100} & \texttt{011} & \texttt{0} \\ \text{8} & \texttt{0} & \texttt{1} & \mathtt{2^3} & \texttt{0100} & \texttt{1011} & \texttt{1} \\ \hline \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\ \hline \end{array} \end{aligned}

Observe that the RESET\mathtt{RESET} polynomial ensures that the constraints labelled Eqn. 11\text{Eqn. 11} above, are satisfied in each row.

The file which describes correct execution of the Negation\texttt{Negation} program (negation.pil) is now as follows:

namespace Negation(%N);
pol commit bits, nbits;
pol commit a, neg_a;
pol constant FACTOR , RESET;

bits*(1-bits) = 0; 
nbits*(1-nbits) = 0;

bits + nbits - 2*bits*nbits = 1;

a' = FACTOR'*bits' + (1 - RESET)*a;
neg_a ' = FACTOR '*nbits ' + (1 - RESET)*neg_a;

Observe that binary checks for the columns bits\texttt{bits} and nbits\texttt{nbits} have been added because we need to ensure that both of them are actually bits.

Now, we connect the Main\texttt{Main} and the Negation\texttt{Negation} program via an inclusion check, adding the following line into the Main\texttt{Main} PIL’s namespace:

{a, neg_a} in {Negation.a, Negation.neg_a};

PIL also allows users to add selectors in the inclusion checks. This feature allows huge malleability as it allows the user to check inclusions between smaller subsets of rows of the execution trace.

With the next line of code, a check on whether a tuple (a,aˉ)(\mathtt{a}, \bar{\mathtt{a}}) is contained in columns a\mathtt{a} and neg_a\mathtt{neg\_a} in a row where RESET=1\mathtt{RESET} = 1, is enforced.

{a, neg_a} in Negation.RESET {Negation.a, Negation.neg_a}

However, this introduces redundancy in the PIL because of the design of program itself.

The same is possible from the other side, adding a selector polynomial SEL\texttt{SEL} in the Main program:

sel {a, neg_a} in {Negation.a, Negation.neg_a}

The above feature, of enforcing row-selective inclusion checks, is crucial in PIL especially in situations where inclusions must be satisfied only in a subset of rows. We later on see that this is important for improved proving performance.

Also, we can use a combination of selectors, one for each side:

sel {a, neg_a} in Negation.RESET {Negation.a, Negation.neg_a}

Up to this point, we have created a program (called Main\mathtt{Main}) that uses another program (called Negation\texttt{Negation}) to validate that the negation of a specific column a\texttt{a} is well constructed.

However, the product of the columns a\texttt{a} and neg_a\mathtt{neg\_a} still needs to be validated.

In order to handle this, the following constraint can be introduced in the PIL code of Main\mathtt{Main} program:

aneg_a = op\texttt{a} \cdot \mathtt{neg\_a}\ =\ \mathtt{op}

Since the aim here is to illustrate application of the op\mathtt{op} polynpomial, and how connections among several programs work, the first version of our previously constructed Multiplier\texttt{Multiplier} program suffices.

We simply add the following line of code to achieve this:

{a, neg_a, op} in {Multiplier.freeIn1, Multiplier.freeIn2, Multiplier.out};

PIL codes

PIL codes of all the newly developed programs can be found below.

include "config.pil"; 

namespace Global(%N);
pol constant BITS4;
include "global.pil"; 
include "multiplier.pil"; 
include "negation.pil"; 
include "config.pil";

namespace Main(%N);
pol commit a, neg_a , op;

a in Global.BITS4;

{a, neg_a} in {Negation.a, Negation.neg_a};
{a, neg_a, op} in {Multiplier.freeIn1, Multiplier.freeIn2, Multiplier.out};
include "config.pil";

namespace Negation(%N);
pol commit bits, nbits;
pol commit a, neg_a;
pol constant FACTOR , RESET;

bits*(1-bits) = 0; 
nbits*(1-nbits) = 0;

bits + nbits - 2*bits*nbits = 1;

a' = FACTOR'*bits' + (1 - RESET)*a;
neg_a ' = FACTOR '*nbits ' + (1 - RESET)*neg_a;

Having all the .pil files in the same directory, we can compile main.pil and obtain the following debug message:

Input Pol Commitments: 10
Q Pol Commitments: 0
Constant Pols: 3
Im Pols: 0
plookupIdentities: 3
permutationIdentities: 0
connectionIdentities: 0
polIdentities: 6
Edit on GitHub

Last updated on