diff --git a/rfcs/starknet/fri.html b/rfcs/starknet/fri.html index e6494b4..29ed9f2 100644 --- a/rfcs/starknet/fri.html +++ b/rfcs/starknet/fri.html @@ -395,8 +395,14 @@

TODO: Step generators

Configuration

+
+

General configuration

The FRI protocol is globally parameterized according to the following variables which from the protocol making use of FRI. For a real-world example, check the Starknet STARK verifier specification.

n_verifier_friendly_commitment_layers. The number of layers (starting from the bottom) that make use of the circuit-friendly hash.

+

proof_of_work_n_bits. The number of bits required for the proof of work. This value should be between 20 and 50.

+
+
+

Commitment configuration

The protocol as implemented accepts proofs created using different parameters. This allows provers to decide on the trade-offs between proof size, prover time and space complexity, and verifier time and space complexity.

A FRI layer reduction can be configured with the following fields:

table_n_columns. The number of values committed in each leaf of the Merkle tree. As explained in the overview, each FRI reduction makes predictible related queries to each layer, as such related points are grouped together to reduce multiple related queries to a single one.

@@ -412,6 +418,9 @@

Configuration

vector: VectorCommitmentConfig, } +
+
+

FRI configuration

A FRI configuration contains the following fields:

log_input_size. The size of the input layer to FRI (the number of evaluations committed). (TODO: double check)

n_layers. The number of layers or folding that will occur as part of the FRI proof.

@@ -444,6 +453,7 @@

