# RFC: Plookup in kimchi

In 2020, plookup showed how to create lookup proofs. Proofs that some witness values are part of a lookup table. Two years later, an independent team published plonkup showing how to integrate Plookup into Plonk.

This document specifies how we integrate plookup in kimchi. It assumes that the reader understands the basics behind plookup.

## Overview

We integrate plookup in kimchi with the following differences:

- we snake-ify the sorted table instead of wrapping it around (see later)
- we allow fixed-ahead-of-time linear combinations of columns of the queries we make
- we only use a single table (XOR) at the moment of this writing
- we allow several lookups (or queries) to be performed within the same row
- zero-knowledgeness is added in a specific way (see later)

The following document explains the protocol in more detail

### Recap on the grand product argument of plookup

As per the Plookup paper, the prover will have to compute three vectors:

- $f$, the (secret)
**query vector**, containing the witness values that the prover wants to prove are part of the lookup table. - $t$, the (public)
**lookup table**. - $s$, the (secret) concatenation of $f$ and $t$, sorted by $t$ (where elements are listed in the order they are listed in $t$).

Essentially, plookup proves that all the elements in $f$ are indeed in the lookup table $t$ if and only if the following multisets are equal:

- ${(1+β)f,diff(t)}$
- $diff(sorted(f,t))$

where $diff$ is a new set derived by applying a “randomized difference” between every successive pairs of a vector. For example:

- $f={5,4,1,5}$
- $t={1,4,5}$
- ${(1+β)f,diff(t)}={(1+β)5,(1+β)4,(1+β)1,(1+β)5,1+β4,4+β5}$
- $diff(sorted(f,t))={1+β1,1+β4,4+β4,4+β5,5+β5,5+β5}$

Note: This assumes that the lookup table is a single column. You will see in the next section how to address lookup tables with more than one column.

The equality between the multisets can be proved with the permutation argument of plonk, which would look like enforcing constraints on the following accumulator:

- init: $acc_{0}=1$
- final: $acc_{n}=1$
- for every $0<i≤n$: $acc_{i}=acc_{i−1}⋅(γ+s_{i−1}+βs_{i})(γ+(1+β)f_{i−1})(γ+t_{i−1}+βt_{i}) $

Note that the plookup paper uses a slightly different equation to make the proof work. I believe the proof would work with the above equation, but for simplicity let’s just use the equation published in plookup:

$acc_{i}=acc_{i−1}⋅(γ(1+β)+s_{i−1}+βs_{i})(1+β)(γ+f_{i−1})(γ(1+β)+t_{i−1}+βt_{i}) $

Note: in plookup $s$ is too large, and so needs to be split into multiple vectors to enforce the constraint at every $i∈[[0;n]]$. We ignore this for now.

### Lookup tables

Kimchi uses a single **lookup table** at the moment of this writing; the XOR table. The XOR table for values of 1 bit is the following:

l | r | o |
---|---|---|

1 | 0 | 1 |

0 | 1 | 1 |

1 | 1 | 0 |

0 | 0 | 0 |

Whereas kimchi uses the XOR table for values of 4 bits, which has $2_{8}$ entries.

Note: the (0, 0, 0) **entry** is at the very end on purpose (as it will be used as dummy entry for rows of the witness that don’t care about lookups).

### Querying the table

The plookup paper handles a vector of lookups $f$ which we do not have. So the first step is to create such a table from the witness columns (or registers). To do this, we define the following objects:

- a
**query**tells us what registers, in what order, and scaled by how much, are part of a query - a
**query selector**tells us which rows are using the query. It is pretty much the same as a gate selector.

Let’s go over the first item in this section.

For example, the following **query** tells us that we want to check if $r_{0}⊕r_{2}=2r_{1}$

l | r | o |
---|---|---|

1, r0 | 1, r2 | 2, r1 |

The grand product argument for the lookup consraint will look like this at this point:

$acc_{i}=acc_{i−1}⋅(γ(1+β)+s_{i−1}+βs_{i})(1+β)(γ+w_{0}(g_{i})+j⋅w_{2}(g_{i})+j_{2}⋅2⋅w_{1}(g_{i}))(γ(1+β)+t_{i−1}+βt_{i}) $

Not all rows need to perform queries into a lookup table. We will use a query selector in the next section to make the constraints work with this in mind.

### Query selector

The associated **query selector** tells us on which rows the query into the XOR lookup table occurs.

row | query selector |
---|---|

0 | 1 |

1 | 0 |

Both the (XOR) lookup table and the query are built-ins in kimchi. The query selector is derived from the circuit at setup time. Currently only the ChaCha gates make use of the lookups.

The grand product argument for the lookup constraint looks like this now:

