1use 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#[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
98pub 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
119pub 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
137pub fn conjectured_commit_phase_error(
151 regime: &FriRegime,
152 shape: &InstanceShape,
153) -> Option<ErrorBits> {
154 commit_phase_error_udr(regime, shape)
155}
156
157pub 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
181pub 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
230pub 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
239pub 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
260pub 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
288pub 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
310pub 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 #[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(®ime, &air, &shape);
380 let (best_m, ldr_ldt) = best_ldr_m(®ime, &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 #[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(®ime, &shape).expect("folds occur");
437 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(®ime, &shape).unwrap().bits()
445 );
446 }
447
448 #[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 assert_eq!(
464 conjectured_error(&ground, &shape).bits(),
465 conjectured_error(&base, &shape).bits()
466 );
467
468 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 #[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 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 #[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(®ime, &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(®ime, &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 #[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(®ime).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(®ime, &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}