use alloc::vec::Vec;
use p3_binary_field::TowerLevel;
use p3_field::{ExtensionField, Field, batch_multiplicative_inverse};
use p3_maybe_rayon::prelude::*;
use p3_multilinear_util::poly::Poly;
use super::composition::Composition;
use super::domain::{SkipDomain, SkipDomainError};
use super::lde::{CHUNK_BITS, CompressedLde, CompressedLdeError, TABLE_ROWS};
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[error("round message length mismatch: expected {expected}, got {actual}")]
pub struct MessageLenMismatch {
pub expected: usize,
pub actual: usize,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum SkipRoundError {
#[error(transparent)]
Domain(#[from] SkipDomainError),
#[error(transparent)]
Lde(#[from] CompressedLdeError),
}
#[derive(Debug, Clone)]
pub struct SkipRound<F> {
domain: SkipDomain<F>,
lde: CompressedLde<F>,
derivative_on_extension: F,
}
impl<F: TowerLevel> SkipRound<F> {
pub fn new(log_size: usize, degree: usize) -> Result<Self, SkipRoundError> {
Self::from_domain(SkipDomain::for_degree(log_size, degree)?)
}
pub fn from_domain(domain: SkipDomain<F>) -> Result<Self, SkipRoundError> {
let lde = CompressedLde::new(&domain)?;
let derivative_on_extension = domain
.subspace()
.iter()
.chain(domain.transmitted())
.copied()
.filter(|point| !point.is_zero())
.product::<F>();
Ok(Self {
domain,
lde,
derivative_on_extension,
})
}
#[must_use]
pub const fn domain(&self) -> &SkipDomain<F> {
&self.domain
}
#[must_use]
pub const fn log_size(&self) -> usize {
self.domain.log_size()
}
#[must_use]
pub const fn num_transmitted(&self) -> usize {
self.domain.num_transmitted()
}
#[must_use]
pub const fn row_bytes(&self) -> usize {
self.lde.num_chunks()
}
pub fn extend_rows(&self, packed: &[u8], out: &mut [F])
where
F: Send + Sync,
{
self.lde.extend_batch(packed, out);
}
#[must_use]
pub fn stream_round_message<EF, C>(
&self,
operands: &[&[u8]],
eq: &[EF],
composition: &C,
) -> Vec<EF>
where
EF: ExtensionField<F>,
F: Send + Sync,
C: Composition<F> + Sync,
{
let arity = composition.arity();
assert_eq!(operands.len(), arity, "one packed witness per operand");
assert!(
self.domain.admits_degree(composition.degree()),
"the transmitted domain must carry the constraint's degree"
);
let stride = self.num_transmitted();
let row_bytes = self.row_bytes();
for rows in operands {
assert_eq!(
rows.len(),
eq.len() * row_bytes,
"one packed row per equality weight"
);
}
(0..eq.len())
.into_par_iter()
.par_fold_reduce(
|| {
(
EF::zero_vec(stride),
F::zero_vec(arity * stride),
alloc::vec![F::ZERO; arity],
)
},
|(mut message, mut extended, mut tuple), row| {
for (operand, rows) in operands.iter().enumerate() {
let packed = &rows[row * row_bytes..][..row_bytes];
self.lde
.extend(packed, &mut extended[operand * stride..][..stride]);
}
let weight = eq[row];
for (point, entry) in message.iter_mut().enumerate() {
for (operand, value) in tuple.iter_mut().enumerate() {
*value = extended[operand * stride + point];
}
*entry += weight * composition.eval(&tuple);
}
(message, extended, tuple)
},
|(mut left, extended, tuple), (right, _, _)| {
for (entry, value) in left.iter_mut().zip(right) {
*entry += value;
}
(left, extended, tuple)
},
)
.0
}
#[must_use]
pub fn round_message<EF>(&self, composed: &[F], eq: &[EF]) -> Vec<EF>
where
EF: ExtensionField<F>,
{
let stride = self.num_transmitted();
assert_eq!(composed.len(), eq.len() * stride, "one block per row");
composed
.par_chunks_exact(stride)
.zip(eq.par_iter())
.par_fold_reduce(
|| EF::zero_vec(stride),
|mut accumulator, (row, &weight)| {
for (entry, &value) in accumulator.iter_mut().zip(row) {
*entry += weight * value;
}
accumulator
},
|mut left, right| {
for (entry, value) in left.iter_mut().zip(right) {
*entry += value;
}
left
},
)
}
pub fn evaluate<EF>(&self, message: &[EF], lambda: EF) -> Result<EF, MessageLenMismatch>
where
EF: ExtensionField<F>,
{
if message.len() != self.num_transmitted() {
return Err(MessageLenMismatch {
expected: self.num_transmitted(),
actual: message.len(),
});
}
if let Some(base) = lambda.as_base() {
if let Some(index) = self.domain.transmitted().iter().position(|&t| t == base) {
return Ok(message[index]);
}
if self.domain.subspace().contains(&base) {
return Ok(EF::ZERO);
}
}
let scale = vanishing_at(self.domain.log_extended(), lambda)
* self.derivative_on_extension.inverse();
let offsets = self
.domain
.transmitted()
.iter()
.map(|&point| lambda + point)
.collect::<Vec<_>>();
let inverses = batch_multiplicative_inverse(&offsets);
Ok(scale
* message
.iter()
.zip(&inverses)
.map(|(&value, &inverse)| value * inverse)
.sum::<EF>())
}
#[must_use]
pub fn selector<EF>(&self, lambda: EF) -> RowSelector<EF>
where
EF: ExtensionField<F>,
{
RowSelector::new(&self.domain, lambda)
}
pub fn lagrange<EF>(&self, lambda: EF) -> Poly<EF>
where
EF: ExtensionField<F>,
{
Poly::new(lagrange_basis(&self.domain, lambda))
}
}
fn lagrange_basis<F, EF>(domain: &SkipDomain<F>, lambda: EF) -> Vec<EF>
where
F: TowerLevel,
EF: ExtensionField<F>,
{
let hit = lambda
.as_base()
.and_then(|base| domain.subspace().iter().position(|&s| s == base));
hit.map_or_else(
|| {
let scale =
vanishing_at(domain.log_size(), lambda) * domain.derivative_on_subspace().inverse();
let offsets = domain
.subspace()
.iter()
.map(|&s| lambda + s)
.collect::<Vec<_>>();
batch_multiplicative_inverse(&offsets)
.into_iter()
.map(|inverse| scale * inverse)
.collect::<Vec<_>>()
},
|hit| {
(0..domain.size())
.map(|index| if index == hit { EF::ONE } else { EF::ZERO })
.collect::<Vec<_>>()
},
)
}
fn vanishing_at<EF: Field>(log_size: usize, x: EF) -> EF {
let mut value = x;
for _ in 0..log_size {
value = value.square() + value;
}
value
}
#[derive(Debug, Clone)]
pub struct RowSelector<EF> {
table: Vec<EF>,
num_chunks: usize,
lagrange: Poly<EF>,
}
impl<EF: Field> RowSelector<EF> {
fn new<F>(domain: &SkipDomain<F>, lambda: EF) -> Self
where
F: TowerLevel,
EF: ExtensionField<F>,
{
let num_chunks = domain.size() / CHUNK_BITS;
let basis = lagrange_basis(domain, lambda);
let mut table = EF::zero_vec(num_chunks * TABLE_ROWS);
for (chunk, rows) in table.as_chunks_mut::<TABLE_ROWS>().0.iter_mut().enumerate() {
for value in 1..1usize << CHUNK_BITS {
let bit = value.trailing_zeros() as usize;
rows[value] = rows[value & (value - 1)] + basis[chunk * CHUNK_BITS + bit];
}
}
Self {
table,
num_chunks,
lagrange: Poly::new(basis),
}
}
#[must_use]
pub const fn row_bytes(&self) -> usize {
self.num_chunks
}
pub const fn lagrange(&self) -> &Poly<EF> {
&self.lagrange
}
#[must_use]
pub fn read(&self, row: &[u8]) -> EF {
assert_eq!(row.len(), self.num_chunks, "one byte per chunk");
row.iter()
.enumerate()
.map(|(chunk, &byte)| self.table[chunk * TABLE_ROWS + usize::from(byte)])
.sum()
}
pub fn bind(&self, packed: &[u8]) -> Poly<EF>
where
EF: Send + Sync,
{
assert_eq!(packed.len() % self.num_chunks, 0, "whole rows");
Poly::new(
packed
.par_chunks_exact(self.num_chunks)
.map(|row| self.read(row))
.collect::<Vec<_>>(),
)
}
}
#[cfg(test)]
mod tests {
use p3_binary_field::{BinaryField8, BinaryField128};
use p3_field::PrimeCharacteristicRing;
use proptest::prelude::*;
use rand::rngs::SmallRng;
use rand::{RngExt, SeedableRng};
use super::*;
use crate::univariate_skip::{Conjunction, SquareProduct};
type F = BinaryField8;
type EF = BinaryField128;
const LOG_SKIP: usize = 6;
struct Witness {
a: Vec<u8>,
b: Vec<u8>,
c: Vec<u8>,
}
fn random_witness(rng: &mut SmallRng, num_rows: usize, row_bytes: usize) -> Witness {
let a = (0..num_rows * row_bytes)
.map(|_| rng.random::<u8>())
.collect::<Vec<_>>();
let b = (0..num_rows * row_bytes)
.map(|_| rng.random::<u8>())
.collect::<Vec<_>>();
let c = a.iter().zip(&b).map(|(&x, &y)| x & y).collect::<Vec<_>>();
Witness { a, b, c }
}
fn compose(round: &SkipRound<F>, witness: &Witness, num_rows: usize) -> Vec<F> {
let stride = round.num_transmitted();
let extended = [&witness.a, &witness.b, &witness.c].map(|packed| {
let mut out = F::zero_vec(num_rows * stride);
round.extend_rows(packed, &mut out);
out
});
(0..num_rows * stride)
.map(|index| extended[0][index] * extended[1][index] + extended[2][index])
.collect()
}
fn unpack(packed: &[u8]) -> Vec<F> {
(0..packed.len() * CHUNK_BITS)
.map(|index| {
let bit = (packed[index / CHUNK_BITS] >> (index % CHUNK_BITS)) & 1;
if bit == 1 { F::ONE } else { F::ZERO }
})
.collect()
}
fn direct_sum(bound: &[Poly<EF>; 3], eq: &Poly<EF>, num_rows: usize) -> EF {
(0..num_rows)
.map(|row| {
let a = bound[0].as_slice()[row];
let b = bound[1].as_slice()[row];
let c = bound[2].as_slice()[row];
eq.as_slice()[row] * (a * b + c)
})
.sum()
}
#[test]
fn streaming_the_message_agrees_with_materialising_it() {
let mut rng = SmallRng::seed_from_u64(0x57EA);
let round = SkipRound::<F>::new(LOG_SKIP, 2).unwrap();
for num_rows in [1usize, 32, 37] {
let witness = random_witness(&mut rng, num_rows, round.row_bytes());
let eq = (0..num_rows)
.map(|_| rng.random::<EF>())
.collect::<Vec<_>>();
let materialised = {
let composed = compose(&round, &witness, num_rows);
round.round_message::<EF>(&composed, &eq)
};
let streamed = round.stream_round_message::<EF, _>(
&[&witness.a, &witness.b, &witness.c],
&eq,
&Conjunction,
);
assert_eq!(streamed, materialised, "{num_rows} rows");
}
}
#[test]
#[should_panic(expected = "must carry the constraint's degree")]
fn a_constraint_the_domain_cannot_carry_is_refused() {
let mut rng = SmallRng::seed_from_u64(0xDE9);
let round = SkipRound::<F>::new(LOG_SKIP, 2).unwrap();
let witness = random_witness(&mut rng, 4, round.row_bytes());
let eq = alloc::vec![EF::ONE; 4];
let _ = round.stream_round_message::<EF, _>(
&[&witness.a, &witness.b, &witness.c],
&eq,
&SquareProduct,
);
}
#[test]
fn a_cubic_constraint_streams_on_a_domain_sized_for_it() {
let mut rng = SmallRng::seed_from_u64(0xCB1);
let round = SkipRound::<F>::new(LOG_SKIP, 3).unwrap();
assert_eq!(round.domain().log_extended(), LOG_SKIP + 2);
for num_rows in [1usize, 37] {
let witness = random_witness(&mut rng, num_rows, round.row_bytes());
let eq = (0..num_rows)
.map(|_| rng.random::<EF>())
.collect::<Vec<_>>();
let stride = round.num_transmitted();
let extended = [&witness.a, &witness.b, &witness.c].map(|packed| {
let mut out = F::zero_vec(num_rows * stride);
round.extend_rows(packed, &mut out);
out
});
let composed = (0..num_rows * stride)
.map(|index| {
Composition::<F>::eval(
&SquareProduct,
&[extended[0][index], extended[1][index], extended[2][index]],
)
})
.collect::<Vec<_>>();
assert_eq!(
round.stream_round_message::<EF, _>(
&[&witness.a, &witness.b, &witness.c],
&eq,
&SquareProduct,
),
round.round_message::<EF>(&composed, &eq),
"{num_rows} rows"
);
}
}
#[test]
fn the_round_message_agrees_with_the_bound_polynomials() {
let mut rng = SmallRng::seed_from_u64(0x5C1);
let round = SkipRound::<F>::new(LOG_SKIP, 2).unwrap();
let num_rows = 1 << 4;
let witness = random_witness(&mut rng, num_rows, round.row_bytes());
let r = (0..4).map(|_| rng.random::<EF>()).collect::<Vec<_>>();
let eq = Poly::new_from_point(&r, EF::ONE);
let composed = compose(&round, &witness, num_rows);
let message = round.round_message::<EF>(&composed, eq.as_slice());
let lambda = rng.random::<EF>();
let read = round.evaluate(&message, lambda).unwrap();
let selector = round.selector::<EF>(lambda);
let bound = [&witness.a, &witness.b, &witness.c].map(|packed| selector.bind(packed));
assert_eq!(read, direct_sum(&bound, &eq, num_rows));
}
fn true_values_on_subspace(
witness: &Witness,
eq: &Poly<EF>,
num_rows: usize,
row_bytes: usize,
) -> Vec<EF> {
let residue = (0..witness.a.len())
.map(|index| (witness.a[index] & witness.b[index]) ^ witness.c[index])
.collect::<Vec<_>>();
(0..row_bytes * CHUNK_BITS)
.map(|column| {
(0..num_rows)
.filter(|&row| {
let byte = residue[row * row_bytes + column / CHUNK_BITS];
(byte >> (column % CHUNK_BITS)) & 1 == 1
})
.map(|row| eq.as_slice()[row])
.sum()
})
.collect()
}
#[test]
fn the_round_polynomial_vanishes_on_the_subspace_exactly_when_the_constraint_holds() {
let mut rng = SmallRng::seed_from_u64(0xC0FFEE);
let round = SkipRound::<F>::new(LOG_SKIP, 2).unwrap();
let num_rows = 1 << 3;
let mut witness = random_witness(&mut rng, num_rows, round.row_bytes());
let r = (0..3).map(|_| rng.random::<EF>()).collect::<Vec<_>>();
let eq = Poly::new_from_point(&r, EF::ONE);
let satisfied = true_values_on_subspace(&witness, &eq, num_rows, round.row_bytes());
assert!(satisfied.iter().all(|&value| value == EF::ZERO));
witness.c[0] ^= 1;
let broken = true_values_on_subspace(&witness, &eq, num_rows, round.row_bytes());
assert_eq!(broken[0], eq.as_slice()[0]);
assert!(broken[1..].iter().all(|&value| value == EF::ZERO));
}
#[test]
fn a_broken_constraint_breaks_the_identity_the_verifier_checks() {
let mut rng = SmallRng::seed_from_u64(0xBADC0DE);
let round = SkipRound::<F>::new(LOG_SKIP, 2).unwrap();
let num_rows = 1 << 3;
let mut witness = random_witness(&mut rng, num_rows, round.row_bytes());
witness.c[0] ^= 1;
let r = (0..3).map(|_| rng.random::<EF>()).collect::<Vec<_>>();
let eq = Poly::new_from_point(&r, EF::ONE);
let composed = compose(&round, &witness, num_rows);
let message = round.round_message::<EF>(&composed, eq.as_slice());
let lambda = rng.random::<EF>();
let selector = round.selector::<EF>(lambda);
let bound = [&witness.a, &witness.b, &witness.c].map(|packed| selector.bind(packed));
assert_ne!(
round.evaluate(&message, lambda).unwrap(),
direct_sum(&bound, &eq, num_rows)
);
}
#[test]
fn reading_the_message_on_a_transmitted_point_returns_that_value() {
let mut rng = SmallRng::seed_from_u64(0xD0A1);
let round = SkipRound::<F>::new(4, 2).unwrap();
let message = (0..round.num_transmitted())
.map(|_| rng.random::<EF>())
.collect::<Vec<_>>();
for (index, &point) in round.domain().transmitted().iter().enumerate() {
assert_eq!(
round.evaluate(&message, EF::from(point)).unwrap(),
message[index]
);
}
}
#[test]
fn reading_the_message_on_the_subspace_returns_zero() {
let mut rng = SmallRng::seed_from_u64(0x2E80);
let round = SkipRound::<F>::new(4, 2).unwrap();
let message = (0..round.num_transmitted())
.map(|_| rng.random::<EF>())
.collect::<Vec<_>>();
assert!(message.iter().any(|&value| value != EF::ZERO));
for &s in round.domain().subspace() {
assert_eq!(
round.evaluate(&message, EF::from(s)).unwrap(),
EF::ZERO,
"s={s:?}"
);
}
}
#[test]
fn binding_on_a_subspace_point_recovers_the_original_bits() {
let mut rng = SmallRng::seed_from_u64(0xB175);
let round = SkipRound::<F>::new(4, 2).unwrap();
let num_rows = 4;
let packed = (0..num_rows * round.row_bytes())
.map(|_| rng.random::<u8>())
.collect::<Vec<_>>();
let bits = unpack(&packed);
for (column, &s) in round.domain().subspace().iter().enumerate() {
let bound = round.selector::<EF>(EF::from(s)).bind(&packed);
for row in 0..num_rows {
let expected = bits[row * round.domain().size() + column];
assert_eq!(bound.as_slice()[row], EF::from(expected), "c={column}");
}
}
}
#[test]
fn rejects_a_skip_the_domain_or_the_table_cannot_host() {
assert!(matches!(
SkipRound::<F>::new(2, 2).unwrap_err(),
SkipRoundError::Lde(_)
));
assert!(matches!(
SkipRound::<F>::new(7, 4).unwrap_err(),
SkipRoundError::Domain(_)
));
}
proptest! {
#[test]
fn the_message_agrees_with_the_bound_polynomials_at_every_shape(
seed: u64,
log_skip in 3usize..=6,
log_rows in 1usize..=4,
) {
let mut rng = SmallRng::seed_from_u64(seed);
let round = SkipRound::<F>::new(log_skip, 2).unwrap();
let num_rows = 1 << log_rows;
let witness = random_witness(&mut rng, num_rows, round.row_bytes());
let r = (0..log_rows).map(|_| rng.random::<EF>()).collect::<Vec<_>>();
let eq = Poly::new_from_point(&r, EF::ONE);
let composed = compose(&round, &witness, num_rows);
let message = round.round_message::<EF>(&composed, eq.as_slice());
let lambda = rng.random::<EF>();
let selector = round.selector::<EF>(lambda);
let bound = [&witness.a, &witness.b, &witness.c].map(|p| selector.bind(p));
prop_assert_eq!(
round.evaluate(&message, lambda).unwrap(),
direct_sum(&bound, &eq, num_rows)
);
}
#[test]
fn binding_is_each_row_read_at_the_challenge(seed: u64) {
let mut rng = SmallRng::seed_from_u64(seed);
let round = SkipRound::<F>::new(3, 2).unwrap();
let num_rows = 4;
let packed = (0..num_rows * round.row_bytes())
.map(|_| rng.random::<u8>())
.collect::<Vec<_>>();
let bits = unpack(&packed);
let lambda = rng.random::<EF>();
let bound = round.selector::<EF>(lambda).bind(&packed);
let size = round.domain().size();
for row in 0..num_rows {
let values = bits[row * size..][..size]
.iter()
.map(|&bit| EF::from(bit))
.collect::<Vec<_>>();
prop_assert_eq!(
bound.as_slice()[row],
lagrange_reference(round.domain(), &values, lambda)
);
}
}
}
fn lagrange_reference(domain: &SkipDomain<F>, values: &[EF], at: EF) -> EF {
if let Some(index) = at
.as_base()
.and_then(|base| domain.subspace().iter().position(|&s| s == base))
{
return values[index];
}
let scale = vanishing_at(domain.log_size(), at)
* EF::from(domain.derivative_on_subspace()).inverse();
scale
* values
.iter()
.zip(domain.subspace())
.map(|(&value, &s)| value * (at + s).inverse())
.sum::<EF>()
}
}