In this document we specify the STARK verifier used in Starknet.

draft

Overview

In this section we give a brief overview of the Starknet STARK protocol. While the protocol used is designed to verify Cairo programs, we provide an agnostic specification. The instantiation of this protocol with Cairo should be the object of a different specification.

Before we delve into the details, let's look at the protocol from a high-level protocol diagram point of view. The Starknet STARK protocol is divided in three main phases:

  1. Construction of an interactive arithmetization. In this phase the prover commits to different parts of the execution trace it wants to prove, using random challenges in-between.
  2. Aggregation of constraints into a composition polynomial. In this phase the prover commits to a composition polynomial that, if checked by FRI, proves that the execution trace satisfies the constraints. It also produces evaluations of commitments at a random point so that the verifier can check that the composition polynomial is well-formed.
  3. Aggregation of FRI proofs and FRI protocol. The composition polynomial FRI check as well as evaluation proofs (using FRI-PCS) of all the sent evaluations are aggregtated into a single FRI check. The FRI protocol is then run to verify the aggregated FRI proof. See the Starknet FRI Verifier specification for more details.

We illustrate the flow in the following diagram:

STARK overview

In the next sections we review the different phases.

Interactive AIR Arithmetization

But first, we quickly remind the reader that the Starknet STARK protocol allows a prover to convince a verifier that an AIR (Algebraic Intermediate Representation) arithmetization is satisfied by their witness. This is generally augmented to also include a public input, usually via a public memory extension.

AIR is essentially two things:

  1. an indexed table representing the execution trace of a run, where columns can be seen as registers and the rows the values they take as one steps through a program. The table takes values when a prover tries to prove an execution.
  2. a list of fixed constraints that are agreed on.

The indexing of the table is chosen as the elements of the smallest subgroup of power 2 that can index the table.

Furthermore, the columns of a table can be grouped, which allows the prover to fill the table group by group, using challenges from the verifier in-between. This is useful in order to perform an interactive arithmetization where parts of the encoded circuit needs verifier randomness to be computed.

We give the example of two "original" columns and one "interaction" column, indexed using the multiplicative subgroup of the 16-th roots of unity:

air

The first phase of the Starknet STARK protocol is to iteratively construct the trace tables (what we previously called interactive arithmetization). The prover sends commitments to parts of the table, and receives verifier challenges in between.

Composition Polynomial

The role of the verifier is now to verify constraints of the form of polynomials on the trace column polynomials, applied on a domain (a list of all the indexes on which the constraint applies).

As with our example above, we can imagine a list of constraints Ci(x) that need to vanish on a list of associated domains described by their domain polynomials Di(x).

By definition, this can be reduced to checking that you can write each Ci as Ci(x)=Di(x)·q(x) for some quotient polynomial q(x) of degree deg(Ci)deg(Di).

While protocols based on polynomial commitments like KZG would commit to the quotient polynomial and then prove the relation Ci(x)=Di(x)·q(x) at a random point (using Schwartz-Zippel), the Starknet STARK protocol uses a different approach: it uses a FRI check to prove that the commitment to the evaluations of q(x)=Ci(x)Di(x) correctly represents a polynomial of low degree.

As such, the role of the verifier is to verify that all the quotient polynomials associated to all the constraints exist and are of low-degree.

TODO: define low-degree better

As we want to avoid having to go through many FRI checks, the verifier sends a challenge α which the prover can use to aggregate all of the constraint quotient polynomials into a composition polynomial h(x):=i=0Ci(x)Di(x)·αi.

This composition polynomial is quite big, so the prover provides a commitment to chunks or columns of the composition polynomials, interpreting h as h(x)=ihi(x)xi.

Finally, to allow the verifier to check that h has correctly been committed, Schwartz-Zippel is used with a random verifier challenge called the "oods point". Specifically, the verifier evaluates the following and check that they match:

Of course, the verifier cannot evaluate both sides without the help of the prover! The left-hand side involves evaluations of the trace polynomials at the oods point (and potentially shifted oods points), and the right-hand side involves evaluations of the composition column polynomials at the oods point as well.

As such, the prover sends the needed evaluations to the verifier so that the verifier can perform the check. (These evaluations are often referred to as the "mask" values.)

