1use crate::{ErrorBits, SecurityAssumption, SecurityTerm};
23
24pub const BINARY_PCS_OPENING_LABEL: &str = "binary-pcs-opening";
26
27pub const BINARY_PCS_FIELD_BITS: usize = 128;
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct BinaryPcsRegime {
36 challenge_field_bits: usize,
37 num_variables: usize,
38 log_inv_rate: usize,
39 log_folding_factor: usize,
40 num_queries: usize,
41 query_pow_bits: usize,
42}
43
44impl BinaryPcsRegime {
45 pub const fn new(
50 challenge_field_bits: usize,
51 num_variables: usize,
52 log_inv_rate: usize,
53 log_folding_factor: usize,
54 num_queries: usize,
55 query_pow_bits: usize,
56 ) -> Option<Self> {
57 let log_domain = match num_variables.checked_add(log_inv_rate) {
58 Some(value) => value,
59 None => return None,
60 };
61 if challenge_field_bits < 8
62 || challenge_field_bits > BINARY_PCS_FIELD_BITS
63 || !challenge_field_bits.is_power_of_two()
64 || num_variables == 0
65 || log_inv_rate == 0
66 || log_domain >= usize::BITS as usize
67 || log_folding_factor == 0
68 || log_folding_factor > num_variables
69 || num_queries == 0
70 {
71 return None;
72 }
73 Some(Self {
74 challenge_field_bits,
75 num_variables,
76 log_inv_rate,
77 log_folding_factor,
78 num_queries,
79 query_pow_bits,
80 })
81 }
82
83 pub fn queries_for_target(
86 security_level: usize,
87 log_inv_rate: usize,
88 query_pow_bits: usize,
89 ) -> usize {
90 SecurityAssumption::UniqueDecoding.queries(
91 security_level
92 .saturating_add(1)
93 .saturating_sub(query_pow_bits)
94 .max(1),
95 log_inv_rate,
96 )
97 }
98
99 pub const fn challenge_field_bits(&self) -> usize {
101 self.challenge_field_bits
102 }
103
104 pub const fn query_pow_bits(&self) -> usize {
106 self.query_pow_bits
107 }
108
109 pub const fn queries_are_exhaustive(&self) -> bool {
111 self.num_queries
112 >= 1usize << (self.num_variables + self.log_inv_rate - self.log_folding_factor)
113 }
114
115 pub const fn field_error_numerator(&self) -> u128 {
117 (2u128 << (self.num_variables + self.log_inv_rate)) - (2u128 << self.log_inv_rate)
118 + 3 * self.num_variables as u128
119 }
120
121 pub const fn field_security_bits(&self) -> usize {
123 let numerator = self.field_error_numerator();
124 self.challenge_field_bits
125 .saturating_sub((u128::BITS - (numerator - 1).leading_zeros()) as usize)
126 }
127
128 pub const fn max_security_bits(&self) -> usize {
130 self.field_security_bits()
131 .saturating_sub(if self.queries_are_exhaustive() { 0 } else { 1 })
132 }
133
134 pub fn query_security_bits(&self) -> f64 {
136 if self.queries_are_exhaustive() {
137 f64::INFINITY
138 } else {
139 SecurityAssumption::UniqueDecoding.queries_error(self.log_inv_rate, self.num_queries)
140 }
141 }
142
143 pub fn max_opening_claims(&self, security_level: usize) -> usize {
149 let reserve = usize::from(!self.queries_are_exhaustive());
150 if security_level == 0
151 || security_level > self.challenge_field_bits - reserve
152 || self.query_security_bits() + (self.query_pow_bits as f64)
153 < (security_level + reserve) as f64
154 {
155 return 0;
156 }
157 let budget = 1u128 << (self.challenge_field_bits - security_level - reserve);
158 let Some(remaining) = budget.checked_sub(self.field_error_numerator()) else {
159 return 0;
160 };
161 (remaining + 1).min(usize::MAX as u128) as usize
162 }
163
164 pub fn opening_error(&self, num_claims: usize) -> ErrorBits {
169 let numerator = self.field_error_numerator() + num_claims.saturating_sub(1) as u128;
170 let mut rounded = numerator as f64;
171 if (rounded as u128) < numerator {
172 rounded = rounded.next_up();
173 }
174 let field = ErrorBits::from_log2(self.challenge_field_bits as f64 - libm::log2(rounded));
175 let queries = ErrorBits::from_log2(self.query_security_bits() + self.query_pow_bits as f64);
176 ErrorBits::sum(&[field, queries])
177 }
178
179 #[must_use]
181 pub fn opening_term(&self, num_claims: usize) -> SecurityTerm {
182 SecurityTerm::new(BINARY_PCS_OPENING_LABEL, self.opening_error(num_claims))
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn exact_claim_boundaries_and_exhaustive_queries() {
192 let tiny = BinaryPcsRegime::new(128, 1, 2, 1, 200, 0).unwrap();
193 assert_eq!(tiny.field_error_numerator(), 11);
194 assert_eq!(tiny.max_opening_claims(124), 6);
195 assert_eq!(tiny.opening_error(6).bits(), 124.0);
196 assert!(tiny.opening_error(7).bits() < 124.0);
197 let batched = BinaryPcsRegime::new(128, 4, 2, 2, 200, 0).unwrap();
198 assert_eq!(batched.field_error_numerator(), 132);
199 assert_eq!(batched.max_opening_claims(119), 381);
201 assert_eq!(batched.opening_error(381).bits(), 119.0);
202 assert!(batched.opening_error(382).bits() < 119.0);
203 }
204
205 #[test]
206 fn an_exhausted_or_invalid_budget_has_zero_capacity() {
207 let tiny = BinaryPcsRegime::new(128, 1, 2, 1, 200, 0).unwrap();
208 for target in [0, 125, 128, 129, usize::MAX] {
209 assert_eq!(tiny.max_opening_claims(target), 0);
210 }
211 let too_few_queries = BinaryPcsRegime::new(128, 10, 2, 1, 1, 0).unwrap();
212 assert_eq!(too_few_queries.max_opening_claims(100), 0);
213 assert!(BinaryPcsRegime::new(128, 0, 2, 1, 1, 0).is_none());
214 assert!(BinaryPcsRegime::new(128, 1, 0, 1, 1, 0).is_none());
215 assert!(BinaryPcsRegime::new(128, 1, 2, 2, 1, 0).is_none());
216 assert!(BinaryPcsRegime::new(128, 1, 2, 1, 0, 0).is_none());
217 assert!(BinaryPcsRegime::new(128, usize::MAX, 2, 1, 1, 0).is_none());
218 }
219
220 #[test]
221 fn a_narrower_challenge_field_loses_exactly_its_missing_bits() {
222 let wide = BinaryPcsRegime::new(128, 10, 2, 1, 150, 0).unwrap();
230 let narrow = BinaryPcsRegime::new(64, 10, 2, 1, 150, 0).unwrap();
231
232 assert_eq!(wide.field_error_numerator(), 8214);
233 assert_eq!(narrow.field_error_numerator(), 8214);
234 assert_eq!(wide.field_security_bits(), 114);
235 assert_eq!(narrow.field_security_bits(), 50);
236
237 assert_eq!(narrow.max_opening_claims(100), 0);
239 assert!(wide.max_opening_claims(100) > 0);
240
241 assert!(narrow.opening_error(4).bits() < wide.opening_error(4).bits());
245
246 let wide_all = BinaryPcsRegime::new(128, 4, 2, 2, 200, 0).unwrap();
248 let narrow_all = BinaryPcsRegime::new(64, 4, 2, 2, 200, 0).unwrap();
249 let gap = wide_all.opening_error(4).bits() - narrow_all.opening_error(4).bits();
250 assert!((gap - 64.0).abs() < 1e-9, "{gap}");
251
252 assert!(BinaryPcsRegime::new(96, 10, 2, 1, 150, 0).is_none());
254 assert!(BinaryPcsRegime::new(256, 10, 2, 1, 150, 0).is_none());
255 assert!(BinaryPcsRegime::new(4, 10, 2, 1, 150, 0).is_none());
256 }
257
258 #[test]
259 fn query_grinding_cannot_subsidize_alpha_or_folds() {
260 let plain = BinaryPcsRegime::new(128, 10, 2, 1, 150, 0).unwrap();
261 let ground = BinaryPcsRegime::new(128, 10, 2, 1, 150, 16).unwrap();
262 assert_eq!(plain.field_error_numerator(), 8214);
263 assert_eq!(
264 plain.max_opening_claims(100),
265 ground.max_opening_claims(100)
266 );
267 assert_eq!(plain.max_opening_claims(100), 134_209_515);
268 assert!(plain.opening_error(2).bits() >= 100.0);
269 assert!(ground.opening_error(2).bits() > plain.opening_error(2).bits());
270 }
271
272 #[test]
273 fn both_folding_schedules_charge_every_round() {
274 let single = BinaryPcsRegime::new(128, 20, 2, 1, 150, 0).unwrap();
275 let batched = BinaryPcsRegime::new(128, 20, 2, 3, 150, 0).unwrap();
276 assert_eq!(single.field_error_numerator(), 8_388_660);
277 assert_eq!(
278 single.field_error_numerator(),
279 batched.field_error_numerator()
280 );
281 assert_eq!(single.max_security_bits(), 103);
282 }
283}