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}