// code forked from:
// https://github.com/EspressoSystems/hyperplonk/tree/main/subroutines/src/poly_iop/sum_check
//
// Copyright (c) 2023 Espresso Systems (espressosys.com)
// This file is part of the HyperPlonk library.
// You should have received a copy of the MIT License
// along with the HyperPlonk library. If not, see .
//! Verifier subroutines for a SumCheck protocol.
use super::{
structs::{IOPProverMessage, IOPVerifierState},
SumCheckSubClaim, SumCheckVerifier,
};
use crate::{transcript::Transcript, utils::virtual_polynomial::VPAuxInfo};
use ark_crypto_primitives::sponge::Absorb;
use ark_ff::PrimeField;
use ark_poly::Polynomial;
use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial};
use ark_std::{end_timer, start_timer};
use espresso_subroutines::poly_iop::prelude::PolyIOPErrors;
#[cfg(feature = "parallel")]
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
impl SumCheckVerifier for IOPVerifierState {
type VPAuxInfo = VPAuxInfo;
type ProverMessage = IOPProverMessage;
type Challenge = F;
type SumCheckSubClaim = SumCheckSubClaim;
/// Initialize the verifier's state.
fn verifier_init(index_info: &Self::VPAuxInfo) -> Self {
let start = start_timer!(|| "sum check verifier init");
let res = Self {
round: 1,
num_vars: index_info.num_variables,
finished: false,
polynomials_received: Vec::with_capacity(index_info.num_variables),
challenges: Vec::with_capacity(index_info.num_variables),
};
end_timer!(start);
res
}
fn verify_round_and_update_state(
&mut self,
prover_msg: & as SumCheckVerifier>::ProverMessage,
transcript: &mut impl Transcript,
) -> Result< as SumCheckVerifier>::Challenge, PolyIOPErrors> {
let start =
start_timer!(|| format!("sum check verify {}-th round and update state", self.round));
if self.finished {
return Err(PolyIOPErrors::InvalidVerifier(
"Incorrect verifier state: Verifier is already finished.".to_string(),
));
}
// In an interactive protocol, the verifier should
//
// 1. check if the received 'P(0) + P(1) = expected`.
// 2. set `expected` to P(r)`
//
// When we turn the protocol to a non-interactive one, it is sufficient to defer
// such checks to `check_and_generate_subclaim` after the last round.
let challenge = transcript.get_challenge();
self.challenges.push(challenge);
self.polynomials_received.push(prover_msg.coeffs.to_vec());
if self.round == self.num_vars {
// accept and close
self.finished = true;
} else {
// proceed to the next round
self.round += 1;
}
end_timer!(start);
Ok(challenge)
}
fn check_and_generate_subclaim(
&self,
asserted_sum: &F,
) -> Result {
let start = start_timer!(|| "sum check check and generate subclaim");
if !self.finished {
return Err(PolyIOPErrors::InvalidVerifier(
"Incorrect verifier state: Verifier has not finished.".to_string(),
));
}
if self.polynomials_received.len() != self.num_vars {
return Err(PolyIOPErrors::InvalidVerifier(
"insufficient rounds".to_string(),
));
}
// the deferred check during the interactive phase:
// 2. set `expected` to P(r)`
#[cfg(feature = "parallel")]
let mut expected_vec = self
.polynomials_received
.clone()
.into_par_iter()
.zip(self.challenges.clone().into_par_iter())
.map(|(coeffs, challenge)| {
// Removed check on number of evaluations here since verifier receives polynomial in coeffs form
let prover_poly = DensePolynomial::from_coefficients_slice(&coeffs);
Ok(prover_poly.evaluate(&challenge))
})
.collect::, PolyIOPErrors>>()?;
#[cfg(not(feature = "parallel"))]
let mut expected_vec = self
.polynomials_received
.clone()
.into_iter()
.zip(self.challenges.clone().into_iter())
.map(|(coeffs, challenge)| {
// Removed check on number of evaluations here since verifier receives polynomial in coeffs form
let prover_poly = DensePolynomial::from_coefficients_slice(&coeffs);
Ok(prover_poly.evaluate(&challenge))
})
.collect::, PolyIOPErrors>>()?;
// insert the asserted_sum to the first position of the expected vector
expected_vec.insert(0, *asserted_sum);
for (coeffs, &expected) in self
.polynomials_received
.iter()
.zip(expected_vec.iter())
.take(self.num_vars)
{
let poly = DensePolynomial::from_coefficients_slice(coeffs);
let eval_at_one: F = poly.iter().sum();
let eval_at_zero: F = poly.coeffs[0];
let eval = eval_at_one + eval_at_zero;
// the deferred check during the interactive phase:
// 1. check if the received 'P(0) + P(1) = expected`.
if eval != expected {
return Err(PolyIOPErrors::InvalidProof(
"Prover message is not consistent with the claim.".to_string(),
));
}
}
end_timer!(start);
Ok(SumCheckSubClaim {
point: self.challenges.clone(),
// the last expected value (not checked within this function) will be included in the
// subclaim
expected_evaluation: expected_vec[self.num_vars],
})
}
}
/// Interpolate a uni-variate degree-`p_i.len()-1` polynomial and evaluate this
/// polynomial at `eval_at`:
///
/// \sum_{i=0}^len p_i * (\prod_{j!=i} (eval_at - j)/(i-j) )
///
/// This implementation is linear in number of inputs in terms of field
/// operations. It also has a quadratic term in primitive operations which is
/// negligible compared to field operations.
/// TODO: The quadratic term can be removed by precomputing the lagrange
/// coefficients.
pub fn interpolate_uni_poly(p_i: &[F], eval_at: F) -> Result {
let start = start_timer!(|| "sum check interpolate uni poly opt");
let len = p_i.len();
let mut evals = vec![];
let mut prod = eval_at;
evals.push(eval_at);
// `prod = \prod_{j} (eval_at - j)`
for e in 1..len {
let tmp = eval_at - F::from(e as u64);
evals.push(tmp);
prod *= tmp;
}
let mut res = F::zero();
// we want to compute \prod (j!=i) (i-j) for a given i
//
// we start from the last step, which is
// denom[len-1] = (len-1) * (len-2) *... * 2 * 1
// the step before that is
// denom[len-2] = (len-2) * (len-3) * ... * 2 * 1 * -1
// and the step before that is
// denom[len-3] = (len-3) * (len-4) * ... * 2 * 1 * -1 * -2
//
// i.e., for any i, the one before this will be derived from
// denom[i-1] = denom[i] * (len-i) / i
//
// that is, we only need to store
// - the last denom for i = len-1, and
// - the ratio between current step and fhe last step, which is the product of
// (len-i) / i from all previous steps and we store this product as a fraction
// number to reduce field divisions.
// We know
// - 2^61 < factorial(20) < 2^62
// - 2^122 < factorial(33) < 2^123
// so we will be able to compute the ratio
// - for len <= 20 with i64
// - for len <= 33 with i128
// - for len > 33 with BigInt
if p_i.len() <= 20 {
let last_denominator = F::from(u64_factorial(len - 1));
let mut ratio_numerator = 1i64;
let mut ratio_denominator = 1u64;
for i in (0..len).rev() {
let ratio_numerator_f = if ratio_numerator < 0 {
-F::from((-ratio_numerator) as u64)
} else {
F::from(ratio_numerator as u64)
};
res += p_i[i] * prod * F::from(ratio_denominator)
/ (last_denominator * ratio_numerator_f * evals[i]);
// compute denom for the next step is current_denom * (len-i)/i
if i != 0 {
ratio_numerator *= -(len as i64 - i as i64);
ratio_denominator *= i as u64;
}
}
} else if p_i.len() <= 33 {
let last_denominator = F::from(u128_factorial(len - 1));
let mut ratio_numerator = 1i128;
let mut ratio_denominator = 1u128;
for i in (0..len).rev() {
let ratio_numerator_f = if ratio_numerator < 0 {
-F::from((-ratio_numerator) as u128)
} else {
F::from(ratio_numerator as u128)
};
res += p_i[i] * prod * F::from(ratio_denominator)
/ (last_denominator * ratio_numerator_f * evals[i]);
// compute denom for the next step is current_denom * (len-i)/i
if i != 0 {
ratio_numerator *= -(len as i128 - i as i128);
ratio_denominator *= i as u128;
}
}
} else {
let mut denom_up = field_factorial::(len - 1);
let mut denom_down = F::one();
for i in (0..len).rev() {
res += p_i[i] * prod * denom_down / (denom_up * evals[i]);
// compute denom for the next step is current_denom * (len-i)/i
if i != 0 {
denom_up *= -F::from((len - i) as u64);
denom_down *= F::from(i as u64);
}
}
}
end_timer!(start);
Ok(res)
}
/// compute the factorial(a) = 1 * 2 * ... * a
#[inline]
fn field_factorial(a: usize) -> F {
let mut res = F::one();
for i in 2..=a {
res *= F::from(i as u64);
}
res
}
/// compute the factorial(a) = 1 * 2 * ... * a
#[inline]
fn u128_factorial(a: usize) -> u128 {
let mut res = 1u128;
for i in 2..=a {
res *= i as u128;
}
res
}
/// compute the factorial(a) = 1 * 2 * ... * a
#[inline]
fn u64_factorial(a: usize) -> u64 {
let mut res = 1u64;
for i in 2..=a {
res *= i as u64;
}
res
}