1use p3_binary_dft::{AdditiveRsEncoder, LchNtt, PolyBasisNtt, domain_point, subspace_polynomial};
4use p3_binary_field::{BinaryField32, BinaryField128, Poly64, TowerLevel};
5use p3_challenger::{FieldChallenger, GrindingChallenger};
6use p3_commit::Encoder;
7use p3_field::{BasedVectorSpace, ExtensionField, Field};
8use p3_matrix::dense::RowMajorMatrix;
9use p3_multilinear_util::point::Point;
10use p3_whir::transcript::query_draws;
11use p3_whir::{SecurityAssumption, WhirConfig, WhirDomain, WhirQueryPoint};
12
13const MAX_LOG_DOMAIN_SIZE: usize = 64;
17
18pub trait BinaryWhirAlphabet: TowerLevel {
24 const DOMAIN_ID: &'static [u8];
26}
27
28impl BinaryWhirAlphabet for Poly64 {
29 const DOMAIN_ID: &'static [u8] = b"p3-whir-domain:cantor-novel-basis-v1";
30}
31
32impl BinaryWhirAlphabet for BinaryField32 {
33 const DOMAIN_ID: &'static [u8] = b"p3-whir-domain:cantor-tower-32-v1";
34}
35
36impl BinaryWhirAlphabet for BinaryField128 {
37 const DOMAIN_ID: &'static [u8] = b"p3-whir-domain:cantor-tower-128-v1";
38}
39
40#[must_use]
48pub fn recommended_cap_height<EF, F, Challenger>(config: &WhirConfig<EF, F, Challenger>) -> usize
49where
50 F: Field,
51 EF: ExtensionField<F>,
52 Challenger: FieldChallenger<F> + GrindingChallenger<Witness = F>,
53{
54 let mut deepest = 0;
55 let mut shallowest = usize::MAX;
56
57 for (log_folded_domain_size, queries) in config
58 .round_parameters()
59 .iter()
60 .map(|round| (round.log_folded_domain_size, round.num_queries))
61 .chain(core::iter::once((
62 config.final_round_config().log_folded_domain_size,
63 config.terminal().num_queries,
64 )))
65 {
66 shallowest = shallowest.min(log_folded_domain_size);
67
68 let draws = 1usize
70 .checked_shl(log_folded_domain_size as u32)
71 .map_or(queries, |folded_domain_size| {
72 query_draws(folded_domain_size, queries)
73 });
74
75 if draws != 0 {
76 deepest = deepest.max(draws.ilog2() as usize);
77 }
78 }
79
80 deepest.min(shallowest)
81}
82
83#[derive(Clone, Debug)]
91pub struct BinaryWhirDomain<F = Poly64, Ntt = LchNtt<F>> {
92 encoder: AdditiveRsEncoder<F, Ntt>,
93}
94
95impl<F, Ntt> BinaryWhirDomain<F, Ntt> {
96 pub const fn new(ntt: Ntt) -> Self {
98 Self {
99 encoder: AdditiveRsEncoder::new(ntt),
100 }
101 }
102}
103
104impl<F, Ntt: Default> Default for BinaryWhirDomain<F, Ntt> {
105 fn default() -> Self {
106 Self::new(Ntt::default())
107 }
108}
109
110fn max_log_domain_size<F: TowerLevel>() -> usize {
112 (1usize << F::LOG_BITS).min(MAX_LOG_DOMAIN_SIZE)
113}
114
115fn encode_extension<F, EF, E>(
119 encoder: &E,
120 message: RowMajorMatrix<EF>,
121 log_inv_rate: usize,
122) -> RowMajorMatrix<EF>
123where
124 F: TowerLevel,
125 EF: ExtensionField<F>,
126 E: Encoder<F>,
127{
128 let width = message.width;
129 let coefficients = EF::flatten_to_base(message.values);
130 let encoded = encoder.encode_batch_padded(
131 RowMajorMatrix::new(coefficients, width * <EF as BasedVectorSpace<F>>::DIMENSION),
132 log_inv_rate,
133 );
134 RowMajorMatrix::new(EF::reconstitute_from_base(encoded.values), width)
135}
136
137fn query_point<F: TowerLevel>(
139 log_domain_size: usize,
140 num_variables: usize,
141 index: usize,
142) -> WhirQueryPoint<F> {
143 assert!(
144 log_domain_size <= max_log_domain_size::<F>(),
145 "additive domain exceeds the alphabet dimension"
146 );
147 assert!(
148 log_domain_size == MAX_LOG_DOMAIN_SIZE || index < 1usize << log_domain_size,
149 "domain index is out of range"
150 );
151 let point = domain_point(index);
152 WhirQueryPoint::Multilinear(Point::new(
153 (0..num_variables)
154 .rev()
155 .map(|j| subspace_polynomial(j, point))
156 .collect(),
157 ))
158}
159
160const fn supports(assumption: SecurityAssumption) -> bool {
164 !matches!(assumption, SecurityAssumption::CapacityBound)
165}
166
167impl<Ntt> Encoder<Poly64> for BinaryWhirDomain<Poly64, Ntt>
168where
169 AdditiveRsEncoder<Poly64, Ntt>: Encoder<Poly64>,
170{
171 fn encode_batch(
172 &self,
173 message: RowMajorMatrix<Poly64>,
174 log_inv_rate: usize,
175 ) -> RowMajorMatrix<Poly64> {
176 self.encoder.encode_batch(message, log_inv_rate)
177 }
178
179 fn encode_batch_padded(
180 &self,
181 message: RowMajorMatrix<Poly64>,
182 log_inv_rate: usize,
183 ) -> RowMajorMatrix<Poly64> {
184 self.encoder.encode_batch_padded(message, log_inv_rate)
185 }
186}
187
188impl<EF, Ntt> WhirDomain<Poly64, EF> for BinaryWhirDomain<Poly64, Ntt>
189where
190 EF: ExtensionField<Poly64>,
191 Ntt: Sync,
192 AdditiveRsEncoder<Poly64, Ntt>: Encoder<Poly64>,
193{
194 fn protocol_id(&self) -> &'static [u8] {
195 <Poly64 as BinaryWhirAlphabet>::DOMAIN_ID
196 }
197
198 fn supports_security_assumption(&self, assumption: SecurityAssumption) -> bool {
199 supports(assumption)
200 }
201
202 fn stratified_queries(&self) -> bool {
203 true
204 }
205
206 fn max_log_domain_size(&self) -> usize {
207 max_log_domain_size::<Poly64>()
208 }
209
210 fn encode_extension_batch_padded(
211 &self,
212 message: RowMajorMatrix<EF>,
213 log_inv_rate: usize,
214 ) -> RowMajorMatrix<EF> {
215 encode_extension::<Poly64, EF, _>(&self.encoder, message, log_inv_rate)
216 }
217
218 fn query_point(
219 &self,
220 log_domain_size: usize,
221 num_variables: usize,
222 index: usize,
223 ) -> WhirQueryPoint<Poly64> {
224 query_point::<Poly64>(log_domain_size, num_variables, index)
225 }
226}
227
228impl<Ntt> Encoder<BinaryField32> for BinaryWhirDomain<BinaryField32, Ntt>
229where
230 AdditiveRsEncoder<BinaryField32, Ntt>: Encoder<BinaryField32>,
231{
232 fn encode_batch(
233 &self,
234 message: RowMajorMatrix<BinaryField32>,
235 log_inv_rate: usize,
236 ) -> RowMajorMatrix<BinaryField32> {
237 self.encoder.encode_batch(message, log_inv_rate)
238 }
239
240 fn encode_batch_padded(
241 &self,
242 message: RowMajorMatrix<BinaryField32>,
243 log_inv_rate: usize,
244 ) -> RowMajorMatrix<BinaryField32> {
245 self.encoder.encode_batch_padded(message, log_inv_rate)
246 }
247}
248
249impl<EF, Ntt> WhirDomain<BinaryField32, EF> for BinaryWhirDomain<BinaryField32, Ntt>
250where
251 EF: ExtensionField<BinaryField32>,
252 Ntt: Sync,
253 AdditiveRsEncoder<BinaryField32, Ntt>: Encoder<BinaryField32>,
254{
255 fn protocol_id(&self) -> &'static [u8] {
256 <BinaryField32 as BinaryWhirAlphabet>::DOMAIN_ID
257 }
258
259 fn supports_security_assumption(&self, assumption: SecurityAssumption) -> bool {
260 supports(assumption)
261 }
262
263 fn stratified_queries(&self) -> bool {
264 true
265 }
266
267 fn max_log_domain_size(&self) -> usize {
268 max_log_domain_size::<BinaryField32>()
269 }
270
271 fn encode_extension_batch_padded(
272 &self,
273 message: RowMajorMatrix<EF>,
274 log_inv_rate: usize,
275 ) -> RowMajorMatrix<EF> {
276 encode_extension::<BinaryField32, EF, _>(&self.encoder, message, log_inv_rate)
277 }
278
279 fn query_point(
280 &self,
281 log_domain_size: usize,
282 num_variables: usize,
283 index: usize,
284 ) -> WhirQueryPoint<BinaryField32> {
285 query_point::<BinaryField32>(log_domain_size, num_variables, index)
286 }
287}
288
289impl<Ntt> Encoder<BinaryField128> for BinaryWhirDomain<BinaryField128, Ntt>
290where
291 AdditiveRsEncoder<BinaryField128, Ntt>: Encoder<BinaryField128>,
292{
293 fn encode_batch(
294 &self,
295 message: RowMajorMatrix<BinaryField128>,
296 log_inv_rate: usize,
297 ) -> RowMajorMatrix<BinaryField128> {
298 self.encoder.encode_batch(message, log_inv_rate)
299 }
300
301 fn encode_batch_padded(
302 &self,
303 message: RowMajorMatrix<BinaryField128>,
304 log_inv_rate: usize,
305 ) -> RowMajorMatrix<BinaryField128> {
306 self.encoder.encode_batch_padded(message, log_inv_rate)
307 }
308}
309
310impl<EF, Ntt> WhirDomain<BinaryField128, EF> for BinaryWhirDomain<BinaryField128, Ntt>
311where
312 EF: ExtensionField<BinaryField128>,
313 Ntt: Sync,
314 AdditiveRsEncoder<BinaryField128, Ntt>: Encoder<BinaryField128>,
315{
316 fn protocol_id(&self) -> &'static [u8] {
317 <BinaryField128 as BinaryWhirAlphabet>::DOMAIN_ID
318 }
319
320 fn supports_security_assumption(&self, assumption: SecurityAssumption) -> bool {
321 supports(assumption)
322 }
323
324 fn stratified_queries(&self) -> bool {
325 true
326 }
327
328 fn max_log_domain_size(&self) -> usize {
329 max_log_domain_size::<BinaryField128>()
330 }
331
332 fn encode_extension_batch_padded(
333 &self,
334 message: RowMajorMatrix<EF>,
335 log_inv_rate: usize,
336 ) -> RowMajorMatrix<EF> {
337 encode_extension::<BinaryField128, EF, _>(&self.encoder, message, log_inv_rate)
338 }
339
340 fn query_point(
341 &self,
342 log_domain_size: usize,
343 num_variables: usize,
344 index: usize,
345 ) -> WhirQueryPoint<BinaryField128> {
346 query_point::<BinaryField128>(log_domain_size, num_variables, index)
347 }
348}
349
350pub type BooleanWhirDomain = BinaryWhirDomain<BinaryField128, PolyBasisNtt>;
352
353#[cfg(test)]
354mod tests {
355 use alloc::vec::Vec;
356
357 use p3_binary_dft::NaiveAdditiveNtt;
358 use p3_binary_field::Poly192;
359 use p3_commit::Encoder;
360 use p3_field::PrimeCharacteristicRing;
361 use p3_matrix::Matrix;
362 use p3_matrix::dense::RowMajorMatrix;
363 use p3_multilinear_util::poly::Poly;
364 use p3_sumcheck::constraints::statement::SelectStatement;
365 use p3_whir::{WhirDomain, WhirQueryPoint};
366
367 use super::{BinaryField32, BinaryField128, BinaryWhirDomain, Poly64};
368
369 #[test]
370 fn extension_encoding_matches_the_definition() {
371 let width = 2;
372 let log_message_height = 3;
373 let log_inv_rate = 2;
374 let mut values = Poly192::zero_vec(width << (log_message_height + log_inv_rate));
375 for (index, value) in values[..width << log_message_height].iter_mut().enumerate() {
376 *value = Poly192::new([
377 Poly64::new(index as u64 + 1),
378 Poly64::new((3 * index) as u64 + 2),
379 Poly64::new((5 * index) as u64 + 4),
380 ]);
381 }
382 let message = RowMajorMatrix::new(values, width);
383 let fast_domain: BinaryWhirDomain<Poly64> = BinaryWhirDomain::default();
384 let fast = WhirDomain::<Poly64, Poly192>::encode_extension_batch_padded(
385 &fast_domain,
386 message.clone(),
387 log_inv_rate,
388 );
389 let reference = BinaryWhirDomain::<Poly64, _>::new(NaiveAdditiveNtt::<Poly64>::default());
390 let reference = WhirDomain::<Poly64, Poly192>::encode_extension_batch_padded(
391 &reference,
392 message,
393 log_inv_rate,
394 );
395 assert_eq!(fast, reference);
396 }
397
398 #[test]
399 fn base_encoding_matches_the_reference_transform() {
400 let message = RowMajorMatrix::new((0..16).map(|index| Poly64::new(index + 1)).collect(), 2);
401 let fast_domain: BinaryWhirDomain<Poly64> = BinaryWhirDomain::default();
402 let fast = fast_domain.encode_batch(message.clone(), 2);
403 let reference = BinaryWhirDomain::<Poly64, _>::new(NaiveAdditiveNtt::<Poly64>::default())
404 .encode_batch(message, 2);
405 assert_eq!(fast, reference);
406 }
407
408 #[test]
409 fn query_coordinates_evaluate_the_encoded_novel_basis_polynomial() {
410 let coefficients = (0..8)
411 .map(|index| Poly64::new((13 * index + 7) as u64))
412 .collect::<Vec<_>>();
413 let domain: BinaryWhirDomain<Poly64> = BinaryWhirDomain::default();
414
415 for log_inv_rate in 1..=3 {
417 let encoded =
418 domain.encode_batch(RowMajorMatrix::new(coefficients.clone(), 1), log_inv_rate);
419
420 for index in 0..encoded.height() {
421 let WhirQueryPoint::Multilinear(point) =
422 WhirDomain::<Poly64, Poly192>::query_point(&domain, 3 + log_inv_rate, 3, index)
423 else {
424 panic!("the additive domain must expose direct selector coordinates");
425 };
426
427 let poly = Poly::new(coefficients.clone());
429 let mut statement = SelectStatement::initialize(3);
430 statement.add_point_constraint(point.clone(), encoded.values[index]);
431 assert!(statement.verify(&poly));
432
433 let mut tampered = SelectStatement::initialize(3);
435 tampered.add_point_constraint(point, encoded.values[index] + Poly64::ONE);
436 assert!(!tampered.verify(&poly));
437 }
438 }
439 }
440
441 #[test]
442 fn every_alphabet_reports_its_own_capacity_and_label() {
443 let word: BinaryWhirDomain<BinaryField32> = BinaryWhirDomain::default();
444 let wide = super::BooleanWhirDomain::default();
445 let poly: BinaryWhirDomain<Poly64> = BinaryWhirDomain::default();
446
447 assert_eq!(
448 WhirDomain::<BinaryField32, BinaryField128>::max_log_domain_size(&word),
449 32
450 );
451 assert_eq!(
453 WhirDomain::<BinaryField128, BinaryField128>::max_log_domain_size(&wide),
454 64
455 );
456 assert_eq!(
457 WhirDomain::<Poly64, Poly192>::max_log_domain_size(&poly),
458 64
459 );
460
461 assert_ne!(
463 WhirDomain::<Poly64, Poly192>::protocol_id(&poly),
464 WhirDomain::<BinaryField128, BinaryField128>::protocol_id(&wide)
465 );
466 }
467
468 #[test]
469 fn a_narrow_alphabet_encodes_through_the_same_selector_identity() {
470 let coefficients = (0..8)
471 .map(|index| BinaryField32::from_u32(13 * index + 7))
472 .collect::<Vec<_>>();
473 let domain: BinaryWhirDomain<BinaryField32> = BinaryWhirDomain::default();
474 let encoded = domain.encode_batch(RowMajorMatrix::new(coefficients.clone(), 1), 2);
475
476 for index in 0..encoded.height() {
477 let WhirQueryPoint::Multilinear(point) =
478 WhirDomain::<BinaryField32, BinaryField128>::query_point(&domain, 5, 3, index)
479 else {
480 panic!("the additive domain must expose direct selector coordinates");
481 };
482 let poly = Poly::new(coefficients.clone());
483 let mut statement = SelectStatement::initialize(3);
484 statement.add_point_constraint(point, encoded.values[index]);
485 assert!(statement.verify(&poly));
486 }
487 }
488}