1use alloc::vec::Vec;
23
24use p3_field::Field;
25use serde::Serialize;
26
27use crate::assumption::SecurityAssumption;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
31#[non_exhaustive]
32pub enum HidingBoundClassification {
33 Proven,
42 Conditional,
47 Approximation,
52 Unavailable,
54}
55
56#[derive(Debug, Clone, PartialEq, Serialize)]
63#[non_exhaustive]
64pub struct HidingErrorBound {
65 pub bits: Option<f64>,
67 pub classification: HidingBoundClassification,
69 pub vacuous: bool,
71}
72
73impl HidingErrorBound {
74 fn available(bits: f64, classification: HidingBoundClassification) -> Self {
75 let vacuous = bits <= 0.;
76 Self {
77 bits: Some(bits.max(0.)),
78 classification,
79 vacuous,
80 }
81 }
82
83 const fn unavailable() -> Self {
84 Self {
85 bits: None,
86 classification: HidingBoundClassification::Unavailable,
87 vacuous: false,
88 }
89 }
90
91 #[must_use]
97 pub const fn proven_bits(&self) -> Option<f64> {
98 match (self.classification, self.bits, self.vacuous) {
99 (HidingBoundClassification::Proven, bits, false) => bits,
100 _ => None,
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Serialize)]
107#[non_exhaustive]
108pub struct HidingListBound {
109 pub log2_size: Option<f64>,
111 pub classification: HidingBoundClassification,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
117#[non_exhaustive]
118pub enum HidingCodeRole {
119 Source,
121 SumcheckMask {
123 round: usize,
125 },
126 CodeSwitchMask {
128 round: usize,
130 },
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct HidingCodeGeometry {
141 role: HidingCodeRole,
142 message_len: usize,
143 randomness_len: usize,
144 domain_size: usize,
145 width: usize,
146 requested_queries: usize,
147}
148
149impl HidingCodeGeometry {
150 #[must_use]
158 pub const fn new(
159 role: HidingCodeRole,
160 message_len: usize,
161 randomness_len: usize,
162 domain_size: usize,
163 width: usize,
164 requested_queries: usize,
165 ) -> Self {
166 assert!(
167 domain_size.is_power_of_two(),
168 "the codeword domain size must be a power of two"
169 );
170 assert!(
171 randomness_len <= domain_size && message_len <= domain_size - randomness_len,
172 "the randomized-code dimension must fit in the codeword domain"
173 );
174 assert!(width > 0, "the interleaving width must be nonzero");
175 assert!(
176 width <= usize::MAX / 2,
177 "the interleaving width must be safe to double"
178 );
179 Self {
180 role,
181 message_len,
182 randomness_len,
183 domain_size,
184 width,
185 requested_queries,
186 }
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Serialize)]
192#[non_exhaustive]
193pub struct HidingCodeSecurityReport {
194 pub role: HidingCodeRole,
196 pub message_len: usize,
198 pub randomness_len: usize,
200 pub randomized_dimension: usize,
202 pub dyadic_dimension: usize,
204 pub domain_size: usize,
206 pub randomized_rate: f64,
208 pub analysis_log_inv_rate: Option<usize>,
213 pub analysis_radius: Option<f64>,
215 pub width: usize,
217 pub requested_queries: usize,
219 pub effective_queries: usize,
223 pub query_miss: HidingErrorBound,
232 pub mca: HidingErrorBound,
235 pub paired_list: HidingListBound,
238}
239
240#[derive(Debug, Clone, PartialEq, Serialize)]
242#[non_exhaustive]
243pub struct HidingQueryRoundReport {
244 pub source_before_pow_bits: Option<f64>,
246 pub masks_before_pow_bits: Vec<Option<f64>>,
248 pub weakest_before_pow_bits: Option<f64>,
250 pub before_pow: HidingErrorBound,
255 pub shared_pow_bits: usize,
257 pub source_with_pow: HidingErrorBound,
259 pub with_pow: HidingErrorBound,
263}
264
265#[derive(Debug, Clone, PartialEq, Serialize)]
267#[non_exhaustive]
268pub struct HidingGammaRoundReport {
269 pub mca_sum: HidingErrorBound,
271 pub product_list: HidingListBound,
273 pub product_list_over_field: HidingErrorBound,
275 pub combined: HidingErrorBound,
277}
278
279#[derive(Debug, Clone, PartialEq, Serialize)]
281#[non_exhaustive]
282pub struct HidingBaseCaseSecurityReport {
283 pub target_bits: usize,
285 pub assumption: SecurityAssumption,
287 pub field_order_log2_estimate: f64,
292 pub field_order_log2_floor: usize,
296 pub source: HidingCodeSecurityReport,
298 pub mask_groups: Vec<HidingCodeSecurityReport>,
300 pub query_round: HidingQueryRoundReport,
302 pub gamma_round: HidingGammaRoundReport,
304 pub round_by_round: HidingErrorBound,
317}
318
319#[must_use]
327pub fn hiding_base_case_security_report<EF: Field>(
328 target_bits: usize,
329 assumption: SecurityAssumption,
330 source: HidingCodeGeometry,
331 mask_groups: Vec<HidingCodeGeometry>,
332 shared_pow_bits: usize,
333) -> HidingBaseCaseSecurityReport {
334 build_hiding_base_case_security_report(
335 target_bits,
336 assumption,
337 log2_order_estimate::<EF>(),
338 EF::order().bits() as usize - 1,
339 source,
340 mask_groups,
341 shared_pow_bits,
342 )
343}
344
345#[allow(clippy::too_many_arguments)]
346fn build_hiding_base_case_security_report(
347 target_bits: usize,
348 assumption: SecurityAssumption,
349 field_order_log2_estimate: f64,
350 field_order_log2_floor: usize,
351 source: HidingCodeGeometry,
352 mask_groups: Vec<HidingCodeGeometry>,
353 shared_pow_bits: usize,
354) -> HidingBaseCaseSecurityReport {
355 let source = code_report(source, assumption, field_order_log2_floor);
356 let mask_groups: Vec<_> = mask_groups
357 .into_iter()
358 .map(|geometry| code_report(geometry, assumption, field_order_log2_floor))
359 .collect();
360
361 let source_before_pow_bits = source.query_miss.bits;
362 let masks_before_pow_bits: Vec<Option<f64>> = mask_groups
363 .iter()
364 .map(|group| group.query_miss.bits)
365 .collect();
366 let weakest_before_pow_bits = minimum_available(
367 core::iter::once(source_before_pow_bits).chain(masks_before_pow_bits.iter().copied()),
368 );
369 let before_pow = weakest_before_pow_bits.map_or_else(HidingErrorBound::unavailable, |bits| {
370 HidingErrorBound::available(bits, HidingBoundClassification::Proven)
371 });
372 let pow_classification = if shared_pow_bits == 0 {
373 HidingBoundClassification::Proven
374 } else {
375 HidingBoundClassification::Conditional
376 };
377 let source_with_pow = source_before_pow_bits
378 .map_or_else(HidingErrorBound::unavailable, |bits| {
379 HidingErrorBound::available(bits + shared_pow_bits as f64, pow_classification)
380 });
381 let with_pow = weakest_before_pow_bits.map_or_else(HidingErrorBound::unavailable, |bits| {
382 HidingErrorBound::available(bits + shared_pow_bits as f64, pow_classification)
383 });
384 let query_round = HidingQueryRoundReport {
385 source_before_pow_bits,
386 masks_before_pow_bits,
387 weakest_before_pow_bits,
388 before_pow,
389 shared_pow_bits,
390 source_with_pow,
391 with_pow,
392 };
393
394 let mca_bounds = core::iter::once(&source)
395 .chain(mask_groups.iter())
396 .map(|component| component.mca.bits);
397 let mca_classification = mca_classification(assumption);
398 let mca_sum = sum_available_probability_bits(mca_bounds)
399 .map_or_else(HidingErrorBound::unavailable, |bits| {
400 HidingErrorBound::available(bits, mca_classification)
401 });
402
403 let list_classification = list_classification(assumption);
404 let product_list_log2 = sum_available(
405 core::iter::once(&source)
406 .chain(mask_groups.iter())
407 .map(|component| component.paired_list.log2_size),
408 );
409 let product_list = HidingListBound {
410 log2_size: product_list_log2,
411 classification: if product_list_log2.is_some() {
412 list_classification
413 } else {
414 HidingBoundClassification::Unavailable
415 },
416 };
417 let product_list_over_field =
418 product_list_log2.map_or_else(HidingErrorBound::unavailable, |list_bits| {
419 HidingErrorBound::available(
420 field_order_log2_floor as f64 - list_bits,
421 list_classification,
422 )
423 });
424 let gamma_classification = combine_classifications(
425 mca_sum.classification,
426 product_list_over_field.classification,
427 );
428 let gamma_combined =
429 sum_available_probability_bits([mca_sum.bits, product_list_over_field.bits])
430 .map_or_else(HidingErrorBound::unavailable, |bits| {
431 HidingErrorBound::available(bits, gamma_classification)
432 });
433 let gamma_round = HidingGammaRoundReport {
434 mca_sum,
435 product_list,
436 product_list_over_field,
437 combined: gamma_combined,
438 };
439
440 let round_by_round_classification = combine_classifications(
441 gamma_round.combined.classification,
442 query_round.before_pow.classification,
443 );
444 let round_by_round =
445 minimum_available([gamma_round.combined.bits, query_round.before_pow.bits])
446 .map_or_else(HidingErrorBound::unavailable, |bits| {
447 HidingErrorBound::available(bits, round_by_round_classification)
448 });
449
450 HidingBaseCaseSecurityReport {
451 target_bits,
452 assumption,
453 field_order_log2_estimate,
454 field_order_log2_floor,
455 source,
456 mask_groups,
457 query_round,
458 gamma_round,
459 round_by_round,
460 }
461}
462
463fn code_report(
464 geometry: HidingCodeGeometry,
465 assumption: SecurityAssumption,
466 field_order_log2_floor: usize,
467) -> HidingCodeSecurityReport {
468 let HidingCodeGeometry {
469 role,
470 message_len,
471 randomness_len,
472 domain_size,
473 width,
474 requested_queries,
475 } = geometry;
476 let randomized_dimension = message_len + randomness_len;
477 let dyadic_dimension = randomized_dimension.next_power_of_two();
478 let log_degree = dyadic_dimension.ilog2() as usize;
479 let raw_log_inv_rate = domain_size.ilog2() as usize - log_degree;
480 let analysis_log_inv_rate = (raw_log_inv_rate > 0).then_some(raw_log_inv_rate);
481 let one_minus_delta =
482 analysis_log_inv_rate.map(|rate| libm::pow(2., assumption.log_1_delta(rate)));
483 let analysis_radius = one_minus_delta.map(|one_minus_delta| 1. - one_minus_delta);
484 let effective_queries = requested_queries.min(domain_size);
485
486 let (query_miss, mca, paired_list) = analysis_log_inv_rate.map_or_else(
487 || {
488 (
489 HidingErrorBound::unavailable(),
490 HidingErrorBound::unavailable(),
491 HidingListBound {
492 log2_size: None,
493 classification: HidingBoundClassification::Unavailable,
494 },
495 )
496 },
497 |log_inv_rate| {
498 let one_minus_delta = one_minus_delta.expect("a positive-rate envelope has a radius");
499 let query_bits = assumption.queries_error(log_inv_rate, effective_queries);
500 let mca_bits =
504 assumption.prox_gaps_error(log_degree, log_inv_rate, field_order_log2_floor, 2)
505 - libm::log2(width as f64);
506 let paired_interleaving = 2 * width;
507 (
508 HidingErrorBound::available(query_bits, HidingBoundClassification::Proven),
509 HidingErrorBound::available(mca_bits, mca_classification(assumption)),
510 HidingListBound {
511 log2_size: Some(interleaved_list_bits(
512 assumption,
513 log_degree,
514 log_inv_rate,
515 paired_interleaving,
516 one_minus_delta,
517 )),
518 classification: list_classification(assumption),
519 },
520 )
521 },
522 );
523
524 HidingCodeSecurityReport {
525 role,
526 message_len,
527 randomness_len,
528 randomized_dimension,
529 dyadic_dimension,
530 domain_size,
531 randomized_rate: randomized_dimension as f64 / domain_size as f64,
532 analysis_log_inv_rate,
533 analysis_radius,
534 width,
535 requested_queries,
536 effective_queries,
537 query_miss,
538 mca,
539 paired_list,
540 }
541}
542
543fn interleaved_list_bits(
544 assumption: SecurityAssumption,
545 log_degree: usize,
546 log_inv_rate: usize,
547 interleaving: usize,
548 one_minus_delta: f64,
549) -> f64 {
550 let base = assumption.list_size_bits(log_degree, log_inv_rate);
551 let crude = interleaving as f64 * base;
552
553 let rho = libm::pow(2., -(log_inv_rate as f64));
558 let distance = 1. - rho;
559 let delta = 1. - one_minus_delta;
560 let gap = one_minus_delta - rho;
561 let b = libm::ceil(delta / gap) as usize;
562 let r = libm::ceil(libm::log2(distance / gap)) as usize;
563 let stable = b
564 .checked_add(r)
565 .map_or(f64::INFINITY, |n| log2_binomial(n, r) + r as f64 * base);
566 crude.min(stable)
567}
568
569fn log2_binomial(n: usize, k: usize) -> f64 {
570 let k = k.min(n - k);
571 (1..=k)
572 .map(|i| libm::log2((n - k + i) as f64) - libm::log2(i as f64))
573 .sum()
574}
575
576const fn mca_classification(assumption: SecurityAssumption) -> HidingBoundClassification {
577 match assumption {
578 SecurityAssumption::UniqueDecoding => HidingBoundClassification::Proven,
579 SecurityAssumption::JohnsonBound => HidingBoundClassification::Proven,
580 SecurityAssumption::CapacityBound => HidingBoundClassification::Conditional,
581 }
582}
583
584const fn list_classification(assumption: SecurityAssumption) -> HidingBoundClassification {
585 match assumption {
586 SecurityAssumption::UniqueDecoding | SecurityAssumption::JohnsonBound => {
587 HidingBoundClassification::Proven
588 }
589 SecurityAssumption::CapacityBound => HidingBoundClassification::Conditional,
590 }
591}
592
593const fn combine_classifications(
594 left: HidingBoundClassification,
595 right: HidingBoundClassification,
596) -> HidingBoundClassification {
597 match (left, right) {
598 (HidingBoundClassification::Unavailable, _)
599 | (_, HidingBoundClassification::Unavailable) => HidingBoundClassification::Unavailable,
600 (HidingBoundClassification::Conditional, _)
601 | (_, HidingBoundClassification::Conditional) => HidingBoundClassification::Conditional,
602 (HidingBoundClassification::Approximation, _)
603 | (_, HidingBoundClassification::Approximation) => HidingBoundClassification::Approximation,
604 (HidingBoundClassification::Proven, HidingBoundClassification::Proven) => {
605 HidingBoundClassification::Proven
606 }
607 }
608}
609
610fn sum_probability_bits(bits: impl IntoIterator<Item = f64>) -> f64 {
612 let bits: Vec<f64> = bits.into_iter().collect();
613 let minimum = bits.iter().copied().fold(f64::INFINITY, f64::min);
614 if minimum == f64::INFINITY {
615 return minimum;
616 }
617 let scaled_sum: f64 = bits
618 .into_iter()
619 .map(|value| libm::pow(2., -(value - minimum)))
620 .sum();
621 minimum - libm::log2(scaled_sum)
622}
623
624fn sum_available_probability_bits(bits: impl IntoIterator<Item = Option<f64>>) -> Option<f64> {
625 let bits: Option<Vec<_>> = bits.into_iter().collect();
626 bits.map(sum_probability_bits)
627}
628
629fn sum_available(bits: impl IntoIterator<Item = Option<f64>>) -> Option<f64> {
630 bits.into_iter()
631 .try_fold(0., |sum, value| value.map(|value| sum + value))
632}
633
634fn minimum_available(bits: impl IntoIterator<Item = Option<f64>>) -> Option<f64> {
635 bits.into_iter().try_fold(f64::INFINITY, |minimum, value| {
636 value.map(|value| minimum.min(value))
637 })
638}
639
640fn log2_order_estimate<EF: Field>() -> f64 {
645 const RETAINED_BITS: u64 = 53;
646
647 let order = EF::order();
648 let order_bits = order.bits();
649 let shift = order_bits.saturating_sub(RETAINED_BITS) as usize;
650 let high = (&order >> shift)
651 .to_u64_digits()
652 .first()
653 .copied()
654 .expect("a field has nonzero order");
655 shift as f64 + libm::log2(high as f64)
656}
657
658#[cfg(test)]
659mod tests {
660 use alloc::vec;
661
662 use super::*;
663
664 const FIELD_LOG2_ESTIMATE: f64 = 128.75;
665 const FIELD_LOG2_FLOOR: usize = 128;
666
667 fn generic_report(
668 assumption: SecurityAssumption,
669 source: HidingCodeGeometry,
670 mask_groups: Vec<HidingCodeGeometry>,
671 shared_pow_bits: usize,
672 ) -> HidingBaseCaseSecurityReport {
673 build_hiding_base_case_security_report(
674 100,
675 assumption,
676 FIELD_LOG2_ESTIMATE,
677 FIELD_LOG2_FLOOR,
678 source,
679 mask_groups,
680 shared_pow_bits,
681 )
682 }
683
684 #[test]
685 fn heterogeneous_report_composes_query_and_gamma_terms() {
686 let source = HidingCodeGeometry::new(HidingCodeRole::Source, 2, 1, 16, 1, 4);
687 let mask =
688 HidingCodeGeometry::new(HidingCodeRole::SumcheckMask { round: 0 }, 1, 1, 16, 2, 3);
689 let report = generic_report(SecurityAssumption::UniqueDecoding, source, vec![mask], 5);
690
691 assert_eq!(report.field_order_log2_estimate, FIELD_LOG2_ESTIMATE);
692 assert_eq!(report.field_order_log2_floor, FIELD_LOG2_FLOOR);
693 assert_eq!(report.source.randomized_dimension, 3);
694 assert_eq!(report.source.dyadic_dimension, 4);
695 assert_eq!(report.source.analysis_log_inv_rate, Some(2));
696 assert_eq!(report.mask_groups[0].analysis_log_inv_rate, Some(3));
697 assert_eq!(
698 report.mask_groups[0].role,
699 HidingCodeRole::SumcheckMask { round: 0 }
700 );
701
702 let source_query = SecurityAssumption::UniqueDecoding.queries_error(2, 4);
703 let mask_query = SecurityAssumption::UniqueDecoding.queries_error(3, 3);
704 let weakest_query = source_query.min(mask_query);
705 assert_eq!(
706 report.query_round.source_before_pow_bits,
707 Some(source_query)
708 );
709 assert_eq!(
710 report.query_round.masks_before_pow_bits,
711 vec![Some(mask_query)]
712 );
713 assert_eq!(
714 report.query_round.weakest_before_pow_bits,
715 Some(weakest_query)
716 );
717 assert_eq!(report.query_round.before_pow.bits, Some(weakest_query));
718 assert_eq!(report.query_round.with_pow.bits, Some(weakest_query + 5.));
719 assert_eq!(
720 report.query_round.with_pow.classification,
721 HidingBoundClassification::Conditional
722 );
723
724 let source_mca = SecurityAssumption::UniqueDecoding.prox_gaps_error(2, 2, 128, 2);
725 let mask_mca = SecurityAssumption::UniqueDecoding.prox_gaps_error(1, 3, 128, 2) - 1.;
726 assert_eq!(
727 report.gamma_round.mca_sum.bits,
728 Some(sum_probability_bits([source_mca, mask_mca]))
729 );
730 assert_eq!(report.gamma_round.product_list.log2_size, Some(0.));
731 assert_eq!(
732 report.gamma_round.combined.classification,
733 HidingBoundClassification::Proven
734 );
735 assert_eq!(
736 report.round_by_round.classification,
737 HidingBoundClassification::Proven
738 );
739 assert_eq!(report.round_by_round.bits, Some(weakest_query));
740 assert_eq!(report.round_by_round.proven_bits(), Some(weakest_query));
741 }
742
743 #[test]
744 fn regime_classifications_propagate_without_overstatement() {
745 let source = HidingCodeGeometry::new(HidingCodeRole::Source, 2, 0, 16, 1, 1);
746
747 for (assumption, expected) in [
748 (
749 SecurityAssumption::UniqueDecoding,
750 HidingBoundClassification::Proven,
751 ),
752 (
753 SecurityAssumption::JohnsonBound,
754 HidingBoundClassification::Proven,
755 ),
756 (
757 SecurityAssumption::CapacityBound,
758 HidingBoundClassification::Conditional,
759 ),
760 ] {
761 let report = generic_report(assumption, source, vec![], 0);
762
763 assert_eq!(
764 report.query_round.before_pow.classification,
765 HidingBoundClassification::Proven
766 );
767 assert_eq!(report.gamma_round.combined.classification, expected);
768 assert_eq!(report.round_by_round.classification, expected);
769 assert_eq!(
770 report.round_by_round.proven_bits().is_some(),
771 expected == HidingBoundClassification::Proven
772 );
773 }
774 }
775
776 #[test]
777 fn aggregate_provenance_includes_a_nonbinding_coordinate() {
778 for classification in [
779 HidingBoundClassification::Conditional,
780 HidingBoundClassification::Approximation,
781 ] {
782 let gamma = HidingErrorBound::available(120., classification);
783 let query = HidingErrorBound::available(80., HidingBoundClassification::Proven);
784 let combined = combine_classifications(gamma.classification, query.classification);
785 let round_by_round =
786 HidingErrorBound::available(gamma.bits.unwrap().min(query.bits.unwrap()), combined);
787
788 assert_eq!(round_by_round.bits, Some(80.));
789 assert_eq!(round_by_round.classification, classification);
790 assert_eq!(round_by_round.proven_bits(), None);
791 }
792 }
793
794 #[test]
795 fn proven_bits_rejects_vacuous_proven_terms() {
796 let vacuous = HidingErrorBound::available(-1., HidingBoundClassification::Proven);
797 let positive = HidingErrorBound::available(1., HidingBoundClassification::Proven);
798
799 assert_eq!(vacuous.bits, Some(0.));
800 assert!(vacuous.vacuous);
801 assert_eq!(vacuous.proven_bits(), None);
802 assert_eq!(positive.proven_bits(), Some(1.));
803 }
804
805 #[test]
806 fn analysis_radius_comes_from_log_one_minus_delta() {
807 for log_inv_rate in 1..=16 {
808 for assumption in [
809 SecurityAssumption::UniqueDecoding,
810 SecurityAssumption::JohnsonBound,
811 SecurityAssumption::CapacityBound,
812 ] {
813 let report = generic_report(
814 assumption,
815 HidingCodeGeometry::new(HidingCodeRole::Source, 2, 0, 2 << log_inv_rate, 1, 1),
816 vec![],
817 0,
818 );
819 let radius = report.source.analysis_radius.unwrap();
820
821 assert!(
822 (libm::log2(1. - radius) - assumption.log_1_delta(log_inv_rate)).abs() < 1e-10
823 );
824 }
825 }
826 }
827
828 #[test]
829 fn interleaved_list_uses_eprint_2026_391_lemma_3_13_bound() {
830 let assumption = SecurityAssumption::JohnsonBound;
831 let log_degree = 3;
832 let log_inv_rate = 2;
833 let interleaving = 4;
834 let one_minus_delta = libm::pow(2., assumption.log_1_delta(log_inv_rate));
835
836 let base = assumption.list_size_bits(log_degree, log_inv_rate);
837 let crude = interleaving as f64 * base;
838 let actual = interleaved_list_bits(
839 assumption,
840 log_degree,
841 log_inv_rate,
842 interleaving,
843 one_minus_delta,
844 );
845
846 assert!((actual - 13.228_818_690_495_881).abs() < 1e-12);
847 assert!((crude - 21.287_712_379_549_45).abs() < 1e-12);
848 assert!(actual < crude);
849 }
850
851 #[test]
852 fn saturated_queries_and_rate_one_envelopes_are_reported() {
853 let saturated = generic_report(
854 SecurityAssumption::UniqueDecoding,
855 HidingCodeGeometry::new(HidingCodeRole::Source, 2, 0, 4, 1, 10),
856 vec![],
857 0,
858 );
859 assert_eq!(saturated.source.requested_queries, 10);
860 assert_eq!(saturated.source.effective_queries, 4);
861 assert_eq!(
862 saturated.source.query_miss.bits,
863 Some(SecurityAssumption::UniqueDecoding.queries_error(1, 4))
864 );
865
866 let rate_one = generic_report(
867 SecurityAssumption::UniqueDecoding,
868 HidingCodeGeometry::new(HidingCodeRole::Source, 4, 4, 8, 1, 1),
869 vec![],
870 0,
871 );
872 assert_eq!(rate_one.source.randomized_rate, 1.);
873 assert_eq!(rate_one.source.analysis_log_inv_rate, None);
874 assert_eq!(rate_one.source.analysis_radius, None);
875 assert_eq!(rate_one.source.query_miss.bits, None);
876 assert_eq!(
877 rate_one.source.query_miss.classification,
878 HidingBoundClassification::Unavailable
879 );
880 assert_eq!(rate_one.source.mca.bits, None);
881 assert_eq!(rate_one.source.paired_list.log2_size, None);
882 assert_eq!(rate_one.round_by_round.bits, None);
883 }
884
885 #[test]
886 #[should_panic(expected = "the codeword domain size must be a power of two")]
887 fn geometry_rejects_non_dyadic_domains() {
888 let _ = HidingCodeGeometry::new(HidingCodeRole::Source, 1, 0, 3, 1, 1);
889 }
890
891 #[test]
892 #[should_panic(expected = "the randomized-code dimension must fit")]
893 fn geometry_rejects_dimensions_larger_than_the_domain() {
894 let _ = HidingCodeGeometry::new(HidingCodeRole::Source, 5, 4, 8, 1, 1);
895 }
896
897 #[test]
898 #[should_panic(expected = "the interleaving width must be nonzero")]
899 fn geometry_rejects_zero_width() {
900 let _ = HidingCodeGeometry::new(HidingCodeRole::Source, 1, 0, 8, 0, 1);
901 }
902
903 #[test]
904 fn probability_sum_is_stable_for_large_security_bits() {
905 let actual = sum_probability_bits([2_000., 2_001.]);
906 let expected = 2_000. - libm::log2(1.5);
907
908 assert!((actual - expected).abs() < 1e-12);
909 }
910}