1use std::f64;
18
19use crate::{
20 config::{ProximityRegime, SystemParams, WhirConfig},
21 WhirProximityStrategy,
22};
23
24#[derive(Clone, Debug)]
25pub struct SoundnessCalculator {
26 pub logup_bits: f64,
28 pub gkr_sumcheck_bits: f64,
30 pub gkr_batching_bits: f64,
32 pub zerocheck_sumcheck_bits: f64,
34 pub constraint_batching_bits: f64,
36 pub stacked_reduction_bits: f64,
38 pub whir_bits: f64,
40 pub whir_details: WhirSoundnessCalculator,
42 pub total_bits: f64,
44}
45
46#[derive(Clone, Debug)]
48pub struct WhirSoundnessCalculator {
49 pub mu_batching_bits: f64,
51 pub fold_rbr_bits: f64,
53 pub proximity_gaps_bits: f64,
55 pub sumcheck_bits: f64,
57 pub ood_rbr_bits: f64,
59 pub shift_rbr_bits: f64,
61 pub query_bits: f64,
63 pub gamma_batching_bits: f64,
65}
66
67#[derive(Clone, Debug)]
68pub struct ProximityGapSecurity {
69 pub log2_err: f64,
70 pub log2_list_size: f64,
71}
72
73impl SoundnessCalculator {
74 #[allow(clippy::too_many_arguments)]
91 pub fn calculate(
92 params: &SystemParams,
93 base_field_order: f64,
94 challenge_field_bits: f64,
95 max_num_constraints_per_air: usize,
96 num_airs: usize,
97 max_constraint_degree: usize,
98 max_log_trace_height: usize,
99 num_trace_columns: usize,
100 num_stacked_columns: usize,
101 n_logup: usize,
102 ) -> Self {
103 let init_prox_gap = Self::whir_proximity_gap_security(
104 params.whir.proximity.initial_round(),
105 challenge_field_bits,
106 params.log_stacked_height(),
107 params.log_blowup,
108 num_stacked_columns,
109 );
110 let log2_list_size = init_prox_gap.log2_list_size;
113 let logup_bits = Self::logup_soundness(
114 params.logup.max_interaction_count,
115 params.logup.log_max_message_length,
116 challenge_field_bits,
117 log2_list_size,
118 ) + Self::effective_pow_bits(params.logup.pow_bits, base_field_order);
119
120 let gkr_sumcheck_bits =
121 Self::calculate_gkr_sumcheck_soundness(challenge_field_bits, params.l_skip, n_logup);
122
123 let gkr_batching_bits =
124 Self::calculate_gkr_batching_soundness(challenge_field_bits, params.l_skip, n_logup);
125
126 let zerocheck_sumcheck_bits = Self::calculate_zerocheck_sumcheck_soundness(
127 challenge_field_bits,
128 max_constraint_degree,
129 params.l_skip,
130 log2_list_size,
131 );
132
133 let constraint_batching_bits = Self::calculate_constraint_batching_soundness(
134 challenge_field_bits,
135 max_num_constraints_per_air,
136 num_airs,
137 params.l_skip,
138 max_log_trace_height,
139 n_logup,
140 log2_list_size,
141 );
142
143 let stacked_reduction_bits = Self::calculate_stacked_reduction_soundness(
144 challenge_field_bits,
145 num_trace_columns,
146 params.l_skip,
147 params.n_stack,
148 log2_list_size,
149 );
150
151 let (whir_bits, whir_details) = Self::calculate_whir_soundness(
152 params,
153 base_field_order,
154 challenge_field_bits,
155 num_stacked_columns,
156 params.whir.proximity,
157 );
158
159 let total_bits = logup_bits
160 .min(gkr_sumcheck_bits)
161 .min(gkr_batching_bits)
162 .min(zerocheck_sumcheck_bits)
163 .min(constraint_batching_bits)
164 .min(stacked_reduction_bits)
165 .min(whir_bits);
166
167 Self {
168 logup_bits,
169 gkr_sumcheck_bits,
170 gkr_batching_bits,
171 zerocheck_sumcheck_bits,
172 constraint_batching_bits,
173 stacked_reduction_bits,
174 whir_bits,
175 whir_details,
176 total_bits,
177 }
178 }
179
180 pub fn logup_soundness(
196 max_interaction_count: u32,
197 log_max_message_length: u32,
198 challenge_field_bits: f64,
199 log2_list_size: f64,
200 ) -> f64 {
201 challenge_field_bits
202 - (2.0 * max_interaction_count as f64).log2()
203 - log_max_message_length as f64
204 - log2_list_size
205 }
206
207 fn calculate_gkr_sumcheck_soundness(
216 challenge_field_bits: f64,
217 l_skip: usize,
218 n_logup: usize,
219 ) -> f64 {
220 let total_rounds = l_skip + n_logup;
221 assert!(total_rounds >= 1, "GKR requires at least 1 round");
222
223 let degree_per_subround = 3;
225 challenge_field_bits - (degree_per_subround as f64).log2()
226 }
227
228 fn calculate_gkr_batching_soundness(
236 challenge_field_bits: f64,
237 _l_skip: usize,
238 _n_logup: usize,
239 ) -> f64 {
240 let degree = 1;
242 challenge_field_bits - (degree as f64).log2()
243 }
244
245 fn calculate_zerocheck_sumcheck_soundness(
260 challenge_field_bits: f64,
261 max_constraint_degree: usize,
262 l_skip: usize,
263 log2_list_size: f64,
264 ) -> f64 {
265 let univariate_degree = (max_constraint_degree + 1) * ((1 << l_skip) - 1);
266 let multilinear_degree = max_constraint_degree + 1;
267
268 let worst_degree = univariate_degree.max(multilinear_degree);
269 challenge_field_bits - (worst_degree as f64).log2() - log2_list_size
270 }
271
272 fn calculate_constraint_batching_soundness(
283 challenge_field_bits: f64,
284 max_num_constraints_per_air: usize,
285 num_airs: usize,
286 l_skip: usize,
287 max_log_trace_height: usize,
288 n_logup: usize,
289 log2_list_size: f64,
290 ) -> f64 {
291 assert!(
292 max_num_constraints_per_air > 0,
293 "batch-constraint soundness requires at least one constraint"
294 );
295 assert!(
296 num_airs > 0,
297 "batch-constraint soundness requires at least one nonempty AIR"
298 );
299
300 let n_trace = max_log_trace_height.saturating_sub(l_skip);
301 let n_extra = n_trace.saturating_sub(n_logup);
302 let skip_degree = (1 << l_skip) - 1;
303
304 let fused_boundary_degree =
309 n_extra.max(3) + skip_degree + (max_num_constraints_per_air - 1);
310 let batch_sumcheck_batching_degree = 3 * num_airs - 1;
311
312 let fused_boundary_bits = challenge_field_bits - (fused_boundary_degree as f64).log2();
313 let batch_sumcheck_batching_bits =
314 challenge_field_bits - (batch_sumcheck_batching_degree as f64).log2();
315
316 fused_boundary_bits.min(batch_sumcheck_batching_bits) - log2_list_size
317 }
318
319 fn calculate_stacked_reduction_soundness(
331 challenge_field_bits: f64,
332 num_trace_columns: usize,
333 l_skip: usize,
334 _n_stack: usize,
335 log2_list_size: f64,
336 ) -> f64 {
337 let batching_bits = challenge_field_bits - (2.0 * num_trace_columns as f64).log2();
338
339 let univariate_degree = 2 * ((1 << l_skip) - 1);
340 let univariate_bits = challenge_field_bits - (univariate_degree as f64).log2();
341
342 let multilinear_bits = challenge_field_bits - 1.0;
344
345 batching_bits.min(univariate_bits).min(multilinear_bits) - log2_list_size
346 }
347
348 fn calculate_whir_soundness(
356 params: &SystemParams,
357 base_field_order: f64,
358 challenge_field_bits: f64,
359 num_stacked_columns: usize,
360 proximity: WhirProximityStrategy,
361 ) -> (f64, WhirSoundnessCalculator) {
362 let whir = ¶ms.whir;
363 let k_whir = whir.k;
364 let log_stacked_height = params.log_stacked_height();
365 let num_whir_rounds = whir.rounds.len();
366
367 let mut min_query_bits = f64::INFINITY;
368 let mut min_prox_gaps_bits = f64::INFINITY;
369 let mut min_sumcheck_bits = f64::INFINITY;
370 let mut min_ood_bits = f64::INFINITY;
371 let mut min_gamma_batching_bits = f64::INFINITY;
372 let mut min_fold_rbr_bits = f64::INFINITY;
373 let mut min_shift_rbr_bits = f64::INFINITY;
374
375 assert!(
376 num_stacked_columns >= 2,
377 "WHIR requires at least 2 stacked columns for μ batching"
378 );
379 let mu_security = Self::whir_proximity_gap_security(
380 proximity.initial_round(),
381 challenge_field_bits,
382 log_stacked_height,
383 params.log_blowup,
384 num_stacked_columns,
385 );
386 let mu_batching_bits =
387 mu_security.log2_err + Self::effective_pow_bits(whir.mu_pow_bits, base_field_order);
388 let mut min_rbr_bits = mu_batching_bits;
389
390 let mut log_inv_rate = params.log_blowup;
391 let mut current_log_degree = log_stacked_height;
392
393 for (round, round_config) in whir.rounds.iter().enumerate() {
394 let proximity_regime = proximity.in_round(round);
395 let is_final_round = round == num_whir_rounds - 1;
396 let next_rate = log_inv_rate + (k_whir - 1);
397 let mut log2_list_size: Option<f64> = None;
400
401 for _ in 0..k_whir {
402 current_log_degree -= 1;
403
404 let prox_gaps = Self::whir_proximity_gap_security(
405 proximity_regime,
406 challenge_field_bits,
407 current_log_degree,
408 log_inv_rate,
409 2,
410 );
411 if let Some(l2) = log2_list_size.as_ref() {
412 debug_assert!((*l2 - prox_gaps.log2_list_size).abs() < 1e-6);
413 } else {
414 log2_list_size = Some(prox_gaps.log2_list_size);
415 }
416 let prox_gaps_bits = prox_gaps.log2_err
417 + Self::effective_pow_bits(whir.folding_pow_bits, base_field_order);
418 min_prox_gaps_bits = min_prox_gaps_bits.min(prox_gaps_bits);
419
420 let sumcheck_bits = Self::whir_sumcheck_security(
421 base_field_order,
422 challenge_field_bits,
423 whir.folding_pow_bits,
424 log2_list_size.unwrap(),
425 );
426 min_sumcheck_bits = min_sumcheck_bits.min(sumcheck_bits);
427
428 let fold_rbr_bits = Self::combine_security_bits(sumcheck_bits, prox_gaps_bits);
430 min_fold_rbr_bits = min_fold_rbr_bits.min(fold_rbr_bits);
431 min_rbr_bits = min_rbr_bits.min(fold_rbr_bits);
432 }
433
434 let log_query_domain = current_log_degree + log_inv_rate;
441 let query_bits =
442 Self::whir_query_security_biased(
443 proximity_regime,
444 round_config.num_queries,
445 log_inv_rate,
446 log_query_domain,
447 base_field_order,
448 ) + Self::effective_pow_bits(whir.query_phase_pow_bits, base_field_order);
449 min_query_bits = min_query_bits.min(query_bits);
450
451 let next_log2_list_size = Self::whir_proximity_gap_security(
454 proximity.in_round(round + 1),
455 challenge_field_bits,
456 current_log_degree,
457 next_rate,
458 2,
459 )
460 .log2_list_size;
461 const NUM_OOD_SAMPLES: usize = 1;
464 let batch_size = round_config.num_queries + NUM_OOD_SAMPLES;
465 debug_assert!(batch_size > 0);
466 let gamma_batching_bits = Self::whir_gamma_batching_security(
467 challenge_field_bits,
468 batch_size,
469 next_log2_list_size,
470 );
471 min_gamma_batching_bits = min_gamma_batching_bits.min(gamma_batching_bits);
472
473 let shift_rbr_bits = Self::combine_security_bits(query_bits, gamma_batching_bits);
477 min_shift_rbr_bits = min_shift_rbr_bits.min(shift_rbr_bits);
478 min_rbr_bits = min_rbr_bits.min(shift_rbr_bits);
479
480 if !is_final_round {
481 let ood_bits = Self::whir_ood_security(
485 next_log2_list_size,
486 challenge_field_bits,
487 current_log_degree,
488 );
489 min_ood_bits = min_ood_bits.min(ood_bits);
490 min_rbr_bits = min_rbr_bits.min(ood_bits);
491
492 tracing::debug!(
493 "WHIR round {} | rate=2^-{} | queries={} | query={:.1} | prox_gaps={:.1} | sumcheck={:.1} | shift={:.1} | ood={:.1} | gamma={:.1}",
494 round, log_inv_rate, round_config.num_queries, query_bits,
495 min_prox_gaps_bits, min_sumcheck_bits, shift_rbr_bits, ood_bits,
496 min_gamma_batching_bits,
497 );
498 } else {
499 tracing::debug!(
500 "WHIR round {} (final) | rate=2^-{} | queries={} | query={:.1} | prox_gaps={:.1} | sumcheck={:.1} | final={:.1} | gamma={:.1}",
501 round, log_inv_rate, round_config.num_queries, query_bits,
502 min_prox_gaps_bits, min_sumcheck_bits, shift_rbr_bits,
503 min_gamma_batching_bits,
504 );
505 }
506
507 log_inv_rate = next_rate;
508 }
509
510 let details = WhirSoundnessCalculator {
511 mu_batching_bits,
512 fold_rbr_bits: min_fold_rbr_bits,
513 ood_rbr_bits: min_ood_bits,
514 shift_rbr_bits: min_shift_rbr_bits,
515 query_bits: min_query_bits,
517 proximity_gaps_bits: min_prox_gaps_bits,
518 sumcheck_bits: min_sumcheck_bits,
519 gamma_batching_bits: min_gamma_batching_bits,
520 };
521
522 let min_security = min_rbr_bits;
523
524 (min_security, details)
525 }
526
527 pub fn whir_proximity_gap_security(
535 proximity_regime: ProximityRegime,
536 challenge_field_bits: f64,
537 log_degree: usize,
538 log_inv_rate: usize,
539 batch_size: usize,
540 ) -> ProximityGapSecurity {
541 debug_assert!(batch_size > 1, "batch_size must be > 1 for err*");
542 match proximity_regime {
543 ProximityRegime::UniqueDecoding => {
544 let log2_err = challenge_field_bits
545 - ((batch_size - 1) as f64).log2()
546 - log_degree as f64
547 - log_inv_rate as f64;
548 ProximityGapSecurity {
549 log2_err,
550 log2_list_size: 0.0,
551 }
552 }
553 ProximityRegime::ListDecoding { m } => {
554 let (log2_a, log2_list_size) =
555 Self::log2_a_bound_bchks25(log_degree, log_inv_rate, m);
556 let log2_err = challenge_field_bits - ((batch_size - 1) as f64).log2() - log2_a;
558 ProximityGapSecurity {
559 log2_err,
560 log2_list_size,
561 }
562 }
563 }
564 }
565
566 #[inline]
568 fn log2_add(log2_x: f64, log2_y: f64) -> f64 {
569 if log2_x.is_infinite() && log2_x.is_sign_positive() {
570 return log2_x;
571 }
572 if log2_y.is_infinite() && log2_y.is_sign_positive() {
573 return log2_y;
574 }
575 if log2_x.is_nan() || log2_y.is_nan() {
576 return f64::NAN;
577 }
578
579 let (hi, lo) = if log2_x >= log2_y {
580 (log2_x, log2_y)
581 } else {
582 (log2_y, log2_x)
583 };
584 let ratio = (lo - hi).exp2();
585 hi + (1.0 + ratio).log2()
586 }
587
588 #[inline]
590 fn combine_security_bits(bits_a: f64, bits_b: f64) -> f64 {
591 if bits_a.is_infinite() && bits_a.is_sign_positive() {
592 return bits_b;
593 }
594 if bits_b.is_infinite() && bits_b.is_sign_positive() {
595 return bits_a;
596 }
597 if bits_a.is_nan() || bits_b.is_nan() {
598 return f64::NAN;
599 }
600
601 -Self::log2_add(-bits_a, -bits_b)
602 }
603
604 #[inline]
614 fn sample_bits_residue_probs(num_sampled_bits: f64, base_field_order: f64) -> (f64, f64, f64) {
615 let two_pow_n = num_sampled_bits.exp2();
616 let c = (base_field_order / two_pow_n).floor();
617 let r = base_field_order - c * two_pow_n; ((c + 1.0) / base_field_order, c / base_field_order, r)
619 }
620
621 fn whir_query_security_biased(
631 proximity_regime: ProximityRegime,
632 num_queries: usize,
633 log_inv_rate: usize,
634 log_query_domain: usize,
635 base_field_order: f64,
636 ) -> f64 {
637 let alpha = proximity_regime.max_agreement(log_inv_rate);
638 let (_p_hi, _p_lo, r) =
639 Self::sample_bits_residue_probs(log_query_domain as f64, base_field_order);
640 let big_n = (log_query_domain as f64).exp2();
641 let heavy_used = (alpha * big_n).min(r);
642 let mass = (alpha * (1.0 - r / base_field_order) + heavy_used / base_field_order)
644 .clamp(f64::MIN_POSITIVE, 1.0);
645 -(num_queries as f64) * mass.log2()
646 }
647
648 #[inline]
654 fn effective_pow_bits(pow_bits: usize, base_field_order: f64) -> f64 {
655 if pow_bits == 0 {
656 return 0.0;
658 }
659 let (p_hi, _p_lo, _r) = Self::sample_bits_residue_probs(pow_bits as f64, base_field_order);
660 -p_hi.log2()
661 }
662
663 #[inline]
664 fn bchks25_log2_a_from_log2_degrees(
665 log2_d_x: f64,
666 log2_d_y: f64,
667 log2_d_z: f64,
668 log2_agreement_term: f64,
669 ) -> f64 {
670 let log2_term_poly = 1.0 + log2_d_x + 2.0 * log2_d_y + log2_d_z;
672 let log2_term_gamma = log2_d_y + log2_agreement_term;
673 Self::log2_add(log2_term_poly, log2_term_gamma)
674 }
675
676 fn bchks25_log2_degrees(
681 log_degree: usize,
682 log_inv_rate: usize,
683 m: usize,
684 _gamma: f64,
685 ) -> (f64, f64, f64) {
686 #[cfg(feature = "soundness-bchks25-optimized")]
687 if let Some((degrees, _)) = bchks25_brute_force_params::bchks25_optimal_degrees_bruteforce(
688 log_degree,
689 log_inv_rate,
690 m,
691 _gamma,
692 ) {
693 debug_assert!(degrees.d_x > 0 && degrees.d_y > 0 && degrees.d_z > 0);
694 return (
695 (degrees.d_x as f64).log2(),
696 (degrees.d_y as f64).log2(),
697 (degrees.d_z as f64).log2(),
698 );
699 }
700
701 Self::bchks25_reference_log2_degrees(log_degree, log_inv_rate, m)
702 }
703
704 fn bchks25_reference_log2_degrees(
705 log_degree: usize,
706 log_inv_rate: usize,
707 m: usize,
708 ) -> (f64, f64, f64) {
709 let m_bar = m.max(1) as f64 + 0.5;
710 let log2_m_bar = m_bar.log2();
711 let log2_n = (log_degree + log_inv_rate) as f64;
712 let log2_3 = 3.0_f64.log2();
713 let log2_rho = -(log_inv_rate as f64);
714
715 let log2_d_x = log2_m_bar + log2_n + 0.5 * log2_rho;
719 let log2_d_y = log2_m_bar - 0.5 * log2_rho;
720 let log2_d_z = 2.0 * log2_m_bar - log2_3 - log2_rho;
721 let log2_d_z = log2_d_y.max(log2_d_z);
722
723 (log2_d_x, log2_d_y, log2_d_z)
724 }
725
726 fn log2_a_bound_bchks25(log_degree: usize, log_inv_rate: usize, m: usize) -> (f64, f64) {
747 const INVALID: (f64, f64) = (f64::INFINITY, f64::INFINITY);
748 let m_eff = m.max(1);
749 let log2_rho = -(log_inv_rate as f64);
750 let rho = log2_rho.exp2();
751 if rho <= 0.0 || !rho.is_finite() {
752 return INVALID;
753 }
754 if m_eff == 1 && rho >= (4.0 / 9.0) {
755 return INVALID;
758 }
759
760 let sqrt_rho = rho.sqrt();
761 let eta = sqrt_rho / (2.0 * m_eff as f64);
762 let gamma = 1.0 - sqrt_rho - eta;
763 if eta <= 0.0 || gamma <= 0.0 || gamma >= 1.0 - sqrt_rho {
764 return INVALID;
766 }
767
768 let log2_n = (log_degree + log_inv_rate) as f64;
769 let (log2_a_real, log2_list_size) = {
770 let (log2_d_x, log2_d_y, log2_d_z) =
773 Self::bchks25_log2_degrees(log_degree, log_inv_rate, m_eff, gamma);
774 let log2_gamma_n_plus_1 = Self::log2_add(gamma.log2() + log2_n, 0.0);
775 let log2_a = Self::bchks25_log2_a_from_log2_degrees(
776 log2_d_x,
777 log2_d_y,
778 log2_d_z,
779 log2_gamma_n_plus_1,
780 );
781 (log2_a, log2_d_y)
783 };
784 if !log2_a_real.is_finite() {
785 return INVALID;
786 }
787
788 let log2_a_real = log2_a_real.max(0.0);
790
791 let a = log2_a_real.exp2();
793 let a_bound = a.ceil().max(1.0);
794 (a_bound.log2(), log2_list_size)
795 }
796
797 fn whir_sumcheck_security(
804 base_field_order: f64,
805 challenge_field_bits: f64,
806 folding_pow_bits: usize,
807 log2_list_size: f64,
808 ) -> f64 {
809 let sumcheck_degree: f64 = 3.0;
811 challenge_field_bits - sumcheck_degree.log2() - log2_list_size
812 + Self::effective_pow_bits(folding_pow_bits, base_field_order)
813 }
814
815 fn whir_ood_security(
822 log2_list_size: f64,
823 challenge_field_bits: f64,
824 log_degree_at_round_start: usize,
825 ) -> f64 {
826 let base_bits = challenge_field_bits - log_degree_at_round_start as f64 + 1.0;
827 base_bits - 2.0 * log2_list_size
828 }
829
830 fn whir_gamma_batching_security(
835 challenge_field_bits: f64,
836 batch_size: usize,
837 log2_list_size: f64,
838 ) -> f64 {
839 debug_assert!(batch_size > 0, "batch_size must be > 0 for gamma batching");
840 challenge_field_bits - (batch_size as f64).log2() - log2_list_size
841 }
842}
843
844#[allow(clippy::too_many_arguments)]
846pub fn print_soundness_report(
847 params: &SystemParams,
848 base_field_order: f64,
849 challenge_field_bits: f64,
850 max_num_constraints_per_air: usize,
851 num_airs: usize,
852 max_constraint_degree: usize,
853 max_log_trace_height: usize,
854 num_trace_columns: usize,
855 num_stacked_columns: usize,
856 n_logup: usize,
857 proximity_regime: ProximityRegime,
858) {
859 let soundness = SoundnessCalculator::calculate(
860 params,
861 base_field_order,
862 challenge_field_bits,
863 max_num_constraints_per_air,
864 num_airs,
865 max_constraint_degree,
866 max_log_trace_height,
867 num_trace_columns,
868 num_stacked_columns,
869 n_logup,
870 );
871
872 println!("=== V2 Proof System Soundness Report ===");
873 println!();
874 println!("System Parameters:");
875 println!(" l_skip: {}", params.l_skip);
876 println!(" n_stack: {}", params.n_stack);
877 println!(" log_blowup: {}", params.log_blowup);
878 println!(" WHIR k: {}", params.whir.k);
879 println!(" WHIR rounds: {}", params.whir.rounds.len());
880 println!(" WHIR mu_pow_bits: {}", params.whir.mu_pow_bits);
881 println!(
882 " WHIR query_phase_pow_bits: {}",
883 params.whir.query_phase_pow_bits
884 );
885 println!(" WHIR folding_pow_bits: {}", params.whir.folding_pow_bits);
886 println!(" LogUp pow_bits: {}", params.logup.pow_bits);
887 println!(
888 " LogUp max_interaction_count: {}",
889 params.logup.max_interaction_count
890 );
891 println!(
892 " LogUp log_max_message_length: {}",
893 params.logup.log_max_message_length
894 );
895 println!(" max_constraint_degree: {}", params.max_constraint_degree);
896 println!();
897 println!("Proving Context:");
898 println!(" challenge_field_bits: {:.0}", challenge_field_bits);
899 println!(" base_field_order: {:.0}", base_field_order);
900 println!(
901 " max_num_constraints_per_air: {}",
902 max_num_constraints_per_air
903 );
904 println!(" num_airs: {}", num_airs);
905 println!(" max_constraint_degree: {}", max_constraint_degree);
906 println!(" max_log_trace_height: {}", max_log_trace_height);
907 println!(" num_trace_columns: {}", num_trace_columns);
908 println!(" num_stacked_columns: {}", num_stacked_columns);
909 println!(" n_logup (GKR depth): {}", n_logup);
910 println!();
911 println!("Security Analysis (bits):");
912 println!(" LogUp (α/β + PoW): {:.1}", soundness.logup_bits);
913 println!(
914 " GKR sumcheck: {:.1}",
915 soundness.gkr_sumcheck_bits
916 );
917 println!(
918 " GKR batching (μ/λ): {:.1}",
919 soundness.gkr_batching_bits
920 );
921 println!(
922 " ZeroCheck sumcheck: {:.1}",
923 soundness.zerocheck_sumcheck_bits
924 );
925 println!(
926 " Fused boundary/batching: {:.1}",
927 soundness.constraint_batching_bits
928 );
929 println!(
930 " Stacked reduction: {:.1}",
931 soundness.stacked_reduction_bits
932 );
933 println!(" WHIR (round-by-round min): {:.1}", soundness.whir_bits);
934 println!();
935 println!(
936 " TOTAL SECURITY: {:.1} bits",
937 soundness.total_bits
938 );
939 println!();
940
941 println!("WHIR Error Source Breakdown:");
942 let whir = &soundness.whir_details;
943 println!(" Query error: {:.1} bits", whir.query_bits);
944 println!(
945 " Proximity gaps: {:.1} bits",
946 whir.proximity_gaps_bits
947 );
948 println!(" Sumcheck error: {:.1} bits", whir.sumcheck_bits);
949 println!(" Min ε_fold: {:.1} bits", whir.fold_rbr_bits);
950 println!(" OOD error: {:.1} bits", whir.ood_rbr_bits);
951 println!(
952 " γ batching error: {:.1} bits",
953 whir.gamma_batching_bits
954 );
955 println!(" Min ε_shift/ε_fin: {:.1} bits", whir.shift_rbr_bits);
956 println!(" μ batching error: {:.1} bits", whir.mu_batching_bits);
957 println!();
958
959 println!("WHIR Round Breakdown:");
960 let k_whir = params.whir.k;
961 let mut log_inv_rate = params.log_blowup;
962 for (round, round_config) in params.whir.rounds.iter().enumerate() {
963 let query_sec =
964 proximity_regime.whir_query_security_bits(round_config.num_queries, log_inv_rate);
965 println!(
966 " Round {} | rate=2^-{:<2} | queries={:<3} | query_sec={:5.1} | pow={} | fold_pow={}",
967 round,
968 log_inv_rate,
969 round_config.num_queries,
970 query_sec,
971 params.whir.query_phase_pow_bits,
972 params.whir.folding_pow_bits
973 );
974 log_inv_rate += k_whir - 1;
975 }
976}
977
978pub fn min_whir_queries(
980 proximity_regime: ProximityRegime,
981 target_security_bits: usize,
982 log_inv_rate: usize,
983) -> usize {
984 WhirConfig::queries(proximity_regime, target_security_bits, log_inv_rate)
985}
986
987#[cfg(test)]
988mod tests {
989 use openvm_stark_sdk::config::{base_field_order, challenge_field_bits};
990
991 use super::*;
992 use crate::{config::WhirRoundConfig, interaction::LogUpSecurityParameters};
993
994 fn test_params() -> SystemParams {
999 SystemParams {
1000 l_skip: 3,
1001 n_stack: 8,
1002 w_stack: 64,
1003 log_blowup: 1,
1004 whir: WhirConfig {
1005 k: 4,
1006 rounds: vec![
1007 WhirRoundConfig { num_queries: 36 },
1008 WhirRoundConfig { num_queries: 18 },
1009 ],
1010 mu_pow_bits: 16,
1011 query_phase_pow_bits: 16,
1012 folding_pow_bits: 10,
1013 proximity: WhirProximityStrategy::UniqueDecoding,
1014 },
1015 logup: LogUpSecurityParameters {
1016 max_interaction_count: 1 << 20,
1017 log_max_message_length: 4,
1018 pow_bits: 16,
1019 },
1020 max_constraint_degree: 5,
1021 }
1022 }
1023
1024 const TARGET_SECURITY_BITS: usize = 100;
1028
1029 #[test]
1030 fn test_soundness_calculation() {
1031 let params = test_params();
1032 let soundness = SoundnessCalculator::calculate(
1033 ¶ms,
1034 base_field_order(),
1035 challenge_field_bits(),
1036 1000,
1037 50,
1038 4,
1039 24,
1040 200,
1041 10,
1042 15,
1043 );
1044
1045 assert!(soundness.logup_bits > 0.0);
1046 assert!(soundness.gkr_sumcheck_bits > 0.0);
1047 assert!(soundness.gkr_batching_bits > 0.0);
1048 assert!(soundness.zerocheck_sumcheck_bits > 0.0);
1049 assert!(soundness.constraint_batching_bits > 0.0);
1050 assert!(soundness.stacked_reduction_bits > 0.0);
1051 assert!(soundness.whir_bits > 0.0);
1052 assert!(soundness.total_bits > 0.0);
1053
1054 let expected_total = soundness
1055 .logup_bits
1056 .min(soundness.gkr_sumcheck_bits)
1057 .min(soundness.gkr_batching_bits)
1058 .min(soundness.zerocheck_sumcheck_bits)
1059 .min(soundness.constraint_batching_bits)
1060 .min(soundness.stacked_reduction_bits)
1061 .min(soundness.whir_bits);
1062 assert!((soundness.total_bits - expected_total).abs() < 0.001);
1063 }
1064
1065 #[test]
1066 fn test_whir_query_calculation() {
1067 let queries = min_whir_queries(ProximityRegime::UniqueDecoding, TARGET_SECURITY_BITS, 1);
1068 assert!(queries > 0);
1069 }
1070
1071 #[test]
1072 fn test_logup_soundness() {
1073 let logup = test_params().logup;
1074 let security =
1075 SoundnessCalculator::logup_soundness(
1076 logup.max_interaction_count,
1077 logup.log_max_message_length,
1078 challenge_field_bits(),
1079 0.0,
1080 ) + SoundnessCalculator::effective_pow_bits(logup.pow_bits, base_field_order());
1081 assert!(security > TARGET_SECURITY_BITS as f64);
1082 }
1083
1084 #[test]
1085 fn test_logup_list_size_is_security_penalty() {
1086 let logup = test_params().logup;
1087 let no_list = SoundnessCalculator::logup_soundness(
1088 logup.max_interaction_count,
1089 logup.log_max_message_length,
1090 challenge_field_bits(),
1091 0.0,
1092 );
1093 let list_size_bits = 5.0;
1094 let with_list = SoundnessCalculator::logup_soundness(
1095 logup.max_interaction_count,
1096 logup.log_max_message_length,
1097 challenge_field_bits(),
1098 list_size_bits,
1099 );
1100 assert!((no_list - with_list - list_size_bits).abs() < 1e-9);
1101 }
1102
1103 #[test]
1104 fn test_fused_batch_constraint_boundary_soundness() {
1105 let security = SoundnessCalculator::calculate_constraint_batching_soundness(
1106 100.0, 11, 7, 3, 10, 4, 2.0, );
1114 let expected_degree: f64 = (3.0_f64 + 7.0 + 10.0).max(20.0);
1115 let expected = 100.0 - expected_degree.log2() - 2.0;
1116 assert!((security - expected).abs() < 1e-9);
1117 }
1118
1119 #[test]
1120 fn test_whir_unique_decoding_security() {
1121 let security = ProximityRegime::UniqueDecoding.whir_query_security_bits(100, 1);
1123 assert!(
1124 (security - 41.5).abs() < 1.0,
1125 "Expected ~41.5, got {}",
1126 security
1127 );
1128
1129 let security_blowup2 = ProximityRegime::UniqueDecoding.whir_query_security_bits(100, 2);
1131 assert!(
1132 (security_blowup2 - 67.8).abs() < 1.0,
1133 "Expected ~67.8, got {}",
1134 security_blowup2
1135 );
1136 }
1137
1138 #[test]
1139 fn test_whir_gamma_batching_uses_list_size_and_full_batch_size() {
1140 let security = SoundnessCalculator::whir_gamma_batching_security(100.0, 5, 3.0);
1141 let expected = 100.0 - 5.0_f64.log2() - 3.0;
1142 assert!((security - expected).abs() < 1e-9);
1143 }
1144
1145 #[test]
1146 fn test_combine_security_bits_sums_errors_before_taking_log() {
1147 let combined = SoundnessCalculator::combine_security_bits(100.0, 100.0);
1148 let expected = 99.0;
1149 assert!((combined - expected).abs() < 1e-9);
1150 }
1151
1152 #[test]
1153 fn test_bchks25_reference_m2_enforces_dz_ge_dy() {
1154 let (_log2_d_x, log2_d_y, log2_d_z) =
1155 SoundnessCalculator::bchks25_reference_log2_degrees(24, 2, 2);
1156 assert!(log2_d_z >= log2_d_y);
1157 }
1158
1159 #[test]
1160 fn test_bchks25_m1_requires_rho_below_four_ninths() {
1161 let invalid = SoundnessCalculator::log2_a_bound_bchks25(12, 1, 1);
1163 assert!(invalid.0.is_infinite() && invalid.1.is_infinite());
1164
1165 let valid = SoundnessCalculator::log2_a_bound_bchks25(12, 2, 1);
1167 assert!(valid.0.is_finite() && valid.1.is_finite());
1168 }
1169}
1170
1171#[allow(dead_code)]
1177#[cfg(feature = "soundness-bchks25-optimized")]
1178mod bchks25_brute_force_params {
1179 use crate::soundness::SoundnessCalculator;
1180
1181 const BCHKS25_DY_SEARCH_MIN_MAX: u128 = 9;
1182 const BCHKS25_DY_SEARCH_REF_MULTIPLIER: u128 = 4;
1183 const BCHKS25_DY_SEARCH_HARD_MAX: u128 = 4096;
1184 const BCHKS25_DZ_SEARCH_MAX: u128 = 500_000;
1185
1186 #[derive(Clone, Copy, Debug)]
1187 pub struct Bchks25Degrees {
1188 pub d_x: u128,
1189 pub d_y: u128,
1190 pub d_z: u128,
1193 }
1194
1195 pub fn bchks25_optimal_degrees_bruteforce(
1216 log_degree: usize,
1217 log_inv_rate: usize,
1218 m: usize,
1219 gamma: f64,
1220 ) -> Option<(Bchks25Degrees, f64)> {
1221 if !gamma.is_finite() || gamma <= 0.0 {
1222 return None;
1223 }
1224
1225 let log_n = log_degree.checked_add(log_inv_rate)?;
1226 if log_n >= u128::BITS as usize || log_degree >= u128::BITS as usize {
1227 return None;
1228 }
1229
1230 let n = 1_u128.checked_shl(log_n as u32)?;
1231 let k = 1_u128.checked_shl(log_degree as u32)?;
1232 let m_u = m as u128;
1233
1234 let agreements_plus_one = (gamma * n as f64).ceil() + 1.0;
1235 if !agreements_plus_one.is_finite() || agreements_plus_one <= 0.0 {
1236 return None;
1237 }
1238 let log2_agreements_plus_one = agreements_plus_one.log2();
1239 let max_d_x_for_gamma = (1.0 - gamma) * (m_u as f64) * (n as f64);
1240 if !max_d_x_for_gamma.is_finite() || max_d_x_for_gamma <= 0.0 {
1241 return None;
1242 }
1243
1244 let mut best: Option<(Bchks25Degrees, f64)> = None;
1245 let d_y_start = 1_u128.max(m_u.saturating_sub(1));
1246 let d_y_end = bchks25_dy_search_upper_bound(log_degree, log_inv_rate, m);
1247 let d_z_floor = if m < 3 {
1248 bchks25_dz_eq9_index_lower_bound(log_inv_rate, m)?
1249 } else {
1250 0
1251 };
1252 if d_y_start > d_y_end {
1253 return None;
1254 }
1255
1256 for d_y in d_y_start..=d_y_end {
1257 let Some(d_x_base) = k.checked_mul(d_y) else {
1258 continue;
1259 };
1260 if (d_x_base as f64) >= max_d_x_for_gamma {
1261 continue;
1262 }
1263
1264 let d_x_upper = max_d_x_for_gamma.ceil() as u128;
1265 let mut d_x_candidates = Vec::with_capacity(24);
1266 d_x_candidates.push(d_x_base);
1267 if let Some(v) = d_x_base.checked_add(1) {
1268 d_x_candidates.push(v);
1269 }
1270
1271 let d_y_plus_1 = d_y.checked_add(1)?;
1272 let k_term = k
1273 .checked_mul(d_y)?
1274 .checked_mul(d_y_plus_1)?
1275 .checked_div(2)?;
1276 let a_e = n.checked_mul(m_u.checked_mul(m_u.checked_add(1)?)?.checked_div(2)?)?;
1277 let d_x_slope_cross = a_e.checked_add(k_term)?.checked_div(d_y_plus_1)?;
1278 for off in [0_u128, 1, 2] {
1279 if d_x_slope_cross >= off {
1280 d_x_candidates.push(d_x_slope_cross - off);
1281 }
1282 if let Some(v) = d_x_slope_cross.checked_add(off) {
1283 d_x_candidates.push(v);
1284 }
1285 }
1286
1287 if d_x_upper > d_x_base {
1288 let step = ((d_x_upper - d_x_base) / 16).max(1);
1289 let mut cur = d_x_base;
1290 while cur <= d_x_upper {
1291 d_x_candidates.push(cur);
1292 let Some(next) = cur.checked_add(step) else {
1293 break;
1294 };
1295 if next <= cur {
1296 break;
1297 }
1298 cur = next;
1299 }
1300 d_x_candidates.push(d_x_upper);
1301 d_x_candidates.push(d_x_upper.saturating_sub(1));
1302 }
1303
1304 d_x_candidates.sort_unstable();
1305 d_x_candidates.dedup();
1306
1307 for d_x in d_x_candidates {
1308 if d_x < d_x_base || (d_x as f64) >= max_d_x_for_gamma {
1309 continue;
1310 }
1311
1312 let Some(d_z) = bchks25_min_dz_for_dx_dy(k, n, m_u, d_x, d_y, d_z_floor) else {
1313 continue;
1314 };
1315
1316 let log2_d_x = (d_x as f64).log2();
1317 let log2_d_y = (d_y as f64).log2();
1318 let log2_d_z = (d_z as f64).log2();
1319 let log2_a = SoundnessCalculator::bchks25_log2_a_from_log2_degrees(
1320 log2_d_x,
1321 log2_d_y,
1322 log2_d_z,
1323 log2_agreements_plus_one,
1324 );
1325 if !log2_a.is_finite() {
1326 continue;
1327 }
1328
1329 let candidate = (Bchks25Degrees { d_x, d_y, d_z }, log2_a);
1330 match best {
1331 None => best = Some(candidate),
1332 Some((best_deg, best_log2_a)) => {
1333 let better = log2_a + 1e-12 < best_log2_a
1336 || ((log2_a - best_log2_a).abs() <= 1e-12
1337 && (d_y < best_deg.d_y
1338 || (d_y == best_deg.d_y
1339 && (d_z < best_deg.d_z
1340 || (d_z == best_deg.d_z && d_x < best_deg.d_x)))));
1341 if better {
1342 best = Some(candidate);
1343 }
1344 }
1345 }
1346 }
1347 }
1348 best
1349 }
1350
1351 fn bchks25_dy_search_upper_bound(log_degree: usize, log_inv_rate: usize, m: usize) -> u128 {
1352 let (_log2_d_x, log2_d_y, _log2_d_z) =
1353 SoundnessCalculator::bchks25_reference_log2_degrees(log_degree, log_inv_rate, m);
1354 if !log2_d_y.is_finite() {
1355 return BCHKS25_DY_SEARCH_HARD_MAX;
1356 }
1357 let ref_d_y = log2_d_y.exp2().ceil();
1358 if !ref_d_y.is_finite() || ref_d_y <= 0.0 {
1359 return BCHKS25_DY_SEARCH_HARD_MAX;
1360 }
1361 let ref_scaled = (ref_d_y as u128).saturating_mul(BCHKS25_DY_SEARCH_REF_MULTIPLIER);
1362 ref_scaled.clamp(BCHKS25_DY_SEARCH_MIN_MAX, BCHKS25_DY_SEARCH_HARD_MAX)
1363 }
1364
1365 fn bchks25_dz_eq9_index_lower_bound(log_inv_rate: usize, m: usize) -> Option<u128> {
1367 let m_u = m.max(1) as u128;
1368 if log_inv_rate >= u128::BITS as usize {
1369 return None;
1370 }
1371
1372 let n_over_k = 1_u128.checked_shl(log_inv_rate as u32)?;
1376 let two_m_plus_1 = m_u.checked_mul(2)?.checked_add(1)?;
1377 let numerator = two_m_plus_1
1378 .checked_mul(two_m_plus_1)?
1379 .checked_mul(n_over_k)?;
1380 let ceil_d_z = numerator.checked_add(11)?.checked_div(12)?;
1381 Some(ceil_d_z.saturating_sub(1))
1382 }
1383
1384 #[cfg(test)]
1391 fn bchks25_num_vars(k: u128, d_x: u128, d_y: u128, d_z: u128) -> Option<u128> {
1392 if d_z < d_y {
1393 return Some(0);
1394 }
1395
1396 let mut total = 0_u128;
1397 for j in 0..=d_y {
1398 let x_terms = d_x.checked_sub(k.checked_mul(j)?)?.checked_add(1)?;
1399 let z_terms = (d_z - j).checked_add(1)?;
1400 let add = x_terms.checked_mul(z_terms)?;
1401 total = total.checked_add(add)?;
1402 }
1403 Some(total)
1404 }
1405
1406 #[cfg(test)]
1411 fn bchks25_num_eqs_eq11(n: u128, m: u128, d_z: u128) -> Option<u128> {
1412 let ceil_d_z = d_z.checked_add(1)?;
1413 let m_plus_1 = m.checked_add(1)?;
1414 let m_choose_2_scaled = m.checked_mul(m_plus_1)?.checked_div(2)?;
1415 let m_cubic_minus_m_over_6 = m
1416 .checked_mul(m)?
1417 .checked_mul(m)?
1418 .checked_sub(m)?
1419 .checked_div(6)?;
1420 let inner = m_choose_2_scaled
1421 .checked_mul(ceil_d_z)?
1422 .checked_sub(m_cubic_minus_m_over_6)?;
1423 n.checked_mul(inner)
1424 }
1425
1426 fn bchks25_min_dz_for_dx_dy(
1434 k: u128,
1435 n: u128,
1436 m: u128,
1437 d_x: u128,
1438 d_y: u128,
1439 d_z_floor: u128,
1440 ) -> Option<u128> {
1441 let low = d_y.max(m.saturating_sub(1)).max(d_z_floor);
1442 let high = BCHKS25_DZ_SEARCH_MAX;
1443 if low > high {
1444 return None;
1445 }
1446
1447 let d_y_plus_1 = d_y.checked_add(1)?;
1452 let d_y_d_y_plus_1_over_2 = d_y.checked_mul(d_y_plus_1)?.checked_div(2)?;
1453 let d_y_d_y_plus_1_two_d_y_plus_1_over_6 = d_y
1454 .checked_mul(d_y)?
1455 .checked_add(d_y)?
1456 .checked_mul(d_y.checked_mul(2)?.checked_add(1)?)?
1457 .checked_div(6)?;
1458 let a_v = d_y_plus_1
1459 .checked_mul(d_x.checked_add(1)?)?
1460 .checked_sub(k.checked_mul(d_y_d_y_plus_1_over_2)?)?;
1461 let b_v = d_x
1462 .checked_add(1)?
1463 .checked_mul(d_y_d_y_plus_1_over_2)?
1464 .checked_sub(k.checked_mul(d_y_d_y_plus_1_two_d_y_plus_1_over_6)?)?;
1465 if a_v == 0 {
1466 return None;
1467 }
1468
1469 let m_plus_1 = m.checked_add(1)?;
1471 let a_e = n.checked_mul(m.checked_mul(m_plus_1)?.checked_div(2)?)?;
1472 let b_e = n.checked_mul(
1473 m.checked_mul(m)?
1474 .checked_mul(m)?
1475 .checked_sub(m)?
1476 .checked_div(6)?,
1477 )?;
1478
1479 let is_valid = |d_z: u128| -> Option<bool> {
1480 let x = d_z.checked_add(1)?;
1481 let n_vars = a_v.checked_mul(x)?.checked_sub(b_v)?;
1482 let n_eqs = a_e.checked_mul(x)?.checked_sub(b_e)?;
1483 Some(n_vars > n_eqs)
1484 };
1485
1486 match a_v.cmp(&a_e) {
1487 core::cmp::Ordering::Greater => {
1488 let slope = a_v.checked_sub(a_e)?;
1490 let candidate = if b_v < b_e {
1491 low
1492 } else {
1493 let rhs = b_v.checked_sub(b_e)?;
1494 let x_min = rhs.checked_div(slope)?.checked_add(1)?;
1495 let d_min = x_min.checked_sub(1)?;
1496 low.max(d_min)
1497 };
1498 if candidate > high {
1499 return None;
1500 }
1501 if is_valid(candidate)? {
1502 Some(candidate)
1503 } else {
1504 None
1505 }
1506 }
1507 core::cmp::Ordering::Equal => {
1508 if b_v < b_e {
1509 Some(low)
1510 } else {
1511 None
1512 }
1513 }
1514 core::cmp::Ordering::Less => {
1515 if is_valid(low)? {
1517 Some(low)
1518 } else {
1519 None
1520 }
1521 }
1522 }
1523 }
1524
1525 #[test]
1526 fn test_bchks25_optimizer_finds_minimal_valid_dz() {
1527 let log_degree = 14;
1528 let log_inv_rate = 5;
1529 let m = 1;
1530
1531 let rho = (-(log_inv_rate as f64)).exp2();
1532 let sqrt_rho = rho.sqrt();
1533 let eta = sqrt_rho / (2.0 * m as f64);
1534 let gamma = 1.0 - sqrt_rho - eta;
1535
1536 let (degrees, _log2_a) =
1537 bchks25_optimal_degrees_bruteforce(log_degree, log_inv_rate, m, gamma)
1538 .expect("optimizer should find valid degrees");
1539 let n = 1_u128 << (log_degree + log_inv_rate);
1540 let k = 1_u128 << log_degree;
1541 let m_u = m as u128;
1542
1543 assert!(degrees.d_y >= m_u.saturating_sub(1));
1544 let max_d_x_for_gamma = (1.0 - gamma) * (m as f64) * (n as f64);
1545 assert!(degrees.d_x >= k * degrees.d_y);
1546 assert!((degrees.d_x as f64) < max_d_x_for_gamma);
1547 let d_z_floor = bchks25_dz_eq9_index_lower_bound(log_inv_rate, m)
1548 .expect("d_z floor should be representable");
1549 assert!(degrees.d_z >= degrees.d_y.max(d_z_floor));
1550 let vars = bchks25_num_vars(k, degrees.d_x, degrees.d_y, degrees.d_z)
1551 .expect("vars should fit in u128");
1552 let eqs = bchks25_num_eqs_eq11(n, m_u, degrees.d_z).expect("eqs should fit in u128");
1553 assert!(vars > eqs);
1554 let low = degrees.d_y.max(m_u.saturating_sub(1)).max(d_z_floor);
1555 if degrees.d_z > low {
1556 let prev_vars = bchks25_num_vars(k, degrees.d_x, degrees.d_y, degrees.d_z - 1)
1557 .expect("vars should fit in u128");
1558 let prev_eqs =
1559 bchks25_num_eqs_eq11(n, m_u, degrees.d_z - 1).expect("eqs should fit in u128");
1560 assert!(prev_vars <= prev_eqs);
1561 }
1562 }
1563
1564 #[test]
1565 fn test_bchks25_eq11_closed_form_matches_expanded_sum() {
1566 let n = 1_u128 << 12;
1567 for m in 2_u128..=8 {
1568 for d_z in (m - 1)..=(m + 12) {
1569 let closed_form =
1570 bchks25_num_eqs_eq11(n, m, d_z).expect("closed-form n_eqs should fit in u128");
1571 let ceil_d_z = d_z + 1;
1572 let mut expanded_sum = 0_u128;
1573 for s in 0..m {
1574 expanded_sum += (ceil_d_z - s) * (m - s);
1575 }
1576 let expanded = n * expanded_sum;
1577 assert_eq!(closed_form, expanded);
1578 }
1579 }
1580 }
1581
1582 #[test]
1583 fn test_bchks25_min_dz_direct_solve_matches_linear_scan() {
1584 let cases = [
1585 (
1587 1_u128 << 20,
1588 1_u128 << 22,
1589 3_u128,
1590 2_u128,
1591 (1_u128 << 20) * 2,
1592 ),
1593 (
1595 1_u128 << 12,
1596 1_u128 << 24,
1597 6_u128,
1598 5_u128,
1599 (1_u128 << 12) * 5,
1600 ),
1601 (
1603 1_u128 << 16,
1604 1_u128 << 20,
1605 4_u128,
1606 3_u128,
1607 (1_u128 << 16) * 3 + 1,
1608 ),
1609 (
1611 1_u128 << 12,
1612 1_u128 << 17,
1613 1_u128,
1614 2_u128,
1615 (1_u128 << 12) * 2 + 17,
1616 ),
1617 ];
1618
1619 for (k, n, m, d_y, d_x) in cases {
1620 assert!(d_x >= k * d_y);
1621 let log_inv_rate = (n / k).ilog2() as usize;
1622 let d_z_floor = if m < 3 {
1623 bchks25_dz_eq9_index_lower_bound(log_inv_rate, m as usize)
1624 .expect("d_z floor should be representable")
1625 } else {
1626 0
1627 };
1628 let got = bchks25_min_dz_for_dx_dy(k, n, m, d_x, d_y, d_z_floor);
1629 let low = d_y.max(m.saturating_sub(1)).max(d_z_floor);
1630 let expected = (low..=BCHKS25_DZ_SEARCH_MAX).find(|&d_z| {
1631 let vars = bchks25_num_vars(k, d_x, d_y, d_z).expect("vars should fit");
1632 let eqs = bchks25_num_eqs_eq11(n, m, d_z).expect("eqs should fit");
1633 vars > eqs
1634 });
1635 assert_eq!(got, expected);
1636 }
1637 }
1638}