ToolszkEVMSpecIndex

Inclusion Arguments

This document describes how Polynomial Identity Language implements Inclusion Arguments. For most of the programs used in the zkEVM's Prover, the values recorde

This document describes how Polynomial Identity Language implements Inclusion Arguments.

For most of the programs used in the zkEVM's Prover, the values recorded in the columns of execution traces are field elements.

In some cases, there may be a need to restrict the sizes of these values to only a certain number of bits. It is therefore necessary to devise a good control strategy for handling both underflows and overflows.

Verify addition of 2-byte numbers

The aim in this section is to design a program (and its corresponding PIL code) to verify addition of two integers each of a size fitting in exactly 2 bytes. Any input to the addition program is considered invalid if it is not an integer in the range [0, 65535].

Naive execution trace

Of course, there are many ways in which such a program can be arithmetized. We are going to use a method based on inclusion arguments. The overall idea is to reduce 22-byte additions to 11-byte additions.

Specifically, the program consists of two input polynomials a\texttt{a} and b\texttt{b} where each introduces a single byte (equivalently, an integer in the range [0, 255]) in each row, starting with the less significant byte, for each operand of the sum.

Hence, a new addition is checked in every second row.

rowaboperation  1ox110x22undefined  20x300x400x3011+0x4022  30xff0xeeundefined  40x000xff0x00ff+0xffee\begin{aligned} \begin{array}{|c|c|c|c|}\hline \texttt{row} & \mathtt{a} & \mathtt{b} & \mathtt{operation} \\ \hline \ \text{ 1} & \texttt{ox11} & \texttt{0x22} & `{undefined}` \\ \ \text{ 2} & \texttt{0x30} & \texttt{0x40} & \texttt{0x3011} + \texttt{0x4022} \\ \hline \ \text{ 3} & \texttt{0xff} & \texttt{0xee} & `{undefined}` \\ \ \text{ 4} & \texttt{0x00} & \texttt{0xff} & \texttt{0x00ff} + \texttt{0xffee} \\ \hline \end{array} \end{aligned}

The output of the addition between 11-byte words can't be stored in a single column that only accepts words of 11-byte, since an overflow may occur.

The result of the addition is split into two columns; carry\texttt{carry} and add\texttt{add}. Each column accepts only 11-byte words, thereby storing the complete result so that a correct addition between bytes can be defined as:

a + b =carry28 +add(Eqn. 9)\texttt{a}\ +\ \texttt{b} \ = \texttt{carry}\cdot 2^8\ + \texttt{add} \qquad\quad \tag{Eqn. 9}

The below table shows an example of a valid execution trace for this program.

rowabcarryadd  1ox110x220x000x33  20x300x400x000x70  30xff0xee0x010xed  40x000xff0x010x00\begin{aligned} \begin{array}{|l|c|c|c|c|}\hline \texttt{row} & \mathtt{a} & \mathtt{b} & \texttt{carry} & \texttt{add} \\ \hline \ \text{ 1} & \texttt{ox11} & \texttt{0x22} & \texttt{0x00} & \texttt{0x33} \\ \ \text{ 2} & \texttt{0x30} & \texttt{0x40} & \texttt{0x00} & \texttt{0x70} \\ \hline \ \text{ 3} & \texttt{0xff} & \texttt{0xee} & \texttt{0x01} & \texttt{0xed}\\ \ \text{ 4} & \texttt{0x00} & \texttt{0xff} & \texttt{0x01} & \texttt{0x00} \\ \hline \end{array} \end{aligned}

Note that the final sum of each pair of 22-byte numbers, in the above table, is composed of the last carry\texttt{carry} value and the last two add\texttt{add} values, from the most significant to the least significant (going from left to right).

For example, 0x3011+0x4022=0x007033\texttt{0x3011} + \texttt{0x4022} = \texttt{0x007033}. Similarly, 0x00ff+0xffee=0x0100ed\texttt{0x00ff} + \texttt{0xffee} = \texttt{0x0100ed}.