Notice that this "oods check" cannot happen in the domain used to index the trace polynomials. This is because the left-hand side involves divisions by domain polynomials Di(oods_point), which might lead to divisions by zero.

This is why the oods point is called "out-of-domain sampling". Although nothing special is done when sampling this point, but the probability that it ends up in the trace domain is very low.

TODO: explain what parts does the term "DEEP" refer to in this protocol.

Aggregation and FRI Proof

The verifier now has to:

  1. Perform a FRI check on h0(x)+xh1(x) (which will verify the original prover claim that the trace polynomials satistify the constraints).
  2. Verify all the evaluations that were sent, the prover and the verifier can use FRI-PCS for that, as described in the FRI-PCS section of the Starknet FRI Verifier specification.

TODO: the second point also should have the effect of proving that the commitments to the trace column polynomials are correct (as they will also act as FRI checks)

In order to avoid running multiple instances of the FRI protocol, the FRI Aggregation technique is used as described in the Aggregating Multiple FRI Proofs section of the Starknet FRI Verifier specification. The verifier sends a challenge called oods_alpha which is used to aggregate all of the first layer of the previously discussed FRI proofs.

Finally, the FRI protocol is run as described in the Starknet FRI Verifier specification.

Dependencies

In this section we list all of the dependencies and interfaces this standard relies on.

AIR Arithmetization Dependency

While this specification was written with Cairo in mind, it should be instantiatiable with any AIR arithmetization that can be verified using the STARK protocol.

A protocol that wants to use this specification should provide the following:

interactive arithmetization. A description of the interactive arithmetization step, which should include in what order the different tables are committed and what verifier challenges are sent in-between.

eval_composition_polynomial. A function that takes all of the commitments, all of the evaluations, and a number of Merkle tree witnesses sent by the prover and produces an evaluation of the composition polynomial at the oods point. (This evaluation will depend heavily on the number of trace columns and the constraints of the given AIR arithmetization.) The function is expected to verify any decommitment (via the Merkle tree witnesses) that it uses.

eval_oods_polynomial. A function that takes all of the commitments, all of the evaluations, and a number of Merkle tree witnesses sent by the prover and produces a list of evaluations of the first layer polynomial of the FRI check at a list of queried points. The function is expected to verify any decommitment (via the Merkle tree witnesses) that it uses.

We refer to the Merkle Tree Polynomial Commitments specification on how to verify decommitments.

Constants

We use the following constants throughout the protocol.

Protocol constants

STARKNET_PRIME = 3618502788666131213697322783095070105623107215331596699973092056135872020481. The Starknet prime (2251+17·2192+1).

FIELD_GENERATOR = 3. The generator for the main multiplicative subgroup of the Starknet field. This is also used as coset factor to produce the coset used in the first layer's evaluation.

Channel

See the Channel specification for more details.

FRI

See the Starknet FRI Verifier specification.

Specifically, we expose the following functions:

as well as the two objects FriVerificationStateConstant, FriVerificationStateVariable defined in that specification.

Configuration

struct StarkConfig {
    traces: TracesConfig,
    composition: TableCommitmentConfig,
    fri: FriConfig,
    proof_of_work: ProofOfWorkConfig,
    // Log2 of the trace domain size.
    log_trace_domain_size: felt252,
    // Number of queries to the last component, FRI.
    n_queries: felt252,
    // Log2 of the number of cosets composing the evaluation domain, where the coset size is the
    // trace length.
    log_n_cosets: felt252,
    // Number of layers that use a verifier friendly hash in each commitment.
    n_verifier_friendly_commitment_layers: felt252,
}

To validate:

Buiding Blocks

The verifier accepts the following proof as argument:

struct StarkProof {
    config: StarkConfig,
    public_input: PublicInput,
    unsent_commitment: StarkUnsentCommitment,
    witness: StarkWitness,
}

struct StarkUnsentCommitment {
    traces: TracesUnsentCommitment,
    composition: felt252,
    // n_oods_values elements. The i-th value is the evaluation of the i-th mask item polynomial at
    // the OODS point, where the mask item polynomial is the interpolation polynomial of the
    // corresponding column shifted by the corresponding row_offset.
    oods_values: Span<felt252>,
    fri: FriUnsentCommitment,
    proof_of_work: ProofOfWorkUnsentCommitment,
}

