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 super::error::GenericDegreeError;
const VERSION: u8 = 1;
pub(crate) const NAME: &[u8] = b"p3-sumcheck-generic-degree";
const CLAIMED_SUM: &str = "claimed_sum";
const ROUND_POLY: &str = "round_poly";
const ROUND_POW: &str = "round_pow";
const ROUND_CHALLENGE: &str = "round_challenge";
type Alphabet<F> = FieldUnit<F>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GenericDegreeShape {
pub num_rounds: usize,
pub degree: usize,
pub pow_bits: usize,
}
impl GenericDegreeShape {
#[must_use]
pub const fn new(num_rounds: usize, degree: usize, pow_bits: usize) -> Self {
Self {
num_rounds,
degree,
pow_bits,
}
}
#[must_use]
pub fn pattern<F, EF>(&self) -> InteractionPattern
where
F: TranscriptField,
EF: ExtensionField<F>,
{
let mut steps = Vec::with_capacity(1 + 3 * self.num_rounds);
steps.push(Interaction::algebra::<F, EF>(
Hierarchy::Atomic,
Kind::Message,
CLAIMED_SUM,
Length::Scalar,
));
for _ in 0..self.num_rounds {
steps.push(Interaction::algebra::<F, EF>(
Hierarchy::Atomic,
Kind::Message,
ROUND_POLY,
Length::Fixed(self.degree),
));
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>,
{
DomainSeparator::new(VERSION, NAME, self.pattern::<F, EF>())
}
}
pub struct ProverTranscript<'a, C, F: TranscriptField, EF> {
state: ProverState<&'a mut C, Alphabet<F>>,
shape: GenericDegreeShape,
_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: GenericDegreeShape, claimed_sum: EF) -> Self {
let separator = shape.domain_separator::<F, EF>();
let mut state = ProverState::new(challenger, &separator);
state.observe_extension::<F, EF, FieldToFieldCodec<F>>(CLAIMED_SUM, &claimed_sum);
Self {
state,
shape,
_ef: PhantomData,
}
}
pub fn round(&mut self, evals: &[EF]) -> (EF, Option<F>) {
self.state
.observe_extensions::<F, EF, FieldToFieldCodec<F>>(ROUND_POLY, evals);
let witness = (self.shape.pow_bits > 0)
.then(|| self.state.observe_pow(ROUND_POW, self.shape.pow_bits));
let challenge = self
.state
.challenge_extension::<F, EF, FieldToFieldCodec<F>>(ROUND_CHALLENGE)
.into_inner();
(challenge, witness)
}
pub fn finish(self) {
assert!(
self.state.finalize().is_empty(),
"the generic-degree sumcheck carries every value in its own proof",
);
}
}
pub struct VerifierTranscript<'a, C, F: TranscriptField, EF> {
state: VerifierState<'static, &'a mut C, Alphabet<F>>,
shape: GenericDegreeShape,
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: GenericDegreeShape, claimed_sum: EF) -> Self {
let separator = shape.domain_separator::<F, EF>();
let mut state = VerifierState::new(challenger, &separator, &[]);
state.observe_extension::<F, EF, FieldToFieldCodec<F>>(CLAIMED_SUM, &claimed_sum);
Self {
state,
shape,
round: 0,
_ef: PhantomData,
}
}
pub fn round(&mut self, evals: &[EF], witness: Option<F>) -> Result<EF, GenericDegreeError> {
let round = self.round;
self.round += 1;
self.state
.observe_extensions::<F, EF, FieldToFieldCodec<F>>(ROUND_POLY, evals)
.map_err(|_| GenericDegreeError::PolyEvalCountMismatch {
round,
expected: self.shape.degree,
actual: evals.len(),
})?;
if self.shape.pow_bits > 0 {
let Some(witness) = witness else {
self.state.abort();
return Err(GenericDegreeError::MissingPowWitness { round });
};
self.state
.observe_pow(ROUND_POW, self.shape.pow_bits, witness)
.map_err(|_| GenericDegreeError::InvalidPowWitness { round })?;
}
Ok(self
.state
.challenge_extension::<F, EF, FieldToFieldCodec<F>>(ROUND_CHALLENGE)
.into_inner())
}
pub fn finish(self) {
self.state
.finalize()
.expect("the generic-degree sumcheck reads an empty wire");
}
}
#[cfg(test)]
mod tests {
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);
let perm = Perm::new_from_rng_128(&mut rng);
Ch::new(perm)
}
fn first_challenge(shape: GenericDegreeShape) -> F {
let mut challenger = fresh_challenger();
shape.domain_separator::<F, EF>().seed(&mut challenger);
challenger.sample()
}
#[test]
fn every_number_that_shapes_a_run_reaches_the_seed() {
let base = first_challenge(GenericDegreeShape::new(4, 3, 0));
assert_ne!(base, first_challenge(GenericDegreeShape::new(5, 3, 0)));
assert_ne!(base, first_challenge(GenericDegreeShape::new(4, 4, 0)));
assert_ne!(base, first_challenge(GenericDegreeShape::new(4, 3, 8)));
assert_ne!(
first_challenge(GenericDegreeShape::new(4, 3, 8)),
first_challenge(GenericDegreeShape::new(4, 3, 9)),
);
}
#[test]
fn a_round_polynomial_of_the_wrong_width_is_rejected() {
let mut challenger = fresh_challenger();
let shape = GenericDegreeShape::new(2, 3, 0);
let mut transcript = VerifierTranscript::<Ch, F, EF>::new(&mut challenger, shape, EF::ZERO);
let err = transcript
.round(&[EF::ONE, EF::ONE], None)
.expect_err("a round polynomial outside the described width must error");
assert_eq!(
err,
GenericDegreeError::PolyEvalCountMismatch {
round: 0,
expected: 3,
actual: 2,
}
);
drop(transcript);
}
#[test]
fn a_round_missing_its_grinding_witness_is_rejected() {
let mut challenger = fresh_challenger();
let shape = GenericDegreeShape::new(2, 3, 4);
let mut transcript = VerifierTranscript::<Ch, F, EF>::new(&mut challenger, shape, EF::ZERO);
let err = transcript
.round(&[EF::ONE; 3], None)
.expect_err("a described grinding step with no witness must error");
assert_eq!(err, GenericDegreeError::MissingPowWitness { round: 0 });
}
#[test]
fn the_described_grinding_matches_the_configured_difficulty() {
for bits in [0, 4] {
let shape = GenericDegreeShape::new(4, 3, bits);
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)
);
}
}
}