Configuration

  • where log_expected_input_degree = sum_of_step_sizes + log_last_layer_degree_bound
  • +

    Commitments

    Commitments of polynomials are done using Merkle trees. The Merkle trees can be configured to hash some parameterized number of the lower layers using a circuit-friendly hash function (Poseidon).

    @@ -555,21 +565,38 @@

    Commit Phase

    Query Phase

    -
    -

    Generating queries

    -

    FRI queries are generated once, and then refined through each reduction of the FRI protocol.

    -

    The generation of each FRI query goes through the same process:

    +

    FRI queries are generated once, and then refined through each reduction of the FRI protocol. The number of queries that is randomly generated is based on configuration.

    +

    Each FRI query is composed of the following fields:

    -

    Finally, when all FRI queries have been generated, they are sorted in ascending order.

    -
    -
    -

    Verify a layer's queries

    -

    TODO: refer to the section on the first layer evaluation stuff (external dependency)

    -

    Besides the first layer, each layer verification of a query happens by simply decommitng a layer's queries.

    +

    That is, we should have for each FRI query for the layer i+1 the following identity:

    +pi+1(1/x_inv_value)=y_value + +

    Or in terms of commitment, that the decommitment at path the path behind index is y_value.

    +
    -
    -

    Computing the next layer's queries

    -

    Each reduction will produce queries to the next layer, which will expect specific evaluations.

    -

    The next queries are derived as:

    - -
    -
    FRI formula
    -

    The next evaluations expected at the queried layers are derived as:

    -

    Queries between layers verify that the next layer pi+j is computed correctly based on the currently layer pi. + + +TODO: link to section on merkle tree + +#### Computing the next layer's queries + +Each reduction will produce queries to the next layer, which will expect specific evaluations. + +The next queries are derived as: + +* index: index / coset_size +* point: point^2 +* value: FRI formula below + +##### FRI formula + +The next evaluations expected at the queried layers are derived as: + +Queries between layers verify that the next layer pi+j is computed correctly based on the currently layer pi. The next layer is either the direct next layer pi+1 or a layer further away if the configuration allows layers to be skipped. -Specifically, each reduction is allowed to skip 0, 1, 2, or 3 layers (see the MAX_FRI_STEP constant).

    -

    TODO: why MAX_FRI_STEP=3?

    -

    no skipping:

    -
      -
    • given a layer evaluations at ±v, a query without skipping layers work this way:
    • -
    • we can compute the next layer's expected evaluation at v2 by computing pi+1(v2)=pi(v)+pi(v)2+ζi·pi(v)pi(v)2v
    • -
    • we can then ask the prover to open the next layer's polynomial at that point and verify that it matches
    • -
    -

    1 skipping with ω4 the generator of the 4-th roots of unity (such that ω42=1):

    -
      -
    • pi+1(v2)=pi(v)+pi(v)2+ζi·pi(v)pi(v)2v
    • -
    • pi+1(v2)=pi(ω4v)+pi(ω4v)2+ζi·pi(v)pi(ω4v)2·ω4·v
    • -
    • pi+2(v4)=pi+1(v2)+pi+1(v2)2+ζi2·pi(v2)pi(v2)2·v2
    • -
    -

    as you can see, this requires 4 evaluations of p_{i} at v, v, ω4v, ω4v.

    -

    2 skippings with ω8 the generator of the 8-th roots of unity (such that ω82=ω4 and ω84=1):

    -
      -
    • pi+1(v2)=pi(v)+pi(v)2+ζi·pi(v)pi(v)2v
    • -
    • pi+1(v2)=pi(ω4v)+pi(ω4v)2+ζi·pi(v)pi(ω4v)2·ω4·v
    • -
    • pi+1(ω4v2)=pi(ω8v)+pi(ω8v)2+ζi·pi(ω8v)pi(ω8v)2ω8v
    • -
    • pi+1(ω4v2)=pi(ω83v)+pi(ω83v)2+ζi·pi(ω83v)pi(ω83v)2·ω83·v
    • -
    • pi+2(v4)=pi+1(v2)+pi+1(v2)2+ζi2·pi+1(v2)pi+1(v2)2·v2
    • -
    • pi+2(v4)=pi+1(ω4v2)+pi+1(ω4v2)2+ζi2·pi+1(ω4v2)pi+1(ω4v2)2·ω4v2
    • -
    • pi+3(v8)=pi+2(v4)+pi+2(v4)2+ζi4·pi+2(v4)pi+2(v4)2·v4
    • -
    -

    as you can see, this requires 8 evaluations of p_{i} at v, v, ω4v, ω4v, ω8v, ω8v, ω83v, ω83v.

    -

    3 skippings with ω16 the generator of the 16-th roots of unity (such that ω162=ω8, ω164=ω4, and ω168=1):

    -
      -
    • pi+1(v2)=pi(v)+pi(v)2+ζi·pi(v)pi(v)2v
    • -
    • pi+1(v2)=pi(ω4v)+pi(ω4v)2+ζi·pi(v)pi(ω4v)2·ω4·v
    • -
    • pi+1(ω4v2)=pi(ω8v)+pi(ω8v)2+ζi·pi(ω8v)pi(ω8v)2ω8v
    • -
    • pi+1(ω4v2)=pi(ω83v)+pi(ω83v)2+ζi·pi(ω83v)pi(ω83v)2·ω83·v
    • -
    • pi+1(ω8v2)=pi(ω16v)+pi(ω16v)2+ζi·pi(ω16v)pi(ω16v)2ω16v
    • -
    • pi+1(ω8v2)=pi(ω165v)+pi(ω165v)2+ζi·pi(ω165v)pi(ω165v)2ω165v
    • -
    • pi+1(ω83v2)=pi(ω163v)+pi(ω163v)2+ζi·pi(ω163v)pi(ω163v)2ω163v
    • -
    • pi+1(ω83v2)=pi(ω167v)+pi(ω167v)2+ζi·pi(ω167v)pi(ω167v)2ω167v
    • -
    • pi+2(v4)=pi+1(v2)+pi+1(v2)2+ζi2·pi+1(v2)pi+1(v2)2·v2
    • -
    • pi+2(v4)=pi+1(ω4v2)+pi+1(ω4v2)2+ζi2·pi+1(ω4v2)pi+1(ω4v2)2·ω4v2
    • -
    • pi+2(ω4v4)=pi+1(ω8v2)+pi+1(ω8v2)2+ζi2·pi+1(ω8v2)pi+1(ω8v2)2·ω8·v2
    • -
    • pi+2(ω4v4)=pi+1(ω83v2)+pi+1(ω83v2)2+ζi2·pi+1(ω83v2)pi+1(ω83v2)2·ω83v2
    • -
    • pi+3(v8)=pi+2(v4)+pi+2(v4)2+ζi4·pi+2(v4)pi+2(v4)2·v4
    • -
    • pi+3(v8)=pi+2(ω4v4)+pi+2(ω4v4)2+ζi4·pi+2(ω4v4)pi+2(ω4v4)2·ω4v4
    • -
    • pi+4(v16)=pi+3(v8)+pi+3(v8)2+ζi8·pi+3(v8)pi+3(v8)2·v8
    • -
    -

    as you can see, this requires 16 evaluations of p_{i} at v, v, ω4v, ω4v, ω8v, ω8v, ω83v, ω83v, ω16v, ω16v, ω163v, ω163v, ω165v, ω165v, ω7v, ω7v.

    -

    TODO: reconcile with section on the differences with vanilla FRI

    -

    TODO: reconcile with constants used for elements and inverses chosen in subgroups of order 2i (the ωs)

    -
    -
    -
    -
    -

    Full Protocol

    -

    The FRI flow is split into three main functions. The reason is that verification of FRI proofs are computationally intensive, and programs might want to verify a FRI proof in multiple calls (for example, if calls have a cost limit). The three main functions are:

    - +Specifically, each reduction is allowed to skip 0, 1, 2, or 3 layers (see the `MAX_FRI_STEP` constant). + +TODO: why MAX_FRI_STEP=3? + +no skipping: + +* given a layer evaluations at ±v, a query without skipping layers work this way: +* we can compute the next layer's *expected* evaluation at v2 by computing pi+1(v2)=pi(v)+pi(v)2+ζi·pi(v)pi(v)2v +* we can then ask the prover to open the next layer's polynomial at that point and verify that it matches + +1 skipping with ω4 the generator of the 4-th roots of unity (such that ω42=1): + +* pi+1(v2)=pi(v)+pi(v)2+ζi·pi(v)pi(v)2v +* pi+1(v2)=pi(ω4v)+pi(ω4v)2+ζi·pi(v)pi(ω4v)2·ω4·v +* pi+2(v4)=pi+1(v2)+pi+1(v2)2+ζi2·pi(v2)pi(v2)2·v2 + +as you can see, this requires 4 evaluations of p_{i} at v, v, ω4v, ω4v. + +2 skippings with ω8 the generator of the 8-th roots of unity (such that ω82=ω4 and ω84=1): + +* pi+1(v2)=pi(v)+pi(v)2+ζi·pi(v)pi(v)2v +* pi+1(v2)=pi(ω4v)+pi(ω4v)2+ζi·pi(v)pi(ω4v)2·ω4·v +* pi+1(ω4v2)=pi(ω8v)+pi(ω8v)2+ζi·pi(ω8v)pi(ω8v)2ω8v +* pi+1(ω4v2)=pi(ω83v)+pi(ω83v)2+ζi·pi(ω83v)pi(ω83v)2·ω83·v +* pi+2(v4)=pi+1(v2)+pi+1(v2)2+ζi2·pi+1(v2)pi+1(v2)2·v2 +* pi+2(v4)=pi+1(ω4v2)+pi+1(ω4v2)2+ζi2·pi+1(ω4v2)pi+1(ω4v2)2·ω4v2 +* pi+3(v8)=pi+2(v4)+pi+2(v4)2+ζi4·pi+2(v4)pi+2(v4)2·v4 + +as you can see, this requires 8 evaluations of p_{i} at v, v, ω4v, ω4v, ω8v, ω8v, ω83v, ω83v. + +3 skippings with ω16 the generator of the 16-th roots of unity (such that ω162=ω8, ω164=ω4, and ω168=1): + +* pi+1(v2)=pi(v)+pi(v)2+ζi·pi(v)pi(v)2v +* pi+1(v2)=pi(ω4v)+pi(ω4v)2+ζi·pi(v)pi(ω4v)2·ω4·v +* pi+1(ω4v2)=pi(ω8v)+pi(ω8v)2+ζi·pi(ω8v)pi(ω8v)2ω8v +* pi+1(ω4v2)=pi(ω83v)+pi(ω83v)2+ζi·pi(ω83v)pi(ω83v)2·ω83·v +* pi+1(ω8v2)=pi(ω16v)+pi(ω16v)2+ζi·pi(ω16v)pi(ω16v)2ω16v +* pi+1(ω8v2)=pi(ω165v)+pi(ω165v)2+ζi·pi(ω165v)pi(ω165v)2ω165v +* pi+1(ω83v2)=pi(ω163v)+pi(ω163v)2+ζi·pi(ω163v)pi(ω163v)2ω163v +* pi+1(ω83v2)=pi(ω167v)+pi(ω167v)2+ζi·pi(ω167v)pi(ω167v)2ω167v +* pi+2(v4)=pi+1(v2)+pi+1(v2)2+ζi2·pi+1(v2)pi+1(v2)2·v2 +* pi+2(v4)=pi+1(ω4v2)+pi+1(ω4v2)2+ζi2·pi+1(ω4v2)pi+1(ω4v2)2·ω4v2 +* pi+2(ω4v4)=pi+1(ω8v2)+pi+1(ω8v2)2+ζi2·pi+1(ω8v2)pi+1(ω8v2)2·ω8·v2 +* pi+2(ω4v4)=pi+1(ω83v2)+pi+1(ω83v2)2+ζi2·pi+1(ω83v2)pi+1(ω83v2)2·ω83v2 +* pi+3(v8)=pi+2(v4)+pi+2(v4)2+ζi4·pi+2(v4)pi+2(v4)2·v4 +* pi+3(v8)=pi+2(ω4v4)+pi+2(ω4v4)2+ζi4·pi+2(ω4v4)pi+2(ω4v4)2·ω4v4 +* pi+4(v16)=pi+3(v8)+pi+3(v8)2+ζi8·pi+3(v8)pi+3(v8)2·v8 + +as you can see, this requires 16 evaluations of p_{i} at v, v, ω4v, ω4v, ω8v, ω8v, ω83v, ω83v, ω16v, ω16v, ω163v, ω163v, ω165v, ω165v, ω7v, ω7v. + +TODO: reconcile with section on the differences with vanilla FRI + +TODO: reconcile with constants used for elements and inverses chosen in subgroups of order 2i (the ωs) + +### Proof of work + +In order to increase the cost of attacks on the protocol, a proof of work is added at the end of the commitment phase. + +Given a 32-bit hash `digest` and a difficulty target of `n_bits`, verify the 64-bit proof of work `nonce` by doing the following: + +1. Produce a `init_hash = hash_n_bytes(0x0123456789abcded || digest || n_bits)` (TODO: endianness) +1. Produce a `hash = hash_n_bytes(init_hash || nonce)` (TODO: endianness) +1. Enforce that the 128-bit high bits of `hash` start with `128 - n_bits` zeros. (TODO: where is `n_bits` enforced to not be more than 128? I can't remember) + + +
    fn proof_of_work_commit(
    +    ref channel: Channel, unsent_commitment: ProofOfWorkUnsentCommitment, config: ProofOfWorkConfig
    +) {
    +    verify_proof_of_work(channel.digest.into(), config.n_bits, unsent_commitment.nonce);
    +    channel.read_uint64_from_prover(unsent_commitment.nonce);
    +}
    +
    +fn verify_proof_of_work(digest: u256, n_bits: u8, nonce: u64) {
    +    // Compute the initial hash.
    +    // Hash(0x0123456789abcded || digest   || n_bits )
    +    //      8 bytes            || 32 bytes || 1 byte
    +    // Total of 0x29 = 41 bytes.
    +
    +    let mut init_hash_data = ArrayTrait::new(); // u8 with blake, u64 with keccak
    +    init_hash_data.append_big_endian(MAGIC);
    +    init_hash_data.append_big_endian(digest);
    +    let init_hash = hash_n_bytes(init_hash_data, n_bits.into(), true).flip_endianness();
    +
    +    // Compute Hash(init_hash || nonce   )
    +    //              32 bytes  || 8 bytes
    +    // Total of 0x28 = 40 bytes.
    +
    +    let mut hash_data = ArrayTrait::new(); // u8 with blake, u64 with keccak
    +    hash_data.append_big_endian(init_hash);
    +    hash_data.append_big_endian(nonce);
    +    let hash = hash_n_bytes(hash_data, 0, false).flip_endianness();
    +
    +    let work_limit = pow(2, 128 - n_bits.into());
    +    assert(
    +        Into::<u128, u256>::into(hash.high) < Into::<felt252, u256>::into(work_limit),
    +        'proof of work failed'
    +    )
    +}
    +
    + + +### Full Protocol + +The FRI flow is split into four main functions. The only reason for doing this is that verification of FRI proofs can be computationally intensive, and users of this specification might want to split the verification of a FRI proof in multiple calls. + +The four main functions are: + +1. `fri_commit`, which returns the commitment to every layers of the FRI proof. +1. `fri_verify_initial`, which returns the initial set of queries. +1. `fri_verify_step`, which takes a set of queries and returns another set of queries. +1. `fri_verify_final`, which takes the final set of queries and the last layer coefficients and returns the final result. + +To retain context, functions pass around two objects: + + +
    struct FriVerificationStateConstant {
    +    // the number of layers in the FRI proof (including skipped layers) (TODO: not the first)
    +    n_layers: u32, 
    +    // commitments to each layer (excluding the first, last, and any skipped layers)
    +    commitment: Span<TableCommitment>, 
    +    // verifier challenges used to produce each (non-skipped) layer polynomial (except the first)
    +    eval_points: Span<felt252>, 
    +    // the number of layers to skip for each reduction
    +    step_sizes: Span<felt252>, 
    +    // the hash of the polynomial of the last layer
    +    last_layer_coefficients_hash: felt252, 
    +}
    +struct FriVerificationStateVariable {
    +    // a counter representing the current layer being verified
    +    iter: u32, 
    +    // the FRI queries for each (non-skipped) layer
    +    queries: Span<FriLayerQuery>, 
    +}
    +
    + + -

    We give more detail to each function below.

    -

    fri_verify_initial(queries, fri_commitment, decommitment).

    - +We give more detail to each function below. + +**`fri_commit` + +1. Initialize the channel with a prologue. A prologue contains any context relevant to this proof. +1. stark_commit(channel, public_input, unsent_commitment, cfg, stark_domains) +1. last_layer_coefficients = stark_commitment.fri.last_layer_coefficients +1. generate_queries(channel, config.n_queries, stark_domains.eval_domain_size) +1. stark_verify(NUM_COLUMNS_FIRST, NUM_COLUMNS_SECOND, queries, stark_Comitment, witness, stark_domains, settings) + +TODO: where is settings used? + +**`fri_verify_initial(queries, fri_commitment, decommitment)`**. + +* enforce that the number of queries matches the number of values to decommit +* enforce that last layer has the right number of coefficients (TODO: how?) +* compute the first layer of queries `gather_first_layer_queries` (TODO: how?) <-- this only happens for the first layer +* initialize and return the two state objects + +
        (
             FriVerificationStateConstant {
                 n_layers: (commitment.config.n_layers - 1).try_into().unwrap(),
    @@ -677,20 +798,23 @@ 

    Full Protocol

    ) }
    -

    fri_verify_step(stateConstant, stateVariable, witness, settings).

    - -

    fri_verify_final(stateConstant, stateVariable, last_layer_coefficients).

    - + + +**`fri_verify_step(stateConstant, stateVariable, witness, settings)`**. + +* enforce that `stateVariable.iter <= stateConstant.n_layers` +* compute the next layer queries (TODO: link to section on that) +* verify the queries +* increment the `iter` counter +* return the next queries and the counter + +**`fri_verify_final(stateConstant, stateVariable, last_layer_coefficients)`**. + +* enforce that the counter has reached the last layer from the constants (`iter == n_layers`) +* enforce that the last_layer_coefficient matches the hash contained in the state (TODO: only relevant if we created that hash in the first function) +* manually evaluate the last layer's polynomial at every query and check that it matches the expected evaluations. + +
    fn fri_verify_final(
         stateConstant: FriVerificationStateConstant,
         stateVariable: FriVerificationStateVariable,
    @@ -710,20 +834,21 @@ 

    Full Protocol

    ) }
    + + + +## Test Vectors + +TKTK + +## Security Considerations + +* number of queries? +* size of domain? +* proof of work stuff? + +security bits: `n_queries * log_n_cosets + proof_of_work_bits`
    - -
    -

    Test Vectors

    -

    TKTK

    -
    -
    -

    Security Considerations

    - -

    security bits: n_queries * log_n_cosets + proof_of_work_bits

    diff --git a/source/starknet/fri.md b/source/starknet/fri.md index e56adc5..0ad2f29 100644 --- a/source/starknet/fri.md +++ b/source/starknet/fri.md @@ -365,10 +365,16 @@ TODO: explain more here ## Configuration +### General configuration + The FRI protocol is globally parameterized according to the following variables which from the protocol making use of FRI. For a real-world example, check the [Starknet STARK verifier specification](stark.md). **`n_verifier_friendly_commitment_layers`**. The number of layers (starting from the bottom) that make use of the circuit-friendly hash. +**`proof_of_work_n_bits`**. The number of bits required for the proof of work. This value should be between 20 and 50. + +### Commitment configuration + The protocol as implemented accepts proofs created using different parameters. This allows provers to decide on the trade-offs between proof size, prover time and space complexity, and verifier time and space complexity. A FRI layer reduction can be configured with the following fields: @@ -391,6 +397,8 @@ struct TableCommitmentConfig { } ``` +### FRI configuration + A FRI configuration contains the following fields: **`log_input_size`**. The size of the input layer to FRI (the number of evaluations committed). (TODO: double check) @@ -428,6 +436,7 @@ TODO: validate(cfg, log_n_cosets, n_verified_friendly_commitment_layers): * TODO: why is log_n_cosets passed? and what is it? (number of additional cosets with the blowup factor?) * where `log_expected_input_degree = sum_of_step_sizes + log_last_layer_degree_bound` + ## Commitments Commitments of polynomials are done using [Merkle trees](). The Merkle trees can be configured to hash some parameterized number of the lower layers using a circuit-friendly hash function (Poseidon). @@ -547,23 +556,41 @@ TODO: explain why, I think this is because you don't want to have to produce too ### Query Phase -#### Generating queries +FRI queries are generated once, and then refined through each reduction of the FRI protocol. The number of queries that is randomly generated is based on [configuration](). -FRI queries are generated once, and then refined through each reduction of the FRI protocol. +Each FRI query is composed of the following fields: + +* `index`: the index of the query in the layer's evaluations. Note that this value needs to be shifted before being used as a path in a Merkle tree commitment. +* `y_value`: the evaluation of the layer's polynomial at the queried point. +* `x_inv_value`: the inverse of the point at which the layer's polynomial is evaluated. This value is derived from the `index` as explained in the next subsection. + +That is, we should have for each FRI query for the layer $i+1$ the following identity: + +$$ +p_{i+1}(1/\text{x_inv_value}) = \text{y_value} +$$ + +Or in terms of commitment, that the decommitment at path the path behind `index` is `y_value`. + +