use std::iter::zip;
use itertools::Itertools;
use num_traits::{One, Zero};
use thiserror::Error;
use super::utils::UnivariatePoly;
use crate::core::channel::Channel;
use crate::core::fields::m31::BaseField;
use crate::core::fields::qm31::SecureField;
pub trait MultivariatePolyOracle: Sized {
fn n_variables(&self) -> usize;
fn sum_as_poly_in_first_variable(&self, claim: SecureField) -> UnivariatePoly<SecureField>;
fn fix_first_variable(self, challenge: SecureField) -> Self;
}
pub fn prove_batch<O: MultivariatePolyOracle>(
mut claims: Vec<SecureField>,
mut multivariate_polys: Vec<O>,
lambda: SecureField,
channel: &mut impl Channel,
) -> (SumcheckProof, Vec<SecureField>, Vec<O>, Vec<SecureField>) {
let n_variables = multivariate_polys.iter().map(O::n_variables).max().unwrap();
assert_eq!(claims.len(), multivariate_polys.len());
let mut round_polys = Vec::new();
let mut assignment = Vec::new();
for (claim, multivariate_poly) in zip(&mut claims, &multivariate_polys) {
let n_unused_variables = n_variables - multivariate_poly.n_variables();
*claim *= BaseField::from(1 << n_unused_variables);
}
for round in 0..n_variables {
let n_remaining_rounds = n_variables - round;
let this_round_polys = zip(&multivariate_polys, &claims)
.enumerate()
.map(|(i, (multivariate_poly, &claim))| {
let round_poly = if n_remaining_rounds == multivariate_poly.n_variables() {
multivariate_poly.sum_as_poly_in_first_variable(claim)
} else {
(claim / BaseField::from(2)).into()
};
let eval_at_0 = round_poly.eval_at_point(SecureField::zero());
let eval_at_1 = round_poly.eval_at_point(SecureField::one());
assert_eq!(eval_at_0 + eval_at_1, claim, "i={i}, round={round}");
assert!(round_poly.degree() <= MAX_DEGREE, "i={i}, round={round}");
round_poly
})
.collect_vec();
let round_poly = random_linear_combination(&this_round_polys, lambda);
channel.mix_felts(&round_poly);
let challenge = channel.draw_secure_felt();
claims = this_round_polys
.iter()
.map(|round_poly| round_poly.eval_at_point(challenge))
.collect();
multivariate_polys = multivariate_polys
.into_iter()
.map(|multivariate_poly| {
if n_remaining_rounds != multivariate_poly.n_variables() {
return multivariate_poly;
}
multivariate_poly.fix_first_variable(challenge)
})
.collect();
round_polys.push(round_poly);
assignment.push(challenge);
}
let proof = SumcheckProof { round_polys };
(proof, assignment, multivariate_polys, claims)
}
fn random_linear_combination(
polys: &[UnivariatePoly<SecureField>],
alpha: SecureField,
) -> UnivariatePoly<SecureField> {
polys
.iter()
.rfold(Zero::zero(), |acc, poly| acc * alpha + poly.clone())
}
pub fn partially_verify(
mut claim: SecureField,
proof: &SumcheckProof,
channel: &mut impl Channel,
) -> Result<(Vec<SecureField>, SecureField), SumcheckError> {
let mut assignment = Vec::new();
for (round, round_poly) in proof.round_polys.iter().enumerate() {
if round_poly.degree() > MAX_DEGREE {
return Err(SumcheckError::DegreeInvalid { round });
}
let sum = round_poly.eval_at_point(Zero::zero()) + round_poly.eval_at_point(One::one());
if claim != sum {
return Err(SumcheckError::SumInvalid { claim, sum, round });
}
channel.mix_felts(round_poly);
let challenge = channel.draw_secure_felt();
claim = round_poly.eval_at_point(challenge);
assignment.push(challenge);
}
Ok((assignment, claim))
}
#[derive(Debug, Clone)]
pub struct SumcheckProof {
pub round_polys: Vec<UnivariatePoly<SecureField>>,
}
pub const MAX_DEGREE: usize = 3;
#[derive(Error, Debug)]
pub enum SumcheckError {
#[error("degree of the polynomial in round {round} is too high")]
DegreeInvalid { round: RoundIndex },
#[error("sum does not match the claim in round {round} (sum {sum}, claim {claim})")]
SumInvalid {
claim: SecureField,
sum: SecureField,
round: RoundIndex,
},
}
pub type RoundIndex = usize;
#[cfg(test)]
mod tests {
use num_traits::One;
use crate::core::channel::{Blake2sChannel, Channel};
use crate::core::fields::qm31::SecureField;
use crate::core::fields::Field;
use crate::prover::backend::CpuBackend;
use crate::prover::lookups::mle::Mle;
use crate::prover::lookups::sumcheck::{partially_verify, prove_batch};
#[test]
fn sumcheck_works() {
let values = test_channel().draw_secure_felts(32);
let claim = values.iter().sum();
let mle = Mle::<CpuBackend, SecureField>::new(values);
let lambda = SecureField::one();
let (proof, ..) = prove_batch(vec![claim], vec![mle.clone()], lambda, &mut test_channel());
let (assignment, eval) = partially_verify(claim, &proof, &mut test_channel()).unwrap();
assert_eq!(eval, mle.eval_at_point(&assignment));
}
#[test]
fn batch_sumcheck_works() {
let mut channel = test_channel();
let values0 = channel.draw_secure_felts(32);
let values1 = channel.draw_secure_felts(32);
let claim0 = values0.iter().sum();
let claim1 = values1.iter().sum();
let mle0 = Mle::<CpuBackend, SecureField>::new(values0.clone());
let mle1 = Mle::<CpuBackend, SecureField>::new(values1.clone());
let lambda = channel.draw_secure_felt();
let claims = vec![claim0, claim1];
let mles = vec![mle0.clone(), mle1.clone()];
let (proof, ..) = prove_batch(claims, mles, lambda, &mut test_channel());
let claim = claim0 + lambda * claim1;
let (assignment, eval) = partially_verify(claim, &proof, &mut test_channel()).unwrap();
let eval0 = mle0.eval_at_point(&assignment);
let eval1 = mle1.eval_at_point(&assignment);
assert_eq!(eval, eval0 + lambda * eval1);
}
#[test]
fn batch_sumcheck_with_different_n_variables() {
let mut channel = test_channel();
let values0 = channel.draw_secure_felts(64);
let values1 = channel.draw_secure_felts(32);
let claim0 = values0.iter().sum();
let claim1 = values1.iter().sum();
let mle0 = Mle::<CpuBackend, SecureField>::new(values0.clone());
let mle1 = Mle::<CpuBackend, SecureField>::new(values1.clone());
let lambda = channel.draw_secure_felt();
let claims = vec![claim0, claim1];
let mles = vec![mle0.clone(), mle1.clone()];
let (proof, ..) = prove_batch(claims, mles, lambda, &mut test_channel());
let claim = claim0 + lambda * claim1.double();
let (assignment, eval) = partially_verify(claim, &proof, &mut test_channel()).unwrap();
let eval0 = mle0.eval_at_point(&assignment);
let eval1 = mle1.eval_at_point(&assignment[1..]);
assert_eq!(eval, eval0 + lambda * eval1);
}
#[test]
fn invalid_sumcheck_proof_fails() {
let values = test_channel().draw_secure_felts(8);
let claim = values.iter().sum::<SecureField>();
let lambda = SecureField::one();
let mut invalid_values = values;
invalid_values[0] += SecureField::one();
let invalid_claim = vec![invalid_values.iter().sum::<SecureField>()];
let invalid_mle = vec![Mle::<CpuBackend, SecureField>::new(invalid_values.clone())];
let (invalid_proof, ..) =
prove_batch(invalid_claim, invalid_mle, lambda, &mut test_channel());
assert!(partially_verify(claim, &invalid_proof, &mut test_channel()).is_err());
}
fn test_channel() -> Blake2sChannel {
Blake2sChannel::default()
}
}