Observe that Eqn. 9\text{Eqn. 9} is not satisfied in row 4, because the carry\texttt{carry} value raised in the third row must be added to the 11-byte addition at row 4.

The problem with this design is that there's no direct way to introduce the previous value of carry\texttt{carry} in the PIL code.

Adding an extra polynomial

There is a need to introduce another polynomial, call it prevCarry\texttt{prevCarry}, which contains a shifted version of the value in the carry\texttt{carry} polynomial.

More specifically, we add the following constraint to ensure that prevCarry\texttt{prevCarry} is correctly defined:

prevCarry =carry(Eqn. 10)\texttt{prevCarry}'\ = \texttt{carry} \tag{Eqn. 10} rowabprevCarrycarryadd  1ox110x220x010x000x33  20x300x400x000x000x70  30xff0xee0x000x010xed  40x000xff0x010x010x00\begin{aligned} \begin{array}{|c|c|c|c|c|c|}\hline \texttt{row} & \mathtt{a} & \mathtt{b} & \texttt{prevCarry} & \texttt{carry} & \texttt{add} \\ \hline \ \text{ 1} & \texttt{ox11} & \texttt{0x22} & \texttt{0x01} & \texttt{0x00} & \texttt{0x33} \\ \ \text{ 2} & \texttt{0x30} & \texttt{0x40} & \texttt{0x00} & \texttt{0x00} & \texttt{0x70} \\ \hline \ \text{ 3} & \texttt{0xff} & \texttt{0xee} & \texttt{0x00} & \texttt{0x01} & \texttt{0xed}\\ \ \text{ 4} & \texttt{0x00} & \texttt{0xff} & \texttt{0x01} & \texttt{0x01} & \texttt{0x00} \\ \hline \end{array} \end{aligned}

There is another snag here. According to Eqn.10\text{Eqn.10}, the previous carry\texttt{carry} value could affect two different 2-byte additions. That is, two non-related operations should not be linked via carries.

A selector polynomial can be used to separate values of carry\texttt{carry} from one addition to another. We use RESET\texttt{RESET} as previously used.

RESET=1\texttt{RESET} = 1 for every odd row index, and RESET=0\texttt{RESET} = 0 otherwise.

The execution trace is now adjusted as follows,

rowabprevCarrycarryaddRESET  1ox110x220x010x000x331  20x300x400x000x000x700  30xff0xee0x000x010xed1  40x000xff0x010x010x000\begin{aligned} \begin{array}{|c|c|c|c|c|c|c|}\hline \texttt{row} & \mathtt{a} & \mathtt{b} & \texttt{prevCarry} & \texttt{carry} & \texttt{add} & \texttt{RESET} \\ \hline \ \text{ 1} & \texttt{ox11} & \texttt{0x22} & \texttt{0x01} & \texttt{0x00} & \texttt{0x33} & \texttt{1} \\ \ \text{ 2} & \texttt{0x30} & \texttt{0x40} & \texttt{0x00} & \texttt{0x00} & \texttt{0x70} & \texttt{0} \\ \hline \ \text{ 3} & \texttt{0xff} & \texttt{0xee} & \texttt{0x00} & \texttt{0x01} & \texttt{0xed} & \texttt{1} \\ \ \text{ 4} & \texttt{0x00} & \texttt{0xff} & \texttt{0x01} & \texttt{0x01} & \texttt{0x00} & \texttt{0} \\ \hline \end{array} \end{aligned}

Following this logic, we can now derive the final (and accurate) constraint:

a + b + (1RESET)prevCarry = carry28 + add.(Eqn. 11)\texttt{a}\ +\ \texttt{b}\ +\ (1 - \texttt{RESET})\cdot \texttt{prevCarry}\ =\ \texttt{carry} \cdot 2^8\ +\ \texttt{add}. \tag{Eqn. 11}