$acc_{i}=acc_{i−1}⋅(γ(1+β)+s_{i−1}+βs_{i})(1+β)⋅query⋅(γ(1+β)+t_{i−1}+βt_{i}) $

where $query$ is constructed so that a dummy query ($0⊕0=0$) is used on rows that don’t have a query.

$query= selector⋅(γ+w_{0}(g_{i})+j⋅w_{2}(g_{i})+j_{2}⋅2⋅w_{1}(g_{i}))+(1−selector)⋅(γ+0+j⋅0+j_{2}⋅0) $

### Queries, not query

Since we allow multiple queries per row, we define multiple **queries**, where each query is associated with a **lookup selector**.

At the moment of this writing, the `ChaCha`

gates all perform $4$ queries in a row. Thus, $4$ is trivially the largest number of queries that happen in a row.

**Important**: to make constraints work, this means that each row must make 4 queries. (Potentially some or all of them are dummy queries.)

For example, the `ChaCha0`

, `ChaCha1`

, and `ChaCha2`

gates will apply the following 4 XOR queries on the current and following rows:

l | r | o | - | l | r | o | - | l | r | o | - | l | r | o |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|

1, r3 | 1, r7 | 1, r11 | - | 1, r4 | 1, r8 | 1, r12 | - | 1, r5 | 1, r9 | 1, r13 | - | 1, r6 | 1, r10 | 1, r14 |

which you can understand as checking for the current and following row that

- $r_{3}⊕r7=r_{11}$
- $r_{4}⊕r8=r_{12}$
- $r_{5}⊕r9=r_{13}$
- $r_{6}⊕r10=r_{14}$

The `ChaChaFinal`

also performs $4$ (somewhat similar) queries in the XOR lookup table. In total this is 8 different queries that could be associated to 8 selector polynomials.

### Grouping queries by queries pattern

Associating each query with a selector polynomial is not necessarily efficient. To summarize:

- the
`ChaCha0`

,`ChaCha1`

, and`ChaCha2`

gates that make $4$ queries into the XOR table - the
`ChaChaFinal`

gate makes $4$ different queries into the XOR table

Using the previous section’s method, we’d have to use $8$ different lookup selector polynomials for each of the different $8$ queries. Since there’s only $2$ use-cases, we can simply group them by **queries patterns** to reduce the number of lookup selector polynomials to $2$.

The grand product argument for the lookup constraint looks like this now:

$acc_{i}=acc_{i−1}⋅(γ(1+β)+s_{i−1}+βs_{i})(1+β)_{4}⋅query⋅(γ(1+β)+t_{i−1}+βt_{i}) $

where $query$ is constructed as:

$query= selector_{1}⋅pattern_{1}+selector_{2}⋅pattern_{2}+(1−selector_{1}−selector_{2})⋅(γ+0+j⋅0+j_{2}⋅0)_{4} $

where, for example the first pattern for the `ChaCha0`

, `ChaCha1`

, and `ChaCha2`

gates looks like this:

$pattern_{1}= (γ+w_{3}(g_{i})+j⋅w_{7}(g_{i})+j_{2}⋅w_{11}(g_{i}))⋅(γ+w_{4}(g_{i})+j⋅w_{8}(g_{i})+j_{2}⋅w_{12}(g_{i}))⋅(γ+w_{5}(g_{i})+j⋅w_{9}(g_{i})+j_{2}⋅w_{13}(g_{i}))⋅(γ+w_{6}(g_{i})+j⋅w_{10}(g_{i})+j_{2}⋅w_{14}(g_{i}))⋅ $

Note:

- there’s now 4 dummy queries, and they only appear when none of the lookup selectors are active
- if a pattern uses less than 4 queries, they’d have to pad their queries with dummy queries as well

## Back to the grand product argument

There are two things that we haven’t touched on:

- The vector $t$ representing the
**combined lookup table**(after its columns have been combined with a joint combiner $j$). The**non-combined loookup table**is fixed at setup time and derived based on the lookup tables used in the circuit (for now only one, the XOR lookup table, can be used in the circuit). - The vector $s$ representing the sorted multiset of both the queries and the lookup table. This is created by the prover and sent as commitment to the verifier.

The first vector $t$ is quite straightforward to think about:

- if it is smaller than the domain (of size $n$), then we can repeat the last entry enough times to make the table of size $n$.
- if it is larger than the domain, then we can either increase the domain or split the vector in two (or more) vectors. This is most likely what we will have to do to support multiple lookup tables later.

What about the second vector?

## The sorted vector $s$

The second vector $s$ is of size

$n⋅∣queries∣+∣lookup_table∣$

That is, it contains the $n$ elements of each **query vectors** (the actual values being looked up, after being combined with the joint combinator, that’s $4$ per row), as well as the elements of our lookup table (after being combined as well).

