1#[cfg(feature = "parallel")]
37use rayon::prelude::*;
38
39use crate::{
40 CombFn,
41 shift_predicate::eval_shift_predicate,
42 sumcheck::{
43 SumCheckError,
44 multi_degree::{
45 MultiDegreeSubClaims, MultiDegreeSumcheck, MultiDegreeSumcheckGroup,
46 MultiDegreeSumcheckProof,
47 },
48 },
49};
50use crypto_primitives::{
51 BaseFieldConfig, ProjectPrimitiveIntegersWithConfig, SemiringConfig, SetConfig,
52};
53use std::marker::PhantomData;
54use thiserror::Error;
55use zinc_poly::{
56 mle::DenseMultilinearExtension,
57 utils::{ArithErrors, build_eq_x_r, build_next_c_r_mle},
58};
59use zinc_transcript::{
60 delegate_transcribable,
61 traits::{ConstTranscribable, Transcript},
62};
63use zinc_uair::ShiftSpec;
64use zinc_utils::cfg_into_iter;
65
66#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct Proof<F> {
79 pub sumcheck_proof: MultiDegreeSumcheckProof<F>,
82}
83
84impl<F> Proof<F> {
85 pub fn try_map<T, E>(&self, f: impl FnMut(&F) -> Result<T, E> + Copy) -> Result<Proof<T>, E> {
89 Ok(Proof {
90 sumcheck_proof: self.sumcheck_proof.try_map(f)?,
91 })
92 }
93}
94
95delegate_transcribable!(Proof<F> { sumcheck_proof: MultiDegreeSumcheckProof<F> }
96 where F: ConstTranscribable);
97
98pub struct MultipointEvalFamilyInputs<'a, C: SetConfig> {
107 pub field_cfg: &'a C,
109 pub trace_mles: &'a [DenseMultilinearExtension<C::Element>],
111 pub bit_op_mles: &'a [DenseMultilinearExtension<C::Element>],
114 pub eval_point: &'a [C::Element],
116 pub up_evals: &'a [C::Element],
119 pub bit_op_evals: &'a [C::Element],
122 pub down_evals: &'a [C::Element],
125}
126
127#[derive(Clone, Debug)]
130pub struct ProverState<F> {
131 pub eval_point: Vec<F>,
135}
136
137#[derive(Clone, Debug)]
146pub struct Subclaim<F> {
147 pub r0: Vec<F>,
149 pub expected_evaluation: F,
153 pub gammas: Vec<F>,
156 pub alphas: Vec<F>,
159 pub bit_op_gammas: Vec<F>,
162 pub eq_at_r0: F,
165 pub shifts_at_r0: Vec<F>,
168}
169
170pub struct MultipointEval<C>(PhantomData<C>);
175
176impl<C> MultipointEval<C>
177where
178 C: BaseFieldConfig + ProjectPrimitiveIntegersWithConfig + 'static,
179 C::Integer: ConstTranscribable,
180{
181 #[allow(
220 clippy::arithmetic_side_effects,
221 clippy::too_many_lines,
222 clippy::type_complexity
223 )]
224 pub fn prove_as_subprotocol(
225 transcript: &mut impl Transcript,
226 families: Vec<MultipointEvalFamilyInputs<'_, C>>,
227 shifts: &[ShiftSpec],
228 q_star_cfg: &C,
229 ) -> Result<Vec<(Proof<C::Element>, ProverState<C::Element>)>, MultipointEvalError<C::Element>>
230 {
231 assert!(!families.is_empty(), "need at least one family");
232
233 let num_families = families.len();
234 let num_cols = families[0].trace_mles.len();
235 let num_vars = families[0].eval_point.len();
236 let num_down_cols = shifts.len();
237 let num_bit_op_cols = families[0].bit_op_evals.len();
238
239 for b in &families {
240 assert_eq!(
241 b.trace_mles.len(),
242 num_cols,
243 "all families must have the same number of columns",
244 );
245 assert_eq!(
246 b.eval_point.len(),
247 num_vars,
248 "all families must have the same num_vars",
249 );
250 assert_eq!(
251 b.up_evals.len(),
252 num_cols,
253 "up_evals length must match trace_mles length",
254 );
255 assert_eq!(
256 b.down_evals.len(),
257 num_down_cols,
258 "down_evals length must match shifts length",
259 );
260 assert_eq!(
261 b.bit_op_mles.len(),
262 num_bit_op_cols,
263 "all families must have the same number of bit-op MLEs",
264 );
265 assert_eq!(
266 b.bit_op_evals.len(),
267 num_bit_op_cols,
268 "all families must have the same number of bit-op evals",
269 );
270 }
271
272 let sample_int = |transcript: &mut _| {
276 let chal: C::Element = Transcript::get_field_challenge(transcript, q_star_cfg);
277 q_star_cfg.lift(&chal)
278 };
279 let shared_alpha_ints: Vec<C::Integer> =
280 (0..num_down_cols).map(|_| sample_int(transcript)).collect();
281 let shared_gamma_ints: Vec<C::Integer> =
282 (0..num_cols).map(|_| sample_int(transcript)).collect();
283 let shared_bit_op_gamma_ints: Vec<C::Integer> = (0..num_bit_op_cols)
284 .map(|_| sample_int(transcript))
285 .collect();
286
287 let per_family_alphas: Vec<Vec<C::Element>> = families
288 .iter()
289 .map(|b| {
290 shared_alpha_ints
291 .iter()
292 .map(|v| b.field_cfg.project(v))
293 .collect()
294 })
295 .collect();
296 let per_family_gammas: Vec<Vec<C::Element>> = families
297 .iter()
298 .map(|b| {
299 shared_gamma_ints
300 .iter()
301 .map(|v| b.field_cfg.project(v))
302 .collect()
303 })
304 .collect();
305 let per_family_bit_op_gammas: Vec<Vec<C::Element>> = families
306 .iter()
307 .map(|b| {
308 shared_bit_op_gamma_ints
309 .iter()
310 .map(|v| b.field_cfg.project(v))
311 .collect()
312 })
313 .collect();
314
315 let mut family_groups: Vec<(Vec<MultiDegreeSumcheckGroup<C>>, &C)> =
321 Vec::with_capacity(num_families);
322
323 let mut debug_expected_sums: Vec<C::Element> = Vec::with_capacity(num_families);
325
326 for (b_idx, family) in families.iter().enumerate() {
327 let cfg = family.field_cfg;
328 let alphas_b = per_family_alphas[b_idx].clone();
329 let gammas_b = &per_family_gammas[b_idx];
330 let bit_op_gammas_b = &per_family_bit_op_gammas[b_idx];
331 let zero = cfg.zero();
332
333 let eq_r = build_eq_x_r(cfg, family.eval_point)?;
337 let (next_mles, down_cols): (Vec<_>, Vec<_>) = shifts
338 .iter()
339 .map(|spec| {
340 let next = build_next_c_r_mle(cfg, family.eval_point, spec.shift_amount())?;
341 let col = family.trace_mles[spec.source_col()].clone();
342 Ok((next, col))
343 })
344 .collect::<Result<Vec<_>, ArithErrors>>()?
345 .into_iter()
346 .unzip();
347
348 let precombined = {
350 let evaluations: Vec<_> = cfg_into_iter!(0..1usize << num_vars)
351 .map(|i| {
352 let mut acc = zero.clone();
353 for (j, gamma) in gammas_b.iter().enumerate() {
354 let term = cfg.mul(&family.trace_mles[j].evaluations[i], gamma);
355 cfg.add_assign(&mut acc, &term);
356 }
357 for (j, bit_op_gamma) in bit_op_gammas_b.iter().enumerate() {
358 let term = cfg.mul(&family.bit_op_mles[j].evaluations[i], bit_op_gamma);
359 cfg.add_assign(&mut acc, &term);
360 }
361 acc
362 })
363 .collect();
364 DenseMultilinearExtension::from_evaluations_vec(num_vars, evaluations, zero.clone())
365 };
366
367 let mut mles = Vec::with_capacity(2 + 2 * num_down_cols);
369 mles.push(eq_r);
370 mles.extend(next_mles);
371 mles.push(precombined);
372 mles.extend(down_cols);
373
374 let comb_fn: CombFn<C::Element> = {
375 let alphas_b = alphas_b.clone();
376 let num_down_cols_local = num_down_cols;
377 let comb_cfg = cfg.clone();
378 Box::new(move |mle_values: &[C::Element]| {
379 let eq_val = &mle_values[0];
380 let precombined_val = &mle_values[num_down_cols_local + 1];
381 alphas_b.iter().enumerate().fold(
382 comb_cfg.mul(eq_val, precombined_val),
383 |acc, (k, alpha)| {
384 let next = &mle_values[1 + k];
385 let down_col = &mle_values[num_down_cols_local + 2 + k];
386 comb_cfg.add(&acc, &comb_cfg.mul(&comb_cfg.mul(alpha, next), down_col))
387 },
388 )
389 })
390 };
391
392 let group = MultiDegreeSumcheckGroup::new(2, mles, comb_fn);
393 family_groups.push((vec![group], cfg));
394
395 if cfg!(debug_assertions) {
396 debug_expected_sums.push(compute_expected_sum(
397 cfg,
398 family.up_evals,
399 family.down_evals,
400 family.bit_op_evals,
401 gammas_b,
402 &alphas_b,
403 bit_op_gammas_b,
404 ));
405 }
406 }
407
408 let sumcheck_outputs = MultiDegreeSumcheck::prove_as_subprotocol(
411 transcript,
412 family_groups,
413 num_vars,
414 q_star_cfg,
415 );
416
417 let mut result = Vec::with_capacity(num_families);
419 for (b_idx, (proof, mut prover_states)) in sumcheck_outputs.into_iter().enumerate() {
420 debug_assert_eq!(
421 prover_states.len(),
422 1,
423 "each family contributed exactly one degree group",
424 );
425 debug_assert_eq!(
426 proof.claimed_sums()[0],
427 debug_expected_sums[b_idx],
428 "claimed sum mismatch on family {b_idx}",
429 );
430 let state = prover_states.pop().expect("single group per family");
431 result.push((
432 Proof {
433 sumcheck_proof: proof,
434 },
435 ProverState {
436 eval_point: state.randomness,
437 },
438 ));
439 }
440
441 Ok(result)
442 }
443
444 #[allow(clippy::arithmetic_side_effects, clippy::too_many_arguments)]
459 pub fn verify_as_subprotocol(
460 transcript: &mut impl Transcript,
461 proofs: Vec<Proof<C::Element>>,
462 families: Vec<MultipointEvalFamilyInputs<'_, C>>,
463 shifts: &[ShiftSpec],
464 num_vars: usize,
465 q_star_cfg: &C,
466 ) -> Result<Vec<Subclaim<C::Element>>, MultipointEvalError<C::Element>> {
467 assert!(!families.is_empty(), "need at least one family");
468 assert_eq!(
469 proofs.len(),
470 families.len(),
471 "proofs and families must have the same length",
472 );
473
474 let num_cols = families[0].up_evals.len();
475 let num_down_cols = shifts.len();
476 let num_bit_op_cols = families[0].bit_op_evals.len();
477
478 for b in &families {
480 assert_eq!(
481 b.up_evals.len(),
482 num_cols,
483 "all families must have the same number of up_evals",
484 );
485 assert_eq!(
486 b.down_evals.len(),
487 num_down_cols,
488 "down_evals length must match shifts length",
489 );
490 assert_eq!(
491 b.eval_point.len(),
492 num_vars,
493 "all families must have the same num_vars",
494 );
495 assert_eq!(
496 b.bit_op_evals.len(),
497 num_bit_op_cols,
498 "all families must have the same number of bit-op evals",
499 );
500 }
501
502 let sample_int = |transcript: &mut _| {
506 let chal: C::Element = Transcript::get_field_challenge(transcript, q_star_cfg);
507 q_star_cfg.lift(&chal)
508 };
509 let shared_alpha_ints: Vec<C::Integer> =
510 (0..num_down_cols).map(|_| sample_int(transcript)).collect();
511 let shared_gamma_ints: Vec<C::Integer> =
512 (0..num_cols).map(|_| sample_int(transcript)).collect();
513 let shared_bit_op_gamma_ints: Vec<C::Integer> = (0..num_bit_op_cols)
514 .map(|_| sample_int(transcript))
515 .collect();
516
517 let per_family_alphas: Vec<Vec<C::Element>> = families
518 .iter()
519 .map(|b| {
520 shared_alpha_ints
521 .iter()
522 .map(|v| b.field_cfg.project(v))
523 .collect()
524 })
525 .collect();
526 let per_family_gammas: Vec<Vec<C::Element>> = families
527 .iter()
528 .map(|b| {
529 shared_gamma_ints
530 .iter()
531 .map(|v| b.field_cfg.project(v))
532 .collect()
533 })
534 .collect();
535 let per_family_bit_op_gammas: Vec<Vec<C::Element>> = families
536 .iter()
537 .map(|b| {
538 shared_bit_op_gamma_ints
539 .iter()
540 .map(|v| b.field_cfg.project(v))
541 .collect()
542 })
543 .collect();
544
545 for (b_idx, (proof, family)) in proofs.iter().zip(families.iter()).enumerate() {
548 let expected = compute_expected_sum(
549 family.field_cfg,
550 family.up_evals,
551 family.down_evals,
552 family.bit_op_evals,
553 &per_family_gammas[b_idx],
554 &per_family_alphas[b_idx],
555 &per_family_bit_op_gammas[b_idx],
556 );
557 let claimed = &proof.sumcheck_proof.claimed_sums()[0];
558 if claimed != &expected {
559 return Err(MultipointEvalError::WrongSumcheckSum {
560 got: claimed.clone(),
561 expected,
562 });
563 }
564 }
565
566 let proof_refs: Vec<(&MultiDegreeSumcheckProof<C::Element>, &C)> = proofs
568 .iter()
569 .zip(families.iter())
570 .map(|(p, b)| (&p.sumcheck_proof, b.field_cfg))
571 .collect();
572 let sub_claims: Vec<MultiDegreeSubClaims<C::Element>> =
573 MultiDegreeSumcheck::verify_as_subprotocol(
574 transcript,
575 num_vars,
576 &proof_refs,
577 q_star_cfg,
578 )?;
579
580 families
582 .iter()
583 .zip(sub_claims)
584 .enumerate()
585 .map(|(b_idx, (family, sub))| {
586 let cfg = family.field_cfg;
587 let r0: Vec<C::Element> = sub.point().to_vec();
588 let expected_evaluation = sub.expected_evaluations()[0].clone();
589
590 let eq_at_r0 = zinc_poly::utils::eq_eval(cfg, &r0, family.eval_point)?;
591 let shifts_at_r0: Vec<C::Element> = shifts
592 .iter()
593 .map(|spec| {
594 eval_shift_predicate(cfg, family.eval_point, &r0, spec.shift_amount())
595 })
596 .collect();
597
598 Ok(Subclaim {
599 r0,
600 expected_evaluation,
601 gammas: per_family_gammas[b_idx].clone(),
602 alphas: per_family_alphas[b_idx].clone(),
603 bit_op_gammas: per_family_bit_op_gammas[b_idx].clone(),
604 eq_at_r0,
605 shifts_at_r0,
606 })
607 })
608 .collect()
609 }
610
611 #[allow(clippy::arithmetic_side_effects)]
621 pub fn verify_subclaim(
622 subclaim: &Subclaim<C::Element>,
623 open_evals: &[C::Element],
624 bit_op_open_evals: &[C::Element],
625 shifts: &[ShiftSpec],
626 field_cfg: &C,
627 ) -> Result<(), MultipointEvalError<C::Element>> {
628 let num_cols = subclaim.gammas.len();
629 let num_bit_op_cols = subclaim.bit_op_gammas.len();
630
631 if open_evals.len() != num_cols {
632 return Err(MultipointEvalError::WrongOpenEvalsNumber {
633 got: open_evals.len(),
634 expected: num_cols,
635 });
636 }
637
638 if bit_op_open_evals.len() != num_bit_op_cols {
639 return Err(MultipointEvalError::WrongBitOpOpenEvalsNumber {
640 got: bit_op_open_evals.len(),
641 expected: num_bit_op_cols,
642 });
643 }
644
645 let zero = field_cfg.zero();
646
647 let batched_up: C::Element = subclaim
648 .gammas
649 .iter()
650 .zip(open_evals.iter())
651 .fold(zero.clone(), |acc, (gamma, eval)| {
652 field_cfg.add(&acc, &field_cfg.mul(gamma, eval))
653 });
654 let batched_up = subclaim
655 .bit_op_gammas
656 .iter()
657 .zip(bit_op_open_evals.iter())
658 .fold(batched_up, |acc, (gamma, eval)| {
659 field_cfg.add(&acc, &field_cfg.mul(gamma, eval))
660 });
661
662 let batched_down: C::Element = subclaim
666 .alphas
667 .iter()
668 .enumerate()
669 .zip(subclaim.shifts_at_r0.iter())
670 .fold(zero, |acc, ((k, alpha), shift_at_r0)| {
671 let src_col = shifts[k].source_col();
672 field_cfg.add(
673 &acc,
674 &field_cfg.mul(&field_cfg.mul(alpha, shift_at_r0), &open_evals[src_col]),
675 )
676 });
677
678 let expected_evaluation = field_cfg.add(
679 &field_cfg.mul(&subclaim.eq_at_r0, &batched_up),
680 &batched_down,
681 );
682
683 if expected_evaluation != subclaim.expected_evaluation {
684 return Err(MultipointEvalError::ClaimMismatch {
685 got: subclaim.expected_evaluation.clone(),
686 expected: expected_evaluation,
687 });
688 }
689
690 Ok(())
691 }
692}
693
694#[allow(clippy::too_many_arguments)]
698fn compute_expected_sum<C: SemiringConfig>(
699 cfg: &C,
700 up_evals: &[C::Element],
701 down_evals: &[C::Element],
702 bit_op_evals: &[C::Element],
703 gammas: &[C::Element],
704 alphas: &[C::Element],
705 bit_op_gammas: &[C::Element],
706) -> C::Element {
707 let up_sum = gammas
708 .iter()
709 .zip(up_evals.iter())
710 .fold(cfg.zero(), |acc, (gamma, up)| {
711 cfg.add(&acc, &cfg.mul(gamma, up))
712 });
713
714 let up_and_down = alphas
715 .iter()
716 .zip(down_evals.iter())
717 .fold(up_sum, |acc, (alpha, down)| {
718 cfg.add(&acc, &cfg.mul(alpha, down))
719 });
720
721 bit_op_gammas
722 .iter()
723 .zip(bit_op_evals.iter())
724 .fold(up_and_down, |acc, (gamma, eval)| {
725 cfg.add(&acc, &cfg.mul(gamma, eval))
726 })
727}
728
729#[derive(Debug, Error)]
734pub enum MultipointEvalError<F: std::fmt::Debug> {
735 #[error("wrong number of open evaluations: got {got}, expected {expected}")]
736 WrongOpenEvalsNumber { got: usize, expected: usize },
737 #[error("wrong number of bit-op open evaluations: got {got}, expected {expected}")]
738 WrongBitOpOpenEvalsNumber { got: usize, expected: usize },
739 #[error("wrong sumcheck claimed sum: got {got:?}, expected {expected:?}")]
740 WrongSumcheckSum { got: F, expected: F },
741 #[error("multi-point eval claim mismatch: got {got:?}, expected {expected:?}")]
742 ClaimMismatch { got: F, expected: F },
743 #[error("sumcheck error: {0}")]
744 SumcheckError(#[from] SumCheckError<F>),
745 #[error("arithmetic error: {0}")]
746 ArithError(#[from] ArithErrors),
747}
748
749#[cfg(test)]
750#[allow(
751 clippy::arithmetic_side_effects,
752 clippy::cast_possible_truncation,
753 clippy::cast_possible_wrap,
754 clippy::cast_sign_loss
755)]
756mod tests {
757 use super::*;
758 use crypto_bigint::{U128, const_monty_params};
759 use crypto_primitives::{FixedConfig, crypto_bigint_const_monty::ConstMontyField};
760 use num_traits::{ConstOne, ConstZero};
761 use zinc_poly::mle::DenseMultilinearExtension;
762 use zinc_transcript::Blake3Transcript;
763
764 const_monty_params!(Params, U128, "00000000b933426489189cb5b47d567f");
765 type F = ConstMontyField<Params, { U128::LIMBS }>;
766 type Cfg = FixedConfig<F>;
767
768 #[derive(Clone)]
770 struct SharedSubprotocolInput {
771 eval_point: Vec<F>,
772 up_evals: Vec<F>,
773 down_evals: Vec<F>,
774 shifts: Vec<ShiftSpec>,
775 num_vars: usize,
776 }
777
778 #[derive(Clone)]
780 struct ProverMessage {
781 proof: Proof<F>,
782 open_evals: Vec<F>,
783 }
784
785 fn make_transcript() -> Blake3Transcript {
786 let mut t = Blake3Transcript::default();
787 t.absorb_bytes(b"Lorem ipsum");
788 t
789 }
790
791 fn build_trace(
792 num_vars: usize,
793 num_cols: usize,
794 shifts: &[ShiftSpec],
795 ) -> (Vec<DenseMultilinearExtension<F>>, SharedSubprotocolInput) {
796 let cfg = Cfg::default();
797 let n = 1usize << num_vars;
798
799 let trace_mles: Vec<DenseMultilinearExtension<_>> = (0..num_cols)
800 .map(|col| {
801 let evals: Vec<_> = (0..n).map(|i| F::from((col * n + i + 1) as u32)).collect();
802 DenseMultilinearExtension::from_evaluations_vec(num_vars, evals, F::ZERO)
803 })
804 .collect();
805
806 let eval_point: Vec<F> = (0..num_vars).map(|i| F::from((i + 7) as u32)).collect();
807
808 let up_evals: Vec<F> = trace_mles
809 .iter()
810 .map(|mle| mle.clone().evaluate(&cfg, &eval_point).unwrap())
811 .collect();
812
813 let down_evals: Vec<F> = shifts
814 .iter()
815 .map(|spec| {
816 let mle = &trace_mles[spec.source_col()];
817 let c = spec.shift_amount();
818 let mut shifted = mle.evaluations[c..].to_vec();
819 shifted.extend(vec![F::ZERO; c]);
820 let shifted_mle =
821 DenseMultilinearExtension::from_evaluations_vec(num_vars, shifted, F::ZERO);
822 shifted_mle.evaluate(&cfg, &eval_point).unwrap()
823 })
824 .collect();
825
826 let public = SharedSubprotocolInput {
827 eval_point,
828 up_evals,
829 down_evals,
830 shifts: shifts.to_vec(),
831 num_vars,
832 };
833 (trace_mles, public)
834 }
835
836 fn run_prover(
838 trace_mles: &[DenseMultilinearExtension<F>],
839 public: &SharedSubprotocolInput,
840 ) -> ProverMessage {
841 let cfg = Cfg::default();
842 let mut transcript = make_transcript();
843 let mut outputs = MultipointEval::<Cfg>::prove_as_subprotocol(
844 &mut transcript,
845 vec![MultipointEvalFamilyInputs {
846 field_cfg: &cfg,
847 trace_mles,
848 bit_op_mles: &[],
849 eval_point: &public.eval_point,
850 up_evals: &public.up_evals,
851 bit_op_evals: &[],
852 down_evals: &public.down_evals,
853 }],
854 &public.shifts,
855 &cfg,
856 )
857 .expect("prover should succeed");
858 assert_eq!(outputs.len(), 1, "single-family shape");
859 let (proof, prover_state) = outputs.pop().expect("single family");
860
861 let r_0 = &prover_state.eval_point;
862 let open_evals: Vec<F> = trace_mles
863 .iter()
864 .map(|mle| mle.clone().evaluate(&cfg, r_0).unwrap())
865 .collect();
866
867 ProverMessage { proof, open_evals }
868 }
869
870 fn run_verifier(
872 public: &SharedSubprotocolInput,
873 msg: &ProverMessage,
874 ) -> Result<Subclaim<F>, MultipointEvalError<F>> {
875 let cfg = Cfg::default();
876 let mut subclaims = MultipointEval::<Cfg>::verify_as_subprotocol(
877 &mut make_transcript(),
878 vec![msg.proof.clone()],
879 vec![MultipointEvalFamilyInputs {
880 field_cfg: &cfg,
881 trace_mles: &[],
882 bit_op_mles: &[],
883 eval_point: &public.eval_point,
884 up_evals: &public.up_evals,
885 bit_op_evals: &[],
886 down_evals: &public.down_evals,
887 }],
888 &public.shifts,
889 public.num_vars,
890 &cfg,
891 )?;
892 assert_eq!(subclaims.len(), 1, "single-family shape");
893 let subclaim = subclaims.pop().expect("single family");
894
895 MultipointEval::<Cfg>::verify_subclaim(
896 &subclaim,
897 &msg.open_evals,
898 &[],
899 &public.shifts,
900 &cfg,
901 )?;
902
903 Ok(subclaim)
904 }
905
906 fn honest_interaction(
908 num_vars: usize,
909 num_cols: usize,
910 shifts: &[ShiftSpec],
911 ) -> (SharedSubprotocolInput, ProverMessage) {
912 let (trace, public) = build_trace(num_vars, num_cols, shifts);
913 let msg = run_prover(&trace, &public);
914 (public, msg)
915 }
916
917 fn all_shift_by_1(num_cols: usize) -> Vec<ShiftSpec> {
919 (0..num_cols).map(|i| ShiftSpec::new(i, 1)).collect()
920 }
921
922 #[test]
925 fn honest_prove_verify_single_column() {
926 let shifts = all_shift_by_1(1);
927 let (public, msg) = honest_interaction(4, 1, &shifts);
928 run_verifier(&public, &msg).unwrap();
929 }
930
931 #[test]
932 fn honest_prove_verify_many_columns() {
933 let shifts = all_shift_by_1(10);
934 let (public, msg) = honest_interaction(3, 10, &shifts);
935 run_verifier(&public, &msg).unwrap();
936 }
937
938 #[test]
939 fn honest_prove_verify_no_shifts() {
940 let (public, msg) = honest_interaction(3, 3, &[]);
941 run_verifier(&public, &msg).unwrap();
942 }
943
944 #[test]
945 fn honest_prove_verify_mixed_shifts() {
946 let shifts = vec![ShiftSpec::new(0, 1), ShiftSpec::new(1, 3)];
947 let (public, msg) = honest_interaction(4, 3, &shifts);
948 run_verifier(&public, &msg).unwrap();
949 }
950
951 #[test]
952 fn honest_prove_verify_shift_by_3() {
953 let shifts = vec![
954 ShiftSpec::new(0, 3),
955 ShiftSpec::new(1, 3),
956 ShiftSpec::new(2, 3),
957 ];
958 let (public, msg) = honest_interaction(4, 3, &shifts);
959 run_verifier(&public, &msg).unwrap();
960 }
961
962 #[test]
963 fn honest_prove_verify_same_col_different_shifts() {
964 let shifts = vec![ShiftSpec::new(0, 2), ShiftSpec::new(0, 5)];
966 let (public, msg) = honest_interaction(4, 3, &shifts);
967 run_verifier(&public, &msg).unwrap();
968 }
969
970 #[test]
971 fn bit_op_virtual_opening_is_bound_in_subclaim() {
972 let cfg = Cfg::default();
973 let shifts = vec![ShiftSpec::new(0, 1)];
974 let (trace_mles, public) = build_trace(3, 2, &shifts);
975
976 let bit_op_mles = vec![DenseMultilinearExtension::from_evaluations_vec(
977 public.num_vars,
978 trace_mles[0]
979 .evaluations
980 .iter()
981 .map(|eval| *eval + F::from(11_u32))
982 .collect(),
983 F::ZERO,
984 )];
985 let bit_op_evals: Vec<F> = bit_op_mles
986 .iter()
987 .map(|mle| mle.clone().evaluate(&cfg, &public.eval_point).unwrap())
988 .collect();
989
990 let mut prover_transcript = make_transcript();
991 let mut prover_outputs = MultipointEval::<Cfg>::prove_as_subprotocol(
992 &mut prover_transcript,
993 vec![MultipointEvalFamilyInputs {
994 field_cfg: &cfg,
995 trace_mles: &trace_mles,
996 bit_op_mles: &bit_op_mles,
997 eval_point: &public.eval_point,
998 up_evals: &public.up_evals,
999 bit_op_evals: &bit_op_evals,
1000 down_evals: &public.down_evals,
1001 }],
1002 &public.shifts,
1003 &cfg,
1004 )
1005 .expect("prover should succeed");
1006 assert_eq!(prover_outputs.len(), 1, "single-family shape");
1007 let (proof, prover_state) = prover_outputs.pop().expect("single family");
1008
1009 let r_0 = &prover_state.eval_point;
1010 let open_evals: Vec<F> = trace_mles
1011 .iter()
1012 .map(|mle| mle.clone().evaluate(&cfg, r_0).unwrap())
1013 .collect();
1014 let bit_op_open_evals: Vec<F> = bit_op_mles
1015 .iter()
1016 .map(|mle| mle.clone().evaluate(&cfg, r_0).unwrap())
1017 .collect();
1018
1019 let mut verifier_transcript = make_transcript();
1020 let mut subclaims = MultipointEval::<Cfg>::verify_as_subprotocol(
1021 &mut verifier_transcript,
1022 vec![proof],
1023 vec![MultipointEvalFamilyInputs {
1024 field_cfg: &cfg,
1025 trace_mles: &[],
1026 bit_op_mles: &[],
1027 eval_point: &public.eval_point,
1028 up_evals: &public.up_evals,
1029 bit_op_evals: &bit_op_evals,
1030 down_evals: &public.down_evals,
1031 }],
1032 &public.shifts,
1033 public.num_vars,
1034 &cfg,
1035 )
1036 .expect("verifier should accept sumcheck");
1037 assert_eq!(subclaims.len(), 1, "single-family shape");
1038 let subclaim = subclaims.pop().expect("single family");
1039
1040 MultipointEval::<Cfg>::verify_subclaim(
1041 &subclaim,
1042 &open_evals,
1043 &bit_op_open_evals,
1044 &public.shifts,
1045 &cfg,
1046 )
1047 .expect("correct bit-op opening should satisfy subclaim");
1048
1049 let mut bad_bit_op_open_evals = bit_op_open_evals;
1050 bad_bit_op_open_evals[0] += F::ONE;
1051 let err = MultipointEval::<Cfg>::verify_subclaim(
1052 &subclaim,
1053 &open_evals,
1054 &bad_bit_op_open_evals,
1055 &public.shifts,
1056 &cfg,
1057 )
1058 .unwrap_err();
1059 assert!(
1060 matches!(err, MultipointEvalError::ClaimMismatch { .. }),
1061 "expected ClaimMismatch, got {err:?}",
1062 );
1063 }
1064
1065 #[test]
1068 fn bad_down_eval_rejected_mixed_shifts() {
1069 let shifts = vec![ShiftSpec::new(0, 1), ShiftSpec::new(1, 3)];
1070 let (mut public, msg) = honest_interaction(4, 3, &shifts);
1071 public.down_evals[0] += F::ONE;
1072 let err = run_verifier(&public, &msg).unwrap_err();
1073 assert!(
1074 matches!(err, MultipointEvalError::WrongSumcheckSum { .. }),
1075 "expected WrongSumcheckSum, got {err:?}",
1076 );
1077 }
1078
1079 #[test]
1082 fn wrong_open_evals_count() {
1083 let shifts = all_shift_by_1(3);
1084 let (public, msg) = honest_interaction(3, 3, &shifts);
1085
1086 let mut msg_short = msg.clone();
1087 msg_short.open_evals.pop();
1088
1089 let mut msg_long = msg;
1090 msg_long.open_evals.push(F::from(42_u32));
1091
1092 for bad_msg in [&msg_short, &msg_long] {
1093 let err = run_verifier(&public, bad_msg).unwrap_err();
1094 assert!(
1095 matches!(err, MultipointEvalError::WrongOpenEvalsNumber {
1096 got,
1097 expected: 3,
1098 } if got == bad_msg.open_evals.len()),
1099 "expected WrongOpenEvalsNumber, got {err:?}",
1100 );
1101 }
1102 }
1103
1104 #[test]
1107 fn wrong_claimed_sum_via_corrupted_up_evals() {
1108 let shifts = all_shift_by_1(3);
1109 let (mut public, msg) = honest_interaction(3, 3, &shifts);
1110 public.up_evals[0] += F::ONE;
1111 let err = run_verifier(&public, &msg).unwrap_err();
1112 assert!(
1113 matches!(err, MultipointEvalError::WrongSumcheckSum { .. }),
1114 "expected WrongSumcheckSum, got {err:?}",
1115 );
1116 }
1117
1118 #[test]
1119 fn wrong_claimed_sum_via_corrupted_down_evals() {
1120 let shifts = all_shift_by_1(3);
1121 let (mut public, msg) = honest_interaction(3, 3, &shifts);
1122 public.down_evals[1] += F::ONE;
1123 let err = run_verifier(&public, &msg).unwrap_err();
1124 assert!(
1125 matches!(err, MultipointEvalError::WrongSumcheckSum { .. }),
1126 "expected WrongSumcheckSum, got {err:?}",
1127 );
1128 }
1129
1130 #[test]
1133 fn wrong_open_eval_value() {
1134 let shifts = all_shift_by_1(3);
1135 let (public, mut msg) = honest_interaction(3, 3, &shifts);
1136 msg.open_evals[0] += F::ONE;
1137 let err = run_verifier(&public, &msg).unwrap_err();
1138 assert!(
1139 matches!(err, MultipointEvalError::ClaimMismatch { .. }),
1140 "expected ClaimMismatch, got {err:?}",
1141 );
1142 }
1143
1144 #[test]
1145 fn all_open_evals_zeroed() {
1146 let shifts = all_shift_by_1(3);
1147 let (public, mut msg) = honest_interaction(3, 3, &shifts);
1148 for e in &mut msg.open_evals {
1149 *e = F::ZERO;
1150 }
1151 let err = run_verifier(&public, &msg).unwrap_err();
1152 assert!(
1153 matches!(err, MultipointEvalError::ClaimMismatch { .. }),
1154 "expected ClaimMismatch, got {err:?}",
1155 );
1156 }
1157
1158 fn mixed_shifts() -> Vec<ShiftSpec> {
1161 vec![ShiftSpec::new(0, 1), ShiftSpec::new(1, 3)]
1162 }
1163
1164 #[test]
1165 fn mixed_shifts_corrupted_up_eval() {
1166 let (mut public, msg) = honest_interaction(4, 3, &mixed_shifts());
1167 public.up_evals[2] += F::ONE; let err = run_verifier(&public, &msg).unwrap_err();
1169 assert!(
1170 matches!(err, MultipointEvalError::WrongSumcheckSum { .. }),
1171 "expected WrongSumcheckSum, got {err:?}",
1172 );
1173 }
1174
1175 #[test]
1176 fn mixed_shifts_wrong_open_eval() {
1177 let (public, mut msg) = honest_interaction(4, 3, &mixed_shifts());
1178 msg.open_evals[1] += F::ONE; let err = run_verifier(&public, &msg).unwrap_err();
1180 assert!(
1181 matches!(err, MultipointEvalError::ClaimMismatch { .. }),
1182 "expected ClaimMismatch, got {err:?}",
1183 );
1184 }
1185
1186 #[test]
1187 fn mixed_shifts_tampered_sumcheck() {
1188 let (public, mut msg) = honest_interaction(4, 3, &mixed_shifts());
1189 msg.proof.sumcheck_proof.group_messages_mut()[0][0]
1190 .0
1191 .tail_evaluations[0] += F::ONE;
1192 let err = run_verifier(&public, &msg).unwrap_err();
1193 assert!(
1194 matches!(
1195 err,
1196 MultipointEvalError::SumcheckError(_) | MultipointEvalError::ClaimMismatch { .. }
1197 ),
1198 "expected sumcheck or consistency error, got {err:?}",
1199 );
1200 }
1201
1202 #[test]
1205 fn tampered_sumcheck_round_message() {
1206 let shifts = all_shift_by_1(3);
1207 let (public, mut msg) = honest_interaction(3, 3, &shifts);
1208 msg.proof.sumcheck_proof.group_messages_mut()[0][0]
1209 .0
1210 .tail_evaluations[0] += F::ONE;
1211 let err = run_verifier(&public, &msg).unwrap_err();
1212 assert!(
1213 matches!(
1214 err,
1215 MultipointEvalError::SumcheckError(_) | MultipointEvalError::ClaimMismatch { .. }
1216 ),
1217 "expected sumcheck or consistency error, got {err:?}",
1218 );
1219 }
1220}