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::opening::OPENING_DEGREE;
use crate::generic_degree::GenericDegreeShape;
const VERSION: u8 = 1;
pub(crate) const NAME: &[u8] = b"p3-sumcheck-univariate-skip";
const ROUND_MESSAGE: &str = "round_message";
const ROUND_POW: &str = "round_pow";
const SKIP_CHALLENGE: &str = "skip_challenge";
const RESIDUAL_SUMCHECK: &str = "residual_sumcheck";
struct ResidualSumcheck;
const OPENING_NAME: &[u8] = b"p3-sumcheck-univariate-skip-opening";
const OPENING_CLAIMS: &str = "opening_claims";
const OPENING_BATCHING: &str = "opening_batching";
const OPENING_SUMCHECK: &str = "opening_sumcheck";
struct OpeningSumcheck;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SkipOpeningShape {
pub num_variables: usize,
pub num_polynomials: usize,
pub pow_bits: usize,
}
impl SkipOpeningShape {
#[must_use]
pub const fn new(num_variables: usize, num_polynomials: usize, pow_bits: usize) -> Self {
assert!(
num_polynomials > 0,
"an opening reduction batches at least one polynomial"
);
Self {
num_variables,
num_polynomials,
pow_bits,
}
}
#[must_use]
pub const fn sumcheck_shape(&self) -> GenericDegreeShape {
GenericDegreeShape::new(self.num_variables, OPENING_DEGREE, self.pow_bits)
}
#[must_use]
pub fn pattern<F, EF>(&self) -> InteractionPattern
where
F: TranscriptField,
EF: ExtensionField<F>,
{
let mut steps = Vec::with_capacity(4);
steps.push(Interaction::algebra::<F, EF>(
Hierarchy::Atomic,
Kind::Message,
OPENING_CLAIMS,
Length::Fixed(self.num_polynomials),
));
if self.num_polynomials > 1 {
steps.push(Interaction::algebra::<F, EF>(
Hierarchy::Atomic,
Kind::Challenge,
OPENING_BATCHING,
Length::Scalar,
));
}
steps.push(Interaction::marker::<OpeningSumcheck>(
Hierarchy::Begin,
Kind::Protocol,
OPENING_SUMCHECK,
));
steps.push(Interaction::marker::<OpeningSumcheck>(
Hierarchy::End,
Kind::Protocol,
OPENING_SUMCHECK,
));
InteractionPattern::new(steps).expect("one matched bracket 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, OPENING_NAME, self.pattern::<F, EF>())
}
}
pub struct SkipOpeningProverTranscript<'a, C, F: TranscriptField, EF> {
state: ProverState<&'a mut C, Alphabet<F>>,
shape: SkipOpeningShape,
_ef: PhantomData<EF>,
}
impl<'a, C, F, EF> SkipOpeningProverTranscript<'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: SkipOpeningShape) -> Self {
let separator = shape.domain_separator::<F, EF>();
let state = ProverState::new(challenger, &separator);
Self {
state,
shape,
_ef: PhantomData,
}
}
pub fn batching_challenge(&mut self, claims: &[EF]) -> EF {
assert_eq!(
claims.len(),
self.shape.num_polynomials,
"one claim per committed polynomial"
);
self.state
.observe_extensions::<F, EF, FieldToFieldCodec<F>>(OPENING_CLAIMS, claims);
if self.shape.num_polynomials <= 1 {
return EF::ONE;
}
self.state
.challenge_extension::<F, EF, FieldToFieldCodec<F>>(OPENING_BATCHING)
.into_inner()
}
pub fn sumcheck<R>(&mut self, run: impl FnOnce(&mut C) -> R) -> R {
self.state
.begin_protocol::<OpeningSumcheck>(OPENING_SUMCHECK);
let output = run(self.state.challenger_mut());
self.state.end_protocol::<OpeningSumcheck>(OPENING_SUMCHECK);
output
}
pub fn finish(self) {
assert!(
self.state.finalize().is_empty(),
"the opening reduction carries every value in its own proof",
);
}
}
pub struct SkipOpeningVerifierTranscript<'a, C, F: TranscriptField, EF> {
state: VerifierState<'static, &'a mut C, Alphabet<F>>,
shape: SkipOpeningShape,
_ef: PhantomData<EF>,
}
impl<'a, C, F, EF> SkipOpeningVerifierTranscript<'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: SkipOpeningShape) -> Self {
let separator = shape.domain_separator::<F, EF>();
let state = VerifierState::new(challenger, &separator, &[]);
Self {
state,
shape,
_ef: PhantomData,
}
}
pub fn batching_challenge(&mut self, claims: &[EF]) -> Result<EF, SkipOpeningTranscriptError> {
self.state
.observe_extensions::<F, EF, FieldToFieldCodec<F>>(OPENING_CLAIMS, claims)
.map_err(|_| {
self.state.abort();
SkipOpeningTranscriptError::ClaimCountMismatch {
expected: self.shape.num_polynomials,
actual: claims.len(),
}
})?;
if self.shape.num_polynomials <= 1 {
return Ok(EF::ONE);
}
Ok(self
.state
.challenge_extension::<F, EF, FieldToFieldCodec<F>>(OPENING_BATCHING)
.into_inner())
}
pub fn sumcheck<T, E>(&mut self, run: impl FnOnce(&mut C) -> Result<T, E>) -> Result<T, E> {
self.state
.begin_protocol::<OpeningSumcheck>(OPENING_SUMCHECK);
let output = run(self.state.challenger_mut());
if output.is_err() {
self.state.abort();
return output;
}
self.state.end_protocol::<OpeningSumcheck>(OPENING_SUMCHECK);
output
}
pub fn abort(&mut self) {
self.state.abort();
}
pub fn finish(self) {
self.state
.finalize()
.expect("the opening reduction reads an empty wire");
}
}
type Alphabet<F> = FieldUnit<F>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct UnivariateSkipShape {
pub log_size: usize,
pub log_extended: usize,
pub num_residual_rounds: usize,
pub residual_degree: usize,
pub pow_bits: usize,
}
impl UnivariateSkipShape {
#[must_use]
pub const fn new(
log_size: usize,
log_extended: usize,
num_residual_rounds: usize,
residual_degree: usize,
pow_bits: usize,
) -> Self {
Self {
log_size,
log_extended,
num_residual_rounds,
residual_degree,
pow_bits,
}
}
#[must_use]
pub const fn message_len(&self) -> usize {
(1 << self.log_extended) - (1 << self.log_size)
}
#[must_use]
pub const fn residual_shape(&self) -> GenericDegreeShape {
GenericDegreeShape::new(
self.num_residual_rounds,
self.residual_degree,
self.pow_bits,
)
}
#[must_use]
pub fn pattern<F, EF>(&self) -> InteractionPattern
where
F: TranscriptField,
EF: ExtensionField<F>,
{
let mut steps = Vec::with_capacity(5);
steps.push(Interaction::algebra::<F, EF>(
Hierarchy::Atomic,
Kind::Message,
ROUND_MESSAGE,
Length::Fixed(self.message_len()),
));
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,
SKIP_CHALLENGE,
Length::Scalar,
));
steps.push(Interaction::marker::<ResidualSumcheck>(
Hierarchy::Begin,
Kind::Protocol,
RESIDUAL_SUMCHECK,
));
steps.push(Interaction::marker::<ResidualSumcheck>(
Hierarchy::End,
Kind::Protocol,
RESIDUAL_SUMCHECK,
));
InteractionPattern::new(steps).expect("one matched bracket 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 UnivariateSkipProverTranscript<'a, C, F: TranscriptField, EF> {
state: ProverState<&'a mut C, Alphabet<F>>,
shape: UnivariateSkipShape,
_ef: PhantomData<EF>,
}
impl<'a, C, F, EF> UnivariateSkipProverTranscript<'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: UnivariateSkipShape) -> Self {
let separator = shape.domain_separator::<F, EF>();
let state = ProverState::new(challenger, &separator);
Self {
state,
shape,
_ef: PhantomData,
}
}
pub fn round_message(&mut self, message: &[EF]) -> (EF, Option<F>) {
self.state
.observe_extensions::<F, EF, FieldToFieldCodec<F>>(ROUND_MESSAGE, message);
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>>(SKIP_CHALLENGE)
.into_inner();
(challenge, witness)
}
pub fn residual_sumcheck<R>(&mut self, run: impl FnOnce(&mut C) -> R) -> R {
self.state
.begin_protocol::<ResidualSumcheck>(RESIDUAL_SUMCHECK);
let output = run(self.state.challenger_mut());
self.state
.end_protocol::<ResidualSumcheck>(RESIDUAL_SUMCHECK);
output
}
pub fn finish(self) {
assert!(
self.state.finalize().is_empty(),
"the univariate-skip reduction carries every value in its own proof",
);
}
}
pub struct UnivariateSkipVerifierTranscript<'a, C, F: TranscriptField, EF> {
state: VerifierState<'static, &'a mut C, Alphabet<F>>,
shape: UnivariateSkipShape,
_ef: PhantomData<EF>,
}
impl<'a, C, F, EF> UnivariateSkipVerifierTranscript<'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: UnivariateSkipShape) -> Self {
let separator = shape.domain_separator::<F, EF>();
let state = VerifierState::new(challenger, &separator, &[]);
Self {
state,
shape,
_ef: PhantomData,
}
}
pub fn round_message(
&mut self,
message: &[EF],
witness: Option<F>,
) -> Result<EF, UnivariateSkipTranscriptError> {
self.state
.observe_extensions::<F, EF, FieldToFieldCodec<F>>(ROUND_MESSAGE, message)
.map_err(|_| UnivariateSkipTranscriptError::MessageLenMismatch {
expected: self.shape.message_len(),
actual: message.len(),
})?;
if self.shape.pow_bits > 0 {
let Some(witness) = witness else {
self.state.abort();
return Err(UnivariateSkipTranscriptError::MissingPowWitness);
};
self.state
.observe_pow(ROUND_POW, self.shape.pow_bits, witness)
.map_err(|_| UnivariateSkipTranscriptError::InvalidPowWitness)?;
} else if witness.is_some() {
self.state.abort();
return Err(UnivariateSkipTranscriptError::UnexpectedPowWitness);
}
Ok(self
.state
.challenge_extension::<F, EF, FieldToFieldCodec<F>>(SKIP_CHALLENGE)
.into_inner())
}
pub fn residual_sumcheck<T, E>(
&mut self,
run: impl FnOnce(&mut C) -> Result<T, E>,
) -> Result<T, E> {
self.state
.begin_protocol::<ResidualSumcheck>(RESIDUAL_SUMCHECK);
let output = run(self.state.challenger_mut());
if output.is_err() {
self.state.abort();
return output;
}
self.state
.end_protocol::<ResidualSumcheck>(RESIDUAL_SUMCHECK);
output
}
pub fn abort(&mut self) {
self.state.abort();
}
pub fn finish(self) {
self.state
.finalize()
.expect("the univariate-skip reduction reads an empty wire");
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum SkipOpeningTranscriptError {
#[error("opening claim count mismatch: expected {expected}, got {actual}")]
ClaimCountMismatch {
expected: usize,
actual: usize,
},
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum UnivariateSkipTranscriptError {
#[error("round message length mismatch: expected {expected}, got {actual}")]
MessageLenMismatch {
expected: usize,
actual: usize,
},
#[error("the round carries no grinding witness")]
MissingPowWitness,
#[error("the round carries a grinding witness at zero difficulty")]
UnexpectedPowWitness,
#[error("the round's grinding witness is invalid")]
InvalidPowWitness,
}
#[cfg(test)]
mod tests {
use p3_binary_field::{BinaryChallenger, BinaryField128};
use p3_challenger::{CanSample, HashChallenger};
use p3_field::PrimeCharacteristicRing;
use p3_keccak::Keccak256Hash;
use super::*;
type F = BinaryField128;
type Ch = BinaryChallenger<F, HashChallenger<u8, Keccak256Hash, 32>>;
fn fresh_challenger() -> Ch {
Ch::from_hasher(Vec::new(), Keccak256Hash)
}
fn first_challenge(shape: UnivariateSkipShape) -> F {
let mut challenger = fresh_challenger();
shape.domain_separator::<F, F>().seed(&mut challenger);
challenger.sample()
}
#[test]
fn the_numbers_shaping_this_run_reach_its_own_seed() {
let base = first_challenge(UnivariateSkipShape::new(6, 7, 4, 3, 0));
assert_ne!(
base,
first_challenge(UnivariateSkipShape::new(6, 8, 4, 3, 0))
);
assert_ne!(
base,
first_challenge(UnivariateSkipShape::new(5, 7, 4, 3, 0))
);
assert_ne!(
base,
first_challenge(UnivariateSkipShape::new(6, 7, 4, 3, 8))
);
assert_ne!(
first_challenge(UnivariateSkipShape::new(6, 7, 4, 3, 8)),
first_challenge(UnivariateSkipShape::new(6, 7, 4, 3, 9)),
);
}
#[test]
fn the_residual_numbers_are_bound_by_the_nested_seed() {
let base = UnivariateSkipShape::new(6, 7, 4, 3, 0);
assert_eq!(
first_challenge(base),
first_challenge(UnivariateSkipShape::new(6, 7, 5, 4, 0))
);
let nested = |shape: UnivariateSkipShape| {
let mut challenger = fresh_challenger();
shape
.residual_shape()
.domain_separator::<F, F>()
.seed(&mut challenger);
CanSample::<F>::sample(&mut challenger)
};
assert_ne!(
nested(base),
nested(UnivariateSkipShape::new(6, 7, 5, 3, 0))
);
assert_ne!(
nested(base),
nested(UnivariateSkipShape::new(6, 7, 4, 4, 0))
);
}
#[test]
fn the_skip_transcript_does_not_collide_with_the_plain_sumcheck() {
let mut skip = fresh_challenger();
UnivariateSkipShape::new(6, 7, 4, 3, 0)
.domain_separator::<F, F>()
.seed(&mut skip);
let mut plain = fresh_challenger();
crate::generic_degree::GenericDegreeShape::new(4, 3, 0)
.domain_separator::<F, F>()
.seed(&mut plain);
assert_ne!(
CanSample::<F>::sample(&mut skip),
CanSample::<F>::sample(&mut plain)
);
}
#[test]
fn a_message_of_the_wrong_width_is_rejected() {
let mut challenger = fresh_challenger();
let shape = UnivariateSkipShape::new(6, 7, 4, 3, 0);
let mut transcript =
UnivariateSkipVerifierTranscript::<Ch, F, F>::new(&mut challenger, shape);
let err = transcript
.round_message(&[F::ONE, F::ONE], None)
.expect_err("a message outside the described width must error");
assert_eq!(
err,
UnivariateSkipTranscriptError::MessageLenMismatch {
expected: 64,
actual: 2,
}
);
drop(transcript);
}
#[test]
fn a_round_missing_its_grinding_witness_is_rejected() {
let mut challenger = fresh_challenger();
let shape = UnivariateSkipShape::new(3, 4, 2, 3, 4);
let mut transcript =
UnivariateSkipVerifierTranscript::<Ch, F, F>::new(&mut challenger, shape);
let err = transcript
.round_message(&[F::ONE; 8], None)
.expect_err("a described grinding step with no witness must error");
assert_eq!(err, UnivariateSkipTranscriptError::MissingPowWitness);
}
#[test]
fn the_message_width_follows_the_two_dimensions() {
assert_eq!(UnivariateSkipShape::new(6, 7, 4, 3, 0).message_len(), 64);
assert_eq!(UnivariateSkipShape::new(6, 8, 4, 3, 0).message_len(), 192);
}
}