use alloc::vec::Vec;
use p3_challenger::fs::TranscriptField;
use p3_challenger::{FieldChallenger, GrindingChallenger};
use p3_field::Field;
use p3_maybe_rayon::prelude::*;
use p3_multilinear_util::point::Point;
use p3_multilinear_util::poly::Poly;
use super::lde::CHUNK_BITS;
use super::transcript::{
SkipOpeningShape, SkipOpeningTranscriptError, SkipOpeningVerifierTranscript,
};
use crate::generic_degree::{GenericDegreeError, GenericDegreeProof, RoundProver};
pub const OPENING_DEGREE: usize = 2;
#[derive(Debug, Clone)]
pub struct SkipOpening<EF> {
lagrange: Poly<EF>,
}
impl<EF: Field> SkipOpening<EF> {
#[must_use]
pub fn new(lagrange: Poly<EF>) -> Self {
assert_eq!(
lagrange.num_evals() % CHUNK_BITS,
0,
"the skipped subspace covers whole byte chunks"
);
Self { lagrange }
}
#[must_use]
pub fn num_variables(&self) -> usize {
self.lagrange.num_variables()
}
#[must_use]
pub fn size(&self) -> usize {
self.lagrange.num_evals()
}
pub const fn lagrange(&self) -> &Poly<EF> {
&self.lagrange
}
#[must_use]
pub fn lagrange_at(&self, tau: &Point<EF>) -> EF {
self.lagrange.eval_base(tau)
}
pub fn partial_evaluation(&self, packed: &[u8], eq_rows: &Poly<EF>) -> Poly<EF>
where
EF: Send + Sync,
{
let row_bytes = self.size() / CHUNK_BITS;
assert_eq!(
packed.len(),
eq_rows.num_evals() * row_bytes,
"one packed row per equality weight"
);
let values = packed
.par_chunks_exact(row_bytes)
.zip(eq_rows.as_slice().par_iter())
.par_fold_reduce(
|| EF::zero_vec(self.size()),
|mut accumulator, (row, &weight)| {
for (chunk, &byte) in row.iter().enumerate() {
let mut bits = byte;
while bits != 0 {
let bit = bits.trailing_zeros() as usize;
accumulator[chunk * CHUNK_BITS + bit] += weight;
bits &= bits - 1;
}
}
accumulator
},
|mut left, right| {
for (entry, value) in left.iter_mut().zip(right) {
*entry += value;
}
left
},
);
Poly::new(values)
}
#[must_use]
pub fn partial_evaluations<'a>(
&self,
packed: impl IntoIterator<Item = &'a [u8]>,
rho: &Point<EF>,
) -> Vec<Poly<EF>>
where
EF: Send + Sync,
{
let eq_rows = Poly::new_from_point(rho.as_slice(), EF::ONE);
packed
.into_iter()
.map(|rows| self.partial_evaluation(rows, &eq_rows))
.collect()
}
pub fn batch(folded: &[Poly<EF>], gamma: EF) -> Poly<EF> {
assert!(!folded.is_empty(), "at least one committed polynomial");
let size = folded[0].num_evals();
assert!(
folded.iter().all(|poly| poly.num_evals() == size),
"every folded witness covers the same skipped cube"
);
let mut batched = EF::zero_vec(size);
for poly in folded.iter().rev() {
for (entry, &value) in batched.iter_mut().zip(poly.as_slice()) {
*entry = *entry * gamma + value;
}
}
Poly::new(batched)
}
#[must_use]
pub fn batch_claims(claims: &[EF], gamma: EF) -> EF {
assert!(!claims.is_empty(), "at least one claim");
claims
.iter()
.rev()
.fold(EF::ZERO, |accumulator, &claim| accumulator * gamma + claim)
}
pub fn verify<C>(
&self,
shape: SkipOpeningShape,
proof: &GenericDegreeProof<EF, EF>,
blends: &[EF],
challenger: &mut C,
) -> Result<SkipOpeningClaim<EF>, SkipOpeningError>
where
EF: TranscriptField,
C: FieldChallenger<EF> + GrindingChallenger<Witness = EF>,
{
if shape.num_variables != self.num_variables() {
return Err(SkipOpeningError::WidthMismatch {
expected: self.num_variables(),
actual: shape.num_variables,
});
}
let mut transcript = SkipOpeningVerifierTranscript::<C, EF, EF>::new(challenger, shape);
let gamma = transcript.batching_challenge(blends)?;
let (tau, closing_value) = transcript.sumcheck(|challenger| {
proof.verify(
challenger,
shape.num_variables,
OPENING_DEGREE,
shape.pow_bits,
)
})?;
transcript.finish();
if proof.claimed_sum != Self::batch_claims(blends, gamma) {
return Err(SkipOpeningError::StartingSumMismatch);
}
Ok(SkipOpeningClaim {
value: self.closing_target(&tau, closing_value)?,
point: tau,
gamma,
num_polynomials: shape.num_polynomials,
})
}
pub fn closing_target(
&self,
tau: &Point<EF>,
closing_value: EF,
) -> Result<EF, SkipOpeningError> {
let weight = self.lagrange_at(tau);
if weight.is_zero() {
return Err(SkipOpeningError::DegenerateWeight);
}
Ok(closing_value * weight.inverse())
}
#[must_use]
pub fn prover(&self, batched: Poly<EF>) -> SkipOpeningProver<EF> {
SkipOpeningProver {
lagrange: self.lagrange.clone(),
batched,
}
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum SkipOpeningError {
#[error("the proof carries {blends} blends and {openings} openings")]
ClaimCountMismatch {
blends: usize,
openings: usize,
},
#[error("the sumcheck's starting sum is not the batch of the claims")]
StartingSumMismatch,
#[error("the sumcheck's closing value is not the weighted batch of the openings")]
ClosingValueMismatch,
#[error("the reduction binds {expected} variables, the shape says {actual}")]
WidthMismatch {
expected: usize,
actual: usize,
},
#[error("the Lagrange weight vanished at the point the rounds reached")]
DegenerateWeight,
#[error(transparent)]
Transcript(#[from] SkipOpeningTranscriptError),
#[error("opening sumcheck: {0}")]
Sumcheck(#[from] GenericDegreeError),
}
#[must_use]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SkipOpeningClaim<EF> {
pub point: Point<EF>,
pub gamma: EF,
pub value: EF,
pub num_polynomials: usize,
}
impl<EF: Field> SkipOpeningClaim<EF> {
pub fn discharge(&self, openings: &[EF]) -> Result<(), SkipOpeningError> {
if openings.len() != self.num_polynomials {
return Err(SkipOpeningError::ClaimCountMismatch {
blends: self.num_polynomials,
openings: openings.len(),
});
}
if SkipOpening::batch_claims(openings, self.gamma) != self.value {
return Err(SkipOpeningError::ClosingValueMismatch);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SkipOpeningProver<EF> {
lagrange: Poly<EF>,
batched: Poly<EF>,
}
impl<EF: Field> SkipOpeningProver<EF> {
#[must_use]
pub fn surviving_claim(&self) -> EF {
assert_eq!(self.batched.num_evals(), 1, "every variable is bound");
self.batched.as_slice()[0]
}
}
impl<EF: Field> RoundProver<EF> for SkipOpeningProver<EF> {
fn fold(&mut self, r: EF) {
self.lagrange.fix_prefix_var_mut(r);
self.batched.fix_prefix_var_mut(r);
}
fn round_poly(&self) -> Vec<EF> {
let half = self.lagrange.num_evals() / 2;
[0, 2]
.into_iter()
.map(|node| {
let node = EF::interpolation_node(node);
(0..half)
.map(|index| {
let at = |poly: &Poly<EF>| {
let values = poly.as_slice();
values[index] + (values[index + half] - values[index]) * node
};
at(&self.lagrange) * at(&self.batched)
})
.sum()
})
.collect()
}
}
#[cfg(test)]
mod tests {
use p3_binary_field::{BinaryChallenger, BinaryField8, BinaryField128};
use p3_challenger::HashChallenger;
use p3_field::PrimeCharacteristicRing;
use p3_keccak::Keccak256Hash;
use proptest::prelude::*;
use rand::rngs::SmallRng;
use rand::{RngExt, SeedableRng};
use super::*;
use crate::generic_degree::{GenericDegreeError, GenericDegreeProof};
use crate::univariate_skip::{
SkipOpeningProverTranscript, SkipOpeningShape, SkipOpeningTranscriptError, SkipRound,
};
type F = BinaryField8;
type EF = BinaryField128;
type Challenger = BinaryChallenger<EF, HashChallenger<u8, Keccak256Hash, 32>>;
const LOG_SKIP: usize = 3;
const LOG_ROWS: usize = 2;
const NUM_POLYS: usize = 3;
const fn fresh_challenger() -> Challenger {
Challenger::from_hasher(Vec::new(), Keccak256Hash)
}
#[derive(Clone, Debug)]
struct Run {
blends: Vec<EF>,
sumcheck: GenericDegreeProof<EF, EF>,
openings: Vec<EF>,
}
const fn shape_at(pow_bits: usize) -> SkipOpeningShape {
SkipOpeningShape::new(LOG_SKIP, NUM_POLYS, pow_bits)
}
const fn shape() -> SkipOpeningShape {
shape_at(0)
}
fn prove(seed: u64) -> (SkipOpening<EF>, Point<EF>, Run) {
prove_at(seed, 0)
}
fn prove_at(seed: u64, pow_bits: usize) -> (SkipOpening<EF>, Point<EF>, Run) {
let mut rng = SmallRng::seed_from_u64(seed);
let round = SkipRound::<F>::new(LOG_SKIP, 2).unwrap();
let num_rows = 1 << LOG_ROWS;
let witnesses = (0..NUM_POLYS)
.map(|_| packed(&mut rng, num_rows, round.row_bytes()))
.collect::<Vec<_>>();
let lambda = rng.random::<EF>();
let opening = SkipOpening::new(round.lagrange::<EF>(lambda));
let rho = Point::new((0..LOG_ROWS).map(|_| rng.random::<EF>()).collect());
let folded = opening.partial_evaluations(witnesses.iter().map(Vec::as_slice), &rho);
let blends = folded
.iter()
.map(|poly| {
poly.as_slice()
.iter()
.zip(opening.lagrange().as_slice())
.map(|(&value, &weight)| value * weight)
.sum::<EF>()
})
.collect::<Vec<_>>();
let mut challenger = fresh_challenger();
let mut transcript = SkipOpeningProverTranscript::<Challenger, EF, EF>::new(
&mut challenger,
shape_at(pow_bits),
);
let gamma = transcript.batching_challenge(&blends);
let starting_sum = SkipOpening::batch_claims(&blends, gamma);
let mut prover = opening.prover(SkipOpening::batch(&folded, gamma));
let (sumcheck, tau) = transcript.sumcheck(|challenger| {
prover.prove::<EF, _>(challenger, LOG_SKIP, OPENING_DEGREE, pow_bits, starting_sum)
});
transcript.finish();
let whole = Point::new(rho.as_slice().iter().copied().chain(tau).collect());
let openings = witnesses
.iter()
.map(|rows| Poly::new(unpack(rows)).eval_base(&whole))
.collect::<Vec<_>>();
(
opening,
rho,
Run {
blends,
sumcheck,
openings,
},
)
}
fn verify(opening: &SkipOpening<EF>, run: &Run) -> Result<(), SkipOpeningError> {
verify_at(opening, run, 0)
}
fn verify_at(
opening: &SkipOpening<EF>,
run: &Run,
pow_bits: usize,
) -> Result<(), SkipOpeningError> {
let mut challenger = fresh_challenger();
let claim = opening.verify(
shape_at(pow_bits),
&run.sumcheck,
&run.blends,
&mut challenger,
)?;
claim.discharge(&run.openings)
}
fn challenge_for(claims: &[EF]) -> EF {
let mut challenger = fresh_challenger();
let mut transcript =
SkipOpeningProverTranscript::<Challenger, EF, EF>::new(&mut challenger, shape());
let gamma = transcript.batching_challenge(claims);
transcript.sumcheck(|_| ());
transcript.finish();
gamma
}
fn unpack(packed: &[u8]) -> Vec<EF> {
(0..packed.len() * CHUNK_BITS)
.map(|index| {
let bit = (packed[index / CHUNK_BITS] >> (index % CHUNK_BITS)) & 1;
if bit == 1 { EF::ONE } else { EF::ZERO }
})
.collect()
}
fn packed(rng: &mut SmallRng, num_rows: usize, row_bytes: usize) -> Vec<u8> {
(0..num_rows * row_bytes)
.map(|_| rng.random::<u8>())
.collect()
}
#[test]
fn the_fold_is_the_witness_read_at_the_residual_point() {
let mut rng = SmallRng::seed_from_u64(0x09E7);
let round = SkipRound::<F>::new(6, 2).unwrap();
let num_rows = 1 << 4;
let rows = packed(&mut rng, num_rows, round.row_bytes());
let bits = unpack(&rows);
let lambda = rng.random::<EF>();
let opening = SkipOpening::new(round.selector::<EF>(lambda).lagrange().clone());
let rho = Point::<EF>::rand(&mut rng, 4);
let folded =
opening.partial_evaluation(&rows, &Poly::new_from_point(rho.as_slice(), EF::ONE));
let eq = Poly::new_from_point(rho.as_slice(), EF::ONE);
let size = opening.size();
for column in 0..size {
let expected = (0..num_rows)
.map(|row| eq.as_slice()[row] * bits[row * size + column])
.sum::<EF>();
assert_eq!(folded.as_slice()[column], expected, "c={column}");
}
}
#[test]
fn the_blend_the_round_leaves_is_the_weighted_fold() {
let mut rng = SmallRng::seed_from_u64(0xB1E0);
let round = SkipRound::<F>::new(6, 2).unwrap();
let num_rows = 1 << 4;
let rows = packed(&mut rng, num_rows, round.row_bytes());
let lambda = rng.random::<EF>();
let selector = round.selector::<EF>(lambda);
let rho = Point::new((0..4).map(|_| rng.random::<EF>()).collect());
let blended = selector.bind(&rows).eval_base(&rho);
let opening = SkipOpening::new(selector.lagrange().clone());
let folded =
opening.partial_evaluation(&rows, &Poly::new_from_point(rho.as_slice(), EF::ONE));
let weighted = folded
.as_slice()
.iter()
.zip(opening.lagrange().as_slice())
.map(|(&value, &weight)| value * weight)
.sum::<EF>();
assert_eq!(blended, weighted);
}
#[test]
fn batching_agrees_with_weighing_the_claims_by_hand() {
let mut rng = SmallRng::seed_from_u64(0xBA7C);
let size = 64;
let polys = (0..3)
.map(|_| Poly::new((0..size).map(|_| rng.random::<EF>()).collect::<Vec<_>>()))
.collect::<Vec<_>>();
let weights = (0..size).map(|_| rng.random::<EF>()).collect::<Vec<_>>();
let gamma = rng.random::<EF>();
let batched = SkipOpening::batch(&polys, gamma);
let combined = batched
.as_slice()
.iter()
.zip(&weights)
.map(|(&value, &weight)| value * weight)
.sum::<EF>();
let claims = polys
.iter()
.map(|poly| {
poly.as_slice()
.iter()
.zip(&weights)
.map(|(&value, &weight)| value * weight)
.sum::<EF>()
})
.collect::<Vec<_>>();
assert_eq!(combined, SkipOpening::batch_claims(&claims, gamma));
}
#[test]
fn an_honest_run_verifies_through_both_drivers() {
let (opening, _, run) = prove(0xE2E);
assert_eq!(verify(&opening, &run), Ok(()));
}
#[test]
fn the_batching_challenge_follows_the_claims() {
let mut rng = SmallRng::seed_from_u64(0x6A3);
let claims = (0..NUM_POLYS)
.map(|_| rng.random::<EF>())
.collect::<Vec<_>>();
let gamma = challenge_for(&claims);
let delta = rng.random::<EF>();
let mut shifted = claims.clone();
shifted[0] += gamma * delta;
shifted[1] -= delta;
assert_eq!(
SkipOpening::batch_claims(&shifted, gamma),
SkipOpening::batch_claims(&claims, gamma),
"the shift is chosen to leave the batch alone"
);
assert_ne!(challenge_for(&shifted), gamma);
}
#[test]
fn a_shift_that_preserves_the_batch_is_still_rejected() {
let (opening, _, honest) = prove(0x5417);
let gamma = challenge_for(&honest.blends);
let delta = SmallRng::seed_from_u64(0x5418).random::<EF>();
let mut run = honest;
run.blends[0] += gamma * delta;
run.blends[1] -= delta;
assert_eq!(
SkipOpening::batch_claims(&run.blends, gamma),
run.sumcheck.claimed_sum
);
assert_eq!(
verify(&opening, &run),
Err(SkipOpeningError::StartingSumMismatch)
);
}
#[test]
fn a_tampered_starting_sum_is_rejected() {
let (opening, _, honest) = prove(0x57A47);
let mut run = honest;
run.sumcheck.claimed_sum += EF::ONE;
assert!(verify(&opening, &run).is_err());
}
#[test]
fn a_tampered_opening_is_rejected() {
let (opening, _, honest) = prove(0x09E1);
let mut run = honest;
run.openings[2] += EF::ONE;
assert_eq!(
verify(&opening, &run),
Err(SkipOpeningError::ClosingValueMismatch)
);
}
#[test]
fn a_tampered_round_polynomial_is_rejected() {
let (opening, _, honest) = prove(0x20D);
let mut run = honest;
run.sumcheck.round_polys[1] = alloc::vec![EF::ZERO; OPENING_DEGREE];
assert_eq!(
verify(&opening, &run),
Err(SkipOpeningError::ClosingValueMismatch)
);
}
#[test]
fn a_round_the_nested_sumcheck_refuses_is_an_error_not_a_panic() {
let (opening, _, honest) = prove(0xAB07);
let mut run = honest;
run.sumcheck.round_polys.pop();
assert_eq!(
verify(&opening, &run),
Err(SkipOpeningError::Sumcheck(
GenericDegreeError::RoundCountMismatch {
expected: LOG_SKIP,
actual: LOG_SKIP - 1,
}
))
);
}
#[test]
fn a_grinding_witness_the_nested_sumcheck_refuses_is_an_error() {
let (opening, _, honest) = prove_at(0xA607, 4);
let mut run = honest;
run.sumcheck.pow_witnesses.pop();
assert!(matches!(
verify_at(&opening, &run, 4),
Err(SkipOpeningError::Sumcheck(
GenericDegreeError::PowWitnessCountMismatch { .. }
))
));
}
#[test]
fn a_wrong_claim_count_is_rejected() {
let (opening, _, honest) = prove(0xC07);
let mut run = honest;
run.blends.pop();
assert_eq!(
verify(&opening, &run),
Err(SkipOpeningError::Transcript(
SkipOpeningTranscriptError::ClaimCountMismatch {
expected: NUM_POLYS,
actual: NUM_POLYS - 1,
}
))
);
}
#[test]
fn blends_and_openings_must_agree_on_count() {
let (opening, _, honest) = prove(0xC08);
let mut run = honest;
run.openings.pop();
assert_eq!(
verify(&opening, &run),
Err(SkipOpeningError::ClaimCountMismatch {
blends: NUM_POLYS,
openings: NUM_POLYS - 1,
})
);
}
#[test]
fn the_verifier_builds_the_same_lagrange_vector_as_the_prover() {
let mut rng = SmallRng::seed_from_u64(0x1A6);
let round = SkipRound::<F>::new(LOG_SKIP, 2).unwrap();
for lambda in (0..8)
.map(|_| rng.random::<EF>())
.chain(round.domain().subspace().iter().map(|&s| EF::from(s)))
{
assert_eq!(
round.lagrange::<EF>(lambda),
*round.selector::<EF>(lambda).lagrange()
);
}
}
#[test]
#[should_panic(expected = "batches at least one polynomial")]
fn an_empty_batch_is_refused() {
let _ = SkipOpeningShape::new(LOG_SKIP, 0, 0);
}
proptest! {
#[test]
fn the_reduction_ends_on_the_committed_evaluation(seed: u64, log_rows in 1usize..=4) {
let mut rng = SmallRng::seed_from_u64(seed);
let round = SkipRound::<F>::new(3, 2).unwrap();
let num_rows = 1 << log_rows;
let rows = packed(&mut rng, num_rows, round.row_bytes());
let bits = Poly::new(unpack(&rows));
let lambda = rng.random::<EF>();
let opening = SkipOpening::new(round.selector::<EF>(lambda).lagrange().clone());
let rho = Point::new((0..log_rows).map(|_| rng.random::<EF>()).collect());
let folded = opening.partial_evaluations([rows.as_slice()], &rho);
let mut prover = opening.prover(SkipOpening::batch(&folded, EF::ONE));
let mut tau = Vec::new();
for _ in 0..opening.num_variables() {
let _ = prover.round_poly();
let challenge = rng.random::<EF>();
prover.fold(challenge);
tau.push(challenge);
}
let whole = Point::new(rho.as_slice().iter().copied().chain(tau).collect());
prop_assert_eq!(prover.surviving_claim(), bits.eval_base(&whole));
}
}
}