Polynomial Commitments

In the previous lecture, we have seen some polynomial commitment schemes based on pairings and discrete-log.

In this lecture, we will look at polynomial commitment schemes based on error-correcting codes. They are quite an awesome tool because:

  • they are plausibly post-quantum secure (recall that d-log is not)
  • no group exponentiations; instead, prover only uses hashes, additions and multiplications
  • small global parameters

Nevertheless, there are some drawbacks too:

  • large proof size
  • not homomorphic & hard to aggregate

Background: Error-correcting Code

An error-correcting code encodes a message of length into a codeword of length , where . The minimum distance (Hamming) between any two codewords is shown as . These parameters are important, and we may refer to an error-correcting code as:

code

Example: Repetitions Code

Imagine messages of 2 bits and codewords of 6 bits, where the encoding is to repeat each bit 3 times.

enc(00) = 000000
enc(01) = 000111
enc(10) = 111000
enc(11) = 111111

Note that the minimum distance between any two codewords is 3. So our parameters are . This code can correct 1 error during the transmission, for example:

dec(010111) = 01
// 010 should be 000

As shown above, encoding is usually shown as enc and decoding is usually shown as dec. In our poly-commit schemes, we won't actually be using the decoding function at all, so we don't have to care about efficient decoders!

Rate & Relative Distance

Given the code, we define:

  • rate as
  • relative distance as

We want both of these to be as high as possible, but generally there is a trade-off between them.

Linear Code

For linear codes, the condition is that "any linear combination of codewords is also a codeword". The results of this condition are:

  • the encoding can always be represented as a vector-matrix multiplication between the message and the generator matrix
  • minimum distance is the same as the codeword with the minimum number of non-zeros (weight)

Reed-Solomon Code

Reed-Solomon is a widely used error-correcting code.

  • the message is viewed as a unique degree univariate polynomial
  • the codeword is the evaluation of this polynomial at publicly known points
    • for example, for -th root of unity
  • distance is which is very good
    • this is because a degree polynomial has at most roots
    • since the codeword is evaluations, we subtract the number of roots from this to get the minimum number of non-zeros
  • encoding time is using the FFT (Fast-Fourier Transform)

For , the rate is and relative distance is which turns out to be the best you can get!

Polynomial as a 2D Matrix

To begin constructing our poly-commit scheme, we will first take a different approach on representing our polynomial. Remember that there was a "coefficient representation" where we simply stored the list of coefficients as a vector. Now, we will use a matrix to do that.

Suppose you have a polynomial of degree where is a perfect square:

The coefficients of this polynomial can be represented by the following matrix:

Evaluation of this polynomial at some point can then be shown as some matrix-vector multiplication:

With this, we will be able to reduce a polynomial commitment of proof size to an argument for vector-matrix product into as shown below:

The prover could send this resulting vector to the verifier, and the verifier could continue with the next multiplication (shown above) which is a column vector made of which the user knows. This way, a size commitment is used to commit to a polynomial of degree .

The verifier also knows the vector on the left side, as it is also made of . The problem here is to somehow convince the verifier that the prover has used the correct coefficients in the 2D matrix. For this, the prover does the following:

  1. Transform the matrix into a matrix, where each row of length is encoded into a codeword of length using a linear code.
  2. Then, the resulting matrix is committed using a Merkle Tree, where each column is a leaf.
  3. The public parameter is just made of the decided Hash function to be used in Merkle Tree, there is no trusted setup required!

So in summary, rows → codewords, and then columns → Merkle Tree leaves. The Merkle Root of this tree becomes the commitment.

With that said, the entire algorithm can be split into two steps:

  1. Proximity Test/Check: Test if the committed matrix indeed consists of codewords, encoded from the original rows.
    1. The verifier can learn the number of columns by looking at the path in Merkle Tree
    2. but it can't know if the rows are indeed codewords that belong to the original matrix rows
  2. Consistency Test/Check: Test if the result of the vector-matrix multiplication is indeed what is claimed to be by the prover.

We will go into detail of each step.

Step 1: Proximity Test

For the proximity test, the Verifier sends a random vector (size ). Then, the prover multiplies the vector with the matrix (size ) to obtain another vector of size . Afterwards, the verifier asks to reveal several columns of this matrix, and the prover reveals them.

With that, the verifier checks the following:

  • The resulting vector is a codeword, which should be true because any linear combination of codewords is a codeword.
  • Columns are as committed in the Merkle Tree.
  • Inner product between and each column is consistent. This is done simply by looking at the corresponding elements in the size vector.

