use p3_challenger::fs::TranscriptField;
use p3_challenger::{FieldChallenger, GrindingChallenger};
use p3_field::{ExtensionField, Field};
use p3_multilinear_util::point::Point;
use p3_multilinear_util::poly::Poly;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::data::SumcheckData;
use crate::error::SumcheckError;
use crate::product_polynomial::ProductPolynomial;
use crate::strategy::{Basis, SumcheckProver, VariableOrder};
pub mod bits;
pub mod equality;
pub mod packing;
pub mod tensor;
pub mod transcript;
pub mod weights;
pub use equality::equality_element;
use packing::compute_s_hat_with_eq;
pub use packing::{compute_s_hat, pack, packed_vars};
pub use tensor::TensorAlgebra;
pub use transcript::{RingSwitchProverTranscript, RingSwitchShape, RingSwitchVerifierTranscript};
use weights::batched_weights_with_eq;
pub use weights::{batch_rows, batched_weights};
#[cfg(test)]
pub(crate) mod test_util;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(bound(
serialize = "F: Field, EF: ExtensionField<F>",
deserialize = "F: Field, EF: ExtensionField<F>"
))]
pub struct RingSwitchProof<F, EF> {
pub s_hat: TensorAlgebra<F, EF>,
pub sumcheck: SumcheckData<F, EF>,
pub final_eval: EF,
}
#[derive(Error, Debug, PartialEq, Eq)]
pub enum RingSwitchError {
#[error("Ring switching: s_hat carries {actual} base coefficients, expected {expected}")]
MalformedTensor {
expected: usize,
actual: usize,
},
#[error("Ring switching: the evaluation point names {actual} coordinates, expected {expected}")]
PointWidthMismatch {
expected: usize,
actual: usize,
},
#[error("Ring switching: the claimed evaluation is not the column reading of s_hat")]
ClaimMismatch,
#[error(transparent)]
Sumcheck(#[from] SumcheckError),
#[error("Ring switching: the surviving claim does not close the sumcheck")]
FinalCheck,
#[error("Ring switching: the sumcheck carries {actual} PoW witnesses, expected none")]
NonEmptyPowWitnesses {
actual: usize,
},
}
pub fn prove_ring_switch<F, EF, Challenger>(
packed: &Poly<EF>,
r: &Point<EF>,
challenger: &mut Challenger,
) -> (RingSwitchProof<F, EF>, Point<EF>, EF)
where
F: TranscriptField,
EF: ExtensionField<F>,
Challenger: FieldChallenger<F> + GrindingChallenger<Witness = F>,
{
let shape = RingSwitchShape::new(r.num_variables());
let ell_prime = shape.sumcheck_rounds::<F, EF>();
assert_eq!(
packed.num_variables(),
ell_prime,
"the packed polynomial must have the {ell_prime} variables the evaluation point leaves \
once the packed ones are removed"
);
let (r_high, _) = r.split_at(ell_prime);
let eq = Poly::<EF>::new_from_point(r_high.as_slice(), EF::ONE);
let s_hat = compute_s_hat_with_eq::<F, EF>(packed, &eq);
let mut transcript = RingSwitchProverTranscript::<Challenger, F, EF>::new(challenger, shape);
let r_batch = transcript.statement(r, s_hat.coefficients());
let weights = batched_weights_with_eq::<F, EF>(&eq, &r_batch);
drop(eq);
let poly = ProductPolynomial::new_unpacked(VariableOrder::Prefix, packed.clone(), weights);
let mut prover = SumcheckProver::new(poly, batch_rows::<F, EF>(&s_hat, &r_batch));
let mut sumcheck = SumcheckData::default();
let r_prime = transcript.batched_sumcheck(|challenger| {
prover.compute_sumcheck_polynomials(&mut sumcheck, challenger, ell_prime, 0, None)
});
let final_eval = prover.evals().as_slice()[0];
transcript.surviving_claim(final_eval);
transcript.finish();
(
RingSwitchProof {
s_hat,
sumcheck,
final_eval,
},
r_prime,
final_eval,
)
}
pub fn verify_ring_switch<F, EF, Challenger>(
proof: &RingSwitchProof<F, EF>,
r: &Point<EF>,
claimed_sum: EF,
challenger: &mut Challenger,
) -> Result<(Point<EF>, EF), RingSwitchError>
where
F: TranscriptField,
EF: ExtensionField<F>,
Challenger: FieldChallenger<F> + GrindingChallenger<Witness = F>,
{
let shape = RingSwitchShape::new(r.num_variables());
let ell_prime = shape.sumcheck_rounds::<F, EF>();
if !proof.s_hat.is_well_formed() {
return Err(RingSwitchError::MalformedTensor {
expected: RingSwitchShape::tensor_coefficients::<F, EF>(),
actual: proof.s_hat.coefficients().len(),
});
}
if !proof.sumcheck.pow_witnesses.is_empty() {
return Err(RingSwitchError::NonEmptyPowWitnesses {
actual: proof.sumcheck.pow_witnesses.len(),
});
}
let (r_high, r_low) = r.split_at(ell_prime);
let mut transcript = RingSwitchVerifierTranscript::<Challenger, F, EF>::new(challenger, shape);
let r_batch = transcript.statement(r, proof.s_hat.coefficients())?;
let eq_low = Poly::<EF>::new_from_point(r_low.as_slice(), EF::ONE);
let combined: EF = proof
.s_hat
.columns()
.iter()
.zip(eq_low.as_slice())
.map(|(&column, &weight)| column * weight)
.sum();
if combined != claimed_sum {
transcript.abort();
return Err(RingSwitchError::ClaimMismatch);
}
let mut sum = batch_rows::<F, EF>(&proof.s_hat, &r_batch);
let rounds = transcript.batched_sumcheck(|challenger| {
proof
.sumcheck
.verify_rounds(challenger, &mut sum, ell_prime, 0, Basis::Evaluation)
});
let r_prime = match rounds {
Ok(r_prime) => r_prime,
Err(error) => {
transcript.abort();
return Err(error.into());
}
};
transcript.surviving_claim(proof.final_eval);
transcript.finish();
let e = equality_element::<F, EF>(&r_high, &r_prime);
if sum != batch_rows::<F, EF>(&e, &r_batch) * proof.final_eval {
return Err(RingSwitchError::FinalCheck);
}
Ok((r_prime, proof.final_eval))
}
#[cfg(test)]
mod tests {
use p3_challenger::CanSample;
use p3_field::PrimeCharacteristicRing;
use p3_multilinear_util::point::Point;
use proptest::prelude::*;
use rand::SeedableRng;
use rand::rngs::SmallRng;
use super::test_util::{Challenger, EF, F, base_poly, challenger};
use super::*;
#[test]
fn the_reduction_round_trips() {
let ell = 7;
let t = base_poly(ell, 13);
let packed = pack::<F, EF>(t.clone());
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(14), ell);
let s = t.eval_base(&r);
let mut p_chal = challenger();
let (proof, r_prime_p, s_prime_p) = prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
let mut v_chal = challenger();
let (r_prime_v, s_prime_v) =
verify_ring_switch::<F, EF, _>(&proof, &r, s, &mut v_chal).unwrap();
assert_eq!(r_prime_p, r_prime_v);
assert_eq!(s_prime_p, s_prime_v);
assert_eq!(s_prime_v, packed.eval_ext::<F>(&r_prime_v));
}
proptest! {
#![proptest_config(ProptestConfig { cases: 16, ..ProptestConfig::default() })]
#[test]
fn the_reduction_round_trips_over_random_inputs(
ell in 5usize..=8,
poly_seed in any::<u64>(),
point_seed in any::<u64>(),
) {
let t = base_poly(ell, poly_seed);
let packed = pack::<F, EF>(t.clone());
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(point_seed), ell);
let s = t.eval_base(&r);
let mut p_chal = challenger();
let (proof, r_prime_p, s_prime_p) =
prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
let mut v_chal = challenger();
let (r_prime_v, s_prime_v) =
verify_ring_switch::<F, EF, _>(&proof, &r, s, &mut v_chal)
.expect("an honest reduction must verify");
prop_assert_eq!(&r_prime_p, &r_prime_v);
prop_assert_eq!(s_prime_p, s_prime_v);
prop_assert_eq!(s_prime_v, packed.eval_ext::<F>(&r_prime_v));
prop_assert_eq!(
CanSample::<F>::sample(&mut p_chal),
CanSample::<F>::sample(&mut v_chal),
);
}
}
#[test]
fn a_wrong_claim_is_rejected() {
let ell = 6;
let t = base_poly(ell, 15);
let packed = pack::<F, EF>(t.clone());
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(16), ell);
let s = t.eval_base(&r);
let mut p_chal = challenger();
let (proof, _, _) = prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
let mut v_chal = challenger();
assert_eq!(
verify_ring_switch::<F, EF, _>(&proof, &r, s + EF::ONE, &mut v_chal),
Err(RingSwitchError::ClaimMismatch)
);
}
#[test]
fn a_perturbed_tensor_element_is_rejected() {
let ell = 6;
let t = base_poly(ell, 17);
let packed = pack::<F, EF>(t);
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(18), ell);
let mut p_chal = challenger();
let (mut proof, _, _) = prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
proof.s_hat.perturb_for_test(0, F::ONE);
let (_, r_low) = r.split_at(ell - packed_vars::<F, EF>());
let eq_low = Poly::<EF>::new_from_point(r_low.as_slice(), EF::ONE);
let tampered_claim: EF = proof
.s_hat
.columns()
.iter()
.zip(eq_low.as_slice())
.map(|(&column, &weight)| column * weight)
.sum();
let mut v_chal = challenger();
assert_eq!(
verify_ring_switch::<F, EF, _>(&proof, &r, tampered_claim, &mut v_chal),
Err(RingSwitchError::FinalCheck)
);
}
#[test]
fn a_perturbed_final_claim_is_rejected() {
let ell = 6;
let t = base_poly(ell, 33);
let packed = pack::<F, EF>(t.clone());
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(34), ell);
let s = t.eval_base(&r);
let mut p_chal = challenger();
let (mut proof, _, _) = prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
proof.final_eval += EF::ONE;
let mut v_chal = challenger();
assert_eq!(
verify_ring_switch::<F, EF, _>(&proof, &r, s, &mut v_chal),
Err(RingSwitchError::FinalCheck)
);
}
#[test]
fn a_perturbed_round_message_is_rejected() {
let ell = 6;
let t = base_poly(ell, 35);
let packed = pack::<F, EF>(t.clone());
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(36), ell);
let s = t.eval_base(&r);
let mut p_chal = challenger();
let (mut proof, _, _) = prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
proof.sumcheck.polynomial_evaluations[0][0] += EF::ONE;
let mut v_chal = challenger();
assert_eq!(
verify_ring_switch::<F, EF, _>(&proof, &r, s, &mut v_chal),
Err(RingSwitchError::FinalCheck)
);
}
#[test]
fn a_proof_does_not_verify_against_a_different_low_point() {
let ell = 6;
let kappa = packed_vars::<F, EF>();
let t = base_poly(ell, 37);
let packed = pack::<F, EF>(t.clone());
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(38), ell);
let mut p_chal = challenger();
let (proof, _, _) = prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
let (r_high, r_low) = r.split_at(ell - kappa);
let mut moved = r_low.as_slice().to_vec();
moved[0] += EF::ONE;
let mut coords = r_high.as_slice().to_vec();
coords.extend_from_slice(&moved);
let other_r = Point::new(coords);
let other_s = t.eval_base(&other_r);
let mut v_chal = challenger();
assert_eq!(
verify_ring_switch::<F, EF, _>(&proof, &other_r, other_s, &mut v_chal),
Err(RingSwitchError::FinalCheck)
);
}
#[test]
fn a_proof_does_not_verify_against_a_different_high_point() {
let ell = 6;
let kappa = packed_vars::<F, EF>();
let t = base_poly(ell, 45);
let packed = pack::<F, EF>(t.clone());
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(46), ell);
let s = t.eval_base(&r);
let mut p_chal = challenger();
let (proof, _, _) = prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
let (r_high, r_low) = r.split_at(ell - kappa);
let mut moved = r_high.as_slice().to_vec();
moved[0] += EF::ONE;
moved.extend_from_slice(r_low.as_slice());
let other_r = Point::new(moved);
let mut v_chal = challenger();
assert_eq!(
verify_ring_switch::<F, EF, _>(&proof, &other_r, s, &mut v_chal),
Err(RingSwitchError::FinalCheck)
);
}
#[test]
fn a_proof_with_pow_witnesses_is_rejected() {
let ell = 6;
let t = base_poly(ell, 40);
let packed = pack::<F, EF>(t.clone());
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(41), ell);
let s = t.eval_base(&r);
let mut p_chal = challenger();
let (mut proof, _, _) = prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
proof.sumcheck.pow_witnesses.push(F::ONE);
let mut v_chal = challenger();
assert_eq!(
verify_ring_switch::<F, EF, _>(&proof, &r, s, &mut v_chal),
Err(RingSwitchError::NonEmptyPowWitnesses { actual: 1 })
);
}
#[test]
fn an_adaptive_cheat_on_s_hat_is_accepted_with_a_false_surviving_claim() {
let ell = 6;
let t = base_poly(ell, 43);
let packed = pack::<F, EF>(t);
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(44), ell);
let kappa = packed_vars::<F, EF>();
let ell_prime = ell - kappa;
let (r_high, r_low) = r.split_at(ell_prime);
let mut s_hat = compute_s_hat::<F, EF>(&packed, &r_high);
s_hat.perturb_for_test(0, F::ONE);
let eq_low = Poly::<EF>::new_from_point(r_low.as_slice(), EF::ONE);
let claimed_sum: EF = s_hat
.columns()
.iter()
.zip(eq_low.as_slice())
.map(|(&column, &weight)| column * weight)
.sum();
let mut chal = challenger();
let shape = RingSwitchShape::new(ell);
let mut transcript = RingSwitchProverTranscript::<Challenger, F, EF>::new(&mut chal, shape);
let r_batch = transcript.statement(&r, s_hat.coefficients());
let shifted_sum = batch_rows::<F, EF>(&s_hat, &r_batch);
let weights = batched_weights::<F, EF>(&r_high, &r_batch);
let honest_sum = batch_rows::<F, EF>(&compute_s_hat::<F, EF>(&packed, &r_high), &r_batch);
let index = weights
.as_slice()
.iter()
.position(|&w| w != EF::ZERO)
.expect("a generic r_high, r_batch pair leaves no weight identically zero");
let mut fake_evals = packed.as_slice().to_vec();
fake_evals[index] += (shifted_sum - honest_sum) * weights.as_slice()[index].inverse();
let fake_packed = Poly::new(fake_evals);
let poly = ProductPolynomial::new_unpacked(VariableOrder::Prefix, fake_packed, weights);
let mut prover = SumcheckProver::new(poly, shifted_sum);
let mut sumcheck = SumcheckData::default();
let r_prime = transcript.batched_sumcheck(|challenger| {
prover.compute_sumcheck_polynomials(&mut sumcheck, challenger, ell_prime, 0, None)
});
let sum_final = prover.claimed_sum();
let e = equality_element::<F, EF>(&r_high, &r_prime);
let a_r_prime = batch_rows::<F, EF>(&e, &r_batch);
let final_eval = sum_final * a_r_prime.inverse();
transcript.surviving_claim(final_eval);
transcript.finish();
let proof = RingSwitchProof {
s_hat,
sumcheck,
final_eval,
};
let mut v_chal = challenger();
let (r_prime_v, s_prime_v) =
verify_ring_switch::<F, EF, _>(&proof, &r, claimed_sum, &mut v_chal).unwrap();
assert_eq!(r_prime_v, r_prime);
assert_eq!(s_prime_v, final_eval);
assert_ne!(s_prime_v, packed.eval_ext::<F>(&r_prime_v));
}
#[test]
#[should_panic(expected = "the evaluation point must name at least the 4 packed variables")]
fn prove_rejects_a_point_shorter_than_the_packed_variables() {
let t = base_poly(6, 19);
let packed = pack::<F, EF>(t);
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(20), 3);
let mut chal = challenger();
let _ = prove_ring_switch::<F, EF, _>(&packed, &r, &mut chal);
}
#[test]
#[should_panic(expected = "the packed polynomial must have the 2 variables")]
fn prove_rejects_a_packed_polynomial_of_the_wrong_arity() {
let t = base_poly(7, 21);
let packed = pack::<F, EF>(t);
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(22), 6);
let mut chal = challenger();
let _ = prove_ring_switch::<F, EF, _>(&packed, &r, &mut chal);
}
#[test]
#[should_panic(expected = "the evaluation point must name at least the 4 packed variables")]
fn verify_rejects_a_point_shorter_than_the_packed_variables() {
let ell = 6;
let t = base_poly(ell, 26);
let packed = pack::<F, EF>(t);
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(27), ell);
let mut p_chal = challenger();
let (proof, _, _) = prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
let short = Point::<EF>::rand(&mut SmallRng::seed_from_u64(28), 3);
let mut v_chal = challenger();
let _ = verify_ring_switch::<F, EF, _>(&proof, &short, EF::ZERO, &mut v_chal);
}
#[test]
fn verify_rejects_a_proof_with_the_wrong_round_count() {
let ell = 6;
let t = base_poly(ell, 29);
let packed = pack::<F, EF>(t.clone());
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(30), ell);
let s = t.eval_base(&r);
let mut p_chal = challenger();
let (mut proof, _, _) = prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
proof.sumcheck.polynomial_evaluations.pop();
let mut v_chal = challenger();
assert_eq!(
verify_ring_switch::<F, EF, _>(&proof, &r, s, &mut v_chal),
Err(RingSwitchError::Sumcheck(
SumcheckError::RoundCountMismatch {
expected: ell - 4,
actual: ell - 5,
}
))
);
}
#[test]
fn verify_rejects_a_malformed_tensor() {
let ell = 6;
let t = base_poly(ell, 31);
let packed = pack::<F, EF>(t.clone());
let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(32), ell);
let s = t.eval_base(&r);
let mut p_chal = challenger();
let (mut proof, _, _) = prove_ring_switch::<F, EF, _>(&packed, &r, &mut p_chal);
proof.s_hat.truncate_for_test(1);
let dimension = TensorAlgebra::<F, EF>::DIMENSION;
let mut v_chal = challenger();
assert_eq!(
verify_ring_switch::<F, EF, _>(&proof, &r, s, &mut v_chal),
Err(RingSwitchError::MalformedTensor {
expected: dimension * dimension,
actual: 1,
})
);
}
}