The PIL code can now be written as follows:

include "config.pil"; 

namespace TwoByteAdd(%N);

pol constant RESET;
pol commit a, b;
pol commit carry , prevCarry , add;

prevCarry ' = carry;
a + b + (1-RESET)*prevCarry = carry*2**8 + add;

Addition of n-byte numbers

Similarly to the Multiplier example of the previous sections, it is worth mentioning that by changing only the RESET\texttt{RESET} polynomial (and not the PIL itself), it is possible to arithmetize the program in such a way that generic nn-byte additions can be verified.

In such cases, RESET=1\texttt{RESET} = 1 for the first n1n-1 rows of each operation, and RESET=0\texttt{RESET} = 0 in the nn-th row of the operation.

Up to this point, one can think that the constraint in Eqn. 11\text{Eqn. 11} restricts sound representation of the program. However, since we are working over a finite field, it is not.

For example, the following execution trace is valid, as it satisfies all the constraints, but does not correspond to a valid computation:

rowabprevCarrycarryaddRESET  1ox110x220x01p280x331  20x300x40p280x000x70+p280  30xff0xee0x000x01+p280xed1  40x000xff0x01+p280x010x00+p280\begin{aligned} \begin{array}{|c|c|c|c|c|c|c|}\hline \texttt{row} & \mathtt{a} & \mathtt{b} & \texttt{prevCarry} & \texttt{carry} & \texttt{add} & \texttt{RESET} \\ \hline \ \text{ 1} & \texttt{ox11} & \texttt{0x22} & \texttt{0x01} & p\cdot 2^8 & \texttt{0x33} & \texttt{1} \\ \ \text{ 2} & \texttt{0x30} & \texttt{0x40} & p\cdot 2^8 & \texttt{0x00} & \texttt{0x70} + p\cdot 2^8 & \texttt{0} \\ \hline \ \text{ 3} & \texttt{0xff} & \texttt{0xee} & \texttt{0x00} & \texttt{0x01} + p\cdot 2^8 & \texttt{0xed} & \texttt{1} \\ \ \text{ 4} & \texttt{0x00} & \texttt{0xff} & \texttt{0x01} + p\cdot 2^8 & \texttt{0x01} & \texttt{0x00} + p\cdot 2^8 & \texttt{0} \\ \hline \end{array} \end{aligned}

The above table gives a concrete example of how to work around any restrictions of the current program.

Nonetheless, the introduction of more bytes in either column does not break the intention of this program, which was designed to only deal with byte-sized columns. It is strictly necessary to enforce that all the evaluations of committed polynomials be correct.

An inclusion argument is used for this purpose.

Inclusion argument

Given two vectors, a=(a1,...,an)Fpn\texttt{a} = (a_1, . . . , a_n) \in \mathbb{F}^n_p and b=(b1,...,bm)Fpm\texttt{b} = (b_1,...,b_m) \in \mathbb{F}^m_p, it is said that,

a  is  contained in  b  if for all  i[n],  there exists a  j[m]  such that  ai=bj.\texttt{a}\ \text{ is } \texttt{ contained in }\ \texttt{b}\ \text{ if for all }\ i ∈ [n],\ \text{ there exists a }\ j ∈ [m]\ \text{ such that }\ a_i = b_j.

In other words, if one thinks of a\texttt{a} and b\texttt{b} as multisets and reduce them to sets (by removing the multiplicity), then a\texttt{a} is contained in b\texttt{b} if a\texttt{a} is a subset of b\texttt{b}. See this document for more details on multisets and Plookup.

A protocol (P,V)(\mathcal{P},\mathcal{V}) is an inclusion argument if the protocol can be used by P\mathcal{P} to prove to V\mathcal{V} that one vector is contained in another vector.

In the PIL context, the implemented inclusion argument is the same as the Plookup\text{Plookup} method provided in [GW20], also discussed here. Other "alternative" method exists such as PlonkUp.

