Skip to main content

p3_binary_pcs/
params.rs

1//! Caller-facing parameters, and the round schedule derived from them.
2//!
3//! [`BinaryPcsParams`] is what a caller picks: a code rate, a grinding budget, and a target
4//! security level. [`BinaryPcsConfig::try_new`] turns those, together with the committed
5//! polynomial's arity, into the schedule the prover and the verifier both read — how many
6//! folds run, how long the final codeword is, and how many queries the unique-decoding regime
7//! demands. Every way that combination can fail to describe a usable protocol is a variant of
8//! [`BinaryPcsConfigError`] and is returned rather than asserted, since the parameters come
9//! from the caller.
10
11use p3_binary_dft::EncodableLevel;
12use p3_binary_field::TowerLevel;
13use p3_security::binary::BinaryPcsRegime;
14use thiserror::Error;
15
16/// Header room the challenger's grinding site reserves above the difficulty.
17///
18/// The challenger asserts `bits + 8 <= min(F::bits(), 64)`.
19///
20/// Which side of that minimum binds depends on the committed alphabet:
21///
22/// ```text
23///     8, 16, 32 bits   ->  the field width binds, so the room is most of it
24///     64, 128 bits     ->  the counter binds, leaving 56 bits of difficulty
25/// ```
26///
27/// At the narrowest encodable level the room is the whole width, so nothing can be ground.
28const POW_HEADER_BITS: usize = 8;
29
30/// Widest grinding counter the challenger samples from.
31const POW_COUNTER_BITS: usize = 64;
32
33/// Parameters chosen by the caller.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct BinaryPcsParams {
36    /// Log of the inverse code rate. The codeword is `2^log_inv_rate` times the message.
37    pub log_inv_rate: usize,
38    /// Grinding bits demanded once, before the query phase.
39    pub pow_bits: usize,
40    /// Target for the union of opening-claim, fold, sumcheck, and query errors, in bits.
41    pub security_level: usize,
42}
43
44/// The round schedule derived from [`BinaryPcsParams`] and the polynomial's arity.
45///
46/// The schedule uses `p3-security` to budget all algebraic opening errors and queries.
47/// Opening protocols are checked against the remaining claim-batching capacity.
48/// In particular it does not price the commitment scheme, which is supplied separately.
49/// A digest narrower than `2 * security_level` bits leaves the reported level undeliverable.
50/// Supplying one wide enough is the caller's obligation.
51#[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/// Why a [`BinaryPcsConfig`] could not be derived.
62#[derive(Clone, Debug, Error, PartialEq, Eq)]
63#[non_exhaustive]
64pub enum BinaryPcsConfigError {
65    /// A batch must fold between one and all remaining message variables.
66    #[error("folding factor log {requested} must be in 1..={num_variables}")]
67    InvalidFoldingFactor {
68        requested: usize,
69        num_variables: usize,
70    },
71
72    /// The challenge field is not one the security model prices.
73    ///
74    /// Every algebraic error is charged against this width.
75    /// A width the model does not recognise would be reported against nothing.
76    #[error("a {bits}-bit challenge field is not a priced binary tower level")]
77    UnpricedChallengeField { bits: usize },
78
79    /// The base codeword is longer than the committed alphabet's additive domain.
80    ///
81    /// A level of `bits` bits spans that many Cantor basis vectors.
82    /// Its domain therefore holds `2^bits` points, and no evaluation exists past them.
83    #[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    /// The codeword length does not fit in a `usize`.
87    ///
88    /// A codeword is indexed by `usize`, so `log_len` must stay below `max_bits`
89    /// (`usize::BITS`): at or past that width, computing the codeword's length as
90    /// `1usize << log_len` is already invalid, regardless of how large the additive domain
91    /// itself is.
92    #[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    /// The committed polynomial has no variables, so there is no round to fold.
96    #[error("polynomial has no variables to fold")]
97    NoVariablesToFold,
98
99    /// The challenger cannot produce a witness this wide.
100    #[error("grinding requests {requested} bits; the witness type admits at most {max}")]
101    PowBitsExceedWitnessCapacity { requested: usize, max: usize },
102
103    /// Grinding would consume the whole budget, leaving nothing for the queries to buy.
104    #[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    /// The target exceeds what the alphabet can deliver at this domain size.
111    ///
112    /// Queries buy back only the proximity term. The fold and sumcheck rounds each lose bits
113    /// to the field's width, and no query count recovers them, so a target above `max` would
114    /// be reported as met while being unreachable at any query count.
115    #[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    /// The derived query count was zero, which would accept any codeword.
121    #[error("derived a query count of zero")]
122    ZeroQueries,
123
124    /// The schedule was derived for a committed alphabet of a different width.
125    ///
126    /// Every cap the derivation applied is a property of that alphabet.
127    ///
128    /// ```text
129    ///     domain cap    the arity and the rate must fit the alphabet's bit width
130    ///     grind cap     the witness is one element, so its width caps the counter
131    ///     encoding      an encoder exists only at the levels the transform covers
132    /// ```
133    ///
134    /// Reusing the schedule at another width would skip all three.
135    #[error(
136        "the schedule was derived for a {derived}-bit committed alphabet, not a {actual}-bit one"
137    )]
138    CommittedFieldMismatch {
139        /// Width the schedule was derived for.
140        derived: usize,
141        /// Width it is being used at.
142        actual: usize,
143    },
144
145    /// The schedule was derived for a challenge field of a different width.
146    ///
147    /// Every algebraic error is charged against that width.
148    /// A wider schedule reused at a narrower field would report bits it cannot deliver.
149    #[error("the schedule was derived for a {derived}-bit challenge field, not a {actual}-bit one")]
150    ChallengeFieldMismatch {
151        /// Width the schedule was derived for.
152        derived: usize,
153        /// Width it is being used at.
154        actual: usize,
155    },
156}
157
158impl BinaryPcsConfig {
159    /// Derive the round schedule for a polynomial in `num_variables` variables.
160    ///
161    /// Every variable is folded: the commit phase lays the whole polynomial out as a single
162    /// codeword column. By default each variable fold commits its own word; use
163    /// [`Self::try_with_folding`] to batch several folds between commitments.
164    ///
165    /// # Errors
166    ///
167    /// Returns a [`BinaryPcsConfigError`] in each of these cases.
168    ///
169    /// - The codeword length does not fit in a `usize`.
170    /// - The polynomial has no variables.
171    /// - The codeword is longer than the committed alphabet's additive domain.
172    /// - Grinding exceeds what the challenger can witness.
173    /// - Grinding exceeds the security budget.
174    /// - The target exceeds what the field can deliver.
175    /// - The derived query count is zero.
176    /// - The challenge field is not a width the security model prices.
177    ///
178    /// The domain cap is the one a caller at a narrow alphabet meets first.
179    /// The arity and the rate expansion together must stay within the alphabet's bit width.
180    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    /// Derive a schedule with batching selected before validating its security target.
188    ///
189    /// Unlike starting with `try_new` and then changing the folding factor, this admits
190    /// targets that need exhaustive batched queries to release the query-error reserve.
191    /// Returns the same configuration errors as [`Self::try_new`], or an invalid fold factor.
192    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        // The codeword length is computed downstream as `1usize << log_len`, so `log_len`
198        // must stay below `usize::BITS` or that shift is already invalid. `checked_add`
199        // guards the sum itself: an adversarial `num_variables` and `log_inv_rate` must not
200        // silently wrap into an in-range `log_len`.
201        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        // The base codeword is evaluated over the committed alphabet's own additive domain.
223        //
224        // That domain has one point per subset of the level's Cantor basis.
225        //
226        // A longer codeword names a point the level does not hold.
227        if log_len > F::bits() {
228            return Err(BinaryPcsConfigError::CodewordExceedsAlphabetDomain {
229                log_len,
230                bits: F::bits(),
231            });
232        }
233
234        // The witness is an element of the committed alphabet, so its width caps the counter.
235        //
236        // The narrowest alphabet a codeword can be encoded over is a byte wide.
237        //
238        // So the width is never below the header room and the difference never underflows.
239        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        // A width the model does not price is rejected here, before anything reads a bound.
272        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    /// Batch up to `log_folding_factor` sequential variable folds between commitments.
289    /// The last batch folds the remaining variables, even when shorter.
290    ///
291    /// For batching, charge every deterministic virtual fold, including those without roots.
292    /// The UDR mutual-agreement bound lifts agreement through each omitted fold: the input
293    /// word is fixed before its challenge, and the query authenticates the whole coset.
294    /// See Appendix A, Theorem 8 of <https://eprint.iacr.org/2024/1553>.
295    /// Sequential independent challenges are used, not powers of one sampled challenge.
296    ///
297    /// Sum `(N / 2^r + 1) / |F|` over every variable fold, plus `2 / |F|` per
298    /// sumcheck round. Reserve one bit each for that sum and the query error so their sum
299    /// meets the requested budget. Query PoW is credited only to the query error.
300    /// Single and batched schedules use the same summed field budget. Exhaustive queries
301    /// have zero error and need no query reserve.
302    ///
303    /// # Errors
304    /// Rejects an empty/oversized batch or a target exceeding the batched field budget.
305    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    /// Maximum number of sequential variable challenges per committed fold batch.
321    #[must_use]
322    pub const fn log_folding_factor(&self) -> usize {
323        self.log_folding_factor
324    }
325
326    /// Number of fold batches, including the final batch sent in the clear.
327    #[must_use]
328    pub const fn num_fold_batches(&self) -> usize {
329        self.num_variables.div_ceil(self.log_folding_factor)
330    }
331
332    /// Each batch's starting variable and number of challenges, in transcript order.
333    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    /// Number of variables of the committed polynomial.
345    #[must_use]
346    pub const fn num_variables(&self) -> usize {
347        self.num_variables
348    }
349
350    /// Number of 2-to-1 codeword folds: one per residual sumcheck round.
351    #[must_use]
352    pub const fn num_fold_rounds(&self) -> usize {
353        self.num_variables
354    }
355
356    /// Log length of the final codeword, which is sent in full.
357    #[must_use]
358    pub const fn log_final_len(&self) -> usize {
359        self.params.log_inv_rate
360    }
361
362    /// Log length of the base codeword: the polynomial's arity blown up by the inverse rate.
363    #[must_use]
364    pub(crate) const fn log_domain_size(&self) -> usize {
365        self.num_variables + self.params.log_inv_rate
366    }
367
368    /// Length of the base codeword committed at commit time.
369    #[must_use]
370    pub(crate) const fn domain_size(&self) -> usize {
371        1usize << self.log_domain_size()
372    }
373
374    /// Number of query indices sampled in the query phase.
375    #[must_use]
376    pub const fn num_queries(&self) -> usize {
377        self.num_queries
378    }
379
380    /// Log of the inverse code rate.
381    #[must_use]
382    pub const fn log_inv_rate(&self) -> usize {
383        self.params.log_inv_rate
384    }
385
386    /// Grinding bits demanded before the query phase.
387    #[must_use]
388    pub const fn pow_bits(&self) -> usize {
389        self.params.pow_bits
390    }
391
392    /// Bit width of the field this schedule draws its challenges from.
393    #[must_use]
394    pub const fn challenge_field_bits(&self) -> usize {
395        self.challenge_field_bits
396    }
397
398    /// Bit width of the alphabet this schedule commits its base codeword over.
399    #[must_use]
400    pub const fn committed_field_bits(&self) -> usize {
401        self.committed_field_bits
402    }
403
404    /// Check that this schedule was derived for exactly this pair of levels.
405    ///
406    /// A level is determined by its width, so comparing the two widths identifies the pair.
407    ///
408    /// Passing means every cap the derivation applied was applied to these two levels.
409    ///
410    /// # Errors
411    ///
412    /// Returns an error if either width differs from the one the schedule was derived for.
413    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    /// Validated security model for this exact fold and query schedule.
432    ///
433    /// # Panics
434    ///
435    /// Never for a configuration this type built.
436    /// Its constructor rejects every shape the model declines to price.
437    #[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    /// The security model, or the reason this schedule has none.
446    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    /// Configured total algebraic opening-security target, excluding hash collisions.
463    #[must_use]
464    pub const fn security_level(&self) -> usize {
465        self.params.security_level
466    }
467
468    /// Conservative capacity for scalar evaluations, counting current and successor columns.
469    ///
470    /// The validated configuration always leaves room for at least one claim. Query grinding
471    /// does not increase this alpha budget: it happens after the batching challenge.
472    #[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    /// Query security before grinding; infinity when every base coset is queried.
479    #[must_use]
480    pub fn query_security_bits(&self) -> f64 {
481        self.security_regime().query_security_bits()
482    }
483
484    /// Whole-bit lower bound for all fold and sumcheck errors, before opening batching.
485    #[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        // Every variable folds the codeword.
520        assert_eq!(config.num_fold_rounds(), 10);
521        // Folding stops when the message is a constant, leaving the rate expansion.
522        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        // Sum (2^(21-r)+1), r=0..18, plus 2*19 = 4_194_353; ceil(log2)=23.
561        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    /// `query_security_bits` reports the achieved security the sampled queries buy back after
590    /// grinding, which must at least meet the protocol's target: `security_level - pow_bits`.
591    #[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    /// Independent comparison with the sum of the FRI per-round UDR bounds and quadratic
627    /// sumcheck errors. Comparing only the largest FRI round would miss omitted folds.
628    #[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                        &regime,
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        // `num_variables + log_inv_rate` lands exactly on `max_bits`, the smallest length
695        // for which `1usize << log_len` is already invalid.
696        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        // At log_inv_rate = 0, log_1_delta(0) = log2(1 + 2^0) - 1 = 0.0 exactly, so the
709        // derived query count saturates to zero regardless of the security level.
710        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    /// A target the alphabet cannot deliver is rejected rather than met on paper by piling on
732    /// queries: at 20 variables and rate `2^-2` the base codeword holds `2^22` symbols, so the
733    /// summed field error plus the query reserve caps the configured target at 103 bits.
734    #[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        // The stated cap is accepted.
747        p.security_level = 103;
748        assert!(BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(20, p).is_ok());
749    }
750
751    /// The cap tightens as the committed polynomial grows: a longer codeword spends more of
752    /// the field's width on the fold's proximity term.
753    #[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}