use alloc::vec;
use core::marker::PhantomData;
use p3_challenger::fs::{
DomainSeparator, FieldToFieldCodec, FieldUnit, Hierarchy, Interaction, InteractionPattern,
Kind, Length, ProverState, TranscriptBound, TranscriptField, VerifierState,
};
use p3_challenger::{CanObserve, CanSample};
use p3_field::{ExtensionField, Field};
use p3_multilinear_util::point::Point;
use super::RingSwitchError;
use super::packing::packed_vars;
use super::tensor::TensorAlgebra;
const VERSION: u8 = 1;
pub(crate) const NAME: &[u8] = b"p3-sumcheck-ring-switch";
const EVALUATION_POINT: &str = "evaluation_point";
const TENSOR_ELEMENT: &str = "tensor_element";
const BATCHING_POINT: &str = "batching_point";
const BATCHED_SUMCHECK: &str = "batched_sumcheck";
const SURVIVING_CLAIM: &str = "surviving_claim";
type Alphabet<F> = FieldUnit<F>;
struct BatchedSumcheck;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RingSwitchShape {
pub num_variables: usize,
}
impl RingSwitchShape {
#[must_use]
pub const fn new(num_variables: usize) -> Self {
Self { num_variables }
}
#[must_use]
pub fn sumcheck_rounds<F, EF>(&self) -> usize
where
F: Field,
EF: ExtensionField<F>,
{
let kappa = packed_vars::<F, EF>();
assert!(
self.num_variables >= kappa,
"the evaluation point must name at least the {kappa} packed variables, got {}",
self.num_variables
);
self.num_variables - kappa
}
#[must_use]
pub const fn tensor_coefficients<F, EF>() -> usize
where
F: Field,
EF: ExtensionField<F>,
{
let dimension = TensorAlgebra::<F, EF>::DIMENSION;
dimension * dimension
}
#[must_use]
pub fn pattern<F, EF>(&self) -> InteractionPattern
where
F: TranscriptField,
EF: ExtensionField<F>,
{
let steps = vec![
Interaction::algebra::<F, EF>(
Hierarchy::Atomic,
Kind::Message,
EVALUATION_POINT,
Length::Fixed(self.num_variables),
),
Interaction::algebra::<F, F>(
Hierarchy::Atomic,
Kind::Message,
TENSOR_ELEMENT,
Length::Fixed(Self::tensor_coefficients::<F, EF>()),
),
Interaction::algebra::<F, EF>(
Hierarchy::Atomic,
Kind::Challenge,
BATCHING_POINT,
Length::Fixed(packed_vars::<F, EF>()),
),
Interaction::marker::<BatchedSumcheck>(
Hierarchy::Begin,
Kind::Protocol,
BATCHED_SUMCHECK,
),
Interaction::marker::<BatchedSumcheck>(
Hierarchy::End,
Kind::Protocol,
BATCHED_SUMCHECK,
),
Interaction::algebra::<F, EF>(
Hierarchy::Atomic,
Kind::Message,
SURVIVING_CLAIM,
Length::Scalar,
),
];
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 RingSwitchProverTranscript<'a, C, F: TranscriptField, EF> {
state: ProverState<&'a mut C, Alphabet<F>>,
_ef: PhantomData<EF>,
}
impl<'a, C, F, EF> RingSwitchProverTranscript<'a, C, F, EF>
where
F: TranscriptField,
EF: ExtensionField<F>,
C: CanObserve<F> + CanSample<F>,
{
pub fn new(challenger: &'a mut C, shape: RingSwitchShape) -> Self {
let separator = shape.domain_separator::<F, EF>();
Self {
state: ProverState::new(challenger, &separator),
_ef: PhantomData,
}
}
pub fn statement(&mut self, point: &Point<EF>, tensor_coefficients: &[F]) -> Point<EF> {
self.state
.observe_extensions::<F, EF, FieldToFieldCodec<F>>(EVALUATION_POINT, point.as_slice());
self.state
.observe_extensions::<F, F, FieldToFieldCodec<F>>(TENSOR_ELEMENT, tensor_coefficients);
Point::new(
self.state
.challenge_extensions::<F, EF, FieldToFieldCodec<F>>(
BATCHING_POINT,
packed_vars::<F, EF>(),
)
.into_iter()
.map(TranscriptBound::into_inner)
.collect(),
)
}
pub fn batched_sumcheck<R>(&mut self, run: impl FnOnce(&mut C) -> R) -> R {
self.state
.begin_protocol::<BatchedSumcheck>(BATCHED_SUMCHECK);
let output = run(self.state.challenger_mut());
self.state.end_protocol::<BatchedSumcheck>(BATCHED_SUMCHECK);
output
}
pub fn surviving_claim(&mut self, value: EF) {
self.state
.observe_extension::<F, EF, FieldToFieldCodec<F>>(SURVIVING_CLAIM, &value);
}
pub fn finish(self) {
assert!(
self.state.finalize().is_empty(),
"the ring-switching reduction carries every value in its own proof",
);
}
}
pub struct RingSwitchVerifierTranscript<'a, C, F: TranscriptField, EF> {
state: VerifierState<'static, &'a mut C, Alphabet<F>>,
shape: RingSwitchShape,
_ef: PhantomData<EF>,
}
impl<'a, C, F, EF> RingSwitchVerifierTranscript<'a, C, F, EF>
where
F: TranscriptField,
EF: ExtensionField<F>,
C: CanObserve<F> + CanSample<F>,
{
pub fn new(challenger: &'a mut C, shape: RingSwitchShape) -> Self {
let separator = shape.domain_separator::<F, EF>();
Self {
state: VerifierState::new(challenger, &separator, &[]),
shape,
_ef: PhantomData,
}
}
pub fn statement(
&mut self,
point: &Point<EF>,
tensor_coefficients: &[F],
) -> Result<Point<EF>, RingSwitchError> {
self.state
.observe_extensions::<F, EF, FieldToFieldCodec<F>>(EVALUATION_POINT, point.as_slice())
.map_err(|_| RingSwitchError::PointWidthMismatch {
expected: self.shape.num_variables,
actual: point.num_variables(),
})?;
self.state
.observe_extensions::<F, F, FieldToFieldCodec<F>>(TENSOR_ELEMENT, tensor_coefficients)
.map_err(|_| RingSwitchError::MalformedTensor {
expected: RingSwitchShape::tensor_coefficients::<F, EF>(),
actual: tensor_coefficients.len(),
})?;
Ok(Point::new(
self.state
.challenge_extensions::<F, EF, FieldToFieldCodec<F>>(
BATCHING_POINT,
packed_vars::<F, EF>(),
)
.into_iter()
.map(TranscriptBound::into_inner)
.collect(),
))
}
pub fn batched_sumcheck<R>(&mut self, run: impl FnOnce(&mut C) -> R) -> R {
self.state
.begin_protocol::<BatchedSumcheck>(BATCHED_SUMCHECK);
let output = run(self.state.challenger_mut());
self.state.end_protocol::<BatchedSumcheck>(BATCHED_SUMCHECK);
output
}
pub fn surviving_claim(&mut self, value: EF) {
self.state
.observe_extension::<F, EF, FieldToFieldCodec<F>>(SURVIVING_CLAIM, &value);
}
pub fn abort(&mut self) {
self.state.abort();
}
pub fn finish(self) {
self.state
.finalize()
.expect("the ring-switching reduction reads an empty wire");
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use alloc::vec::Vec;
use p3_baby_bear::{BabyBear, Poseidon2BabyBear};
use p3_challenger::DuplexChallenger;
use p3_challenger::testing::{
SeedDigest, assert_seeds_pairwise_distinct, pow_difficulties, seed_digest,
};
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>;
const NUM_VARIABLES: usize = 6;
const TENSOR_WIDTH: usize = 16;
fn fresh_challenger() -> Ch {
let mut rng = SmallRng::seed_from_u64(0x1E56);
Ch::new(Perm::new_from_rng_128(&mut rng))
}
const fn base_shape() -> RingSwitchShape {
RingSwitchShape::new(NUM_VARIABLES)
}
fn seed_of(shape: RingSwitchShape) -> SeedDigest {
seed_digest(&shape.domain_separator::<F, EF>())
}
fn one_step_from_base() -> Vec<(&'static str, RingSwitchShape)> {
let RingSwitchShape { num_variables: _ } = base_shape();
let mut wider = base_shape();
wider.num_variables += 1;
vec![("num_variables", wider)]
}
#[test]
fn the_same_shape_seeds_the_same_stream_twice() {
assert_eq!(seed_of(base_shape()), seed_of(base_shape()));
}
#[test]
fn no_two_configurations_of_the_shape_share_a_seed() {
let mut seeds = vec![("base", seed_of(base_shape()))];
seeds.extend(
one_step_from_base()
.into_iter()
.map(|(field, shape)| (field, seed_of(shape))),
);
assert_seeds_pairwise_distinct(&seeds);
}
#[test]
fn the_description_asks_for_no_grinding() {
assert!(pow_difficulties(&base_shape().pattern::<F, EF>()).is_empty());
}
#[test]
fn the_two_derived_widths_come_from_the_extension_degree() {
assert_eq!(packed_vars::<F, EF>(), 2);
assert_eq!(
RingSwitchShape::tensor_coefficients::<F, EF>(),
TENSOR_WIDTH
);
assert_eq!(base_shape().sumcheck_rounds::<F, EF>(), 4);
}
fn point_of(num_variables: usize, seed: u32) -> Point<EF> {
Point::new(
(0..num_variables)
.map(|i| EF::from_u32(seed * 31 + i as u32 + 1))
.collect(),
)
}
fn coefficients_of(seed: u32) -> Vec<F> {
(0..TENSOR_WIDTH)
.map(|i| F::from_u32(seed * 17 + i as u32 + 1))
.collect()
}
type Draws = (Point<EF>, F);
fn drive_prover(
challenger: &mut Ch,
shape: RingSwitchShape,
point: &Point<EF>,
coefficients: &[F],
claim: EF,
) -> Draws {
let mut transcript = RingSwitchProverTranscript::<Ch, F, EF>::new(challenger, shape);
let batching_point = transcript.statement(point, coefficients);
let delegated = transcript.batched_sumcheck(<Ch as CanSample<F>>::sample);
transcript.surviving_claim(claim);
transcript.finish();
(batching_point, delegated)
}
fn drive_verifier(
challenger: &mut Ch,
shape: RingSwitchShape,
point: &Point<EF>,
coefficients: &[F],
claim: EF,
) -> Result<Draws, RingSwitchError> {
let mut transcript = RingSwitchVerifierTranscript::<Ch, F, EF>::new(challenger, shape);
let batching_point = transcript.statement(point, coefficients)?;
let delegated = transcript.batched_sumcheck(<Ch as CanSample<F>>::sample);
transcript.surviving_claim(claim);
transcript.finish();
Ok((batching_point, delegated))
}
#[test]
fn both_sides_of_a_run_draw_the_same_stream() {
let point = point_of(NUM_VARIABLES, 3);
let coefficients = coefficients_of(5);
let mut prover_challenger = fresh_challenger();
let proved = drive_prover(
&mut prover_challenger,
base_shape(),
&point,
&coefficients,
EF::ONE,
);
let mut verifier_challenger = fresh_challenger();
let replayed = drive_verifier(
&mut verifier_challenger,
base_shape(),
&point,
&coefficients,
EF::ONE,
)
.expect("the honest run must replay");
assert_eq!(proved, replayed);
assert_eq!(
CanSample::<F>::sample(&mut prover_challenger),
CanSample::<F>::sample(&mut verifier_challenger),
);
}
fn replay(point: &Point<EF>, coefficients: &[F], claim: EF) -> (Draws, F) {
let mut challenger = fresh_challenger();
let draws = drive_verifier(
&mut challenger,
RingSwitchShape::new(point.num_variables()),
point,
coefficients,
claim,
)
.expect("a well-shaped run always replays");
(draws, CanSample::<F>::sample(&mut challenger))
}
#[test]
fn a_perturbed_point_coordinate_moves_every_later_draw() {
let honest = point_of(NUM_VARIABLES, 3);
let coefficients = coefficients_of(5);
let baseline = replay(&honest, &coefficients, EF::ONE);
for position in 0..NUM_VARIABLES {
let mut moved = honest.as_slice().to_vec();
moved[position] += EF::ONE;
assert_ne!(
baseline,
replay(&Point::new(moved), &coefficients, EF::ONE),
"tampering with coordinate {position} left the stream where it was",
);
}
}
#[test]
fn a_perturbed_tensor_coefficient_moves_every_later_draw() {
let point = point_of(NUM_VARIABLES, 3);
let honest = coefficients_of(5);
let baseline = replay(&point, &honest, EF::ONE);
for position in [0, TENSOR_WIDTH / 2, TENSOR_WIDTH - 1] {
let mut tampered = honest.clone();
tampered[position] += F::ONE;
assert_ne!(
baseline,
replay(&point, &tampered, EF::ONE),
"tampering with coefficient {position} left the stream where it was",
);
}
}
#[test]
fn a_perturbed_surviving_claim_moves_the_state_the_sponge_is_handed_back_in() {
let point = point_of(NUM_VARIABLES, 3);
let coefficients = coefficients_of(5);
assert_ne!(
replay(&point, &coefficients, EF::ONE),
replay(&point, &coefficients, EF::TWO),
);
}
#[test]
fn a_tensor_element_of_the_wrong_width_is_rejected() {
let mut challenger = fresh_challenger();
let mut transcript =
RingSwitchVerifierTranscript::<Ch, F, EF>::new(&mut challenger, base_shape());
let err = transcript
.statement(&point_of(NUM_VARIABLES, 3), &[F::ONE])
.expect_err("a tensor element outside the described width must error");
assert_eq!(
err,
RingSwitchError::MalformedTensor {
expected: TENSOR_WIDTH,
actual: 1,
},
);
drop(transcript);
}
#[test]
fn an_evaluation_point_of_the_wrong_width_is_rejected() {
let mut challenger = fresh_challenger();
let mut transcript =
RingSwitchVerifierTranscript::<Ch, F, EF>::new(&mut challenger, base_shape());
let err = transcript
.statement(&point_of(3, 3), &coefficients_of(5))
.expect_err("a point outside the described width must error");
assert_eq!(
err,
RingSwitchError::PointWidthMismatch {
expected: NUM_VARIABLES,
actual: 3,
},
);
drop(transcript);
}
#[test]
#[should_panic(expected = "the evaluation point must name at least the 2 packed variables")]
fn a_point_shorter_than_the_packed_tail_has_no_round_count() {
let _ = RingSwitchShape::new(1).sumcheck_rounds::<F, EF>();
}
}