Skip to main content

p3_binary_pcs/whir/
domain.rs

1//! Binary-field evaluation domains for the WHIR commitment scheme.
2
3use 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
13/// Widest domain a machine word can index.
14///
15/// A query index is a word, so a wider subspace could never be addressed.
16const MAX_LOG_DOMAIN_SIZE: usize = 64;
17
18/// A binary alphabet the additive transform encodes over.
19///
20/// Two alphabets of the same width can still carry different codes, so each one names itself.
21///
22/// The label separates one code from every other inside a shared transcript.
23pub trait BinaryWhirAlphabet: TowerLevel {
24    /// Stable label bound into the Fiat--Shamir instance description.
25    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/// Returns a Merkle cap height that every round's tree can carry.
41///
42/// One cap node covers each deepest stratum, so authentication paths stop at a protocol-fixed layer.
43///
44/// A round that opens every position draws nothing and fixes no stratum.
45///
46/// All rounds share one commitment scheme, so the shallowest tree bounds the result.
47#[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        // A domain too wide to size as a machine word can never be saturated.
69        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/// WHIR's Reed--Solomon code over nested binary Cantor subspaces.
84///
85/// The alphabet is a type parameter, so a narrow trace is encoded at its own width.
86///
87/// A folded value wider than the alphabet is encoded as one column per coordinate.
88///
89/// Every butterfly therefore keeps its twiddle in the alphabet.
90#[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    /// Build the domain around an additive transform implementation.
97    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
110/// The largest subspace dimension an alphabet supports.
111fn max_log_domain_size<F: TowerLevel>() -> usize {
112    (1usize << F::LOG_BITS).min(MAX_LOG_DOMAIN_SIZE)
113}
114
115/// Encode a padded matrix whose entries are wider than the alphabet.
116///
117/// Each entry becomes one column per coordinate, so every butterfly stays in the alphabet.
118fn 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
137/// Selector coordinates of one queried codeword position.
138fn 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
160/// Whether a regime's distance assumption holds for a characteristic-two subspace domain.
161///
162/// Capacity is refuted there, so it is the one regime refused.
163const 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
350/// The additive domain a bit witness packed into the widest tower level commits over.
351pub 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        // Every tested rate extends beyond the message subspace.
416        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                // The selector must recover the encoded row at every domain index.
428                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                // A changed codeword value must fail the same selector identity.
434                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        // The subspace is wider than a machine word, so the index type bounds it.
452        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        // A sixty-four-bit polynomial basis and a sixty-four-bit tower level are different codes.
462        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}