Because the vector $s$ is larger than the domain size $n$, it is split into several vectors of size $n$. Specifically, in the plonkup paper, the two halves of $s$ (which are then interpolated as $h_{1}$ and $h_{2}$).

$acc_{i}=acc_{i−1}⋅(γ(1+β)+s_{i−1}+βs_{i})(γ(1+β)+s_{n+i−1}+βs_{n+i})(1+β)_{4}⋅query⋅(γ(1+β)+t_{i−1}+βt_{i}) $

Since you must compute the difference of every contiguous pairs, the last element of the first half is the replicated as the first element of the second half ($s_{n−1}=s_{n}$), and a separate constraint enforces that continuity on the interpolated polynomials $h_{1}$ and $h_{2}$:

$L_{n−1}(h_{1}(x)−h_{2}(g⋅x))=0$

which is equivalent with checking that

$h_{1}(g_{n−1})=h_{2}(1)$

## The sorted vector $s$ in kimchi

Since this vector is known only by the prover, and is evaluated as part of the protocol, zero-knowledge must be added to the polynomial. To do this in kimchi, we use the same technique as with the other prover polynomials: we randomize the last evaluations (or rows, on the domain) of the polynomial.

This means two things for the lookup grand product argument:

- we cannot use the wrap around trick to make sure that the list is split in two correctly (enforced by $L_{n−1}(h_{1}(x)−h_{2}(g⋅x))=0$ which is equivalent to $h_{1}(g_{n−1})=h_{2}(1)$ in the plookup paper)
- we have even less space to store an entire query vector. Which is actually super correct, as the witness also has some zero-knowledge rows at the end that should not be part of the queries anyway.

The first problem can be solved in two ways:

**Zig-zag technique**. By reorganizing $s$ to alternate its values between the columns. For example, $h_{1}=(s_{0},s_{2},s_{4},⋯)$ and $h_{2}=(s_{1},s_{3},s_{5},⋯)$ so that you can simply write the denominator of the grand product argument as $(γ(1+β)+h_{1}(x)+βh_{2}(x))(γ(1+β)+h_{2}(x)+βh_{1}(x⋅g))$ this is what the plonkup paper does.**Snake technique**. by reorganizing $s$ as a snake. This is what is done in kimchi currently.

The snake technique rearranges $s$ into the following shape:

```
_ _
| | | | |
| | | | |
|_| |_| |
```

so that the denominator becomes the following equation:

$(γ(1+β)+h_{1}(x)+βh_{1}(x⋅g))(γ(1+β)+h_{2}(x⋅g)+βh_{2}(x))$

and the snake doing a U-turn is constrained via something like

$L_{n−1}⋅(h_{1}(x)−h_{2}(x))=0$

If there’s an $h_{3}$ (because the table is very large, for example), then you’d have something like:

$(γ(1+β)+h_{1}(x)+βh_{1}(x⋅g))(γ(1+β)+h_{2}(x⋅g)+βh_{2}(x))(γ(1+β)+h_{3}(x)+βh_{3}(x⋅g))$

with the added U-turn constraint:

$L_{0}⋅(h_{2}(x)−h_{3}(x))=0$

## Unsorted $t$ in $s$

Note that at setup time, $t$ cannot be sorted as it is not combined yet. Since $s$ needs to be sorted by $t$ (in other words, not sorted, but sorted following the elements of $t$), there are two solutions:

- both the prover and the verifier can sort the combined $t$, so that $s$ can be sorted via the typical sorting algorithms
- the prover can sort $s$ by $t$, so that the verifier doesn’t have to do any sorting and can just rely on the commitment of the columns of $t$ (which the prover can evaluate in the protocol).

We do the second one, but there is an edge-case: the combined $t$ entries can repeat. For some $i,l$ such that $i=l$, we might have

$t_{0}[i]+jt_{1}[i]+j_{2}t_{2}[i]=t_{0}[l]+jt_{1}[l]+j_{2}t_{2}[l]$

For example, if $f={1,2,2,3}$ and $t={2,1,2,3}$, then $sorted(f,t)={2,2,2,1,1,2,3,3}$ would be one way of sorting things out. But $sorted(f,t)={2,2,2,2,1,1,3,3}$ would be incorrect.

## Recap

So to recap, to create the sorted polynomials $h_{i}$, the prover:

- creates a large query vector which contains the concatenation of the $4$ per-row (combined with the joint combinator) queries (that might contain dummy queries) for all rows
- creates the (combined with the joint combinator) table vector
- sorts all of that into a big vector $s$
- divides that vector $s$ into as many $h_{i}$ vectors as a necessary following the snake method
- interpolate these $h_{i}$ vectors into $h_{i}$ polynomials
- commit to them, and evaluate them as part of the protocol.