use alloc::vec;
use alloc::vec::Vec;
use p3_binary_dft::EncodableLevel;
use p3_binary_field::{PackedGf2, TowerLevel, Underlier};
use p3_challenger::fs::TranscriptField;
use p3_challenger::{CanObserve, CanSampleUniformBits, FieldChallenger, GrindingChallenger};
use p3_commit::{Mmcs, MultilinearPcs};
use p3_field::Field;
use p3_multilinear_util::point::Point;
use p3_security::SecurityTerm;
use p3_security::multilinear::{
bit_ring_switch_claim_batching_term, bit_ring_switch_tensors_term, bit_ring_switch_term,
};
use p3_sumcheck::layout::{Layout, SuffixProver};
use p3_sumcheck::ring_switch::bits::{
BitPacking, BitPackingView, BitRingSwitch, BitRingSwitchClaims, BitRingSwitchClaimsProof,
BitRingSwitchProofError,
};
use p3_sumcheck::{
OpeningBatch, OpeningProtocol, PrescribedOpeningSecurity, PrescribedPointPcs, TableShape,
TableSpec,
};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::error::BinaryPcsError;
use crate::fold::{ChallengeField, FoldAlphabet};
use crate::packing::{Coordinates, PackError, PackedStack};
use crate::params::{BinaryPcsConfig, BinaryPcsConfigError};
use crate::pcs::BinaryPcs;
use crate::proof::BinaryPcsProof;
use crate::prover::BinaryPcsProverData;
pub trait BooleanBackend<EF> {
type Commitment: Clone + Serialize + DeserializeOwned;
type ProverData;
type Proof: Clone + Serialize + DeserializeOwned;
type Error: core::fmt::Debug;
fn num_variables(&self) -> usize;
}
pub trait BooleanMultilinearPcs<EF, Challenger>: BooleanBackend<EF> {
fn observe_commitment(&self, commitment: &Self::Commitment, challenger: &mut Challenger);
fn commit_bits<U: Underlier>(
&self,
bits: &[PackedGf2<U>],
challenger: &mut Challenger,
) -> Result<(Self::Commitment, Self::ProverData), Self::Error>;
#[allow(clippy::type_complexity)]
fn open_readings(
&self,
prover_data: Self::ProverData,
openings: &[BitOpening<EF>],
challenger: &mut Challenger,
) -> Result<(Vec<BitReadings<EF>>, Self::Proof), Self::Error>;
fn verify_readings(
&self,
commitment: &Self::Commitment,
openings: &[BitOpening<EF>],
readings: &[BitReadings<EF>],
proof: &Self::Proof,
challenger: &mut Challenger,
) -> Result<(), Self::Error>;
fn readings_security(
&self,
num_claims: usize,
successor_tensors: bool,
) -> Option<PrescribedOpeningSecurity>;
fn open_at_points(
&self,
prover_data: Self::ProverData,
points: &[Point<EF>],
challenger: &mut Challenger,
) -> Result<(Vec<EF>, Self::Proof), Self::Error>;
fn verify_at_points(
&self,
commitment: &Self::Commitment,
points: &[Point<EF>],
values: &[EF],
proof: &Self::Proof,
challenger: &mut Challenger,
) -> Result<(), Self::Error>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BitOpening<EF> {
pub point: Point<EF>,
pub row_variables: usize,
pub current: bool,
pub next: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BitReadings<EF> {
pub current: Option<EF>,
pub next: Option<EF>,
}
pub struct BooleanPcs<EF: EncodableLevel, MT, MX> {
inner: BinaryPcs<EF, EF, MT, MX>,
config: BinaryPcsConfig,
num_variables: usize,
}
impl<EF, MT, MX> BooleanPcs<EF, MT, MX>
where
EF: ChallengeField<EF>
+ EncodableLevel
+ TranscriptField
+ TowerLevel
+ FoldAlphabet<EF>
+ Coordinates,
MT: Mmcs<EF>,
MX: Mmcs<EF, Error = MT::Error>,
{
pub fn new(
config: BinaryPcsConfig,
mmcs: MT,
round_mmcs: MX,
num_variables: usize,
) -> Result<Self, BooleanPcsError<EF, MT::Error>> {
let packed = Self::packed_variables(num_variables)?;
if config.num_variables() != packed {
return Err(BooleanPcsError::ConfigArity {
expected: packed,
actual: config.num_variables(),
});
}
Ok(Self {
inner: BinaryPcs::new(config, mmcs, round_mmcs).map_err(BooleanPcsError::Config)?,
config,
num_variables,
})
}
fn packed_variables(num_variables: usize) -> Result<usize, BooleanPcsError<EF, MT::Error>> {
let absorbed = BitRingSwitch::<EF>::ABSORBED;
num_variables
.checked_sub(absorbed)
.ok_or(BooleanPcsError::WitnessTooNarrow {
needed: absorbed,
actual: num_variables,
})
}
#[must_use]
pub const fn num_variables(&self) -> usize {
self.num_variables
}
fn protocol(&self) -> OpeningProtocol {
OpeningProtocol::new(vec![TableSpec::new(
TableShape::new(self.inner.num_variables(), 1),
vec![OpeningBatch::new(vec![0], Vec::new())],
)])
}
#[must_use]
pub fn reduction_security(&self, num_claims: usize) -> [SecurityTerm; 2] {
[
bit_ring_switch_term(
num_claims.min(1),
BitRingSwitch::<EF>::ABSORBED,
self.inner.num_variables(),
EF::bits(),
),
bit_ring_switch_claim_batching_term(num_claims, EF::bits()),
]
}
#[must_use]
pub fn opening_security(&self, num_claims: usize) -> PrescribedOpeningSecurity {
self.readings_security(num_claims, false)
}
#[must_use]
pub fn readings_security(
&self,
num_claims: usize,
successor_tensors: bool,
) -> PrescribedOpeningSecurity {
let num_tensors = if successor_tensors { 3 } else { 1 };
let mut terms = vec![
self.config
.security_regime()
.opening_term(num_claims.min(1)),
bit_ring_switch_tensors_term(
num_claims.min(1),
num_tensors,
BitRingSwitch::<EF>::ABSORBED,
self.inner.num_variables(),
EF::bits(),
),
];
if num_claims > 1 {
terms.push(bit_ring_switch_claim_batching_term(num_claims, EF::bits()));
}
PrescribedOpeningSecurity {
terms,
log2_max_candidates: 0.0,
}
}
fn packing(prover_data: &BinaryPcsProverData<EF, EF, MT>) -> BitPackingView<'_, EF> {
BitPacking::from_packed(prover_data.table(0).poly(0))
.expect("a committed table is a hypercube over a byte-aligned level")
}
fn check_openings(
&self,
openings: &[BitOpening<EF>],
) -> Result<(), BooleanPcsError<EF, MT::Error>> {
if openings.is_empty() {
return Err(BooleanPcsError::NoPoints);
}
for (index, opening) in openings.iter().enumerate() {
if opening.point.num_variables() != self.num_variables {
return Err(BooleanPcsError::PointArity {
expected: self.num_variables,
actual: opening.point.num_variables(),
});
}
if !opening.current && !opening.next {
return Err(BooleanPcsError::NoReading { index });
}
if opening.row_variables > self.num_variables {
return Err(BooleanPcsError::RowVariables {
index,
row_variables: opening.row_variables,
num_variables: self.num_variables,
});
}
}
Ok(())
}
fn claims(
openings: &[BitOpening<EF>],
) -> Result<BitRingSwitchClaims<EF>, BooleanPcsError<EF, MT::Error>> {
let reductions = openings
.iter()
.map(Self::reduction)
.collect::<Result<Vec<_>, _>>()?;
BitRingSwitchClaims::new(reductions).map_err(BooleanPcsError::Reduction)
}
fn reduction(
opening: &BitOpening<EF>,
) -> Result<BitRingSwitch<EF>, BooleanPcsError<EF, MT::Error>> {
if opening.next {
BitRingSwitch::with_successor(&opening.point, opening.row_variables)
} else {
BitRingSwitch::new(&opening.point)
}
.map_err(BooleanPcsError::Reduction)
}
fn current_openings(points: &[Point<EF>]) -> Vec<BitOpening<EF>> {
points
.iter()
.map(|point| BitOpening {
point: point.clone(),
row_variables: point.num_variables(),
current: true,
next: false,
})
.collect()
}
#[allow(clippy::type_complexity)]
pub fn open_readings<Challenger>(
&self,
prover_data: BinaryPcsProverData<EF, EF, MT>,
openings: &[BitOpening<EF>],
challenger: &mut Challenger,
) -> Result<(Vec<BitReadings<EF>>, BooleanProof<EF, MT, MX>), BooleanPcsError<EF, MT::Error>>
where
Challenger: FieldChallenger<EF>
+ GrindingChallenger<Witness = EF>
+ CanSampleUniformBits<EF>
+ CanObserve<MT::Commitment>
+ CanObserve<MX::Commitment>,
{
self.check_openings(openings)?;
let claims = Self::claims(openings)?;
let packing =
tracing::info_span!("read committed packing").in_scope(|| Self::packing(&prover_data));
let (reduction, surviving_point, surviving_value) = tracing::info_span!("bit ring switch")
.in_scope(|| {
claims.prove::<<EF as ChallengeField<EF>>::SumcheckRepr, _, _>(&packing, challenger)
});
let readings = openings
.iter()
.zip(claims.reductions())
.zip(&reduction.claims)
.map(|((opening, switch), elements)| {
let current = opening
.current
.then(|| switch.incoming_claim(&elements.tensor));
let next = opening
.next
.then(|| switch.successor_claim(&elements.tensor, elements.successor.as_ref()))
.transpose()
.map_err(BooleanPcsError::Reduction)?;
Ok(BitReadings { current, next })
})
.collect::<Result<Vec<_>, BooleanPcsError<EF, MT::Error>>>()?;
let opening = self
.inner
.try_open_at_known(
prover_data,
&self.protocol(),
&[surviving_point],
&[OpeningBatch::new(vec![surviving_value], Vec::new())],
challenger,
)
.map_err(BooleanPcsError::Commitment)?;
Ok((readings, BooleanProof { reduction, opening }))
}
pub fn verify_readings<Challenger>(
&self,
commitment: &MT::Commitment,
openings: &[BitOpening<EF>],
readings: &[BitReadings<EF>],
proof: &BooleanProof<EF, MT, MX>,
challenger: &mut Challenger,
) -> Result<(), BooleanPcsError<EF, MT::Error>>
where
Challenger: FieldChallenger<EF>
+ GrindingChallenger<Witness = EF>
+ CanSampleUniformBits<EF>
+ CanObserve<MT::Commitment>
+ CanObserve<MX::Commitment>,
{
self.check_openings(openings)?;
if readings.len() != openings.len() || proof.reduction.claims.len() != openings.len() {
return Err(BooleanPcsError::ClaimCount {
expected: openings.len(),
values: readings.len(),
reductions: proof.reduction.claims.len(),
});
}
if let Some(index) = openings
.iter()
.zip(readings)
.position(|(opening, reading)| {
reading.current.is_some() != opening.current
|| reading.next.is_some() != opening.next
})
{
return Err(BooleanPcsError::ReadingShape { index });
}
let claims = Self::claims(openings)?;
let readings = readings
.iter()
.map(|reading| (reading.current, reading.next))
.collect::<Vec<_>>();
let (surviving_point, surviving_value) = claims
.verify_readings(&proof.reduction, &readings, challenger)
.map_err(BooleanPcsError::ReductionProof)?;
let evals = self
.inner
.verify_at(
commitment,
&proof.opening,
&self.protocol(),
&[surviving_point],
challenger,
)
.map_err(BooleanPcsError::Commitment)?;
match evals.as_slice() {
[batch] if batch.current().first() == Some(&surviving_value) => Ok(()),
_ => Err(BooleanPcsError::SurvivingClaim),
}
}
}
impl<EF, MT, MX, Challenger> BooleanMultilinearPcs<EF, Challenger> for BooleanPcs<EF, MT, MX>
where
EF: ChallengeField<EF>
+ EncodableLevel
+ TranscriptField
+ TowerLevel
+ FoldAlphabet<EF>
+ Coordinates,
MT: Mmcs<EF>,
MX: Mmcs<EF, Error = MT::Error>,
Challenger: FieldChallenger<EF>
+ GrindingChallenger<Witness = EF>
+ CanSampleUniformBits<EF>
+ CanObserve<MT::Commitment>
+ CanObserve<MX::Commitment>,
{
fn observe_commitment(&self, commitment: &Self::Commitment, challenger: &mut Challenger) {
self.inner.observe_commitment(commitment, challenger);
}
fn open_readings(
&self,
prover_data: Self::ProverData,
openings: &[BitOpening<EF>],
challenger: &mut Challenger,
) -> Result<(Vec<BitReadings<EF>>, Self::Proof), Self::Error> {
Self::open_readings(self, prover_data, openings, challenger)
}
fn verify_readings(
&self,
commitment: &Self::Commitment,
openings: &[BitOpening<EF>],
readings: &[BitReadings<EF>],
proof: &Self::Proof,
challenger: &mut Challenger,
) -> Result<(), Self::Error> {
Self::verify_readings(self, commitment, openings, readings, proof, challenger)
}
fn readings_security(
&self,
num_claims: usize,
successor_tensors: bool,
) -> Option<PrescribedOpeningSecurity> {
Some(Self::readings_security(self, num_claims, successor_tensors))
}
fn commit_bits<U: Underlier>(
&self,
bits: &[PackedGf2<U>],
challenger: &mut Challenger,
) -> Result<(Self::Commitment, Self::ProverData), Self::Error> {
let stack = PackedStack::<PackedGf2<U>, EF>::from_columns(&[bits])?;
if stack.column_num_variables() != self.inner.num_variables() {
return Err(BooleanPcsError::WitnessArity {
expected: self.inner.num_variables(),
actual: stack.column_num_variables(),
});
}
let witness = SuffixProver::<EF, EF>::new_witness(vec![stack.into_table()], 0);
self.inner
.commit(witness, challenger)
.map_err(BooleanPcsError::Commitment)
}
fn open_at_points(
&self,
prover_data: Self::ProverData,
points: &[Point<EF>],
challenger: &mut Challenger,
) -> Result<(Vec<EF>, Self::Proof), Self::Error> {
let (readings, proof) =
self.open_readings(prover_data, &Self::current_openings(points), challenger)?;
let values = readings
.into_iter()
.map(|reading| {
reading
.current
.expect("an opening asking for the current reading carries it")
})
.collect();
Ok((values, proof))
}
fn verify_at_points(
&self,
commitment: &Self::Commitment,
points: &[Point<EF>],
values: &[EF],
proof: &Self::Proof,
challenger: &mut Challenger,
) -> Result<(), Self::Error> {
let readings: Vec<BitReadings<EF>> = values
.iter()
.map(|&value| BitReadings {
current: Some(value),
next: None,
})
.collect();
self.verify_readings(
commitment,
&Self::current_openings(points),
&readings,
proof,
challenger,
)
}
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(bound(
serialize = "EF: TowerLevel, MT::Commitment: Serialize, MT::MultiProof: Serialize, MX::Commitment: Serialize, MX::MultiProof: Serialize",
deserialize = "EF: TowerLevel, MT::Commitment: Deserialize<'de>, MT::MultiProof: Deserialize<'de>, MX::Commitment: Deserialize<'de>, MX::MultiProof: Deserialize<'de>"
))]
pub struct BooleanProof<EF: Field, MT: Mmcs<EF>, MX: Mmcs<EF>> {
pub reduction: BitRingSwitchClaimsProof<EF>,
pub opening: BinaryPcsProof<EF, EF, MT, MX>,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum BooleanPcsError<EF, MmcsError> {
#[error("a bit witness of {actual} variables cannot absorb {needed} into one element")]
WitnessTooNarrow {
needed: usize,
actual: usize,
},
#[error(transparent)]
Config(BinaryPcsConfigError),
#[error("the schedule commits {actual} variables, the packing holds {expected}")]
ConfigArity {
expected: usize,
actual: usize,
},
#[error("the packed witness has {actual} variables, expected {expected}")]
WitnessArity {
expected: usize,
actual: usize,
},
#[error("an opening needs at least one point")]
NoPoints,
#[error("{expected} points against {values} values and {reductions} reductions")]
ClaimCount {
expected: usize,
values: usize,
reductions: usize,
},
#[error("the opening point names {actual} variables, expected {expected}")]
PointArity {
expected: usize,
actual: usize,
},
#[error("opening {index} asks for neither the current nor the successor reading")]
NoReading {
index: usize,
},
#[error("opening {index} names {row_variables} row variables, the witness {num_variables}")]
RowVariables {
index: usize,
row_variables: usize,
num_variables: usize,
},
#[error("the readings at opening {index} are not the ones it asks for")]
ReadingShape {
index: usize,
},
#[error(transparent)]
Packing(#[from] PackError),
#[error(transparent)]
Reduction(p3_sumcheck::ring_switch::bits::BitRingSwitchError),
#[error(transparent)]
ReductionProof(BitRingSwitchProofError),
#[error(transparent)]
Commitment(BinaryPcsError<EF, MmcsError>),
#[error("the opened value is not the claim the reduction left behind")]
SurvivingClaim,
}
impl<EF, MT, MX> BooleanBackend<EF> for BooleanPcs<EF, MT, MX>
where
EF: EncodableLevel + TowerLevel,
MT: Mmcs<EF>,
MX: Mmcs<EF, Error = MT::Error>,
{
type Commitment = MT::Commitment;
type ProverData = BinaryPcsProverData<EF, EF, MT>;
type Proof = BooleanProof<EF, MT, MX>;
type Error = BooleanPcsError<EF, MT::Error>;
fn num_variables(&self) -> usize {
self.num_variables
}
}
#[cfg(test)]
mod tests {
use p3_binary_field::{BinaryField64, BinaryField128, Gf2, Ghash128, PackedGf2x64};
use p3_field::PrimeCharacteristicRing;
use p3_multilinear_util::poly::Poly;
use proptest::prelude::*;
use rand::rngs::SmallRng;
use rand::{RngExt, SeedableRng};
use super::*;
use crate::params::BinaryPcsParams;
use crate::test_util::{MyChallenger, MyMmcs, challenger, mmcs};
fn replayed(
pcs: &BooleanPcs<EF, MyMmcs, MyMmcs>,
commitment: &<MyMmcs as Mmcs<EF>>::Commitment,
) -> MyChallenger {
let mut challenger = challenger();
pcs.observe_commitment(commitment, &mut challenger);
challenger
}
type EF = BinaryField128;
const fn blocks(log_bits: usize) -> usize {
1 << (log_bits - 6)
}
fn boolean_pcs(log_bits: usize) -> BooleanPcs<EF, MyMmcs, MyMmcs> {
let params = BinaryPcsParams {
log_inv_rate: 2,
pow_bits: 0,
security_level: 40,
};
let config = BinaryPcsConfig::try_new::<EF, EF>(log_bits - 7, params).unwrap();
BooleanPcs::new(config, mmcs(), mmcs(), log_bits).unwrap()
}
fn witness(seed: u64, log_bits: usize) -> Vec<PackedGf2x64> {
let mut rng = SmallRng::seed_from_u64(seed);
(0..blocks(log_bits))
.map(|_| PackedGf2x64::new(rng.random::<u64>()))
.collect()
}
fn embedded(bits: &[PackedGf2x64]) -> Poly<EF> {
Poly::new(
bits.iter()
.flat_map(|block| {
(0..PackedGf2x64::WIDTH).map(move |lane| {
if block.get(lane) == Gf2::ONE {
EF::ONE
} else {
EF::ZERO
}
})
})
.collect::<Vec<EF>>(),
)
}
#[test]
fn a_boolean_opening_round_trips_at_the_value_the_bits_define() {
const LOG_BITS: usize = 13;
let bits = witness(0xB001, LOG_BITS);
let pcs = boolean_pcs(LOG_BITS);
assert_eq!(pcs.num_variables(), LOG_BITS);
let mut prover_chal = challenger();
let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
let point = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0xB002), LOG_BITS);
let points = alloc::vec![point.clone()];
let (values, proof) = pcs.open_at_points(data, &points, &mut prover_chal).unwrap();
assert_eq!(values, alloc::vec![embedded(&bits).eval_base(&point)]);
pcs.verify_at_points(
&commitment,
&points,
&values,
&proof,
&mut replayed(&pcs, &commitment),
)
.unwrap();
}
#[test]
fn several_points_cost_one_commitment_opening() {
const LOG_BITS: usize = 13;
const NUM_POINTS: usize = 4;
let bits = witness(0xB00F, LOG_BITS);
let pcs = boolean_pcs(LOG_BITS);
let mut rng = SmallRng::seed_from_u64(0xB010);
let points: Vec<Point<EF>> = (0..NUM_POINTS)
.map(|_| Point::<EF>::rand(&mut rng, LOG_BITS))
.collect();
let mut prover_chal = challenger();
let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
let (values, proof) = pcs.open_at_points(data, &points, &mut prover_chal).unwrap();
assert_eq!(proof.reduction.claims.len(), NUM_POINTS);
assert_eq!(proof.opening.evals.len(), 1);
let reference = embedded(&bits);
for (point, &value) in points.iter().zip(&values) {
assert_eq!(value, reference.eval_base(point));
}
pcs.verify_at_points(
&commitment,
&points,
&values,
&proof,
&mut replayed(&pcs, &commitment),
)
.unwrap();
for index in 0..NUM_POINTS {
let mut tampered = values.clone();
tampered[index] += EF::ONE;
let refused = pcs
.verify_at_points(
&commitment,
&points,
&tampered,
&proof,
&mut replayed(&pcs, &commitment),
)
.unwrap_err();
assert!(
matches!(
refused,
BooleanPcsError::ReductionProof(BitRingSwitchProofError::ClaimMismatch)
),
"value {index}: {refused:?}"
);
}
let evidence = pcs.opening_security(NUM_POINTS);
let labels: Vec<&str> = evidence.terms.iter().map(|term| term.label).collect();
assert_eq!(
labels,
alloc::vec![
p3_security::binary::BINARY_PCS_OPENING_LABEL,
p3_security::BIT_RING_SWITCH_LABEL,
p3_security::BIT_RING_SWITCH_CLAIM_BATCHING_LABEL,
]
);
assert_eq!(
evidence.terms[1].bits.bits(),
p3_security::bit_ring_switch_error(1, 7, 6, 128).bits()
);
assert_eq!(
evidence.terms[2].bits.bits(),
p3_security::bit_ring_switch_claim_batching_error(NUM_POINTS, 128).bits()
);
assert_eq!(
evidence.terms[0].bits.bits(),
pcs.opening_security(1).terms[0].bits.bits()
);
}
#[test]
fn a_false_value_is_rejected() {
const LOG_BITS: usize = 13;
let bits = witness(0xFA15, LOG_BITS);
let pcs = boolean_pcs(LOG_BITS);
let mut prover_chal = challenger();
let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
let points = alloc::vec![Point::<EF>::rand(
&mut SmallRng::seed_from_u64(0xFA16),
LOG_BITS
)];
let (values, proof) = pcs.open_at_points(data, &points, &mut prover_chal).unwrap();
let false_values = alloc::vec![values[0] + EF::ONE];
let err = pcs
.verify_at_points(
&commitment,
&points,
&false_values,
&proof,
&mut replayed(&pcs, &commitment),
)
.unwrap_err();
assert!(matches!(err, BooleanPcsError::ReductionProof(_)), "{err:?}");
}
#[test]
fn a_batch_over_another_witness_leaves_a_claim_the_commitment_does_not_open() {
const LOG_BITS: usize = 13;
let pcs = boolean_pcs(LOG_BITS);
let mut rng = SmallRng::seed_from_u64(0x0A11);
let points = (0..3)
.map(|_| Point::<EF>::rand(&mut rng, LOG_BITS))
.collect::<Vec<_>>();
let (_, data_a) = pcs
.commit_bits(&witness(0xAAAA, LOG_BITS), &mut challenger())
.unwrap();
let mut chal = challenger();
let (root_b, data_b) = pcs
.commit_bits(&witness(0xBBBB, LOG_BITS), &mut chal)
.unwrap();
let packing_a = BooleanPcs::<EF, MyMmcs, MyMmcs>::packing(&data_a);
let claims = BitRingSwitchClaims::new(
points
.iter()
.map(|point| BitRingSwitch::new(point).unwrap())
.collect(),
)
.unwrap();
let (reduction, surviving_point, surviving_value) =
claims.prove::<Ghash128, _, _>(&packing_a, &mut chal);
let values = claims
.reductions()
.iter()
.zip(&reduction.claims)
.map(|(switch, elements)| switch.incoming_claim(&elements.tensor))
.collect::<Vec<_>>();
let opening = pcs
.inner
.try_open_at(data_b, &pcs.protocol(), &[surviving_point], &mut chal)
.unwrap();
assert_ne!(opening.evals[0].current()[0], surviving_value);
let proof = BooleanProof { reduction, opening };
let err = pcs
.verify_at_points(
&root_b,
&points,
&values,
&proof,
&mut replayed(&pcs, &root_b),
)
.unwrap_err();
assert!(matches!(err, BooleanPcsError::SurvivingClaim), "{err:?}");
let mut honest = challenger();
let (root, data) = pcs
.commit_bits(&witness(0xBBBB, LOG_BITS), &mut honest)
.unwrap();
let (honest_values, honest_proof) = pcs.open_at_points(data, &points, &mut honest).unwrap();
pcs.verify_at_points(
&root,
&points,
&honest_values,
&honest_proof,
&mut replayed(&pcs, &root),
)
.unwrap();
}
#[test]
fn a_shape_the_commitment_does_not_hold_is_refused() {
let params = BinaryPcsParams {
log_inv_rate: 2,
pow_bits: 0,
security_level: 40,
};
let config = BinaryPcsConfig::try_new::<EF, EF>(6, params).unwrap();
let narrow_witness = BooleanPcs::<EF, MyMmcs, MyMmcs>::new(config, mmcs(), mmcs(), 4).err();
assert!(matches!(
narrow_witness,
Some(BooleanPcsError::WitnessTooNarrow {
needed: 7,
actual: 4
})
));
let config = BinaryPcsConfig::try_new::<BinaryField64, EF>(6, params).unwrap();
let wrong_field = BooleanPcs::<EF, MyMmcs, MyMmcs>::new(config, mmcs(), mmcs(), 13).err();
assert!(matches!(
wrong_field,
Some(BooleanPcsError::Config(
BinaryPcsConfigError::CommittedFieldMismatch {
derived: 64,
actual: 128,
}
))
));
let config = BinaryPcsConfig::try_new::<EF, EF>(6, params).unwrap();
let wrong_arity = BooleanPcs::<EF, MyMmcs, MyMmcs>::new(config, mmcs(), mmcs(), 14).err();
assert!(matches!(
wrong_arity,
Some(BooleanPcsError::ConfigArity {
expected: 7,
actual: 6
})
));
let pcs = boolean_pcs(13);
let mut chal = challenger();
let (commitment, data) = pcs.commit_bits(&witness(0x5417, 13), &mut chal).unwrap();
let narrow = alloc::vec![Point::<EF>::rand(&mut SmallRng::seed_from_u64(0x5418), 12)];
assert!(matches!(
pcs.open_at_points(data, &narrow, &mut chal).err(),
Some(BooleanPcsError::PointArity {
expected: 13,
actual: 12
})
));
assert!(matches!(
pcs.verify_at_points(&commitment, &narrow, &[EF::ZERO], &proof_stub(), &mut chal),
Err(BooleanPcsError::PointArity { .. })
));
let (_, data) = pcs.commit_bits(&witness(0x5419, 13), &mut chal).unwrap();
assert!(matches!(
pcs.open_at_points(data, &[], &mut chal).err(),
Some(BooleanPcsError::NoPoints)
));
}
fn proof_stub() -> BooleanProof<EF, MyMmcs, MyMmcs> {
let bits = witness(0x57AB, 13);
let pcs = boolean_pcs(13);
let mut chal = challenger();
let (_, data) = pcs.commit_bits(&bits, &mut chal).unwrap();
let points = alloc::vec![Point::<EF>::rand(&mut SmallRng::seed_from_u64(0x57AC), 13)];
pcs.open_at_points(data, &points, &mut chal).unwrap().1
}
fn dense_successor_reading(cells: &Poly<EF>, point: &Point<EF>, row_variables: usize) -> EF {
let (selector, rho) = point.split_at(point.num_variables() - row_variables);
let eq_selector = Poly::<EF>::new_from_point(selector.as_slice(), EF::ONE);
let eq_rho = Poly::<EF>::new_from_point(rho.as_slice(), EF::ONE);
let rows = 1usize << row_variables;
let mut reading = EF::ZERO;
for (c, &gate) in eq_selector.as_slice().iter().enumerate() {
for (z, &weight) in eq_rho.as_slice().iter().enumerate() {
let x = (z + 1).min(rows - 1);
reading += gate * weight * cells.as_slice()[c * rows + x];
}
}
reading
}
fn mixed_openings(seed: u64, log_bits: usize) -> Vec<BitOpening<EF>> {
let mut rng = SmallRng::seed_from_u64(seed);
[(log_bits, true, false), (10, false, true), (3, true, true)]
.into_iter()
.map(|(row_variables, current, next)| BitOpening {
point: Point::<EF>::rand(&mut rng, log_bits),
row_variables,
current,
next,
})
.collect()
}
#[test]
fn a_successor_reading_round_trips_at_the_value_the_bits_define() {
const LOG_BITS: usize = 13;
let bits = witness(0x5ACC, LOG_BITS);
let reference = embedded(&bits);
let pcs = boolean_pcs(LOG_BITS);
let point = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0x5ACD), LOG_BITS);
let mut committed = challenger();
let (commitment, data) = pcs.commit_bits(&bits, &mut committed).unwrap();
for row_variables in [0, 3, 7, 10, 13] {
let openings = alloc::vec![BitOpening {
point: point.clone(),
row_variables,
current: true,
next: true,
}];
let mut prover_chal = committed.clone();
let (readings, proof) = pcs
.open_readings(data.clone(), &openings, &mut prover_chal)
.unwrap();
assert_eq!(
readings,
alloc::vec![BitReadings {
current: Some(reference.eval_base(&point)),
next: Some(dense_successor_reading(&reference, &point, row_variables)),
}],
"rows {row_variables}"
);
assert_eq!(
proof.reduction.claims[0].successor.is_some(),
row_variables > BitRingSwitch::<EF>::ABSORBED,
"rows {row_variables}"
);
pcs.verify_readings(
&commitment,
&openings,
&readings,
&proof,
&mut replayed(&pcs, &commitment),
)
.unwrap();
}
}
#[test]
fn several_openings_with_mixed_views_cost_one_commitment_opening() {
const LOG_BITS: usize = 13;
let bits = witness(0x3A1D, LOG_BITS);
let reference = embedded(&bits);
let pcs = boolean_pcs(LOG_BITS);
let openings = mixed_openings(0x3A1E, LOG_BITS);
let mut prover_chal = challenger();
let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
let (readings, proof) = pcs
.open_readings(data, &openings, &mut prover_chal)
.unwrap();
assert_eq!(proof.reduction.claims.len(), 3);
assert_eq!(proof.opening.evals.len(), 1);
for batch in &proof.opening.evals {
assert_eq!(batch.current().len(), 1);
assert!(batch.next().is_empty());
}
for (index, (opening, reading)) in openings.iter().zip(&readings).enumerate() {
let current = opening.current.then(|| reference.eval_base(&opening.point));
let next = opening.next.then(|| {
dense_successor_reading(&reference, &opening.point, opening.row_variables)
});
assert_eq!(*reading, BitReadings { current, next }, "opening {index}");
}
pcs.verify_readings(
&commitment,
&openings,
&readings,
&proof,
&mut replayed(&pcs, &commitment),
)
.unwrap();
}
#[test]
fn a_false_successor_reading_is_rejected() {
const LOG_BITS: usize = 13;
let bits = witness(0xFA17, LOG_BITS);
let pcs = boolean_pcs(LOG_BITS);
let point = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0xFA18), LOG_BITS);
for row_variables in [3, 10] {
let mut prover_chal = challenger();
let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
let openings = alloc::vec![BitOpening {
point: point.clone(),
row_variables,
current: true,
next: true,
}];
let (readings, proof) = pcs
.open_readings(data, &openings, &mut prover_chal)
.unwrap();
let false_readings = alloc::vec![BitReadings {
next: readings[0].next.map(|next| next + EF::ONE),
..readings[0]
}];
let err = pcs
.verify_readings(
&commitment,
&openings,
&false_readings,
&proof,
&mut replayed(&pcs, &commitment),
)
.unwrap_err();
assert!(
matches!(
err,
BooleanPcsError::ReductionProof(
BitRingSwitchProofError::SuccessorClaimMismatch
)
),
"rows {row_variables}: {err:?}"
);
}
}
#[test]
fn the_point_api_is_the_current_reading_api() {
const LOG_BITS: usize = 13;
const NUM_POINTS: usize = 3;
let bits = witness(0x9017, LOG_BITS);
let pcs = boolean_pcs(LOG_BITS);
let mut rng = SmallRng::seed_from_u64(0x9018);
let points: Vec<Point<EF>> = (0..NUM_POINTS)
.map(|_| Point::<EF>::rand(&mut rng, LOG_BITS))
.collect();
let openings: Vec<BitOpening<EF>> = points
.iter()
.map(|point| BitOpening {
point: point.clone(),
row_variables: point.num_variables(),
current: true,
next: false,
})
.collect();
let mut committed = challenger();
let (commitment, data) = pcs.commit_bits(&bits, &mut committed).unwrap();
let mut point_chal = committed.clone();
let (values, point_proof) = pcs
.open_at_points(data.clone(), &points, &mut point_chal)
.unwrap();
let mut reading_chal = committed;
let (readings, reading_proof) = pcs
.open_readings(data, &openings, &mut reading_chal)
.unwrap();
assert_eq!(
postcard::to_allocvec(&point_proof).unwrap(),
postcard::to_allocvec(&reading_proof).unwrap()
);
let expected: Vec<BitReadings<EF>> = values
.iter()
.map(|&value| BitReadings {
current: Some(value),
next: None,
})
.collect();
assert_eq!(readings, expected);
assert_eq!(
point_chal.sample_algebra_element::<EF>(),
reading_chal.sample_algebra_element::<EF>()
);
let mut point_verifier = replayed(&pcs, &commitment);
pcs.verify_at_points(
&commitment,
&points,
&values,
&point_proof,
&mut point_verifier,
)
.unwrap();
let mut reading_verifier = replayed(&pcs, &commitment);
pcs.verify_readings(
&commitment,
&openings,
&readings,
&reading_proof,
&mut reading_verifier,
)
.unwrap();
assert_eq!(
point_verifier.sample_algebra_element::<EF>(),
reading_verifier.sample_algebra_element::<EF>()
);
}
#[test]
fn successor_tensors_are_priced() {
let pcs = boolean_pcs(13);
let with = pcs.readings_security(1, true);
let without = pcs.readings_security(1, false);
assert_eq!(with.terms[0], without.terms[0]);
assert_eq!(with.terms[1].label, p3_security::BIT_RING_SWITCH_LABEL);
assert!(with.terms[1].bits.bits() < without.terms[1].bits.bits());
assert_eq!(
with.terms[1].bits.bits(),
p3_security::bit_ring_switch_tensors_error(1, 3, 7, 6, 128).bits()
);
let points = pcs.opening_security(1);
assert_eq!(points.terms, without.terms);
assert_eq!(points.log2_max_candidates, without.log2_max_candidates);
}
#[test]
fn a_reading_shape_the_opening_did_not_ask_for_is_refused() {
const LOG_BITS: usize = 13;
let pcs = boolean_pcs(LOG_BITS);
let openings = mixed_openings(0x54A9, LOG_BITS);
let mut prover_chal = challenger();
let (commitment, data) = pcs
.commit_bits(&witness(0x5AA0, LOG_BITS), &mut prover_chal)
.unwrap();
let (readings, proof) = pcs
.open_readings(data, &openings, &mut prover_chal)
.unwrap();
let stray = EF::ONE;
let mutations = [
(
0,
BitReadings {
next: Some(stray),
..readings[0]
},
),
(
0,
BitReadings {
current: None,
..readings[0]
},
),
(
1,
BitReadings {
current: Some(stray),
..readings[1]
},
),
(
2,
BitReadings {
next: None,
..readings[2]
},
),
];
for (index, reading) in mutations {
let mut tampered = readings.clone();
tampered[index] = reading;
let mut verifier = replayed(&pcs, &commitment);
let err = pcs
.verify_readings(&commitment, &openings, &tampered, &proof, &mut verifier)
.unwrap_err();
assert!(
matches!(err, BooleanPcsError::ReadingShape { index: refused } if refused == index),
"{err:?}"
);
assert_eq!(
verifier.sample_algebra_element::<EF>(),
replayed(&pcs, &commitment).sample_algebra_element::<EF>(),
"opening {index}"
);
}
}
#[test]
fn an_opening_the_witness_cannot_answer_is_refused() {
const LOG_BITS: usize = 13;
let pcs = boolean_pcs(LOG_BITS);
let mut chal = challenger();
let (commitment, data) = pcs
.commit_bits(&witness(0xA5C0, LOG_BITS), &mut chal)
.unwrap();
let mut rng = SmallRng::seed_from_u64(0xA5C1);
let answered = BitOpening {
point: Point::<EF>::rand(&mut rng, LOG_BITS),
row_variables: LOG_BITS,
current: true,
next: false,
};
let silent = BitOpening {
point: Point::<EF>::rand(&mut rng, LOG_BITS),
row_variables: LOG_BITS,
current: false,
next: false,
};
let past_the_witness = BitOpening {
point: Point::<EF>::rand(&mut rng, LOG_BITS),
row_variables: LOG_BITS + 1,
current: false,
next: true,
};
let readings = [
BitReadings {
current: Some(EF::ZERO),
next: None,
},
BitReadings {
current: None,
next: Some(EF::ZERO),
},
];
let silent_openings = [answered.clone(), silent];
let wide_openings = [answered, past_the_witness];
let mut before = chal.clone();
let err = pcs
.open_readings(data.clone(), &silent_openings, &mut chal)
.err();
assert!(
matches!(err, Some(BooleanPcsError::NoReading { index: 1 })),
"{err:?}"
);
let err = pcs.open_readings(data, &wide_openings, &mut chal).err();
assert!(
matches!(
err,
Some(BooleanPcsError::RowVariables {
index: 1,
row_variables: 14,
num_variables: 13,
})
),
"{err:?}"
);
assert_eq!(
chal.sample_algebra_element::<EF>(),
before.sample_algebra_element::<EF>()
);
let proof = proof_stub();
let mut verifier = replayed(&pcs, &commitment);
let err = pcs
.verify_readings(
&commitment,
&silent_openings,
&readings,
&proof,
&mut verifier,
)
.unwrap_err();
assert!(
matches!(err, BooleanPcsError::NoReading { index: 1 }),
"{err:?}"
);
let err = pcs
.verify_readings(
&commitment,
&wide_openings,
&readings,
&proof,
&mut verifier,
)
.unwrap_err();
assert!(
matches!(err, BooleanPcsError::RowVariables { index: 1, .. }),
"{err:?}"
);
assert_eq!(
verifier.sample_algebra_element::<EF>(),
replayed(&pcs, &commitment).sample_algebra_element::<EF>()
);
}
proptest! {
#![proptest_config(ProptestConfig { cases: 8, ..ProptestConfig::default() })]
#[test]
fn a_boolean_opening_round_trips_over_random_inputs(
log_bits in 13usize..=15,
num_points in 1usize..=3,
witness_seed: u64,
point_seed: u64,
) {
let bits = witness(witness_seed, log_bits);
let pcs = boolean_pcs(log_bits);
let mut prover_chal = challenger();
let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
let mut rng = SmallRng::seed_from_u64(point_seed);
let points: Vec<Point<EF>> = (0..num_points)
.map(|_| Point::<EF>::rand(&mut rng, log_bits))
.collect();
let (values, proof) = pcs.open_at_points(data, &points, &mut prover_chal).unwrap();
let reference = embedded(&bits);
for (point, &value) in points.iter().zip(&values) {
prop_assert_eq!(value, reference.eval_base(point));
}
prop_assert!(
pcs.verify_at_points(
&commitment,
&points,
&values,
&proof,
&mut replayed(&pcs, &commitment)
)
.is_ok()
);
}
}
}