use alloc::vec::Vec;
use core::marker::PhantomData;
use p3_challenger::fs::{
DomainSeparator, FieldToFieldCodec, FieldUnit, Hierarchy, Interaction, InteractionPattern,
Kind, Length, ProverState, TranscriptField, VerifierState,
};
use p3_challenger::{CanObserve, CanSample, CanSampleUniformBits, GrindingChallenger};
use p3_field::{AlgebraIdentity, Field};
use p3_util::log2_strict_usize;
use crate::params::BinaryPcsConfig;
use crate::verifier::num_distinct_queries;
const VERSION: u8 = 1;
const NAME: &[u8] = b"p3-binary-pcs";
const ORACLE_COMMITMENT: &str = "oracle_commitment";
const FINAL_CODEWORD: &str = "final_codeword";
const QUERY_POW: &str = "query_pow";
const QUERY_INDICES: &str = "query_indices";
type Alphabet<F> = FieldUnit<F>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BinaryPcsShape {
pub num_variables: usize,
pub log_folding_factor: usize,
pub num_oracles: usize,
pub final_codeword_len: usize,
pub pow_bits: usize,
pub pair_bits: usize,
pub num_pairs: usize,
}
impl BinaryPcsShape {
#[must_use]
pub const fn new(config: &BinaryPcsConfig) -> Self {
let shift = config.log_folding_factor() - 1;
let pair_domain = config.domain_size() >> shift;
Self {
num_variables: config.num_variables(),
log_folding_factor: config.log_folding_factor(),
num_oracles: config.num_fold_batches() - 1,
final_codeword_len: 1 << config.log_final_len(),
pow_bits: config.pow_bits(),
pair_bits: log2_strict_usize(pair_domain / 2),
num_pairs: num_distinct_queries(pair_domain, config.num_queries()),
}
}
#[must_use]
pub fn pattern<F, EF>(&self) -> InteractionPattern
where
F: TranscriptField,
EF: Field + AlgebraIdentity<F>,
{
let mut steps = Vec::with_capacity(self.num_oracles + 3);
for _ in 0..self.num_oracles {
steps.push(Interaction::opaque(
Hierarchy::Atomic,
Kind::Message,
ORACLE_COMMITMENT,
Length::Scalar,
));
}
steps.push(Interaction::algebra::<F, EF>(
Hierarchy::Atomic,
Kind::Message,
FINAL_CODEWORD,
Length::Fixed(self.final_codeword_len),
));
if self.pow_bits > 0 {
steps.push(Interaction::algebra::<F, F>(
Hierarchy::Atomic,
Kind::Pow,
QUERY_POW,
Length::Fixed(self.pow_bits),
));
}
steps.push(Interaction::uniform_bits(
Hierarchy::Atomic,
Kind::Challenge,
QUERY_INDICES,
self.pair_bits,
Length::Fixed(self.num_pairs),
));
InteractionPattern::new(steps).expect("a flat sequence of leaf steps is always well formed")
}
#[must_use]
pub fn domain_separator<F, EF>(&self) -> DomainSeparator<Alphabet<F>>
where
F: TranscriptField,
EF: Field + AlgebraIdentity<F>,
{
let mut separator = DomainSeparator::new(VERSION, NAME, self.pattern::<F, EF>());
separator
.instance(&(self.num_variables as u64).to_be_bytes())
.instance(&(self.log_folding_factor as u64).to_be_bytes());
separator
}
}
pub struct BinaryPcsProverTranscript<'a, F: TranscriptField, EF, C> {
state: ProverState<&'a mut C, Alphabet<F>>,
shape: BinaryPcsShape,
_challenge: PhantomData<EF>,
}
impl<'a, F, EF, C> BinaryPcsProverTranscript<'a, F, EF, C>
where
F: TranscriptField,
EF: Field + AlgebraIdentity<F>,
C: CanObserve<F> + CanSample<F> + CanSampleUniformBits<F> + GrindingChallenger<Witness = F>,
{
pub fn new(challenger: &'a mut C, shape: BinaryPcsShape) -> Self {
let separator = shape.domain_separator::<F, EF>();
Self {
state: ProverState::new(challenger, &separator),
shape,
_challenge: PhantomData,
}
}
pub fn fold_batch<R>(&mut self, run: impl FnOnce(&mut C) -> R) -> R {
run(self.state.challenger_mut())
}
pub fn oracle_commitment<Com>(&mut self, commitment: Com)
where
Com: Clone,
C: CanObserve<Com>,
{
self.state.observe_opaque(ORACLE_COMMITMENT, commitment);
}
pub fn final_codeword(&mut self, codeword: &[EF]) {
assert_eq!(
codeword.len(),
self.shape.final_codeword_len,
"the final codeword must carry the described number of symbols",
);
let _bound = self
.state
.observe_extensions::<F, EF, FieldToFieldCodec<F>>(FINAL_CODEWORD, codeword);
}
pub fn query_pow(&mut self) -> F {
if self.shape.pow_bits == 0 {
return F::ZERO;
}
self.state.observe_pow(QUERY_POW, self.shape.pow_bits)
}
pub fn query_pairs(&mut self) -> Vec<usize> {
let kept = self.state.challenge_uniform_bits_rejecting::<F>(
QUERY_INDICES,
self.shape.pair_bits,
self.shape.num_pairs,
|candidate, kept| !kept.contains(&candidate),
);
sorted_pairs(kept)
}
pub fn abort(&mut self) {
self.state.abort();
}
pub fn finish(self) {
assert!(
self.state.finalize().is_empty(),
"a binary PCS run carries its own proof values",
);
}
}
pub struct BinaryPcsVerifierTranscript<'a, F: TranscriptField, EF, C> {
state: VerifierState<'static, &'a mut C, Alphabet<F>>,
shape: BinaryPcsShape,
_challenge: PhantomData<EF>,
}
impl<'a, F, EF, C> BinaryPcsVerifierTranscript<'a, F, EF, C>
where
F: TranscriptField,
EF: Field + AlgebraIdentity<F>,
C: CanObserve<F> + CanSample<F> + CanSampleUniformBits<F> + GrindingChallenger<Witness = F>,
{
pub fn new(challenger: &'a mut C, shape: BinaryPcsShape) -> Self {
let separator = shape.domain_separator::<F, EF>();
Self {
state: VerifierState::new(challenger, &separator, &[]),
shape,
_challenge: PhantomData,
}
}
pub fn fold_batch<R>(&mut self, run: impl FnOnce(&mut C) -> R) -> R {
run(self.state.challenger_mut())
}
pub fn oracle_commitment<Com>(&mut self, commitment: Com)
where
Com: Clone,
C: CanObserve<Com>,
{
self.state.observe_opaque(ORACLE_COMMITMENT, commitment);
}
pub fn final_codeword(&mut self, codeword: &[EF]) -> Result<(), TranscriptFailure<F>> {
if codeword.len() != self.shape.final_codeword_len {
self.state.abort();
return Err(TranscriptFailure::FinalCodewordLength {
expected: self.shape.final_codeword_len,
got: codeword.len(),
});
}
let _bound = self
.state
.observe_extensions::<F, EF, FieldToFieldCodec<F>>(FINAL_CODEWORD, codeword);
Ok(())
}
pub fn query_pow(&mut self, witness: F) -> Result<(), TranscriptFailure<F>> {
if self.shape.pow_bits == 0 {
if witness != F::ZERO {
self.state.abort();
return Err(TranscriptFailure::NonCanonicalPowWitness { actual: witness });
}
return Ok(());
}
self.state
.observe_pow(QUERY_POW, self.shape.pow_bits, witness)
.map_err(|_| TranscriptFailure::PowWitness {
bits: self.shape.pow_bits,
})
}
pub fn query_pairs(&mut self) -> Vec<usize> {
let kept = self.state.challenge_uniform_bits_rejecting::<F>(
QUERY_INDICES,
self.shape.pair_bits,
self.shape.num_pairs,
|candidate, kept| !kept.contains(&candidate),
);
sorted_pairs(kept)
}
pub fn abort(&mut self) {
self.state.abort();
}
pub fn finish(self) {
self.state
.finalize()
.expect("a binary PCS run reads an empty wire");
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum TranscriptFailure<F> {
#[error("final codeword length mismatch: expected {expected}, got {got}")]
FinalCodewordLength {
expected: usize,
got: usize,
},
#[error("query grinding witness clears fewer than {bits} bits")]
PowWitness {
bits: usize,
},
#[error("query grinding witness is {actual} at zero difficulty, expected zero")]
NonCanonicalPowWitness {
actual: F,
},
}
fn sorted_pairs(kept: Vec<p3_challenger::fs::TranscriptBound<usize>>) -> Vec<usize> {
let mut pairs: Vec<usize> = kept
.into_iter()
.map(p3_challenger::fs::TranscriptBound::into_inner)
.collect();
pairs.sort_unstable();
pairs.into_iter().map(|pair| pair << 1).collect()
}
#[cfg(test)]
mod tests {
use alloc::string::String;
use alloc::vec;
use p3_binary_field::BinaryField128;
use p3_challenger::CanSample;
use p3_challenger::fs::PROTOCOL_ID_LEN;
use p3_challenger::testing::{assert_seeds_pairwise_distinct, pow_difficulties, seed_digest};
use p3_field::PrimeCharacteristicRing;
use p3_security::grinding::{
UNPRICED_GRINDING_SITES, grinding_step, is_unpriced_grinding_site,
};
use super::*;
use crate::params::BinaryPcsParams;
const NAME_STR: &str = "p3-binary-pcs";
use crate::test_util::challenger;
fn base_config() -> BinaryPcsConfig {
BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(
8,
BinaryPcsParams {
log_inv_rate: 2,
pow_bits: 4,
security_level: 100,
},
)
.unwrap()
.try_with_folding(1)
.unwrap()
}
#[test]
fn every_knob_of_a_run_reaches_the_seed() {
let BinaryPcsShape {
num_variables: _,
log_folding_factor: _,
num_oracles: _,
final_codeword_len: _,
pow_bits: _,
pair_bits: _,
num_pairs: _,
} = BinaryPcsShape::new(&base_config());
let digest = |shape: BinaryPcsShape| {
seed_digest(&shape.domain_separator::<BinaryField128, BinaryField128>())
};
let base = BinaryPcsShape::new(&base_config());
let mut seeds = vec![(String::from("baseline"), digest(base))];
let mut wider = base;
wider.num_variables += 1;
seeds.push((String::from("num variables"), digest(wider)));
let mut coarser = base;
coarser.log_folding_factor += 1;
seeds.push((String::from("folding factor"), digest(coarser)));
let mut more_oracles = base;
more_oracles.num_oracles += 1;
seeds.push((String::from("oracle count"), digest(more_oracles)));
let mut longer_final = base;
longer_final.final_codeword_len += 1;
seeds.push((String::from("final codeword"), digest(longer_final)));
let mut harder = base;
harder.pow_bits += 1;
seeds.push((String::from("pow bits"), digest(harder)));
let mut narrower = base;
narrower.pair_bits -= 1;
seeds.push((String::from("pair bits"), digest(narrower)));
let mut more_pairs = base;
more_pairs.num_pairs += 1;
seeds.push((String::from("pair count"), digest(more_pairs)));
assert_seeds_pairwise_distinct(&seeds);
}
#[test]
fn a_zero_difficulty_run_describes_no_grinding_step() {
let mut unground = BinaryPcsShape::new(&base_config());
unground.pow_bits = 0;
assert!(pow_difficulties(&unground.pattern::<BinaryField128, BinaryField128>()).is_empty());
let ground = BinaryPcsShape::new(&base_config());
assert_eq!(
pow_difficulties(&ground.pattern::<BinaryField128, BinaryField128>()),
vec![(QUERY_POW, 4)]
);
}
#[test]
fn the_protocol_name_fits_the_identifier_and_extends_nothing() {
assert!(NAME.len() < PROTOCOL_ID_LEN - 1);
assert!(!NAME.starts_with(b"p3-binary-field"));
}
#[test]
fn the_grinding_sites_and_the_unpriced_table_name_each_other() {
let shape = BinaryPcsShape::new(&base_config());
for (label, _bits) in pow_difficulties(&shape.pattern::<BinaryField128, BinaryField128>()) {
let budgeted = grinding_step(NAME_STR, label).is_some();
let priced_elsewhere = is_unpriced_grinding_site(NAME_STR, label);
assert!(
budgeted || priced_elsewhere,
"p3-binary-pcs/{label} grinds, but no vocabulary classifies it",
);
assert!(
!(budgeted && priced_elsewhere),
"p3-binary-pcs/{label} is both compared against the model and priced elsewhere",
);
}
let described: Vec<&str> =
pow_difficulties(&shape.pattern::<BinaryField128, BinaryField128>())
.into_iter()
.map(|(label, _)| label)
.collect();
for &(protocol, label) in &UNPRICED_GRINDING_SITES {
if protocol != NAME_STR {
continue;
}
assert!(
described.contains(&label),
"p3-binary-pcs/{label} is listed as priced elsewhere, but no step describes it",
);
}
}
#[test]
fn a_perturbed_final_codeword_moves_the_state_the_caller_continues_from() {
let shape = BinaryPcsShape::new(&base_config());
let run = |codeword: &[BinaryField128]| {
let mut ch = challenger();
let mut transcript = BinaryPcsProverTranscript::new(&mut ch, shape);
for _ in 0..shape.num_oracles {
transcript.oracle_commitment(
p3_symmetric::MerkleCap::<BinaryField128, [u8; 32]>::new(vec![[0u8; 32]]),
);
}
transcript.final_codeword(codeword);
let _witness = transcript.query_pow();
let positions = transcript.query_pairs();
transcript.finish();
(positions, CanSample::<BinaryField128>::sample(&mut ch))
};
let honest = vec![BinaryField128::ZERO; shape.final_codeword_len];
let mut tampered = honest.clone();
*tampered.last_mut().unwrap() = BinaryField128::ONE;
assert_ne!(run(&honest), run(&tampered));
}
}