We assume that the public input is instantiated and verified by the parent protocol, and thus is out of scope of this standard.

Trace and Evaluation Domains

There are three types of domains:

  1. The trace domain, this is the domain chosen to evaluate the execution trace polynomials. It is typically the smallest subgroup of order 2nt for some nt, such that it can include all the constraints.
  2. The evaluation domain, which is chosen as a subgroup of a power of two 2ne that encompasses the trace domain (i.e. et). The "blown up factor" typically dictates how much larger the evaluation domain as a multiple. (TODO: and then typically moved to a coset). (TODO: why n_cosets then? can this be seen as a union of cosets formed from the trace domain?)

As such, the generator of the trace domain can be found as ωe=3(p1)/nt (since ωent=1), and the generator of the evaluation domain can be found as ω=3(p1)/ne.

Commitments

Commitments to all the polynomials, before the FRI protocol, are done on evaluations of polynomials in the evaluation domain (as defined in the previous subsection).

Commitments to all the polynomials, before the FRI protocol, are done using table commitments as described in the Table Commitments section of the Merkle Tree Polynomial Commitments specification.

STARK commit

The goal of the STARK commit is to process all of the commitments produced by the prover during the protocol (including the FRI commitments), as well as produce the verifier challenges.

  1. Interactive arithmetization to absorb the execution trace tables:
    1. Absorb the original table with the channel.
    2. Sample the interaction challenges (e.g. z and alpha for the memory check argument (different alpha called memory_alpha to distinguish it from the alpha used to aggregate the different constraints into the composition polynomial)).
    3. Absorb the interaction table with the channel.
  2. Produce the aggregation of the constraint quotient polynomials as the composition polynomial:
    1. Sample the alpha challenge ("composition_alpha") to aggregate all the constraint quotient polynomials (caches the powers of alpha into "traces_coefficients").
    2. Absorb the composition columns (the hi in h(x)=ihixi) with the channel.
    3. Sample the oods point (interaction_after_composition).
    4. Absorb all evaluations with the channel.
    5. Verify that the composition polynomial is correct by checking that its evaluation at the oods point is correct using some of the evaluations jCj(oods_point)Dj(odds_point)=ihi(oods_point)×oods_pointi.
      1. The right-hand side can be computed directly using the evaluations sent by the prover
      2. The left-hand side has to be computed using the eval_composition_polynomial function defined in the AIR Arithemtization Dependency section.
  3. Produce a challenge to aggregate all FRI checks and run the FRI protocol:
    1. Sample the oods_alpha challenge with the channel.
    2. Call fri_commit.

STARK verify

The goal of STARK verify is to verify evaluation queries (by checking that evaluations exist in the committed polynomials) and the FRI queries (by running the FRI verification).

To do this, we simply call the fri_verify_initial function contained in the FRI specification and give it the values computed by eval_oods_polynomial (as defined in the AIR Arithmetization Dependency section) as first evaluations associated to the queried points. It should return two FRI objects.

Full Protocol

The protocol is split into 3 core functions:

The verify initial function is defined as:

  1. Validate the configuration and return the security_bits (TODO: how is security bits calculated).
  2. Produce a stark domain object based on the configuration log_trace_domain_size and log_n_coset (TODO:).
  3. Validate the public input (TODO: specify an external function for that?).
  4. Compute the initial digest as get_public_input_hash(public_input, cfg.n_verifier_friendly_commitment_layers, settings) (TODO: define external function for that).
  5. Initialize the channel using the digest as defined in the Channel section.
  6. Call STARK commit as defined in the STARK commit section.
  7. Call STARK verify as defined in the STARK verify section and return the two FRI objects to the caller.

One can successively call them in the following order to verify a proof:

  1. Call verify_initial on the proof and return:
    • the FriVerificationStateConstant object
    • the FriVerificationStateVariable object
    • the last_layer_coefficients
    • the security bits <-- TODO: remove this?
  2. Call verify_step in a loop on each layer of the proof (n_layers of them according to the StateConstant returned) and pass the FriVerificationStateVariable in between each calls
  3. Call verify_final on the StateConstant and StateVariable objects
  4. Enforce that the the StateVariable's iter field is n_layers + 1
  5. Return the security bits. (TODO: do we need this)