Skip to main content

p3_security/
fri.rs

1//! FRI low-degree-test soundness.
2//!
3//! Conjectured regime: random-words bound, [2025/2010] §1.5.
4//! Legacy regime: historical pre-random-words ethSTARK query estimate, [2021/582].
5//! Proven regime: round-by-round, [2024/1553] Theorems 2 & 3, with the
6//! DKT26 Johnson line-MCA commit bound ([2026/2056] Theorem 5.12 and
7//! Appendix B.1–B.2, extended to powers batching through the order-zero
8//! case of §7.2, Equation (88)). Its characteristic-free transfer is Lemma
9//! 5.3, not the positive-order powers-batching corollary in that section.
10//!
11//! Correspondence with [`crate::assumption::SecurityAssumption`]:
12//! - [`proven_error_udr`] is the FRI counterpart of `UniqueDecoding`
13//!   (with FRI-specific α = (1 + ρ⁺)/2 incorporating OOD expansion).
14//! - [`proven_error_ldr_m`] / [`best_ldr_m`] is the FRI counterpart of
15//!   `JohnsonBound`, but searches `m ∈ [3, LDR_M_CAP]` rather than
16//!   fixing `m = 10` as WHIR does.
17//! - `CapacityBound` is not currently supported by FRI's commit-phase
18//!   analysis.
19
20use alloc::vec;
21use alloc::vec::Vec;
22
23use libm::{log2, pow};
24
25use crate::error::ErrorBits;
26use crate::ldt::LowDegreeTest;
27use crate::proximity::{
28    LDR_M_CAP, alpha_ldr_m, alpha_udr, compute_upper_m, gamma_ldr_m,
29    johnson_exceptional_line_count_log2,
30};
31use crate::report::{LDT_COMMIT_LABEL, LDT_QUERY_LABEL, SecurityTerm};
32use crate::shape::{InstanceShape, StarkAirParams};
33
34/// Security-relevant mirror of `p3_fri::FriParameters`.
35///
36/// Keep in sync with `FriParameters` whenever a security-affecting field
37/// is added there. There is intentionally no `From<FriParameters>` impl:
38/// the protocol crate assembles this from its own params + instance
39/// shape, baking in protocol-specific assumptions at the call site.
40#[derive(Copy, Clone, Debug)]
41pub struct FriRegime {
42    pub log_blowup: usize,
43    pub num_queries: usize,
44    pub log_final_poly_len: usize,
45    pub max_log_arity: usize,
46    pub commit_pow_bits: usize,
47    pub query_pow_bits: usize,
48}
49
50impl FriRegime {
51    const fn folding_factor(self) -> f64 {
52        (1usize << self.max_log_arity) as f64
53    }
54}
55
56impl LowDegreeTest for FriRegime {
57    fn log_blowup(&self) -> usize {
58        self.log_blowup
59    }
60
61    fn proven_error_udr(&self, air: &StarkAirParams, shape: &InstanceShape) -> ErrorBits {
62        proven_error_udr(self, air, shape)
63    }
64
65    fn best_ldr(&self, air: &StarkAirParams, shape: &InstanceShape) -> Option<(usize, ErrorBits)> {
66        best_ldr_m(self, air, shape)
67    }
68
69    fn ldr_candidates(
70        &self,
71        air: &StarkAirParams,
72        shape: &InstanceShape,
73    ) -> Vec<(usize, ErrorBits)> {
74        ldr_candidates(self, air, shape)
75    }
76
77    fn conjectured_error(&self, shape: &InstanceShape) -> ErrorBits {
78        conjectured_error(self, shape)
79    }
80
81    fn legacy_conjectured_error(&self, _shape: &InstanceShape) -> Option<ErrorBits> {
82        Some(legacy_conjectured_error(self))
83    }
84
85    fn conjectured_terms(&self, shape: &InstanceShape) -> Vec<SecurityTerm> {
86        let mut terms = vec![SecurityTerm::new(
87            LDT_QUERY_LABEL,
88            conjectured_error(self, shape),
89        )];
90        terms.extend(
91            conjectured_commit_phase_error(self, shape)
92                .map(|bits| SecurityTerm::new(LDT_COMMIT_LABEL, bits)),
93        );
94        terms
95    }
96}
97
98/// Conjectured low-degree-test soundness (random-words, [2025/2010] §1.5).
99///
100/// `b = num_queries · (−log2(ρ + η)) + query_pow`,
101/// with `η ≈ (log2(e/ρ) · ρ) / log2(q)`.
102pub fn conjectured_error(regime: &FriRegime, shape: &InstanceShape) -> ErrorBits {
103    if regime.log_blowup == 0 || shape.modulus_bits == 0 {
104        return ErrorBits::from_log2(regime.query_pow_bits as f64);
105    }
106    let log_blowup_f = regime.log_blowup as f64;
107    let rho = pow(2.0, -log_blowup_f);
108    let log2_e_over_rho = core::f64::consts::LOG2_E + log_blowup_f;
109    let eta = (log2_e_over_rho * rho) / shape.modulus_bits as f64;
110    let effective = rho + eta;
111    if effective <= 0.0 || effective >= 1.0 {
112        return ErrorBits::from_log2(regime.query_pow_bits as f64);
113    }
114    let bits_per_query = -log2(effective);
115    let bits = regime.num_queries as f64 * bits_per_query + regime.query_pow_bits as f64;
116    ErrorBits::from_log2(bits)
117}
118
119/// Historical FRI query-error estimate (ethSTARK
120/// [2021/582](https://eprint.iacr.org/2021/582) §5.10.1, pre-random-words).
121///
122/// `b = num_queries · log_blowup + query_pow` is `-log2(2^-zeta * eps_1)`:
123/// the paper's FRI query error `eps_1 = rho^s`, boosted by grinding. It does
124/// not reproduce the paper's total-error `lambda`, whose Eq. (19) includes
125/// the one-bit union-bound loss from Eq. (18). The composite below instead
126/// takes the round-by-round minimum over its terms.
127///
128/// This omits the random-words correction in [`conjectured_error`] and the
129/// folding round in [`conjectured_commit_phase_error`]. It is for historical
130/// comparison, not a soundness bound or a guide to deployment parameters.
131/// Use [`crate::stark::legacy_security_report`] to compose it with the
132/// AIR, DEEP-ALI, batching, extra, and commitment-collision terms.
133pub const fn legacy_conjectured_error(regime: &FriRegime) -> ErrorBits {
134    ErrorBits::from_log2((regime.log_blowup * regime.num_queries + regime.query_pow_bits) as f64)
135}
136
137/// FRI commit-phase per-round error in the conjectured regime.
138///
139/// Identical to [`commit_phase_error_udr`], and deliberately so: the bound
140/// counts the folding challenges that are *exceptional* for a given committed
141/// word, and that count does not depend on the decoding regime. What the
142/// random-words conjecture buys is the removal of the list-size multiplier
143/// `L⁺` that the Johnson-bound analysis pays (see
144/// [`crate::stark::conjectured_security_report`]) — it does not assert that a
145/// bad folding challenge cannot exist. Dropping the round entirely, as an
146/// LDT-only conjectured number does, therefore overstates security for a large
147/// LDE domain over a small field.
148///
149/// Returns `None` when no fold occurs, in which case there is no such round.
150pub fn conjectured_commit_phase_error(
151    regime: &FriRegime,
152    shape: &InstanceShape,
153) -> Option<ErrorBits> {
154    commit_phase_error_udr(regime, shape)
155}
156
157/// FRI commit-phase per-round error in UDR.
158///
159/// ε ≤ (folding − 1)·(n + 1) / |F|, applied when at least one fold occurs.
160/// Slightly conservative versus `soundcalc`'s tighter `(γn + 1)` factor.
161///
162/// Returns `None` when `regime.max_log_arity` is `0` (folding factor `1`,
163/// i.e. no fold at all) — such a regime has no commit-phase round, rather
164/// than being indistinguishable from arity 2.
165pub fn commit_phase_error_udr(regime: &FriRegime, shape: &InstanceShape) -> Option<ErrorBits> {
166    let folding_minus_one = regime.folding_factor() - 1.0;
167    if folding_minus_one <= 0.0 {
168        return None;
169    }
170    let lde_log = shape.log_trace_length + regime.log_blowup;
171    let num_layers = lde_log.saturating_sub(regime.log_final_poly_len) / regime.max_log_arity;
172    if num_layers == 0 {
173        return None;
174    }
175    let n = (1u64 << lde_log) as f64;
176    let bits = shape.modulus_bits as f64 - log2(folding_minus_one * (n + 1.0))
177        + regime.commit_pow_bits as f64;
178    Some(ErrorBits::from_log2(bits.max(0.0)))
179}
180
181/// FRI commit-phase per-round error in LDR with explicit proximity
182/// parameter `m`. DKT26 Theorem 5.12 and Appendix B.1–B.2 give the
183/// Johnson-regime line-MCA count `C = 8·n·(m + 1/2)^3/(3·rho_minus)`;
184/// multiplying by `folding − 1` accounts for the polynomial-curve degree of
185/// a folding round. We also retain the independent `n/|F|` bound from
186/// [2024/1553] and take the minimum of their security bits. Round-by-
187/// round soundness is dominated by round 0 (largest `n`), so we use
188/// `n = lde_domain_size` for every round.
189/// For positive-degree codes at `m >= 3`, MCA gives fewer security bits and
190/// determines this minimum. Reconstruction can instead cap the separate
191/// constant-code bound.
192///
193/// Returns `None` when `regime.max_log_arity` is `0` (folding factor `1`,
194/// i.e. no fold at all) — such a regime has no commit-phase round, rather
195/// than being indistinguishable from arity 2.
196pub fn commit_phase_error_ldr_m(
197    regime: &FriRegime,
198    shape: &InstanceShape,
199    m: usize,
200) -> Option<ErrorBits> {
201    let rho = pow(2.0, -(regime.log_blowup as f64));
202    let pp = gamma_ldr_m(regime.log_blowup, m);
203    if pp <= 0.0 {
204        return Some(ErrorBits::from_log2(0.0));
205    }
206    let folding_minus_one = regime.folding_factor() - 1.0;
207    if folding_minus_one <= 0.0 {
208        return None;
209    }
210    let lde_log = shape.log_trace_length + regime.log_blowup;
211    let line_count_log =
212        johnson_exceptional_line_count_log2(shape.log_trace_length, regime.log_blowup, m);
213    let bits_linear = shape.modulus_bits as f64 - line_count_log - log2(folding_minus_one)
214        + regime.commit_pow_bits as f64;
215
216    let log_n_plus_one = lde_log as f64 + log2(1.0 + pow(2.0, -(lde_log as f64)));
217
218    let bits_n_over_q = shape.modulus_bits as f64
219        - log2(regime.folding_factor())
220        - log_n_plus_one
221        - log2(2.0 * m as f64 + 1.0)
222        + 0.5 * log2(rho)
223        + regime.commit_pow_bits as f64;
224
225    Some(ErrorBits::from_log2(
226        bits_linear.min(bits_n_over_q).max(0.0),
227    ))
228}
229
230/// FRI query-phase error: ε ≤ αᵏ, contributing `query_pow − k·log2(α)` bits.
231pub fn query_phase_error(alpha: f64, num_queries: usize, query_pow_bits: usize) -> ErrorBits {
232    if !alpha.is_finite() || alpha <= 0.0 || alpha >= 1.0 {
233        return ErrorBits::from_log2(0.0);
234    }
235    let bits = query_pow_bits as f64 - log2(pow(alpha, num_queries as f64));
236    ErrorBits::from_log2(bits)
237}
238
239/// Proven LDT-only error in the UDR regime. Combines commit-phase and
240/// query-phase contributions; AIR/DEEP terms compose at the protocol
241/// call site (e.g. `crate::stark::proven_security`).
242pub fn proven_error_udr(
243    regime: &FriRegime,
244    air: &StarkAirParams,
245    shape: &InstanceShape,
246) -> ErrorBits {
247    if regime.log_blowup == 0 || shape.log_trace_length == 0 || shape.modulus_bits == 0 {
248        return ErrorBits::from_log2(0.0);
249    }
250    let alpha = alpha_udr(shape.log_trace_length, regime.log_blowup, air.max_combo);
251    let lde = (1u64 << (shape.log_trace_length + regime.log_blowup)) as f64;
252    let k = (1u64 << shape.log_trace_length) as f64;
253    if k + air.max_combo as f64 >= alpha * lde {
254        return ErrorBits::from_log2(0.0);
255    }
256    let query = query_phase_error(alpha, regime.num_queries, regime.query_pow_bits);
257    commit_phase_error_udr(regime, shape).map_or(query, |commit| ErrorBits::min(&[commit, query]))
258}
259
260/// Proven LDT-only error in the LDR regime with explicit `m`.
261pub fn proven_error_ldr_m(
262    regime: &FriRegime,
263    air: &StarkAirParams,
264    shape: &InstanceShape,
265    m: usize,
266) -> ErrorBits {
267    if regime.log_blowup == 0 || shape.log_trace_length == 0 || shape.modulus_bits == 0 {
268        return ErrorBits::from_log2(0.0);
269    }
270    let alpha = alpha_ldr_m(regime.log_blowup, m);
271    if alpha >= 1.0 {
272        return ErrorBits::from_log2(0.0);
273    }
274    let pp = gamma_ldr_m(regime.log_blowup, m);
275    if pp <= 0.0 {
276        return ErrorBits::from_log2(0.0);
277    }
278    let lde = (1u64 << (shape.log_trace_length + regime.log_blowup)) as f64;
279    let k = (1u64 << shape.log_trace_length) as f64;
280    if k + air.max_combo as f64 >= (1.0 - pp) * lde {
281        return ErrorBits::from_log2(0.0);
282    }
283    let query = query_phase_error(alpha, regime.num_queries, regime.query_pow_bits);
284    commit_phase_error_ldr_m(regime, shape, m)
285        .map_or(query, |commit| ErrorBits::min(&[commit, query]))
286}
287
288/// Search `m ∈ [3, min(compute_upper_m, LDR_M_CAP)]` for the value
289/// maximising LDR security; returns `(best_m, ldt_error_at_best_m)`.
290///
291/// Optimizes `min(commit, query)` only — the LDT-only error — not the full
292/// `min(ALI, DEEP, commit, query)` composite ultimately reported by
293/// [`crate::stark::proven_security`]. In practice ALI/DEEP don't bind at
294/// the optimum, so the chosen `m` matches what optimizing the full
295/// composite would pick, but this function does not verify that.
296pub fn best_ldr_m(
297    regime: &FriRegime,
298    air: &StarkAirParams,
299    shape: &InstanceShape,
300) -> Option<(usize, ErrorBits)> {
301    ldr_candidates(regime, air, shape)
302        .into_iter()
303        .max_by(|a, b| {
304            a.1.bits()
305                .partial_cmp(&b.1.bits())
306                .unwrap_or(core::cmp::Ordering::Equal)
307        })
308}
309
310/// Every admissible `m ∈ [3, min(compute_upper_m, LDR_M_CAP)]` with the
311/// LDT-only error attained there, in increasing order of `m`.
312///
313/// [`best_ldr_m`] picks from this list by the LDT error alone;
314/// [`crate::stark::proven_security_report`] takes the whole list and picks by
315/// the full composite, which is the choice that stays valid once a term
316/// outside the LDT depends on `m` (see [`crate::ldt::LowDegreeTest::ldr_candidates`]).
317///
318/// Empty when the trace is too small to admit any valid `m`.
319pub fn ldr_candidates(
320    regime: &FriRegime,
321    air: &StarkAirParams,
322    shape: &InstanceShape,
323) -> Vec<(usize, ErrorBits)> {
324    let trace_length = 1usize << shape.log_trace_length;
325    let m_max = core::cmp::min(compute_upper_m(trace_length, air.max_combo), LDR_M_CAP);
326    let m_min = 3usize;
327    if m_max < m_min {
328        return Vec::new();
329    }
330    (m_min..=m_max)
331        .map(|m| (m, proven_error_ldr_m(regime, air, shape, m)))
332        .collect()
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::stark::proven_security;
339
340    fn benchmark_regime() -> FriRegime {
341        FriRegime {
342            log_blowup: 1,
343            num_queries: 100,
344            log_final_poly_len: 0,
345            max_log_arity: 3,
346            commit_pow_bits: 0,
347            query_pow_bits: 16,
348        }
349    }
350
351    fn benchmark_shape() -> InstanceShape {
352        InstanceShape {
353            log_trace_length: 20,
354            modulus_bits: 252,
355            collision_resistance: 128,
356            num_batched_functions: 1,
357        }
358    }
359
360    fn benchmark_air() -> StarkAirParams {
361        StarkAirParams {
362            num_constraints: 1,
363            max_constraint_degree: 2,
364            num_quotient_chunks: 1,
365            max_combo: 2,
366        }
367    }
368
369    /// Regression vector for the benchmark configuration: log_blowup=1,
370    /// num_queries=100, query_pow=16, commit_pow=0, max_log_arity=3,
371    /// |F|=252 bits, trace 2^20, num_constraints=1, max_deg=2,
372    /// max_combo=2 → UDR=57 bits, LDR=65 bits.
373    #[test]
374    fn proven_security_regression_benchmark_high_arity() {
375        let regime = benchmark_regime();
376        let air = benchmark_air();
377        let shape = benchmark_shape();
378
379        let udr_ldt = proven_error_udr(&regime, &air, &shape);
380        let (best_m, ldr_ldt) = best_ldr_m(&regime, &air, &shape).unwrap();
381
382        let udr_bits = crate::stark::proven_security_udr(&air, &shape, udr_ldt, &[])
383            .bits()
384            .floor() as usize;
385        let ldr_bits = crate::stark::proven_security_ldr_m(
386            &air,
387            &shape,
388            regime.log_blowup,
389            best_m,
390            ldr_ldt,
391            &[],
392        )
393        .bits()
394        .floor() as usize;
395
396        assert_eq!(udr_bits, 57);
397        assert_eq!(ldr_bits, 65);
398
399        let combined = proven_security(
400            &air,
401            &shape,
402            regime.log_blowup,
403            udr_ldt,
404            best_m,
405            ldr_ldt,
406            &[],
407        )
408        .bits()
409        .floor() as usize;
410        assert_eq!(combined, 65);
411    }
412
413    /// The conjectured commit-phase round is a real constraint, not a
414    /// formality: over a 128-bit field with a 2^26 LDE domain it lands near 100
415    /// bits, below a 128-bit target, so an LDT-only conjectured number that
416    /// omits it overstates security. It also matches the UDR bound exactly —
417    /// the conjecture removes the list-size multiplier, not the count of
418    /// exceptional folding challenges.
419    #[test]
420    fn conjectured_commit_phase_binds_below_a_128_bit_target() {
421        let regime = FriRegime {
422            log_blowup: 3,
423            num_queries: 27,
424            log_final_poly_len: 0,
425            max_log_arity: 2,
426            commit_pow_bits: 0,
427            query_pow_bits: 16,
428        };
429        let shape = InstanceShape {
430            log_trace_length: 23,
431            modulus_bits: 128,
432            collision_resistance: 128,
433            num_batched_functions: 1,
434        };
435
436        let commit = conjectured_commit_phase_error(&regime, &shape).expect("folds occur");
437        // |F| - log2(3 * (2^26 + 1)) = 128 - 27.585
438        assert!(
439            (commit.bits() - (128.0 - libm::log2(3.0 * (65_536.0 * 1024.0 + 1.0)))).abs() < 1e-9
440        );
441        assert!(commit.bits() < 128.0, "got {}", commit.bits());
442        assert_eq!(
443            commit.bits(),
444            commit_phase_error_udr(&regime, &shape).unwrap().bits()
445        );
446    }
447
448    /// Commit-phase grinding is credited to the commit-phase term and to
449    /// nothing else, and a fold-free configuration has no such round at all.
450    #[test]
451    fn conjectured_commit_phase_credits_only_its_own_grinding() {
452        let base = benchmark_regime();
453        let shape = benchmark_shape();
454
455        let ground = FriRegime {
456            commit_pow_bits: 12,
457            ..base
458        };
459        let b0 = conjectured_commit_phase_error(&base, &shape).expect("folds occur");
460        let b12 = conjectured_commit_phase_error(&ground, &shape).expect("folds occur");
461        assert!((b12.bits() - b0.bits() - 12.0).abs() < 1e-12);
462        // The query phase is untouched by it.
463        assert_eq!(
464            conjectured_error(&ground, &shape).bits(),
465            conjectured_error(&base, &shape).bits()
466        );
467
468        // Folding down to the full domain leaves no commit round.
469        let no_folds = FriRegime {
470            log_final_poly_len: shape.log_trace_length + base.log_blowup,
471            ..base
472        };
473        assert!(conjectured_commit_phase_error(&no_folds, &shape).is_none());
474        assert_eq!(
475            LowDegreeTest::conjectured_terms(&no_folds, &shape).len(),
476            1,
477            "a fold-free regime reports the query phase only"
478        );
479    }
480
481    /// `max_log_arity: 0` (folding factor 1) is a genuinely fold-free regime,
482    /// not a stand-in for arity 2: both the UDR and LDR commit-phase bounds
483    /// must report no round rather than silently clamping to arity 2's
484    /// `folding_minus_one = 1`.
485    #[test]
486    fn arity_one_reports_no_commit_round_rather_than_impersonating_arity_two() {
487        let base = benchmark_regime();
488        let shape = benchmark_shape();
489        let air = benchmark_air();
490        let no_arity = FriRegime {
491            max_log_arity: 0,
492            ..base
493        };
494
495        assert!(commit_phase_error_udr(&no_arity, &shape).is_none());
496        assert!(commit_phase_error_ldr_m(&no_arity, &shape, 10).is_none());
497        assert!(conjectured_commit_phase_error(&no_arity, &shape).is_none());
498
499        // The UDR/LDR composites fall back to the query-phase error alone.
500        assert_eq!(
501            proven_error_udr(&no_arity, &air, &shape).bits(),
502            query_phase_error(
503                alpha_udr(shape.log_trace_length, no_arity.log_blowup, air.max_combo),
504                no_arity.num_queries,
505                no_arity.query_pow_bits,
506            )
507            .bits()
508        );
509    }
510
511    /// FRI-only Johnson line-MCA vector: k = 2^20, blowup = 8,
512    /// arity = 4, m = 3, commit grinding = 28, query grinding = 17, and
513    /// 81 queries over a conservative 127-bit field. This is an LDT-only
514    /// example, not a complete VM security claim.
515    #[test]
516    fn johnson_line_mca_fri_example_keeps_curve_and_grinding_terms() {
517        let regime = FriRegime {
518            log_blowup: 3,
519            num_queries: 81,
520            log_final_poly_len: 0,
521            max_log_arity: 2,
522            commit_pow_bits: 28,
523            query_pow_bits: 17,
524        };
525        let shape = InstanceShape {
526            log_trace_length: 20,
527            modulus_bits: 127,
528            collision_resistance: 128,
529            num_batched_functions: 1,
530        };
531
532        let commit = commit_phase_error_ldr_m(&regime, &shape, 3)
533            .expect("arity four has folding rounds")
534            .bits();
535        let alpha = alpha_ldr_m(regime.log_blowup, 3);
536        let query = query_phase_error(alpha, regime.num_queries, regime.query_pow_bits).bits();
537        let ldt = proven_error_ldr_m(&regime, &benchmark_air(), &shape, 3).bits();
538        assert!(
539            (ldt - 120.4862138717).abs() < 1e-9,
540            "commit={commit}, query={query}, ldt={ldt}",
541        );
542        assert!((commit - 120.57793385796532).abs() < 1e-9, "got {commit}");
543
544        let ungrounded = FriRegime {
545            commit_pow_bits: 0,
546            ..regime
547        };
548        let ungrounded_commit = commit_phase_error_ldr_m(&ungrounded, &shape, 3)
549            .expect("arity four has folding rounds")
550            .bits();
551        assert!((commit - ungrounded_commit - 28.0).abs() < 1e-9);
552        assert_eq!(
553            query_phase_error(alpha, regime.num_queries, regime.query_pow_bits).bits(),
554            query_phase_error(alpha, ungrounded.num_queries, ungrounded.query_pow_bits).bits()
555        );
556    }
557
558    /// ethSTARK §5.10.1 uses R = 2, zeta = 20, and s = 31/41/55 for
559    /// lambda = 80/100/128. These are the boosted query terms, before the
560    /// paper's pre-query cap and one-bit union-bound loss.
561    #[test]
562    fn legacy_conjectured_error_matches_ethstark_query_vectors() {
563        for (num_queries, expected) in [(31, 82.0), (41, 102.0), (55, 130.0)] {
564            let regime = FriRegime {
565                log_blowup: 2,
566                num_queries,
567                query_pow_bits: 20,
568                ..benchmark_regime()
569            };
570            assert_eq!(legacy_conjectured_error(&regime).bits(), expected);
571        }
572    }
573
574    #[test]
575    fn conjectured_bounded_by_collision_resistance() {
576        let regime = FriRegime {
577            log_blowup: 8,
578            num_queries: 32,
579            log_final_poly_len: 0,
580            max_log_arity: 1,
581            commit_pow_bits: 0,
582            query_pow_bits: 0,
583        };
584        let shape = InstanceShape {
585            log_trace_length: 16,
586            modulus_bits: 128,
587            collision_resistance: 128,
588            num_batched_functions: 1,
589        };
590        let bits = conjectured_error(&regime, &shape)
591            .bits()
592            .min(shape.collision_resistance as f64)
593            .min(shape.modulus_bits as f64)
594            .floor() as usize;
595        assert_eq!(bits, 128);
596    }
597}