use alloc::vec;
use alloc::vec::Vec;
use p3_challenger::fs::{DomainSeparator, FieldUnit, InteractionPattern, TranscriptField};
use p3_challenger::{CanObserve, CanSampleUniformBits, FieldChallenger, GrindingChallenger};
use p3_field::{ExtensionField, Field};
use p3_multilinear_util::point::Point;
use p3_security::{ErrorBits, SecurityTerm};
use p3_util::log2_ceil_usize;
use serde::{Deserialize, Serialize};
use super::error::JaggedOpeningError;
use super::layout::JaggedLayout;
use super::transcript::encode_layout;
use super::{JaggedPoint, JaggedProof};
use crate::prescribed_pcs::{PrescribedOpeningSecurity, PrescribedPointPcs};
use crate::table::{OpeningBatch, OpeningProtocol, TableShape, TableSpec};
const VERSION: u8 = 1;
const NAME: &[u8] = b"p3-sumcheck-jagged-layout";
const REDUCTION_LABEL: &str = "jagged sparse-to-dense reduction";
#[must_use]
#[derive(Clone, Copy, Debug)]
pub struct BoundJaggedLayout<'a>(&'a JaggedLayout);
#[derive(Clone, Copy, Debug)]
pub struct JaggedOpeningShape<'a> {
layout: &'a JaggedLayout,
claims: usize,
}
impl<'a> JaggedOpeningShape<'a> {
#[must_use]
pub const fn new(layout: &'a JaggedLayout, claims: usize) -> Self {
Self { layout, claims }
}
}
impl From<JaggedOpeningShape<'_>> for OpeningProtocol {
fn from(shape: JaggedOpeningShape<'_>) -> Self {
Self::new(vec![TableSpec::new(
TableShape::new(shape.layout.dense_variables(), 1),
vec![OpeningBatch::new(vec![0], Vec::new()); shape.claims],
)])
}
}
impl<'a> BoundJaggedLayout<'a> {
pub fn new<F, Challenger>(layout: &'a JaggedLayout, challenger: &mut Challenger) -> Self
where
F: TranscriptField,
Challenger: CanObserve<F>,
{
let pattern = InteractionPattern::new(Vec::new())
.expect("an empty description is structurally valid");
let mut separator = DomainSeparator::<FieldUnit<F>>::new(VERSION, NAME, pattern);
separator.instance(&encode_layout(layout));
separator.seed(challenger);
Self(layout)
}
#[must_use]
pub const fn layout(&self) -> &'a JaggedLayout {
self.0
}
pub fn sample_point<F, EF, Challenger>(&self, challenger: &mut Challenger) -> JaggedPoint<EF>
where
F: Field,
EF: ExtensionField<F>,
Challenger: FieldChallenger<F>,
{
let mut draw = |count| {
Point::new(
(0..count)
.map(|_| challenger.sample_algebra_element::<EF>())
.collect(),
)
};
let row = draw(self.0.row_variables());
let column = draw(self.0.column_variables());
JaggedPoint::new(row, column)
}
pub fn open<F, EF, Pcs, Challenger>(
&self,
pcs: &Pcs,
prover_data: Pcs::ProverData,
dense_witness: &[F],
claims: &[(JaggedPoint<EF>, EF)],
challenger: &mut Challenger,
) -> Result<JaggedOpening<F, EF, Pcs::Proof>, JaggedOpeningError<Pcs::ProverError>>
where
F: TranscriptField,
EF: ExtensionField<F>,
Pcs: PrescribedPointPcs<EF, Challenger, Val = F>,
Challenger: FieldChallenger<F>
+ GrindingChallenger<Witness = F>
+ CanSampleUniformBits<F>
+ CanObserve<Pcs::Commitment>,
{
if claims.is_empty() {
return Err(JaggedOpeningError::NoClaims);
}
let mut reductions = Vec::with_capacity(claims.len());
let mut points = Vec::with_capacity(claims.len());
for (point, value) in claims {
let (reduction, claim) = self
.0
.prove(dense_witness, point, *value, challenger)?
.into_parts();
reductions.push(reduction);
points.push(claim.point().clone());
}
let dense = pcs
.open_at(
prover_data,
&OpeningProtocol::from(JaggedOpeningShape::new(self.0, claims.len())),
&points,
challenger,
)
.map_err(JaggedOpeningError::Commitment)?;
Ok(JaggedOpening { reductions, dense })
}
#[must_use]
pub fn security<EF, Pcs, Challenger>(
&self,
pcs: &Pcs,
claims: usize,
) -> Option<PrescribedOpeningSecurity>
where
EF: ExtensionField<Pcs::Val>,
Pcs: PrescribedPointPcs<EF, Challenger>,
Challenger: FieldChallenger<Pcs::Val>
+ GrindingChallenger<Witness = Pcs::Val>
+ CanSampleUniformBits<Pcs::Val>
+ CanObserve<Pcs::Commitment>,
{
if claims == 0 {
return None;
}
let protocol = OpeningProtocol::from(JaggedOpeningShape::new(self.0, claims));
let mut security = pcs.prescribed_security(&protocol)?;
security.charge_reduction(reduction_term::<EF>(self.0.dense_variables(), claims));
Some(security)
}
pub fn verify<F, EF, Pcs, Challenger>(
&self,
pcs: &Pcs,
commitment: &Pcs::Commitment,
opening: &JaggedOpening<F, EF, Pcs::Proof>,
claims: &[(JaggedPoint<EF>, EF)],
challenger: &mut Challenger,
) -> Result<(), JaggedOpeningError<Pcs::Error>>
where
F: TranscriptField,
EF: ExtensionField<F>,
Pcs: PrescribedPointPcs<EF, Challenger, Val = F>,
Challenger: FieldChallenger<F>
+ GrindingChallenger<Witness = F>
+ CanSampleUniformBits<F>
+ CanObserve<Pcs::Commitment>,
{
if claims.is_empty() {
return Err(JaggedOpeningError::NoClaims);
}
if opening.reductions.len() != claims.len() {
return Err(JaggedOpeningError::ReductionCountMismatch {
expected: claims.len(),
actual: opening.reductions.len(),
});
}
let mut values = Vec::with_capacity(claims.len());
let mut points = Vec::with_capacity(claims.len());
for ((point, value), reduction) in claims.iter().zip(&opening.reductions) {
let claim = self.0.verify(point, *value, reduction, challenger)?;
values.push(*claim.value());
points.push(claim.point().clone());
}
let opened = pcs
.verify_at(
commitment,
&opening.dense,
&OpeningProtocol::from(JaggedOpeningShape::new(self.0, claims.len())),
&points,
challenger,
)
.map_err(JaggedOpeningError::Commitment)?;
if opened.len() != claims.len() {
return Err(JaggedOpeningError::OpeningCountMismatch {
expected: claims.len(),
actual: opened.len(),
});
}
for (index, batch) in opened.iter().enumerate() {
if batch.current().len() != 1 || !batch.next().is_empty() {
return Err(JaggedOpeningError::OpeningShape {
reading: index,
direct: batch.current().len(),
successor: batch.next().len(),
});
}
if batch.current()[0] != values[index] {
return Err(JaggedOpeningError::DenseMismatch { reading: index });
}
}
Ok(())
}
}
fn reduction_term<EF: Field>(dense_variables: usize, claims: usize) -> SecurityTerm {
let error = if dense_variables == 0 || claims == 0 {
ErrorBits::from_log2(f64::INFINITY)
} else {
let degree = dense_variables.saturating_mul(claims).saturating_mul(2);
let bits = EF::bits().saturating_sub(1) as f64 - log2_ceil_usize(degree) as f64;
ErrorBits::from_log2(bits)
};
SecurityTerm::new(REDUCTION_LABEL, error)
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct JaggedOpening<F, EF, Proof> {
reductions: Vec<JaggedProof<F, EF>>,
dense: Proof,
}
impl<F, EF, Proof> JaggedOpening<F, EF, Proof> {
#[must_use]
pub fn reductions(&self) -> &[JaggedProof<F, EF>] {
&self.reductions
}
#[must_use]
pub const fn dense(&self) -> &Proof {
&self.dense
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{EF, F, challenger};
fn first_coordinate_after_binding(layout: &JaggedLayout) -> EF {
let mut challenger = challenger();
let bound = BoundJaggedLayout::new::<F, _>(layout, &mut challenger);
bound.sample_point::<F, EF, _>(&mut challenger).row()[0]
}
#[test]
fn sealing_a_transcript_to_one_geometry_excludes_every_other() {
let layout = JaggedLayout::new(3, &[3, 0, 5, 1]).unwrap();
let baseline = first_coordinate_after_binding(&layout);
assert_eq!(baseline, first_coordinate_after_binding(&layout));
let moved = JaggedLayout::new(3, &[2, 1, 5, 1]).unwrap();
assert_eq!(moved.dense_variables(), layout.dense_variables());
assert_eq!(moved.area(), layout.area());
assert_ne!(baseline, first_coordinate_after_binding(&moved));
let taller = JaggedLayout::new(4, &[3, 0, 5, 1]).unwrap();
assert_ne!(baseline, first_coordinate_after_binding(&taller));
}
#[test]
fn a_drawn_point_has_the_coordinates_the_geometry_addresses() {
let layout = JaggedLayout::new(5, &[3, 0, 5, 1]).unwrap();
let mut challenger = challenger();
let point = BoundJaggedLayout::new::<F, _>(&layout, &mut challenger)
.sample_point::<F, EF, _>(&mut challenger);
assert_eq!(point.row().num_variables(), 5);
assert_eq!(point.column().num_variables(), 2);
assert_ne!(point.row()[0], point.column()[0]);
}
#[test]
fn the_dense_opening_reads_one_column_once_per_claim() {
let layout = JaggedLayout::new(3, &[3, 0, 5, 1]).unwrap();
let protocol = OpeningProtocol::from(JaggedOpeningShape::new(&layout, 3));
assert_eq!(protocol.table_shapes(), vec![TableShape::new(4, 1)]);
assert_eq!(protocol.num_openings(), 3);
for (table, batch) in protocol.iter_openings() {
assert_eq!(table, 0);
assert_eq!(batch.current(), &[0]);
assert!(batch.next().is_empty());
}
}
#[test]
fn the_reduction_term_prices_every_challenge_the_claims_draw() {
let field_bits = (EF::bits() - 1) as f64;
assert_eq!(
reduction_term::<EF>(4, 1).bits.bits(),
field_bits - 3.0,
"one claim over four variables"
);
assert_eq!(
reduction_term::<EF>(4, 4).bits.bits(),
field_bits - 5.0,
"four claims over four variables"
);
assert!(reduction_term::<EF>(0, 1).bits.bits().is_infinite());
}
}