1use crate::{
45 CombFn,
46 sumcheck::{
47 SumCheckError, multi_degree::MultiDegreeSumcheckGroup,
48 prover::ProverState as SumcheckProverState,
49 },
50};
51use crypto_primitives::PrimeField;
52use num_traits::Zero;
53use std::{marker::PhantomData, slice};
54use thiserror::Error;
55use zinc_poly::{
56 EvaluationError,
57 mle::{DenseMultilinearExtension, MultilinearExtensionWithConfig},
58 univariate::binary::BinaryPoly,
59 utils::{ArithErrors, build_eq_x_r_inner, eq_eval},
60};
61use zinc_transcript::{
62 delegate_transcribable,
63 traits::{ConstTranscribable, Transcript},
64};
65use zinc_utils::{add, inner_transparent_field::InnerTransparentField, mul, powers};
66
67pub struct BooleanityChecker<F: InnerTransparentField>(PhantomData<F>);
73
74#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct BooleanityProof<F: PrimeField> {
80 pub bit_slice_evals: Vec<F>,
83}
84
85delegate_transcribable!(BooleanityProof<F> { bit_slice_evals: Vec<F> }
86 where F: PrimeField, F::Inner: ConstTranscribable, F::Modulus: ConstTranscribable);
87
88pub struct BoolProverAncillary {
91 pub num_wit_bin_cols: usize,
93 pub bit_width: usize,
95 pub num_vars: usize,
97}
98
99pub struct BoolVerifierAncillary<F: PrimeField> {
102 pub gamma_powers: Vec<F>,
105 pub delta_powers: Vec<F>,
108 pub zerocheck_point: Vec<F>,
110 pub num_wit_bin_cols: usize,
112 pub bit_width: usize,
114 pub num_vars: usize,
116}
117
118#[derive(Clone, Debug)]
127pub struct BoolVerifierSubclaim<F: PrimeField> {
128 pub bit_slice_evals: Vec<F>,
130 pub evaluation_point: Vec<F>,
132}
133
134impl<F> BooleanityChecker<F>
139where
140 F: InnerTransparentField + Send + Sync + 'static,
141 F::Inner: ConstTranscribable + Clone + Send + Sync + Zero + Default,
142 F::Modulus: ConstTranscribable,
143{
144 pub fn prepare_sumcheck_group<const D: usize>(
147 transcript: &mut impl Transcript,
148 trace_bin_poly: &[DenseMultilinearExtension<BinaryPoly<D>>],
149 num_vars: usize,
150 field_cfg: &F::Config,
151 ) -> Result<(MultiDegreeSumcheckGroup<F>, BoolProverAncillary), BooleanityError<F>> {
152 let n = trace_bin_poly.len();
153 let one = F::one_with_cfg(field_cfg);
154 let zero = F::zero_with_cfg(field_cfg);
155
156 let r: Vec<F> = transcript.get_field_challenges(num_vars, field_cfg);
160
161 let gamma: F = transcript.get_field_challenge(field_cfg);
163 let gamma_powers: Vec<F> = powers(gamma, one.clone(), D);
164
165 let delta: F = transcript.get_field_challenge(field_cfg);
167 let delta_powers: Vec<F> = powers(delta, one.clone(), n);
168
169 let eq_r = build_eq_x_r_inner(&r, field_cfg)?;
171
172 let mut mles = Vec::with_capacity(add!(n.saturating_mul(D), 1));
174 mles.push(eq_r);
175 mles.extend(build_witness_bit_slice_mles::<F, D>(
176 trace_bin_poly,
177 field_cfg,
178 ));
179
180 let comb_fn: CombFn<F> = Box::new(move |mle_values: &[F]| {
183 let eq_r_val = &mle_values[0];
184 let sum =
185 batched_booleanity_sum(&mle_values[1..], &delta_powers, &gamma_powers, &zero, &one);
186 sum * eq_r_val
187 });
188
189 Ok((
190 MultiDegreeSumcheckGroup::new(3, mles, comb_fn),
191 BoolProverAncillary {
192 num_wit_bin_cols: n,
193 bit_width: D,
194 num_vars,
195 },
196 ))
197 }
198
199 pub fn finalize_prover(
207 transcript: &mut impl Transcript,
208 sumcheck_prover_state: SumcheckProverState<F>,
209 ancillary: BoolProverAncillary,
210 field_cfg: &F::Config,
211 ) -> Result<BooleanityProof<F>, BooleanityError<F>> {
212 debug_assert!(
213 sumcheck_prover_state
214 .mles
215 .iter()
216 .all(|mle| mle.num_vars == 1),
217 "sumcheck should reduce MLEs to num_vars == 1"
218 );
219
220 let last_sumcheck_challenge = sumcheck_prover_state
221 .randomness
222 .last()
223 .expect("sumcheck cannot have had 0 rounds")
224 .clone();
225
226 let expected_len = ancillary
227 .num_wit_bin_cols
228 .saturating_mul(ancillary.bit_width);
229
230 let mut mles = sumcheck_prover_state.mles;
231 let bit_slice_evals: Vec<F> = mles
233 .drain(1..)
234 .map(|mle| {
235 mle.evaluate_with_config(slice::from_ref(&last_sumcheck_challenge), field_cfg)
236 })
237 .collect::<Result<Vec<_>, _>>()?;
238
239 debug_assert_eq!(bit_slice_evals.len(), expected_len);
240
241 let mut transcription_buf: Vec<u8> = vec![0; F::Inner::NUM_BYTES];
242 transcript.absorb_random_field_slice(&bit_slice_evals, &mut transcription_buf);
243
244 Ok(BooleanityProof { bit_slice_evals })
245 }
246
247 pub fn prepare_verifier(
253 transcript: &mut impl Transcript,
254 claimed_sum: &F,
255 num_wit_bin_cols: usize,
256 bit_width: usize,
257 num_vars: usize,
258 field_cfg: &F::Config,
259 ) -> Result<BoolVerifierAncillary<F>, BooleanityError<F>> {
260 if num_wit_bin_cols == 0 {
261 return Err(BooleanityError::NoBinaryPolyColumns);
262 }
263 if !F::is_zero(claimed_sum) {
264 return Err(BooleanityError::NonZeroClaimedSum {
265 got: claimed_sum.clone(),
266 });
267 }
268
269 let one = F::one_with_cfg(field_cfg);
270
271 let zerocheck_point: Vec<F> = transcript.get_field_challenges(num_vars, field_cfg);
273 let gamma: F = transcript.get_field_challenge(field_cfg);
274 let gamma_powers: Vec<F> = powers(gamma, one.clone(), bit_width);
275 let delta: F = transcript.get_field_challenge(field_cfg);
276 let delta_powers: Vec<F> = powers(delta, one, num_wit_bin_cols);
277
278 Ok(BoolVerifierAncillary {
279 gamma_powers,
280 delta_powers,
281 zerocheck_point,
282 num_wit_bin_cols,
283 bit_width,
284 num_vars,
285 })
286 }
287
288 pub fn finalize_verifier(
297 transcript: &mut impl Transcript,
298 proof: BooleanityProof<F>,
299 shared_point: Vec<F>,
300 expected_evaluation: &F,
301 ancillary: BoolVerifierAncillary<F>,
302 field_cfg: &F::Config,
303 ) -> Result<BoolVerifierSubclaim<F>, BooleanityError<F>> {
304 let expected_len = ancillary
305 .num_wit_bin_cols
306 .saturating_mul(ancillary.bit_width);
307 if proof.bit_slice_evals.len() != expected_len {
308 return Err(BooleanityError::WrongBitSliceEvalsNumber {
309 expected: expected_len,
310 got: proof.bit_slice_evals.len(),
311 });
312 }
313
314 let one = F::one_with_cfg(field_cfg);
315 let zero = F::zero_with_cfg(field_cfg);
316
317 let eq_r_val = eq_eval(&shared_point, &ancillary.zerocheck_point, one.clone())?;
319
320 let sum = batched_booleanity_sum(
322 &proof.bit_slice_evals,
323 &ancillary.delta_powers,
324 &ancillary.gamma_powers,
325 &zero,
326 &one,
327 );
328 let expected_claim_value = sum * eq_r_val;
329
330 if expected_claim_value != *expected_evaluation {
331 return Err(BooleanityError::ClaimValueDoesNotMatch {
332 expected: expected_claim_value,
333 got: expected_evaluation.clone(),
334 });
335 }
336
337 let mut transcription_buf: Vec<u8> = vec![0; F::Inner::NUM_BYTES];
338 transcript.absorb_random_field_slice(&proof.bit_slice_evals, &mut transcription_buf);
339
340 Ok(BoolVerifierSubclaim {
341 bit_slice_evals: proof.bit_slice_evals,
342 evaluation_point: shared_point,
343 })
344 }
345}
346
347#[derive(Debug, Error)]
349pub enum BooleanityError<F: PrimeField> {
350 #[error("no binary polynomial columns provided to booleanity checker")]
351 NoBinaryPolyColumns,
352 #[error("sumcheck error: {0}")]
353 SumcheckError(#[from] SumCheckError<F>),
354 #[error("expected booleanity claimed sum is non-zero: got {got}")]
355 NonZeroClaimedSum { got: F },
356 #[error("wrong number of bit-slice evaluations: expected {expected}, got {got}")]
357 WrongBitSliceEvalsNumber { expected: usize, got: usize },
358 #[error("booleanity claim value does not match: expected {expected}, got {got}")]
359 ClaimValueDoesNotMatch { expected: F, got: F },
360 #[error("error evaluating MLE: {0}")]
361 MleEvaluationError(#[from] EvaluationError),
362 #[error("arithmetic error: {0}")]
363 Arith(#[from] ArithErrors),
364}
365
366fn batched_booleanity_sum<F: PrimeField>(
384 bit_slice_values: &[F],
385 delta_powers: &[F],
386 gamma_powers: &[F],
387 zero: &F,
388 one: &F,
389) -> F {
390 let n = delta_powers.len();
391 let d = gamma_powers.len();
392 debug_assert_eq!(bit_slice_values.len(), mul!(n, d));
393
394 let mut sum = zero.clone();
395 for (j, delta_j) in delta_powers.iter().enumerate().take(n) {
396 let jd = mul!(j, d);
397 for i in 0..d {
398 let v = &bit_slice_values[add!(i, jd)];
399 let gamma_i = &gamma_powers[i];
400 let booleanity = (v.clone() - one) * v;
401 sum += booleanity * delta_j * gamma_i;
402 }
403 }
404 sum
405}
406
407pub fn build_witness_bit_slice_mles<F, const D: usize>(
418 trace_bin_poly: &[DenseMultilinearExtension<BinaryPoly<D>>],
419 field_cfg: &F::Config,
420) -> Vec<DenseMultilinearExtension<F::Inner>>
421where
422 F: PrimeField,
423 F::Inner: Clone + Send + Sync,
424{
425 let zero_inner = F::zero_with_cfg(field_cfg).into_inner();
426 let one_inner = F::one_with_cfg(field_cfg).into_inner();
427
428 let mut out = Vec::with_capacity(trace_bin_poly.len().saturating_mul(D));
429 for col in trace_bin_poly {
430 let num_vars = col.num_vars;
431 let n_rows = col.evaluations.len();
432 for i in 0..D {
433 let mut evals: Vec<F::Inner> = Vec::with_capacity(n_rows);
434 for entry in col.iter() {
435 let bit = entry
436 .iter()
437 .nth(i)
438 .expect("BinaryPoly<D> has D coefficients")
439 .into_inner();
440 evals.push(if bit {
441 one_inner.clone()
442 } else {
443 zero_inner.clone()
444 });
445 }
446 out.push(DenseMultilinearExtension {
447 num_vars,
448 evaluations: evals,
449 });
450 }
451 }
452 out
453}
454
455#[cfg(test)]
460#[allow(
461 clippy::cast_possible_truncation,
462 clippy::cast_precision_loss,
463 clippy::cast_sign_loss
464)]
465mod tests {
466 use super::*;
467 use crate::sumcheck::multi_degree::MultiDegreeSumcheck;
468 use crypto_bigint::{U128, const_monty_params};
469 use crypto_primitives::crypto_bigint_const_monty::ConstMontyField;
470 use zinc_transcript::Blake3Transcript;
471
472 const_monty_params!(TestParams, U128, "00000000b933426489189cb5b47d567f");
473 type F = ConstMontyField<TestParams, { U128::LIMBS }>;
474
475 const D: usize = 4;
476
477 fn binp_from_bits(bits: [bool; D]) -> BinaryPoly<D> {
479 let value: u64 = bits
480 .iter()
481 .enumerate()
482 .filter_map(|(i, &b)| b.then_some(1_u64 << i))
483 .fold(0_u64, |acc, mask| acc | mask);
484 BinaryPoly::from(value)
485 }
486
487 fn build_col(rows: Vec<[bool; D]>) -> DenseMultilinearExtension<BinaryPoly<D>> {
489 let num_vars = (rows.len() as f64).log2().round() as usize;
490 debug_assert_eq!(rows.len(), 1usize << num_vars);
491 let zero = BinaryPoly::zero();
492 let mut evals: Vec<BinaryPoly<D>> = rows.into_iter().map(binp_from_bits).collect();
493 if evals.is_empty() {
495 evals.push(zero);
496 }
497 DenseMultilinearExtension {
498 num_vars,
499 evaluations: evals,
500 }
501 }
502
503 fn zero_col_4rows() -> DenseMultilinearExtension<BinaryPoly<D>> {
505 build_col(vec![[false; D]; 4])
506 }
507
508 fn make_transcript() -> Blake3Transcript {
509 let mut t = Blake3Transcript::default();
510 t.absorb_slice(b"booleanity test");
511 t
512 }
513
514 fn run_roundtrip(
518 cols: &[DenseMultilinearExtension<BinaryPoly<D>>],
519 num_vars: usize,
520 tamper_proof: impl FnOnce(&mut BooleanityProof<F>),
521 expect_err_is: impl FnOnce(&BooleanityError<F>) -> bool,
522 expect_ok: bool,
523 ) {
524 let cfg = &();
525
526 let mut pt = make_transcript();
528 let (group, anc) =
529 BooleanityChecker::<F>::prepare_sumcheck_group::<D>(&mut pt, cols, num_vars, cfg)
530 .expect("prepare_sumcheck_group failed");
531 let (md_proof, states) =
532 MultiDegreeSumcheck::<F>::prove_as_subprotocol(&mut pt, vec![group], num_vars, cfg);
533 let state = states.into_iter().next().unwrap();
534 let mut proof = BooleanityChecker::<F>::finalize_prover(&mut pt, state, anc, cfg)
535 .expect("finalize prover");
536 tamper_proof(&mut proof);
537
538 let mut vt = make_transcript();
540 let anc_v = BooleanityChecker::<F>::prepare_verifier(
541 &mut vt,
542 &md_proof.claimed_sums()[0],
543 cols.len(),
544 D,
545 num_vars,
546 cfg,
547 )
548 .expect("prepare_verifier (claimed sum should be zero for honest input)");
549 let md_subclaims =
550 MultiDegreeSumcheck::<F>::verify_as_subprotocol(&mut vt, num_vars, &md_proof, cfg)
551 .expect("md verify");
552
553 let res = BooleanityChecker::<F>::finalize_verifier(
554 &mut vt,
555 proof,
556 md_subclaims.point().to_vec(),
557 &md_subclaims.expected_evaluations()[0],
558 anc_v,
559 cfg,
560 );
561
562 if expect_ok {
563 res.expect("finalize_verifier should succeed");
564 } else {
565 let err = res.expect_err("finalize_verifier should fail");
566 assert!(
567 expect_err_is(&err),
568 "unexpected booleanity error variant: {err:?}",
569 );
570 }
571 }
572
573 #[test]
574 fn happy_path_all_bits() {
575 let c0 = build_col(vec![
577 [true, false, true, false],
578 [false, true, false, true],
579 [true, true, false, false],
580 [false, false, true, true],
581 ]);
582 let c1 = build_col(vec![
583 [false, false, false, false],
584 [true, true, true, true],
585 [false, true, true, false],
586 [true, false, false, true],
587 ]);
588 run_roundtrip(&[c0, c1], 2, |_| {}, |_| false, true);
589 }
590
591 #[test]
592 fn empty_column_set_is_supported_by_helper() {
593 let cols: Vec<DenseMultilinearExtension<BinaryPoly<D>>> = vec![];
597 let mles = build_witness_bit_slice_mles::<F, D>(&cols, &());
598 assert!(mles.is_empty());
599 }
600
601 #[test]
602 fn tampered_bit_slice_evals_rejected() {
603 let c0 = zero_col_4rows();
604 run_roundtrip(
608 &[c0],
609 2,
610 |proof| {
611 proof.bit_slice_evals[0] += F::from(7u32);
612 },
613 |err| matches!(err, BooleanityError::ClaimValueDoesNotMatch { .. }),
614 false,
615 );
616 }
617
618 #[test]
619 fn non_bit_witness_rejected() {
620 let c0 = build_col(vec![
628 [true, true, false, false],
629 [false, true, true, false],
630 [false, false, false, true],
631 [true, false, true, true],
632 ]);
633 run_roundtrip(
634 &[c0],
635 2,
636 |proof| {
637 proof.bit_slice_evals[1] += F::from(3u32);
639 },
640 |err| matches!(err, BooleanityError::ClaimValueDoesNotMatch { .. }),
641 false,
642 );
643 }
644
645 #[test]
646 fn wrong_bit_slice_evals_length_rejected() {
647 let c0 = zero_col_4rows();
648 run_roundtrip(
649 &[c0],
650 2,
651 |proof| {
652 proof.bit_slice_evals.pop();
653 },
654 |err| matches!(err, BooleanityError::WrongBitSliceEvalsNumber { .. }),
655 false,
656 );
657 }
658}