If all these are correct, then the proximity test is passed with overwhelming probability.

Soundness (Intuition)

So why is this secure? Let us tackle each point one by one:

  • If an adversarial prover tries to use a different matrix, by the linear property of codewords, the resulting vector will NOT be a codeword. The first check ensures this.
  • The second check ensures that columns are as committted.
  • By the first check, the prover has to use the correct matrix, but it can still send a different result vector (one that is still a codeword). In that case, due to the distance property, this new vector must have some elements that are different than the original vector. Reed-Solomon has distance 1/2, so the new vector is different at around half of the points! The second check ensured the columns to be correct, so the prover's fake vector will most likely fail the third check because at least half of the points are different!

Soundness (Formal)

Things are a bit more complex formally. A new parameter is introduced. For , if the committed matrix is -far from any codeword (meaning that the minimum distance of all rows to any codeword in the linear code is at least ), then:

So, if is -far from any codeword then finally:

Discovery

This test was discovered independently by the two papers:

Both of these constructions were targeted to general-purpose SNARKs!

Optimization

The prover can actually send a message instead of the size result vector, such that the encoding of is equal to the codeword that is the resulting vector! This is good because:

  • the message (size ) is smaller than the vector (size )
  • check 1 is implicitly passed, because the resulting vector is literally the encoding of
  • furthermore, a very cool property is that the message is actually equal to multiplied by the original coefficient matrix!

Step 2: Consistency Test

The algorithm for consistency test is almost the same as the optimized proximity test. The prover sends a message , which is the multiplication of (that is ) with the coefficient matrix . Then, the verifier finds the encoding of this message.

Columns are ensured to be the committed ones, because we have already made that check in the proximity test. Furthermore, using the same randomly picked columns (corresponding to elements in the codeword) the verifier will check whether the multiplication is consistent.

In short:

  • The resulting vector is a codeword, true because vector was created from the encoding of
  • Columns are as committed in the Merkle Tree, true because this was done in the previous test.
  • Inner product between and each column is consistent, which is checked using the same randomly picked columns (for efficiency).

Soundness (Intuition)

By the proximity test, the committed matrix is close to a codeword. Furthermore, there exists an efficient extractor that extracts by Merkle Tree commitment, and then decoding that to find such that with overwhelming probability.

Polynomial Commitment based on Linear Code

Let us now describe the polynomial commitment scheme that makes us of linear codes (with constant relative distance).

  • Keygen: Sample a hash function
    • Hash functions are public, so this is a transparent setup!
    • complexity, yummy.
  • Commit: Encode the coefficient matrix of row-wise with a linear code, and commit to it using Merkle Tree
    • Encoding takes field operations using Reed-Solomon code, or using linear code
    • Merkle Tree commitment takes hashes, but commitment size is
  • Eval & Verify: You are given , encode and do the proximity & consistency tests, then find
    • Eval takes field operations
    • Can be made non-interactive using Fiat-Shamir

This method has proof size and verifier time

In Practice

