use alloc::vec;
use alloc::vec::Vec;
use p3_binary_dft::{EncodableLevel, domain_point, domain_point_steps};
use p3_binary_field::{
BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128, Ghash128, Poly64,
TowerLevel, poly_basis,
};
use p3_field::{Algebra, ExtensionField, Field, PackedValue, PrimeCharacteristicRing};
use p3_maybe_rayon::prelude::*;
use p3_sumcheck::strategy::IntoTranscriptField;
type Packed = <Ghash128 as Field>::Packing;
const WIDTH: usize = Packed::WIDTH;
const FOLD_GRAIN: usize = 1 << 10;
const BLOCK_SYMBOLS: usize = 2 * FOLD_GRAIN;
const _: () = assert!(
FOLD_GRAIN.is_multiple_of(WIDTH),
"the fold grain must be a whole number of packed groups"
);
#[inline]
pub fn fold_pair<F, EF>(index: usize, beta: EF, lo: F, hi: F) -> EF
where
F: TowerLevel,
EF: ExtensionField<F>,
{
let x: F = domain_point(index << 1);
let f1 = lo + hi;
let f0 = lo + x * f1;
beta * (f0 + f1) + f0
}
fn fold_round<F, EF>(codeword: &[F], beta: EF) -> Vec<EF>
where
F: TowerLevel + Sync,
EF: ExtensionField<F> + Send + Sync,
{
let num_pairs = codeword.len() / 2;
let steps = domain_point_steps::<F>(num_pairs.next_power_of_two().trailing_zeros() as usize);
let mut folded = EF::zero_vec(num_pairs);
folded
.par_chunks_mut(FOLD_GRAIN)
.zip(codeword.par_chunks(2 * FOLD_GRAIN))
.enumerate()
.for_each(|(task, (slots, symbols))| {
let start = task * FOLD_GRAIN;
let mut x: F = domain_point(start << 1);
let (pairs, _) = symbols.as_chunks::<2>();
for (offset, (slot, pair)) in slots.iter_mut().zip(pairs).enumerate() {
if offset != 0 {
x += steps[(start + offset).trailing_zeros() as usize];
}
let f1 = pair[0] + pair[1];
let f0 = pair[0] + x * f1;
*slot = beta * (f0 + f1) + f0;
}
});
folded
}
fn fold_rounds_scalar<EF>(codeword: &[EF], challenges: &[EF]) -> Vec<EF>
where
EF: TowerLevel + Send + Sync,
{
let mut folded = fold_round(codeword, challenges[0]);
for &beta in &challenges[1..] {
folded = fold_round(&folded, beta);
}
folded
}
fn fold_rounds_lifting<F, EF>(codeword: &[F], challenges: &[EF]) -> Vec<EF>
where
F: TowerLevel + Sync,
EF: ExtensionField<F> + FoldAlphabet<EF> + Send + Sync,
{
let lifted = fold_round(codeword, challenges[0]);
match challenges.len() {
1 => lifted,
_ => EF::fold_rounds(&lifted, &challenges[1..]),
}
}
pub trait FoldAlphabet<EF: TowerLevel>: TowerLevel {
fn fold_rounds(codeword: &[Self], challenges: &[EF]) -> Vec<EF>;
}
impl FoldAlphabet<Self> for BinaryField128 {
fn fold_rounds(codeword: &[Self], challenges: &[Self]) -> Vec<Self> {
fold_rounds_packed(codeword, challenges)
}
}
impl FoldAlphabet<Self> for BinaryField64 {
fn fold_rounds(codeword: &[Self], challenges: &[Self]) -> Vec<Self> {
fold_rounds_scalar(codeword, challenges)
}
}
impl FoldAlphabet<BinaryField128> for BinaryField64 {
fn fold_rounds(codeword: &[Self], challenges: &[BinaryField128]) -> Vec<BinaryField128> {
fold_rounds_packed(codeword, challenges)
}
}
impl FoldAlphabet<BinaryField128> for BinaryField32 {
fn fold_rounds(codeword: &[Self], challenges: &[BinaryField128]) -> Vec<BinaryField128> {
fold_rounds_packed(codeword, challenges)
}
}
impl FoldAlphabet<BinaryField64> for BinaryField32 {
fn fold_rounds(codeword: &[Self], challenges: &[BinaryField64]) -> Vec<BinaryField64> {
fold_rounds_lifting(codeword, challenges)
}
}
impl FoldAlphabet<BinaryField128> for BinaryField16 {
fn fold_rounds(codeword: &[Self], challenges: &[BinaryField128]) -> Vec<BinaryField128> {
fold_rounds_packed(codeword, challenges)
}
}
impl FoldAlphabet<BinaryField64> for BinaryField16 {
fn fold_rounds(codeword: &[Self], challenges: &[BinaryField64]) -> Vec<BinaryField64> {
fold_rounds_lifting(codeword, challenges)
}
}
impl FoldAlphabet<BinaryField128> for BinaryField8 {
fn fold_rounds(codeword: &[Self], challenges: &[BinaryField128]) -> Vec<BinaryField128> {
fold_rounds_packed(codeword, challenges)
}
}
impl FoldAlphabet<BinaryField64> for BinaryField8 {
fn fold_rounds(codeword: &[Self], challenges: &[BinaryField64]) -> Vec<BinaryField64> {
fold_rounds_lifting(codeword, challenges)
}
}
const REPR_CONVERSION_BLOCK: usize = 1 << 10;
pub trait ChallengeField<F: Field>: EncodableLevel {
type SumcheckRepr: IntoTranscriptField<Self> + Algebra<F>;
fn from_sumcheck_repr(values: &[Self::SumcheckRepr], out: &mut [Self]) {
assert_eq!(values.len(), out.len(), "one output per value");
out.par_iter_mut()
.zip(values.par_iter())
.for_each(|(slot, &value)| *slot = value.into_transcript());
}
}
impl<F: Field> ChallengeField<F> for BinaryField128
where
Ghash128: Algebra<F>,
{
type SumcheckRepr = Ghash128;
fn from_sumcheck_repr(values: &[Ghash128], out: &mut [Self]) {
assert_eq!(values.len(), out.len(), "one output per value");
out.par_chunks_mut(REPR_CONVERSION_BLOCK)
.zip(values.par_chunks(REPR_CONVERSION_BLOCK))
.for_each_init(Vec::new, |words, (out, values)| {
words.clear();
words.extend(values.iter().map(|value| value.to_repr()));
poly_basis::to_tower_slice(words);
for (slot, &word) in out.iter_mut().zip(words.iter()) {
*slot = Self::from_repr(word);
}
});
}
}
impl<F: Field> ChallengeField<F> for BinaryField64
where
Poly64: Algebra<F>,
{
type SumcheckRepr = Poly64;
}
fn lane_offsets<P: PackedValue<Value = Ghash128>>() -> P {
P::from_fn(|lane| domain_point(lane << 1))
}
fn group_steps<const W: usize>(num_pairs: usize) -> Vec<Ghash128> {
let log_stride = (2 * W).trailing_zeros() as usize;
let levels = (num_pairs / W).next_power_of_two().trailing_zeros() as usize;
let mut step = Ghash128::ZERO;
(0..levels)
.map(|level| {
step += Ghash128::cantor_basis(log_stride + level);
step
})
.collect()
}
fn fold_pairs(
src: &[u128],
dst: &mut [u128],
start: usize,
beta: Ghash128,
lane_offsets: Packed,
group_steps: &[Ghash128],
) {
assert_eq!(
src.len(),
2 * dst.len(),
"a fold must read one pair per output symbol"
);
debug_assert!(
start.is_multiple_of(WIDTH),
"a fold must start on a packed group boundary"
);
let beta_packed = Packed::broadcast(beta);
let (slot_groups, tail) = dst.as_chunks_mut::<WIDTH>();
let (symbol_blocks, tail_pairs) = src.as_chunks::<{ 2 * WIDTH }>();
let tail_offset = slot_groups.len() * WIDTH;
let first_group = start / WIDTH;
let mut base: Ghash128 = domain_point(start << 1);
for (group, (slots, block)) in slot_groups.iter_mut().zip(symbol_blocks).enumerate() {
if group != 0 {
base += group_steps[(first_group + group).trailing_zeros() as usize];
}
let x = Packed::broadcast(base) + lane_offsets;
let lo = Packed::from_fn(|lane| Ghash128::from_repr(block[2 * lane]));
let hi = Packed::from_fn(|lane| Ghash128::from_repr(block[2 * lane + 1]));
let f1 = lo + hi;
let f0 = lo + x * f1;
let folded = f0 + beta_packed * (f0 + f1);
for (slot, &value) in slots.iter_mut().zip(folded.as_slice()) {
*slot = value.to_repr();
}
}
let (tail_blocks, _) = tail_pairs.as_chunks::<2>();
for (offset, (slot, pair)) in tail.iter_mut().zip(tail_blocks).enumerate() {
let x: Ghash128 = domain_point((start + tail_offset + offset) << 1);
let lo = Ghash128::from_repr(pair[0]);
let hi = Ghash128::from_repr(pair[1]);
let f1 = lo + hi;
let f0 = lo + x * f1;
*slot = (f0 + beta * (f0 + f1)).to_repr();
}
}
#[must_use]
pub fn fold_codeword<F, EF>(codeword: &[F], beta: EF) -> Vec<EF>
where
F: FoldAlphabet<EF>,
EF: TowerLevel,
{
assert!(
codeword.is_empty() || codeword.len().is_power_of_two(),
"codeword length must be a power of two"
);
F::fold_rounds(codeword, &[beta])
}
pub(crate) fn fold_coset<F, EF>(
coset_index: usize,
values: &[F],
challenges: &[EF],
scratch: &mut Vec<EF>,
) -> EF
where
F: TowerLevel,
EF: ExtensionField<F> + TowerLevel,
{
assert_eq!(values.len(), 1usize << challenges.len());
let mut len = values.len() / 2;
scratch.clear();
scratch.extend((0..len).map(|i| {
fold_pair(
coset_index * len + i,
challenges[0],
values[2 * i],
values[2 * i + 1],
)
}));
for &beta in &challenges[1..] {
len /= 2;
for i in 0..len {
scratch[i] = fold_pair::<EF, EF>(
coset_index * len + i,
beta,
scratch[2 * i],
scratch[2 * i + 1],
);
}
}
scratch[0]
}
pub(crate) fn fold_codeword_batch<F, EF>(codeword: &[F], challenges: &[EF]) -> Vec<EF>
where
F: FoldAlphabet<EF>,
EF: TowerLevel,
{
assert!(!challenges.is_empty());
assert!(codeword.len().is_power_of_two());
assert!(challenges.len() <= codeword.len().ilog2() as usize);
F::fold_rounds(codeword, challenges)
}
fn fold_rounds_packed<F>(codeword: &[F], challenges: &[BinaryField128]) -> Vec<BinaryField128>
where
F: TowerLevel + Sync,
F::Repr: Into<u128>,
{
let offsets = lane_offsets::<Packed>();
fold_rounds::<WIDTH, F, _>(codeword, challenges, &|src, dst, start, beta, steps| {
fold_pairs(src, dst, start, beta, offsets, steps);
})
}
fn fold_rounds<const W: usize, F, K>(
codeword: &[F],
challenges: &[BinaryField128],
kernel: &K,
) -> Vec<BinaryField128>
where
F: TowerLevel + Sync,
F::Repr: Into<u128>,
K: Fn(&[u128], &mut [u128], usize, Ghash128, &[Ghash128]) + Sync,
{
assert!(!challenges.is_empty(), "a fold runs at least one round");
let arity = challenges.len();
let size = 1usize << arity;
let num_cosets = codeword.len() >> arity;
let codeword = &codeword[..num_cosets << arity];
let betas: Vec<Ghash128> = challenges.iter().copied().map(Ghash128::from).collect();
let steps: Vec<Vec<Ghash128>> = (0..arity)
.map(|round| group_steps::<W>(codeword.len() >> (round + 1)))
.collect();
let block = (BLOCK_SYMBOLS >> arity).max(W);
debug_assert!(
FOLD_GRAIN.is_multiple_of(block),
"a task must hold whole blocks"
);
let mut folded = vec![0u128; num_cosets];
folded
.par_chunks_mut(FOLD_GRAIN)
.zip(codeword.par_chunks(FOLD_GRAIN.saturating_mul(size)))
.enumerate()
.for_each_init(
|| (Vec::new(), Vec::new()),
|(even, odd), (task, (out, input))| {
for (index, (slots, symbols)) in out
.chunks_mut(block)
.zip(input.chunks(block * size))
.enumerate()
{
let first = task * FOLD_GRAIN + index * block;
even.clear();
even.extend(symbols.iter().map(|&symbol| symbol.to_repr().into()));
poly_basis::from_tower_slice(even);
odd.resize(if arity > 1 { symbols.len() / 2 } else { 0 }, 0);
for (round, (&beta, steps)) in betas.iter().zip(&steps).enumerate() {
let len = symbols.len() >> round;
let (src, spare) = if round % 2 == 0 {
(&even[..len], &mut odd[..])
} else {
(&odd[..len], &mut even[..])
};
let dst = if round + 1 == arity {
&mut *slots
} else {
&mut spare[..len / 2]
};
kernel(src, dst, first * (size >> (round + 1)), beta, steps);
}
}
poly_basis::to_tower_slice(out);
},
);
folded.into_iter().map(BinaryField128::from_repr).collect()
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use core::array;
use p3_binary_dft::{AdditiveNtt, NaiveAdditiveNtt, domain_point};
use p3_binary_field::{
BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128, Ghash128,
TowerLevel,
};
use p3_field::PrimeCharacteristicRing;
use p3_matrix::dense::RowMajorMatrix;
use p3_multilinear_util::poly::Poly;
use proptest::prelude::*;
use rand::distr::{Distribution, StandardUniform};
use rand::rngs::SmallRng;
use rand::{RngExt, SeedableRng};
use super::{
FOLD_GRAIN, FoldAlphabet, WIDTH, fold_codeword, fold_pair, fold_rounds, fold_rounds_scalar,
lane_offsets,
};
const ARITIES: [usize; 9] = [1, 2, 3, 4, 5, 9, 10, 11, 12];
#[test]
fn batched_folds_match_sequential_folds_at_every_coset_offset() {
let mut rng = SmallRng::seed_from_u64(0xBA7C);
for log_len in [4, 7, 12, 14] {
let word: Vec<BinaryField128> = (0..1 << log_len).map(|_| rng.random()).collect();
for arity in ARITIES.into_iter().filter(|&arity| arity <= log_len) {
let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
let mut expected = word.clone();
for &beta in &challenges {
expected = fold_codeword(&expected, beta);
}
assert_eq!(super::fold_codeword_batch(&word, &challenges), expected);
for (index, coset) in word.chunks(1 << arity).enumerate() {
assert_eq!(
super::fold_coset(index, coset, &challenges, &mut Vec::new()),
expected[index]
);
}
}
}
}
fn encode(coeffs: &[BinaryField128]) -> Vec<BinaryField128> {
NaiveAdditiveNtt::<BinaryField128>::default()
.ntt_batch(RowMajorMatrix::new(coeffs.to_vec(), 1))
.values
}
fn gray_chain_steps(num_pairs: usize) -> Vec<BinaryField128> {
let levels = num_pairs.next_power_of_two().trailing_zeros() as usize;
let mut steps = Vec::with_capacity(levels);
let mut acc = BinaryField128::ZERO;
for level in 0..levels {
acc += BinaryField128::cantor_basis(level + 1);
steps.push(acc);
}
steps
}
fn fold_codeword_tower_reference(
codeword: &[BinaryField128],
beta: BinaryField128,
) -> Vec<BinaryField128> {
let num_pairs = codeword.len() / 2;
let steps = gray_chain_steps(num_pairs);
codeword
.chunks(2 * FOLD_GRAIN)
.enumerate()
.flat_map(|(block, chunk)| {
let start = block * FOLD_GRAIN;
let mut x: BinaryField128 = domain_point(start << 1);
let steps = &steps;
chunk
.chunks(2)
.enumerate()
.map(move |(offset, pair)| {
if offset != 0 {
x += steps[(start + offset).trailing_zeros() as usize];
}
let (lo, hi) = (pair[0], pair[1]);
let f1 = lo + hi;
let f0 = lo + x * f1;
f0 + beta * (f0 + f1)
})
.collect::<Vec<_>>()
})
.collect()
}
#[test]
fn a_narrow_domain_point_embeds_into_the_wide_one() {
for i in 0..4096usize {
let narrow: BinaryField64 = domain_point(i);
let wide: BinaryField128 = domain_point(i);
assert_eq!(BinaryField128::from(narrow), wide, "i={i}");
}
for i in 0..256usize {
let narrow: BinaryField8 = domain_point(i);
assert_eq!(
BinaryField128::from(narrow),
domain_point::<BinaryField128>(i),
"i={i}"
);
}
}
#[test]
fn the_first_pair_folds_to_a_hand_computed_value() {
let lo = BinaryField64::from_repr(0x0123_4567_89AB_CDEF);
let hi = BinaryField64::from_repr(0xFEDC_BA98_7654_3210);
let beta = BinaryField128::from_repr(0x3141_5926_5358_9793_2384_6264_3383_2795);
assert_eq!(domain_point::<BinaryField64>(0), BinaryField64::ZERO);
let expected = BinaryField128::from(lo) + beta * hi;
assert_eq!(fold_pair(0, beta, lo, hi), expected);
let novel_basis = BinaryField128::from(lo) + beta * (lo + hi);
assert_ne!(novel_basis, expected);
}
#[test]
fn the_tower_route_and_the_packed_route_agree() {
let mut rng = SmallRng::seed_from_u64(0x70A7_E12E);
for log_len in [1usize, 3, 11, 12, 13] {
let codeword: Vec<BinaryField128> = (0..1 << log_len).map(|_| rng.random()).collect();
for arity in ARITIES.into_iter().filter(|&arity| arity <= log_len) {
let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
assert_eq!(
fold_rounds_scalar(&codeword, &challenges),
super::fold_rounds_packed(&codeword, &challenges),
"log_len={log_len} arity={arity}"
);
}
}
}
fn check_folds_like_its_embedding<F>(seed: u64)
where
F: TowerLevel + Sync + FoldAlphabet<BinaryField128>,
BinaryField128: p3_field::ExtensionField<F>,
StandardUniform: Distribution<F>,
{
let mut rng = SmallRng::seed_from_u64(seed);
for log_len in [1usize, 4, 8, 11, 12]
.into_iter()
.filter(|&log_len| log_len <= F::bits())
{
let narrow: Vec<F> = (0..1 << log_len).map(|_| rng.random::<F>()).collect();
let embedded: Vec<BinaryField128> =
narrow.iter().copied().map(BinaryField128::from).collect();
for arity in ARITIES.into_iter().filter(|&arity| arity <= log_len) {
let challenges: Vec<BinaryField128> =
(0..arity).map(|_| rng.random::<BinaryField128>()).collect();
let expected = challenges.iter().fold(embedded.clone(), |word, &beta| {
fold_codeword_tower_reference(&word, beta)
});
assert_eq!(
super::fold_codeword_batch(&narrow, &challenges),
expected,
"bits={} log_len={log_len} arity={arity}",
F::bits()
);
}
}
}
#[test]
fn every_narrow_alphabet_folds_like_its_embedding() {
check_folds_like_its_embedding::<BinaryField8>(0x11F7_1408);
check_folds_like_its_embedding::<BinaryField16>(0x11F7_1416);
check_folds_like_its_embedding::<BinaryField32>(0x11F7_1432);
check_folds_like_its_embedding::<BinaryField64>(0x11F7_1467);
}
#[test]
fn a_narrow_coset_folds_like_its_slice_of_the_whole_codeword() {
let mut rng = SmallRng::seed_from_u64(0xC05E_7000);
let narrow: Vec<BinaryField64> = (0..1 << 10).map(|_| rng.random()).collect();
for arity in [1usize, 2, 3, 5] {
let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
let whole = super::fold_codeword_batch(&narrow, &challenges);
let mut scratch = Vec::new();
for (index, coset) in narrow.chunks(1 << arity).enumerate() {
assert_eq!(
super::fold_coset(index, coset, &challenges, &mut scratch),
whole[index],
"arity={arity} index={index}"
);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn every_narrow_shape_matches_the_embedded_wide_fold(
(log_len, arity) in (1usize..=12)
.prop_flat_map(|log_len| (Just(log_len), 1..=log_len)),
seed: u64,
) {
let mut rng = SmallRng::seed_from_u64(seed);
let narrow: Vec<BinaryField64> = (0..1 << log_len).map(|_| rng.random()).collect();
let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
let embedded: Vec<BinaryField128> =
narrow.iter().copied().map(BinaryField128::from).collect();
let expected = challenges.iter().fold(embedded, |word, &beta| {
fold_codeword_tower_reference(&word, beta)
});
prop_assert_eq!(super::fold_codeword_batch(&narrow, &challenges), expected);
}
}
#[test]
fn a_narrow_challenge_field_folds_pair_by_pair() {
let mut rng = SmallRng::seed_from_u64(0x6A11_0064);
let codeword: Vec<BinaryField64> = (0..1 << 11).map(|_| rng.random()).collect();
let beta: BinaryField64 = rng.random();
let folded = fold_codeword(&codeword, beta);
for (j, &value) in folded.iter().enumerate() {
assert_eq!(
value,
fold_pair(j, beta, codeword[2 * j], codeword[2 * j + 1]),
"j={j}"
);
}
}
#[test]
fn the_folded_domain_is_domain_point_at_halved_index() {
for i in 0..512usize {
let x: BinaryField128 = domain_point(i);
assert_eq!(
x.square() + x,
domain_point::<BinaryField128>(i >> 1),
"i={i}"
);
}
}
#[test]
fn folding_partners_are_adjacent_and_differ_by_one() {
for j in 0..256usize {
let lo: BinaryField128 = domain_point(2 * j);
let hi: BinaryField128 = domain_point(2 * j + 1);
assert_eq!(lo + BinaryField128::ONE, hi, "j={j}");
}
}
#[test]
fn a_lane_offset_shifts_a_group_start_to_its_own_domain_point() {
for group in 0..64usize {
let first = group * WIDTH;
for lane in 0..WIDTH {
assert_eq!(first & lane, 0, "group={group} lane={lane}");
assert_eq!(
domain_point::<BinaryField128>((first + lane) << 1),
domain_point::<BinaryField128>(first << 1)
+ domain_point::<BinaryField128>(lane << 1),
"group={group} lane={lane}"
);
}
}
}
fn fold_pairs_model<const W: usize>(
src: &[u128],
dst: &mut [u128],
start: usize,
beta: Ghash128,
lane_offsets: [Ghash128; W],
group_steps: &[Ghash128],
) {
debug_assert!(
start.is_multiple_of(W),
"a fold must start on a packed group boundary"
);
let (slot_groups, tail) = dst.as_chunks_mut::<W>();
let tail_offset = slot_groups.len() * W;
let (symbol_blocks, tail_pairs) = src.split_at(2 * tail_offset);
let first_group = start / W;
let mut base: Ghash128 = domain_point(start << 1);
for (group, slots) in slot_groups.iter_mut().enumerate() {
if group != 0 {
base += group_steps[(first_group + group).trailing_zeros() as usize];
}
let block = &symbol_blocks[2 * W * group..][..2 * W];
let x: [Ghash128; W] = array::from_fn(|lane| base + lane_offsets[lane]);
let lo: [Ghash128; W] = array::from_fn(|lane| Ghash128::from_repr(block[2 * lane]));
let hi: [Ghash128; W] = array::from_fn(|lane| Ghash128::from_repr(block[2 * lane + 1]));
for (lane, slot) in slots.iter_mut().enumerate() {
let f1 = lo[lane] + hi[lane];
let f0 = lo[lane] + x[lane] * f1;
*slot = (f0 + beta * (f0 + f1)).to_repr();
}
}
let (tail_blocks, _) = tail_pairs.as_chunks::<2>();
for (offset, (slot, pair)) in tail.iter_mut().zip(tail_blocks).enumerate() {
let x: Ghash128 = domain_point((start + tail_offset + offset) << 1);
let lo = Ghash128::from_repr(pair[0]);
let hi = Ghash128::from_repr(pair[1]);
let f1 = lo + hi;
let f0 = lo + x * f1;
*slot = (f0 + beta * (f0 + f1)).to_repr();
}
}
fn fold_rounds_model<const W: usize>(
codeword: &[BinaryField128],
challenges: &[BinaryField128],
) -> Vec<BinaryField128> {
let offsets = lane_offsets::<[Ghash128; W]>();
fold_rounds::<W, BinaryField128, _>(
codeword,
challenges,
&|src, dst, start, beta, steps| {
fold_pairs_model::<W>(src, dst, start, beta, offsets, steps);
},
)
}
fn check_width<const W: usize>() {
let mut rng = SmallRng::seed_from_u64(0x1A5E_0FF5_E750_0000);
for len in [0usize, 2, 4, 8, 16, 1024, 2048, 4096, 8192] {
let codeword: Vec<BinaryField128> = (0..len).map(|_| rng.random()).collect();
let beta: BinaryField128 = rng.random();
assert_eq!(
fold_rounds_model::<W>(&codeword, &[beta]),
fold_codeword_tower_reference(&codeword, beta),
"W={W} len={len}"
);
}
let shapes = [1, 3, 6, 12]
.into_iter()
.flat_map(|log_len| {
ARITIES
.into_iter()
.filter(move |&arity| arity <= log_len)
.map(move |arity| (log_len, arity))
})
.chain([(14, 2)]);
for (log_len, arity) in shapes {
let codeword: Vec<BinaryField128> = (0..1 << log_len).map(|_| rng.random()).collect();
let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
let expected = challenges.iter().fold(codeword.clone(), |word, &beta| {
fold_codeword_tower_reference(&word, beta)
});
assert_eq!(
fold_rounds_model::<W>(&codeword, &challenges),
expected,
"W={W} log_len={log_len} arity={arity}"
);
}
}
#[test]
fn the_lane_arithmetic_holds_at_every_packing_width() {
check_width::<1>();
check_width::<2>();
check_width::<4>();
check_width::<8>();
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn every_batch_shape_matches_the_tower_reference(
(log_len, arity) in (1usize..=14)
.prop_flat_map(|log_len| (Just(log_len), 1..=log_len)),
seed: u64,
) {
let mut rng = SmallRng::seed_from_u64(seed);
let codeword: Vec<BinaryField128> = (0..1 << log_len).map(|_| rng.random()).collect();
let challenges: Vec<BinaryField128> = (0..arity).map(|_| rng.random()).collect();
let expected = challenges.iter().fold(codeword.clone(), |word, &beta| {
fold_codeword_tower_reference(&word, beta)
});
prop_assert_eq!(&fold_rounds_model::<1>(&codeword, &challenges), &expected);
prop_assert_eq!(&fold_rounds_model::<2>(&codeword, &challenges), &expected);
prop_assert_eq!(&fold_rounds_model::<4>(&codeword, &challenges), &expected);
prop_assert_eq!(&fold_rounds_model::<8>(&codeword, &challenges), &expected);
}
}
#[test]
fn the_packed_fold_matches_the_tower_reference() {
let mut rng = SmallRng::seed_from_u64(0xF01D_0BA5_15C0_DE00);
for len in [0usize, 2, 4, 8, 16, 64, 1024, 2048, 4096, 8192, 65536] {
let codeword: Vec<BinaryField128> = (0..len).map(|_| rng.random()).collect();
let beta: BinaryField128 = rng.random();
assert_eq!(
fold_codeword(&codeword, beta),
fold_codeword_tower_reference(&codeword, beta),
"len={len}"
);
}
}
#[test]
fn the_whole_fold_chain_matches_the_tower_reference() {
let mut rng = SmallRng::seed_from_u64(0x0C0F_FEE0_DEAD_BEEF);
let start: Vec<BinaryField128> = (0..8192).map(|_| rng.random()).collect();
let betas: Vec<BinaryField128> = (0..13).map(|_| rng.random()).collect();
let mut packed = start.clone();
let mut reference = start;
for (round, &beta) in betas.iter().enumerate() {
packed = fold_codeword(&packed, beta);
reference = fold_codeword_tower_reference(&reference, beta);
assert_eq!(packed, reference, "round={round}");
}
assert_eq!(packed.len(), 1);
assert_eq!(reference.len(), 1);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn folding_the_codeword_equals_binding_the_lowest_variable(
log_inv_rate in 0usize..=2,
log_n in 1usize..=7,
raw in prop::collection::vec(any::<u128>(), 128),
beta_raw: u128,
) {
let n = 1usize << log_n;
let coeffs: Vec<BinaryField128> = raw[..n]
.iter()
.copied()
.map(BinaryField128::from_repr)
.collect();
let beta = BinaryField128::from_repr(beta_raw);
let mut message = coeffs.clone();
message.resize(n << log_inv_rate, BinaryField128::ZERO);
let bound = Poly::new(coeffs).fix_suffix_var(beta);
let mut bound_message = bound.into_evals();
bound_message.resize(bound_message.len() << log_inv_rate, BinaryField128::ZERO);
prop_assert_eq!(
fold_codeword(&encode(&message), beta),
encode(&bound_message)
);
}
}
#[test]
fn the_pair_form_agrees_with_the_vector_form() {
let mut rng = SmallRng::seed_from_u64(0x9E37_79B9_7F4A_7C15);
let codeword: Vec<BinaryField128> = (0..4096).map(|_| rng.random()).collect();
let beta: BinaryField128 = rng.random();
let folded = fold_codeword(&codeword, beta);
for (j, &value) in folded.iter().enumerate() {
assert_eq!(
value,
fold_pair(j, beta, codeword[2 * j], codeword[2 * j + 1]),
"j={j}"
);
}
}
#[test]
#[should_panic(expected = "codeword length must be a power of two")]
fn an_odd_length_codeword_is_rejected() {
let odd = [BinaryField128::ONE; 3];
let _ = fold_codeword(&odd, BinaryField128::ONE);
}
#[test]
#[should_panic(expected = "codeword length must be a power of two")]
fn an_even_non_power_of_two_codeword_is_rejected() {
let six = [BinaryField128::ONE; 6];
let _ = fold_codeword(&six, BinaryField128::ONE);
}
#[test]
fn an_empty_codeword_folds_to_an_empty_one() {
assert!(fold_codeword::<BinaryField128, _>(&[], BinaryField128::ONE).is_empty());
}
#[test]
fn a_single_symbol_codeword_folds_to_an_empty_one() {
assert!(fold_codeword(&[BinaryField128::ONE], BinaryField128::ONE).is_empty());
}
#[test]
#[should_panic(expected = "a fold runs at least one round")]
fn a_fold_with_no_challenges_is_rejected() {
let codeword = [BinaryField128::ONE; 4];
let _ = fold_rounds_model::<1>(&codeword, &[]);
}
#[test]
fn the_novel_basis_fold_is_a_different_operation() {
let mut rng = SmallRng::seed_from_u64(0x51DE);
let j = 5usize;
let x: BinaryField128 = domain_point(2 * j);
let lo: BinaryField128 = rng.random();
let hi: BinaryField128 = rng.random();
let beta: BinaryField128 = rng.random();
let f1 = lo + hi;
let f0 = lo + x * f1;
let novel_basis_fold = lo + (x + beta) * (lo + hi);
assert_eq!(
fold_pair(j, beta, lo, hi) + novel_basis_fold,
beta * f0,
"the evaluation-basis and novel-basis folds differ by beta * f_0"
);
}
proptest! {
#[test]
fn folding_is_affine_in_the_challenge(
lo_raw: u128, hi_raw: u128, b0_raw: u128, b1_raw: u128, j in 0usize..64,
) {
let lo = BinaryField128::from_repr(lo_raw);
let hi = BinaryField128::from_repr(hi_raw);
let b0 = BinaryField128::from_repr(b0_raw);
let b1 = BinaryField128::from_repr(b1_raw);
let x: BinaryField128 = domain_point(2 * j);
let f1 = lo + hi;
let f0 = lo + x * f1;
prop_assert_eq!(
fold_pair(j, b0, lo, hi) + fold_pair(j, b1, lo, hi),
(b0 + b1) * (f0 + f1)
);
prop_assert_eq!(fold_pair(j, BinaryField128::ZERO, lo, hi), f0);
}
}
}