An inclusion argument is invoked in PIL with the "in\texttt{in}" keyword.

Specifically, given two columns a\texttt{a} and b\texttt{b}, we can declare an inclusion argument between them using the syntax {a} in {b}\{\texttt{a}\}\ \texttt{in} \ \{\texttt{b}\}, where a\texttt{a} and b\texttt{b} do not necessarily need to be defined in different programs.

For instance, the PIL code for a program called A\texttt{A}, with an inclusion argument is as follows.

include "config.pil"; 

namespace A(%N);
pol commit a, b;

{a} in {b};

namespace B(%N); 
pol commit a, b;

{a} in {Example1.b};

A valid execution trace (with N=4\texttt{N} = 4) for the above example is shown in the below table.

Two Programs each with 2-column Execution Traces

Generalized inclusion arguments

In PIL we can also write inclusion arguments not only over single columns but over multiple columns. That is, given two subsets of committed columns a1,,am\mathtt{a_1}, \dots , \mathtt{a}_m and b1,,bm\texttt{b}_1, \dots , \texttt{b}_m of some program(s) we can write as,

{a1,...,am} in {b1,...,bm}\{ \mathtt{a}_1,...,\mathtt{a}_m \}\ \texttt{in}\ \{ \mathtt{b}_1,..., \mathtt{b}_m \}

to denote that the rows generated by columns a1,,am\mathtt{a}_1, \dots , \mathtt{a}_m are included (as sets) in the rows generated by columns {bm,,bm}\{\mathtt{b}_m , \dots , \mathtt{b}_m\}.

A natural application for this generalization shows that a set of columns that a program repeatedly computes, probably with the same pair of inputs/outputs, an operation such as AND\texttt{AND}, where the correct AND\texttt{AND} operation is carried on a distinct program (see the following example).

include "config.pil"; 

namespace Main(%N);
pol commit a, b, c; 

{a,b,c} in {in1,in2,xor};

namespace AND(%N);
pol constant in1, in2, and;

Following on with the previous "TwoByteAdd" Addition program example, one can construct four new constant polynomials; BYTE_A\mathtt{BYTE\_A}, BYTE_B\mathtt{BYTE\_B}, BYTE_CARRY\mathtt{BYTE\_CARRY} and BYTE_ADD\mathtt{BYTE\_ADD}; containing all possible byte additions.

The execution trace of these polynomials can be constructed as follows:

rowBYTE_ABYTE_BBYTE_CARRYBYTE_ADD10x000x000x000x0020x000x010x000x0130x000x020x000x022560x000xff0x000xff2580x010x000x000x012590x010x010x000x02655350xff0xfe0x010xfd655360xff0xff0x010xfe\begin{aligned} \begin{array}{|c|c|c|c|c|}\hline \texttt{row} & \mathtt{ BYTE\_A} & \mathtt{BYTE\_B} & \mathtt{BYTE\_CARRY} & \mathtt{BYTE\_ADD} \\ \hline \texttt{1} & \texttt{0x00} & \texttt{0x00} & \texttt{0x00} & \texttt{0x00} \\ \texttt{2} & \texttt{0x00} & \texttt{0x01} & \texttt{0x00} & \texttt{0x01} \\ \texttt{3} & \texttt{0x00} & \texttt{0x02} & \texttt{0x00} & \texttt{0x02} \\ \vdots & \vdots & \vdots & \vdots & \vdots \\ \texttt{256} & \texttt{0x00} & \texttt{0xff} & \texttt{0x00} & \texttt{0xff} \\ \texttt{258} & \texttt{0x01} & \texttt{0x00} & \texttt{0x00} & \texttt{0x01} \\ \texttt{259} & \texttt{0x01} & \texttt{0x01} & \texttt{0x00} & \texttt{0x02} \\ \vdots & \vdots & \vdots & \vdots & \vdots \\ \texttt{65535} & \texttt{0xff} & \texttt{0xfe} & \texttt{0x01} & \texttt{0xfd} \\ \texttt{65536} & \texttt{0xff} & \texttt{0xff} & \texttt{0x01} & \texttt{0xfe} \\ \hline \end{array} \end{aligned}