[Bootle-Chiesa-Groth'20] dives into tensor query IOP, and they generalize the method to multiple dimensions with proof size for some constant .

Brakedown [Golovnev-Lee-Setty-Thaler-Wahby'21] using tensor query IOP, made some evaluations using linear code for some polynomial of degree

  • Commit time: 36s
  • Eval time: 3.2s
  • Proof size: 49MB (ouch…)
  • Verifier time: 0.7s

They have also show that you can prove knowledge soundness without an efficient decoder. This is huge because normally an extractor would use the decoder to do the extraction, which was a problem if decoder was not efficient.

[Bootle-Chiesa-Liu'21] reduces the proof size to using proof composition of tensor IOP and PCP of proximity [Mie'09]

Orion [Xie-Zhang-Song'22] achieves a proof size of using proof composition of the code-switching technique [RonZewi-Rothblum'20]

Looking at SNARKs with linear prover time in order:

PaperProof SizeMethodology
[Bootle-Cerulli-Ghadafi-Groth-Hajiabadi-Jakobsen'17]Ideal Linear Model
[Bootle-Chiesa-Groth'20]Tensor IOP
[Bootle-Chiesa-Liu'21]Tensor IOP + PCP
[Golovnev-Lee-Setty-Thaler-Wahby'21]Polynomial Commitment
[Xie-Zhang-Song'22]Code-switching Proof Composition

Background: Linear-time Encodable Code

A linear code was introduced for binary messages back then in [Spielman'96], and then generalized to finite field elements by [Druk-Ishai'14]. The construction uses what is called "expander graphs".

Expander Graphs

Below is an example Bipartite graph, a graph that can be split in two parts (A, B here) such that no vertex within that sub-graph are connected. Furthermore, in this example each vertex on the left side is connected to 2 nodes in the right side, and each vertex on right is connected to 3 nodes on left.

flowchart LR
	subgraph left
		a1(( ))
		a2(( ))
		a3(( ))
		a4(( ))
	end

	subgraph right
		b1(( ))
		b2(( ))
		b3(( ))
		b4(( ))
	  b5(( ))
		b6(( ))
	end

  a1 --- b1
  a3 --- b1
  a2 --- b2
  a3 --- b2
  a3 --- b3
  a4 --- b3
  a1 --- b4
  a2 --- b4
  a2 --- b5
  a3 --- b5
  a1 --- b6
  a4 --- b6

You can think of larger bipartite expander graphs. The trick of using an expander as a linear code is: let each vertex in the left correspond to symbols of the message , and let the right side correspond to symbols of the codeword!

Each symbol in the codeword is simply the sum of the connected symbols in message. This relationship can be easily captured as the multiplication of the message with the adjacency matrix of the expander graph.

That sounded really good, but sadly it is not sufficient; it fails on the "constant relative distance" requirement. Take a message with a single non-zero for example, the codeword must look the same for all such messages. Obviously, this is not the case here, because codewords symbols change depending on which symbol of the message is non-zero.

Lossless Expander Graph

Let be the number of vertices in the left graph, and set to be for some constant . In the example above, is larger than 1 because right has more nodes than left, but in practice we actually have . Let be the number of edges per node in the left side, e.g. for the example above.

What is the maximum possible expansion of a subset in the left side? Well, it is simply . We let denote the set of neighbors for a set, i.e. . However, this is not true for all subsets, you must have enough nodes on the right side for that, which can be defined by the constraint:

Turns out this is too good to be true! So in practice, we use a more relaxed definition. We let the maximum expansion with the constraint:

for some . Note that the previous "too good to be true" definition uses and . The smaller you have the less-relaxed this thing is.

Recursive Encoding

Lossless expander itself is not enough, we will do the encoding recursively. In this case, we will start with a message of length . We will obtain a codeword of size .

For now, assume that we already have an encoder with rate 1/4.

flowchart
	m[message len=k]
	e[code len=k/2]

	subgraph codeword len=4k
	mc[message len=k]
	c1[code1 len=2k]
	c2[code2 len=k]
	end

	m --copy---> mc
	m --lossless expand--> e
	e --encode--> c1
	c1 --lossless expand--> c2

As shown above, the codeword has 3 parts:

  • The message itself, length . This is a common approach in error correcting codes, and such codes with message being the start of the codeword are called "systematic code".
  • Then, the message will be encoded using a lossless expander with . The resulting code has size . This result is then encoded using an existing (assumed) encoder of rate . The resulting codeword has length . Denote this as , this guy will be the middle part of our actual codeword.
  • Finally, use a lossless expander with to encode and obtain of length . This is the final part of the codeword.

Now, about that "assumed" encoder, how do we implement it? Well, notice that the input to that encoder is of length . We can actually use this entire algorithm as that encoder, this time the message being of length instead of . This is why the name "recursive" is used. Once you get to a certain constant size, just use any code with good distance (e.g. Reed-Solomon) to do the encoding job.

Also note that we use two lossless expanders with , but they are not the same! This is because their input sizes () are different.

Sampling the Lossless Expander

As we can see, we need lossless expanders for these recursions, so we must be able to sample them efficiently. Are there any methods to do so?

[Capalbo-Reingold-Vadhan-Widgerson'02] shows an explicit construction of lossless expander, but they require a large hidden constant that is hard to find in the first place. Alternatively, one can argue that sampling a random graph has a probability of failing to find a lossless expander.

Improvements

Brakedown [Golovnev-Lee-Setty-Thaler-Wahby'21] assigns random weights to the edges in the graph, and they show that the resulting random summations leads to better distance metrics.

Orion [Xie-Zhang-Song'22] shows a way to do the lossless expander testing with a negligible probability (instead of ) which is awesome, because you can then do rejection sampling to efficiently find a good lossless expander! They do this by looking at the maximum density in a graph.

Summary

Polynomial commitment (and SNARK) based on linear code has the following properties:

  • 🟢 Transparent setup,
  • 🟢 Commit and Prover times are field operations
  • 🟢 Plausibly post-quantum secure
  • 🟢 Field agnostic
  • 🔴 Proof size , order of MBs in practice

That is the end of this lecture!