use alloc::vec::Vec;
use core::marker::PhantomData;
use p3_challenger::fs::{
DomainSeparator, FieldToFieldCodec, FieldUnit, Hierarchy, Interaction, InteractionPattern,
Kind, Length, ProverState, TranscriptField, VerifierState,
};
use p3_challenger::{CanObserve, CanSample, GrindingChallenger};
use p3_field::ExtensionField;
use crate::error::SumcheckError;
use crate::strategy::Basis;
const VERSION: u8 = 1;
pub(crate) const NAME: &[u8] = b"p3-sumcheck-quadratic";
const ROUND_POLY: &str = "round_poly";
const ROUND_POW: &str = "round_pow";
const ROUND_CHALLENGE: &str = "round_challenge";
const VALUES_PER_ROUND: usize = 2;
type Alphabet<F> = FieldUnit<F>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SumcheckShape {
pub num_rounds: usize,
pub pow_bits: usize,
pub basis: Basis,
}
impl SumcheckShape {
#[must_use]
pub const fn new(num_rounds: usize, pow_bits: usize, basis: Basis) -> Self {
Self {
num_rounds,
pow_bits,
basis,
}
}
#[must_use]
pub fn pattern<F, EF>(&self) -> InteractionPattern
where
F: TranscriptField,
EF: ExtensionField<F>,
{
let values_per_round = match self.basis {
Basis::Evaluation => VALUES_PER_ROUND,
Basis::Projective => VALUES_PER_ROUND,
};
let mut steps = Vec::with_capacity(3 * self.num_rounds);
for _ in 0..self.num_rounds {
steps.push(Interaction::algebra::<F, EF>(
Hierarchy::Atomic,
Kind::Message,
ROUND_POLY,
Length::Fixed(values_per_round),
));
if self.pow_bits > 0 {
steps.push(Interaction::algebra::<F, F>(
Hierarchy::Atomic,
Kind::Pow,
ROUND_POW,
Length::Fixed(self.pow_bits),
));
}
steps.push(Interaction::algebra::<F, EF>(
Hierarchy::Atomic,
Kind::Challenge,
ROUND_CHALLENGE,
Length::Scalar,
));
}
InteractionPattern::new(steps).expect("a flat sequence of leaf steps is always well formed")
}
#[must_use]
pub fn domain_separator<F, EF>(&self) -> DomainSeparator<Alphabet<F>>
where
F: TranscriptField,
EF: ExtensionField<F>,
{
let mut separator = DomainSeparator::new(VERSION, NAME, self.pattern::<F, EF>());
separator.instance(&[match self.basis {
Basis::Evaluation => 0,
Basis::Projective => 1,
}]);
separator
}
}
pub struct ProverTranscript<'a, C, F: TranscriptField, EF> {
state: Option<ProverState<&'a mut C, Alphabet<F>>>,
shape: SumcheckShape,
_ef: PhantomData<EF>,
}
impl<'a, C, F, EF> ProverTranscript<'a, C, F, EF>
where
F: TranscriptField,
EF: ExtensionField<F>,
C: CanObserve<F> + CanSample<F> + GrindingChallenger<Witness = F>,
{
pub fn new(challenger: &'a mut C, shape: SumcheckShape) -> Self {
let state = (shape.num_rounds > 0).then(|| {
let separator = shape.domain_separator::<F, EF>();
ProverState::new(challenger, &separator)
});
Self {
state,
shape,
_ef: PhantomData,
}
}
pub fn round(&mut self, c_a: EF, c_inf: EF) -> (EF, Option<F>) {
let state = self
.state
.as_mut()
.expect("a batch described with no rounds has no round to play");
state.observe_extensions::<F, EF, FieldToFieldCodec<F>>(ROUND_POLY, &[c_a, c_inf]);
let witness =
(self.shape.pow_bits > 0).then(|| state.observe_pow(ROUND_POW, self.shape.pow_bits));
let challenge = state
.challenge_extension::<F, EF, FieldToFieldCodec<F>>(ROUND_CHALLENGE)
.into_inner();
(challenge, witness)
}
pub fn finish(self) {
if let Some(state) = self.state {
assert!(
state.finalize().is_empty(),
"the quadratic sumcheck carries every value in its own proof",
);
}
}
}
pub struct VerifierTranscript<'a, C, F: TranscriptField, EF> {
state: Option<VerifierState<'static, &'a mut C, Alphabet<F>>>,
shape: SumcheckShape,
round: usize,
_ef: PhantomData<EF>,
}
impl<'a, C, F, EF> VerifierTranscript<'a, C, F, EF>
where
F: TranscriptField,
EF: ExtensionField<F>,
C: CanObserve<F> + CanSample<F> + GrindingChallenger<Witness = F>,
{
pub fn new(challenger: &'a mut C, shape: SumcheckShape) -> Self {
let state = (shape.num_rounds > 0).then(|| {
let separator = shape.domain_separator::<F, EF>();
VerifierState::new(challenger, &separator, &[])
});
Self {
state,
shape,
round: 0,
_ef: PhantomData,
}
}
pub fn round(&mut self, c_a: EF, c_inf: EF, witness: Option<F>) -> Result<EF, SumcheckError> {
let round = self.round;
self.round += 1;
let state = self
.state
.as_mut()
.expect("a batch described with no rounds has no round to play");
state
.observe_extensions::<F, EF, FieldToFieldCodec<F>>(ROUND_POLY, &[c_a, c_inf])
.expect("a round always carries exactly the two values the step describes");
if self.shape.pow_bits > 0 {
let Some(witness) = witness else {
state.abort();
return Err(SumcheckError::MissingPowWitness {
round,
difficulty: self.shape.pow_bits,
});
};
state
.observe_pow(ROUND_POW, self.shape.pow_bits, witness)
.map_err(|_| SumcheckError::InvalidPowWitness {
round,
difficulty: self.shape.pow_bits,
})?;
}
Ok(state
.challenge_extension::<F, EF, FieldToFieldCodec<F>>(ROUND_CHALLENGE)
.into_inner())
}
pub fn finish(self) {
if let Some(state) = self.state {
state
.finalize()
.expect("the quadratic sumcheck reads an empty wire");
}
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use p3_baby_bear::{BabyBear, Poseidon2BabyBear};
use p3_challenger::testing::pow_difficulties;
use p3_challenger::{CanSample, DuplexChallenger};
use p3_field::PrimeCharacteristicRing;
use p3_field::extension::BinomialExtensionField;
use rand::SeedableRng;
use rand::rngs::SmallRng;
use super::*;
type F = BabyBear;
type EF = BinomialExtensionField<F, 4>;
type Perm = Poseidon2BabyBear<16>;
type Ch = DuplexChallenger<F, Perm, 16, 8>;
fn fresh_challenger() -> Ch {
let mut rng = SmallRng::seed_from_u64(0xDEADBEEF);
Ch::new(Perm::new_from_rng_128(&mut rng))
}
fn first_challenge(shape: SumcheckShape) -> F {
let mut challenger = fresh_challenger();
shape.domain_separator::<F, EF>().seed(&mut challenger);
challenger.sample()
}
#[test]
fn every_number_that_shapes_a_batch_reaches_the_seed() {
let base = SumcheckShape::new(4, 0, Basis::Evaluation);
assert_ne!(
first_challenge(base),
first_challenge(SumcheckShape::new(5, 0, Basis::Evaluation))
);
assert_ne!(
first_challenge(base),
first_challenge(SumcheckShape::new(4, 8, Basis::Evaluation))
);
assert_ne!(
first_challenge(SumcheckShape::new(4, 8, Basis::Evaluation)),
first_challenge(SumcheckShape::new(4, 9, Basis::Evaluation))
);
assert_ne!(
first_challenge(base),
first_challenge(SumcheckShape::new(4, 0, Basis::Projective))
);
}
#[test]
fn a_batch_of_no_rounds_leaves_the_sponge_untouched() {
let mut untouched = fresh_challenger();
let expected: F = untouched.sample();
let mut challenger = fresh_challenger();
let transcript = ProverTranscript::<Ch, F, EF>::new(
&mut challenger,
SumcheckShape::new(0, 0, Basis::Evaluation),
);
transcript.finish();
assert_eq!(CanSample::<F>::sample(&mut challenger), expected);
}
#[test]
fn a_round_missing_its_grinding_witness_is_rejected() {
let mut challenger = fresh_challenger();
let mut transcript = VerifierTranscript::<Ch, F, EF>::new(
&mut challenger,
SumcheckShape::new(2, 4, Basis::Evaluation),
);
let err = transcript
.round(EF::ONE, EF::ONE, None)
.expect_err("a described grinding step with no witness must error");
assert_eq!(
err,
SumcheckError::MissingPowWitness {
round: 0,
difficulty: 4
}
);
}
#[test]
fn a_grinding_witness_below_the_difficulty_is_rejected() {
let mut challenger = fresh_challenger();
let mut transcript = VerifierTranscript::<Ch, F, EF>::new(
&mut challenger,
SumcheckShape::new(1, 12, Basis::Evaluation),
);
let err = transcript
.round(EF::ONE, EF::ONE, Some(F::ZERO))
.expect_err("a witness below the required difficulty must error");
assert_eq!(
err,
SumcheckError::InvalidPowWitness {
round: 0,
difficulty: 12
}
);
}
fn replay(rounds: &[[EF; 2]]) -> Vec<EF> {
let mut challenger = fresh_challenger();
let shape = SumcheckShape::new(rounds.len(), 0, Basis::Evaluation);
let mut transcript = VerifierTranscript::<Ch, F, EF>::new(&mut challenger, shape);
let replayed = rounds
.iter()
.map(|&[c_a, c_inf]| transcript.round(c_a, c_inf, None).unwrap())
.collect();
transcript.finish();
replayed
}
#[test]
fn a_perturbed_finite_point_value_moves_every_later_challenge() {
let honest = [[EF::ONE, EF::TWO], [EF::TWO, EF::ONE]];
let mut tampered = honest;
tampered[0][0] = EF::from_u8(3);
assert_ne!(replay(&honest), replay(&tampered));
}
#[test]
fn a_perturbed_leading_coefficient_moves_every_later_challenge() {
let honest = [[EF::ONE, EF::TWO], [EF::TWO, EF::ONE]];
let mut tampered = honest;
tampered[0][1] = EF::from_u8(3);
assert_ne!(replay(&honest), replay(&tampered));
}
#[test]
fn a_recorded_grinding_witness_replays_to_the_same_challenge() {
let shape = SumcheckShape::new(1, 4, Basis::Evaluation);
let mut challenger = fresh_challenger();
let mut prover = ProverTranscript::<Ch, F, EF>::new(&mut challenger, shape);
let (challenge, witness) = prover.round(EF::ONE, EF::TWO);
prover.finish();
let witness = witness.expect("a positive difficulty always yields a witness");
let mut challenger = fresh_challenger();
let mut verifier = VerifierTranscript::<Ch, F, EF>::new(&mut challenger, shape);
let replayed = verifier.round(EF::ONE, EF::TWO, Some(witness)).unwrap();
verifier.finish();
assert_eq!(challenge, replayed);
}
#[test]
fn both_sides_of_a_round_draw_the_same_challenge() {
let shape = SumcheckShape::new(2, 0, Basis::Evaluation);
let mut prover_challenger = fresh_challenger();
let mut prover = ProverTranscript::<Ch, F, EF>::new(&mut prover_challenger, shape);
let (first, _) = prover.round(EF::ONE, EF::TWO);
let (second, _) = prover.round(EF::TWO, EF::ONE);
prover.finish();
let mut verifier_challenger = fresh_challenger();
let mut verifier = VerifierTranscript::<Ch, F, EF>::new(&mut verifier_challenger, shape);
let replayed_first = verifier.round(EF::ONE, EF::TWO, None).unwrap();
let replayed_second = verifier.round(EF::TWO, EF::ONE, None).unwrap();
verifier.finish();
assert_eq!(first, replayed_first);
assert_eq!(second, replayed_second);
assert_eq!(
CanSample::<F>::sample(&mut prover_challenger),
CanSample::<F>::sample(&mut verifier_challenger)
);
}
#[test]
fn the_described_grinding_matches_the_configured_difficulty() {
for bits in [0, 4] {
let shape = SumcheckShape::new(4, bits, Basis::Evaluation);
let described = pow_difficulties(&shape.pattern::<F, EF>());
let expected = if bits == 0 { 0 } else { shape.num_rounds };
assert_eq!(described.len(), expected);
assert!(
described
.iter()
.all(|&(label, got)| label == ROUND_POW && got == bits)
);
}
}
}