Recall that there is no need to enforce constraints between these polynomials since they are constant and therefore, publicly known.

As to whether the tuple (a,b,carry,add)(\texttt{a}, \texttt{b}, \texttt{carry}, \texttt{add}) is contained in the previous table, an inclusion argument can be utilized and thus ensure a sound description of the program. The inclusion constraint is not only ensuring that all the values are single bytes, but also that the addition is correctly computed.

Consequently, none of the rows of the above table is contained in the previous table, marking them as non-valid rows.

Of course, this introduces some redundancy into the PIL code, because the byte operation is being checked twice. First with the polynomial constraint, and second with the inclusion argument. However, the polynomial constraint cannot be dropped as it is necessary for linking rows belonging to the same addition.

The following line of code completes the PIL for the TwoByteAdd\texttt{TwoByteAdd} program:

{a, b, carry, add} in {BYTE_A, BYTE_B, BYTE_CARRY, BYTE_ADD};

To sum up, the following PIL program correctly describes the complete TwoByteAdd\texttt{TwoByteAdd} program:

include "config.pil"; 

namespace TwoByteAdd(%N);

pol constant BYTE_A, BYTE_B, BYTE_CARRY, BYTE_ADD;
pol constant RESET;
pol commit a, b;
pol commit carry, prevCarry, add;

prevCarry ' = carry;
a + b + (1 - RESET)*prevCarry = carry*2**8 + add;

{a, b, carry, add} in {BYTE_A, BYTE_B, BYTE_CARRY, BYTE_ADD};

Compiling this .pil file, we get the following debugging message:

Input Pol Commitmets: 5
Q Pol Commitmets: 0
Constant Pols: 5
Im Pols: 0
plookupIdentities: 1
permutationIdentities: 0
connectionIdentities: 0
polIdentities: 3

Observe that plookupIdentities\texttt{plookupIdentities} counts the number of inclusion arguments used in the PIL program (one in our example).

Avoiding redundancy

Further modifications can be added to avoid redundancy in the PIL. This can be achieved by introducing another constant polynomial BYTE_PREVCARRY\mathtt{BYTE\_PREVCARRY}.

In this case, the constant polynomials' table formed by the polynomials; BYTE_A\mathtt{BYTE\_A}, BYTE_B\mathtt{BYTE\_B}, BYTE_PREVCARRY\mathtt{BYTE\_PREVCARRY}, BYTE_CARRY\mathtt{BYTE\_CARRY} and BYTE_ADD\mathtt{BYTE\_ADD}; should be generated by iterating among all the possible combinations of the tuple (BYTE_A, BYTE_B, BYTE_PREVCARRY)\big(\mathtt{BYTE\_A},\ \mathtt{BYTE\_B},\ \mathtt{BYTE\_PREVCARRY} \big) and computing BYTE_CARRY\mathtt{BYTE\_CARRY} and BYTE_ADD\mathtt{BYTE\_ADD} accordingly in each of the combinations.

The table only becomes twice bigger because BYTE_PREVCARRY\mathtt{BYTE\_PREVCARRY} is binary.

A summary of how the table looks like with the new changes is already in the table below:

