1use crate::{
80 CombFn,
81 sumcheck::{
82 SumCheckError,
83 multi_degree::{MultiDegreeSumcheckGroup, Round1FastPath, Round1Output},
84 prover::ProverState as SumcheckProverState,
85 },
86};
87use crypto_primitives::{
88 BaseFieldConfig, ProjectPrimitiveIntegersWithConfig, SemiringConfig, SetConfig, Wrapper,
89};
90use itertools::Itertools;
91use std::{marker::PhantomData, slice};
92use thiserror::Error;
93use zinc_poly::{
94 EvaluationError,
95 mle::DenseMultilinearExtension,
96 univariate::binary::BinaryPoly,
97 utils::{ArithErrors, build_eq_x_r, eq_eval},
98};
99use zinc_transcript::{
100 delegate_transcribable,
101 traits::{ConstTranscribable, Transcript},
102};
103use zinc_uair::AffineVirtualSpec;
104use zinc_utils::{add, mul, powers};
105
106pub struct BooleanityChecker<C: SemiringConfig>(PhantomData<C>);
112
113type BooleanityZerocheckSetup<F> = (Vec<F>, Vec<F>, CombFn<F>);
114
115#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct BooleanityProof<F> {
125 pub bit_slice_evals: Vec<F>,
128}
129
130impl<F> BooleanityProof<F> {
131 pub fn try_map<T, E>(
135 &self,
136 f: impl FnMut(&F) -> Result<T, E> + Copy,
137 ) -> Result<BooleanityProof<T>, E> {
138 Ok(BooleanityProof {
139 bit_slice_evals: self.bit_slice_evals.iter().map(f).try_collect()?,
140 })
141 }
142}
143
144delegate_transcribable!(BooleanityProof<F> { bit_slice_evals: Vec<F> }
145 where F: ConstTranscribable);
146
147pub struct BoolProverAncillary {
150 pub num_wit_bin_cols: usize,
155 pub bit_width: usize,
157 pub num_vars: usize,
159}
160
161pub struct BoolVerifierAncillary<F> {
164 pub alpha_powers: Vec<F>,
167 pub zerocheck_point: Vec<F>,
169 pub num_wit_bin_cols: usize,
171 pub bit_width: usize,
173 pub num_vars: usize,
175}
176
177#[derive(Clone, Debug)]
186pub struct BoolVerifierSubclaim<F> {
187 pub bit_slice_evals: Vec<F>,
189}
190
191impl<C> BooleanityChecker<C>
196where
197 C: BaseFieldConfig + ProjectPrimitiveIntegersWithConfig + 'static,
198 C::Integer: ConstTranscribable,
199{
200 pub fn prepare_sumcheck_group<const D: usize>(
209 transcript: &mut impl Transcript,
210 trace_bin_poly: &[DenseMultilinearExtension<BinaryPoly<D>>],
211 num_vars: usize,
212 field_cfg: &C,
213 ) -> Result<(MultiDegreeSumcheckGroup<C>, BoolProverAncillary), BooleanityError<C::Element>>
214 {
215 let n = trace_bin_poly.len();
216 let (r, alpha_powers, comb_fn) =
217 booleanity_zerocheck_setup(transcript, n, D, num_vars, field_cfg);
218
219 let one = field_cfg.one();
223 let eq_other_table: Vec<C::Element> = if num_vars <= 1 {
224 vec![one]
225 } else {
226 build_eq_x_r(field_cfg, &r[1..])?.evaluations
227 };
228
229 let r_first_coord = r
230 .into_iter()
231 .next()
232 .expect("num_vars >= 1 guarantees a first coordinate");
233
234 let fast_path = BooleanityRound1FastPath::<C, D> {
235 binary_cols: trace_bin_poly.to_vec(),
236 alpha_powers,
237 eq_other_table,
238 r_first_coord,
239 num_vars,
240 };
241
242 Ok((
243 MultiDegreeSumcheckGroup::new_with_fast_path(
244 3,
245 Vec::new(),
246 comb_fn,
247 Box::new(fast_path),
248 ),
249 BoolProverAncillary {
250 num_wit_bin_cols: n,
251 bit_width: D,
252 num_vars,
253 },
254 ))
255 }
256
257 pub fn prepare_affine_virtual_sumcheck_group<const D: usize>(
265 transcript: &mut impl Transcript,
266 all_trace_bin_poly: &[DenseMultilinearExtension<BinaryPoly<D>>],
267 affine_virtual_specs: &[AffineVirtualSpec],
268 num_vars: usize,
269 field_cfg: &C,
270 ) -> Result<(MultiDegreeSumcheckGroup<C>, BoolProverAncillary), BooleanityError<C::Element>>
271 {
272 if affine_virtual_specs.is_empty() {
273 return Err(BooleanityError::NoAffineVirtualSpecs);
274 }
275 if all_trace_bin_poly.is_empty() {
276 return Err(BooleanityError::NoBinaryPolyColumns);
277 }
278
279 let bit_slice_mles = build_affine_virtual_bit_slice_mles::<C, D>(
280 all_trace_bin_poly,
281 affine_virtual_specs,
282 num_vars,
283 field_cfg,
284 );
285 let num_cols = affine_virtual_specs.len();
286 let active_len = num_cols.saturating_mul(D);
287 let (r, _, comb_fn) =
288 booleanity_zerocheck_setup(transcript, num_cols, D, num_vars, field_cfg);
289
290 debug_assert_eq!(bit_slice_mles.len(), active_len);
291
292 let mut mles = Vec::with_capacity(add!(1, bit_slice_mles.len()));
293 mles.push(build_eq_x_r(field_cfg, &r)?);
294 mles.extend(bit_slice_mles);
295
296 Ok((
297 MultiDegreeSumcheckGroup::new(3, mles, comb_fn),
298 BoolProverAncillary {
299 num_wit_bin_cols: num_cols,
300 bit_width: D,
301 num_vars,
302 },
303 ))
304 }
305
306 pub fn finalize_prover(
314 transcript: &mut impl Transcript,
315 sumcheck_prover_state: SumcheckProverState<C>,
316 ancillary: BoolProverAncillary,
317 field_cfg: &C,
318 ) -> Result<BooleanityProof<C::Element>, BooleanityError<C::Element>> {
319 debug_assert!(
320 sumcheck_prover_state
321 .mles
322 .iter()
323 .all(|mle| mle.num_vars == 1),
324 "sumcheck should reduce MLEs to num_vars == 1"
325 );
326
327 let last_sumcheck_challenge = sumcheck_prover_state
328 .randomness
329 .last()
330 .expect("sumcheck cannot have had 0 rounds")
331 .clone();
332
333 let active_len = ancillary
334 .num_wit_bin_cols
335 .saturating_mul(ancillary.bit_width);
336
337 let mut mles = sumcheck_prover_state.mles;
338 debug_assert_eq!(mles.len(), add!(1, active_len));
339
340 let bit_slice_evals: Vec<C::Element> = mles
342 .drain(1..)
343 .map(|mle| mle.evaluate(field_cfg, slice::from_ref(&last_sumcheck_challenge)))
344 .collect::<Result<Vec<_>, _>>()?;
345
346 debug_assert_eq!(bit_slice_evals.len(), active_len);
347
348 let mut transcription_buf: Vec<u8> = vec![0; <C::Integer as ConstTranscribable>::NUM_BYTES];
349 transcript.absorb_field_element_slice(field_cfg, &bit_slice_evals, &mut transcription_buf);
350
351 Ok(BooleanityProof { bit_slice_evals })
352 }
353
354 pub fn prepare_verifier(
360 transcript: &mut impl Transcript,
361 claimed_sum: &C::Element,
362 num_wit_bin_cols: usize,
363 bit_width: usize,
364 num_vars: usize,
365 field_cfg: &C,
366 ) -> Result<BoolVerifierAncillary<C::Element>, BooleanityError<C::Element>> {
367 if num_wit_bin_cols == 0 {
368 return Err(BooleanityError::NoBinaryPolyColumns);
369 }
370 if !field_cfg.is_zero(claimed_sum) {
371 return Err(BooleanityError::NonZeroClaimedSum {
372 got: claimed_sum.clone(),
373 });
374 }
375
376 let zerocheck_point: Vec<C::Element> = transcript.get_field_challenges(num_vars, field_cfg);
378 let alpha: C::Element = transcript.get_field_challenge(field_cfg);
379 let alpha_powers: Vec<C::Element> = powers(
380 field_cfg,
381 &alpha,
382 num_wit_bin_cols.saturating_mul(bit_width),
383 );
384
385 Ok(BoolVerifierAncillary {
386 alpha_powers,
387 zerocheck_point,
388 num_wit_bin_cols,
389 bit_width,
390 num_vars,
391 })
392 }
393
394 pub fn finalize_verifier(
407 transcript: &mut impl Transcript,
408 proof: BooleanityProof<C::Element>,
409 shared_point: Vec<C::Element>,
410 expected_eval: &C::Element,
411 ancillary: BoolVerifierAncillary<C::Element>,
412 field_cfg: &C,
413 ) -> Result<BoolVerifierSubclaim<C::Element>, BooleanityError<C::Element>> {
414 let expected_len = ancillary
415 .num_wit_bin_cols
416 .saturating_mul(ancillary.bit_width);
417 if proof.bit_slice_evals.len() != expected_len {
418 return Err(BooleanityError::WrongBitSliceEvalsNumber {
419 expected: expected_len,
420 got: proof.bit_slice_evals.len(),
421 });
422 }
423
424 let eq_r_val = eq_eval(field_cfg, &shared_point, &ancillary.zerocheck_point)?;
426
427 let sum =
429 batched_booleanity_sum(field_cfg, &proof.bit_slice_evals, &ancillary.alpha_powers);
430 let expected_claim_value = field_cfg.mul(&sum, &eq_r_val);
431
432 if expected_claim_value != *expected_eval {
433 return Err(BooleanityError::ClaimValueDoesNotMatch {
434 expected: expected_claim_value,
435 got: expected_eval.clone(),
436 });
437 }
438
439 let mut transcription_buf: Vec<u8> = vec![0; <C::Integer as ConstTranscribable>::NUM_BYTES];
440 transcript.absorb_field_element_slice(
441 field_cfg,
442 &proof.bit_slice_evals,
443 &mut transcription_buf,
444 );
445
446 Ok(BoolVerifierSubclaim {
447 bit_slice_evals: proof.bit_slice_evals,
448 })
449 }
450}
451
452struct BooleanityRound1FastPath<C: SetConfig, const D: usize> {
479 binary_cols: Vec<DenseMultilinearExtension<BinaryPoly<D>>>,
481 alpha_powers: Vec<C::Element>,
484 eq_other_table: Vec<C::Element>,
488 r_first_coord: C::Element,
490 num_vars: usize,
492}
493
494impl<C, const D: usize> Round1FastPath<C> for BooleanityRound1FastPath<C, D>
495where
496 C: SemiringConfig,
497{
498 fn round_1_message(&self, config: &C) -> Round1Output<C::Element> {
499 let zero = config.zero();
500 let one = config.one();
501 let half_n: usize = 1_usize << self.num_vars.saturating_sub(1);
502
503 let mut t1 = zero.clone();
505 for b_prime in 0..half_n {
506 let mut s_b = zero.clone();
507 for (j, col) in self.binary_cols.iter().enumerate() {
508 let two_b = mul!(2, b_prime);
509 let row_a = &col.evaluations[two_b];
510 let row_b = &col.evaluations[add!(two_b, 1)];
511 for (i, (a_bit, b_bit)) in row_a.iter().zip(row_b.iter()).enumerate() {
512 if a_bit != b_bit {
513 let k = add!(i, mul!(j, D));
514 config.add_assign(&mut s_b, &self.alpha_powers[k]);
515 }
516 }
517 }
518 let term = config.mul(&s_b, &self.eq_other_table[b_prime]);
519 config.add_assign(&mut t1, &term);
520 }
521
522 let two = config.add(&one, &one);
527 let three = config.add(&two, &one);
528 let five = config.add(&three, &two);
529 let six = config.mul(&three, &two);
530
531 let three_r0_minus_one = config.sub(&config.mul(&three, &self.r_first_coord), &one);
532 let five_r0_minus_two = config.sub(&config.mul(&five, &self.r_first_coord), &two);
533
534 let p1_at_1 = zero.clone();
535 let p1_at_2 = config.mul(&config.mul(&two, &three_r0_minus_one), &t1);
536 let p1_at_3 = config.mul(&config.mul(&six, &five_r0_minus_two), &t1);
537
538 Round1Output {
539 asserted_sum: zero,
541 tail_evaluations: vec![p1_at_1, p1_at_2, p1_at_3],
542 }
543 }
544
545 fn fold_with_challenge(
546 self: Box<Self>,
547 r_1: &C::Element,
548 config: &C,
549 ) -> Vec<DenseMultilinearExtension<C::Element>> {
550 if self.num_vars <= 1 {
557 let r = [self.r_first_coord.clone()];
558 let eq_r =
559 build_eq_x_r(config, &r).expect("num_vars == 1 implies a single-coordinate r");
560 let mut out: Vec<DenseMultilinearExtension<C::Element>> =
561 Vec::with_capacity(self.binary_cols.len().saturating_mul(D).saturating_add(1));
562 out.push(eq_r);
563 out.extend(build_witness_bit_slice_mles::<C, D>(
564 &self.binary_cols,
565 config,
566 ));
567 return out;
568 }
569
570 let n_cols = self.binary_cols.len();
571 let total_bit_slices = n_cols.saturating_mul(D);
572 let new_num_vars = self.num_vars.saturating_sub(1);
573 let half_n: usize = 1_usize << new_num_vars;
574
575 let one = config.one();
576
577 let bit_fold_table: [C::Element; 4] = [
583 config.zero(),
584 r_1.clone(),
585 config.sub(&one, r_1),
586 one.clone(),
587 ];
588
589 let one_minus_r0 = config.sub(&one, &self.r_first_coord);
592 let one_minus_r1 = config.sub(&one, r_1);
593 let e_0_r1: C::Element = config.add(
594 &config.mul(&one_minus_r1, &one_minus_r0),
595 &config.mul(r_1, &self.r_first_coord),
596 );
597
598 let folded_eq_r_evals = (0..half_n)
599 .map(|b_prime| config.mul(&e_0_r1, &self.eq_other_table[b_prime]))
600 .collect_vec();
601
602 let mut out: Vec<DenseMultilinearExtension<C::Element>> =
604 Vec::with_capacity(total_bit_slices.saturating_add(1));
605 out.push(DenseMultilinearExtension {
606 num_vars: new_num_vars,
607 evaluations: folded_eq_r_evals,
608 });
609
610 for col in &self.binary_cols {
613 let mut col_evals: Vec<Vec<C::Element>> =
614 (0..D).map(|_| Vec::with_capacity(half_n)).collect();
615 for b_prime in 0..half_n {
616 let two_b = mul!(2, b_prime);
617 let row_a = &col.evaluations[two_b];
618 let row_b = &col.evaluations[add!(two_b, 1)];
619 for (i, (a_bit, b_bit)) in row_a.iter().zip(row_b.iter()).enumerate() {
620 let idx =
621 (usize::from(a_bit.into_inner()) << 1) | usize::from(b_bit.into_inner());
622 col_evals[i].push(bit_fold_table[idx].clone());
623 }
624 }
625 for evals in col_evals {
626 out.push(DenseMultilinearExtension {
627 num_vars: new_num_vars,
628 evaluations: evals,
629 });
630 }
631 }
632
633 out
634 }
635}
636
637#[derive(Debug, Error)]
639pub enum BooleanityError<F: std::fmt::Debug> {
640 #[error("no binary polynomial columns provided to booleanity checker")]
641 NoBinaryPolyColumns,
642 #[error("no affine virtual specs provided to booleanity checker")]
643 NoAffineVirtualSpecs,
644 #[error("sumcheck error: {0}")]
645 SumcheckError(#[from] SumCheckError<F>),
646 #[error("expected booleanity claimed sum is non-zero: got {got:?}")]
647 NonZeroClaimedSum { got: F },
648 #[error("wrong number of bit-slice evaluations: expected {expected}, got {got}")]
649 WrongBitSliceEvalsNumber { expected: usize, got: usize },
650 #[error("booleanity claim value does not match: expected {expected:?}, got {got:?}")]
651 ClaimValueDoesNotMatch { expected: F, got: F },
652 #[error("error evaluating MLE: {0}")]
653 MleEvaluationError(#[from] EvaluationError),
654 #[error("arithmetic error: {0}")]
655 Arith(#[from] ArithErrors),
656}
657
658fn batched_booleanity_sum<C: SemiringConfig>(
675 cfg: &C,
676 bit_slice_values: &[C::Element],
677 alpha_powers: &[C::Element],
678) -> C::Element {
679 debug_assert_eq!(bit_slice_values.len(), alpha_powers.len());
680
681 let one = cfg.one();
682 let mut sum = cfg.zero();
683 for (v, alpha_k) in bit_slice_values.iter().zip(alpha_powers.iter()) {
684 let booleanity = cfg.mul(&cfg.sub(v, &one), v);
685 cfg.add_assign(&mut sum, &cfg.mul(&booleanity, alpha_k));
686 }
687 sum
688}
689
690fn build_witness_bit_slice_mles<C, const D: usize>(
702 trace_bin_poly: &[DenseMultilinearExtension<BinaryPoly<D>>],
703 field_cfg: &C,
704) -> Vec<DenseMultilinearExtension<C::Element>>
705where
706 C: SemiringConfig,
707{
708 let zero = field_cfg.zero();
709 let one = field_cfg.one();
710
711 let mut out = Vec::with_capacity(trace_bin_poly.len().saturating_mul(D));
712 for col in trace_bin_poly {
713 let num_vars = col.num_vars;
714 let n_rows = col.evaluations.len();
715 for i in 0..D {
716 let mut evals: Vec<C::Element> = Vec::with_capacity(n_rows);
717 for entry in col.iter() {
718 let bit = entry
719 .iter()
720 .nth(i)
721 .expect("BinaryPoly<D> has D coefficients")
722 .into_inner();
723 evals.push(if bit { one.clone() } else { zero.clone() });
724 }
725 out.push(DenseMultilinearExtension {
726 num_vars,
727 evaluations: evals,
728 });
729 }
730 }
731 out
732}
733
734fn booleanity_zerocheck_setup<C>(
735 transcript: &mut impl Transcript,
736 num_cols: usize,
737 bit_width: usize,
738 num_vars: usize,
739 field_cfg: &C,
740) -> BooleanityZerocheckSetup<C::Element>
741where
742 C: BaseFieldConfig + ProjectPrimitiveIntegersWithConfig + 'static,
743 C::Integer: ConstTranscribable,
744{
745 let r: Vec<C::Element> = transcript.get_field_challenges(num_vars, field_cfg);
749
750 let alpha: C::Element = transcript.get_field_challenge(field_cfg);
753 let alpha_powers: Vec<C::Element> =
754 powers(field_cfg, &alpha, num_cols.saturating_mul(bit_width));
755
756 let comb_fn: CombFn<C::Element> = {
765 let alpha_powers = alpha_powers.clone();
766 let field_cfg = field_cfg.clone();
767 Box::new(move |mle_values: &[C::Element]| {
768 let eq_r_val = &mle_values[0];
769 let sum = batched_booleanity_sum(&field_cfg, &mle_values[1..], &alpha_powers);
770 field_cfg.mul(&sum, eq_r_val)
771 })
772 };
773
774 (r, alpha_powers, comb_fn)
775}
776
777pub fn build_affine_virtual_bit_slice_mles<C, const D: usize>(
792 all_trace_bin_poly: &[DenseMultilinearExtension<BinaryPoly<D>>],
793 affine_virtual_specs: &[AffineVirtualSpec],
794 num_vars: usize,
795 field_cfg: &C,
796) -> Vec<DenseMultilinearExtension<C::Element>>
797where
798 C: SemiringConfig + ProjectPrimitiveIntegersWithConfig,
799{
800 if affine_virtual_specs.is_empty() {
801 return Vec::new();
802 }
803
804 let n_rows = 1usize << num_vars;
805 for col in all_trace_bin_poly {
806 assert_eq!(
807 col.num_vars, num_vars,
808 "all affine virtual source columns must have the same num_vars",
809 );
810 assert_eq!(
811 col.evaluations.len(),
812 n_rows,
813 "source column length must match num_vars",
814 );
815 }
816
817 let mut out = Vec::with_capacity(affine_virtual_specs.len().saturating_mul(D));
818 for spec in affine_virtual_specs {
819 let ones = field_cfg.project(&spec.ones_coefficient());
820 let terms: Vec<_> = spec
821 .terms()
822 .iter()
823 .map(|term| {
824 assert!(
825 term.source_col() < all_trace_bin_poly.len(),
826 "AffineVirtualTerm source_col {} out of range (binary columns = {})",
827 term.source_col(),
828 all_trace_bin_poly.len(),
829 );
830 (
831 term.source_col(),
832 field_cfg.project(&term.coefficient()),
833 term.row_shift(),
834 )
835 })
836 .collect();
837
838 for bit_idx in 0..D {
839 let mut evals: Vec<C::Element> = Vec::with_capacity(n_rows);
840 for row_idx in 0..n_rows {
841 let mut residual = ones.clone();
842 for (source_col, coeff, row_shift) in &terms {
843 let shifted_row = add!(row_idx, *row_shift);
844 if shifted_row >= n_rows {
845 continue;
846 }
847 let bit = all_trace_bin_poly[*source_col].evaluations[shifted_row]
848 .iter()
849 .nth(bit_idx)
850 .expect("BinaryPoly<D> has D coefficients")
851 .into_inner();
852 if bit {
853 field_cfg.add_assign(&mut residual, coeff);
854 }
855 }
856 evals.push(residual);
857 }
858 out.push(DenseMultilinearExtension {
859 num_vars,
860 evaluations: evals,
861 });
862 }
863 }
864
865 out
866}
867
868#[cfg(test)]
873#[allow(
874 clippy::arithmetic_side_effects,
875 clippy::cast_possible_truncation,
876 clippy::cast_precision_loss,
877 clippy::cast_sign_loss,
878 clippy::clone_on_copy
879)]
880mod tests {
881 use super::*;
882 use crate::sumcheck::multi_degree::MultiDegreeSumcheck;
883 use crypto_bigint::{U128, const_monty_params};
884 use crypto_primitives::{
885 FixedConfig, ProjectElementWithConfig, crypto_bigint_const_monty::ConstMontyField,
886 };
887 use num_traits::{One, Zero};
888 use zinc_poly::mle::MultilinearExtension;
889 use zinc_transcript::Blake3Transcript;
890 use zinc_uair::{AffineVirtualSpec, AffineVirtualTerm};
891 use zinc_utils::powers;
892
893 const_monty_params!(TestParams, U128, "00000000b933426489189cb5b47d567f");
894 type F = ConstMontyField<TestParams, { U128::LIMBS }>;
895 type FC = FixedConfig<F>;
896
897 const D: usize = 4;
898
899 fn binp_from_bits(bits: [bool; D]) -> BinaryPoly<D> {
901 let value: u64 = bits
902 .iter()
903 .enumerate()
904 .filter_map(|(i, &b)| b.then_some(1_u64 << i))
905 .fold(0_u64, |acc, mask| acc | mask);
906 BinaryPoly::from(value)
907 }
908
909 fn build_col(rows: Vec<[bool; D]>) -> DenseMultilinearExtension<BinaryPoly<D>> {
911 let num_vars = (rows.len() as f64).log2().round() as usize;
912 debug_assert_eq!(rows.len(), 1_usize << num_vars);
913 let zero = BinaryPoly::zero();
914 let mut evals: Vec<BinaryPoly<D>> = rows.into_iter().map(binp_from_bits).collect();
915 if evals.is_empty() {
917 evals.push(zero);
918 }
919 DenseMultilinearExtension {
920 num_vars,
921 evaluations: evals,
922 }
923 }
924
925 fn zero_col_4rows() -> DenseMultilinearExtension<BinaryPoly<D>> {
927 build_col(vec![[false; D]; 4])
928 }
929
930 fn make_transcript() -> Blake3Transcript {
931 let mut t = Blake3Transcript::default();
932 t.absorb_bytes(b"booleanity test");
933 t
934 }
935
936 fn run_roundtrip(
940 cols: &[DenseMultilinearExtension<BinaryPoly<D>>],
941 num_vars: usize,
942 tamper_proof: impl FnOnce(&mut BooleanityProof<F>),
943 expect_err_is: impl FnOnce(&BooleanityError<F>) -> bool,
944 expect_ok: bool,
945 ) {
946 let cfg = &FC::default();
947
948 let mut pt = make_transcript();
950 let (group, anc) =
951 BooleanityChecker::<FC>::prepare_sumcheck_group::<D>(&mut pt, cols, num_vars, cfg)
952 .expect("prepare_sumcheck_group failed");
953 let mut sumcheck_outputs = MultiDegreeSumcheck::<FC>::prove_as_subprotocol(
954 &mut pt,
955 vec![(vec![group], cfg)],
956 num_vars,
957 cfg,
958 );
959 let (md_proof, states) = sumcheck_outputs.pop().expect("single family");
960 let state = states.into_iter().next().unwrap();
961 let mut proof = BooleanityChecker::<FC>::finalize_prover(&mut pt, state, anc, cfg)
962 .expect("finalize prover");
963 tamper_proof(&mut proof);
964
965 let mut vt = make_transcript();
967 let anc_v = BooleanityChecker::<FC>::prepare_verifier(
968 &mut vt,
969 &md_proof.claimed_sums()[0],
970 cols.len(),
971 D,
972 num_vars,
973 cfg,
974 )
975 .expect("prepare_verifier (claimed sum should be zero for honest input)");
976 let md_subclaims = MultiDegreeSumcheck::<FC>::verify_as_subprotocol(
977 &mut vt,
978 num_vars,
979 &[(&md_proof, cfg)],
980 cfg,
981 )
982 .expect("md verify")
983 .pop()
984 .expect("single family");
985
986 let res = BooleanityChecker::<FC>::finalize_verifier(
987 &mut vt,
988 proof,
989 md_subclaims.point().to_vec(),
990 &md_subclaims.expected_evaluations()[0],
991 anc_v,
992 cfg,
993 );
994
995 if expect_ok {
996 res.expect("finalize_verifier should succeed");
997 } else {
998 let err = res.expect_err("finalize_verifier should fail");
999 assert!(
1000 expect_err_is(&err),
1001 "unexpected booleanity error variant: {err:?}",
1002 );
1003 }
1004 }
1005
1006 #[test]
1007 fn happy_path_all_bits() {
1008 let c0 = build_col(vec![
1010 [true, false, true, false],
1011 [false, true, false, true],
1012 [true, true, false, false],
1013 [false, false, true, true],
1014 ]);
1015 let c1 = build_col(vec![
1016 [false, false, false, false],
1017 [true, true, true, true],
1018 [false, true, true, false],
1019 [true, false, false, true],
1020 ]);
1021 run_roundtrip(&[c0, c1], 2, |_| {}, |_| false, true);
1022 }
1023
1024 #[test]
1028 fn happy_path_num_vars_one() {
1029 let c0 = build_col(vec![[true, false, true, false], [false, true, true, true]]);
1033 let c1 = build_col(vec![[false, false, true, true], [true, true, false, false]]);
1034 run_roundtrip(&[c0, c1], 1, |_| {}, |_| false, true);
1035 }
1036
1037 #[test]
1038 fn empty_column_set_is_supported_by_helper() {
1039 let cols: Vec<DenseMultilinearExtension<BinaryPoly<D>>> = vec![];
1043 let mles = build_witness_bit_slice_mles::<FC, D>(&cols, &FC::default());
1044 assert!(mles.is_empty());
1045 }
1046
1047 #[test]
1048 fn tampered_bit_slice_evals_rejected() {
1049 let c0 = zero_col_4rows();
1050 run_roundtrip(
1054 &[c0],
1055 2,
1056 |proof| {
1057 proof.bit_slice_evals[0] += F::from(7u32);
1058 },
1059 |err| matches!(err, BooleanityError::ClaimValueDoesNotMatch { .. }),
1060 false,
1061 );
1062 }
1063
1064 #[test]
1065 fn non_bit_witness_rejected() {
1066 let c0 = build_col(vec![
1074 [true, true, false, false],
1075 [false, true, true, false],
1076 [false, false, false, true],
1077 [true, false, true, true],
1078 ]);
1079 run_roundtrip(
1080 &[c0],
1081 2,
1082 |proof| {
1083 proof.bit_slice_evals[1] += F::from(3u32);
1085 },
1086 |err| matches!(err, BooleanityError::ClaimValueDoesNotMatch { .. }),
1087 false,
1088 );
1089 }
1090
1091 #[test]
1092 fn wrong_bit_slice_evals_length_rejected() {
1093 let c0 = zero_col_4rows();
1094 run_roundtrip(
1095 &[c0],
1096 2,
1097 |proof| {
1098 proof.bit_slice_evals.pop();
1099 },
1100 |err| matches!(err, BooleanityError::WrongBitSliceEvalsNumber { .. }),
1101 false,
1102 );
1103 }
1104
1105 #[test]
1106 fn affine_virtual_bit_slice_mles_materialize_residual_coefficients() {
1107 let cfg = &FC::default();
1108 let public = build_col(vec![[true; D], [true; D], [true; D], [true; D]]);
1109 let a = build_col(vec![
1110 [true, false, true, false],
1111 [false, true, false, true],
1112 [true, true, false, false],
1113 [false, false, true, true],
1114 ]);
1115 let b = build_col(vec![
1116 [false, true, true, false],
1117 [true, false, false, false],
1118 [false, true, true, true],
1119 [true, true, false, false],
1120 ]);
1121 let m = build_col(vec![
1122 [false, false, true, false],
1123 [false, false, false, false],
1124 [false, true, false, false],
1125 [false, false, false, false],
1126 ]);
1127 let specs = vec![AffineVirtualSpec::new(vec![
1128 AffineVirtualTerm::new(1, 1),
1129 AffineVirtualTerm::new(2, 1),
1130 AffineVirtualTerm::new(3, -2),
1131 ])];
1132
1133 let mles = build_affine_virtual_bit_slice_mles::<FC, D>(&[public, a, b, m], &specs, 2, cfg);
1136
1137 assert_eq!(mles.len(), D);
1138 let expected_by_bit: [[i32; 4]; D] =
1139 [[1, 1, 1, 1], [1, 1, 0, 1], [0, 0, 1, 1], [0, 1, 1, 1]];
1140 for (bit_idx, expected_rows) in expected_by_bit.iter().enumerate() {
1141 let expected: Vec<_> = expected_rows
1142 .iter()
1143 .map(|value| cfg.project(value))
1144 .collect();
1145 assert_eq!(
1146 mles[bit_idx].evaluations, expected,
1147 "unexpected affine virtual residual at bit {bit_idx}"
1148 );
1149 }
1150 }
1151
1152 #[test]
1153 fn affine_virtual_bit_slice_mles_apply_forward_shift_and_ones_offset() {
1154 let cfg = &FC::default();
1155 let a = build_col(vec![
1156 [true, false, false, false],
1157 [false, true, false, false],
1158 [true, true, false, false],
1159 [false, false, false, false],
1160 ]);
1161 let specs = vec![AffineVirtualSpec::with_ones_coefficient(
1162 vec![AffineVirtualTerm::new_shifted(0, -1, 1)],
1163 1,
1164 )];
1165
1166 let mles = build_affine_virtual_bit_slice_mles::<FC, D>(&[a], &specs, 2, cfg);
1167
1168 assert_eq!(mles.len(), D);
1169 let expected_by_bit: [[i32; 4]; D] =
1170 [[1, 0, 1, 1], [0, 0, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]];
1171 for (bit_idx, expected_rows) in expected_by_bit.iter().enumerate() {
1172 let expected: Vec<_> = expected_rows
1173 .iter()
1174 .map(|value| cfg.project(value))
1175 .collect();
1176 assert_eq!(
1177 mles[bit_idx].evaluations, expected,
1178 "unexpected shifted affine virtual residual at bit {bit_idx}"
1179 );
1180 }
1181 }
1182
1183 fn run_affine_virtual_roundtrip(
1184 all_trace_bin_poly: &[DenseMultilinearExtension<BinaryPoly<D>>],
1185 specs: &[AffineVirtualSpec],
1186 num_vars: usize,
1187 tamper_proof: impl FnOnce(&mut BooleanityProof<F>),
1188 ) -> Result<(), BooleanityError<F>> {
1189 let cfg = &FC::default();
1190
1191 let mut pt = make_transcript();
1192 let (group, anc) = BooleanityChecker::<FC>::prepare_affine_virtual_sumcheck_group::<D>(
1193 &mut pt,
1194 all_trace_bin_poly,
1195 specs,
1196 num_vars,
1197 cfg,
1198 )
1199 .expect("prepare_affine_virtual_sumcheck_group failed");
1200 let mut sumcheck_outputs = MultiDegreeSumcheck::<FC>::prove_as_subprotocol(
1201 &mut pt,
1202 vec![(vec![group], cfg)],
1203 num_vars,
1204 cfg,
1205 );
1206 let (md_proof, states) = sumcheck_outputs.pop().expect("single family");
1207 let state = states.into_iter().next().unwrap();
1208 let mut proof = BooleanityChecker::<FC>::finalize_prover(&mut pt, state, anc, cfg)
1209 .expect("finalize prover");
1210 tamper_proof(&mut proof);
1211
1212 let mut vt = make_transcript();
1213 let verifier_anc = BooleanityChecker::<FC>::prepare_verifier(
1214 &mut vt,
1215 &md_proof.claimed_sums()[0],
1216 specs.len(),
1217 D,
1218 num_vars,
1219 cfg,
1220 )?;
1221 let md_subclaims = MultiDegreeSumcheck::<FC>::verify_as_subprotocol(
1222 &mut vt,
1223 num_vars,
1224 &[(&md_proof, cfg)],
1225 cfg,
1226 )
1227 .expect("md verify")
1228 .pop()
1229 .expect("single family");
1230
1231 BooleanityChecker::<FC>::finalize_verifier(
1232 &mut vt,
1233 proof,
1234 md_subclaims.point().to_vec(),
1235 &md_subclaims.expected_evaluations()[0],
1236 verifier_anc,
1237 cfg,
1238 )
1239 .map(|_| ())
1240 }
1241
1242 #[test]
1243 fn affine_virtual_group_accepts_boolean_residual_and_rejects_non_boolean_residual() {
1244 let public = build_col(vec![[true; D], [true; D], [true; D], [true; D]]);
1245 let a = build_col(vec![
1246 [true, false, true, false],
1247 [false, true, false, true],
1248 [true, true, false, false],
1249 [false, false, true, true],
1250 ]);
1251 let b = build_col(vec![
1252 [false, true, true, false],
1253 [true, false, false, false],
1254 [false, true, true, true],
1255 [true, true, false, false],
1256 ]);
1257 let m = build_col(vec![
1258 [false, false, true, false],
1259 [false, false, false, false],
1260 [false, true, false, false],
1261 [false, false, false, false],
1262 ]);
1263 let specs = vec![AffineVirtualSpec::new(vec![
1264 AffineVirtualTerm::new(1, 1),
1265 AffineVirtualTerm::new(2, 1),
1266 AffineVirtualTerm::new(3, -2),
1267 ])];
1268
1269 run_affine_virtual_roundtrip(
1270 &[public.clone(), a.clone(), b.clone(), m],
1271 &specs,
1272 2,
1273 |_| {},
1274 )
1275 .expect("boolean affine residual should verify");
1276
1277 let bad_m = build_col(vec![
1278 [true, false, true, false],
1279 [false, false, false, false],
1280 [false, true, false, false],
1281 [false, false, false, false],
1282 ]);
1283 let err = run_affine_virtual_roundtrip(&[public, a, b, bad_m], &specs, 2, |_| {})
1284 .expect_err("non-boolean affine residual should be rejected");
1285 assert!(matches!(err, BooleanityError::NonZeroClaimedSum { .. }));
1286 }
1287
1288 #[test]
1289 fn affine_virtual_group_rejects_tampered_bit_slice_eval() {
1290 let zero = zero_col_4rows();
1291 let specs = vec![AffineVirtualSpec::new(vec![AffineVirtualTerm::new(0, 1)])];
1292
1293 let err = run_affine_virtual_roundtrip(&[zero], &specs, 2, |proof| {
1294 proof.bit_slice_evals[0] += F::from(7u32);
1295 })
1296 .expect_err("tampered affine bit-slice evaluation should be rejected");
1297 assert!(matches!(
1298 err,
1299 BooleanityError::ClaimValueDoesNotMatch { .. }
1300 ));
1301 }
1302
1303 #[test]
1304 fn affine_virtual_prepare_rejects_missing_inputs_with_precise_errors() {
1305 let cfg = &FC::default();
1306 let mut transcript = make_transcript();
1307 let col = zero_col_4rows();
1308 let no_specs = BooleanityChecker::<FC>::prepare_affine_virtual_sumcheck_group::<D>(
1309 &mut transcript,
1310 &[col],
1311 &[],
1312 2,
1313 cfg,
1314 );
1315 assert!(matches!(
1316 no_specs,
1317 Err(BooleanityError::NoAffineVirtualSpecs)
1318 ));
1319
1320 let mut transcript = make_transcript();
1321 let specs = vec![AffineVirtualSpec::new(vec![AffineVirtualTerm::new(0, 1)])];
1322 let no_columns = BooleanityChecker::<FC>::prepare_affine_virtual_sumcheck_group::<D>(
1323 &mut transcript,
1324 &[],
1325 &specs,
1326 2,
1327 cfg,
1328 );
1329 assert!(matches!(
1330 no_columns,
1331 Err(BooleanityError::NoBinaryPolyColumns)
1332 ));
1333 }
1334
1335 struct FastPathHarness {
1346 cols: Vec<DenseMultilinearExtension<BinaryPoly<D>>>,
1347 num_vars: usize,
1348 r: Vec<F>,
1349 alpha_powers: Vec<F>,
1350 zero: F,
1351 one: F,
1352 }
1353
1354 fn cross_validation_inputs() -> FastPathHarness {
1355 let c0 = build_col(vec![
1358 [true, false, true, false],
1359 [false, true, false, true],
1360 [true, true, false, false],
1361 [false, false, true, true],
1362 [true, true, true, false],
1363 [false, true, true, false],
1364 [true, false, false, true],
1365 [false, false, false, false],
1366 ]);
1367 let c1 = build_col(vec![
1368 [false, false, false, false],
1369 [true, true, true, true],
1370 [false, true, true, false],
1371 [true, false, false, true],
1372 [false, false, true, false],
1373 [true, true, false, true],
1374 [false, true, false, true],
1375 [true, false, true, false],
1376 ]);
1377 let c2 = build_col(vec![
1378 [true; D],
1379 [false; D],
1380 [true, false, false, false],
1381 [false, false, false, true],
1382 [true, true, false, false],
1383 [false, false, true, true],
1384 [true, false, true, true],
1385 [false, true, false, false],
1386 ]);
1387 let cols = vec![c0, c1, c2];
1388
1389 let num_vars = 3usize;
1390 let one = F::one();
1391 let zero = F::zero();
1392
1393 let r: Vec<F> = (0..num_vars)
1395 .map(|i| F::from((i as u32) * 13 + 41))
1396 .collect();
1397 let alpha = F::from(23u32);
1398 let alpha_powers = powers(&FC::default(), &alpha, cols.len() * D);
1399
1400 FastPathHarness {
1401 cols,
1402 num_vars,
1403 r,
1404 alpha_powers,
1405 zero,
1406 one,
1407 }
1408 }
1409
1410 fn standard_path_mles(
1411 cols: &[DenseMultilinearExtension<BinaryPoly<D>>],
1412 r: &[F],
1413 cfg: &FC,
1414 ) -> Vec<DenseMultilinearExtension<F>> {
1415 let mut mles = Vec::with_capacity(1_usize.saturating_add(cols.len().saturating_mul(D)));
1416 mles.push(build_eq_x_r(cfg, r).expect("eq_r build"));
1417 mles.extend(build_witness_bit_slice_mles::<FC, D>(cols, cfg));
1418 mles
1419 }
1420
1421 fn make_comb_fn(alpha_powers: Vec<F>) -> CombFn<F> {
1422 Box::new(move |mle_values: &[F]| {
1423 let cfg = FC::default();
1424 let eq_r_val = &mle_values[0];
1425 let sum = batched_booleanity_sum(&cfg, &mle_values[1..], &alpha_powers);
1426 sum * eq_r_val
1427 })
1428 }
1429
1430 fn make_fast_path(h: &FastPathHarness, cfg: &FC) -> BooleanityRound1FastPath<FC, D> {
1431 let eq_other_table: Vec<F> = if h.num_vars <= 1 {
1432 vec![h.one.clone()]
1433 } else {
1434 build_eq_x_r(cfg, &h.r[1..])
1435 .expect("eq_other build")
1436 .evaluations
1437 };
1438 BooleanityRound1FastPath::<FC, D> {
1439 binary_cols: h.cols.clone(),
1440 alpha_powers: h.alpha_powers.clone(),
1441 eq_other_table,
1442 r_first_coord: h.r[0].clone(),
1443 num_vars: h.num_vars,
1444 }
1445 }
1446
1447 #[test]
1451 fn fast_path_round_1_matches_standard_prove_round() {
1452 let cfg = &FC::default();
1453 let h = cross_validation_inputs();
1454 let fast_path = make_fast_path(&h, cfg);
1455
1456 let fp_out = fast_path.round_1_message(cfg);
1457
1458 let mles = standard_path_mles(&h.cols, &h.r, cfg);
1461 let comb_fn = make_comb_fn(h.alpha_powers.clone());
1462 let mut state = SumcheckProverState::<FC>::new(mles, h.num_vars, 3);
1463 let std_msg = state.prove_round(&None, &comb_fn, cfg);
1464
1465 assert_eq!(
1466 fp_out.tail_evaluations, std_msg.0.tail_evaluations,
1467 "round-1 tail evaluations differ between fast and standard paths"
1468 );
1469 assert_eq!(
1470 Some(fp_out.asserted_sum.clone()),
1471 state.asserted_sum,
1472 "round-1 asserted sums differ between fast and standard paths"
1473 );
1474 assert_eq!(
1475 fp_out.asserted_sum, h.zero,
1476 "booleanity is a zerocheck — asserted sum must be 0"
1477 );
1478 }
1479
1480 #[test]
1484 fn fast_path_fold_with_r1_matches_standard_fix_variables() {
1485 let cfg = &FC::default();
1486 let h = cross_validation_inputs();
1487 let r_1 = F::from(97u32);
1488
1489 let fast_path = Box::new(make_fast_path(&h, cfg));
1491 let fp_mles = fast_path.fold_with_challenge(&r_1, cfg);
1492
1493 let mut std_mles = standard_path_mles(&h.cols, &h.r, cfg);
1495 for mle in &mut std_mles {
1496 mle.fix_variables(cfg, std::slice::from_ref(&r_1));
1497 }
1498
1499 assert_eq!(
1500 fp_mles.len(),
1501 std_mles.len(),
1502 "fast and standard paths must produce the same number of MLEs"
1503 );
1504 for (k, (fp_mle, std_mle)) in fp_mles.iter().zip(std_mles.iter()).enumerate() {
1505 assert_eq!(
1506 fp_mle.num_vars, std_mle.num_vars,
1507 "num_vars mismatch on folded MLE #{k}"
1508 );
1509 assert_eq!(
1510 fp_mle.evaluations, std_mle.evaluations,
1511 "evaluations mismatch on folded MLE #{k}"
1512 );
1513 }
1514 }
1515}