Skip to main content

openvm_stark_backend/
config.rs

1use core::fmt::Debug;
2
3use getset::Getters;
4use p3_field::{BasedVectorSpace, ExtensionField, PrimeField64, TwoAdicField};
5use serde::{Deserialize, Serialize};
6
7use crate::{hasher::MerkleHasher, interaction::LogUpSecurityParameters};
8
9/// Trait that holds the associated types for the SWIRL protocol. These are the types needed by the
10/// verifier and must be independent of the prover backend.
11///
12/// This trait only holds the associated types and the struct implementing the trait does not hold
13/// the system parameters. System parameters are specified and stored separated in [SystemParams].
14///
15/// The trait has an **implicit** associated Fiat-Shamir transcript type, including the hash used.
16/// There is no explicit associated type because the concrete implementation of the transcript may
17/// differ between prover and verifier and the verifier may further employ different implementations
18/// for logging or debugging purposes. The trait controlling concrete implementations of the
19/// transcript is specified by [`FiatShamirTranscript`](crate::FiatShamirTranscript).
20pub trait StarkProtocolConfig: 'static + Clone + Send + Sync {
21    /// The prime base field.
22    type F: TwoAdicField + PrimeField64;
23    /// The extension field, used for random challenges.
24    type EF: TwoAdicField + ExtensionField<Self::F>;
25    /// The digest type used for commitments.
26    type Digest: Copy
27        + Send
28        + Sync
29        + Debug
30        + Default
31        + PartialEq
32        + Eq
33        + Serialize
34        + for<'de> Deserialize<'de>;
35    /// The merkle tree hasher used by the polynomial commitment scheme.
36    type Hasher: MerkleHasher<F = Self::F, Digest = Self::Digest>;
37
38    /// Degree of the extension field.
39    const D_EF: usize = <Self::EF as BasedVectorSpace<Self::F>>::DIMENSION;
40
41    fn params(&self) -> &SystemParams;
42
43    fn hasher(&self) -> &Self::Hasher;
44}
45
46/// Type alias for backwards compatibility. New implementations should use `SC::F`.
47pub type Val<SC> = <SC as StarkProtocolConfig>::F;
48/// Type alias for backwards compatibility. New implementations should use `SC::Digest`.
49pub type Com<SC> = <SC as StarkProtocolConfig>::Digest;
50
51#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Getters)]
52pub struct SystemParams {
53    pub l_skip: usize,
54    pub n_stack: usize,
55    /// Maximum number of stacked polynomials (i.e. stacked matrix width). This implies a max
56    /// stacked cell count of `w_stack * 2^(n_stack + l_skip)`.
57    pub w_stack: usize,
58    /// `-log_2` of the rate for the initial Reed-Solomon code.
59    pub log_blowup: usize,
60    #[getset(get = "pub")]
61    pub whir: WhirConfig,
62    pub logup: LogUpSecurityParameters,
63    /// Global max constraint degree enforced across all AIR and Interaction constraints
64    pub max_constraint_degree: usize,
65}
66
67impl SystemParams {
68    pub fn logup_pow_bits(&self) -> usize {
69        self.logup.pow_bits
70    }
71
72    pub fn k_whir(&self) -> usize {
73        self.whir.k
74    }
75
76    #[inline]
77    pub fn log_stacked_height(&self) -> usize {
78        self.l_skip + self.n_stack
79    }
80
81    #[inline]
82    pub fn log_final_poly_len(&self) -> usize {
83        self.whir.log_final_poly_len(self.log_stacked_height())
84    }
85
86    #[inline]
87    pub fn num_whir_rounds(&self) -> usize {
88        self.whir.num_whir_rounds()
89    }
90
91    #[inline]
92    pub fn num_whir_sumcheck_rounds(&self) -> usize {
93        self.whir.num_sumcheck_rounds()
94    }
95
96    /// Constructor with many configuration options. Only `k_whir`, `max_constraint_degree`,
97    /// `query_phase_pow_bits` are preset with constants.
98    ///
99    /// The `security_bits` is the target bits of security. It is used to select the number of WHIR
100    /// queries, but does **not** guarantee the target security level is achieved by the overall
101    /// protocol using these parameters. Use the soundness calculator to ensure the target security
102    /// level is met.
103    ///
104    /// This function should only be used for internal cryptography libraries. Most users should
105    /// instead use preset parameters provided in SDKs.
106    #[allow(clippy::too_many_arguments)]
107    pub fn new(
108        log_blowup: usize,
109        l_skip: usize,
110        n_stack: usize,
111        w_stack: usize,
112        log_final_poly_len: usize,
113        folding_pow_bits: usize,
114        mu_pow_bits: usize,
115        proximity: WhirProximityStrategy,
116        security_bits: usize,
117        logup: LogUpSecurityParameters,
118        max_constraint_degree: usize,
119        whir_query_phase_pow_bits: usize,
120        k_whir: usize,
121    ) -> SystemParams {
122        let log_stacked_height = l_skip + n_stack;
123
124        SystemParams {
125            l_skip,
126            n_stack,
127            w_stack,
128            log_blowup,
129            whir: WhirConfig::new(
130                log_blowup,
131                log_stacked_height,
132                WhirParams {
133                    k: k_whir,
134                    log_final_poly_len,
135                    query_phase_pow_bits: whir_query_phase_pow_bits,
136                    proximity,
137                    folding_pow_bits,
138                    mu_pow_bits,
139                },
140                security_bits,
141            ),
142            logup,
143            max_constraint_degree,
144        }
145    }
146}
147
148/// Configurable parameters that are used to determine the [WhirConfig] for a target security level.
149///
150/// The `*_pow_bits` fields set proof-of-work grinding difficulty in *bits*, i.e. hash evaluations.
151/// The security those bits provide depends on the cost of the grinding hash (the transcript hash):
152/// cheaper hashes make each grinding attempt cheaper for an attacker. When changing the
153/// configuration—especially the hash function—revisit the PoW difficulty so it still meets the
154/// intended security against an attacker grinding with that hash. See [`crate::soundness`].
155#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
156pub struct WhirParams {
157    pub k: usize,
158    /// WHIR rounds will stop as soon as `log2` of the final polynomial length is `<=
159    /// log_final_poly_len`.
160    pub log_final_poly_len: usize,
161    pub query_phase_pow_bits: usize,
162    /// Proximity regime to use within each WHIR round. This is used to determine the number of
163    /// queries in each round for a target security level.
164    pub proximity: WhirProximityStrategy,
165    /// Number of bits of grinding to increase security of each folding step.
166    pub folding_pow_bits: usize,
167    /// Number of bits of grinding before sampling the μ batching challenge.
168    pub mu_pow_bits: usize,
169}
170
171#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
172pub struct WhirConfig {
173    /// Constant folding factor. This means that `2^k` terms are folded per round.
174    pub k: usize,
175    pub rounds: Vec<WhirRoundConfig>,
176    /// Number of bits of grinding before sampling the μ batching challenge.
177    pub mu_pow_bits: usize,
178    /// Number of bits of grinding for the query phase of each WHIR round.
179    /// The PoW bits can vary per round, but for simplicity we use the same number for all rounds.
180    pub query_phase_pow_bits: usize,
181    /// Number of bits of grinding before sampling folding randomness in each WHIR round.
182    /// The folding PoW bits can vary per round, but for simplicity (and efficiency of the
183    /// recursion circuit) we use the same number for all rounds.
184    pub folding_pow_bits: usize,
185    /// Proximity regimes for WHIR rounds. We use only proven error bounds.
186    ///
187    /// Note: this field is not needed by the verifier as it is only used to determine the number
188    /// of queries in the `rounds` field. However we store it in `WhirConfig` for security analysis
189    /// purposes.
190    pub proximity: WhirProximityStrategy,
191}
192
193#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
194pub struct WhirRoundConfig {
195    /// Number of in-domain queries sampled in this WHIR round.
196    pub num_queries: usize,
197}
198
199#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
200pub enum WhirProximityStrategy {
201    /// Unique decoding regime in every WHIR round.
202    UniqueDecoding,
203    /// Unique decoding regime in the initial `list_start_round` WHIR rounds. Then list decoding
204    /// regime with the same proximity radius derived from `m` (see `ListDecoding`) for all
205    /// subsequent WHIR rounds (0-indices `>= list_start_round`). Note that a WHIR round
206    /// consists of codewords of different degrees with the same rate. The WHIR round changes
207    /// when the rate changes.
208    SplitUniqueList { m: usize, list_start_round: usize },
209    /// List decoding regime in every WHIR round, with the same proximity radius derived from `m`,
210    /// where `m = ceil(sqrt(\rho) / 2 \eta), \eta = 1 - sqrt(\rho) - \delta, where \delta is the
211    /// proximity radius.
212    ListDecoding { m: usize },
213}
214
215impl WhirProximityStrategy {
216    pub fn initial_round(&self) -> ProximityRegime {
217        self.in_round(0)
218    }
219
220    pub fn in_round(&self, whir_round: usize) -> ProximityRegime {
221        match *self {
222            Self::UniqueDecoding => ProximityRegime::UniqueDecoding,
223            Self::SplitUniqueList {
224                m,
225                list_start_round,
226            } => {
227                if whir_round < list_start_round {
228                    ProximityRegime::UniqueDecoding
229                } else {
230                    ProximityRegime::ListDecoding { m }
231                }
232            }
233            Self::ListDecoding { m } => ProximityRegime::ListDecoding { m },
234        }
235    }
236}
237
238/// Defines the proximity regime for the proof system.
239#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
240pub enum ProximityRegime {
241    /// Unique decoding guarantees a single valid witness.
242    UniqueDecoding,
243    /// List decoding bounded by multiplicity `m`.
244    ListDecoding { m: usize },
245}
246
247impl ProximityRegime {
248    /// Maximum fraction of the RS domain on which a far word can agree with a codeword (the
249    /// per-query evasion probability under ideal uniform query sampling).
250    ///
251    /// - `UniqueDecoding`: `(1 + ρ) / 2`.
252    /// - `ListDecoding { m }`: finite-multiplicity Guruswami-Sudan threshold, `sqrt(ρ) (1 +
253    ///   1/(2m))`.
254    pub fn max_agreement(&self, log_inv_rate: usize) -> f64 {
255        let rho = 2.0_f64.powf(-(log_inv_rate as f64));
256        let max_agreement = match *self {
257            ProximityRegime::UniqueDecoding => (1.0 + rho) / 2.0,
258            ProximityRegime::ListDecoding { m } => {
259                let m = m.max(1) as f64;
260                // The stronger bound of sqrt(ρ(1 + 1/m)) could be used for the Guruswami-Sudan
261                // threshold, but to match the explicit statement in the WHIR paper, we use the
262                // weaker Taylor series expansion to sqrt(ρ) * (1 + 1/(2m))
263                rho.sqrt() * (1.0 + 1.0 / (2.0 * m))
264            }
265        };
266        // Keep the `log2` well-defined.
267        max_agreement.clamp(f64::MIN_POSITIVE, 1.0)
268    }
269
270    /// Returns total security bits for `num_queries` WHIR queries under ideal (uniform) query
271    /// sampling. See [`crate::soundness::SoundnessCalculator`] for the variant that additionally
272    /// charges the `sample_bits` sampling bias.
273    pub fn whir_query_security_bits(&self, num_queries: usize, log_inv_rate: usize) -> f64 {
274        -(num_queries as f64) * self.max_agreement(log_inv_rate).log2()
275    }
276
277    /// Returns the per-query security bits for WHIR query sampling.
278    pub fn whir_per_query_security_bits(&self, log_inv_rate: usize) -> f64 {
279        self.whir_query_security_bits(1, log_inv_rate)
280    }
281}
282
283impl WhirConfig {
284    /// Sets parameters targeting `security_bits` of provable security (including grinding), using
285    /// the given proximity regime.
286    pub fn new(
287        log_blowup: usize,
288        log_stacked_height: usize,
289        whir_params: WhirParams,
290        security_bits: usize,
291    ) -> Self {
292        let query_phase_pow_bits = whir_params.query_phase_pow_bits;
293        let protocol_security_level = security_bits.saturating_sub(query_phase_pow_bits);
294        let k_whir = whir_params.k;
295        let num_rounds = log_stacked_height
296            .saturating_sub(whir_params.log_final_poly_len)
297            .div_ceil(k_whir);
298        let mut log_inv_rate = log_blowup;
299        let proximity = whir_params.proximity;
300
301        let mut round_parameters = Vec::with_capacity(num_rounds);
302        for round in 0..num_rounds {
303            // Queries are set w.r.t. to old rate, while the rest to the new rate
304            let next_rate = log_inv_rate + (k_whir - 1);
305
306            let num_queries = Self::queries(
307                proximity.in_round(round),
308                protocol_security_level,
309                log_inv_rate,
310            );
311            round_parameters.push(WhirRoundConfig { num_queries });
312
313            log_inv_rate = next_rate;
314        }
315
316        Self {
317            k: k_whir,
318            rounds: round_parameters,
319            mu_pow_bits: whir_params.mu_pow_bits,
320            query_phase_pow_bits,
321            folding_pow_bits: whir_params.folding_pow_bits,
322            proximity,
323        }
324    }
325
326    #[inline]
327    pub fn log_final_poly_len(&self, log_stacked_height: usize) -> usize {
328        log_stacked_height - self.num_whir_rounds() * self.k
329    }
330
331    pub fn num_whir_rounds(&self) -> usize {
332        self.rounds.len()
333    }
334
335    #[inline]
336    pub fn num_sumcheck_rounds(&self) -> usize {
337        self.num_whir_rounds() * self.k
338    }
339
340    /// Pure function to calculate the number of queries necessary for a given WHIR round.
341    /// - `protocol_security_level` refers to the target bits of security without grinding.
342    /// - `log_inv_rate` is the log blowup for the WHIR round we want to calculate the number of
343    ///   queries for.
344    // Source: https://github.com/WizardOfMenlo/whir/blob/cf1599b56ff50e09142ebe6d2e2fbd86875c9986/src/whir/parameters.rs#L457
345    pub fn queries(
346        proximity_regime: ProximityRegime,
347        protocol_security_level: usize,
348        log_inv_rate: usize,
349    ) -> usize {
350        let per_query_bits = proximity_regime.whir_per_query_security_bits(log_inv_rate);
351        let num_queries_f = (protocol_security_level as f64) / per_query_bits;
352        num_queries_f.ceil() as usize
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn test_whir_list_decoding_query_bits_monotone_in_num_queries() {
362        let regime = ProximityRegime::ListDecoding { m: 2 };
363        let sec_10 = regime.whir_query_security_bits(10, 1);
364        let sec_20 = regime.whir_query_security_bits(20, 1);
365        assert!(sec_20 > sec_10);
366        assert!(sec_10 > 0.0);
367    }
368}