1use p3_binary_dft::EncodableLevel;
12use p3_binary_field::TowerLevel;
13use p3_security::binary::BinaryPcsRegime;
14use thiserror::Error;
15
16const POW_HEADER_BITS: usize = 8;
29
30const POW_COUNTER_BITS: usize = 64;
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct BinaryPcsParams {
36 pub log_inv_rate: usize,
38 pub pow_bits: usize,
40 pub security_level: usize,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct BinaryPcsConfig {
53 params: BinaryPcsParams,
54 committed_field_bits: usize,
55 challenge_field_bits: usize,
56 num_variables: usize,
57 num_queries: usize,
58 log_folding_factor: usize,
59}
60
61#[derive(Clone, Debug, Error, PartialEq, Eq)]
63#[non_exhaustive]
64pub enum BinaryPcsConfigError {
65 #[error("folding factor log {requested} must be in 1..={num_variables}")]
67 InvalidFoldingFactor {
68 requested: usize,
69 num_variables: usize,
70 },
71
72 #[error("a {bits}-bit challenge field is not a priced binary tower level")]
77 UnpricedChallengeField { bits: usize },
78
79 #[error("codeword length 2^{log_len} exceeds the 2^{bits} points a {bits}-bit level spans")]
84 CodewordExceedsAlphabetDomain { log_len: usize, bits: usize },
85
86 #[error("codeword length 2^{log_len} does not fit in a {max_bits}-bit usize index")]
93 CodewordLengthExceedsUsize { log_len: usize, max_bits: usize },
94
95 #[error("polynomial has no variables to fold")]
97 NoVariablesToFold,
98
99 #[error("grinding requests {requested} bits; the witness type admits at most {max}")]
101 PowBitsExceedWitnessCapacity { requested: usize, max: usize },
102
103 #[error("security level {security_level} does not exceed the grinding budget {pow_bits}")]
105 SecurityLevelBelowPowBits {
106 security_level: usize,
107 pow_bits: usize,
108 },
109
110 #[error(
116 "security level {security_level} exceeds the {max} bits the field leaves at this domain size"
117 )]
118 SecurityLevelExceedsFieldCapacity { security_level: usize, max: usize },
119
120 #[error("derived a query count of zero")]
122 ZeroQueries,
123
124 #[error(
136 "the schedule was derived for a {derived}-bit committed alphabet, not a {actual}-bit one"
137 )]
138 CommittedFieldMismatch {
139 derived: usize,
141 actual: usize,
143 },
144
145 #[error("the schedule was derived for a {derived}-bit challenge field, not a {actual}-bit one")]
150 ChallengeFieldMismatch {
151 derived: usize,
153 actual: usize,
155 },
156}
157
158impl BinaryPcsConfig {
159 pub fn try_new<F: EncodableLevel, EF: TowerLevel>(
181 num_variables: usize,
182 params: BinaryPcsParams,
183 ) -> Result<Self, BinaryPcsConfigError> {
184 Self::try_new_with_folding::<F, EF>(num_variables, params, 1)
185 }
186
187 pub fn try_new_with_folding<F: EncodableLevel, EF: TowerLevel>(
193 num_variables: usize,
194 params: BinaryPcsParams,
195 log_folding_factor: usize,
196 ) -> Result<Self, BinaryPcsConfigError> {
197 let max_bits = usize::BITS as usize;
202 let log_len = num_variables.saturating_add(params.log_inv_rate);
203 let in_range = matches!(
204 num_variables.checked_add(params.log_inv_rate),
205 Some(len) if len < max_bits
206 );
207 if !in_range {
208 return Err(BinaryPcsConfigError::CodewordLengthExceedsUsize { log_len, max_bits });
209 }
210
211 if num_variables == 0 {
212 return Err(BinaryPcsConfigError::NoVariablesToFold);
213 }
214
215 if log_folding_factor == 0 || log_folding_factor > num_variables {
216 return Err(BinaryPcsConfigError::InvalidFoldingFactor {
217 requested: log_folding_factor,
218 num_variables,
219 });
220 }
221
222 if log_len > F::bits() {
228 return Err(BinaryPcsConfigError::CodewordExceedsAlphabetDomain {
229 log_len,
230 bits: F::bits(),
231 });
232 }
233
234 let max_pow_bits = F::bits().min(POW_COUNTER_BITS) - POW_HEADER_BITS;
240 if params.pow_bits > max_pow_bits {
241 return Err(BinaryPcsConfigError::PowBitsExceedWitnessCapacity {
242 requested: params.pow_bits,
243 max: max_pow_bits,
244 });
245 }
246
247 if params.security_level <= params.pow_bits {
248 return Err(BinaryPcsConfigError::SecurityLevelBelowPowBits {
249 security_level: params.security_level,
250 pow_bits: params.pow_bits,
251 });
252 }
253
254 let num_queries = BinaryPcsRegime::queries_for_target(
255 params.security_level,
256 params.log_inv_rate,
257 params.pow_bits,
258 );
259 if num_queries == 0 {
260 return Err(BinaryPcsConfigError::ZeroQueries);
261 }
262 let config = Self {
263 params,
264 committed_field_bits: F::bits(),
265 challenge_field_bits: EF::bits(),
266 num_variables,
267 num_queries,
268 log_folding_factor,
269 };
270
271 config.try_security_regime()?;
273 config.validate_security()?;
274 Ok(config)
275 }
276
277 const fn validate_security(&self) -> Result<(), BinaryPcsConfigError> {
278 let max = self.security_regime().max_security_bits();
279 if self.params.security_level > max {
280 return Err(BinaryPcsConfigError::SecurityLevelExceedsFieldCapacity {
281 security_level: self.params.security_level,
282 max,
283 });
284 }
285 Ok(())
286 }
287
288 pub fn try_with_folding(
306 mut self,
307 log_folding_factor: usize,
308 ) -> Result<Self, BinaryPcsConfigError> {
309 if log_folding_factor == 0 || log_folding_factor > self.num_variables {
310 return Err(BinaryPcsConfigError::InvalidFoldingFactor {
311 requested: log_folding_factor,
312 num_variables: self.num_variables,
313 });
314 }
315 self.log_folding_factor = log_folding_factor;
316 self.validate_security()?;
317 Ok(self)
318 }
319
320 #[must_use]
322 pub const fn log_folding_factor(&self) -> usize {
323 self.log_folding_factor
324 }
325
326 #[must_use]
328 pub const fn num_fold_batches(&self) -> usize {
329 self.num_variables.div_ceil(self.log_folding_factor)
330 }
331
332 pub(crate) fn fold_batches(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
334 (0..self.num_variables)
335 .step_by(self.log_folding_factor)
336 .map(|start| {
337 (
338 start,
339 self.log_folding_factor.min(self.num_variables - start),
340 )
341 })
342 }
343
344 #[must_use]
346 pub const fn num_variables(&self) -> usize {
347 self.num_variables
348 }
349
350 #[must_use]
352 pub const fn num_fold_rounds(&self) -> usize {
353 self.num_variables
354 }
355
356 #[must_use]
358 pub const fn log_final_len(&self) -> usize {
359 self.params.log_inv_rate
360 }
361
362 #[must_use]
364 pub(crate) const fn log_domain_size(&self) -> usize {
365 self.num_variables + self.params.log_inv_rate
366 }
367
368 #[must_use]
370 pub(crate) const fn domain_size(&self) -> usize {
371 1usize << self.log_domain_size()
372 }
373
374 #[must_use]
376 pub const fn num_queries(&self) -> usize {
377 self.num_queries
378 }
379
380 #[must_use]
382 pub const fn log_inv_rate(&self) -> usize {
383 self.params.log_inv_rate
384 }
385
386 #[must_use]
388 pub const fn pow_bits(&self) -> usize {
389 self.params.pow_bits
390 }
391
392 #[must_use]
394 pub const fn challenge_field_bits(&self) -> usize {
395 self.challenge_field_bits
396 }
397
398 #[must_use]
400 pub const fn committed_field_bits(&self) -> usize {
401 self.committed_field_bits
402 }
403
404 pub fn check_alphabets<F: EncodableLevel, EF: TowerLevel>(
414 &self,
415 ) -> Result<(), BinaryPcsConfigError> {
416 if self.committed_field_bits != F::bits() {
417 return Err(BinaryPcsConfigError::CommittedFieldMismatch {
418 derived: self.committed_field_bits,
419 actual: F::bits(),
420 });
421 }
422 if self.challenge_field_bits != EF::bits() {
423 return Err(BinaryPcsConfigError::ChallengeFieldMismatch {
424 derived: self.challenge_field_bits,
425 actual: EF::bits(),
426 });
427 }
428 Ok(())
429 }
430
431 #[must_use]
438 pub const fn security_regime(&self) -> BinaryPcsRegime {
439 match self.try_security_regime() {
440 Ok(regime) => regime,
441 Err(_) => panic!("configuration invariants must describe a binary PCS regime"),
442 }
443 }
444
445 const fn try_security_regime(&self) -> Result<BinaryPcsRegime, BinaryPcsConfigError> {
447 match BinaryPcsRegime::new(
448 self.challenge_field_bits,
449 self.num_variables,
450 self.params.log_inv_rate,
451 self.log_folding_factor,
452 self.num_queries,
453 self.params.pow_bits,
454 ) {
455 Some(regime) => Ok(regime),
456 None => Err(BinaryPcsConfigError::UnpricedChallengeField {
457 bits: self.challenge_field_bits,
458 }),
459 }
460 }
461
462 #[must_use]
464 pub const fn security_level(&self) -> usize {
465 self.params.security_level
466 }
467
468 #[must_use]
473 pub fn max_opening_claims(&self) -> usize {
474 self.security_regime()
475 .max_opening_claims(self.params.security_level)
476 }
477
478 #[must_use]
480 pub fn query_security_bits(&self) -> f64 {
481 self.security_regime().query_security_bits()
482 }
483
484 #[must_use]
486 pub const fn field_security_bits(&self) -> usize {
487 self.security_regime().field_security_bits()
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use p3_binary_field::BinaryField128;
494 use p3_field::Field;
495 use p3_security::InstanceShape;
496 use p3_security::binary::BINARY_PCS_FIELD_BITS;
497 use p3_security::fri::{FriRegime, commit_phase_error_udr};
498
499 use super::{BinaryPcsConfig, BinaryPcsConfigError, BinaryPcsParams};
500
501 const fn params() -> BinaryPcsParams {
502 BinaryPcsParams {
503 log_inv_rate: 2,
504 pow_bits: 16,
505 security_level: 100,
506 }
507 }
508
509 #[test]
510 fn the_alphabet_width_matches_the_field() {
511 assert_eq!(BINARY_PCS_FIELD_BITS, BinaryField128::bits());
512 }
513
514 #[test]
515 fn derives_the_fold_schedule_and_query_count() {
516 let config =
517 BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(10, params()).unwrap();
518 assert_eq!(config.num_variables(), 10);
519 assert_eq!(config.num_fold_rounds(), 10);
521 assert_eq!(config.log_final_len(), 2);
523 assert!(config.num_queries() > 0);
524 }
525
526 #[test]
527 fn batched_schedule_keeps_all_variables_and_a_short_final_batch() {
528 let config = BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(7, params())
529 .unwrap()
530 .try_with_folding(3)
531 .unwrap();
532 assert_eq!(
533 config.fold_batches().collect::<alloc::vec::Vec<_>>(),
534 [(0, 3), (3, 3), (6, 1)]
535 );
536 assert_eq!(config.num_fold_rounds(), 7);
537 assert_eq!(config.num_fold_batches(), 3);
538 assert_eq!(config.log_final_len(), 2);
539 assert!(config.query_security_bits() >= 85.0);
540 assert!(
541 BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(7, params())
542 .unwrap()
543 .try_with_folding(0)
544 .is_err()
545 );
546 assert!(
547 BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(7, params())
548 .unwrap()
549 .try_with_folding(8)
550 .is_err()
551 );
552 }
553
554 #[test]
555 fn batching_prices_every_virtual_fold_and_reserves_error_budget() {
556 let config = BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(19, params())
557 .unwrap()
558 .try_with_folding(3)
559 .unwrap();
560 assert_eq!(config.field_security_bits(), 105);
562 let mut p = params();
563 p.security_level = 105;
564 assert!(BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(19, p).is_err());
565 p.security_level = 104;
566 assert!(
567 BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(19, p)
568 .unwrap()
569 .try_with_folding(3)
570 .is_ok()
571 );
572 }
573
574 #[test]
575 fn exhaustive_batching_can_reclaim_the_query_reserve_at_construction() {
576 let params = BinaryPcsParams {
577 log_inv_rate: 2,
578 pow_bits: 0,
579 security_level: 117,
580 };
581 assert!(BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(7, params).is_err());
582 let batched =
583 BinaryPcsConfig::try_new_with_folding::<BinaryField128, BinaryField128>(7, params, 2)
584 .unwrap();
585 assert_eq!(batched.max_opening_claims(), 1012);
586 assert!(batched.try_with_folding(1).is_err());
587 }
588
589 #[test]
592 fn query_security_bits_meets_the_protocol_target() {
593 let params = BinaryPcsParams {
594 log_inv_rate: 2,
595 pow_bits: 8,
596 security_level: 100,
597 };
598 let config =
599 BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(16, params).unwrap();
600 let protocol_target = (params.security_level - params.pow_bits) as f64;
601 assert!(
602 config.query_security_bits() >= protocol_target,
603 "{} does not meet the protocol target {protocol_target}",
604 config.query_security_bits()
605 );
606 }
607
608 #[test]
609 fn query_count_grows_as_the_rate_approaches_one() {
610 let mut low = params();
611 low.log_inv_rate = 1;
612 let mut high = params();
613 high.log_inv_rate = 4;
614 let few = BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(10, high)
615 .unwrap()
616 .num_queries();
617 let many = BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(10, low)
618 .unwrap()
619 .num_queries();
620 assert!(
621 many > few,
622 "a worse rate must demand more queries: {many} vs {few}"
623 );
624 }
625
626 #[test]
629 fn the_field_security_agrees_with_p3_security() {
630 for num_variables in [10, 16, 20, 24] {
631 let config = BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(
632 num_variables,
633 BinaryPcsParams {
634 security_level: 90,
635 ..params()
636 },
637 )
638 .unwrap();
639 let regime = FriRegime {
640 log_blowup: config.log_inv_rate(),
641 num_queries: config.num_queries(),
642 log_final_poly_len: config.log_final_len(),
643 max_log_arity: 1,
644 commit_pow_bits: 0,
645 query_pow_bits: config.pow_bits(),
646 };
647 let mut errors: alloc::vec::Vec<_> = (1..=num_variables)
648 .map(|remaining| {
649 commit_phase_error_udr(
650 ®ime,
651 &InstanceShape {
652 log_trace_length: remaining,
653 modulus_bits: BINARY_PCS_FIELD_BITS,
654 collision_resistance: BINARY_PCS_FIELD_BITS,
655 num_batched_functions: 1,
656 },
657 )
658 .unwrap()
659 })
660 .collect();
661 errors.push(p3_security::ErrorBits::from_log2(
662 128.0 - (2.0 * num_variables as f64).log2(),
663 ));
664 let reference = p3_security::ErrorBits::sum(&errors).bits();
665 let ours = config.field_security_bits() as f64;
666 assert!(ours <= reference && reference - ours < 1.0);
667 }
668 }
669
670 #[test]
671 fn rejects_a_polynomial_with_no_variables() {
672 assert_eq!(
673 BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(0, params()),
674 Err(BinaryPcsConfigError::NoVariablesToFold)
675 );
676 }
677
678 #[test]
679 fn rejects_grinding_beyond_the_witness_capacity() {
680 let mut p = params();
681 p.pow_bits = 57;
682 assert_eq!(
683 BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(10, p),
684 Err(BinaryPcsConfigError::PowBitsExceedWitnessCapacity {
685 requested: 57,
686 max: 56,
687 })
688 );
689 }
690
691 #[test]
692 fn rejects_a_codeword_at_least_as_wide_as_the_index_type() {
693 let max_bits = usize::BITS as usize;
694 let num_variables = max_bits - params().log_inv_rate;
697 assert_eq!(
698 BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(num_variables, params()),
699 Err(BinaryPcsConfigError::CodewordLengthExceedsUsize {
700 log_len: max_bits,
701 max_bits,
702 })
703 );
704 }
705
706 #[test]
707 fn rejects_a_rate_that_derives_zero_queries() {
708 let mut p = params();
711 p.log_inv_rate = 0;
712 assert_eq!(
713 BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(10, p),
714 Err(BinaryPcsConfigError::ZeroQueries)
715 );
716 }
717
718 #[test]
719 fn rejects_a_security_level_at_or_below_the_grinding_budget() {
720 let mut p = params();
721 p.security_level = 16;
722 assert_eq!(
723 BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(10, p),
724 Err(BinaryPcsConfigError::SecurityLevelBelowPowBits {
725 security_level: 16,
726 pow_bits: 16,
727 })
728 );
729 }
730
731 #[test]
735 fn rejects_a_security_level_the_field_cannot_deliver() {
736 let mut p = params();
737 p.security_level = 120;
738 assert_eq!(
739 BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(20, p),
740 Err(BinaryPcsConfigError::SecurityLevelExceedsFieldCapacity {
741 security_level: 120,
742 max: 103,
743 })
744 );
745
746 p.security_level = 103;
748 assert!(BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(20, p).is_ok());
749 }
750
751 #[test]
754 fn the_field_capacity_shrinks_as_the_domain_grows() {
755 let small = BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(10, params())
756 .unwrap()
757 .field_security_bits();
758 let large = BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(20, params())
759 .unwrap()
760 .field_security_bits();
761 assert!(
762 large < small,
763 "a larger domain must leave fewer bits: {large} vs {small}"
764 );
765 }
766}