rowBYTE_ABYTE_BBYTE_PREVCARRYBYTE_CARRYBYTE_ADD 10x000x000x000x000x00 20x000x010x000x000x01 30x000x020x000x000x022560x000xff0x000x000xff2570x010x000x000x000x012580x010x010x000x000x02655350xff0xfe0x000x010xfd655360xff0xff0x000x010xfe655370x000x000x010x000x00655380x000x010x010x000x02655390x000x020x010x000x03657920x000xff0x010x010x00657930x010x000x010x000x02657940x010x010x010x000x031310710xff0xfe0x010x010xfe1310720xff0xff0x010x010xff\begin{aligned} \begin{array}{|c|c|c|c|c|c|}\hline \texttt{row} & \mathtt{ BYTE\_A} & \mathtt{BYTE\_B} & \mathtt{BYTE\_PREVCARRY} & \mathtt{BYTE\_CARRY} & \mathtt{BYTE\_ADD} \\ \hline \text{ 1} & \texttt{0x00} & \texttt{0x00} & \texttt{0x00} & \texttt{0x00} & \texttt{0x00} \\ \text{ 2} & \texttt{0x00} & \texttt{0x01} & \texttt{0x00} & \texttt{0x00} & \texttt{0x01} \\ \text{ 3} & \texttt{0x00} & \texttt{0x02} & \texttt{0x00} & \texttt{0x00} & \texttt{0x02} \\ \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\ \text{256} & \texttt{0x00} & \texttt{0xff} & \texttt{0x00} & \texttt{0x00} & \texttt{0xff} \\ \text{257} & \texttt{0x01} & \texttt{0x00} & \texttt{0x00} & \texttt{0x00} & \texttt{0x01} \\ \text{258} & \texttt{0x01} & \texttt{0x01} & \texttt{0x00} & \texttt{0x00} & \texttt{0x02} \\ \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\ \text{65535} & \texttt{0xff} & \texttt{0xfe} & \texttt{0x00} & \texttt{0x01} & \texttt{0xfd} \\ \text{65536} & \texttt{0xff} & \texttt{0xff} & \texttt{0x00} & \texttt{0x01} & \texttt{0xfe} \\ \text{65537} & \texttt{0x00} & \texttt{0x00} & \texttt{0x01} & \texttt{0x00} & \texttt{0x00} \\ \text{65538} & \texttt{0x00} & \texttt{0x01} & \texttt{0x01} & \texttt{0x00} & \texttt{0x02} \\ \text{65539} & \texttt{0x00} & \texttt{0x02} & \texttt{0x01} & \texttt{0x00} & \texttt{0x03} \\ \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\ \text{65792} & \texttt{0x00} & \texttt{0xff} & \texttt{0x01} & \texttt{0x01} & \texttt{0x00} \\ \text{65793} & \texttt{0x01} & \texttt{0x00} & \texttt{0x01} & \texttt{0x00} & \texttt{0x02} \\ \text{65794} & \texttt{0x01} & \texttt{0x01} & \texttt{0x01} & \texttt{0x00} & \texttt{0x03} \\ \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\ \text{131071} & \texttt{0xff} & \texttt{0xfe} & \texttt{0x01} & \texttt{0x01} & \texttt{0xfe} \\ \text{131072} & \texttt{0xff} & \texttt{0xff} & \texttt{0x01} & \texttt{0x01} & \texttt{0xff} \\ \hline \end{array} \end{aligned}

In addition, recall that we only have to take into account prevCarry\texttt{prevCarry} whenever RESET\texttt{RESET} is 00.

PIL is flexible enough to consider this kind of situation involving Plookups. To introduce this requirement, the inclusion check can be modified as follows:

{a, b, (1 - RESET)*prevCarry, carry, add} in {BYTE_A, BYTE_B, BYTE_PREVCARRY, BYTE_CARRY, BYTE_ADD };

With this modification, the PIL program becomes:

include "config.pil"; 

namespace TwoByteAdd(%N);

pol constant BYTE_A, BYTE_B, BYTE_PREVCARRY, BYTE_CARRY , BYTE_ADD; 
pol constant RESET;
pol commit a, b;
pol commit carry, prevCarry, add;

prevCarry' = carry;

{a, b, (1 - RESET)*prevCarry, carry, add} in {BYTE_A, BYTE_B, BYTE_PREVCARRY, BYTE_CARRY, BYTE_ADD};
Edit on GitHub

Last updated on