Skip to main content

p3_security/
assumption.rs

1//! Security assumption (regime selector) and the Reed–Solomon proximity-gap
2//! primitives that any RS-IOP can share.
3//!
4//! WHIR / STIR composition lives in [`crate::whir`]; FRI's per-regime error
5//! orchestration lives in [`crate::fri`]. Both consume the methods on
6//! [`SecurityAssumption`].
7//!
8//! # References
9//! - **[BCI+20]** Ben-Sasson, Carmon, Ishai, Kopparty, Saraf.
10//!   *Proximity Gaps for Reed-Solomon Codes*. FOCS 2020.
11//!   <https://eprint.iacr.org/2020/654>
12//! - **\[BCSS25\]** Ben-Sasson, Carmon, Haboeck, Kopparty, Saraf.
13//!   *On Proximity Gaps for Reed-Solomon Codes*.
14//!   <https://eprint.iacr.org/2025/2055>
15//! - **\[Hab25\]** Haboeck. *A note on mutual correlated agreement for
16//!   Reed-Solomon codes*. <https://eprint.iacr.org/2025/2110>
17//! - **\[BCGM26\]** Bordage, Chiesa, Guan, Manzur. *All Polynomial Generators
18//!   Preserve Distance with Mutual Correlated Agreement*. CCC 2026.
19//! - **\[DKT26\]** Dao, Kominers, Thaler. *Reed–Solomon Codes Beyond Johnson*.
20//!   Theorem 5.12 and Appendix B.1–B.2.
21//!   <https://eprint.iacr.org/2026/2056>
22//!
23//! \[BCSS25\] improves the Johnson-bound proximity gap from `O(n²/η⁷)` to
24//! `O(n/η⁵)`, enabling 128-bit provable security with degree-5 extensions
25//! of small prime fields (e.g. KoalaBear).
26//! DKT26 Theorem 5.12 gives the line-MCA count used here as
27//! `8·n·(m + 1/2)^3/(3·rho_minus)` at the finite Johnson rate.
28
29use alloc::format;
30use alloc::string::String;
31use core::f64::consts::LOG2_10;
32use core::fmt::Display;
33use core::str::FromStr;
34
35use serde::Serialize;
36
37/// Proximity regime selector for Reed–Solomon-based IOPs.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
39pub enum SecurityAssumption {
40    /// Unique decoding: each oracle is within the UDR. No conjectures.
41    UniqueDecoding,
42
43    /// Johnson bound at `δ = 1 − √ρ − η`, with `η = √ρ / 20`. No conjectures.
44    ///
45    /// Reed-Solomon mutual correlated agreement is proven at this radius in \[Hab25\] and \[BCGM26\].
46    ///
47    /// The line-MCA proximity-gap error uses \[DKT26\] Theorem 5.12 and
48    /// Appendix B.1–B.2: `a < 8·n·(m + 1/2)^3/(3·rho_minus)` exceptional
49    /// challenges, with `rho_minus = (k - 1)/n`. This finite-rate bound is
50    /// the Johnson-regime improvement applied by this crate.
51    JohnsonBound,
52
53    /// Capacity bound at `δ = 1 − ρ − η`, with `η = ρ / 20`. Requires
54    /// conjecturing capacity-rate list decodability and correlated
55    /// agreement up to capacity.
56    CapacityBound,
57}
58
59impl SecurityAssumption {
60    /// `log₂(η)`, where η is the safety gap below the regime's distance.
61    ///
62    /// # Panics
63    /// Undefined for [`SecurityAssumption::UniqueDecoding`] (UD uses
64    /// `δ = (1 − ρ)/2`, no η term). Callers must branch on UD first; the
65    /// panic locks down that invariant.
66    #[must_use]
67    pub const fn log_eta(&self, log_inv_rate: usize) -> f64 {
68        match self {
69            Self::UniqueDecoding => panic!("log_eta is undefined for UniqueDecoding"),
70            // Set as sqrt(rho)/20
71            Self::JohnsonBound => -(0.5 * log_inv_rate as f64 + LOG2_10 + 1.),
72            // Set as rho/20
73            Self::CapacityBound => -(log_inv_rate as f64 + LOG2_10 + 1.),
74        }
75    }
76
77    /// `log₂(η)`, or `0.` for [`Self::UniqueDecoding`], where no eta term applies and every
78    /// `_at_log_eta` formula ignores the value in its UD branch.
79    ///
80    /// Public alongside the `_at_log_eta` family: a caller feeding those its own schedule of
81    /// eta values still needs this convention to reproduce the fixed-eta entry points, and
82    /// has no other way to spell "the regime's default" for `UniqueDecoding`.
83    #[must_use]
84    pub const fn log_eta_or_zero(&self, log_inv_rate: usize) -> f64 {
85        match self {
86            Self::UniqueDecoding => 0.,
87            _ => self.log_eta(log_inv_rate),
88        }
89    }
90
91    /// `log₂(L⁺)` for the regime's list size at an explicit `log_eta`, rather than the
92    /// regime's own default safety margin ([`Self::log_eta`]).
93    ///
94    /// A caller deriving its own schedule of `eta` values across a protocol (STIR's per-round
95    /// bisection, for instance) needs the list size at each of those working points, not just
96    /// at the regime's fixed default; [`Self::list_size_bits`] is the
97    /// `log_eta = self.log_eta(log_inv_rate)` specialization of this.
98    #[must_use]
99    pub const fn list_size_bits_at_log_eta(
100        &self,
101        log_degree: usize,
102        log_inv_rate: usize,
103        log_eta: f64,
104    ) -> f64 {
105        match self {
106            // In UD the list size is 1
107            Self::UniqueDecoding => 0.,
108
109            // By the JB, RS codes are (1 - sqrt(rho) - eta, (2*eta*sqrt(rho))^-1)-list decodable.
110            //
111            // This is the classical Johnson list size, used to grade WHIR and STIR.
112            // `crate::proximity::list_size_ldr_m` grades FRI with the tighter [BCSS25]
113            // Theorem 4.2 bound instead. Both are valid theorems for the same code at a matched
114            // radius, and are never co-evaluated for the same regime; this one is the more
115            // conservative of the two, so the difference is intentional, not a defect to unify.
116            Self::JohnsonBound => {
117                let log_inv_sqrt_rate: f64 = log_inv_rate as f64 / 2.;
118                log_inv_sqrt_rate - (1. + log_eta)
119            }
120
121            // In CB we assume that RS codes are (1 - rho - eta, d/rho*eta)-list decodable (see Conjecture 5.6 in STIR).
122            Self::CapacityBound => (log_degree + log_inv_rate) as f64 - log_eta,
123        }
124    }
125
126    /// `log₂(L⁺)` for the regime's list size at distance δ.
127    #[must_use]
128    pub const fn list_size_bits(&self, log_degree: usize, log_inv_rate: usize) -> f64 {
129        self.list_size_bits_at_log_eta(log_degree, log_inv_rate, self.log_eta_or_zero(log_inv_rate))
130    }
131
132    /// Proximity-gap error in bits for combining `num_functions` functions
133    /// at the regime's distance.
134    ///
135    /// The Johnson-bound branch uses the DKT26 Theorem 5.12 line-MCA bound
136    /// at the fixed safety choice `m = max(ceil(sqrt(rho)/(2*eta)), 3) = 10`
137    /// (η = √ρ/20, see [`Self::log_eta`]). The shared line-count helper
138    /// implements `8·n·(m + 1/2)^3/(3·rho_minus)` from Theorem 5.12 and
139    /// Appendix B.1–B.2, with the finite `rho_minus = (k - 1)/n`.
140    /// Use [`Self::prox_gaps_error_jb_at_m`] when the surrounding regime
141    /// decodes at a different explicit `m` (e.g. FRI's `best_m`) — the
142    /// fixed `m = 10` here is a WHIR-style default, not necessarily the `m`
143    /// the caller's list-decoding regime actually operates at.
144    ///
145    /// # Panics
146    /// Panics when `num_functions < 2`: this API bounds a nontrivial batching
147    /// challenge. Callers must handle empty and singleton batches separately.
148    #[must_use]
149    pub fn prox_gaps_error(
150        &self,
151        log_degree: usize,
152        log_inv_rate: usize,
153        field_size_bits: usize,
154        num_functions: usize,
155    ) -> f64 {
156        self.prox_gaps_error_at_log_eta(
157            log_degree,
158            log_inv_rate,
159            field_size_bits,
160            num_functions,
161            self.log_eta_or_zero(log_inv_rate),
162        )
163    }
164
165    /// [`Self::prox_gaps_error`] at an explicit `log_eta`, rather than the regime's own
166    /// default safety margin ([`Self::log_eta`]).
167    ///
168    /// A caller deriving its own schedule of `eta` values across a protocol (STIR's per-round
169    /// bisection, for instance) needs the proximity-gap error at each of those working
170    /// points, not just at the regime's fixed default. On [`SecurityAssumption::JohnsonBound`]
171    /// this derives the DKT26 proximity parameter `m = max(ceil(√ρ / (2η)), 3)` from
172    /// `log_eta` and defers to [`Self::prox_gaps_error_jb_at_m`] — at
173    /// `log_eta = self.log_eta(log_inv_rate)` that derivation reduces to exactly `m = 10`,
174    /// [`Self::prox_gaps_error`]'s fixed safety choice.
175    ///
176    /// # Panics
177    /// Panics when `num_functions < 2`, as in [`Self::prox_gaps_error`].
178    #[must_use]
179    pub fn prox_gaps_error_at_log_eta(
180        &self,
181        log_degree: usize,
182        log_inv_rate: usize,
183        field_size_bits: usize,
184        num_functions: usize,
185        log_eta: f64,
186    ) -> f64 {
187        assert!(
188            num_functions >= 2,
189            "num_functions must be >= 2 to compute proximity gaps error",
190        );
191
192        match self {
193            // In UD the error is |L|/|F| = d/(rho*|F|)
194            Self::UniqueDecoding => {
195                let error = (log_degree + log_inv_rate) as f64;
196                let num_functions_1_log = libm::log2(num_functions as f64 - 1.);
197                field_size_bits as f64 - (error + num_functions_1_log)
198            }
199
200            // DKT26 Theorem 5.12 and Appendix B.1–B.2, with
201            // m = max(ceil(sqrt(rho)/(2*eta)), 3).
202            Self::JohnsonBound => {
203                let log_sqrt_rho_over_2eta = -(log_inv_rate as f64) / 2. - 1. - log_eta;
204                let m = libm::ceil(libm::pow(2., log_sqrt_rho_over_2eta)).max(3.) as usize;
205                Self::prox_gaps_error_jb_at_m(
206                    log_degree,
207                    log_inv_rate,
208                    field_size_bits,
209                    num_functions,
210                    m,
211                )
212            }
213
214            // In CB we assume the error is degree/(eta*rho^2)
215            Self::CapacityBound => {
216                let error = (log_degree + 2 * log_inv_rate) as f64 - log_eta;
217                let num_functions_1_log = libm::log2(num_functions as f64 - 1.);
218                field_size_bits as f64 - (error + num_functions_1_log)
219            }
220        }
221    }
222
223    /// Johnson-bound line-MCA error at an explicit proximity parameter `m`,
224    /// rather than the fixed `m = 10` safety choice [`Self::prox_gaps_error`]
225    /// uses. DKT26 Theorem 5.12 and Appendix B.1–B.2 give the exceptional
226    /// line count `C = 8·n·(m + 1/2)^3/(3·rho_minus)`. The helper excludes the
227    /// curve-degree factor, so a batch of `num_functions` words contributes
228    /// one factor of `num_functions - 1` here.
229    ///
230    /// For use when the caller already knows the `m` the surrounding
231    /// list-decoding regime decodes at (e.g. FRI's `best_m` from
232    /// [`crate::fri::best_ldr_m`]) and needs the batch-combination term
233    /// evaluated at that same radius rather than the WHIR-style fixed
234    /// safety margin.
235    ///
236    /// # Decoding-radius contract
237    /// The caller must use agreement at least
238    /// `alpha = (1 + 1/(2m)) * sqrt(k/n)`, where `k = 2^log_degree`.
239    /// Writing `rho_minus = (k - 1)/n`, this gives
240    /// `eta0 = alpha - sqrt(rho_minus) >= sqrt(rho_minus)/(2m)`.
241    /// Thus the theorem's minimum `max(3, ceil(sqrt(rho_minus)/(2*eta0)))`
242    /// is at most the supplied `m >= 3`; Appendix B.1's interpolant remains
243    /// valid, and the closed exceptional-count bound grows with `m`.
244    /// Supplying a smaller `m` than the surrounding radius permits is unsound.
245    ///
246    /// Unsupported `m` or inverse rate returns `-∞` security bits through the
247    /// shared helper. The batch-size precondition is checked separately.
248    ///
249    /// # Panics
250    /// Panics when `num_functions < 2`, as in [`Self::prox_gaps_error`].
251    #[must_use]
252    pub fn prox_gaps_error_jb_at_m(
253        log_degree: usize,
254        log_inv_rate: usize,
255        field_size_bits: usize,
256        num_functions: usize,
257        m: usize,
258    ) -> f64 {
259        assert!(
260            num_functions >= 2,
261            "num_functions must be >= 2 to compute proximity gaps error",
262        );
263        let error =
264            crate::proximity::johnson_exceptional_line_count_log2(log_degree, log_inv_rate, m);
265        let num_functions_1_log = libm::log2(num_functions as f64 - 1.);
266        field_size_bits as f64 - (error + num_functions_1_log)
267    }
268
269    /// `log₂(1 − δ)` for the regime's distance δ.
270    /// - UD: δ = (1 − ρ)/2
271    /// - JB: δ = 1 − √ρ − η
272    /// - CB: δ = 1 − ρ − η
273    #[must_use]
274    pub fn log_1_delta(&self, log_inv_rate: usize) -> f64 {
275        let log_twenty_one_over_twenty = libm::log2(21. / 20.);
276        match self {
277            Self::UniqueDecoding => libm::log2(1. + libm::pow(2., -(log_inv_rate as f64))) - 1.,
278            Self::JohnsonBound => log_twenty_one_over_twenty - 0.5 * log_inv_rate as f64,
279            Self::CapacityBound => log_twenty_one_over_twenty - log_inv_rate as f64,
280        }
281    }
282
283    /// Number of queries needed for `(1 − δ)^t < 2^{−λ}`.
284    #[must_use]
285    pub fn queries(&self, protocol_security_level: usize, log_inv_rate: usize) -> usize {
286        let num_queries_f = -(protocol_security_level as f64) / self.log_1_delta(log_inv_rate);
287
288        libm::ceil(num_queries_f) as usize
289    }
290
291    /// Bits of security from `num_queries` queries.
292    #[must_use]
293    pub fn queries_error(&self, log_inv_rate: usize, num_queries: usize) -> f64 {
294        let num_queries = num_queries as f64;
295
296        -num_queries * self.log_1_delta(log_inv_rate)
297    }
298}
299
300impl Display for SecurityAssumption {
301    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
302        f.write_str(match self {
303            Self::JohnsonBound => "JohnsonBound",
304            Self::CapacityBound => "CapacityBound",
305            Self::UniqueDecoding => "UniqueDecoding",
306        })
307    }
308}
309
310impl FromStr for SecurityAssumption {
311    type Err = String;
312
313    fn from_str(s: &str) -> Result<Self, Self::Err> {
314        match s {
315            "JohnsonBound" => Ok(Self::JohnsonBound),
316            "CapacityBound" => Ok(Self::CapacityBound),
317            "UniqueDecoding" => Ok(Self::UniqueDecoding),
318            _ => Err(format!("Invalid soundness specification: {s}")),
319        }
320    }
321}
322
323#[cfg(test)]
324#[allow(clippy::cast_lossless)]
325mod tests {
326    use alloc::string::ToString;
327
328    use super::*;
329
330    /// Field size in bits used by the BCSS25 regression tests.
331    ///
332    /// Equals `5 * ceil(log_2(p_KoalaBear))` with `p_KoalaBear = 2^31 - 2^24 + 1`,
333    /// i.e. a degree-5 extension of the KoalaBear prime field. The smallest
334    /// extension that gives the [BCSS25] bound enough headroom for 128-bit
335    /// WHIR soundness in the regimes tested.
336    pub(crate) const KOALABEAR_QUINTIC_BITS: usize = 155;
337
338    #[test]
339    fn test_soundness_type_display() {
340        assert_eq!(SecurityAssumption::JohnsonBound.to_string(), "JohnsonBound");
341        assert_eq!(
342            SecurityAssumption::CapacityBound.to_string(),
343            "CapacityBound"
344        );
345        assert_eq!(
346            SecurityAssumption::UniqueDecoding.to_string(),
347            "UniqueDecoding"
348        );
349    }
350
351    #[test]
352    fn test_soundness_type_from_str() {
353        assert_eq!(
354            SecurityAssumption::from_str("JohnsonBound"),
355            Ok(SecurityAssumption::JohnsonBound)
356        );
357        assert_eq!(
358            SecurityAssumption::from_str("CapacityBound"),
359            Ok(SecurityAssumption::CapacityBound)
360        );
361        assert_eq!(
362            SecurityAssumption::from_str("UniqueDecoding"),
363            Ok(SecurityAssumption::UniqueDecoding)
364        );
365
366        // Invalid cases
367        assert!(SecurityAssumption::from_str("InvalidType").is_err());
368        assert!(SecurityAssumption::from_str("").is_err()); // Empty string
369    }
370
371    #[test]
372    #[should_panic(expected = "num_functions must be >= 2")]
373    fn prox_gaps_error_panics_when_num_functions_is_one() {
374        let assumption = SecurityAssumption::UniqueDecoding;
375        let _ = assumption.prox_gaps_error(1, 1, 64, 1);
376    }
377
378    #[test]
379    #[should_panic(expected = "num_functions must be >= 2")]
380    fn prox_gaps_error_panics_when_num_functions_is_zero() {
381        let assumption = SecurityAssumption::UniqueDecoding;
382        let _ = assumption.prox_gaps_error(1, 1, 64, 0);
383    }
384
385    #[test]
386    #[should_panic(expected = "num_functions must be >= 2")]
387    fn explicit_johnson_gap_rejects_a_singleton_batch() {
388        let _ = SecurityAssumption::prox_gaps_error_jb_at_m(1, 1, 64, 1, 3);
389    }
390
391    #[test]
392    #[should_panic(expected = "num_functions must be >= 2")]
393    fn explicit_johnson_gap_rejects_an_empty_batch() {
394        let _ = SecurityAssumption::prox_gaps_error_jb_at_m(1, 1, 64, 0, 3);
395    }
396
397    #[test]
398    fn test_ud_errors() {
399        let assumption = SecurityAssumption::UniqueDecoding;
400
401        // Setting
402        let log_degree = 20;
403        let degree = (1 << log_degree) as f64;
404        let log_inv_rate = 2;
405        let rate = 1. / (1 << log_inv_rate) as f64;
406
407        let field_size_bits = 128;
408
409        // List size
410        assert!(assumption.list_size_bits(log_degree, log_inv_rate) - 0. < 0.01);
411
412        // Prox gaps
413        let computed_error =
414            assumption.prox_gaps_error(log_degree, log_inv_rate, field_size_bits, 2);
415        let real_error_non_log = degree / rate;
416        let real_error = field_size_bits as f64 - real_error_non_log.log2();
417
418        assert!((computed_error - real_error).abs() < 0.01);
419    }
420
421    #[test]
422    fn test_jb_errors() {
423        let assumption = SecurityAssumption::JohnsonBound;
424
425        // Setting
426        let log_degree = 20;
427        let log_inv_rate = 2;
428        let rate = 1. / (1 << log_inv_rate) as f64;
429
430        let eta = rate.sqrt() / 20.;
431
432        let field_size_bits = 128;
433
434        // List size
435        let real_list_size = 1. / (2. * eta * rate.sqrt());
436        let computed_list_size = assumption.list_size_bits(log_degree, log_inv_rate);
437        assert!((real_list_size.log2() - computed_list_size).abs() < 0.01);
438
439        // Proximity gaps use DKT26 Theorem 5.12 and Appendix B.1–B.2.
440        // At eta = sqrt(rho)/20, m = 10, the line count is
441        //   C = 8 * n * (m + 1/2)^3 / (3 * rho_minus),
442        // where rho_minus = (2^log_degree - 1) / n.
443        let computed_error =
444            assumption.prox_gaps_error(log_degree, log_inv_rate, field_size_bits, 2);
445
446        // n = 2^(log_degree + log_inv_rate) = 2^22
447        let n = (1_u64 << (log_degree + log_inv_rate)) as f64;
448        let rho_minus = ((1_u64 << log_degree) - 1) as f64 / n;
449        let real_error_non_log = 8. * n * 10.5_f64.powi(3) / (3. * rho_minus);
450        let real_error = field_size_bits as f64 - real_error_non_log.log2();
451
452        assert!(
453            (computed_error - real_error).abs() < 0.01,
454            "computed: {computed_error}, expected: {real_error}"
455        );
456    }
457
458    #[test]
459    fn test_cb_errors() {
460        let assumption = SecurityAssumption::CapacityBound;
461
462        // Setting
463        let log_degree = 20;
464        let degree = (1 << log_degree) as f64;
465        let log_inv_rate = 2;
466        let rate = 1. / (1 << log_inv_rate) as f64;
467
468        let eta = rate / 20.;
469
470        let field_size_bits = 128;
471
472        // List size
473        let real_list_size = degree / (rate * eta);
474        let computed_list_size = assumption.list_size_bits(log_degree, log_inv_rate);
475        assert!((real_list_size.log2() - computed_list_size).abs() < 0.01);
476
477        // Prox gaps
478        let computed_error =
479            assumption.prox_gaps_error(log_degree, log_inv_rate, field_size_bits, 2);
480        let real_error_non_log = degree / (eta * rate.powi(2));
481        let real_error = field_size_bits as f64 - real_error_non_log.log2();
482
483        assert!((computed_error - real_error).abs() < 0.01);
484    }
485
486    #[test]
487    #[should_panic(expected = "log_eta is undefined for UniqueDecoding")]
488    fn log_eta_panics_for_unique_decoding() {
489        // eta does not appear in the UD distance formula `delta = (1 - rho) / 2`.
490        // Reading log_eta in the UD branch is a programmer error; the panic
491        // locks that down so a future refactor that strays into the eta path
492        // under UD fails loudly instead of silently propagating a bogus value.
493        let _ = SecurityAssumption::UniqueDecoding.log_eta(5);
494    }
495
496    #[test]
497    fn log_one_minus_delta_is_stable_at_large_inverse_rates() {
498        let log_twenty_one_over_twenty = libm::log2(21. / 20.);
499
500        for log_inv_rate in [31, 32, 63] {
501            let unique = SecurityAssumption::UniqueDecoding.log_1_delta(log_inv_rate);
502            let johnson = SecurityAssumption::JohnsonBound.log_1_delta(log_inv_rate);
503            let capacity = SecurityAssumption::CapacityBound.log_1_delta(log_inv_rate);
504
505            assert!(unique.is_finite());
506            assert!(johnson.is_finite());
507            assert!(capacity.is_finite());
508            assert!(
509                (unique - (libm::log2(1. + libm::pow(2., -(log_inv_rate as f64))) - 1.)).abs()
510                    < 1e-12
511            );
512            assert!(
513                (johnson - (log_twenty_one_over_twenty - 0.5 * log_inv_rate as f64)).abs() < 1e-12
514            );
515            assert!((capacity - (log_twenty_one_over_twenty - log_inv_rate as f64)).abs() < 1e-12);
516        }
517    }
518
519    /// Old prox-gap baseline used by the improvement test.
520    ///
521    /// [BCI+20] Theorem 5.1 at η = √ρ/20 (m = 10):
522    /// `|S| > (m + 1/2)^7 / 3 · n^2 / ρ^{3/2}`.
523    fn bci20_jb_prox_gaps_error(
524        log_degree: usize,
525        log_inv_rate: usize,
526        field_size_bits: usize,
527    ) -> f64 {
528        const M_PLUS_HALF: f64 = 10.5;
529        let log_n_squared = 2.0 * (log_degree + log_inv_rate) as f64;
530        let log_leading_constant = libm::log2(libm::pow(M_PLUS_HALF, 7.0) / 3.0);
531        let log_rho_pow_neg_three_halves = 1.5 * log_inv_rate as f64;
532        let error_bits = log_n_squared + log_leading_constant + log_rho_pow_neg_three_halves;
533        field_size_bits as f64 - error_bits
534    }
535
536    #[test]
537    fn jb_prox_gap_strictly_improves_over_old_bound() {
538        // Compare the DKT26 line count against the [BCI+20] baseline at the
539        // same finite degree and nominal rate.
540        let jb = SecurityAssumption::JohnsonBound;
541        for log_degree in 10..=25 {
542            for log_inv_rate in 1..=4 {
543                let new_bits =
544                    jb.prox_gaps_error(log_degree, log_inv_rate, KOALABEAR_QUINTIC_BITS, 2);
545                let old_bits =
546                    bci20_jb_prox_gaps_error(log_degree, log_inv_rate, KOALABEAR_QUINTIC_BITS);
547
548                assert!(
549                    new_bits > old_bits,
550                    "no improvement at log_degree={log_degree}, log_inv_rate={log_inv_rate}: \
551                     new={new_bits:.4}, old={old_bits:.4}"
552                );
553
554                let log_n = (log_degree + log_inv_rate) as f64;
555                let rho = 2.0_f64.powi(-(log_inv_rate as i32));
556                let rho_minus = rho * (1.0 - 2.0_f64.powi(-(log_degree as i32)));
557                let new_count_log =
558                    libm::log2(8.0 * 2.0_f64.powf(log_n) * 10.5_f64.powi(3) / (3.0 * rho_minus));
559                let old_count_log =
560                    libm::log2(10.5_f64.powi(7) / 3.0) + 2.0 * log_n + 1.5 * log_inv_rate as f64;
561                let observed = new_bits - old_bits;
562                let expected = old_count_log - new_count_log;
563
564                assert!(
565                    (observed - expected).abs() < 1e-9,
566                    "gap mismatch at log_degree={log_degree}, log_inv_rate={log_inv_rate}: \
567                     expected={expected:.6}, got={observed:.6}"
568                );
569            }
570        }
571    }
572
573    #[test]
574    fn jb_prox_gap_scales_by_log_curve_degree() {
575        // [BCSS25] Thm 4.2: combining M+1 functions costs log_2(M) bits.
576        let jb = SecurityAssumption::JohnsonBound;
577        let log_degree = 20;
578        let log_inv_rate = 2;
579
580        let line_bits = jb.prox_gaps_error(log_degree, log_inv_rate, KOALABEAR_QUINTIC_BITS, 2);
581
582        for (num_functions, expected_loss) in [(3_usize, 1.0_f64), (5, 2.0), (9, 3.0)] {
583            let curve_bits = jb.prox_gaps_error(
584                log_degree,
585                log_inv_rate,
586                KOALABEAR_QUINTIC_BITS,
587                num_functions,
588            );
589
590            let loss = line_bits - curve_bits;
591
592            assert!(
593                (loss - expected_loss).abs() < 1e-9,
594                "curve scaling off at num_functions={num_functions}: \
595                 expected log_2({}) = {expected_loss:.1} bits, got {loss:.6}",
596                num_functions - 1
597            );
598        }
599    }
600
601    /// Field size used by the known-answer table below: a degree-5 extension of KoalaBear.
602    const TABLE_FIELD_BITS: usize = 155;
603
604    #[test]
605    fn fixed_eta_formulas_match_their_recorded_values() {
606        // These five entry points feed WHIR's, FRI's and STIR's derived parameters, so any
607        // drift in them silently changes every protocol's security. Freezing the values is
608        // the point: a property test would need the pre-refactor implementation as its
609        // oracle, and that no longer exists.
610        //
611        // Columns: list_size_bits, prox_gaps_error(num_functions = 17),
612        // ood_error(ood_samples = 2), fold_sumcheck_error,
613        // queries_combination_error(ood_samples = 2, num_queries = 40).
614        #[allow(clippy::type_complexity)]
615        let table: [(SecurityAssumption, usize, usize, [f64; 5]); 45] = [
616            (
617                SecurityAssumption::JohnsonBound,
618                1,
619                10,
620                [
621                    4.321928094887362,
622                    127.4066006621302,
623                    282.35614381022526,
624                    149.67807190511263,
625                    144.28575448233389,
626                ],
627            ),
628            (
629                SecurityAssumption::JohnsonBound,
630                1,
631                20,
632                [
633                    4.321928094887362,
634                    117.40800885652301,
635                    262.35614381022526,
636                    149.67807190511263,
637                    144.28575448233389,
638                ],
639            ),
640            (
641                SecurityAssumption::JohnsonBound,
642                1,
643                24,
644                [
645                    4.321928094887362,
646                    113.40801014639354,
647                    254.35614381022526,
648                    149.67807190511263,
649                    144.28575448233389,
650                ],
651            ),
652            (
653                SecurityAssumption::JohnsonBound,
654                2,
655                10,
656                [
657                    5.321928094887362,
658                    125.4066006621302,
659                    280.35614381022526,
660                    148.67807190511263,
661                    143.28575448233389,
662                ],
663            ),
664            (
665                SecurityAssumption::JohnsonBound,
666                2,
667                20,
668                [
669                    5.321928094887362,
670                    115.40800885652301,
671                    260.35614381022526,
672                    148.67807190511263,
673                    143.28575448233389,
674                ],
675            ),
676            (
677                SecurityAssumption::JohnsonBound,
678                2,
679                24,
680                [
681                    5.321928094887362,
682                    111.40801014639355,
683                    252.35614381022526,
684                    148.67807190511263,
685                    143.28575448233389,
686                ],
687            ),
688            (
689                SecurityAssumption::JohnsonBound,
690                3,
691                10,
692                [
693                    6.321928094887362,
694                    123.4066006621302,
695                    278.35614381022526,
696                    147.67807190511263,
697                    142.28575448233389,
698                ],
699            ),
700            (
701                SecurityAssumption::JohnsonBound,
702                3,
703                20,
704                [
705                    6.321928094887362,
706                    113.40800885652301,
707                    258.35614381022526,
708                    147.67807190511263,
709                    142.28575448233389,
710                ],
711            ),
712            (
713                SecurityAssumption::JohnsonBound,
714                3,
715                24,
716                [
717                    6.321928094887362,
718                    109.40801014639355,
719                    250.35614381022526,
720                    147.67807190511263,
721                    142.28575448233389,
722                ],
723            ),
724            (
725                SecurityAssumption::JohnsonBound,
726                4,
727                10,
728                [
729                    7.321928094887362,
730                    121.4066006621302,
731                    276.35614381022526,
732                    146.67807190511263,
733                    141.28575448233389,
734                ],
735            ),
736            (
737                SecurityAssumption::JohnsonBound,
738                4,
739                20,
740                [
741                    7.321928094887362,
742                    111.40800885652301,
743                    256.35614381022526,
744                    146.67807190511263,
745                    141.28575448233389,
746                ],
747            ),
748            (
749                SecurityAssumption::JohnsonBound,
750                4,
751                24,
752                [
753                    7.321928094887362,
754                    107.40801014639355,
755                    248.35614381022526,
756                    146.67807190511263,
757                    141.28575448233389,
758                ],
759            ),
760            (
761                SecurityAssumption::JohnsonBound,
762                8,
763                10,
764                [
765                    11.321928094887362,
766                    113.4066006621302,
767                    268.35614381022526,
768                    142.67807190511263,
769                    137.28575448233389,
770                ],
771            ),
772            (
773                SecurityAssumption::JohnsonBound,
774                8,
775                20,
776                [
777                    11.321928094887362,
778                    103.40800885652301,
779                    248.35614381022526,
780                    142.67807190511263,
781                    137.28575448233389,
782                ],
783            ),
784            (
785                SecurityAssumption::JohnsonBound,
786                8,
787                24,
788                [
789                    11.321928094887362,
790                    99.40801014639355,
791                    240.35614381022526,
792                    142.67807190511263,
793                    137.28575448233389,
794                ],
795            ),
796            (
797                SecurityAssumption::CapacityBound,
798                1,
799                10,
800                [
801                    16.32192809488736,
802                    133.67807190511263,
803                    258.35614381022526,
804                    137.67807190511263,
805                    132.28575448233389,
806                ],
807            ),
808            (
809                SecurityAssumption::CapacityBound,
810                1,
811                20,
812                [
813                    26.32192809488736,
814                    123.67807190511263,
815                    218.35614381022526,
816                    127.67807190511263,
817                    122.28575448233389,
818                ],
819            ),
820            (
821                SecurityAssumption::CapacityBound,
822                1,
823                24,
824                [
825                    30.32192809488736,
826                    119.67807190511263,
827                    202.35614381022526,
828                    123.67807190511263,
829                    118.28575448233389,
830                ],
831            ),
832            (
833                SecurityAssumption::CapacityBound,
834                2,
835                10,
836                [
837                    18.32192809488736,
838                    130.67807190511263,
839                    254.35614381022526,
840                    135.67807190511263,
841                    130.28575448233389,
842                ],
843            ),
844            (
845                SecurityAssumption::CapacityBound,
846                2,
847                20,
848                [
849                    28.32192809488736,
850                    120.67807190511263,
851                    214.35614381022526,
852                    125.67807190511263,
853                    120.28575448233389,
854                ],
855            ),
856            (
857                SecurityAssumption::CapacityBound,
858                2,
859                24,
860                [
861                    32.32192809488736,
862                    116.67807190511263,
863                    198.35614381022526,
864                    121.67807190511263,
865                    116.28575448233389,
866                ],
867            ),
868            (
869                SecurityAssumption::CapacityBound,
870                3,
871                10,
872                [
873                    20.32192809488736,
874                    127.67807190511263,
875                    250.35614381022526,
876                    133.67807190511263,
877                    128.28575448233389,
878                ],
879            ),
880            (
881                SecurityAssumption::CapacityBound,
882                3,
883                20,
884                [
885                    30.32192809488736,
886                    117.67807190511263,
887                    210.35614381022526,
888                    123.67807190511263,
889                    118.28575448233389,
890                ],
891            ),
892            (
893                SecurityAssumption::CapacityBound,
894                3,
895                24,
896                [
897                    34.32192809488736,
898                    113.67807190511263,
899                    194.35614381022526,
900                    119.67807190511263,
901                    114.28575448233389,
902                ],
903            ),
904            (
905                SecurityAssumption::CapacityBound,
906                4,
907                10,
908                [
909                    22.32192809488736,
910                    124.67807190511263,
911                    246.35614381022526,
912                    131.67807190511263,
913                    126.28575448233389,
914                ],
915            ),
916            (
917                SecurityAssumption::CapacityBound,
918                4,
919                20,
920                [
921                    32.32192809488736,
922                    114.67807190511263,
923                    206.35614381022526,
924                    121.67807190511263,
925                    116.28575448233389,
926                ],
927            ),
928            (
929                SecurityAssumption::CapacityBound,
930                4,
931                24,
932                [
933                    36.32192809488736,
934                    110.67807190511263,
935                    190.35614381022526,
936                    117.67807190511263,
937                    112.28575448233389,
938                ],
939            ),
940            (
941                SecurityAssumption::CapacityBound,
942                8,
943                10,
944                [
945                    30.32192809488736,
946                    112.67807190511263,
947                    230.35614381022526,
948                    123.67807190511263,
949                    118.28575448233389,
950                ],
951            ),
952            (
953                SecurityAssumption::CapacityBound,
954                8,
955                20,
956                [
957                    40.32192809488736,
958                    102.67807190511263,
959                    190.35614381022526,
960                    113.67807190511263,
961                    108.28575448233389,
962                ],
963            ),
964            (
965                SecurityAssumption::CapacityBound,
966                8,
967                24,
968                [
969                    44.32192809488736,
970                    98.67807190511263,
971                    174.35614381022526,
972                    109.67807190511263,
973                    104.28575448233389,
974                ],
975            ),
976            (
977                SecurityAssumption::UniqueDecoding,
978                1,
979                10,
980                [0.0, 140.0, 0.0, 154.0, 148.60768257722123],
981            ),
982            (
983                SecurityAssumption::UniqueDecoding,
984                1,
985                20,
986                [0.0, 130.0, 0.0, 154.0, 148.60768257722123],
987            ),
988            (
989                SecurityAssumption::UniqueDecoding,
990                1,
991                24,
992                [0.0, 126.0, 0.0, 154.0, 148.60768257722123],
993            ),
994            (
995                SecurityAssumption::UniqueDecoding,
996                2,
997                10,
998                [0.0, 139.0, 0.0, 154.0, 148.60768257722123],
999            ),
1000            (
1001                SecurityAssumption::UniqueDecoding,
1002                2,
1003                20,
1004                [0.0, 129.0, 0.0, 154.0, 148.60768257722123],
1005            ),
1006            (
1007                SecurityAssumption::UniqueDecoding,
1008                2,
1009                24,
1010                [0.0, 125.0, 0.0, 154.0, 148.60768257722123],
1011            ),
1012            (
1013                SecurityAssumption::UniqueDecoding,
1014                3,
1015                10,
1016                [0.0, 138.0, 0.0, 154.0, 148.60768257722123],
1017            ),
1018            (
1019                SecurityAssumption::UniqueDecoding,
1020                3,
1021                20,
1022                [0.0, 128.0, 0.0, 154.0, 148.60768257722123],
1023            ),
1024            (
1025                SecurityAssumption::UniqueDecoding,
1026                3,
1027                24,
1028                [0.0, 124.0, 0.0, 154.0, 148.60768257722123],
1029            ),
1030            (
1031                SecurityAssumption::UniqueDecoding,
1032                4,
1033                10,
1034                [0.0, 137.0, 0.0, 154.0, 148.60768257722123],
1035            ),
1036            (
1037                SecurityAssumption::UniqueDecoding,
1038                4,
1039                20,
1040                [0.0, 127.0, 0.0, 154.0, 148.60768257722123],
1041            ),
1042            (
1043                SecurityAssumption::UniqueDecoding,
1044                4,
1045                24,
1046                [0.0, 123.0, 0.0, 154.0, 148.60768257722123],
1047            ),
1048            (
1049                SecurityAssumption::UniqueDecoding,
1050                8,
1051                10,
1052                [0.0, 133.0, 0.0, 154.0, 148.60768257722123],
1053            ),
1054            (
1055                SecurityAssumption::UniqueDecoding,
1056                8,
1057                20,
1058                [0.0, 123.0, 0.0, 154.0, 148.60768257722123],
1059            ),
1060            (
1061                SecurityAssumption::UniqueDecoding,
1062                8,
1063                24,
1064                [0.0, 119.0, 0.0, 154.0, 148.60768257722123],
1065            ),
1066        ];
1067
1068        for (assumption, log_inv_rate, log_degree, expected) in table {
1069            let actual = [
1070                assumption.list_size_bits(log_degree, log_inv_rate),
1071                assumption.prox_gaps_error(log_degree, log_inv_rate, TABLE_FIELD_BITS, 17),
1072                assumption.ood_error(log_degree, log_inv_rate, TABLE_FIELD_BITS, 2),
1073                assumption.fold_sumcheck_error(TABLE_FIELD_BITS, log_degree, log_inv_rate),
1074                assumption.queries_combination_error(
1075                    TABLE_FIELD_BITS,
1076                    log_degree,
1077                    log_inv_rate,
1078                    2,
1079                    40,
1080                ),
1081            ];
1082            assert_eq!(
1083                actual, expected,
1084                "{assumption:?} log_inv_rate={log_inv_rate} log_degree={log_degree}"
1085            );
1086        }
1087    }
1088
1089    #[test]
1090    fn jb_prox_gaps_default_eta_is_m_10() {
1091        // `prox_gaps_error`'s Johnson branch delegates to `prox_gaps_error_jb_at_m` through a
1092        // `f64 -> usize` round-trip whose input lands one ULP *below* 10 — on the correct side
1093        // of the `ceil`, but only just. Reassociating the `log_eta` expression could push the
1094        // residue the other way, making `m = 11` and moving every JB proximity-gap error by
1095        // about 0.39 bits, with nothing else in the suite noticing.
1096        let jb = SecurityAssumption::JohnsonBound;
1097        for log_inv_rate in 0..=32 {
1098            for log_degree in [10, 20, 24] {
1099                for num_functions in [2, 3, 17] {
1100                    assert_eq!(
1101                        jb.prox_gaps_error(log_degree, log_inv_rate, 128, num_functions),
1102                        SecurityAssumption::prox_gaps_error_jb_at_m(
1103                            log_degree,
1104                            log_inv_rate,
1105                            128,
1106                            num_functions,
1107                            10,
1108                        ),
1109                        "log_inv_rate={log_inv_rate} log_degree={log_degree} \
1110                         num_functions={num_functions}"
1111                    );
1112                }
1113            }
1114        }
1115    }
1116}