1use alloc::vec::Vec;
4
5use p3_challenger::{FieldChallenger, GrindingChallenger};
6use p3_field::{ExtensionField, Field};
7use p3_whir::{
8 FoldingFactor, ProtocolParameters, SecurityAssumption, WhirConfig, WhirConfigError, WhirDomain,
9};
10
11use crate::whir::error::ProfileError;
12
13const GRIND_SEARCH_BITS: usize = 64;
17
18const GRIND_MARGIN_BITS: usize = 8;
20
21fn grinding_ceiling<F: Field>() -> usize {
23 F::bits()
24 .min(GRIND_SEARCH_BITS)
25 .saturating_sub(GRIND_MARGIN_BITS)
26}
27
28fn raise_allowance<F: Field>(required: usize) -> Result<usize, ProfileError> {
34 let ceiling = grinding_ceiling::<F>();
35 if required > ceiling {
36 return Err(ProfileError::Grinding { required, ceiling });
37 }
38 Ok(required)
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct BinaryWhirProfile {
50 assumption: SecurityAssumption,
52 security_level: usize,
54 starting_log_inv_rate: usize,
56 folding_factor: usize,
58}
59
60impl BinaryWhirProfile {
61 #[must_use]
63 pub const fn unique_decoding(
64 security_level: usize,
65 starting_log_inv_rate: usize,
66 folding_factor: usize,
67 ) -> Self {
68 Self {
69 assumption: SecurityAssumption::UniqueDecoding,
70 security_level,
71 starting_log_inv_rate,
72 folding_factor,
73 }
74 }
75
76 #[must_use]
80 pub const fn proven_list_decoding(
81 security_level: usize,
82 starting_log_inv_rate: usize,
83 folding_factor: usize,
84 ) -> Self {
85 Self {
86 assumption: SecurityAssumption::JohnsonBound,
87 security_level,
88 starting_log_inv_rate,
89 folding_factor,
90 }
91 }
92
93 #[must_use]
97 pub const fn conjectural(
98 security_level: usize,
99 starting_log_inv_rate: usize,
100 folding_factor: usize,
101 ) -> Self {
102 Self {
103 assumption: SecurityAssumption::CapacityBound,
104 security_level,
105 starting_log_inv_rate,
106 folding_factor,
107 }
108 }
109
110 #[must_use]
112 pub const fn assumption(&self) -> SecurityAssumption {
113 self.assumption
114 }
115
116 #[must_use]
118 pub const fn security_level(&self) -> usize {
119 self.security_level
120 }
121
122 pub fn config<EF, F, Challenger, Domain>(
138 &self,
139 num_variables: usize,
140 domain: &Domain,
141 ) -> Result<WhirConfig<EF, F, Challenger>, ProfileError>
142 where
143 F: Field,
144 EF: ExtensionField<F>,
145 Challenger: FieldChallenger<F> + GrindingChallenger<Witness = F>,
146 Domain: WhirDomain<F, EF>,
147 {
148 let mut pow_bits = 0;
149 loop {
150 let parameters = ProtocolParameters {
151 starting_log_inv_rate: self.starting_log_inv_rate,
152 round_log_inv_rates: Vec::new(),
153 folding_factor: FoldingFactor::Constant(self.folding_factor),
154 soundness_type: self.assumption,
155 security_level: self.security_level,
156 pow_bits,
157 };
158 match WhirConfig::new_with_domain(num_variables, parameters, domain) {
159 Ok(config) => return Ok(config),
160 Err(WhirConfigError::PowBitsExceedBudget { required, .. })
163 if required > pow_bits =>
164 {
165 pow_bits = raise_allowance::<F>(required)?;
166 }
167 Err(WhirConfigError::ZeroQueries) => return Err(ProfileError::ZeroQueries),
171 Err(error) => return Err(ProfileError::Schedule(error)),
172 }
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use p3_binary_field::{BinaryChallenger, BinaryField32, BinaryField64, BinaryField128};
180 use p3_challenger::{GrindingChallenger, HashChallenger};
181 use p3_keccak::Keccak256Hash;
182 use p3_whir::{FoldingFactorError, SecurityAssumption, WhirConfigError};
183
184 use super::{BinaryWhirProfile, grinding_ceiling, raise_allowance};
185 use crate::test_util::MyChallenger;
186 use crate::whir::error::ProfileError;
187 use crate::whir::{BinaryWhirDomain, BooleanWhirDomain, ProofShape};
188
189 type EF = BinaryField128;
190 type NarrowChallenger = BinaryChallenger<BinaryField32, HashChallenger<u8, Keccak256Hash, 32>>;
191
192 const NUM_VARIABLES: usize = 9;
193 const SECURITY_LEVEL: usize = 100;
194 const LOG_INV_RATE: usize = 2;
195 const FOLDING: usize = 3;
196
197 #[test]
198 fn a_schedule_that_opens_no_position_is_refused() {
199 let domain = BooleanWhirDomain::default();
207 let profile = BinaryWhirProfile::proven_list_decoding(0, LOG_INV_RATE, FOLDING);
208 assert!(matches!(
209 profile
210 .config::<EF, EF, MyChallenger, _>(NUM_VARIABLES, &domain)
211 .err(),
212 Some(ProfileError::ZeroQueries)
213 ));
214
215 let real = BinaryWhirProfile::proven_list_decoding(SECURITY_LEVEL, LOG_INV_RATE, FOLDING)
217 .config::<EF, EF, MyChallenger, _>(NUM_VARIABLES, &domain)
218 .unwrap();
219 assert!(real.terminal().num_queries > 0);
220 }
221
222 #[test]
223 fn a_profile_that_folds_nothing_is_refused_by_the_schedule() {
224 let domain = BooleanWhirDomain::default();
230 let profile = BinaryWhirProfile::proven_list_decoding(SECURITY_LEVEL, LOG_INV_RATE, 0);
231 assert!(matches!(
232 profile
233 .config::<EF, EF, MyChallenger, _>(NUM_VARIABLES, &domain)
234 .err(),
235 Some(ProfileError::Schedule(WhirConfigError::FoldingFactor(
236 FoldingFactorError::ZeroFactor
237 )))
238 ));
239 }
240
241 #[test]
242 fn each_regime_is_reached_by_its_own_constructor() {
243 assert_eq!(
244 BinaryWhirProfile::unique_decoding(SECURITY_LEVEL, LOG_INV_RATE, FOLDING).assumption(),
245 SecurityAssumption::UniqueDecoding
246 );
247 assert_eq!(
248 BinaryWhirProfile::proven_list_decoding(SECURITY_LEVEL, LOG_INV_RATE, FOLDING)
249 .assumption(),
250 SecurityAssumption::JohnsonBound
251 );
252 assert_eq!(
253 BinaryWhirProfile::conjectural(SECURITY_LEVEL, LOG_INV_RATE, FOLDING).assumption(),
254 SecurityAssumption::CapacityBound
255 );
256 }
257
258 #[test]
259 fn decoding_further_buys_fewer_opened_positions() {
260 let domain = BooleanWhirDomain::default();
261 let shape = |profile: BinaryWhirProfile| {
262 ProofShape::of(
263 &profile
264 .config::<EF, EF, MyChallenger, _>(NUM_VARIABLES, &domain)
265 .unwrap(),
266 1,
267 )
268 };
269 let unique = shape(BinaryWhirProfile::unique_decoding(
270 SECURITY_LEVEL,
271 LOG_INV_RATE,
272 FOLDING,
273 ));
274 let proven = shape(BinaryWhirProfile::proven_list_decoding(
275 SECURITY_LEVEL,
276 LOG_INV_RATE,
277 FOLDING,
278 ));
279 assert!(proven.stir_queries < unique.stir_queries);
280 }
281
282 #[test]
283 fn the_conjectural_regime_is_refused_by_the_additive_domain() {
284 let refused = BinaryWhirProfile::conjectural(SECURITY_LEVEL, LOG_INV_RATE, FOLDING)
285 .config::<EF, EF, MyChallenger, _>(NUM_VARIABLES, &BooleanWhirDomain::default())
286 .unwrap_err();
287 assert!(matches!(
291 refused,
292 ProfileError::Schedule(WhirConfigError::UnsupportedSecurityAssumption {
293 assumption: SecurityAssumption::CapacityBound
294 })
295 ));
296 assert_eq!(
297 alloc::format!("{refused}"),
298 "the evaluation domain does not support the CapacityBound soundness regime"
299 );
300 }
301
302 fn inside_the_ceiling(derived: &Result<usize, ProfileError>, ceiling: usize) {
304 match derived {
305 Ok(pow_bits) => assert!(*pow_bits <= ceiling, "{pow_bits} bits past {ceiling}"),
306 Err(ProfileError::Grinding {
307 required,
308 ceiling: named,
309 }) => {
310 assert_eq!(*named, ceiling);
311 assert!(*required > ceiling, "{required} bits is not past {ceiling}");
312 }
313 Err(ProfileError::Schedule(_) | ProfileError::ZeroQueries) => {}
315 }
316 }
317
318 #[test]
319 fn the_ceiling_is_the_headroom_the_search_keeps_below_one_witness() {
320 assert_eq!(grinding_ceiling::<BinaryField32>(), 24);
322 assert_eq!(grinding_ceiling::<BinaryField64>(), 56);
323 assert_eq!(grinding_ceiling::<EF>(), 56);
324 }
325
326 #[test]
327 fn a_request_one_bit_past_the_ceiling_is_refused() {
328 let refused = raise_allowance::<BinaryField32>(25).unwrap_err();
330 assert!(
331 matches!(
332 refused,
333 ProfileError::Grinding {
334 required: 25,
335 ceiling: 24
336 }
337 ),
338 "{refused:?}"
339 );
340 assert_eq!(
341 alloc::format!("{refused}"),
342 "the schedule needs 25 grinding bits, one witness allows 24"
343 );
344 assert!(matches!(
345 raise_allowance::<EF>(57).unwrap_err(),
346 ProfileError::Grinding {
347 required: 57,
348 ceiling: 56
349 }
350 ));
351
352 assert_eq!(raise_allowance::<BinaryField32>(24).unwrap(), 24);
354 assert_eq!(raise_allowance::<EF>(56).unwrap(), 56);
355 }
356
357 #[test]
358 fn no_schedule_the_profile_returns_can_abort_the_prover() {
359 let narrow = BinaryWhirDomain::<BinaryField32>::default();
361 let wide = BooleanWhirDomain::default();
362 for security_level in [80, 100, 128, 160, 200, 256] {
363 for log_inv_rate in [1, 2, 3, 4] {
364 for num_variables in [8, 12, 16, 20] {
365 for folding in [2, 3, 4] {
366 let profile = BinaryWhirProfile::proven_list_decoding(
367 security_level,
368 log_inv_rate,
369 folding,
370 );
371 inside_the_ceiling(
372 &profile
373 .config::<EF, BinaryField32, NarrowChallenger, _>(
374 num_variables,
375 &narrow,
376 )
377 .map(|config| config.max_pow_bits()),
378 grinding_ceiling::<BinaryField32>(),
379 );
380 inside_the_ceiling(
381 &profile
382 .config::<EF, EF, MyChallenger, _>(num_variables, &wide)
383 .map(|config| config.max_pow_bits()),
384 grinding_ceiling::<EF>(),
385 );
386 }
387 }
388 }
389 }
390 }
391
392 #[test]
393 #[should_panic = "too small a margin"]
394 fn the_narrow_ceiling_is_the_one_the_challenger_enforces() {
395 let mut challenger = NarrowChallenger::from_hasher(alloc::vec::Vec::new(), Keccak256Hash);
397 let _ = challenger.grind(grinding_ceiling::<BinaryField32>() + 1);
398 }
399
400 #[test]
401 #[should_panic = "too small a margin"]
402 fn the_wide_ceiling_is_the_one_the_challenger_enforces() {
403 let mut challenger = MyChallenger::from_hasher(alloc::vec::Vec::new(), Keccak256Hash);
404 let _ = challenger.grind(grinding_ceiling::<EF>() + 1);
405 }
406}