use alloc::vec;
use alloc::vec::Vec;
use core::ops::Range;
use p3_binary_field::{BinaryField8, BitCoordinates, TowerLevel};
use p3_challenger::fs::TranscriptField;
use p3_challenger::{CanObserve, CanSampleUniformBits, FieldChallenger, GrindingChallenger};
use p3_commit::MultilinearPcs;
use p3_field::Field;
use p3_matrix::dense::RowMajorMatrix;
use p3_maybe_rayon::prelude::*;
use p3_multilinear_util::point::Point;
use p3_sumcheck::layout::Table;
use p3_sumcheck::{
OpeningBatch, OpeningEvals, OpeningProtocol, PrescribedOpeningSecurity, PrescribedPointPcs,
TableShape, TableSpec,
};
use crate::boolean::{BooleanMultilinearPcs, BooleanPcs};
use crate::boolean_trace::{
BooleanTraceCommitment, BooleanTraceCommitmentData, BooleanTraceCommitmentError,
BooleanTraceCommitmentProof,
};
use crate::packing::{Coordinates, coordinate_bytes, pack};
const WORD_BITS: usize = 64;
#[must_use]
pub fn coordinate_basis<EF: Coordinates>() -> Vec<EF> {
let bytes = EF::COORDINATES / 8;
(0..EF::COORDINATES)
.map(|k| {
let mut cells = vec![BinaryField8::from_le_bytes([0]); bytes];
cells[k / 8] = BinaryField8::from_le_bytes([1 << (k % 8)]);
pack::<BinaryField8, EF>(&cells)[0]
})
.collect()
}
#[must_use]
pub fn committed_shapes<EF: Coordinates>(
shapes: &[TableShape],
boolean_columns: &[usize],
) -> Vec<TableShape> {
assert_eq!(
shapes.len(),
boolean_columns.len(),
"one bit region per table"
);
shapes
.iter()
.zip(boolean_columns)
.map(|(shape, &bits)| {
assert!(bits <= shape.width(), "a bit region fits inside its table");
let width = bits + (shape.width() - bits) * EF::COORDINATES;
TableShape::new(shape.num_variables(), width)
})
.collect()
}
const fn committed_columns<EF: Coordinates>(bits: usize, column: usize) -> Range<usize> {
if column < bits {
column..column + 1
} else {
let start = bits + (column - bits) * EF::COORDINATES;
start..start + EF::COORDINATES
}
}
pub struct MixedTraceCommitment<EF, B> {
inner: BooleanTraceCommitment<EF, B>,
boolean_columns: Vec<usize>,
basis: Vec<EF>,
}
pub type MixedTracePcs<EF, MT, MX> = MixedTraceCommitment<EF, BooleanPcs<EF, MT, MX>>;
impl<EF: Coordinates, B> MixedTraceCommitment<EF, B> {
pub fn new(inner: BooleanTraceCommitment<EF, B>, boolean_columns: Vec<usize>) -> Self {
Self {
inner,
boolean_columns,
basis: coordinate_basis(),
}
}
#[must_use]
pub fn boolean_columns(&self) -> &[usize] {
&self.boolean_columns
}
#[must_use]
pub const fn inner(&self) -> &BooleanTraceCommitment<EF, B> {
&self.inner
}
}
impl<EF: Field + Coordinates, B> MixedTraceCommitment<EF, B> {
fn check_regions<E>(
&self,
shapes: &[TableShape],
) -> Result<(), BooleanTraceCommitmentError<E>> {
if shapes.len() != self.boolean_columns.len() {
return Err(BooleanTraceCommitmentError::TableCountMismatch {
expected: self.boolean_columns.len(),
actual: shapes.len(),
});
}
for (table, (shape, &bits)) in shapes.iter().zip(&self.boolean_columns).enumerate() {
if bits > shape.width() {
return Err(BooleanTraceCommitmentError::BitRegionWidth {
table,
bits,
width: shape.width(),
});
}
}
Ok(())
}
fn expand_protocol(&self, protocol: &OpeningProtocol) -> OpeningProtocol {
let shapes = protocol.table_shapes();
let committed = committed_shapes::<EF>(&shapes, &self.boolean_columns);
let mut successor = vec![false; shapes.len()];
for (table, batch) in protocol.iter_openings() {
successor[table] |= !batch.next().is_empty();
}
let mut schedules = vec![Vec::new(); shapes.len()];
for (table, _) in protocol.iter_openings() {
let all: Vec<usize> = (0..committed[table].width()).collect();
let next = if successor[table] {
all.clone()
} else {
Vec::new()
};
schedules[table].push(OpeningBatch::new(all, next));
}
OpeningProtocol::new(
committed
.into_iter()
.zip(schedules)
.map(|(shape, schedule)| TableSpec::new(shape, schedule))
.collect(),
)
}
fn recombine(
&self,
protocol: &OpeningProtocol,
evals: &[OpeningEvals<EF>],
) -> Vec<OpeningEvals<EF>> {
protocol
.iter_openings()
.zip(evals)
.map(|((table, batch), committed)| {
let bits = self.boolean_columns[table];
let side = |columns: &[usize], values: &[EF]| {
columns
.iter()
.map(|&column| {
let range = committed_columns::<EF>(bits, column);
if column < bits {
values[range.start]
} else {
EF::batched_linear_combination(&values[range], &self.basis)
}
})
.collect()
};
OpeningBatch::new(
side(batch.current(), committed.current()),
side(batch.next(), committed.next()),
)
})
.collect()
}
fn expand_table<E>(
&self,
index: usize,
table: &Table<EF>,
) -> Result<Table<EF>, BooleanTraceCommitmentError<E>> {
let num_variables = table.num_variables();
let height = 1usize << num_variables;
let width = table.num_polys();
let bits = self.boolean_columns[index];
let committed = bits + (width - bits) * EF::COORDINATES;
let blocks = height.div_ceil(WORD_BITS);
let mut words = vec![0u64; blocks * committed];
let refused = words
.par_chunks_mut(committed)
.enumerate()
.filter_map(|(block, row)| {
let rows = block * WORD_BITS..((block + 1) * WORD_BITS).min(height);
for (column, slot) in row[..bits].iter_mut().enumerate() {
let view = table.column(column);
*slot = match view.boolean_word(block) {
Some(word) => word,
None => {
let mut word = 0;
for (lane, r) in rows.clone().enumerate() {
let value = view.value(r);
if value == EF::ONE {
word |= 1 << lane;
} else if value != EF::ZERO {
return Some(column);
}
}
word
}
};
}
for column in bits..width {
let view = table.column(column);
let out = &mut row[committed_columns::<EF>(bits, column)];
for (lane, r) in rows.clone().enumerate() {
let value = view.value(r);
for (byte_index, &byte) in coordinate_bytes(core::slice::from_ref(&value))
.iter()
.enumerate()
{
let mut rest = byte;
while rest != 0 {
let bit = rest.trailing_zeros() as usize;
out[byte_index * 8 + bit] |= 1 << lane;
rest &= rest - 1;
}
}
}
}
None
})
.min();
if let Some(column) = refused {
return Err(BooleanTraceCommitmentError::NonBooleanCell {
table: index,
column,
});
}
Ok(Table::from_packed_bits(
RowMajorMatrix::new(words, committed),
num_variables,
))
}
}
pub struct MixedTraceData<EF: Field, D> {
inner: BooleanTraceCommitmentData<EF, D>,
tables: Vec<Table<EF>>,
}
impl<EF: Field, D: Clone> Clone for MixedTraceData<EF, D> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
tables: self.tables.clone(),
}
}
}
impl<EF: Field, D> MixedTraceData<EF, D> {
#[must_use]
pub fn table(&self, index: usize) -> &Table<EF> {
&self.tables[index]
}
}
impl<EF, B, Challenger> MultilinearPcs<EF, Challenger> for MixedTraceCommitment<EF, B>
where
EF: BitCoordinates + TranscriptField + TowerLevel + Coordinates,
B: BooleanMultilinearPcs<EF, Challenger, Val = EF>,
Challenger: FieldChallenger<EF>
+ GrindingChallenger<Witness = EF>
+ CanSampleUniformBits<EF>
+ CanObserve<B::Commitment>,
{
type Val = EF;
type Commitment = B::Commitment;
type ProverData = MixedTraceData<EF, B::ProverData>;
type Proof = BooleanTraceCommitmentProof<EF, B::Proof>;
type Error = BooleanTraceCommitmentError<B::Error>;
type ProverError = BooleanTraceCommitmentError<B::Error>;
type Witness = Vec<Table<EF>>;
type OpeningProtocol = OpeningProtocol;
fn num_vars(&self) -> usize {
self.inner.num_variables()
}
fn commit(
&self,
witness: Self::Witness,
challenger: &mut Challenger,
) -> Result<(Self::Commitment, Self::ProverData), Self::ProverError> {
let shapes: Vec<_> = witness.iter().map(Table::shape).collect();
self.check_regions(&shapes)?;
let committed = tracing::info_span!("expand mixed tables").in_scope(|| {
witness
.iter()
.enumerate()
.map(|(index, table)| self.expand_table(index, table))
.collect::<Result<Vec<_>, _>>()
})?;
let (commitment, inner) = self.inner.commit(committed, challenger)?;
Ok((
commitment,
MixedTraceData {
inner,
tables: witness,
},
))
}
fn observe_commitment(&self, commitment: &Self::Commitment, challenger: &mut Challenger) {
MultilinearPcs::observe_commitment(&self.inner, commitment, challenger);
}
fn open(
&self,
prover_data: Self::ProverData,
protocol: Self::OpeningProtocol,
challenger: &mut Challenger,
) -> Result<Self::Proof, Self::ProverError> {
self.check_regions(&protocol.table_shapes())?;
let committed = self.expand_protocol(&protocol);
self.inner.open(prover_data.inner, committed, challenger)
}
fn verify(
&self,
commitment: &Self::Commitment,
proof: &Self::Proof,
challenger: &mut Challenger,
protocol: Self::OpeningProtocol,
) -> Result<(), Self::Error> {
self.check_regions(&protocol.table_shapes())?;
let committed = self.expand_protocol(&protocol);
self.inner.verify(commitment, proof, challenger, committed)
}
}
impl<EF, B, Challenger> PrescribedPointPcs<EF, Challenger> for MixedTraceCommitment<EF, B>
where
EF: BitCoordinates + TranscriptField + TowerLevel + Coordinates,
B: BooleanMultilinearPcs<EF, Challenger, Val = EF>,
Challenger: FieldChallenger<EF>
+ GrindingChallenger<Witness = EF>
+ CanSampleUniformBits<EF>
+ CanObserve<B::Commitment>,
{
fn bit_region(&self, table: usize, _width: usize) -> usize {
self.boolean_columns.get(table).copied().unwrap_or(0)
}
fn prescribed_security(&self, protocol: &OpeningProtocol) -> Option<PrescribedOpeningSecurity> {
self.check_regions::<B::Error>(&protocol.table_shapes())
.ok()?;
self.inner
.prescribed_security(&self.expand_protocol(protocol))
}
fn open_at(
&self,
prover_data: Self::ProverData,
protocol: &OpeningProtocol,
points: &[Point<EF>],
challenger: &mut Challenger,
) -> Result<Self::Proof, Self::ProverError> {
self.check_regions(&protocol.table_shapes())?;
let committed = self.expand_protocol(protocol);
self.inner
.open_at(prover_data.inner, &committed, points, challenger)
}
fn verify_at(
&self,
commitment: &Self::Commitment,
proof: &Self::Proof,
protocol: &OpeningProtocol,
points: &[Point<EF>],
challenger: &mut Challenger,
) -> Result<Vec<OpeningEvals<EF>>, Self::Error> {
self.check_regions(&protocol.table_shapes())?;
let committed = self.expand_protocol(protocol);
let evals = self
.inner
.verify_at(commitment, proof, &committed, points, challenger)?;
Ok(self.recombine(protocol, &evals))
}
}
#[cfg(test)]
mod tests;