We talked about FRI, STARK.
Fast Reed-Solomon Interactive Oracle Proof of Proximity (FRI) is a commitment scheme that is based on hash functions and proximity tests. You show that a given polynomial is "close" to a "low-degree" polynomial.
FRI can be simply shown as:
- Receive random
$\beta$ - Apply the FRI Operator
- Commit
- Go-to step 1 until you reach a constant polynomial (i.e. degree
$<1$ ) - Send all commitments & the final constant polynomial
The goal of FRI Operator is to go from "prove a function (over domain of size
First, lets recall that a SNARK is made up of two components:
-
Interactive Oracle Proof (IOP): A protocol that allows a prover to convince a verifier that a statement is true. The prover sends a series of messages to the verifier, and the verifier responds with challenges. The prover must respond to these challenges in a way that convinces the verifier that the statement is true.
-
Commitment Scheme: A commitment is a cryptographic primitive that allows a prover to commit to a value without revealing it. The prover can later reveal the value and prove that it was the same value that was committed to.
Most often, we use IOPs and commitment schemes built for polynomials, to which we refer to as polynomial IOPs (poly-IOP) and polynomial commitment schemes (PCS). PCS is more often the performance bounding part, and can significantly affect the proving time and proof size. A PCS should be binding and hiding; and it has three algorithms (1) setup, (2) commitment, (3) evaluation.
Last week we have covered KZG commitments:
- used pairings over Elliptic Curves
- requires trusted setup
- computes commitment with multi-scalar multiplication (MSM) where the result is just points on the curve.
- evaluation proofs are short: just a commitment to the quotient polynomial & independent of the size of polynomial
MSM is the most expensive part here.
We had covered Reed-Solomon Codes before, but here is a quick recap: a message
The distance between two codewords is the number of points where they differ. The Reed-Solomon code has distance
In FRI, we will first choose a domain:
Here,
Using this domain, we will compute
Using these
Notice that the verifier can query an evaluation from
Now how do we know that the committed values are the evaluations of a polynomial, and not some other garbage? We will use a proximity test for this. However, proximity tests over a large domain can be expensive. So, we will use a low-degree test instead.
Again, consider the polynomial
If
Now, we compute the domain for this polynomial
Finally, we will compute the evaluations of
Verifier gives us
As we can see, this is a recursive process and we continue this until at
This process of splitting the polynomial in two, asking for a random coefficient, and computing a half-degree polynomial from that is called random-folding.
sequenceDiagram
actor P as Prover
actor V as Verifier
note over P: P_0(x)
P ->> V: root_0
V ->> P: beta_0
note over P: P_1(y=x^2)
P ->> V: root_1
V ->> P: beta_1
note over P, V: and so on...
Instead of asking the verifier for randomness, we can use Fiat-Shamir transform to generate the randomness without any interactions with the verifier. To do this, a transcript of all prover steps is kept within the protocol, and a hash of this transcript is used to generate the randomness, under the random-oracle model.
You can see this within the code as well, see: https://github.com/lambdaclass/lambdaworks/blob/main/provers/stark/src/transcript.rs.
The number of coefficients in our polynomial is
If you have a larger blow-up factor, you can make less queries for the same amount of security. This means that the proof size is smaller. However, the computations will be more expensive the the memory usage will be higher, due to storing Merkle Trees.
In the query phase, the verifier will ask for evaluations of the polynomial at some random points. The prover will provide the evaluations and the Merkle Paths to the verifier. The verifier can then compute the Merkle Roots and verify that the evaluations are correct.
Recall that
So, we can imagine how an evaluation "folds" with a pictoral example such as below:
graph TD
subgraph evaluations of P_2
direction TB
20[" "]; 21[" "];
end
subgraph evaluations of P_1
direction TB
10[" "]; 11[" "]; 12[" "]; 13[" "];
end
subgraph evaluations of P_0
direction TB
00[" "]; 01[" "]; 02[" "]; 03[" "]; 04[" "]; 05[" "]; 06[" "]; 07[" "];
end
03["P0(x)"] & 07["P0(-x)"] --> 10
10["P1(x^2)"] & 12["P1(-x^2)"] --> 21
21["P1(x^4)"]
As shown above, we have 2 evaluations for each step. In the end, the resulting evaluation is that of the constant polynomial. In
For each evaluation, the prover provides an authentication path (the path of the respective Merkle Tree for that evaluation) which has
Now that we have made sure of the degree of polynomial, we must also make sure that it evaluates to some value at a point that verifier picked, i.e.
Now, all we have to do is apply FRI to this quotient polynomial instead, and we can be sure that the polynomial is close to a low-degree polynomial & it evaluates to
If you have a series of polynomials
In STARKs, instead of using Square Span programs that was based on
- Execution Trace
- Set of Polynomial Constraints
An execution trace is to be thought of like a table with some columns, where every column denotes a register (as in CPU register) and a column denotes the "clock". In this table, to ensure correctness you have to:
- Check that initial values are correct
- Check that jumps between each step (i.e. transitions) are correct
- Check that final values are correct
- Variables are consistent / well-formed, i.e. some register is boolean or such
There are several types of constraints:
- Boundary constraints
- Consistency constraints
- Transition constraints
These constraints will have steps where they apply, along with the registers that they apply to.
-
$r_1(t=0)=5$ means that register 1 at time 0 should be 5 -
$r_3(t)(1 - r_3(t)) = 0$ means that register 3 is a boolean (bit) for all steps
Transition steps are given as a multivariate polynomial
Consider the table with columns
$x_0 = 2$ $x_{i+1} = x_i^2$
You are given the execution trace (i.e. witness):
0 | 2 |
1 | 4 |
2 | 16 |
Our set of polynomial constraints are:
- Initial:
$x(t = 0) = 2$ - Transitions:
$x_{i+1} = x_i^2$ captured by$P(x, y) = y - x^2$ - Final:
$x(t=n) = 2^{2^n}$
We will now describe the non-interactive protocol (i.e. using Fiat-Shamir transform):
-
Start with a transcript containing all the public inputs
-
Suppose the number of steps (
$n+1$ in this case) is a power of two; even if its not you can add dummy rows to ensure this. Choose an interpolation domain$D_i = {g^0, g^1, \ldots, g^n}$ where$g$ is a primitive$n$ -th root of unity that generates the group of order power of two. Then, view$x_i$ as the evaluations of$t(x)$ over$D_i$ , as in$t(g^i) = x_i$ . Interpolate this polynomial (using FFT) to find$t(x)$ which represents the trace. -
Commit to the trace polynomial
$t(x)$ . Choose a low-degree extension domain$D_0$ (such that$|D_0| > |D_i|$ and$|D_0| = 2^b|D_i|$ ).
Here,
$D_0 = {h\omega, h\omega^1, h\omega^2, \ldots}$ where$h$ is offset and$\omega$ is a primitive root of unity. Note that$h$ is not in this set by itself.
-
Append the Merkle root of
$t$ to the transcript. Once the root is appended, we are committed to this transcript. Now, we are ready to ask the Verifier for some random challenge. -
Compose the polynomial
$t(x)$ with the constraints. Notice that by multiplying$x$ with$g$ in the polynomial, you can reach to later steps, i.e.$t(gx)$ goes to the evaluation of$x$ at one step forward.
- Sample random values (i.e. hash your transcript to obtain randomness) to create a linear combination of polynomials (i.e. Batch FRI), obtaining the Composition Polynomial.
Here,
$Z(x)$ is the vanishing polynomial over domain$D_i$ except the final element.
$$ Z(x) = \prod_{i=0}^{n-1} (x - g^i) = \prod_{i=0}^{n} (x - g^i)(x - g^n)^{-1} = \frac{x^{n+1} - 1}{x - g^n} $$ The last reduction to obtain
$x^{n+1} - 1$ from the product itself is due to the fact that$g^n$ is a primitive root of unity.
-
Commit to
$CP(x)$ using$D_0$ , where you do a low-degree extension to obtain a Merkle root. -
Append this root to transcript.
-
Sample an out-of-domain point
$z$ that is neither within$D_i$ nor$D_0$ . -
Compute
$CP(z)$ ,$t(z)$ and$t(gz)$ ; this is also known as "Mask". Here, the verifier will check that indeed$CP(z)$ can be obtained from$t(z)$ and$t(gz)$ as shown in step 6 above. Using an out-of-domain sample here makes things more secure, and makes things harder for a malicious prover.
You can ignore this step (along with step 9) if you want, but you will have to do more consistency checks otherwise.
-
Append
$CP(z)$ ,$t(z)$ and$t(gz)$ to the transcript. -
Sample random
$\delta, \epsilon, \zeta$ for the next step. -
Compute
$P_0$ which we call DEEP ALI (Algebraic Linking Identity). This is done to ensure that evaluations related to point$z$ belong to the actual polynomials$CP$ and$t$ .
- Apply FRI to
$P_0(x)$ (i.e. Commitment and Query phases of FRI).
The resulting proof has some evaluations alongs with the authentication paths (Merkle Tree path) and roots, and the out-of-domain evaluations.
You will have one polynomial for each column in the table. In our example above we have only one column for
$x$ .
The verifier is a bit more simpler than the prover, still plenty of work though.
-
Replay all the challenges. Start with the transcript of public inputs, append root of
$t(x)$ , sample$\alpha, \beta, \delta$ , append root of$CP(x)$ , sample$z$ , append$CP(z), t(z)$ and$t(gz)$ , sample$\delta, \epsilon, \zeta$ and resample all FRI randomness & queries. -
Check that
$CP(z)$ is correctly linked with$t(z), t(gz)$ . To do this, first compute$C_t(z)$ and then find the linear combination using$\alpha, \beta, \delta$ and see if it equals$CP(z)$ as given by the prover:
- Check FRI. This means checking all the authentication paths and the folding steps, as well as checking the correctness of the DEEP ALI polynomial. For the latter, simply do:
If all these checks pass, the proof is correct.
Of course, LambdaWorks has an implementation: https://github.com/lambdaclass/lambdaworks/tree/main/provers/stark.
They also have a "grinding" implementation, which is a proof-of-work that makes proving a bit more costly for more security.