From 4044b4d2f5044eaed6a175b02bcd485c776297b8 Mon Sep 17 00:00:00 2001 From: John Wu Date: Wed, 3 Jun 2026 17:02:05 -0700 Subject: [PATCH 01/49] perf(piop): avoid clones in virtual bit-op evals Use borrowed/in-place field addition while accumulating rotated and shifted binary polynomial evaluations. These paths run once per relevant set bit across each virtual bit-op or shifted bit-slice column, so avoiding clones removes a large amount of temporary field-element copying without changing the immediate-reduction semantics. --- piop/src/combined_poly_resolver.rs | 2 +- piop/src/ideal_check/combined_poly_builder.rs | 2 +- piop/src/lookup/booleanity.rs | 2 +- test-uair/src/sha_ecdsa.rs | 6 ++++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/piop/src/combined_poly_resolver.rs b/piop/src/combined_poly_resolver.rs index c57bb2d0..1e5e4c88 100644 --- a/piop/src/combined_poly_resolver.rs +++ b/piop/src/combined_poly_resolver.rs @@ -125,7 +125,7 @@ where continue; } if let Some(w) = &weights[i] { - acc += w.clone(); + acc += w; } } acc.into_inner() diff --git a/piop/src/ideal_check/combined_poly_builder.rs b/piop/src/ideal_check/combined_poly_builder.rs index 955121eb..183aaac4 100644 --- a/piop/src/ideal_check/combined_poly_builder.rs +++ b/piop/src/ideal_check/combined_poly_builder.rs @@ -6,7 +6,7 @@ use crate::{ scalar_proj_cache::ScalarProjCache, }; use crypto_primitives::PrimeField; -use num_traits::{ConstZero, Zero}; +use num_traits::Zero; use std::cell::RefCell; use zinc_poly::{ EvaluationError, diff --git a/piop/src/lookup/booleanity.rs b/piop/src/lookup/booleanity.rs index 0de0f1fb..592a6796 100644 --- a/piop/src/lookup/booleanity.rs +++ b/piop/src/lookup/booleanity.rs @@ -132,7 +132,7 @@ where // visits each coefficient once in O(D). for (bit_idx, coeff) in bp.iter().enumerate() { if coeff.into_inner() { - accs[bit_idx] = accs[bit_idx].clone() + eq_t.clone(); + accs[bit_idx] += eq_t; } } } diff --git a/test-uair/src/sha_ecdsa.rs b/test-uair/src/sha_ecdsa.rs index a0a33d8c..97c80dc3 100644 --- a/test-uair/src/sha_ecdsa.rs +++ b/test-uair/src/sha_ecdsa.rs @@ -80,15 +80,17 @@ use zinc_uair::{ use crate::{ GenerateRandomTrace, - ecdsa::{self, FINAL_ROW as ECDSA_FINAL_ROW, NUM_SHAMIR_ROUNDS}, + ecdsa, ecdsa_doubling::{EC_FP_INT_LIMBS, EcdsaFpRing}, sha256::{self, Sha256CompressionSliceUair, Sha256Ideal}, }; +#[cfg(test)] +use crate::ecdsa::FINAL_ROW as ECDSA_FINAL_ROW; use crypto_primitives::crypto_bigint_int::Int; // Re-export for convenience. -pub use crate::ecdsa::FINAL_ROW; +pub use crate::ecdsa::{FINAL_ROW, NUM_SHAMIR_ROUNDS}; // --------------------------------------------------------------------------- // Column layout for the merged trace. From 69fe0d5a89a521339323dbae287c9dcc5d2eb6b6 Mon Sep 17 00:00:00 2001 From: John Wu Date: Wed, 3 Jun 2026 22:25:36 -0700 Subject: [PATCH 02/49] Add arkworks MSM commitment backend --- Cargo.toml | 1 + zip-plus/Cargo.toml | 10 +- zip-plus/benches/msm_commitment_benches.rs | 104 ++++ zip-plus/src/pcs.rs | 1 + zip-plus/src/pcs/msm_commitment.rs | 667 +++++++++++++++++++++ 5 files changed, 782 insertions(+), 1 deletion(-) create mode 100644 zip-plus/benches/msm_commitment_benches.rs create mode 100644 zip-plus/src/pcs/msm_commitment.rs diff --git a/Cargo.toml b/Cargo.toml index 1b877138..aa232980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ crypto-bigint = { version = "= 0.7.0-rc.9", features = ["zeroize"] } criterion = "0.7.0" derive_more = { version = "2.1.1", features = ["full"] } itertools = "0.14" +num-integer = "0.1" num-traits = "0.2" proptest = "1.9.0" rand = "0.9" diff --git a/zip-plus/Cargo.toml b/zip-plus/Cargo.toml index 9d63f86c..e8949a65 100644 --- a/zip-plus/Cargo.toml +++ b/zip-plus/Cargo.toml @@ -17,11 +17,14 @@ zinc-transcript = { workspace = true } zinc-utils = { workspace = true } ark-ff = { version = "0.5.0", default-features = false } +ark-ec = { version = "0.5.0", default-features = false } ark-poly = { version = "0.5.0", default-features = false } +ark-std = { version = "0.5.0", default-features = false, features = ["std", "getrandom"] } itertools = { workspace = true } thiserror = { workspace = true } crypto-bigint = { workspace = true } +num-integer = { workspace = true } num-traits = { workspace = true } rand = { workspace = true } rand_core = { workspace = true } @@ -33,6 +36,7 @@ uninit = "0.6.2" zstd = "0.13" [dev-dependencies] +ark-bn254 = { version = "0.5.0", default-features = false, features = ["curve"] } criterion = { workspace = true } proptest = { workspace = true } @@ -40,7 +44,7 @@ proptest = { workspace = true } workspace = true [features] -parallel = ["dep:rayon", "zinc-utils/parallel", "zinc-poly/parallel", "ark-ff/parallel", "ark-poly/parallel"] +parallel = ["dep:rayon", "zinc-utils/parallel", "zinc-poly/parallel", "ark-ff/parallel", "ark-ec/parallel", "ark-poly/parallel"] simd = ["zinc-poly/simd"] unchecked = [] @@ -51,3 +55,7 @@ harness = false [[bench]] name = "zip_plus_benches" harness = false + +[[bench]] +name = "msm_commitment_benches" +harness = false diff --git a/zip-plus/benches/msm_commitment_benches.rs b/zip-plus/benches/msm_commitment_benches.rs new file mode 100644 index 00000000..d17e9db9 --- /dev/null +++ b/zip-plus/benches/msm_commitment_benches.rs @@ -0,0 +1,104 @@ +use ark_bn254::{Fr, G1Affine, G1Projective}; +use ark_ec::{CurveGroup, PrimeGroup}; +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use std::hint::black_box; +use zip_plus::pcs::msm_commitment::{ + BoolSubsetMsm, MsmCommitmentEngine, ScalarPippengerMsm, U8BucketMsm, +}; + +fn fr(value: usize) -> Fr { + Fr::from(u64::try_from(value).expect("benchmark value must fit into u64")) +} + +fn setup(width: usize, n: usize) -> zip_plus::pcs::msm_commitment::MsmCommitmentKey { + let generator = G1Projective::generator(); + let bases = (1..=width) + .map(|idx| (generator * fr(idx)).into_affine()) + .collect(); + let h = generator * fr(width + 1); + let (ck, _) = MsmCommitmentEngine::::setup_from_bases(width, bases, h) + .expect("benchmark setup must be valid"); + let _blind = MsmCommitmentEngine::::blind(&ck, n); + ck +} + +fn bool_values(n: usize) -> Vec { + (0..n).map(|idx| idx % 3 == 0 || idx % 11 == 1).collect() +} + +fn u8_values(n: usize, modulus: u8) -> Vec { + (0..n) + .map(|idx| { + let value = (idx * 17 + 5) % usize::from(modulus); + u8::try_from(value).expect("benchmark u8 value must fit") + }) + .collect() +} + +fn scalar_values(values: &[u8]) -> Vec { + values + .iter() + .map(|value| Fr::from(u64::from(*value))) + .collect() +} + +fn msm_commitment_benches(c: &mut Criterion) { + let width = 64; + let n = width * 1024; + let ck = setup(width, n); + let blind = MsmCommitmentEngine::::blind(&ck, n); + let bools = bool_values(n); + let u8_small = u8_values(n, 32); + let u8_full = u8_values(n, 255); + let scalars = scalar_values(&u8_full); + + let mut group = c.benchmark_group("msm_commitment"); + group.bench_with_input(BenchmarkId::new("bool_subset", width), &width, |b, _| { + b.iter(|| { + MsmCommitmentEngine::::commit_with::>( + black_box(&ck), + black_box(&bools), + black_box(&blind), + ) + .expect("bool benchmark commit must succeed") + }); + }); + group.bench_with_input(BenchmarkId::new("u8_0_32", width), &width, |b, _| { + b.iter(|| { + MsmCommitmentEngine::::commit_with::( + black_box(&ck), + black_box(&u8_small), + black_box(&blind), + ) + .expect("u8 small benchmark commit must succeed") + }); + }); + group.bench_with_input(BenchmarkId::new("u8_0_255", width), &width, |b, _| { + b.iter(|| { + MsmCommitmentEngine::::commit_with::( + black_box(&ck), + black_box(&u8_full), + black_box(&blind), + ) + .expect("u8 full benchmark commit must succeed") + }); + }); + group.bench_with_input( + BenchmarkId::new("scalar_pippenger", width), + &width, + |b, _| { + b.iter(|| { + MsmCommitmentEngine::::commit_with::( + black_box(&ck), + black_box(&scalars), + black_box(&blind), + ) + .expect("scalar benchmark commit must succeed") + }); + }, + ); + group.finish(); +} + +criterion_group!(benches, msm_commitment_benches); +criterion_main!(benches); diff --git a/zip-plus/src/pcs.rs b/zip-plus/src/pcs.rs index da82e61f..1d95af67 100644 --- a/zip-plus/src/pcs.rs +++ b/zip-plus/src/pcs.rs @@ -5,6 +5,7 @@ mod phase_verify; pub use phase_prove::ZipPlusProveByteBreakdown; pub use phase_verify::{VerifyPreOpen, VerifyPreOpenReads}; pub mod folding; +pub mod msm_commitment; pub mod multi_zip; pub mod structs; #[cfg(test)] diff --git a/zip-plus/src/pcs/msm_commitment.rs b/zip-plus/src/pcs/msm_commitment.rs new file mode 100644 index 00000000..9f9432b8 --- /dev/null +++ b/zip-plus/src/pcs/msm_commitment.rs @@ -0,0 +1,667 @@ +#![allow(clippy::arithmetic_side_effects)] +#![allow(clippy::cast_possible_truncation)] +#![allow(clippy::cast_possible_wrap)] +#![allow(clippy::cast_sign_loss)] + +use std::marker::PhantomData; + +use ark_ec::{AffineRepr, CurveGroup}; +use ark_ff::{AdditiveGroup, BigInteger, One, PrimeField, UniformRand, Zero}; +use num_integer::Integer; +use thiserror::Error; + +#[derive(Clone, Debug)] +pub struct MsmCommitmentKey { + pub(crate) num_cols: usize, + pub(crate) bases: Vec, + pub(crate) h: C::Group, +} + +#[derive(Clone, Debug)] +#[allow(dead_code)] +pub struct MsmVerifierKey { + pub(crate) num_cols: usize, + pub(crate) bases: Vec, + pub(crate) h: C::Group, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MsmCommitment { + pub(crate) comm: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MsmBlind { + pub(crate) blind: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MsmCommitmentEngine(PhantomData); + +#[derive(Clone, Debug, PartialEq, Eq, Error)] +pub enum MsmError { + #[error("MSM commitment width must be non-zero")] + InvalidWidth, + #[error("MSM commitment expected {expected} bases, got {actual}")] + BaseCountMismatch { expected: usize, actual: usize }, + #[error("MSM commitment expected {expected} blinds, got {actual}")] + BlindCountMismatch { expected: usize, actual: usize }, + #[error("MSM row length must be at most {max}, got {actual}")] + RowLengthMismatch { max: usize, actual: usize }, + #[error("MSM commitment expected {expected} row commitments, got {actual}")] + CommitmentShapeMismatch { expected: usize, actual: usize }, + #[error("MSM window size must be in 1..usize::BITS, got {0}")] + InvalidWindowBits(usize), +} + +pub trait RowMsmStrategy +where + C: AffineRepr, + V: Copy + Send + Sync, +{ + fn precompute_ck(ck: &MsmCommitmentKey); + + fn msm_row(ck: &MsmCommitmentKey, values: &[V]) -> Result; + + fn is_zero(value: V) -> bool; + + fn to_scalar(value: V) -> C::ScalarField; +} + +pub struct BoolSubsetMsm; +pub struct U8BucketMsm; +pub struct ScalarPippengerMsm; + +impl MsmCommitmentEngine { + pub fn setup_from_bases( + width: usize, + bases: Vec, + h: C::Group, + ) -> Result<(MsmCommitmentKey, MsmVerifierKey), MsmError> { + if width == 0 { + return Err(MsmError::InvalidWidth); + } + if bases.len() != width { + return Err(MsmError::BaseCountMismatch { + expected: width, + actual: bases.len(), + }); + } + + let vk = MsmVerifierKey { + num_cols: width, + bases: bases.clone(), + h, + }; + let ck = MsmCommitmentKey { + num_cols: width, + bases, + h, + }; + + Ok((ck, vk)) + } + + pub fn precompute_ck(ck: &MsmCommitmentKey) { + ScalarPippengerMsm::precompute_ck(ck); + } + + pub fn blind(ck: &MsmCommitmentKey, n: usize) -> MsmBlind { + let num_rows = num_rows(n, ck.num_cols).unwrap_or(0); + let mut rng = ark_std::rand::thread_rng(); + let blind = (0..num_rows) + .map(|_| C::ScalarField::rand(&mut rng)) + .collect(); + MsmBlind { blind } + } + + pub fn commit_with( + ck: &MsmCommitmentKey, + values: &[V], + blind: &MsmBlind, + ) -> Result, MsmError> + where + V: Copy + Send + Sync, + S: RowMsmStrategy, + { + let expected_rows = num_rows(values.len(), ck.num_cols)?; + if blind.blind.len() != expected_rows { + return Err(MsmError::BlindCountMismatch { + expected: expected_rows, + actual: blind.blind.len(), + }); + } + + let mut comm = Vec::with_capacity(expected_rows); + for (row_idx, row) in values.chunks(ck.num_cols).enumerate() { + let mut row_comm = if row.iter().copied().all(S::is_zero) { + C::Group::zero() + } else { + S::msm_row(ck, row)? + }; + row_comm += ck.h * blind.blind[row_idx]; + comm.push(row_comm); + } + + Ok(MsmCommitment { comm }) + } + + pub fn commit( + ck: &MsmCommitmentKey, + values: &[C::ScalarField], + blind: &MsmBlind, + ) -> Result, MsmError> { + Self::commit_with::(ck, values, blind) + } + + pub fn commit_zeros( + ck: &MsmCommitmentKey, + n: usize, + blind: &MsmBlind, + ) -> Result, MsmError> { + let expected_rows = num_rows(n, ck.num_cols)?; + if blind.blind.len() != expected_rows { + return Err(MsmError::BlindCountMismatch { + expected: expected_rows, + actual: blind.blind.len(), + }); + } + + let comm = blind.blind.iter().map(|r| ck.h * r).collect(); + Ok(MsmCommitment { comm }) + } + + pub fn check_commitment( + comm: &MsmCommitment, + n: usize, + width: usize, + ) -> Result<(), MsmError> { + let expected_rows = num_rows(n, width)?; + if comm.comm.len() != expected_rows { + return Err(MsmError::CommitmentShapeMismatch { + expected: expected_rows, + actual: comm.comm.len(), + }); + } + Ok(()) + } +} + +impl RowMsmStrategy + for BoolSubsetMsm +{ + fn precompute_ck(_ck: &MsmCommitmentKey) {} + + fn msm_row(ck: &MsmCommitmentKey, values: &[bool]) -> Result { + validate_row_len(ck, values.len())?; + validate_window_bits(WINDOW_BITS)?; + + let mut acc = C::Group::zero(); + for (window_idx, bits) in values.chunks(WINDOW_BITS).enumerate() { + let start = window_idx * WINDOW_BITS; + let end = start + bits.len(); + let table = subset_table::(&ck.bases[start..end])?; + acc += table[bit_mask(bits)]; + } + Ok(acc) + } + + fn is_zero(value: bool) -> bool { + !value + } + + fn to_scalar(value: bool) -> C::ScalarField { + if value { + C::ScalarField::one() + } else { + C::ScalarField::zero() + } + } +} + +impl RowMsmStrategy for U8BucketMsm { + fn precompute_ck(_ck: &MsmCommitmentKey) {} + + fn msm_row(ck: &MsmCommitmentKey, values: &[u8]) -> Result { + validate_row_len(ck, values.len())?; + + let max_value = values.iter().copied().max().unwrap_or(0); + if max_value == 0 { + return Ok(C::Group::zero()); + } + + let mut buckets = vec![C::Group::zero(); usize::from(max_value)]; + for (&value, base) in values.iter().zip(ck.bases.iter()) { + if value != 0 { + buckets[usize::from(value) - 1] += base; + } + } + + Ok(bucket_running_sum(&buckets)) + } + + fn is_zero(value: u8) -> bool { + value == 0 + } + + fn to_scalar(value: u8) -> C::ScalarField { + C::ScalarField::from(u64::from(value)) + } +} + +impl RowMsmStrategy for ScalarPippengerMsm { + fn precompute_ck(_ck: &MsmCommitmentKey) {} + + fn msm_row(ck: &MsmCommitmentKey, values: &[C::ScalarField]) -> Result { + validate_row_len(ck, values.len())?; + signed_window_pippenger::(values, &ck.bases[..values.len()]) + } + + fn is_zero(value: C::ScalarField) -> bool { + value.is_zero() + } + + fn to_scalar(value: C::ScalarField) -> C::ScalarField { + value + } +} + +fn num_rows(n: usize, width: usize) -> Result { + if width == 0 { + return Err(MsmError::InvalidWidth); + } + Ok(::div_ceil(&n, &width)) +} + +fn validate_row_len( + ck: &MsmCommitmentKey, + actual: usize, +) -> Result<(), MsmError> { + if actual > ck.num_cols { + return Err(MsmError::RowLengthMismatch { + max: ck.num_cols, + actual, + }); + } + Ok(()) +} + +fn validate_window_bits(window_bits: usize) -> Result<(), MsmError> { + if window_bits == 0 || window_bits >= usize::BITS as usize { + return Err(MsmError::InvalidWindowBits(window_bits)); + } + Ok(()) +} + +fn bit_mask(bits: &[bool]) -> usize { + bits.iter().enumerate().fold( + 0usize, + |mask, (idx, bit)| { + if *bit { mask | (1usize << idx) } else { mask } + }, + ) +} + +fn subset_table(bases: &[C]) -> Result, MsmError> { + validate_window_bits(bases.len())?; + let table_len = 1usize << bases.len(); + let mut table = vec![C::Group::zero(); table_len]; + + for mask in 1..table_len { + let bit = mask.trailing_zeros() as usize; + let previous = mask & !(1usize << bit); + table[mask] = table[previous] + bases[bit]; + } + + Ok(table) +} + +fn bucket_running_sum(buckets: &[G]) -> G { + let mut acc = G::zero(); + let mut running_sum = G::zero(); + for bucket in buckets.iter().rev() { + running_sum += bucket; + acc += running_sum; + } + acc +} + +fn signed_window_pippenger( + scalars: &[C::ScalarField], + bases: &[C], +) -> Result { + if scalars.len() != bases.len() { + return Err(MsmError::BaseCountMismatch { + expected: scalars.len(), + actual: bases.len(), + }); + } + if scalars.is_empty() { + return Ok(C::Group::zero()); + } + + let window_bits = scalar_window_bits(scalars.len()); + validate_window_bits(window_bits)?; + + let num_bits = C::ScalarField::MODULUS_BIT_SIZE as usize; + let segments = ::div_ceil(&num_bits, &window_bits); + let total_segments = segments + 1; + let n = scalars.len(); + let half = 1usize << (window_bits - 1); + let full = 1usize << window_bits; + let mut signed_digits = vec![0isize; total_segments * n]; + let mut carries = vec![0usize; n]; + + for (j, scalar) in scalars.iter().enumerate() { + let bits = scalar.into_bigint().to_bits_le(); + for segment in 0..segments { + let offset = segment * n; + let raw = window_value(&bits, segment * window_bits, window_bits) + carries[j]; + carries[j] = 0; + if raw >= half { + signed_digits[offset + j] = -isize::try_from(full - raw) + .map_err(|_| MsmError::InvalidWindowBits(window_bits))?; + carries[j] = 1; + } else { + signed_digits[offset + j] = + isize::try_from(raw).map_err(|_| MsmError::InvalidWindowBits(window_bits))?; + } + } + } + + let mut highest_segment = segments; + let carry_offset = segments * n; + for (j, carry) in carries.iter().copied().enumerate() { + if carry != 0 { + signed_digits[carry_offset + j] = + isize::try_from(carry).map_err(|_| MsmError::InvalidWindowBits(window_bits))?; + highest_segment = segments + 1; + } + } + + let mut acc = C::Group::zero(); + for segment in (0..highest_segment).rev() { + for _ in 0..window_bits { + acc.double_in_place(); + } + + let mut buckets = vec![C::Group::zero(); half]; + let offset = segment * n; + for j in 0..n { + let digit = signed_digits[offset + j]; + if digit > 0 { + buckets[digit as usize - 1] += bases[j]; + } else if digit < 0 { + buckets[(-digit) as usize - 1] -= bases[j]; + } + } + + acc += bucket_running_sum(&buckets); + } + + Ok(acc) +} + +fn scalar_window_bits(n: usize) -> usize { + if n < 4 { + 1 + } else if n < 32 { + 3 + } else { + (usize::BITS - n.leading_zeros()) as usize + } +} + +fn window_value(bits: &[bool], start: usize, width: usize) -> usize { + (0..width).fold(0usize, |value, bit_idx| { + if bits.get(start + bit_idx).copied().unwrap_or(false) { + value | (1usize << bit_idx) + } else { + value + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + use ark_bn254::{Fr, G1Affine, G1Projective}; + use ark_ec::PrimeGroup; + use ark_ff::UniformRand; + + type TestCurve = G1Affine; + + fn fr(value: usize) -> Fr { + Fr::from(u64::try_from(value).expect("test value must fit into u64")) + } + + fn setup(width: usize) -> (MsmCommitmentKey, MsmVerifierKey) { + let generator = G1Projective::generator(); + let bases = (1..=width) + .map(|idx| (generator * fr(idx)).into_affine()) + .collect(); + let h = generator * fr(width + 1); + MsmCommitmentEngine::::setup_from_bases(width, bases, h) + .expect("valid test setup") + } + + fn blind(width: usize, n: usize) -> MsmBlind { + let rows = ::div_ceil(&n, &width); + MsmBlind { + blind: (0..rows).map(|idx| fr(idx + 11)).collect(), + } + } + + fn bool_values(n: usize) -> Vec { + (0..n).map(|idx| idx % 3 == 0 || idx % 7 == 1).collect() + } + + fn u8_values(n: usize, modulus: u8) -> Vec { + (0..n) + .map(|idx| { + let value = (idx * 17 + 5) % usize::from(modulus); + u8::try_from(value).expect("test u8 value must fit") + }) + .collect() + } + + fn scalars_from_bool(values: &[bool]) -> Vec { + values + .iter() + .map(|value| if *value { Fr::one() } else { Fr::zero() }) + .collect() + } + + fn scalars_from_u8(values: &[u8]) -> Vec { + values + .iter() + .map(|value| Fr::from(u64::from(*value))) + .collect() + } + + fn naive_scalar_commit( + ck: &MsmCommitmentKey, + values: &[Fr], + blind: &MsmBlind, + ) -> MsmCommitment { + let comm = values + .chunks(ck.num_cols) + .enumerate() + .map(|(row_idx, row)| { + let mut acc = G1Projective::zero(); + for (scalar, base) in row.iter().zip(ck.bases.iter()) { + acc += *base * scalar; + } + acc += ck.h * blind.blind[row_idx]; + acc + }) + .collect(); + MsmCommitment { comm } + } + + #[test] + fn bool_commit_matches_scalar_commit_for_configured_widths() { + for width in [8, 32, 64] { + let (ck, _) = setup(width); + let n = width * 3 + 5; + let values = bool_values(n); + let scalars = scalars_from_bool(&values); + let blind = blind(width, n); + + let bool_comm = + MsmCommitmentEngine::::commit_with::>( + &ck, &values, &blind, + ) + .expect("bool commit must succeed"); + let scalar_comm = MsmCommitmentEngine::::commit(&ck, &scalars, &blind) + .expect("scalar commit must succeed"); + + assert_eq!(bool_comm, scalar_comm); + } + } + + #[test] + fn u8_commit_matches_scalar_commit_for_configured_widths() { + for width in [8, 32, 64] { + let (ck, _) = setup(width); + let n = width * 2 + width / 2 + 1; + let cases = [vec![0; n], vec![1; n], u8_values(n, 32), u8_values(n, 255)]; + + for values in cases { + let scalars = scalars_from_u8(&values); + let blind = blind(width, n); + + let u8_comm = MsmCommitmentEngine::::commit_with::( + &ck, &values, &blind, + ) + .expect("u8 commit must succeed"); + let scalar_comm = MsmCommitmentEngine::::commit(&ck, &scalars, &blind) + .expect("scalar commit must succeed"); + + assert_eq!(u8_comm, scalar_comm); + } + } + } + + #[test] + fn scalar_commit_matches_naive_full_field_commit_for_configured_widths() { + let mut rng = ark_std::test_rng(); + for width in [8, 32, 64] { + let (ck, _) = setup(width); + let n = width * 2 + 3; + let values = (0..n).map(|_| Fr::rand(&mut rng)).collect::>(); + let blind = blind(width, n); + + let scalar_comm = MsmCommitmentEngine::::commit(&ck, &values, &blind) + .expect("scalar commit must succeed"); + let naive_comm = naive_scalar_commit(&ck, &values, &blind); + + assert_eq!(scalar_comm, naive_comm); + } + } + + #[test] + fn commit_zeros_matches_strategy_zero_paths() { + let width = 32; + let n = width * 2 + 9; + let (ck, _) = setup(width); + let blind = blind(width, n); + let zeros_bool = vec![false; n]; + let zeros_u8 = vec![0u8; n]; + let zeros_scalar = vec![Fr::zero(); n]; + + let zero_comm = MsmCommitmentEngine::::commit_zeros(&ck, n, &blind) + .expect("zero commit must succeed"); + let bool_comm = MsmCommitmentEngine::::commit_with::>( + &ck, + &zeros_bool, + &blind, + ) + .expect("bool zero commit must succeed"); + let u8_comm = MsmCommitmentEngine::::commit_with::( + &ck, &zeros_u8, &blind, + ) + .expect("u8 zero commit must succeed"); + let scalar_comm = MsmCommitmentEngine::::commit(&ck, &zeros_scalar, &blind) + .expect("scalar zero commit must succeed"); + + assert_eq!(zero_comm, bool_comm); + assert_eq!(zero_comm, u8_comm); + assert_eq!(zero_comm, scalar_comm); + } + + #[test] + fn changing_one_blind_changes_only_that_row_by_delta_h() { + let width = 16; + let n = width * 3; + let (ck, _) = setup(width); + let values = u8_values(n, 32); + let mut blind_a = blind(width, n); + let mut blind_b = blind_a.clone(); + let delta = fr(5); + blind_b.blind[1] += delta; + + let comm_a = MsmCommitmentEngine::::commit_with::( + &ck, &values, &blind_a, + ) + .expect("first commit must succeed"); + let comm_b = MsmCommitmentEngine::::commit_with::( + &ck, &values, &blind_b, + ) + .expect("second commit must succeed"); + + for row_idx in 0..comm_a.comm.len() { + let actual_delta = comm_b.comm[row_idx] - comm_a.comm[row_idx]; + let expected_delta = if row_idx == 1 { + ck.h * delta + } else { + G1Projective::zero() + }; + assert_eq!(actual_delta, expected_delta); + } + + blind_a.blind[1] += delta; + assert_eq!(blind_a, blind_b); + } + + #[test] + fn rejects_invalid_shapes() { + let width = 8; + let (ck, _) = setup(width); + let n = 17; + let values = vec![Fr::one(); n]; + let blind = blind(width, n); + + assert!(matches!( + MsmCommitmentEngine::::setup_from_bases(0, Vec::new(), G1Projective::zero()), + Err(MsmError::InvalidWidth) + )); + assert!(matches!( + MsmCommitmentEngine::::setup_from_bases( + width, + vec![G1Affine::generator(); width - 1], + G1Projective::generator(), + ), + Err(MsmError::BaseCountMismatch { .. }) + )); + + let short_blind = MsmBlind { + blind: blind.blind[..1].to_vec(), + }; + assert!(matches!( + MsmCommitmentEngine::::commit(&ck, &values, &short_blind), + Err(MsmError::BlindCountMismatch { .. }) + )); + + let comm = MsmCommitment { comm: Vec::new() }; + assert!(matches!( + MsmCommitmentEngine::::check_commitment(&comm, n, width), + Err(MsmError::CommitmentShapeMismatch { .. }) + )); + assert!(matches!( + MsmCommitmentEngine::::check_commitment(&comm, n, 0), + Err(MsmError::InvalidWidth) + )); + } +} From 6a7f044450e5e03c510976b1358c8104611f628c Mon Sep 17 00:00:00 2001 From: John Wu Date: Thu, 4 Jun 2026 10:03:50 -0700 Subject: [PATCH 03/49] perf: Optimize binary polynomial evaluation with delayed reduction Add a narrow delayed modular reduction path for 4-limb Montgomery fields and use it in the hot binary polynomial evaluation paths. The new `zinc_utils::delayed_reduction` module introduces: - `MontgomeryLimbs` for exposing reduced Montgomery-form field limbs. - `DelayedModularReduction` for sum-only delayed accumulation. - `BarrettReductionParams` with const `mu` computation. - A `Uint<5>` accumulator implementation for summing 4-limb field elements. - An optimized `barrett_reduce_5` path for reducing bounded 5-limb sums. - Implementations for both `MontyField<4>` and `ConstMontyField<_, 4>`. This lets binary polynomial evaluation accumulate many selected `eq(r, b)` values as raw Montgomery limbs, then perform one Barrett reduction per output coefficient instead of doing a field reduction after every conditional add. Apply the accumulator to two hot paths: - Lifted binary polynomial evaluation in the protocol layer. - Streaming shifted bit-slice evaluation in the PIOP booleanity code. The lifted binary evaluation now builds the `eq(point, *)` table once, scans the binary trace rows, conditionally adds `eq_b` into per-bit `Uint<5>` accumulators, and reduces once per bit coefficient. The shifted bit-slice streaming path uses the same delayed accumulation strategy while continuing to avoid materializing shifted bit-slice MLE buffers. Use `crypto_bigint::Uint<5>` directly as the accumulator rather than a custom wide-limb wrapper, keeping the representation aligned with the rest of the integer code. The Barrett reducer is specialized to the actual accumulator width, avoiding the unused sixth limb from the earlier 6-limb reducer shape. Also extend the relevant protocol prover/verifier bounds so the optimized paths can access Montgomery limbs, and generalize `ConstMontyField` projection support through `FromRef`. --- piop/src/lookup/booleanity.rs | 77 ++++-- protocol/benches/e2e.rs | 3 +- protocol/src/lib.rs | 132 ++++++++--- protocol/src/prover.rs | 7 + protocol/src/verifier.rs | 11 +- utils/src/delayed_reduction.rs | 418 +++++++++++++++++++++++++++++++++ utils/src/field/const_monty.rs | 19 +- utils/src/lib.rs | 1 + 8 files changed, 610 insertions(+), 58 deletions(-) create mode 100644 utils/src/delayed_reduction.rs diff --git a/piop/src/lookup/booleanity.rs b/piop/src/lookup/booleanity.rs index 592a6796..1a1f14c2 100644 --- a/piop/src/lookup/booleanity.rs +++ b/piop/src/lookup/booleanity.rs @@ -13,7 +13,9 @@ //! `max_degree + 3`. For SHA-style UAIRs (max_degree ≥ 6) with hundreds //! of bit-slice MLEs, this is a 2–2.5× saving on step 4 alone. -use crypto_primitives::{FromPrimitiveWithConfig, PrimeField, semiring::boolean::Boolean}; +use crypto_primitives::{ + FromPrimitiveWithConfig, PrimeField, crypto_bigint_uint::Uint, semiring::boolean::Boolean, +}; use num_traits::Zero; #[cfg(feature = "parallel")] use rayon::prelude::*; @@ -31,7 +33,10 @@ use zinc_uair::{ VirtualBoolSpec, }; use zinc_utils::{ - cfg_into_iter, cfg_iter, inner_transparent_field::InnerTransparentField, powers, + cfg_into_iter, cfg_iter, + delayed_reduction::{DelayedModularReduction, MontgomeryLimbs}, + inner_transparent_field::InnerTransparentField, + powers, }; /// Build the F::Inner-valued shifted bit-slice MLEs for each @@ -88,15 +93,11 @@ where /// sumcheck point `r*`. Equivalent to /// `build_shifted_bit_slice_mles(...).iter().map(evaluate_at(r*))`, /// but skips materializing the `num_shifted_specs · D` F::Inner-valued -/// MLE buffers (~`num_shifted · D · n` F::Inner allocations). Builds -/// the size-`n` `eq(r*, ·)` table once and accumulates per-bit sums -/// directly from the `BinaryPoly` trace columns in a single pass -/// per spec (t outer, bits inner — avoids `iter().nth(bit_idx)`'s -/// linear cost on custom binary-poly iterators). +/// MLE buffers (~`num_shifted · D · n` F::Inner allocations). /// -/// Used when the prover doesn't otherwise need the materialized bit- -/// slice MLEs (i.e. when no `VirtualBoolSpec` is registered — the -/// `VirtualBinaryPolySpec` path reads source binary_polys directly). +/// The hot per-bit sums are accumulated with delayed modular reduction: +/// each bit accumulator is a small `Uint<5>` integer and is reduced +/// once at the end of the spec scan. #[allow(clippy::arithmetic_side_effects)] pub fn compute_shifted_bit_slice_evals_streaming( trace_witness_binary_poly: &[DenseMultilinearExtension>], @@ -105,39 +106,42 @@ pub fn compute_shifted_bit_slice_evals_streaming( field_cfg: &F::Config, ) -> Result, ArithErrors> where - F: PrimeField + Send + Sync, + F: PrimeField + MontgomeryLimbs + Send + Sync, F::Config: Sync, { if shifted_specs.is_empty() { return Ok(Vec::new()); } - // Single shared eq table — one O(n) pass + O(n) memory across all - // (spec, bit) sums. let eq_table = build_eq_x_r_vec(point, field_cfg)?; + let reduction_params = F::barrett_reduction_params(field_cfg); - let zero = F::zero_with_cfg(field_cfg); let out: Vec = cfg_iter!(shifted_specs) .flat_map(|spec| { let col = &trace_witness_binary_poly[spec.witness_col_idx]; let shift = spec.shift_amount; let n = col.evaluations.len(); - // Per-bit accumulators, one F-element each. - let mut accs: Vec = vec![zero.clone(); D]; + let mut accs: Vec> = vec![Uint::zero(); D]; for t in 0..n { let src_t = t.checked_add(shift).filter(|&v| v < n); if let Some(s) = src_t { let bp = &col.evaluations[s]; let eq_t = &eq_table[t]; - // Walk bits in their stored order; the iterator - // visits each coefficient once in O(D). for (bit_idx, coeff) in bp.iter().enumerate() { if coeff.into_inner() { - accs[bit_idx] += eq_t; + as DelayedModularReduction>::add(&mut accs[bit_idx], eq_t); } } } } - accs + accs.into_iter() + .map(|acc| { + as DelayedModularReduction>::reduce( + acc, + field_cfg, + &reduction_params, + ) + }) + .collect::>() }) .collect(); Ok(out) @@ -1195,6 +1199,39 @@ mod tests { } } + #[test] + fn shifted_bit_slice_streaming_matches_materialized_mles() { + let cfg = test_cfg(); + let col = col_from_u8s(&[0b0000_0001, 0b0000_1010, 0b1010_0000, 0b1111_0000]); + let specs = [ + ShiftedBitSliceSpec::new(0, 1), + ShiftedBitSliceSpec::new(0, 2), + ]; + let point = vec![ + F::from_with_cfg(3u64, &cfg), + F::from_with_cfg(5u64, &cfg), + ]; + + let materialized = build_shifted_bit_slice_mles::( + std::slice::from_ref(&col), + &specs, + &cfg, + ) + .into_iter() + .map(|mle| mle.evaluate_with_config(&point, &cfg)) + .collect::, _>>() + .unwrap(); + let streaming = compute_shifted_bit_slice_evals_streaming::( + std::slice::from_ref(&col), + &specs, + &point, + &cfg, + ) + .unwrap(); + + assert_eq!(streaming, materialized); + } + #[test] fn bit_slices_round_trip_recovers_original_bits() { let cfg = test_cfg(); diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index b41b0a3f..69a6eeb7 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -317,8 +317,7 @@ where const DEGREE_PLUS_ONE: usize = 32; const INT_LIMBS: usize = U64::LIMBS; -// `fixed-prime` branch: 256-bit field modulus (4 × u64 limbs) so that the -// fixed secp256k1 base prime fits in `Fmod = Uint`. +// 256-bit field modulus (4 × u64 limbs). const FIELD_LIMBS: usize = U64::LIMBS * 4; type F = MontyField; diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 22dd910e..7235c2df 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -24,7 +24,10 @@ pub mod verifier; #[cfg(feature = "parallel")] use rayon::prelude::*; -use crypto_primitives::{ConstIntRing, ConstIntSemiring, FromWithConfig, PrimeField, Semiring}; +use crypto_primitives::{ + ConstIntRing, ConstIntSemiring, FromWithConfig, PrimeField, Semiring, + crypto_bigint_uint::Uint, +}; use std::{fmt::Debug, marker::PhantomData}; use thiserror::Error; use zinc_piop::{ @@ -47,7 +50,11 @@ use zinc_poly::{ use zinc_primality::PrimalityTest; use zinc_transcript::traits::{ConstTranscribable, GenTranscribable, Transcribable, Transcript}; use zinc_uair::{Uair, ideal::Ideal}; -use zinc_utils::{cfg_extend, cfg_into_iter, cfg_iter, named::Named}; +use zinc_utils::{ + cfg_extend, cfg_into_iter, cfg_iter, + delayed_reduction::{DelayedModularReduction, MontgomeryLimbs}, + named::Named, +}; use zip_plus::{ ZipError, code::LinearCode, @@ -505,12 +512,16 @@ fn absorb_public_columns( /// Binary columns exploit the 0/1 structure for conditional additions only. /// The `eq(point, *)` table is built once and reused across all columns. #[allow(clippy::arithmetic_side_effects)] -fn compute_lifted_evals( +fn compute_lifted_evals( point: &[F], trace_bin_poly: &[DenseMultilinearExtension>], projected_trace: &ProjectedTrace, field_cfg: &F::Config, -) -> Vec> { +) -> Vec> +where + F: PrimeField + MontgomeryLimbs + Send + Sync, + F::Config: Sync, +{ compute_lifted_evals_capped::(point, trace_bin_poly, projected_trace, field_cfg, None) } @@ -524,46 +535,48 @@ fn compute_lifted_evals( /// section here is wasted work. Pass `Some(num_total_arb_cols)` to stop /// the non-binary iter right after arbitrary cols. #[allow(clippy::arithmetic_side_effects)] -pub fn compute_lifted_evals_capped( +pub fn compute_lifted_evals_capped( point: &[F], trace_bin_poly: &[DenseMultilinearExtension>], projected_trace: &ProjectedTrace, field_cfg: &F::Config, non_binary_cap: Option, -) -> Vec> { +) -> Vec> +where + F: PrimeField + MontgomeryLimbs + Send + Sync, + F::Config: Sync, +{ let eq_table = zinc_poly::utils::build_eq_x_r_vec(point, field_cfg) .expect("compute_lifted_evals: eq table build failed"); + let reduction_params = F::barrett_reduction_params(field_cfg); let n_bin = trace_bin_poly.len(); let zero = F::zero_with_cfg(field_cfg); - // Binary columns: exploit 0/1 structure for conditional additions. - // Pack each entry's up-to-64 boolean coefficients into a u64 so we - // can (a) skip entries that are identically zero, and (b) walk only - // the SET bits via `trailing_zeros` + Brian Kernighan's clear-lowest - // instead of branching on every slot. - debug_assert!(D <= 64, "compute_lifted_evals: bitmask packing assumes D <= 64"); + // Binary columns: exploit 0/1 structure for conditional additions, + // accumulating with delayed modular reduction and reducing once per + // coefficient. let mut result: Vec> = cfg_iter!(trace_bin_poly) .map(|col| { - let mut coeffs = vec![zero.clone(); D]; + let mut coeffs = vec![Uint::<5>::default(); D]; for (b, entry) in col.iter().enumerate() { - let mut bits: u64 = 0; + let eq_b = &eq_table[b]; for (l, coeff) in entry.iter().enumerate().take(D) { if coeff.into_inner() { - bits |= 1u64 << l; + as DelayedModularReduction>::add(&mut coeffs[l], eq_b); } } - if bits == 0 { - continue; - } - let eq_b = &eq_table[b]; - let mut remaining = bits; - while remaining != 0 { - let l = remaining.trailing_zeros() as usize; - coeffs[l] += eq_b; - remaining &= remaining - 1; - } } + let coeffs: Vec = coeffs + .into_iter() + .map(|acc| { + as DelayedModularReduction>::reduce( + acc, + field_cfg, + &reduction_params, + ) + }) + .collect(); DynamicPolynomialF::new_trimmed(coeffs) }) .collect(); @@ -779,7 +792,8 @@ mod tests { use super::*; use crypto_bigint::U64; use crypto_primitives::{ - Field, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, + Field, FromWithConfig, boolean::Boolean, crypto_bigint_int::Int, + crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, }; use rand::rng; use zinc_piop::{ @@ -813,8 +827,7 @@ mod tests { }; const INT_LIMBS: usize = U64::LIMBS; - // `fixed-prime` branch: 256-bit field modulus (4 × u64 limbs) so the - // hardcoded secp256k1 base prime fits in `Fmod = Uint`. + // 256-bit field modulus (4 × u64 limbs). const FIELD_LIMBS: usize = U64::LIMBS * 4; const DEGREE_PLUS_ONE: usize = 32; @@ -851,6 +864,67 @@ mod tests { type F = MontyField; + #[test] + fn lifted_binary_eval_matches_definition() { + let field_cfg = fixed_prime::secp256k1_field_cfg::>(); + let point = vec![ + F::from_with_cfg(7u64, &field_cfg), + F::from_with_cfg(11u64, &field_cfg), + ]; + let trace_bin_poly: Vec>> = [ + [0b0000_0001, 0b0000_1010, 0b1010_0000, 0b1111_0000], + [0b1111_1111, 0b0101_0101, 0b0011_0011, 0b0000_0000], + ] + .into_iter() + .map(|patterns| { + let evaluations: Vec> = patterns + .into_iter() + .map(|p| { + let coeffs: [Boolean; 8] = + std::array::from_fn(|i| Boolean::new((p >> i) & 1 != 0)); + BinaryPoly::<8>::new(coeffs) + }) + .collect(); + DenseMultilinearExtension { + num_vars: evaluations.len().next_power_of_two().trailing_zeros() as usize, + evaluations, + } + }) + .collect(); + let projected_trace = ProjectedTrace::RowMajor(vec![ + vec![ + DynamicPolynomialF::new_trimmed(vec![F::zero_with_cfg(&field_cfg)]), + DynamicPolynomialF::new_trimmed(vec![F::zero_with_cfg(&field_cfg)]), + ]; + 4 + ]); + + let eq_table = zinc_poly::utils::build_eq_x_r_vec(&point, &field_cfg).unwrap(); + let expected: Vec<_> = trace_bin_poly + .iter() + .map(|col| { + let mut coeffs = vec![F::zero_with_cfg(&field_cfg); 8]; + for (row_idx, entry) in col.iter().enumerate() { + for (bit_idx, coeff) in entry.iter().enumerate() { + if coeff.into_inner() { + coeffs[bit_idx] += &eq_table[row_idx]; + } + } + } + DynamicPolynomialF::new_trimmed(coeffs) + }) + .collect(); + let lifted = compute_lifted_evals_capped::( + &point, + &trace_bin_poly, + &projected_trace, + &field_cfg, + None, + ); + + assert_eq!(lifted, expected); + } + #[derive(Debug, Clone)] pub struct BinPolyZipTypes {} impl ZipTypes for BinPolyZipTypes { @@ -1034,7 +1108,7 @@ mod tests { check_verification: impl Fn(Result<(), ProtocolError>>>), ) where Zt: ZincTypes, - Zt::Int: num_traits::Zero, + Zt::Int: ProjectableToField + num_traits::Zero, ::Cw: ProjectableToField, ::Eval: ProjectableToField, ::Cw: ProjectableToField, diff --git a/protocol/src/prover.rs b/protocol/src/prover.rs index 44b265d8..eae81873 100644 --- a/protocol/src/prover.rs +++ b/protocol/src/prover.rs @@ -255,6 +255,7 @@ macro_rules! impl_with_type_bounds { ::Eval: ProjectableToField, U: Uair + 'static, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Zt::Int> + for<'b> FromWithConfig<&'b ::CombR> @@ -975,6 +976,7 @@ where Zt::Int: ProjectableToField, ::Eval: ProjectableToField, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Zt::Int> + for<'a> FromWithConfig<&'a ::CombR> @@ -1127,6 +1129,7 @@ where BinaryPoly: ProjectableToField, U: Uair + 'static, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a ZtF::Int> + for<'a> FromWithConfig<&'a ::CombR> @@ -1643,6 +1646,7 @@ where BinaryPoly: ProjectableToField, U: Uair, D>> + 'static, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Int> + for<'a> FromWithConfig<&'a Int> @@ -1708,6 +1712,7 @@ where BinaryPoly: ProjectableToField, U: Uair, D>> + 'static, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Int> + for<'a> FromWithConfig<&'a Int> @@ -1779,6 +1784,7 @@ where BinaryPoly: ProjectableToField, U: Uair, D>> + 'static, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Int> + for<'a> FromWithConfig<&'a Int> @@ -1846,6 +1852,7 @@ where BinaryPoly: ProjectableToField, U: Uair, D>> + 'static, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Int> + for<'a> FromWithConfig<&'a Int> diff --git a/protocol/src/verifier.rs b/protocol/src/verifier.rs index 855d682a..365642c5 100644 --- a/protocol/src/verifier.rs +++ b/protocol/src/verifier.rs @@ -34,8 +34,9 @@ use zinc_uair::{ ideal_collector::IdealOrZero, }; use zinc_utils::{ - add, cfg_join, from_ref::FromRef, inner_transparent_field::InnerTransparentField, - mul_by_scalar::MulByScalar, projectable_to_field::ProjectableToField, + add, cfg_join, delayed_reduction::MontgomeryLimbs, from_ref::FromRef, + inner_transparent_field::InnerTransparentField, mul_by_scalar::MulByScalar, + projectable_to_field::ProjectableToField, }; use zip_plus::{ pcs::structs::{ZipPlus, ZipPlusParams, ZipTypes}, @@ -756,6 +757,7 @@ where Zt::Int: ProjectableToField, ::Eval: ProjectableToField, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Zt::Int> + for<'b> FromWithConfig<&'b Zt::Chal> @@ -992,6 +994,7 @@ where ::Cw: ProjectableToField, ::Cw: ProjectableToField, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Zt::Int> + for<'a> FromWithConfig<&'a ::CombR> @@ -1098,6 +1101,7 @@ where ::Cw: ProjectableToField, U: Uair + 'static, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b ZtF::Int> + for<'b> FromWithConfig<&'b ::CombR> @@ -1672,6 +1676,7 @@ where ::Cw: ProjectableToField, U: Uair, D>> + 'static, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Int> + for<'b> FromWithConfig<&'b Int> @@ -1746,6 +1751,7 @@ where ::Cw: ProjectableToField, U: Uair, D>> + 'static, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Int> + for<'b> FromWithConfig<&'b Int> @@ -1821,6 +1827,7 @@ where ::Cw: ProjectableToField, U: Uair, D>> + 'static, F: InnerTransparentField + + MontgomeryLimbs + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Int> + for<'b> FromWithConfig<&'b Int> diff --git a/utils/src/delayed_reduction.rs b/utils/src/delayed_reduction.rs new file mode 100644 index 00000000..3b57dbd8 --- /dev/null +++ b/utils/src/delayed_reduction.rs @@ -0,0 +1,418 @@ +//! Delayed modular reduction helpers for fixed 4-limb Montgomery fields. +//! +//! This module is intentionally narrow: it supports summing Montgomery-form +//! field elements into a 5-limb accumulator, then reducing once with Barrett +//! reduction. The limb routines are adapted from Spartan2's MIT-licensed +//! `big_num` helpers. + +use crypto_bigint::modular::ConstMontyParams; +use crypto_primitives::{ + PrimeField, crypto_bigint_const_monty::ConstMontyField, crypto_bigint_monty::MontyField, + crypto_bigint_uint::Uint, +}; +use num_traits::Zero; + +/// Barrett reduction parameters modulo a 4-limb prime. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BarrettReductionParams { + /// The 4-limb prime modulus in little-endian limb order. + pub modulus: [u64; 4], + + /// `floor(2^512 / MODULUS)`, stored in little-endian limb order. + pub mu: [u64; 5], +} + +impl BarrettReductionParams { + #[inline(always)] + pub const fn new(modulus: [u64; 4]) -> Self { + Self { + modulus, + mu: compute_barrett_mu(modulus), + } + } +} + +/// Field types that expose reduced Montgomery-form limbs. +pub trait MontgomeryLimbs: PrimeField> + Sized { + /// Construct a field element from reduced Montgomery-form limbs. + fn from_montgomery_limbs(limbs: [u64; 4], cfg: &Self::Config) -> Self; + + /// Borrow the field element's Montgomery-form limbs. + fn montgomery_limbs(&self) -> &[u64; 4]; + + /// Return Barrett reduction parameters for this field configuration. + fn barrett_reduction_params(cfg: &Self::Config) -> BarrettReductionParams; +} + +/// Accumulator trait for delayed modular reduction. +pub trait DelayedModularReduction: Zero + Clone + Send + Sync +where + F: PrimeField, +{ + fn add(&mut self, value: &F); + fn reduce(self, cfg: &F::Config, params: &BarrettReductionParams) -> F; +} + +impl DelayedModularReduction for Uint<5> +where + F: MontgomeryLimbs + Send + Sync, +{ + #[inline(always)] + fn add(&mut self, value: &F) { + let acc = self.as_mut_words(); + let rhs = value.montgomery_limbs(); + let mut carry = 0u64; + let mut i = 0; + while i < 4 { + let (sum, c0) = acc[i].overflowing_add(rhs[i]); + let (sum, c1) = sum.overflowing_add(carry); + acc[i] = sum; + carry = (c0 as u64) + (c1 as u64); + i += 1; + } + + let old_hi = acc[4]; + acc[4] = acc[4].wrapping_add(carry); + debug_assert!( + acc[4] >= old_hi, + "Uint<5> delayed accumulator overflowed high limb" + ); + } + + #[inline(always)] + fn reduce(self, cfg: &F::Config, params: &BarrettReductionParams) -> F { + let acc = self.as_words(); + F::from_montgomery_limbs(barrett_reduce_5(acc, params), cfg) + } +} + +impl MontgomeryLimbs for ConstMontyField +where + Mod: ConstMontyParams<4>, +{ + #[inline(always)] + fn from_montgomery_limbs(limbs: [u64; 4], _cfg: &Self::Config) -> Self { + Self::new_unchecked(Uint::<4>::from_words(limbs)) + } + + #[inline(always)] + fn montgomery_limbs(&self) -> &[u64; 4] { + self.inner().as_words() + } + + #[inline(always)] + fn barrett_reduction_params(_cfg: &Self::Config) -> BarrettReductionParams { + BarrettReductionParams::new(Uint::<4>::new(*Mod::PARAMS.modulus().as_ref()).to_words()) + } +} + +impl MontgomeryLimbs for MontyField<4> { + #[inline(always)] + fn from_montgomery_limbs(limbs: [u64; 4], cfg: &Self::Config) -> Self { + Self::new_unchecked_with_cfg(Uint::<4>::from_words(limbs), cfg) + } + + #[inline(always)] + fn montgomery_limbs(&self) -> &[u64; 4] { + self.inner().as_words() + } + + #[inline(always)] + fn barrett_reduction_params(cfg: &Self::Config) -> BarrettReductionParams { + BarrettReductionParams::new(Uint::<4>::new(cfg.modulus().get()).to_words()) + } +} + +/// Barrett reduction for a 5-limb value modulo a 4-limb modulus. +/// +/// This uses the 5-limb remainder path, which is required for moduli near +/// `2^256` such as the secp256k1 base prime. +#[inline(always)] +pub fn barrett_reduce_5(c: &[u64; 5], params: &BarrettReductionParams) -> [u64; 4] { + let q1 = [c[3], c[4]]; + let q2 = mul_2x5_to_7(&q1, ¶ms.mu); + let q3 = [q2[5], q2[6]]; + + let r1 = *c; + let r2 = mul_2x4_lo5(&q3, ¶ms.modulus); + let mut r = sub::<5>(&r1, &r2); + + if r[4] != 0 || gte::<4>(&[r[0], r[1], r[2], r[3]], ¶ms.modulus) { + r = sub_5_4(&r, ¶ms.modulus); + } + + debug_assert!( + r[4] == 0 && !gte::<4>(&[r[0], r[1], r[2], r[3]], ¶ms.modulus), + "Barrett reduction produced non-canonical result" + ); + + [r[0], r[1], r[2], r[3]] +} + +#[inline(always)] +fn mul_2x5_to_7(a: &[u64; 2], b: &[u64; 5]) -> [u64; 7] { + let mut result = [0u64; 7]; + for i in 0..2 { + let mut carry = 0u128; + for j in 0..5 { + let prod = (a[i] as u128) * (b[j] as u128) + (result[i + j] as u128) + carry; + result[i + j] = prod as u64; + carry = prod >> 64; + } + result[i + 5] = carry as u64; + } + result +} + +#[inline(always)] +fn mul_2x4_lo5(a: &[u64; 2], b: &[u64; 4]) -> [u64; 5] { + let mut result = [0u64; 5]; + + let mut carry = 0u128; + for j in 0..4 { + let prod = (a[0] as u128) * (b[j] as u128) + carry; + result[j] = prod as u64; + carry = prod >> 64; + } + result[4] = carry as u64; + + carry = 0; + for j in 0..4 { + let prod = (a[1] as u128) * (b[j] as u128) + (result[1 + j] as u128) + carry; + result[1 + j] = prod as u64; + carry = prod >> 64; + } + + result +} + +#[inline(always)] +const fn gte(a: &[u64; N], b: &[u64; N]) -> bool { + let mut i = N; + while i > 0 { + i -= 1; + if a[i] > b[i] { + return true; + } + if a[i] < b[i] { + return false; + } + } + true +} + +#[inline(always)] +const fn sub(a: &[u64; N], b: &[u64; N]) -> [u64; N] { + let mut result = [0u64; N]; + let mut borrow = 0u64; + let mut i = 0; + while i < N { + let (diff, b1) = a[i].overflowing_sub(b[i]); + let (diff2, b2) = diff.overflowing_sub(borrow); + result[i] = diff2; + borrow = (b1 as u64) + (b2 as u64); + i += 1; + } + result +} + +#[inline(always)] +const fn sub_5_4(a: &[u64; 5], b: &[u64; 4]) -> [u64; 5] { + let mut result = [0u64; 5]; + let mut borrow = 0u64; + let mut i = 0; + while i < 4 { + let (diff, b1) = a[i].overflowing_sub(b[i]); + let (diff2, b2) = diff.overflowing_sub(borrow); + result[i] = diff2; + borrow = (b1 as u64) + (b2 as u64); + i += 1; + } + let (diff, _) = a[4].overflowing_sub(borrow); + result[4] = diff; + result +} + +#[inline(always)] +const fn shl(a: &[u64; N]) -> [u64; N] { + let mut result = [0u64; N]; + let mut carry = 0u64; + let mut i = 0; + while i < N { + let new_carry = a[i] >> 63; + result[i] = (a[i] << 1) | carry; + carry = new_carry; + i += 1; + } + result +} + +#[inline(always)] +const fn shr(a: &[u64; N]) -> [u64; N] { + let mut result = [0u64; N]; + let mut carry = 0u64; + let mut i = N; + while i > 0 { + i -= 1; + let new_carry = a[i] << 63; + result[i] = (a[i] >> 1) | carry; + carry = new_carry; + } + result +} + +#[inline(always)] +const fn clz(a: &[u64; N]) -> u32 { + let mut i = N; + let mut count = 0u32; + while i > 0 { + i -= 1; + if a[i] != 0 { + return count + a[i].leading_zeros(); + } + count += 64; + } + count +} + +pub const fn compute_barrett_mu(p: [u64; 4]) -> [u64; 5] { + let mut dividend: [u64; 9] = [0, 0, 0, 0, 0, 0, 0, 0, 1]; + let divisor: [u64; 9] = [p[0], p[1], p[2], p[3], 0, 0, 0, 0, 0]; + let mut quotient: [u64; 5] = [0; 5]; + + let dividend_clz = clz::<9>(÷nd); + let divisor_clz = clz::<9>(&divisor); + if divisor_clz <= dividend_clz { + return quotient; + } + + let shift_bits = divisor_clz - dividend_clz; + let mut shifted_divisor = divisor; + let whole_limbs = (shift_bits / 64) as usize; + let rem_bits = shift_bits % 64; + + if whole_limbs > 0 { + let mut i = 8; + while i >= whole_limbs { + shifted_divisor[i] = shifted_divisor[i - whole_limbs]; + if i == whole_limbs { + break; + } + i -= 1; + } + let mut j = 0; + while j < whole_limbs { + shifted_divisor[j] = 0; + j += 1; + } + } + + let mut i = 0; + while i < rem_bits { + shifted_divisor = shl::<9>(&shifted_divisor); + i += 1; + } + + let mut bit_pos = shift_bits; + loop { + if gte::<9>(÷nd, &shifted_divisor) { + dividend = sub::<9>(÷nd, &shifted_divisor); + let limb_idx = (bit_pos / 64) as usize; + let bit_idx = bit_pos % 64; + if limb_idx < 5 { + quotient[limb_idx] |= 1u64 << bit_idx; + } + } + + if bit_pos == 0 { + break; + } + bit_pos -= 1; + shifted_divisor = shr::<9>(&shifted_divisor); + } + + quotient +} + +#[cfg(test)] +mod tests { + use super::*; + use crypto_primitives::{FromWithConfig, crypto_bigint_monty::MontyField}; + + type F = MontyField<4>; + + fn secp256k1_cfg() -> ::Config { + let modulus = Uint::<4>::from_words([ + 0xFFFF_FFFE_FFFF_FC2F, + 0xFFFF_FFFF_FFFF_FFFF, + 0xFFFF_FFFF_FFFF_FFFF, + 0xFFFF_FFFF_FFFF_FFFF, + ]); + F::make_cfg(&modulus).expect("secp256k1 base field prime is valid") + } + + #[test] + fn secp256k1_barrett_params_match_expected_modulus() { + let cfg = secp256k1_cfg(); + assert_eq!( + F::barrett_reduction_params(&cfg).modulus, + [ + 0xFFFF_FFFE_FFFF_FC2F, + 0xFFFF_FFFF_FFFF_FFFF, + 0xFFFF_FFFF_FFFF_FFFF, + 0xFFFF_FFFF_FFFF_FFFF, + ], + ); + } + + #[test] + fn delayed_sum_matches_field_addition() { + let cfg = secp256k1_cfg(); + let reduction_params = F::barrett_reduction_params(&cfg); + let values: Vec = (0..512) + .map(|i| F::from_with_cfg(i as u64 + 1, &cfg)) + .collect(); + + let mut expected = F::zero_with_cfg(&cfg); + for value in &values { + expected += value; + } + + let mut acc = Uint::<5>::zero(); + for value in &values { + as DelayedModularReduction>::add(&mut acc, value); + } + + assert_eq!( + as DelayedModularReduction>::reduce(acc, &cfg, &reduction_params), + expected + ); + } + + #[test] + fn barrett_reduce_5_matches_uint_remainder_for_bounded_sum() { + let cfg = secp256k1_cfg(); + let reduction_params = F::barrett_reduction_params(&cfg); + let mut acc = Uint::<5>::zero(); + let max = -F::from_with_cfg(1u64, &cfg); + for _ in 0..512 { + as DelayedModularReduction>::add(&mut acc, &max); + } + + let wide = acc; + let modulus = Uint::<5>::from_words([ + reduction_params.modulus[0], + reduction_params.modulus[1], + reduction_params.modulus[2], + reduction_params.modulus[3], + 0, + ]); + let expected = (wide % &modulus) + .checked_resize::<4>() + .expect("remainder fits in four limbs"); + + let acc = acc.as_words(); + let reduced = barrett_reduce_5(acc, &reduction_params); + assert_eq!(Uint::<4>::from_words(reduced), expected); + } +} diff --git a/utils/src/field/const_monty.rs b/utils/src/field/const_monty.rs index 3c529850..3c785485 100644 --- a/utils/src/field/const_monty.rs +++ b/utils/src/field/const_monty.rs @@ -31,7 +31,7 @@ macro_rules! impl_from_primitive_ref { )* }; } -impl_from_primitive_ref!(u8, u16, u32, u64, u128); +impl_from_primitive_ref!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128); impl, const LIMBS: usize> FromRef> for ConstMontyForm @@ -49,14 +49,23 @@ impl, const LIMBS: usize> FromRef } } -impl, const LIMBS: usize, const LIMBS2: usize> - ProjectableToField> for Int +impl, const LIMBS: usize, const LIMBS2: usize> FromRef> + for ConstMontyField +{ + fn from_ref(value: &Int) -> Self { + value.into() + } +} + +impl, const LIMBS: usize> + ProjectableToField> for T +where + ConstMontyField: FromRef, { fn prepare_projection( _sampled_value: &ConstMontyField, ) -> impl Fn(&Self) -> ConstMontyField + Send + Sync + 'static { - // No need to read anything - |value: &Int| value.into() + |value: &T| ConstMontyField::::from_ref(value) } } diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 17ccba30..2d04445b 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -1,3 +1,4 @@ +pub mod delayed_reduction; pub mod field; pub mod from_ref; pub mod inner_product; From 7c38f5fb6f7b19827818ea3e1b3a0008bd0b56e6 Mon Sep 17 00:00:00 2001 From: John Wu Date: Thu, 4 Jun 2026 10:24:36 -0700 Subject: [PATCH 04/49] perf: stream shifted bit-slice evals in 4x prover --- protocol/src/prover.rs | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/protocol/src/prover.rs b/protocol/src/prover.rs index eae81873..9af6b687 100644 --- a/protocol/src/prover.rs +++ b/protocol/src/prover.rs @@ -2072,12 +2072,16 @@ where .iter() .map(|&idx| projected_trace_f[int_offset + idx].clone()) .collect(); - let shifted_bit_slice_mles = build_shifted_bit_slice_mles::( - &trace.binary_poly[num_pub_bin..], - uair_signature.shifted_bit_slice_specs(), - &field_cfg, - ); let virtual_specs = uair_signature.virtual_booleanity_cols(); + let shifted_bit_slice_mles = if virtual_specs.is_empty() { + Vec::new() + } else { + build_shifted_bit_slice_mles::( + &trace.binary_poly[num_pub_bin..], + uair_signature.shifted_bit_slice_specs(), + &field_cfg, + ) + }; let virtual_mles = if virtual_specs.is_empty() { Vec::new() } else { @@ -2148,11 +2152,21 @@ where cpr_ancillary, &field_cfg, )?; - let shifted_bit_slice_evals: Vec = shifted_bit_slice_mles - .into_iter() - .map(|mle| mle.evaluate_with_config(&cpr_prover_state.evaluation_point, &field_cfg)) - .collect::, _>>() - .map_err(ProtocolError::ShiftedBitSliceEval)?; + let shifted_bit_slice_evals: Vec = if shifted_bit_slice_mles.is_empty() { + compute_shifted_bit_slice_evals_streaming::( + &trace.binary_poly[num_pub_bin..], + uair_signature.shifted_bit_slice_specs(), + &cpr_prover_state.evaluation_point, + &field_cfg, + ) + .map_err(|e| ProtocolError::Booleanity(e.into()))? + } else { + shifted_bit_slice_mles + .into_iter() + .map(|mle| mle.evaluate_with_config(&cpr_prover_state.evaluation_point, &field_cfg)) + .collect::, _>>() + .map_err(ProtocolError::ShiftedBitSliceEval)? + }; cpr_proof.shifted_bit_slice_evals = shifted_bit_slice_evals; if let Some(ba) = bool_ancillary_opt { let bool_state = md_states.remove(0); From 6dab2641af0eaa56d2863e5287f23ceedb939655 Mon Sep 17 00:00:00 2001 From: John Wu Date: Thu, 4 Jun 2026 20:39:05 -0700 Subject: [PATCH 05/49] Add Hyrax PCS backend and benchmarks --- piop/src/combined_poly_resolver.rs | 42 +- piop/src/combined_poly_resolver/structs.rs | 4 +- piop/src/lookup/booleanity.rs | 126 ++- piop/src/projections.rs | 5 +- protocol/Cargo.toml | 4 + protocol/benches/e2e.rs | 682 +++++++++++-- protocol/src/fixed_prime.rs | 60 +- protocol/src/lib.rs | 214 +++- protocol/src/pcs.rs | 122 +++ protocol/src/prover.rs | 543 +++++++--- protocol/src/verifier.rs | 562 +++++++---- test-uair/src/ecdsa.rs | 84 +- test-uair/src/ecdsa_addition.rs | 13 +- test-uair/src/ecdsa_affine.rs | 20 +- test-uair/src/ecdsa_doubling.rs | 16 +- test-uair/src/lib.rs | 7 +- test-uair/src/sha256.rs | 185 ++-- test-uair/src/sha_ecdsa.rs | 93 +- uair/src/lib.rs | 21 +- zip-plus/Cargo.toml | 6 + zip-plus/benches/hyrax_commit_breakdown.rs | 250 +++++ zip-plus/benches/msm_commitment_benches.rs | 127 ++- zip-plus/src/code/iprs.rs | 3 +- zip-plus/src/merkle.rs | 6 +- zip-plus/src/pcs.rs | 2 + zip-plus/src/pcs/folding.rs | 7 +- zip-plus/src/pcs/generic.rs | 177 ++++ zip-plus/src/pcs/hyrax.rs | 1056 ++++++++++++++++++++ zip-plus/src/pcs/multi_zip.rs | 50 +- zip-plus/src/pcs/phase_prove.rs | 4 +- zip-plus/src/utils.rs | 10 +- 31 files changed, 3602 insertions(+), 899 deletions(-) create mode 100644 protocol/src/pcs.rs create mode 100644 zip-plus/benches/hyrax_commit_breakdown.rs create mode 100644 zip-plus/src/pcs/generic.rs create mode 100644 zip-plus/src/pcs/hyrax.rs diff --git a/piop/src/combined_poly_resolver.rs b/piop/src/combined_poly_resolver.rs index c57bb2d0..153dcd5b 100644 --- a/piop/src/combined_poly_resolver.rs +++ b/piop/src/combined_poly_resolver.rs @@ -5,6 +5,7 @@ mod structs; pub use structs::*; +use crate::projections::ScalarMap; use crate::{ CombFn, combined_poly_resolver::{ @@ -23,7 +24,6 @@ use itertools::Itertools; use num_traits::Zero; #[cfg(feature = "parallel")] use rayon::prelude::*; -use crate::projections::ScalarMap; use std::{cell::RefCell, marker::PhantomData, slice}; use thiserror::Error; use zinc_poly::{ @@ -283,8 +283,7 @@ impl CombinedP // `scalar_proj_cache` for details. Lazily initialized — UAIRs // that never invoke `from_ref`/`mbs` pay only the Option's // discriminant write per call. - let cache: RefCell>> = - RefCell::new(None); + let cache: RefCell>> = RefCell::new(None); let project = |scalar: &U::Scalar| -> F { if let Some(v) = cache.borrow().as_ref().and_then(|c| c.get(scalar)) { return v; @@ -573,11 +572,7 @@ impl CombinedP &proof.up_evals, uair_sig.total_cols().as_column_layout(), ), - TraceRow::from_slice_with_layout_and_bit_op( - &down_combined, - down_layout, - bit_op_count, - ), + TraceRow::from_slice_with_layout_and_bit_op(&down_combined, down_layout, bit_op_count), project, |x, y| Some(project(y) * x), ImpossibleIdeal::from_ref, @@ -735,22 +730,23 @@ mod tests { project_scalars_to_field(projected_scalars, &projecting_element).unwrap(); // Prover: prepare → MultiDegreeSumcheck → finalize - let (cpr_group, cpr_ancillary) = CombinedPolyResolver::prepare_sumcheck_group::( - &mut prover_transcript, - evaluate_trace_to_column_mles( - &ProjectedTrace::RowMajor(projected_trace), + let (cpr_group, cpr_ancillary) = + CombinedPolyResolver::prepare_sumcheck_group::( + &mut prover_transcript, + evaluate_trace_to_column_mles( + &ProjectedTrace::RowMajor(projected_trace), + &projecting_element, + ), + &ic_prover_state.evaluation_point, + &projected_scalars, + num_constraints, + num_vars, + max_degree, + &test_config(), + &trace.binary_poly, &projecting_element, - ), - &ic_prover_state.evaluation_point, - &projected_scalars, - num_constraints, - num_vars, - max_degree, - &test_config(), - &trace.binary_poly, - &projecting_element, - ) - .expect("CPR prepare failed"); + ) + .expect("CPR prepare failed"); let (md_proof, states) = MultiDegreeSumcheck::prove_as_subprotocol( &mut prover_transcript, diff --git a/piop/src/combined_poly_resolver/structs.rs b/piop/src/combined_poly_resolver/structs.rs index 0c1eab26..9b6d80b1 100644 --- a/piop/src/combined_poly_resolver/structs.rs +++ b/piop/src/combined_poly_resolver/structs.rs @@ -58,7 +58,9 @@ where let buf = self.down_evals.write_transcription_bytes_subset(buf); let buf = self.bit_slice_evals.write_transcription_bytes_subset(buf); let buf = self.bit_op_down_evals.write_transcription_bytes_subset(buf); - let buf = self.shifted_bit_slice_evals.write_transcription_bytes_subset(buf); + let buf = self + .shifted_bit_slice_evals + .write_transcription_bytes_subset(buf); assert!(buf.is_empty(), "Entire buffer should be used"); } } diff --git a/piop/src/lookup/booleanity.rs b/piop/src/lookup/booleanity.rs index 0de0f1fb..30c1f2f4 100644 --- a/piop/src/lookup/booleanity.rs +++ b/piop/src/lookup/booleanity.rs @@ -30,9 +30,7 @@ use zinc_uair::{ ShiftedBitSliceSpec, VirtualBinaryPolySource, VirtualBinaryPolySpec, VirtualBoolSource, VirtualBoolSpec, }; -use zinc_utils::{ - cfg_into_iter, cfg_iter, inner_transparent_field::InnerTransparentField, powers, -}; +use zinc_utils::{cfg_into_iter, cfg_iter, inner_transparent_field::InnerTransparentField, powers}; /// Build the F::Inner-valued shifted bit-slice MLEs for each /// `ShiftedBitSliceSpec`, in flat layout `spec*D + bit`. Each MLE has @@ -218,19 +216,15 @@ where VirtualBoolSource::SelfBitSlice { witness_col_idx, bit_idx, - } => &self_bit_slices[*witness_col_idx * D + *bit_idx] - .evaluations, + } => &self_bit_slices[*witness_col_idx * D + *bit_idx].evaluations, VirtualBoolSource::ShiftedBitSlice { shifted_spec_idx, bit_idx, - } => &shifted_bit_slice_mles - [*shifted_spec_idx * D + *bit_idx] - .evaluations, + } => &shifted_bit_slice_mles[*shifted_spec_idx * D + *bit_idx].evaluations, VirtualBoolSource::PublicBitSlice { public_col_idx, bit_idx, - } => &public_bit_slices[*public_col_idx * D + *bit_idx] - .evaluations, + } => &public_bit_slices[*public_col_idx * D + *bit_idx].evaluations, VirtualBoolSource::IntCol { witness_col_idx } => { &int_witness_cols[*witness_col_idx].evaluations } @@ -242,43 +236,38 @@ where match *coeff { 1 => { for t in 0..n { - evals[t] = - F::add_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::add_inner(&evals[t], &src[t], field_cfg); } } -1 => { for t in 0..n { - evals[t] = - F::sub_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::sub_inner(&evals[t], &src[t], field_cfg); } } 2 => { for t in 0..n { - evals[t] = - F::add_inner(&evals[t], &src[t], field_cfg); - evals[t] = - F::add_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::add_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::add_inner(&evals[t], &src[t], field_cfg); } } -2 => { for t in 0..n { - evals[t] = - F::sub_inner(&evals[t], &src[t], field_cfg); - evals[t] = - F::sub_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::sub_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::sub_inner(&evals[t], &src[t], field_cfg); } } c => { for t in 0..n { - let term = - apply_coeff_inner::(c, &src[t], field_cfg); - evals[t] = - F::add_inner(&evals[t], &term, field_cfg); + let term = apply_coeff_inner::(c, &src[t], field_cfg); + evals[t] = F::add_inner(&evals[t], &term, field_cfg); } } } } - DenseMultilinearExtension { evaluations: evals, num_vars } + DenseMultilinearExtension { + evaluations: evals, + num_vars, + } }) .collect() } @@ -314,13 +303,11 @@ where VirtualBoolSource::ShiftedBitSlice { shifted_spec_idx, bit_idx, - } => &shifted_bit_slice_evals - [*shifted_spec_idx * D + *bit_idx], + } => &shifted_bit_slice_evals[*shifted_spec_idx * D + *bit_idx], VirtualBoolSource::PublicBitSlice { public_col_idx, bit_idx, - } => &public_bit_slice_evals - [*public_col_idx * D + *bit_idx], + } => &public_bit_slice_evals[*public_col_idx * D + *bit_idx], VirtualBoolSource::IntCol { witness_col_idx } => { &int_witness_up_evals[*witness_col_idx] } @@ -456,7 +443,10 @@ where } evaluations.push(BinaryPoly::::new(coeffs.as_slice())); } - DenseMultilinearExtension { evaluations, num_vars } + DenseMultilinearExtension { + evaluations, + num_vars, + } }) .collect() } @@ -729,9 +719,8 @@ where let r1_inner = r1.inner().clone(); let one_minus_r1_inner = one_minus_r1.inner().clone(); - let mut mles: Vec> = Vec::with_capacity( - 1 + self.binary_cols.len() * D + self.extra_bit_cols.len(), - ); + let mut mles: Vec> = + Vec::with_capacity(1 + self.binary_cols.len() * D + self.extra_bit_cols.len()); mles.push(eq_folded); // BinaryPoly does not impl Index on every backend @@ -880,8 +869,7 @@ where let one = F::one_with_cfg(field_cfg); let folding_challenge: F = transcript.get_field_challenge(field_cfg); - let folding_challenge_powers: Vec = - powers(folding_challenge, one.clone(), num_bit_slices); + let folding_challenge_powers: Vec = powers(folding_challenge, one.clone(), num_bit_slices); // Pre-build E_other = eq(b', ic_evaluation_point[1..]) for the // round-1 fast path. The full-size eq_r is only needed for rounds @@ -1125,13 +1113,12 @@ pub fn verify_bit_decomposition_consistency( for (col_idx, parent_eval) in parent_evals_per_col.iter().enumerate() { let base = col_idx * bits_per_col; - let recombined = - bit_slice_evals[base..base + bits_per_col] - .iter() - .zip(&a_powers) - .fold(zero.clone(), |acc, (bit_eval, a_pow)| { - acc + bit_eval.clone() * a_pow - }); + let recombined = bit_slice_evals[base..base + bits_per_col] + .iter() + .zip(&a_powers) + .fold(zero.clone(), |acc, (bit_eval, a_pow)| { + acc + bit_eval.clone() * a_pow + }); if &recombined != parent_eval { return Err(BooleanityError::ConsistencyMismatch { @@ -1147,9 +1134,7 @@ pub fn verify_bit_decomposition_consistency( #[derive(Debug, Error)] pub enum BooleanityError { - #[error( - "wrong bit-slice evaluation count: got {got}, expected {expected}" - )] + #[error("wrong bit-slice evaluation count: got {got}, expected {expected}")] WrongBitSliceEvalCount { got: usize, expected: usize }, #[error( "bit-decomposition consistency mismatch on binary_poly column {col_idx}: got Σ a^i·bᵢ = {got:?}, expected parent eval {expected:?}" @@ -1168,9 +1153,7 @@ pub enum BooleanityError { #[cfg(test)] mod tests { use super::*; - use crypto_primitives::{ - FromWithConfig, boolean::Boolean, crypto_bigint_monty::MontyField, - }; + use crypto_primitives::{FromWithConfig, boolean::Boolean, crypto_bigint_monty::MontyField}; type F = MontyField<4>; @@ -1183,8 +1166,7 @@ mod tests { let evaluations: Vec> = patterns .iter() .map(|&p| { - let coeffs: [Boolean; 8] = - array::from_fn(|i| Boolean::new((p >> i) & 1 != 0)); + let coeffs: [Boolean; 8] = array::from_fn(|i| Boolean::new((p >> i) & 1 != 0)); BinaryPoly::<8>::new(coeffs) }) .collect(); @@ -1214,7 +1196,10 @@ mod tests { } else { zero.clone() }; - assert_eq!(bit_slices[bit].evaluations[row], want, "row {row} bit {bit}"); + assert_eq!( + bit_slices[bit].evaluations[row], want, + "row {row} bit {bit}" + ); } } } @@ -1239,13 +1224,8 @@ mod tests { a_pow = a_pow * a.clone(); } - verify_bit_decomposition_consistency( - std::slice::from_ref(&parent_eval), - &bit_evals, - &a, - 8, - ) - .expect("honest decomposition should satisfy consistency check"); + verify_bit_decomposition_consistency(std::slice::from_ref(&parent_eval), &bit_evals, &a, 8) + .expect("honest decomposition should satisfy consistency check"); } #[test] @@ -1277,7 +1257,10 @@ mod tests { &a, 4, ); - assert!(matches!(res, Err(BooleanityError::ConsistencyMismatch { .. }))); + assert!(matches!( + res, + Err(BooleanityError::ConsistencyMismatch { .. }) + )); } #[test] @@ -1306,8 +1289,14 @@ mod tests { // Mix of fully-zero, fully-one, and varying patterns to exercise all // four (A, B) cases for the XOR fold structure. let binary_cols = vec![ - col_from_u8s(&[0b00000000, 0b00010001, 0b00100010, 0b00110011, 0b01000100, 0b01010101, 0b01100110, 0b01110111]), - col_from_u8s(&[0b11110000, 0b11100001, 0b11010010, 0b11000011, 0b10110100, 0b10100101, 0b10010110, 0b10000111]), + col_from_u8s(&[ + 0b00000000, 0b00010001, 0b00100010, 0b00110011, 0b01000100, 0b01010101, 0b01100110, + 0b01110111, + ]), + col_from_u8s(&[ + 0b11110000, 0b11100001, 0b11010010, 0b11000011, 0b10110100, 0b10100101, 0b10010110, + 0b10000111, + ]), ]; let num_vars = 3; const D: usize = 8; @@ -1430,7 +1419,8 @@ mod tests { let mut std_eq_r = eq_r_full; std_eq_r.fix_variables_with_config(slice::from_ref(&r_1), &cfg); assert_eq!( - fast_mles[0].num_vars, num_vars - 1, + fast_mles[0].num_vars, + num_vars - 1, "fast-path eq_r_folded must have num_vars - 1 variables" ); assert_eq!( @@ -1441,7 +1431,8 @@ mod tests { for (idx, mut bit_mle) in bit_slices_full.into_iter().enumerate() { bit_mle.fix_variables_with_config(slice::from_ref(&r_1), &cfg); assert_eq!( - fast_mles[1 + idx].evaluations, bit_mle.evaluations, + fast_mles[1 + idx].evaluations, + bit_mle.evaluations, "fast-path bit-slice {idx} folded value must match standard fix_variables" ); } @@ -1454,6 +1445,9 @@ mod tests { let parent_evals = vec![one.clone()]; let bit_evals: Vec = vec![one.clone(), one.clone()]; let res = verify_bit_decomposition_consistency(&parent_evals, &bit_evals, &one, 8); - assert!(matches!(res, Err(BooleanityError::WrongBitSliceEvalCount { .. }))); + assert!(matches!( + res, + Err(BooleanityError::WrongBitSliceEvalCount { .. }) + )); } } diff --git a/piop/src/projections.rs b/piop/src/projections.rs index be5af276..484f920b 100644 --- a/piop/src/projections.rs +++ b/piop/src/projections.rs @@ -337,9 +337,8 @@ where { let zero_inner = F::Inner::default(); - let mut result = Vec::with_capacity( - trace.binary_poly.len() + trace.arbitrary_poly.len() + trace.int.len(), - ); + let mut result = + Vec::with_capacity(trace.binary_poly.len() + trace.arbitrary_poly.len() + trace.int.len()); let bin_proj = BinaryPoly::::prepare_projection(projecting_element); cfg_extend!( diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 8083869d..f3429c01 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -19,11 +19,15 @@ zinc-primality = { workspace = true } zinc-transcript = { workspace = true } zinc-uair = { workspace = true } zinc-utils = { workspace = true } +ark-ec = { version = "0.5.0", default-features = false } +ark-ff = { version = "0.5.0", default-features = false } [lib] bench = false [dev-dependencies] +ark-bn254 = { version = "0.5.0", default-features = false, features = ["curve"] } +ark-secp256k1 = { version = "0.5.0", default-features = false } criterion = { workspace = true } crypto-bigint = { workspace = true } rand = { workspace = true } diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index b41b0a3f..48336b79 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -1,5 +1,6 @@ #![allow(clippy::arithmetic_side_effects)] +use ark_ec::{AffineRepr, CurveGroup, PrimeGroup}; use criterion::{ BatchSize, BenchmarkGroup, BenchmarkId, Criterion, criterion_group, criterion_main, measurement::WallTime, @@ -11,6 +12,7 @@ use crypto_primitives::{ }; use rand::rng; use std::{fmt::Debug, hint::black_box, marker::PhantomData, ops::Neg}; +use zinc_poly::univariate::dynamic::over_field::DynamicPolyVecF; use zinc_poly::{ ConstCoeffBitWidth, Polynomial, univariate::{ @@ -22,13 +24,17 @@ use zinc_poly::{ use zinc_primality::{MillerRabin, PrimalityTest}; use zinc_protocol::{ FoldedZincTypes, IntFoldedZincTypes4x, Proof, ZincPlusPiop, ZincTypes, + fixed_prime::field_cfg_from_curve_scalar, + pcs::{ + AllZipPCSTypes, BinaryHyraxZipRest, PCSCommitments, PCSParams, PCSVerifierParams, + ZincPCSTypes, + }, }; use zinc_test_uair::{ BigLinearUair, BigLinearUairWithPublicInput, BinaryDecompositionUair, EC_FP_INT_LIMBS, EcdsaUair, GenerateRandomTrace, Sha256CompressionSliceUair, Sha256Ideal, ShaEcdsaUair, ShaProxy, TestUairNoMultiplication, }; -use zinc_poly::univariate::dynamic::over_field::DynamicPolyVecF; use zinc_transcript::traits::{ConstTranscribable, Transcribable}; use zinc_uair::{ Uair, UairTrace, @@ -44,9 +50,14 @@ use zinc_utils::{ projectable_to_field::ProjectableToField, }; use zip_plus::{ - code::iprs::{IprsCode, PnttConfigF65537}, + code::{ + LinearCode, + iprs::{IprsCode, PnttConfigF65537}, + }, + pcs::generic::{PCS, ZipPlusPCS}, + pcs::hyrax::{BinaryLanes, HyraxPCS}, pcs::structs::{ZipPlus, ZipPlusCommitment, ZipPlusParams, ZipTypes}, - utils::{eprint_bytes_size_breakdown, eprint_proof_size}, + utils::{eprint_bytes_size, eprint_bytes_size_breakdown, eprint_proof_size}, }; // @@ -186,20 +197,8 @@ struct GenericBenchZincTypes< )>, ); -impl - ZincTypes - for GenericBenchZincTypes< - Int, - CwR, - Chal, - Pt, - BinaryCombR, - CombR, - IntCombR, - Fmod, - PrimeTest, - D, - > +impl ZincTypes + for GenericBenchZincTypes where Int: ConstIntSemiring + for<'a> MulByScalar<&'a i64, CwR> @@ -756,6 +755,290 @@ fn do_bench_steps( ); } +fn append_transcribable_bytes(out: &mut Vec, value: &T) { + let offset = out.len(); + out.resize(offset + T::LENGTH_NUM_BYTES + value.get_num_bytes(), 0); + let rest = value.write_transcription_bytes_subset(&mut out[offset..]); + assert!(rest.is_empty(), "transcription buffer should be exact"); +} + +fn generic_pcs_proof_raw_bytes( + proof: &Proof>, +) -> Vec +where + Zt: ZincTypes, + P: ZincPCSTypes, +{ + let mut out = Vec::new(); + <

>::BinaryPCS as PCS< + F, + BinaryPoly, + DEGREE_PLUS_ONE, + >>::write_commitment_bytes(&proof.commitments.binary, &mut out); + <

>::ArbitraryPCS as PCS< + F, + DensePolynomial, + DEGREE_PLUS_ONE, + >>::write_commitment_bytes(&proof.commitments.arbitrary, &mut out); + <

>::IntPCS as PCS< + F, + Zt::Int, + DEGREE_PLUS_ONE, + >>::write_commitment_bytes(&proof.commitments.int, &mut out); + + let zip_len = u32::try_from(proof.zip.len()).expect("zip length must fit into u32"); + out.extend_from_slice(&zip_len.to_le_bytes()); + out.extend_from_slice(&proof.zip); + append_transcribable_bytes(&mut out, &proof.ideal_check); + append_transcribable_bytes(&mut out, &proof.resolver); + append_transcribable_bytes(&mut out, &proof.combined_sumcheck); + append_transcribable_bytes(&mut out, &proof.multipoint_eval); + append_transcribable_bytes( + &mut out, + DynamicPolyVecF::reinterpret(&proof.witness_lifted_evals), + ); + out +} + +fn eprint_generic_pcs_proof_size( + label: &str, + proof: &Proof>, +) where + Zt: ZincTypes, + P: ZincPCSTypes, +{ + let raw = generic_pcs_proof_raw_bytes::(proof); + eprint_bytes_size(label, &raw); +} + +#[allow(clippy::too_many_arguments)] +fn do_bench_pcs_e2e( + group: &mut BenchmarkGroup, + label: &str, + num_vars: usize, + pp: &PCSParams, + vp: &PCSVerifierParams, + trace: &UairTrace<'static, Zt::Int, Zt::Int, DEGREE_PLUS_ONE>, + field_cfg: ::Config, + project_scalar: impl Fn(&U::Scalar, &::Config) -> DynamicPolynomialF + + Copy + + Sync, + project_ideal: impl Fn(&IdealOrZero, &::Config) -> IdealOverF + Copy, +) where + Zt: ZincTypes, + Zt::Int: ProjectableToField + num_traits::Zero, + ::Cw: ProjectableToField, + ::Eval: ProjectableToField, + ::Cw: ProjectableToField, + ::Cw: ProjectableToField, + F: FromWithConfig + + for<'a> FromWithConfig<&'a ::CombR> + + for<'a> FromWithConfig<&'a ::CombR> + + for<'a> FromWithConfig<&'a ::CombR> + + for<'a> FromWithConfig<&'a Zt::Chal> + + for<'a> FromWithConfig<&'a Zt::Pt> + + for<'a> MulByScalar<&'a F> + + FromRef + + Send + + Sync + + 'static, + F: for<'a> FromWithConfig<&'a Zt::Int>, + ::Modulus: ConstTranscribable + FromRef, + U: Uair + 'static, + IdealOverF: Ideal + IdealCheck>, + P: ZincPCSTypes, +{ + let params = format!("{label}/nvars={num_vars}"); + + macro_rules! zinc_plus { + () => { + ZincPlusPiop:: + }; + } + + macro_rules! bench_prove { + ($label:literal, $mle_first:expr) => { + group.bench_function(BenchmarkId::new($label, ¶ms), |bench| { + bench.iter(|| { + black_box(::prove_with_pcs_and_field_cfg::< + P, + { $mle_first }, + PERFORM_CHECKS, + >( + pp, trace, num_vars, project_scalar, field_cfg.clone() + )) + .expect("Prover failed"); + }); + }); + }; + } + + bench_prove!("Prove (Combined)", false); + if count_effective_max_degree::() <= 1 { + bench_prove!("Prove (MLE-first)", true); + } + + let proof = ::prove_with_pcs_and_field_cfg::( + pp, + trace, + num_vars, + project_scalar, + field_cfg.clone(), + ) + .expect("proof generation for verifier bench"); + + let sig = U::signature(); + let public_trace = trace.public(&sig); + + group.bench_function(BenchmarkId::new("Verify", ¶ms), |bench| { + bench.iter_batched( + || proof.clone(), + |proof| { + black_box(::verify_with_pcs_and_field_cfg::< + P, + IdealOverF, + PERFORM_CHECKS, + >( + vp, + proof, + &public_trace, + num_vars, + project_scalar, + project_ideal, + field_cfg.clone(), + )) + .expect("Verifier failed"); + }, + BatchSize::SmallInput, + ); + }); + + eprint_generic_pcs_proof_size::(¶ms, &proof); +} + +#[allow(clippy::too_many_arguments, clippy::unwrap_used)] +fn do_bench_pcs_steps( + group: &mut BenchmarkGroup, + label: &str, + num_vars: usize, + pp: &PCSParams, + vp: &PCSVerifierParams, + trace: &UairTrace<'static, Zt::Int, Zt::Int, DEGREE_PLUS_ONE>, + field_cfg: ::Config, + project_scalar: fn(&U::Scalar, &::Config) -> DynamicPolynomialF, + project_ideal: impl Fn(&IdealOrZero, &::Config) -> IdealOverF + Copy, +) where + Zt: ZincTypes, + Zt::Int: ProjectableToField + num_traits::Zero, + ::Cw: ProjectableToField, + ::Eval: ProjectableToField, + ::Cw: ProjectableToField, + ::Cw: ProjectableToField, + F: FromWithConfig + + for<'a> FromWithConfig<&'a ::CombR> + + for<'a> FromWithConfig<&'a ::CombR> + + for<'a> FromWithConfig<&'a ::CombR> + + for<'a> FromWithConfig<&'a Zt::Chal> + + for<'a> FromWithConfig<&'a Zt::Pt> + + for<'a> MulByScalar<&'a F> + + FromRef + + Send + + Sync + + 'static, + F: for<'a> FromWithConfig<&'a Zt::Int>, + ::Modulus: ConstTranscribable + FromRef, + U: Uair + 'static, + IdealOverF: Ideal + IdealCheck>, + P: ZincPCSTypes, +{ + let params = format!("{label}/nvars={num_vars}"); + + macro_rules! step_bench { + ($side:literal / $step_name:literal, setup = || $setup:expr, run = |$s:ident| $run:expr $(,)?) => { + group.bench_function( + BenchmarkId::new(format!("{}/{}", $side, $step_name), ¶ms), + |b| { + b.iter_batched( + || $setup, + |$s| { + black_box($run).expect("step failed"); + }, + BatchSize::SmallInput, + ); + }, + ); + }; + } + + macro_rules! piop { + () => { + ZincPlusPiop:: + }; + } + + let p_committed = ::step0_commit_with_pcs::

(pp, trace, num_vars).unwrap(); + let p_projected = p_committed + .clone() + .step1_combined_with_field_cfg(project_scalar, field_cfg.clone()) + .unwrap(); + let p_ideal_checked = p_projected.clone().step2_ideal_check().unwrap(); + let p_eval_projected = p_ideal_checked.clone().step3_eval_projection().unwrap(); + let p_sumchecked = p_eval_projected.clone().step4_sumcheck().unwrap(); + let p_mp_evaled = p_sumchecked.clone().step5_multipoint_eval().unwrap(); + let p_lifted = p_mp_evaled.clone().step6_lift_and_project().unwrap(); + + step_bench!( + "Prove" / "0: Commit", + setup = || {}, + run = |_s| ::step0_commit_with_pcs::

(pp, trace, num_vars), + ); + + step_bench!( + "Prove" / "7: PCS open", + setup = || p_lifted.clone(), + run = |s| s.step7_pcs_open::(), + ); + + let proof = ::prove_with_pcs_and_field_cfg::( + pp, + trace, + num_vars, + project_scalar, + field_cfg.clone(), + ) + .expect("proof generation for verifier bench"); + let sig = U::signature(); + let public_trace = trace.public(&sig); + let v_transcript = ::step0_reconstruct_transcript_with_pcs::( + vp, + proof, + &public_trace, + num_vars, + ) + .unwrap(); + let v_prime_projected = v_transcript + .clone() + .step1_prime_projection_with_field_cfg(field_cfg.clone()) + .unwrap(); + let v_ideal_checked = v_prime_projected + .clone() + .step2_ideal_check(project_ideal) + .unwrap(); + let v_eval_projected = v_ideal_checked + .clone() + .step3_eval_projection(project_scalar) + .unwrap(); + let v_sumchecked = v_eval_projected.clone().step4_sumcheck_verify().unwrap(); + let v_mp_evaled = v_sumchecked.clone().step5_multipoint_eval::().unwrap(); + let v_lifted = v_mp_evaled.clone().step6_lifted_evals::().unwrap(); + + step_bench!( + "Verify" / "7: PCS verify", + setup = || v_lifted.clone(), + run = |s| s.step7_pcs_verify::(), + ); +} + // // Specific benchmarks for each UAIR // @@ -839,11 +1122,10 @@ fn bench_real_ecdsa_e2e(group: &mut BenchmarkGroup, num_vars: usize) { let trace = U::generate_random_trace(num_vars, &mut rng); let pp = setup_pp_real_ecdsa(num_vars); - let proj_ideal = |_: &IdealOrZero<::Ideal>, - _: &::Config| - -> ImpossibleIdeal { - unreachable!("EcdsaUair has only assert_zero constraints") - }; + let proj_ideal = + |_: &IdealOrZero<::Ideal>, _: &::Config| -> ImpossibleIdeal { + unreachable!("EcdsaUair has only assert_zero constraints") + }; do_bench_e2e::( group, @@ -863,11 +1145,10 @@ fn bench_real_ecdsa_steps(group: &mut BenchmarkGroup, num_vars: usize) let trace = U::generate_random_trace(num_vars, &mut rng); let pp = setup_pp_real_ecdsa(num_vars); - let proj_ideal = |_: &IdealOrZero<::Ideal>, - _: &::Config| - -> ImpossibleIdeal { - unreachable!("EcdsaUair has only assert_zero constraints") - }; + let proj_ideal = + |_: &IdealOrZero<::Ideal>, _: &::Config| -> ImpossibleIdeal { + unreachable!("EcdsaUair has only assert_zero constraints") + }; do_bench_steps::( group, @@ -916,6 +1197,226 @@ fn bench_real_sha256_steps(group: &mut BenchmarkGroup, num_vars: usize ); } +fn zip_pcs_params( + num_vars: usize, +) -> ( + PCSParams, + PCSVerifierParams, +) { + let pp = setup_pp_real_ecdsa(num_vars); + ( + PCSParams:: { + binary: pp.0.clone(), + arbitrary: pp.1.clone(), + int: pp.2.clone(), + }, + PCSVerifierParams:: { + binary: pp.0, + arbitrary: pp.1, + int: pp.2, + }, + ) +} + +fn hyrax_pcs_params( + num_vars: usize, +) -> ( + PCSParams, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>, + PCSVerifierParams, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>, +) +where + BinaryHyraxZipRest: ZincPCSTypes< + RealEcdsaBenchZincTypes, + F, + DEGREE_PLUS_ONE, + BinaryPCS = HyraxPCS, + ArbitraryPCS = ZipPlusPCS< + >::ArbitraryZt, + >::ArbitraryLc, + >, + IntPCS = ZipPlusPCS< + >::IntZt, + >::IntLc, + >, + >, +{ + let pp = setup_pp_real_ecdsa(num_vars); + let width = pp.0.linear_code.row_len(); + let generator = C::Group::generator(); + let bases = (1..=width) + .map(|idx| { + let scalar = C::ScalarField::from( + u64::try_from(idx).expect("Hyrax basis index must fit in u64"), + ); + (generator * scalar).into_affine() + }) + .collect(); + let h_scalar = C::ScalarField::from( + u64::try_from(width + 1).expect("Hyrax blinding basis index must fit in u64"), + ); + let h = generator * h_scalar; + let (ck, vk) = HyraxPCS::::setup_from_bases(width, bases, h) + .expect("Hyrax benchmark setup must be valid"); + ( + PCSParams::, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE> { + binary: ck, + arbitrary: pp.1.clone(), + int: pp.2.clone(), + }, + PCSVerifierParams::, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE> { + binary: vk, + arbitrary: pp.1, + int: pp.2, + }, + ) +} + +fn bench_real_sha256_pcs_curve_e2e( + group: &mut BenchmarkGroup, + num_vars: usize, + zip_label: &str, + hyrax_label: &str, +) where + BinaryHyraxZipRest: ZincPCSTypes< + RealEcdsaBenchZincTypes, + F, + DEGREE_PLUS_ONE, + BinaryPCS = HyraxPCS, + ArbitraryPCS = ZipPlusPCS< + >::ArbitraryZt, + >::ArbitraryLc, + >, + IntPCS = ZipPlusPCS< + >::IntZt, + >::IntLc, + >, + >, +{ + type U = Sha256CompressionSliceUair; + + let mut rng = rng(); + let trace = U::generate_random_trace(num_vars, &mut rng); + let field_cfg = field_cfg_from_curve_scalar::< + F, + >::Fmod, + C, + >(); + + let (zip_pp, zip_vp) = zip_pcs_params(num_vars); + do_bench_pcs_e2e::( + group, + zip_label, + num_vars, + &zip_pp, + &zip_vp, + &trace, + field_cfg.clone(), + zinc_protocol::project_scalar_fn, + sha256_real_project_ideal, + ); + + let (hyrax_pp, hyrax_vp) = hyrax_pcs_params::(num_vars); + do_bench_pcs_e2e::>( + group, + hyrax_label, + num_vars, + &hyrax_pp, + &hyrax_vp, + &trace, + field_cfg, + zinc_protocol::project_scalar_fn, + sha256_real_project_ideal, + ); +} + +fn bench_real_sha256_pcs_curve_steps( + group: &mut BenchmarkGroup, + num_vars: usize, + zip_label: &str, + hyrax_label: &str, +) where + BinaryHyraxZipRest: ZincPCSTypes< + RealEcdsaBenchZincTypes, + F, + DEGREE_PLUS_ONE, + BinaryPCS = HyraxPCS, + ArbitraryPCS = ZipPlusPCS< + >::ArbitraryZt, + >::ArbitraryLc, + >, + IntPCS = ZipPlusPCS< + >::IntZt, + >::IntLc, + >, + >, +{ + type U = Sha256CompressionSliceUair; + + let mut rng = rng(); + let trace = U::generate_random_trace(num_vars, &mut rng); + let field_cfg = field_cfg_from_curve_scalar::< + F, + >::Fmod, + C, + >(); + + let (zip_pp, zip_vp) = zip_pcs_params(num_vars); + do_bench_pcs_steps::( + group, + zip_label, + num_vars, + &zip_pp, + &zip_vp, + &trace, + field_cfg.clone(), + zinc_protocol::project_scalar_fn, + sha256_real_project_ideal, + ); + + let (hyrax_pp, hyrax_vp) = hyrax_pcs_params::(num_vars); + do_bench_pcs_steps::>( + group, + hyrax_label, + num_vars, + &hyrax_pp, + &hyrax_vp, + &trace, + field_cfg, + zinc_protocol::project_scalar_fn, + sha256_real_project_ideal, + ); +} + +fn bench_real_sha256_pcs_e2e(group: &mut BenchmarkGroup, num_vars: usize) { + bench_real_sha256_pcs_curve_e2e::( + group, + num_vars, + "RealSha256PCS/ZipBn254Fr", + "RealSha256PCS/HyraxBn254", + ); + bench_real_sha256_pcs_curve_e2e::( + group, + num_vars, + "RealSha256PCS/ZipSecp256k1Fr", + "RealSha256PCS/HyraxSecp256k1", + ); +} + +fn bench_real_sha256_pcs_steps(group: &mut BenchmarkGroup, num_vars: usize) { + bench_real_sha256_pcs_curve_steps::( + group, + num_vars, + "RealSha256PCS/ZipBn254Fr", + "RealSha256PCS/HyraxBn254", + ); + bench_real_sha256_pcs_curve_steps::( + group, + num_vars, + "RealSha256PCS/ZipSecp256k1Fr", + "RealSha256PCS/HyraxSecp256k1", + ); +} + fn bench_real_sha_ecdsa_e2e(group: &mut BenchmarkGroup, num_vars: usize) { type U = ShaEcdsaUair; @@ -994,19 +1495,19 @@ fn e2e_benches(c: &mut Criterion) { // bench_no_mult_e2e(&mut group, 8); // bench_no_mult_e2e(&mut group, 10); // bench_no_mult_e2e(&mut group, 12); -// + // // bench_binary_decomposition_e2e(&mut group, 8); // bench_binary_decomposition_e2e(&mut group, 10); // bench_binary_decomposition_e2e(&mut group, 12); -// + // // bench_big_linear_e2e(&mut group, 8); // bench_big_linear_e2e(&mut group, 10); // bench_big_linear_e2e(&mut group, 12); -// + // // bench_big_linear_public_input_e2e(&mut group, 8); // bench_big_linear_public_input_e2e(&mut group, 10); // bench_big_linear_public_input_e2e(&mut group, 12); -// + // // bench_sha_proxy_e2e(&mut group, 8); // bench_sha_proxy_e2e(&mut group, 10); // bench_sha_proxy_e2e(&mut group, 12); @@ -1015,6 +1516,7 @@ fn e2e_benches(c: &mut Criterion) { // rows (Shamir loop), so num_vars=9 is the smallest meaningful size. // bench_real_ecdsa_e2e(&mut group, 9); bench_real_sha256_e2e(&mut group, 9); + bench_real_sha256_pcs_e2e(&mut group, 9); bench_real_sha_ecdsa_e2e(&mut group, 9); group.finish(); @@ -1026,19 +1528,19 @@ fn e2e_steps_benches(c: &mut Criterion) { // bench_no_mult_steps(&mut group, 8); // bench_no_mult_steps(&mut group, 10); // bench_no_mult_steps(&mut group, 12); -// + // // bench_binary_decomposition_steps(&mut group, 8); // bench_binary_decomposition_steps(&mut group, 10); // bench_binary_decomposition_steps(&mut group, 12); -// + // // bench_big_linear_steps(&mut group, 8); // bench_big_linear_steps(&mut group, 10); // bench_big_linear_steps(&mut group, 12); -// + // // bench_big_linear_public_input_steps(&mut group, 8); // bench_big_linear_public_input_steps(&mut group, 10); // bench_big_linear_public_input_steps(&mut group, 12); -// + // // bench_sha_proxy_steps(&mut group, 8); // bench_sha_proxy_steps(&mut group, 10); // bench_sha_proxy_steps(&mut group, 12); @@ -1047,6 +1549,7 @@ fn e2e_steps_benches(c: &mut Criterion) { // num_vars=9 lower-bound rationale. bench_real_ecdsa_steps(&mut group, 9); bench_real_sha256_steps(&mut group, 9); + bench_real_sha256_pcs_steps(&mut group, 9); bench_real_sha_ecdsa_steps(&mut group, 9); group.finish(); @@ -1221,7 +1724,6 @@ type FoldedPp4x = ( >, ); - /// 4× folded e2e bench: routes binary AND int through `MultiZip3` for /// shared-Merkle collapse, then opens at the doubly-extended point /// `(r_0 ‖ γ₁ ‖ γ₂)`. Calls [`prove_folded_4x`] / [`verify_folded_4x`]. @@ -1314,39 +1816,34 @@ fn do_bench_e2e_folded_4x( let sig = U::signature(); let public_trace = trace.public(&sig); - group.bench_function( - BenchmarkId::new("Verify (folded 4×)", ¶ms), - |bench| { - bench.iter_batched( - || proof.clone(), - |proof| { - black_box( - zinc_protocol::verifier::verify_folded_4x::< - ZtF, - U, - F, - IdealOverF, - DEGREE_PLUS_ONE, - HALF_DEGREE_PLUS_ONE, - QUARTER_DEGREE_PLUS_ONE, - EC_FP_INT_LIMBS, - INT_QUARTER_LIMBS_BENCH, - PERFORM_CHECKS, - >( - pp, - proof, - &public_trace, - num_vars, - project_scalar, - project_ideal, - ), - ) - .expect("Folded 4× verifier failed"); - }, - BatchSize::SmallInput, - ); - }, - ); + group.bench_function(BenchmarkId::new("Verify (folded 4×)", ¶ms), |bench| { + bench.iter_batched( + || proof.clone(), + |proof| { + black_box(zinc_protocol::verifier::verify_folded_4x::< + ZtF, + U, + F, + IdealOverF, + DEGREE_PLUS_ONE, + HALF_DEGREE_PLUS_ONE, + QUARTER_DEGREE_PLUS_ONE, + EC_FP_INT_LIMBS, + INT_QUARTER_LIMBS_BENCH, + PERFORM_CHECKS, + >( + pp, + proof, + &public_trace, + num_vars, + project_scalar, + project_ideal, + )) + .expect("Folded 4× verifier failed"); + }, + BatchSize::SmallInput, + ); + }); let label_full = format!("Folded 4×/{params}"); eprint_proof_size(&label_full, &proof); @@ -1436,7 +1933,7 @@ fn eprint_folded_4x_per_region_prove_timings( > + 'static, S: Fn(&U::Scalar, &::Config) -> DynamicPolynomialF + Copy + Sync, { - use zinc_protocol::prover::{prove_folded_4x_with_timings, FoldedProveTimings}; + use zinc_protocol::prover::{FoldedProveTimings, prove_folded_4x_with_timings}; const N: u32 = 100; @@ -1580,9 +2077,7 @@ fn eprint_folded_4x_per_region_verify_timings( S: Fn(&U::Scalar, &::Config) -> DynamicPolynomialF + Copy + Sync, I: Fn(&IdealOrZero, &::Config) -> IdealOverF + Copy, { - use zinc_protocol::verifier::{ - verify_folded_4x_with_timings, FoldedVerifyTimings, - }; + use zinc_protocol::verifier::{FoldedVerifyTimings, verify_folded_4x_with_timings}; const N: u32 = 100; @@ -1685,7 +2180,6 @@ fn eprint_folded_4x_per_region_verify_timings( ); } - /// Serialize each `Proof` component into its own byte buffer and report /// per-part raw + zstd-compressed sizes, so we can see how much each part /// of the proof contributes to the total size. Sizes match the per-field @@ -1705,8 +2199,9 @@ where } // 3 commitments concatenated (each ConstTranscribable, no length prefix). - let mut commits = - Vec::with_capacity(3_usize.saturating_mul(::NUM_BYTES)); + let mut commits = Vec::with_capacity( + 3_usize.saturating_mul(::NUM_BYTES), + ); commits.extend_from_slice(&to_bytes(&proof.commitments.0)); commits.extend_from_slice(&to_bytes(&proof.commitments.1)); commits.extend_from_slice(&to_bytes(&proof.commitments.2)); @@ -1879,8 +2374,6 @@ fn eprint_folded_4x_zip_substep_breakdown( ); } - - // // Real-UAIR folded benches (1× and 4×). These reuse the generic // `do_bench_e2e_folded` / `do_bench_e2e_folded_4x` helpers above with @@ -1908,13 +2401,7 @@ impl FoldedZincTypes for BenchFoldedRealE Int<5>, DensePolynomial, HALF_DEGREE_PLUS_ONE>, BinaryPolyInnerProduct, - DensePolyInnerProduct< - Int<5>, - Self::Chal, - Int<5>, - MBSInnerProduct, - HALF_DEGREE_PLUS_ONE, - >, + DensePolyInnerProduct, Self::Chal, Int<5>, MBSInnerProduct, HALF_DEGREE_PLUS_ONE>, MBSInnerProduct, >; @@ -1926,7 +2413,6 @@ impl FoldedZincTypes for BenchFoldedRealE type IntLc = >::IntLc; } - // // 4× int-fold variant of the bench Zinc-types. Implements // `IntFoldedZincTypes4x` so that `prove_folded_4x` / @@ -1994,13 +2480,7 @@ impl Int<5>, DensePolynomial, QUARTER_DEGREE_PLUS_ONE>, BinaryPolyInnerProduct, - DensePolyInnerProduct< - Int<5>, - Self::Chal, - Int<5>, - MBSInnerProduct, - QUARTER_DEGREE_PLUS_ONE, - >, + DensePolyInnerProduct, Self::Chal, Int<5>, MBSInnerProduct, QUARTER_DEGREE_PLUS_ONE>, MBSInnerProduct, >; type ArbitraryZt = >::ArbitraryZt; @@ -2085,11 +2565,10 @@ fn bench_real_ecdsa_e2e_folded(group: &mut BenchmarkGroup, num_vars: u let trace = U::generate_random_trace(num_vars, &mut rng); let pp = setup_folded_pp_real_ecdsa(num_vars); - let proj_ideal = |_: &IdealOrZero<::Ideal>, - _: &::Config| - -> ImpossibleIdeal { - unreachable!("EcdsaUair has only assert_zero constraints") - }; + let proj_ideal = + |_: &IdealOrZero<::Ideal>, _: &::Config| -> ImpossibleIdeal { + unreachable!("EcdsaUair has only assert_zero constraints") + }; do_bench_e2e_folded::( group, @@ -2138,14 +2617,10 @@ fn bench_real_sha_ecdsa_e2e_folded(group: &mut BenchmarkGroup, num_var ); } - /// ShaEcdsa 4× folded: binary AND int both quartered /// (BinaryPoly<8> / Int<2>) and committed under one Merkle tree /// via `MultiZip3`. One Merkle path per opening instead of three. -fn bench_real_sha_ecdsa_e2e_folded_4x( - group: &mut BenchmarkGroup, - num_vars: usize, -) { +fn bench_real_sha_ecdsa_e2e_folded_4x(group: &mut BenchmarkGroup, num_vars: usize) { type U = ShaEcdsaUair; let mut rng = rng(); @@ -2223,7 +2698,6 @@ fn print_peak_rss(label: &str) { eprintln!("[{label}] peak RSS: {bytes} B ({mib:.2} MiB / {gib:.3} GiB)"); } - criterion_group! { name = e2e; config = Criterion::default().sample_size(500); diff --git a/protocol/src/fixed_prime.rs b/protocol/src/fixed_prime.rs index eb11da95..7bca1dd2 100644 --- a/protocol/src/fixed_prime.rs +++ b/protocol/src/fixed_prime.rs @@ -14,6 +14,8 @@ //! are honest mod `p`). Do not reuse this branch for other applications //! without re-doing the soundness analysis. +use ark_ec::AffineRepr; +use ark_ff::{BigInteger, PrimeField as ArkPrimeField}; use crypto_primitives::PrimeField; use zinc_transcript::traits::ConstTranscribable; use zinc_utils::from_ref::FromRef; @@ -24,10 +26,8 @@ use zinc_utils::from_ref::FromRef; /// as little-endian limb chunks (see `transcript::traits` impl), so we /// store the prime in that same order. pub const SECP256K1_P_LE_BYTES: [u8; 32] = [ - 0x2F, 0xFC, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x2F, 0xFC, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, ]; /// Build `F::Config` from the secp256k1 base prime, replacing the @@ -50,8 +50,39 @@ where "Fmod must be exactly 256 bits to hold the secp256k1 base prime", ); let prime = FMod::read_transcription_bytes_exact(&SECP256K1_P_LE_BYTES); + F::make_cfg(&F::Modulus::from_ref(&prime)).expect("secp256k1 base field prime is prime") +} + +/// Build `F::Config` from the scalar field of an arkworks curve. +/// +/// This is the field configuration Hyrax must use: PCS scalar operations +/// happen in `C::ScalarField`, so the PIOP field modulus must match it. +pub fn field_cfg_from_curve_scalar() -> F::Config +where + F: PrimeField, + FMod: ConstTranscribable, + F::Modulus: FromRef, + C: AffineRepr, +{ + let prime = fmod_from_curve_scalar::(); F::make_cfg(&F::Modulus::from_ref(&prime)) - .expect("secp256k1 base field prime is prime") + .expect("curve scalar modulus must define a valid prime field") +} + +fn fmod_from_curve_scalar() -> FMod +where + FMod: ConstTranscribable, + C: AffineRepr, +{ + let modulus_bytes = ::MODULUS.to_bytes_le(); + assert!( + modulus_bytes.len() <= FMod::NUM_BYTES, + "curve scalar modulus does not fit in the protocol modulus type", + ); + + let mut bytes = vec![0u8; FMod::NUM_BYTES]; + bytes[..modulus_bytes.len()].copy_from_slice(&modulus_bytes); + FMod::read_transcription_bytes_exact(&bytes) } #[cfg(test)] @@ -83,4 +114,23 @@ mod tests { fn secp256k1_field_cfg_constructs() { let _cfg = secp256k1_field_cfg::, Uint<4>>(); } + + #[test] + fn curve_scalar_field_cfg_constructs() { + let bn_cfg = field_cfg_from_curve_scalar::, Uint<4>, ark_bn254::G1Affine>(); + let secp_cfg = + field_cfg_from_curve_scalar::, Uint<4>, ark_secp256k1::Affine>(); + + let bn_modulus = MontyField::<4>::one_with_cfg(&bn_cfg).modulus(); + let secp_modulus = MontyField::<4>::one_with_cfg(&secp_cfg).modulus(); + + assert_eq!( + bn_modulus, + fmod_from_curve_scalar::, ark_bn254::G1Affine>(), + ); + assert_eq!( + secp_modulus, + fmod_from_curve_scalar::, ark_secp256k1::Affine>(), + ); + } } diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 22dd910e..cfe5ca46 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -18,6 +18,7 @@ //! - Step 7: Zip+ PCS open/verify at r_0 pub mod fixed_prime; +pub mod pcs; pub mod prover; pub mod verifier; @@ -60,9 +61,9 @@ use zip_plus::{ /// Full proof produced by the Zinc+ PIOP for UCS. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Proof { +pub struct Proof { /// Zip+ commitments to the witness columns. - pub commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment), + pub commitments: Commitments, /// Serialized PCS proof data (Zip+ proving transcripts). pub zip: Vec, /// Randomized ideal check proof. @@ -542,7 +543,10 @@ pub fn compute_lifted_evals_capped( // can (a) skip entries that are identically zero, and (b) walk only // the SET bits via `trailing_zeros` + Brian Kernighan's clear-lowest // instead of branching on every slot. - debug_assert!(D <= 64, "compute_lifted_evals: bitmask packing assumes D <= 64"); + debug_assert!( + D <= 64, + "compute_lifted_evals: bitmask packing assumes D <= 64" + ); let mut result: Vec> = cfg_iter!(trace_bin_poly) .map(|col| { let mut coeffs = vec![zero.clone(); D]; @@ -636,8 +640,7 @@ pub fn compute_int_fold_lifted_evals( field_cfg: &F::Config, ) -> Vec> where - F: PrimeField - + for<'a> FromWithConfig<&'a crypto_primitives::crypto_bigint_int::Int>, + F: PrimeField + for<'a> FromWithConfig<&'a crypto_primitives::crypto_bigint_int::Int>, { use crypto_primitives::crypto_bigint_int::Int; assert!(HALF_H >= 2); @@ -777,6 +780,11 @@ where #[cfg(test)] mod tests { use super::*; + use crate::{ + fixed_prime::field_cfg_from_curve_scalar, + pcs::{AllZipPCSTypes, BinaryHyraxZipRest, PCSParams, PCSVerifierParams, ZincPCSTypes}, + }; + use ark_ec::{AffineRepr, CurveGroup, PrimeGroup}; use crypto_bigint::U64; use crypto_primitives::{ Field, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, @@ -805,10 +813,15 @@ mod tests { }; use zip_plus::{ code::{ + LinearCode, iprs::{IprsCode, PnttConfigF65537}, raa::{RaaCode, RaaConfig}, }, - pcs::structs::{ZipPlus, ZipPlusParams}, + pcs::{ + generic::ZipPlusPCS, + hyrax::{BinaryLanes, HyraxPCS}, + structs::{ZipPlus, ZipPlusParams}, + }, pcs_transcript::PcsProverTranscript, }; @@ -1349,9 +1362,7 @@ mod tests { |res| { assert!(matches!( res.unwrap_err(), - ProtocolError::Resolver( - CombinedPolyResolverError::WrongSumcheckSum { .. } - ) + ProtocolError::Resolver(CombinedPolyResolverError::WrongSumcheckSum { .. }) )); }, ); @@ -1800,10 +1811,7 @@ mod tests { tamper(&mut proof); - let verification_result = ZincPlusPiop::::verify::< - _, - CHECKED, - >( + let verification_result = ZincPlusPiop::::verify::<_, CHECKED>( &pp, proof, &public_trace, @@ -1814,6 +1822,179 @@ mod tests { check_verification(verification_result); } + #[allow(clippy::type_complexity)] + fn sha256_zip_pcs_params( + num_vars: usize, + ) -> ( + PCSParams, + PCSVerifierParams, + ) { + let pp = setup_pp::( + num_vars, + ( + make_iprs(num_vars), + make_iprs(num_vars), + make_iprs(num_vars), + ), + ); + ( + PCSParams:: { + binary: pp.0.clone(), + arbitrary: pp.1.clone(), + int: pp.2.clone(), + }, + PCSVerifierParams:: { + binary: pp.0, + arbitrary: pp.1, + int: pp.2, + }, + ) + } + + #[allow(clippy::type_complexity)] + fn sha256_hyrax_pcs_params( + num_vars: usize, + ) -> ( + PCSParams, TestShaEcdsaZincTypes, F, DEGREE_PLUS_ONE>, + PCSVerifierParams, TestShaEcdsaZincTypes, F, DEGREE_PLUS_ONE>, + ) + where + BinaryHyraxZipRest: ZincPCSTypes< + TestShaEcdsaZincTypes, + F, + DEGREE_PLUS_ONE, + BinaryPCS = HyraxPCS, + ArbitraryPCS = ZipPlusPCS< + >::ArbitraryZt, + >::ArbitraryLc, + >, + IntPCS = ZipPlusPCS< + >::IntZt, + >::IntLc, + >, + >, + { + let pp = setup_pp::( + num_vars, + ( + make_iprs(num_vars), + make_iprs(num_vars), + make_iprs(num_vars), + ), + ); + let width = pp.0.linear_code.row_len(); + let generator = C::Group::generator(); + let bases = (1..=width) + .map(|idx| { + let scalar = + C::ScalarField::from(u64::try_from(idx).expect("basis index fits in u64")); + (generator * scalar).into_affine() + }) + .collect(); + let h_scalar = + C::ScalarField::from(u64::try_from(width + 1).expect("basis index fits in u64")); + let (binary, binary_vk) = + HyraxPCS::::setup_from_bases(width, bases, generator * h_scalar) + .expect("Hyrax setup must be valid"); + ( + PCSParams::, TestShaEcdsaZincTypes, F, DEGREE_PLUS_ONE> { + binary, + arbitrary: pp.1.clone(), + int: pp.2.clone(), + }, + PCSVerifierParams::, TestShaEcdsaZincTypes, F, DEGREE_PLUS_ONE> { + binary: binary_vk, + arbitrary: pp.1, + int: pp.2, + }, + ) + } + + fn run_sha256_pcs_round_trip

( + pp: &PCSParams, + vp: &PCSVerifierParams, + field_cfg: ::Config, + ) where + P: ZincPCSTypes, + { + type U = Sha256CompressionSliceUair; + + const NUM_VARS: usize = 9; + + let mut rng = rng(); + let trace = U::generate_random_trace(NUM_VARS, &mut rng); + let public_trace = trace.public(&U::signature()); + + let proof = + ZincPlusPiop::::prove_with_pcs_and_field_cfg::< + P, + false, + CHECKED, + >(pp, &trace, NUM_VARS, project_scalar_fn, field_cfg.clone()) + .expect("SHA PCS prover failed"); + + ZincPlusPiop::::verify_with_pcs_and_field_cfg::< + P, + Sha256Ideal, + CHECKED, + >( + vp, + proof, + &public_trace, + NUM_VARS, + project_scalar_fn, + sha256_test_project_ideal, + field_cfg, + ) + .expect("SHA PCS verifier rejected an honest proof"); + } + + #[test] + fn test_real_sha256_pcs_variants_round_trip() { + const NUM_VARS: usize = 9; + + let bn_field_cfg = field_cfg_from_curve_scalar::< + F, + >::Fmod, + ark_bn254::G1Affine, + >(); + let (zip_bn_pp, zip_bn_vp) = sha256_zip_pcs_params(NUM_VARS); + run_sha256_pcs_round_trip::(&zip_bn_pp, &zip_bn_vp, bn_field_cfg); + + let secp_field_cfg = field_cfg_from_curve_scalar::< + F, + >::Fmod, + ark_secp256k1::Affine, + >(); + let (zip_secp_pp, zip_secp_vp) = sha256_zip_pcs_params(NUM_VARS); + run_sha256_pcs_round_trip::(&zip_secp_pp, &zip_secp_vp, secp_field_cfg); + + let bn_field_cfg = field_cfg_from_curve_scalar::< + F, + >::Fmod, + ark_bn254::G1Affine, + >(); + let (hyrax_bn_pp, hyrax_bn_vp) = sha256_hyrax_pcs_params::(NUM_VARS); + run_sha256_pcs_round_trip::>( + &hyrax_bn_pp, + &hyrax_bn_vp, + bn_field_cfg, + ); + + let secp_field_cfg = field_cfg_from_curve_scalar::< + F, + >::Fmod, + ark_secp256k1::Affine, + >(); + let (hyrax_secp_pp, hyrax_secp_vp) = + sha256_hyrax_pcs_params::(NUM_VARS); + run_sha256_pcs_round_trip::>( + &hyrax_secp_pp, + &hyrax_secp_vp, + secp_field_cfg, + ); + } + /// `num_vars` for SHA-ECDSA tests. ECDSA's Shamir scalar /// multiplication needs `n_rows > 256`, so `num_vars >= 9`. const SHA_ECDSA_NUM_VARS: usize = 9; @@ -1894,9 +2075,7 @@ mod tests { serialized_len.div_ceil(1024), ); let mut transcript = transcript.into_verification_transcript(); - let proof_2 = transcript - .read() - .expect("Failed to deserialize proof"); + let proof_2 = transcript.read().expect("Failed to deserialize proof"); assert_eq!(proof, proof_2); verify_folded_4x::< @@ -2116,7 +2295,7 @@ mod tests { HALF_DEGREE_PLUS_ONE, >>::IntLc, >, - ) { + ){ let split_size = 1 << (num_vars + 1); let normal_size = 1 << num_vars; ( @@ -2213,5 +2392,4 @@ mod tests { >; type ArrCombRDotChal = MBSInnerProduct; } - } diff --git a/protocol/src/pcs.rs b/protocol/src/pcs.rs new file mode 100644 index 00000000..a447108d --- /dev/null +++ b/protocol/src/pcs.rs @@ -0,0 +1,122 @@ +use std::{fmt::Debug, marker::PhantomData}; + +use ark_ec::AffineRepr; +use crypto_primitives::PrimeField; +use zinc_poly::univariate::{binary::BinaryPoly, dense::DensePolynomial}; +use zip_plus::pcs::{ + generic::{PCS, ZipPlusPCS}, + hyrax::{BinaryLanes, HyraxPCS}, + structs::ZipPlusCommitment, +}; + +use crate::ZincTypes; + +pub type ZipPCSCommitments = (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment); + +pub trait ZincPCSTypes: Clone + Debug + Send + Sync +where + Zt: ZincTypes, + F: PrimeField, +{ + type BinaryPCS: PCS, D>; + type ArbitraryPCS: PCS, D>; + type IntPCS: PCS; +} + +#[derive(Clone, Debug)] +pub struct AllZipPCSTypes; + +impl ZincPCSTypes for AllZipPCSTypes +where + Zt: ZincTypes, + F: PrimeField, + ZipPlusPCS: PCS, D>, + ZipPlusPCS: PCS, D>, + ZipPlusPCS: PCS, +{ + type BinaryPCS = ZipPlusPCS; + type ArbitraryPCS = ZipPlusPCS; + type IntPCS = ZipPlusPCS; +} + +#[derive(Clone, Debug)] +pub struct BinaryHyraxZipRest(PhantomData); + +impl ZincPCSTypes for BinaryHyraxZipRest +where + Zt: ZincTypes, + F: PrimeField, + C: AffineRepr, + HyraxPCS: PCS, D>, + ZipPlusPCS: PCS, D>, + ZipPlusPCS: PCS, +{ + type BinaryPCS = HyraxPCS; + type ArbitraryPCS = ZipPlusPCS; + type IntPCS = ZipPlusPCS; +} + +#[derive(Clone, Debug)] +pub struct PCSParams +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub binary: + <

>::BinaryPCS as PCS, D>>::CommitmentKey, + pub arbitrary: <

>::ArbitraryPCS as PCS< + F, + DensePolynomial, + D, + >>::CommitmentKey, + pub int: <

>::IntPCS as PCS>::CommitmentKey, +} + +#[derive(Clone, Debug)] +pub struct PCSVerifierParams +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub binary: <

>::BinaryPCS as PCS, D>>::VerifierKey, + pub arbitrary: <

>::ArbitraryPCS as PCS< + F, + DensePolynomial, + D, + >>::VerifierKey, + pub int: <

>::IntPCS as PCS>::VerifierKey, +} + +#[derive(Clone, Debug)] +pub struct PCSCommitments +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub binary: <

>::BinaryPCS as PCS, D>>::Commitment, + pub arbitrary: <

>::ArbitraryPCS as PCS< + F, + DensePolynomial, + D, + >>::Commitment, + pub int: <

>::IntPCS as PCS>::Commitment, +} + +#[derive(Clone, Debug)] +pub struct PCSProverData +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub binary: <

>::BinaryPCS as PCS, D>>::ProverData, + pub arbitrary: <

>::ArbitraryPCS as PCS< + F, + DensePolynomial, + D, + >>::ProverData, + pub int: <

>::IntPCS as PCS>::ProverData, +} diff --git a/protocol/src/prover.rs b/protocol/src/prover.rs index 44b265d8..9d4a2e27 100644 --- a/protocol/src/prover.rs +++ b/protocol/src/prover.rs @@ -3,7 +3,7 @@ use crypto_primitives::{ ConstIntSemiring, FromPrimitiveWithConfig, FromWithConfig, crypto_bigint_int::Int, }; use num_traits::Zero; -use std::fmt::Debug; +use std::{fmt::Debug, io::Cursor}; use zinc_piop::{ combined_poly_resolver::CombinedPolyResolver, ideal_check::IdealCheckProtocol, @@ -21,11 +21,14 @@ use zinc_piop::{ }, sumcheck::multi_degree::MultiDegreeSumcheck, }; +use zinc_poly::{mle::DenseMultilinearExtension, univariate::binary::BinaryPoly}; use zinc_poly::{ - mle::MultilinearExtensionWithConfig, - univariate::dynamic::over_field::DynamicPolynomialF, + mle::MultilinearExtensionWithConfig, univariate::dynamic::over_field::DynamicPolynomialF, +}; +use zinc_transcript::{ + Blake3Transcript, + traits::{ConstTranscribable, Transcript}, }; -use zinc_transcript::traits::{ConstTranscribable, Transcript}; use zinc_uair::{ Uair, UairSignature, UairTrace, constraint_counter::count_constraints, degree_counter::count_max_degree, @@ -37,12 +40,14 @@ use zinc_utils::{ use zip_plus::{ pcs::{ ZipPlusProveByteBreakdown, + generic::PCS, multi_zip::MultiZip3, structs::{ZipPlus, ZipPlusHint, ZipPlusParams, ZipTypes}, }, pcs_transcript::PcsProverTranscript, }; -use zinc_poly::{mle::DenseMultilinearExtension, univariate::binary::BinaryPoly}; + +use crate::pcs::{AllZipPCSTypes, PCSCommitments, PCSParams, PCSProverData, ZincPCSTypes}; /// Drop the witness binary_poly columns the UAIR opted out of (sorted, /// dedup'd `skip_indices` relative to `witness_cols`) and return the @@ -73,24 +78,25 @@ fn filter_booleanity_witness( /// Fiat-Shamir transcript, PCS parameters/hints/commitments, and trace /// reference. #[derive(Clone, Debug)] -pub struct ProverBase<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> { +pub struct ProverBase< + 'a, + Zt: ZincTypes, + U: Uair, + F: PrimeField, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { num_vars: usize, uair_signature: UairSignature, pcs_transcript: PcsProverTranscript, trace: &'a UairTrace<'static, Zt::Int, Zt::Int, D>, // Commitment info - pp_bin: &'a ZipPlusParams, - pp_arb: &'a ZipPlusParams, - pp_int: &'a ZipPlusParams, - hint_bin: Option::Cw>>, - hint_arb: Option::Cw>>, - hint_int: Option::Cw>>, - commitment_bin: ZipPlusCommitment, - commitment_arb: ZipPlusCommitment, - commitment_int: ZipPlusCommitment, - - _phantom: PhantomData<(U, F)>, + pcs_params: PCSParams, + pcs_data: PCSProverData, + pcs_commitments: PCSCommitments, + + _phantom: PhantomData<(U, F, P)>, } // @@ -100,8 +106,15 @@ pub struct ProverBase<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usi /// After step 1 via [`step1_combined`](ProverCommitted::step1_combined) /// (row-major / "combined" projection). `project_scalar` has been consumed. #[derive(Clone, Debug)] -pub struct ProverProjectedCombined<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> { - base: ProverBase<'a, Zt, U, F, D>, +pub struct ProverProjectedCombined< + 'a, + Zt: ZincTypes, + U: Uair, + F: PrimeField, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { + base: ProverBase<'a, Zt, U, F, D, P>, field_cfg: F::Config, projected_trace: RowMajorTrace, projected_scalars_fx: ScalarMap>, @@ -110,8 +123,15 @@ pub struct ProverProjectedCombined<'a, Zt: ZincTypes, U: Uair, F: PrimeField, /// After step 1 via [`step1_mle_first`](ProverCommitted::step1_mle_first) /// (column-major / MLE-first projection). `project_scalar` has been consumed. #[derive(Clone, Debug)] -pub struct ProverProjectedMleFirst<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> { - base: ProverBase<'a, Zt, U, F, D>, +pub struct ProverProjectedMleFirst< + 'a, + Zt: ZincTypes, + U: Uair, + F: PrimeField, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { + base: ProverBase<'a, Zt, U, F, D, P>, field_cfg: F::Config, projected_trace: ColumnMajorTrace, projected_scalars_fx: ScalarMap>, @@ -123,8 +143,15 @@ pub struct ProverProjectedMleFirst<'a, Zt: ZincTypes, U: Uair, F: PrimeField, /// through the combined-poly lane (row-major). `project_scalar` has been /// consumed. #[derive(Clone, Debug)] -pub struct ProverProjectedHybrid<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> { - base: ProverBase<'a, Zt, U, F, D>, +pub struct ProverProjectedHybrid< + 'a, + Zt: ZincTypes, + U: Uair, + F: PrimeField, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { + base: ProverBase<'a, Zt, U, F, D, P>, field_cfg: F::Config, row_major_trace: RowMajorTrace, column_major_trace: ColumnMajorTrace, @@ -133,8 +160,15 @@ pub struct ProverProjectedHybrid<'a, Zt: ZincTypes, U: Uair, F: PrimeField, c /// After step 2 (ideal check). #[derive(Clone, Debug)] -pub struct ProverIdealChecked<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> { - base: ProverBase<'a, Zt, U, F, D>, +pub struct ProverIdealChecked< + 'a, + Zt: ZincTypes, + U: Uair, + F: PrimeField, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { + base: ProverBase<'a, Zt, U, F, D, P>, field_cfg: F::Config, projected_trace: ProjectedTrace, projected_scalars_fx: ScalarMap>, @@ -146,8 +180,15 @@ pub struct ProverIdealChecked<'a, Zt: ZincTypes, U: Uair, F: PrimeField, cons /// After step 3 (eval projection). `projected_scalars_fx` has been consumed. #[derive(Clone, Debug)] -pub struct ProverEvalProjected<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> { - base: ProverBase<'a, Zt, U, F, D>, +pub struct ProverEvalProjected< + 'a, + Zt: ZincTypes, + U: Uair, + F: PrimeField, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { + base: ProverBase<'a, Zt, U, F, D, P>, field_cfg: F::Config, projected_trace: ProjectedTrace, ic_proof: IdealCheckProof, @@ -164,8 +205,15 @@ pub struct ProverEvalProjected<'a, Zt: ZincTypes, U: Uair, F: PrimeField, con /// After step 4 (sumcheck). #[allow(clippy::type_complexity)] #[derive(Clone, Debug)] -pub struct ProverSumchecked<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> { - base: ProverBase<'a, Zt, U, F, D>, +pub struct ProverSumchecked< + 'a, + Zt: ZincTypes, + U: Uair, + F: PrimeField, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { + base: ProverBase<'a, Zt, U, F, D, P>, field_cfg: F::Config, projected_trace: ProjectedTrace, ic_proof: IdealCheckProof, @@ -185,8 +233,15 @@ pub struct ProverSumchecked<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const /// After step 5 (multipoint eval). #[derive(Clone, Debug)] -pub struct ProverMultipointEvaled<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> { - base: ProverBase<'a, Zt, U, F, D>, +pub struct ProverMultipointEvaled< + 'a, + Zt: ZincTypes, + U: Uair, + F: PrimeField, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { + base: ProverBase<'a, Zt, U, F, D, P>, field_cfg: F::Config, projected_trace: ProjectedTrace, ic_proof: IdealCheckProof, @@ -201,8 +256,15 @@ pub struct ProverMultipointEvaled<'a, Zt: ZincTypes, U: Uair, F: PrimeField, /// After step 6 (lift-and-project). #[derive(Clone, Debug)] -pub struct ProverLifted<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> { - base: ProverBase<'a, Zt, U, F, D>, +pub struct ProverLifted< + 'a, + Zt: ZincTypes, + U: Uair, + F: PrimeField, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { + base: ProverBase<'a, Zt, U, F, D, P>, field_cfg: F::Config, ic_proof: IdealCheckProof, cpr_proof: CombinedPolyResolverProof, @@ -215,8 +277,8 @@ pub struct ProverLifted<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: u lifted_evals: Vec>, } -impl<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> - ProverLifted<'a, Zt, U, F, D> +impl<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize, P: ZincPCSTypes> + ProverLifted<'a, Zt, U, F, D, P> { /// PIOP evaluation point `r_0` produced by step 5. Used by external /// per-step bench harnesses (folded paths) to seed step 7 setups. @@ -230,8 +292,15 @@ impl<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> /// Ready for generating the final proof object in /// [`finish`](ProverPcsOpened::finish). #[derive(Clone, Debug)] -pub struct ProverPcsOpened<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D: usize> { - base: ProverBase<'a, Zt, U, F, D>, +pub struct ProverPcsOpened< + 'a, + Zt: ZincTypes, + U: Uair, + F: PrimeField, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { + base: ProverBase<'a, Zt, U, F, D, P>, ic_proof: IdealCheckProof, cpr_proof: CombinedPolyResolverProof, combined_sumcheck: MultiDegreeSumcheckProof, @@ -248,9 +317,10 @@ pub struct ProverPcsOpened<'a, Zt: ZincTypes, U: Uair, F: PrimeField, const D /// define them macro_rules! impl_with_type_bounds { ($type_name:ident { $($code:tt)* }) => { - impl<'a, Zt, U, F, const D: usize> $type_name<'a, Zt, U, F, D> + impl<'a, Zt, U, F, const D: usize, P> $type_name<'a, Zt, U, F, D, P> where Zt: ZincTypes, + P: ZincPCSTypes, Zt::Int: ProjectableToField, ::Eval: ProjectableToField, U: Uair + 'static, @@ -278,39 +348,83 @@ macro_rules! impl_with_type_bounds { impl ZincPlusPiop where Zt: ZincTypes, - U: Uair, - F: PrimeField, - F::Inner: ConstTranscribable, + Zt::Int: ProjectableToField, + ::Eval: ProjectableToField, + U: Uair + 'static, + F: InnerTransparentField + + FromPrimitiveWithConfig + + for<'b> FromWithConfig<&'b Zt::Int> + + for<'b> FromWithConfig<&'b ::CombR> + + for<'b> FromWithConfig<&'b ::CombR> + + for<'b> FromWithConfig<&'b ::CombR> + + for<'b> FromWithConfig<&'b Zt::Chal> + + for<'b> MulByScalar<&'b F> + + FromRef + + Send + + Sync + + 'static, + F::Inner: + ConstIntSemiring + ConstTranscribable + FromRef + Send + Sync + Zero + Default, + F::Modulus: ConstTranscribable + FromRef, { - /// Step 0: Prover entry point. - /// Commit *witness* columns via Zip+ PCS, absorb roots and public - /// data into the Fiat-Shamir transcript. + /// Step 0: Prover entry point using the default all-Zip PCS bundle. #[allow(clippy::type_complexity)] pub fn step0_commit<'a>( - (pp_bin, pp_arb, pp_int): &'a ( + pp: &'a ( ZipPlusParams, ZipPlusParams, ZipPlusParams, ), trace: &'a UairTrace<'static, Zt::Int, Zt::Int, D>, num_vars: usize, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> + where + AllZipPCSTypes: ZincPCSTypes< + Zt, + F, + D, + BinaryPCS = zip_plus::pcs::generic::ZipPlusPCS, + ArbitraryPCS = zip_plus::pcs::generic::ZipPlusPCS, + IntPCS = zip_plus::pcs::generic::ZipPlusPCS, + >, + { + let pcs_params = PCSParams:: { + binary: pp.0.clone(), + arbitrary: pp.1.clone(), + int: pp.2.clone(), + }; + Self::step0_commit_with_pcs::(&pcs_params, trace, num_vars) + } + + /// Step 0 with an explicit PCS bundle. + pub fn step0_commit_with_pcs<'a, P>( + pcs_params: &PCSParams, + trace: &'a UairTrace<'static, Zt::Int, Zt::Int, D>, + num_vars: usize, + ) -> Result, ProtocolError> + where + P: ZincPCSTypes, + { let uair_signature = U::signature(); let public_trace = trace.public(&uair_signature); let witness_trace = trace.witness(&uair_signature); let (res_bin, (res_arb, res_int)) = cfg_join!( - commit_optionally(pp_bin, &witness_trace.binary_poly), - commit_optionally(pp_arb, &witness_trace.arbitrary_poly), - commit_optionally(pp_int, &witness_trace.int), + P::BinaryPCS::commit(&pcs_params.binary, &witness_trace.binary_poly), + P::ArbitraryPCS::commit(&pcs_params.arbitrary, &witness_trace.arbitrary_poly), + P::IntPCS::commit(&pcs_params.int, &witness_trace.int), ); - let (hint_bin, commitment_bin) = res_bin?; - let (hint_arb, commitment_arb) = res_arb?; - let (hint_int, commitment_int) = res_int?; + let (data_bin, commitment_bin) = res_bin?; + let (data_arb, commitment_arb) = res_arb?; + let (data_int, commitment_int) = res_int?; - let mut pcs_transcript = PcsProverTranscript::new_from_commitments( - [&commitment_bin, &commitment_arb, &commitment_int].into_iter(), - ); + let mut pcs_transcript = PcsProverTranscript { + fs_transcript: Blake3Transcript::default(), + stream: Cursor::default(), + }; + P::BinaryPCS::absorb_commitment(&mut pcs_transcript.fs_transcript, &commitment_bin); + P::ArbitraryPCS::absorb_commitment(&mut pcs_transcript.fs_transcript, &commitment_arb); + P::IntPCS::absorb_commitment(&mut pcs_transcript.fs_transcript, &commitment_int); absorb_public_columns(&mut pcs_transcript.fs_transcript, &public_trace.binary_poly); absorb_public_columns( @@ -324,15 +438,17 @@ where uair_signature, pcs_transcript, trace, - pp_bin, - pp_arb, - pp_int, - hint_bin, - hint_arb, - hint_int, - commitment_bin, - commitment_arb, - commitment_int, + pcs_params: pcs_params.clone(), + pcs_data: PCSProverData { + binary: data_bin, + arbitrary: data_arb, + int: data_int, + }, + pcs_commitments: PCSCommitments { + binary: commitment_bin, + arbitrary: commitment_arb, + int: commitment_int, + }, _phantom: PhantomData, }) } @@ -351,6 +467,17 @@ impl_with_type_bounds!(ProverBase // See `crate::fixed_prime` for the soundness caveat. let field_cfg = crate::fixed_prime::secp256k1_field_cfg::(); + self.project_common_with_field_cfg(project_scalar, field_cfg) + } + + fn project_common_with_field_cfg< + S: Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF + Sync, + >( + &mut self, + project_scalar: S, + field_cfg: F::Config, + ) -> Result<(F::Config, ScalarMap>), ProtocolError> + { let projected_scalars_fx = project_scalars::(|s| project_scalar(s, &field_cfg)); Ok((field_cfg, projected_scalars_fx)) } @@ -362,7 +489,7 @@ impl_with_type_bounds!(ProverBase pub fn step1_combined DynamicPolynomialF + Sync>( mut self, project_scalar: S, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { let (field_cfg, projected_scalars_fx) = self.project_common(project_scalar)?; let projected_trace = project_trace_coeffs_row_major(self.trace, &field_cfg); @@ -374,6 +501,25 @@ impl_with_type_bounds!(ProverBase }) } + pub fn step1_combined_with_field_cfg< + S: Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF + Sync, + >( + mut self, + project_scalar: S, + field_cfg: F::Config, + ) -> Result, ProtocolError> { + let (field_cfg, projected_scalars_fx) = + self.project_common_with_field_cfg(project_scalar, field_cfg)?; + + let projected_trace = project_trace_coeffs_row_major(self.trace, &field_cfg); + Ok(ProverProjectedCombined { + base: self, + field_cfg, + projected_trace, + projected_scalars_fx, + }) + } + /// Step 1 (MLE-first / column-major): Prime projection /// (`\phi_q`: `Z[X] -> F_q[X]`). Samples a random prime, projects the /// full trace and scalars using the column-major layout. @@ -381,7 +527,7 @@ impl_with_type_bounds!(ProverBase pub fn step1_mle_first DynamicPolynomialF + Sync>( mut self, project_scalar: S, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { let (field_cfg, projected_scalars_fx) = self.project_common(project_scalar)?; let projected_trace = project_trace_coeffs_column_major(self.trace, &field_cfg); @@ -393,6 +539,25 @@ impl_with_type_bounds!(ProverBase }) } + pub fn step1_mle_first_with_field_cfg< + S: Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF + Sync, + >( + mut self, + project_scalar: S, + field_cfg: F::Config, + ) -> Result, ProtocolError> { + let (field_cfg, projected_scalars_fx) = + self.project_common_with_field_cfg(project_scalar, field_cfg)?; + + let projected_trace = project_trace_coeffs_column_major(self.trace, &field_cfg); + Ok(ProverProjectedMleFirst { + base: self, + field_cfg, + projected_trace, + projected_scalars_fx, + }) + } + /// Step 1 (hybrid): Prime projection that produces **both** layouts. /// Used when the UAIR has a mix of linear and non-linear constraints, /// so the ideal-check can route them through their respective fast/slow @@ -401,7 +566,7 @@ impl_with_type_bounds!(ProverBase pub fn step1_hybrid DynamicPolynomialF + Sync>( mut self, project_scalar: S, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { let (field_cfg, projected_scalars_fx) = self.project_common(project_scalar)?; let row_major_trace = project_trace_coeffs_row_major(self.trace, &field_cfg); @@ -414,6 +579,27 @@ impl_with_type_bounds!(ProverBase projected_scalars_fx, }) } + + pub fn step1_hybrid_with_field_cfg< + S: Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF + Sync, + >( + mut self, + project_scalar: S, + field_cfg: F::Config, + ) -> Result, ProtocolError> { + let (field_cfg, projected_scalars_fx) = + self.project_common_with_field_cfg(project_scalar, field_cfg)?; + + let row_major_trace = project_trace_coeffs_row_major(self.trace, &field_cfg); + let column_major_trace = project_trace_coeffs_column_major(self.trace, &field_cfg); + Ok(ProverProjectedHybrid { + base: self, + field_cfg, + row_major_trace, + column_major_trace, + projected_scalars_fx, + }) + } }); impl_with_type_bounds!(ProverProjectedCombined @@ -422,7 +608,7 @@ impl_with_type_bounds!(ProverProjectedCombined /// trace. Works for both linear and non-linear constraints. pub fn step2_ideal_check( mut self, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { let num_constraints = count_constraints::(); let (ic_proof, ic_prover_state) = U::prove_combined( @@ -451,7 +637,7 @@ impl_with_type_bounds!(ProverProjectedMleFirst /// trace. Only suitable for linear constraints. pub fn step2_ideal_check( mut self, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { let num_constraints = count_constraints::(); let (ic_proof, ic_prover_state) = U::prove_linear( @@ -483,7 +669,7 @@ impl_with_type_bounds!(ProverProjectedHybrid /// linear and non-linear constraints. pub fn step2_ideal_check( mut self, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { let num_constraints = count_constraints::(); let (ic_proof, ic_prover_state) = U::prove_hybrid( @@ -516,7 +702,7 @@ impl_with_type_bounds!(ProverIdealChecked /// `a in F_q`, evaluates polynomials at `X = a`. pub fn step3_eval_projection( mut self, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { let projecting_element: Zt::Chal = self.base.pcs_transcript.fs_transcript.get_challenge(); let projecting_element_f: F = F::from_with_cfg(&projecting_element, &self.field_cfg); @@ -551,7 +737,7 @@ impl_with_type_bounds!(ProverEvalProjected /// Produces `up_evals` and `down_evals` (CPR) and lookup auxiliary witnesses at `r*`. pub fn step4_sumcheck( mut self, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { let num_constraints = count_constraints::(); // Sumcheck protocol degree must accommodate the actual fold // polynomial's per-variable degree, including `assert_zero` @@ -778,7 +964,7 @@ impl_with_type_bounds!(ProverSumchecked /// to the source's `lifted_eval` (free arithmetic in F_q[X]). pub fn step5_multipoint_eval( mut self, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { let sig = &self.base.uair_signature; // Materialize the bit-op virtual MLEs (under ψ_α) — same shape @@ -834,7 +1020,7 @@ impl_with_type_bounds!(ProverMultipointEvaled /// evaluations at `r_0` in `F_q[X]` and absorbs them into the transcript. pub fn step6_lift_and_project( mut self, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { // Compute per-column polynomial MLE evaluations at r_0 in F_q[X] // (after \phi_q but before \psi_a). The verifier derives the scalar // open_evals via \psi_a for the sumcheck consistency check, and @@ -873,39 +1059,33 @@ impl_with_type_bounds!(ProverLifted /// Step 7: PCS open at `r_0` (witness columns only). pub fn step7_pcs_open( mut self, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { let witness_trace = self.base.trace.witness(&self.base.uair_signature); - if let Some(hint_bin) = &self.base.hint_bin { - let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( - &mut self.base.pcs_transcript, - self.base.pp_bin, - &witness_trace.binary_poly, - &self.r_0, - hint_bin, - &self.field_cfg, - )?; - } - if let Some(hint_arb) = &self.base.hint_arb { - let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( - &mut self.base.pcs_transcript, - self.base.pp_arb, - &witness_trace.arbitrary_poly, - &self.r_0, - hint_arb, - &self.field_cfg, - )?; - } - if let Some(hint_int) = &self.base.hint_int { - let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( - &mut self.base.pcs_transcript, - self.base.pp_int, - &witness_trace.int, - &self.r_0, - hint_int, - &self.field_cfg, - )?; - } + P::BinaryPCS::prove_open::( + &mut self.base.pcs_transcript, + &self.base.pcs_params.binary, + &witness_trace.binary_poly, + &self.r_0, + &self.base.pcs_data.binary, + &self.field_cfg, + )?; + P::ArbitraryPCS::prove_open::( + &mut self.base.pcs_transcript, + &self.base.pcs_params.arbitrary, + &witness_trace.arbitrary_poly, + &self.r_0, + &self.base.pcs_data.arbitrary, + &self.field_cfg, + )?; + P::IntPCS::prove_open::( + &mut self.base.pcs_transcript, + &self.base.pcs_params.int, + &witness_trace.int, + &self.r_0, + &self.base.pcs_data.int, + &self.field_cfg, + )?; Ok(ProverPcsOpened { base: self.base, @@ -922,14 +1102,10 @@ impl_with_type_bounds!(ProverLifted impl_with_type_bounds!(ProverPcsOpened { /// Assemble the final proof from accumulated state. - pub fn finish(self) -> Result, ProtocolError> { + pub fn finish(self) -> Result>, ProtocolError> { let sig = self.base.uair_signature; let zip_proof = self.base.pcs_transcript.stream.into_inner(); - let commitments = ( - self.base.commitment_bin, - self.base.commitment_arb, - self.base.commitment_int, - ); + let commitments = self.base.pcs_commitments; let lifted_evals = self.lifted_evals; @@ -1024,7 +1200,60 @@ where num_vars: usize, project_scalar: impl Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF + Sync, ) -> Result, ProtocolError> { - let committed = Self::step0_commit(pp, trace, num_vars)?; + let pcs_params = PCSParams:: { + binary: pp.0.clone(), + arbitrary: pp.1.clone(), + int: pp.2.clone(), + }; + let proof = Self::prove_with_pcs::( + &pcs_params, + trace, + num_vars, + project_scalar, + )?; + let commitments = proof.commitments; + Ok(Proof { + commitments: (commitments.binary, commitments.arbitrary, commitments.int), + zip: proof.zip, + ideal_check: proof.ideal_check, + resolver: proof.resolver, + combined_sumcheck: proof.combined_sumcheck, + multipoint_eval: proof.multipoint_eval, + witness_lifted_evals: proof.witness_lifted_evals, + lookup_proof: proof.lookup_proof, + }) + } + + pub fn prove_with_pcs( + pp: &PCSParams, + trace: &UairTrace<'static, Zt::Int, Zt::Int, D>, + num_vars: usize, + project_scalar: impl Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF + Sync, + ) -> Result>, ProtocolError> + where + P: ZincPCSTypes, + { + let field_cfg = crate::fixed_prime::secp256k1_field_cfg::(); + Self::prove_with_pcs_and_field_cfg::( + pp, + trace, + num_vars, + project_scalar, + field_cfg, + ) + } + + pub fn prove_with_pcs_and_field_cfg( + pp: &PCSParams, + trace: &UairTrace<'static, Zt::Int, Zt::Int, D>, + num_vars: usize, + project_scalar: impl Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF + Sync, + field_cfg: F::Config, + ) -> Result>, ProtocolError> + where + P: ZincPCSTypes, + { + let committed = Self::step0_commit_with_pcs::

(pp, trace, num_vars)?; let ideal_checked = if MLE_FIRST { // Classify constraints by degree, ignoring zero-ideal (their @@ -1039,22 +1268,26 @@ where if i.is_zero_ideal() { continue; } - if *m { any_linear = true } else { any_nonlinear = true } + if *m { + any_linear = true + } else { + any_nonlinear = true + } } match (any_linear, any_nonlinear) { (true, false) => committed - .step1_mle_first(project_scalar)? + .step1_mle_first_with_field_cfg(project_scalar, field_cfg)? .step2_ideal_check()?, (false, _) => committed - .step1_combined(project_scalar)? + .step1_combined_with_field_cfg(project_scalar, field_cfg)? .step2_ideal_check()?, (true, true) => committed - .step1_hybrid(project_scalar)? + .step1_hybrid_with_field_cfg(project_scalar, field_cfg)? .step2_ideal_check()?, } } else { committed - .step1_combined(project_scalar)? + .step1_combined_with_field_cfg(project_scalar, field_cfg)? .step2_ideal_check()? }; @@ -1266,9 +1499,8 @@ where let projected_trace_f = evaluate_trace_to_column_mles_fast(trace, &projecting_element_f, &field_cfg); - let projected_scalars_f = - project_scalars_to_field(projected_scalars_fx, &projecting_element_f) - .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?; + let projected_scalars_f = project_scalars_to_field(projected_scalars_fx, &projecting_element_f) + .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?; // ── Step 4: CPR + booleanity multi-degree sumcheck ────────────────── let max_degree = count_max_degree::(); @@ -1312,14 +1544,10 @@ where let virtual_mles = if virtual_specs.is_empty() { Vec::new() } else { - let self_bit_slices = compute_bit_slices_flat::( - &trace.binary_poly[num_pub_bin..], - &field_cfg, - ); - let public_bit_slices = compute_bit_slices_flat::( - &trace.binary_poly[..num_pub_bin], - &field_cfg, - ); + let self_bit_slices = + compute_bit_slices_flat::(&trace.binary_poly[num_pub_bin..], &field_cfg); + let public_bit_slices = + compute_bit_slices_flat::(&trace.binary_poly[..num_pub_bin], &field_cfg); let int_witness_cols: Vec<_> = (0..num_wit_int) .map(|i| projected_trace_f[int_offset + num_pub_int + i].clone()) .collect(); @@ -1464,15 +1692,14 @@ where )?; } if let Some(hint_arb) = &hint_arb { - let _ = - ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( - &mut pcs_transcript, - pp_arb, - &witness_trace.arbitrary_poly, - &r_0, - hint_arb, - &field_cfg, - )?; + let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( + &mut pcs_transcript, + pp_arb, + &witness_trace.arbitrary_poly, + &r_0, + hint_arb, + &field_cfg, + )?; } if let Some(hint_int) = &hint_int { let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( @@ -1533,8 +1760,6 @@ where // from `witness_lifted_evals` coefficient eighths (see `verify_folded_4x`). // - - /// Per-domain (binary / arbitrary / integer) byte breakdown of the /// PCS bytes written during step 7 of [`prove_folded_4x`]. Each domain /// holds its own [`ZipPlusProveByteBreakdown`] (sums of the four @@ -1548,7 +1773,6 @@ pub struct FoldedProveZipBreakdown { pub int: ZipPlusProveByteBreakdown, } - /// Per-region wall-time breakdown of a single [`prove_folded_4x`] run, /// populated by [`prove_folded_4x_with_timings`]. Useful as a /// criterion-bypassing diagnostic — each step's `Duration` is measured @@ -1737,7 +1961,14 @@ where INT_QUARTER_LIMBS, MLE_FIRST, CHECK_FOR_OVERFLOW, - >(pp, trace, num_vars, project_scalar, Some(&mut timings), None)?; + >( + pp, + trace, + num_vars, + project_scalar, + Some(&mut timings), + None, + )?; let (_compressed, dt) = zip_plus::utils::serialize_and_compress(&proof); timings.step8_compress = dt; @@ -1808,7 +2039,14 @@ where INT_QUARTER_LIMBS, MLE_FIRST, CHECK_FOR_OVERFLOW, - >(pp, trace, num_vars, project_scalar, None, Some(&mut breakdown))?; + >( + pp, + trace, + num_vars, + project_scalar, + None, + Some(&mut breakdown), + )?; Ok((proof, breakdown)) } @@ -2033,9 +2271,8 @@ where let projected_trace_f = evaluate_trace_to_column_mles_fast(trace, &projecting_element_f, &field_cfg); - let projected_scalars_f = - project_scalars_to_field(projected_scalars_fx, &projecting_element_f) - .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?; + let projected_scalars_f = project_scalars_to_field(projected_scalars_fx, &projecting_element_f) + .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?; if let Some(t) = timings.as_mut() { t.step3_eval_projection = _t_step3.elapsed(); } @@ -2074,14 +2311,10 @@ where let virtual_mles = if virtual_specs.is_empty() { Vec::new() } else { - let self_bit_slices = compute_bit_slices_flat::( - &trace.binary_poly[num_pub_bin..], - &field_cfg, - ); - let public_bit_slices = compute_bit_slices_flat::( - &trace.binary_poly[..num_pub_bin], - &field_cfg, - ); + let self_bit_slices = + compute_bit_slices_flat::(&trace.binary_poly[num_pub_bin..], &field_cfg); + let public_bit_slices = + compute_bit_slices_flat::(&trace.binary_poly[..num_pub_bin], &field_cfg); let int_witness_cols: Vec<_> = (0..num_wit_int) .map(|i| projected_trace_f[int_offset + num_pub_int + i].clone()) .collect(); @@ -2209,9 +2442,7 @@ where ); let int_lifted_evals_4coeff: Vec> = crate::compute_int_fold_4x_lifted_evals::( - &r_0, - &trace.int, - &field_cfg, + &r_0, &trace.int, &field_cfg, ); // Append the 4-coeff int section. lifted_evals.extend(int_lifted_evals_4coeff); diff --git a/protocol/src/verifier.rs b/protocol/src/verifier.rs index 855d682a..4bdd6da8 100644 --- a/protocol/src/verifier.rs +++ b/protocol/src/verifier.rs @@ -38,10 +38,15 @@ use zinc_utils::{ mul_by_scalar::MulByScalar, projectable_to_field::ProjectableToField, }; use zip_plus::{ - pcs::structs::{ZipPlus, ZipPlusParams, ZipTypes}, + pcs::{ + generic::PCS, + structs::{ZipPlus, ZipPlusParams, ZipTypes}, + }, pcs_transcript::PcsVerifierTranscript, }; +use crate::pcs::{AllZipPCSTypes, PCSCommitments, PCSVerifierParams, ZincPCSTypes}; + /// Drop the witness binary_poly column evals the UAIR opted out of /// (sorted, dedup'd `skip_indices` relative to the witness slice). The /// surviving evals line up positionally with the bit-slice blocks @@ -68,16 +73,22 @@ fn filter_skipped_parent_evals( /// Persistent verifier infrastructure carried across every step. #[derive(Clone, Debug)] -pub struct VerifierBase<'a, Zt: ZincTypes, const D: usize> { +pub struct VerifierBase< + 'a, + Zt: ZincTypes, + F: PrimeField, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { num_vars: usize, uair_signature: UairSignature, pcs_transcript: PcsVerifierTranscript, public_trace: &'a UairTrace<'a, Zt::Int, Zt::Int, D>, // Commitment info - vp_bin: &'a ZipPlusParams, - vp_arb: &'a ZipPlusParams, - vp_int: &'a ZipPlusParams, + vp: PCSVerifierParams, + + _phantom: PhantomData<(F, P)>, } // @@ -93,11 +104,12 @@ pub struct VerifierTranscriptReconstructed< F: PrimeField, IdealOverF, const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, > { - base: VerifierBase<'a, Zt, D>, + base: VerifierBase<'a, Zt, F, D, P>, // Proof leftovers - proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment), + proof_commitments: PCSCommitments, proof_ideal_check: IdealCheckProof, proof_resolver: CombinedPolyResolverProof, proof_combined_sumcheck: MultiDegreeSumcheckProof, @@ -116,12 +128,13 @@ pub struct VerifierPrimeProjected< F: PrimeField, IdealOverF, const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, > { - base: VerifierBase<'a, Zt, D>, + base: VerifierBase<'a, Zt, F, D, P>, field_cfg: F::Config, // Proof leftovers - proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment), + proof_commitments: PCSCommitments, proof_ideal_check: IdealCheckProof, proof_resolver: CombinedPolyResolverProof, proof_combined_sumcheck: MultiDegreeSumcheckProof, @@ -140,13 +153,14 @@ pub struct VerifierIdealChecked< F: PrimeField, IdealOverF, const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, > { - base: VerifierBase<'a, Zt, D>, + base: VerifierBase<'a, Zt, F, D, P>, field_cfg: F::Config, ic_subclaim: ideal_check::VerifierSubclaim, // Proof leftovers - proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment), + proof_commitments: PCSCommitments, proof_resolver: CombinedPolyResolverProof, proof_combined_sumcheck: MultiDegreeSumcheckProof, proof_multipoint_eval: MultipointEvalProof, @@ -164,15 +178,16 @@ pub struct VerifierEvalProjected< F: PrimeField, IdealOverF, const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, > { - base: VerifierBase<'a, Zt, D>, + base: VerifierBase<'a, Zt, F, D, P>, field_cfg: F::Config, ic_subclaim: ideal_check::VerifierSubclaim, projecting_element_f: F, projected_scalars_f: ScalarMap, // Proof leftovers - proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment), + proof_commitments: PCSCommitments, proof_resolver: CombinedPolyResolverProof, proof_combined_sumcheck: MultiDegreeSumcheckProof, proof_multipoint_eval: MultipointEvalProof, @@ -183,14 +198,21 @@ pub struct VerifierEvalProjected< /// After step 4 (sumcheck verify). #[derive(Clone, Debug)] -pub struct VerifierSumchecked<'a, Zt: ZincTypes, F: PrimeField, IdealOverF, const D: usize> { - base: VerifierBase<'a, Zt, D>, +pub struct VerifierSumchecked< + 'a, + Zt: ZincTypes, + F: PrimeField, + IdealOverF, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { + base: VerifierBase<'a, Zt, F, D, P>, field_cfg: F::Config, projecting_element_f: F, cpr_subclaim: combined_poly_resolver::VerifierSubclaim, // Proof leftovers - proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment), + proof_commitments: PCSCommitments, proof_multipoint_eval: MultipointEvalProof, proof_witness_lifted_evals: Vec>, proof_lookup_proof: Option>, @@ -199,15 +221,21 @@ pub struct VerifierSumchecked<'a, Zt: ZincTypes, F: PrimeField, IdealOverF, c /// After step 5 (multi-point eval). #[derive(Clone, Debug)] -pub struct VerifierMultipointEvaled<'a, Zt: ZincTypes, F: PrimeField, IdealOverF, const D: usize> -{ - base: VerifierBase<'a, Zt, D>, +pub struct VerifierMultipointEvaled< + 'a, + Zt: ZincTypes, + F: PrimeField, + IdealOverF, + const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, +> { + base: VerifierBase<'a, Zt, F, D, P>, field_cfg: F::Config, projecting_element_f: F, mp_subclaim: multipoint_eval::Subclaim, // Proof leftovers - proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment), + proof_commitments: PCSCommitments, proof_witness_lifted_evals: Vec>, proof_lookup_proof: Option>, _phantom: PhantomData, @@ -222,14 +250,15 @@ pub struct VerifierLiftedEvalsChecked< F: PrimeField, IdealOverF, const D: usize, + P: ZincPCSTypes = AllZipPCSTypes, > { - base: VerifierBase<'a, Zt, D>, + base: VerifierBase<'a, Zt, F, D, P>, field_cfg: F::Config, mp_subclaim: multipoint_eval::Subclaim, all_lifted_evals: Vec>, // Proof leftovers - proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment), + proof_commitments: PCSCommitments, proof_lookup_proof: Option>, _phantom: PhantomData, } @@ -249,19 +278,27 @@ impl ZincPlusPiop where Zt: ZincTypes, U: Uair, - F: PrimeField, + F: PrimeField + + FromPrimitiveWithConfig + + for<'b> FromWithConfig<&'b ::CombR> + + for<'b> FromWithConfig<&'b ::CombR> + + for<'b> FromWithConfig<&'b ::CombR> + + for<'b> FromWithConfig<&'b Zt::Chal> + + for<'b> MulByScalar<&'b F> + + FromRef, F::Inner: ConstTranscribable, + F::Modulus: FromRef, { /// Step 0: Verifier entry point. /// Reconstruct Fiat-Shamir transcript from commitments and public data. #[allow(clippy::type_complexity)] pub fn step0_reconstruct_transcript<'a, IdealOverF>( - (vp_bin, vp_arb, vp_int): &'a ( + (vp_bin, vp_arb, vp_int): &( ZipPlusParams, ZipPlusParams, ZipPlusParams, ), - mut proof: Proof, + proof: Proof, public_trace: &'a UairTrace<'a, Zt::Int, Zt::Int, D>, num_vars: usize, ) -> Result< @@ -270,6 +307,57 @@ where > where IdealOverF: Ideal, + AllZipPCSTypes: ZincPCSTypes< + Zt, + F, + D, + BinaryPCS = zip_plus::pcs::generic::ZipPlusPCS, + ArbitraryPCS = zip_plus::pcs::generic::ZipPlusPCS, + IntPCS = zip_plus::pcs::generic::ZipPlusPCS, + >, + { + let pcs_vp = PCSVerifierParams:: { + binary: vp_bin.clone(), + arbitrary: vp_arb.clone(), + int: vp_int.clone(), + }; + let commitments = proof.commitments; + let proof = Proof { + commitments: PCSCommitments:: { + binary: commitments.0, + arbitrary: commitments.1, + int: commitments.2, + }, + zip: proof.zip, + ideal_check: proof.ideal_check, + resolver: proof.resolver, + combined_sumcheck: proof.combined_sumcheck, + multipoint_eval: proof.multipoint_eval, + witness_lifted_evals: proof.witness_lifted_evals, + lookup_proof: proof.lookup_proof, + }; + Self::step0_reconstruct_transcript_with_pcs::( + &pcs_vp, + proof, + public_trace, + num_vars, + ) + } + + /// Step 0 with an explicit PCS bundle. + #[allow(clippy::type_complexity)] + pub fn step0_reconstruct_transcript_with_pcs<'a, IdealOverF, P>( + vp: &PCSVerifierParams, + mut proof: Proof>, + public_trace: &'a UairTrace<'a, Zt::Int, Zt::Int, D>, + num_vars: usize, + ) -> Result< + VerifierTranscriptReconstructed<'a, Zt, U, F, IdealOverF, D, P>, + ProtocolError, + > + where + P: ZincPCSTypes, + IdealOverF: Ideal, { let zip_proof = std::mem::take(&mut proof.zip); let mut base = VerifierBase { @@ -280,18 +368,22 @@ where fs_transcript: Blake3Transcript::default(), stream: Cursor::new(zip_proof), }, - vp_bin, - vp_arb, - vp_int, + vp: vp.clone(), + _phantom: PhantomData, }; - for comm in [ - &proof.commitments.0, - &proof.commitments.1, - &proof.commitments.2, - ] { - base.pcs_transcript.fs_transcript.absorb_slice(&comm.root); - } + P::BinaryPCS::absorb_commitment( + &mut base.pcs_transcript.fs_transcript, + &proof.commitments.binary, + ); + P::ArbitraryPCS::absorb_commitment( + &mut base.pcs_transcript.fs_transcript, + &proof.commitments.arbitrary, + ); + P::IntPCS::absorb_commitment( + &mut base.pcs_transcript.fs_transcript, + &proof.commitments.int, + ); absorb_public_columns( &mut base.pcs_transcript.fs_transcript, @@ -320,10 +412,11 @@ where } } -impl<'a, Zt, U, F, IdealOverF, const D: usize> - VerifierTranscriptReconstructed<'a, Zt, U, F, IdealOverF, D> +impl<'a, Zt, U, F, IdealOverF, const D: usize, P> + VerifierTranscriptReconstructed<'a, Zt, U, F, IdealOverF, D, P> where Zt: ZincTypes, + P: ZincPCSTypes, F: InnerTransparentField + FromPrimitiveWithConfig + FromRef + Send + Sync + 'static, F::Inner: ConstIntSemiring + ConstTranscribable + Send + Sync + Zero + Default, F::Modulus: ConstTranscribable + FromRef, @@ -335,13 +428,21 @@ where #[allow(clippy::type_complexity)] pub fn step1_prime_projection( self, - ) -> Result, ProtocolError> + ) -> Result, ProtocolError> { // `fixed-prime` branch: use the secp256k1 base field prime as the // projecting prime instead of drawing one from the transcript. // See `crate::fixed_prime` for the soundness caveat. let field_cfg = crate::fixed_prime::secp256k1_field_cfg::(); + self.step1_prime_projection_with_field_cfg(field_cfg) + } + + pub fn step1_prime_projection_with_field_cfg( + self, + field_cfg: F::Config, + ) -> Result, ProtocolError> + { Ok(VerifierPrimeProjected { base: self.base, field_cfg, @@ -357,9 +458,11 @@ where } } -impl<'a, Zt, U, F, IdealOverF, const D: usize> VerifierPrimeProjected<'a, Zt, U, F, IdealOverF, D> +impl<'a, Zt, U, F, IdealOverF, const D: usize, P> + VerifierPrimeProjected<'a, Zt, U, F, IdealOverF, D, P> where Zt: ZincTypes, + P: ZincPCSTypes, Zt::Int: ProjectableToField, ::Eval: ProjectableToField, F: InnerTransparentField @@ -384,7 +487,7 @@ where pub fn step2_ideal_check( mut self, project_ideal: impl Fn(&IdealOrZero, &F::Config) -> IdealOverF, - ) -> Result, ProtocolError> + ) -> Result, ProtocolError> { let num_constraints = count_constraints::(); @@ -412,9 +515,11 @@ where } } -impl<'a, Zt, U, F, IdealOverF, const D: usize> VerifierIdealChecked<'a, Zt, U, F, IdealOverF, D> +impl<'a, Zt, U, F, IdealOverF, const D: usize, P> + VerifierIdealChecked<'a, Zt, U, F, IdealOverF, D, P> where Zt: ZincTypes, + P: ZincPCSTypes, F: InnerTransparentField + for<'b> FromWithConfig<&'b Zt::Chal> + FromRef @@ -430,7 +535,7 @@ where pub fn step3_eval_projection( mut self, project_scalar: impl Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF + Sync, - ) -> Result, ProtocolError> + ) -> Result, ProtocolError> { let projecting_element: Zt::Chal = self.base.pcs_transcript.fs_transcript.get_challenge(); let projecting_element_f: F = F::from_with_cfg(&projecting_element, &self.field_cfg); @@ -457,9 +562,11 @@ where } } -impl<'a, Zt, U, F, IdealOverF, const D: usize> VerifierEvalProjected<'a, Zt, U, F, IdealOverF, D> +impl<'a, Zt, U, F, IdealOverF, const D: usize, P> + VerifierEvalProjected<'a, Zt, U, F, IdealOverF, D, P> where Zt: ZincTypes, + P: ZincPCSTypes, Zt::Int: ProjectableToField, ::Eval: ProjectableToField, F: InnerTransparentField @@ -482,31 +589,26 @@ where /// Step 4: Sumcheck verification (CPR + algebraic booleanity). pub fn step4_sumcheck_verify( mut self, - ) -> Result, ProtocolError> { + ) -> Result, ProtocolError> { let num_constraints = count_constraints::(); let num_pub_bin = self .base .uair_signature .public_cols() .num_binary_poly_cols(); - let num_total_bin = - self.base.uair_signature.total_cols().num_binary_poly_cols(); + let num_total_bin = self.base.uair_signature.total_cols().num_binary_poly_cols(); let bool_skip = self.base.uair_signature.booleanity_skip_indices(); // Booleanity covers: witness binary_poly cols (minus // `booleanity_skip_indices`), packed virtual binary_poly cols, // declared int bit cols, and virtual booleanity linear-combo cols. - let num_int_bit_cols = - self.base.uair_signature.int_witness_bit_cols().len(); - let num_virtual_cols = - self.base.uair_signature.virtual_booleanity_cols().len(); - let num_virtual_bp_cols = - self.base.uair_signature.virtual_binary_poly_cols().len(); + let num_int_bit_cols = self.base.uair_signature.int_witness_bit_cols().len(); + let num_virtual_cols = self.base.uair_signature.virtual_booleanity_cols().len(); + let num_virtual_bp_cols = self.base.uair_signature.virtual_binary_poly_cols().len(); let num_bit_slices = ((num_total_bin - num_pub_bin) - bool_skip.len()) * D + num_virtual_bp_cols * D + num_int_bit_cols + num_virtual_cols; - let num_shifted_bit_slices = - self.base.uair_signature.shifted_bit_slice_specs().len() * D; + let num_shifted_bit_slices = self.base.uair_signature.shifted_bit_slice_specs().len() * D; let cpr_verifier_ancillary = CombinedPolyResolver::prepare_verifier::( &mut self.base.pcs_transcript.fs_transcript, @@ -524,8 +626,7 @@ where // 4b: Booleanity verifier prep — samples α_b, validates that the // booleanity group's claimed sum is zero (zerocheck). let bool_verifier_ancillary_opt = if num_bit_slices > 0 { - let bool_claimed_sum = - self.proof_combined_sumcheck.claimed_sums()[1].clone(); + let bool_claimed_sum = self.proof_combined_sumcheck.claimed_sums()[1].clone(); prepare_booleanity_verifier::( &mut self.base.pcs_transcript.fs_transcript, bool_claimed_sum, @@ -564,7 +665,11 @@ where // booleanity-bound MLE to the committed sources without a // separate equality check. let int_offset = self.base.uair_signature.total_cols().num_binary_poly_cols() - + self.base.uair_signature.total_cols().num_arbitrary_poly_cols(); + + self + .base + .uair_signature + .total_cols() + .num_arbitrary_poly_cols(); let num_pub_int = self.base.uair_signature.public_cols().num_int_cols(); let num_wit_int = self.base.uair_signature.witness_cols().num_int_cols(); let num_binary_bit_slices_for_overrides = (num_total_bin - num_pub_bin) * D; @@ -574,21 +679,20 @@ where // Public binary_poly bit slice evals at the shared sumcheck // point — verifier computes locally from public_trace; reused // by both virtual-bool and virtual-binary-poly overrides. - let public_bit_slice_evals: Vec = if !virtual_bp_specs.is_empty() - || !virtual_specs.is_empty() - { - let public_bit_slice_mles = compute_bit_slices_flat::( - &self.base.public_trace.binary_poly, - &self.field_cfg, - ); - public_bit_slice_mles - .into_iter() - .map(|mle| mle.evaluate_with_config(md_subclaims.point(), &self.field_cfg)) - .collect::, _>>() - .map_err(ProtocolError::ShiftedBitSliceEval)? - } else { - Vec::new() - }; + let public_bit_slice_evals: Vec = + if !virtual_bp_specs.is_empty() || !virtual_specs.is_empty() { + let public_bit_slice_mles = compute_bit_slices_flat::( + &self.base.public_trace.binary_poly, + &self.field_cfg, + ); + public_bit_slice_mles + .into_iter() + .map(|mle| mle.evaluate_with_config(md_subclaims.point(), &self.field_cfg)) + .collect::, _>>() + .map_err(ProtocolError::ShiftedBitSliceEval)? + } else { + Vec::new() + }; // closing_overrides_tail layout (in trailing-position order): // [virtual_binary_poly_per_bit (V_b * D), @@ -617,9 +721,7 @@ where ); if !virtual_specs.is_empty() { let int_witness_up_evals: Vec = (0..num_wit_int) - .map(|i| { - cpr_subclaim.up_evals[int_offset + num_pub_int + i].clone() - }) + .map(|i| cpr_subclaim.up_evals[int_offset + num_pub_int + i].clone()) .collect(); let virtual_overrides = compute_virtual_closing_overrides::( virtual_specs, @@ -670,8 +772,7 @@ where // Shifted bit-slice consistency: tie each spec's emitted bit // slices to the corresponding `down_eval` (= parent col at // shifted point) via the same projection-element trick. - let shifted_down_indices = - self.base.uair_signature.shifted_bit_slice_down_indices(); + let shifted_down_indices = self.base.uair_signature.shifted_bit_slice_down_indices(); let shifted_parent_evals: Vec = shifted_down_indices .iter() .map(|&i| cpr_subclaim.down_evals[i].clone()) @@ -700,9 +801,10 @@ where } } -impl<'a, Zt, F, IdealOverF, const D: usize> VerifierSumchecked<'a, Zt, F, IdealOverF, D> +impl<'a, Zt, F, IdealOverF, const D: usize, P> VerifierSumchecked<'a, Zt, F, IdealOverF, D, P> where Zt: ZincTypes, + P: ZincPCSTypes, F: InnerTransparentField + FromPrimitiveWithConfig + FromRef + Send + Sync + 'static, F::Inner: ConstIntSemiring + ConstTranscribable + Send + Sync + Zero + Default, F::Modulus: ConstTranscribable + FromRef, @@ -719,7 +821,7 @@ where /// lifted eval (free arithmetic in F_q[X]). pub fn step5_multipoint_eval( mut self, - ) -> Result, ProtocolError> + ) -> Result, ProtocolError> { let cpr_eval_point = self.cpr_subclaim.evaluation_point.clone(); @@ -750,9 +852,10 @@ where } } -impl<'a, Zt, F, IdealOverF, const D: usize> VerifierMultipointEvaled<'a, Zt, F, IdealOverF, D> +impl<'a, Zt, F, IdealOverF, const D: usize, P> VerifierMultipointEvaled<'a, Zt, F, IdealOverF, D, P> where Zt: ZincTypes, + P: ZincPCSTypes, Zt::Int: ProjectableToField, ::Eval: ProjectableToField, F: InnerTransparentField @@ -781,7 +884,7 @@ where /// `MultipointEval(ClaimMismatch)` from `verify_subclaim`. pub fn step6_lifted_evals( mut self, - ) -> Result, ProtocolError> + ) -> Result, ProtocolError> { let r_0 = &self.mp_subclaim.sumcheck_subclaim.point; @@ -869,9 +972,11 @@ where } } -impl<'a, Zt, F, IdealOverF, const D: usize> VerifierLiftedEvalsChecked<'a, Zt, F, IdealOverF, D> +impl<'a, Zt, F, IdealOverF, const D: usize, P> + VerifierLiftedEvalsChecked<'a, Zt, F, IdealOverF, D, P> where Zt: ZincTypes, + P: ZincPCSTypes, Zt::Int: ProjectableToField, ::Cw: ProjectableToField, ::Eval: ProjectableToField, @@ -913,60 +1018,33 @@ where let field_cfg = &self.field_cfg; let all_lifted_evals = &self.all_lifted_evals; - macro_rules! verify_pcs_batch { - ($Zt:ty, $Lc:ty, $vp:expr, $idx:tt, [$evals_range:expr]) => {{ - let comm = &commitments.$idx; - if comm.batch_size > 0 { - let per_poly_alphas = ZipPlus::<$Zt, $Lc>::sample_alphas( - &mut pcs_transcript.fs_transcript, - comm.batch_size, - ); - let mut eval_f = F::zero_with_cfg(field_cfg); - for (bar_u, alphas) in all_lifted_evals[$evals_range] - .iter() - .zip(per_poly_alphas.iter()) - { - for (coeff, alpha) in bar_u.coeffs.iter().zip(alphas.iter()) { - let mut term = F::from_with_cfg(alpha, field_cfg); - term *= coeff; - eval_f += &term; - } - } - ZipPlus::<$Zt, $Lc>::verify_with_alphas::( - pcs_transcript, - $vp, - comm, - field_cfg, - r_0, - &eval_f, - &per_poly_alphas, - ) - .map_err(|e| ProtocolError::PcsVerification($idx, e))?; - } - }}; - } - - verify_pcs_batch!( - Zt::BinaryZt, - Zt::BinaryLc, - self.base.vp_bin, - 0, - [num_pub_bin..num_total_bin] - ); - verify_pcs_batch!( - Zt::ArbitraryZt, - Zt::ArbitraryLc, - self.base.vp_arb, - 1, - [add!(num_total_bin, num_pub_arb)..add!(num_total_bin, num_total_arb)] - ); - verify_pcs_batch!( - Zt::IntZt, - Zt::IntLc, - self.base.vp_int, - 2, - [add!(add!(num_total_bin, num_total_arb), num_pub_int)..] - ); + P::BinaryPCS::verify_open::( + pcs_transcript, + &self.base.vp.binary, + &commitments.binary, + r_0, + &all_lifted_evals[num_pub_bin..num_total_bin], + field_cfg, + ) + .map_err(|e| ProtocolError::PcsVerification(0, e))?; + P::ArbitraryPCS::verify_open::( + pcs_transcript, + &self.base.vp.arbitrary, + &commitments.arbitrary, + r_0, + &all_lifted_evals[add!(num_total_bin, num_pub_arb)..add!(num_total_bin, num_total_arb)], + field_cfg, + ) + .map_err(|e| ProtocolError::PcsVerification(1, e))?; + P::IntPCS::verify_open::( + pcs_transcript, + &self.base.vp.int, + &commitments.int, + r_0, + &all_lifted_evals[add!(add!(num_total_bin, num_total_arb), num_pub_int)..], + field_cfg, + ) + .map_err(|e| ProtocolError::PcsVerification(2, e))?; Ok(VerifierPcsVerified { _phantom: PhantomData, @@ -1028,24 +1106,95 @@ where ) -> Result<(), ProtocolError> where IdealOverF: Ideal + IdealCheck>, + AllZipPCSTypes: ZincPCSTypes< + Zt, + F, + D, + BinaryPCS = zip_plus::pcs::generic::ZipPlusPCS, + ArbitraryPCS = zip_plus::pcs::generic::ZipPlusPCS, + IntPCS = zip_plus::pcs::generic::ZipPlusPCS, + >, + { + let pcs_vp = PCSVerifierParams:: { + binary: vp.0.clone(), + arbitrary: vp.1.clone(), + int: vp.2.clone(), + }; + let commitments = proof.commitments; + let proof = Proof { + commitments: PCSCommitments:: { + binary: commitments.0, + arbitrary: commitments.1, + int: commitments.2, + }, + zip: proof.zip, + ideal_check: proof.ideal_check, + resolver: proof.resolver, + combined_sumcheck: proof.combined_sumcheck, + multipoint_eval: proof.multipoint_eval, + witness_lifted_evals: proof.witness_lifted_evals, + lookup_proof: proof.lookup_proof, + }; + + Self::verify_with_pcs::( + &pcs_vp, + proof, + public_trace, + num_vars, + project_scalar, + project_ideal, + ) + } + + #[allow(clippy::too_many_arguments, clippy::type_complexity)] + pub fn verify_with_pcs( + vp: &PCSVerifierParams, + proof: Proof>, + public_trace: &UairTrace, + num_vars: usize, + project_scalar: impl Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF + Sync, + project_ideal: impl Fn(&IdealOrZero, &F::Config) -> IdealOverF, + ) -> Result<(), ProtocolError> + where + P: ZincPCSTypes, + IdealOverF: Ideal + IdealCheck>, + { + let field_cfg = crate::fixed_prime::secp256k1_field_cfg::(); + Self::verify_with_pcs_and_field_cfg::( + vp, + proof, + public_trace, + num_vars, + project_scalar, + project_ideal, + field_cfg, + ) + } + + #[allow(clippy::too_many_arguments, clippy::type_complexity)] + pub fn verify_with_pcs_and_field_cfg( + vp: &PCSVerifierParams, + proof: Proof>, + public_trace: &UairTrace, + num_vars: usize, + project_scalar: impl Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF + Sync, + project_ideal: impl Fn(&IdealOrZero, &F::Config) -> IdealOverF, + field_cfg: F::Config, + ) -> Result<(), ProtocolError> + where + P: ZincPCSTypes, + IdealOverF: Ideal + IdealCheck>, { - // Verifier-side public-column structural checks. UAIRs that - // need to enforce structural properties of public columns - // (compensator-zero on active rows, tail-corrector-zero on - // inner rows, etc.) discharge them here, by direct row-wise - // inspection of public_trace, before any algebraic check - // begins. Default impl is a no-op for UAIRs that don't need - // such checks. U::verify_public_structure(public_trace, num_vars) .map_err(ProtocolError::PublicStructure)?; - ZincPlusPiop::::step0_reconstruct_transcript::( + ZincPlusPiop::::step0_reconstruct_transcript_with_pcs::( vp, proof, public_trace, num_vars, )? - .step1_prime_projection()? + .step1_prime_projection_with_field_cfg(field_cfg)? .step2_ideal_check(project_ideal)? .step3_eval_projection(project_scalar)? .step4_sumcheck_verify()? @@ -1116,8 +1265,7 @@ where // Verifier-side public-column structural checks (compensator/ // corrector zero-pinning, etc.). UAIRs that don't need extra // structural checks fall through this with a no-op default impl. - U::verify_public_structure(public_trace, num_vars) - .map_err(ProtocolError::PublicStructure)?; + U::verify_public_structure(public_trace, num_vars).map_err(ProtocolError::PublicStructure)?; // ── Step 0: Reconstruct transcript ────────────────────────────────── let zip_proof = std::mem::take(&mut proof.zip); @@ -1164,9 +1312,8 @@ where let projecting_element_f: F = F::from_with_cfg(&projecting_element, &field_cfg); let projected_scalars_fx = project_scalars::(|s| project_scalar(s, &field_cfg)); - let projected_scalars_f = - project_scalars_to_field(projected_scalars_fx, &projecting_element_f) - .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?; + let projected_scalars_f = project_scalars_to_field(projected_scalars_fx, &projecting_element_f) + .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?; // ── Step 4: Sumcheck verify (CPR + algebraic booleanity) ──────────── let num_pub_bin = uair_signature.public_cols().num_binary_poly_cols(); @@ -1179,8 +1326,7 @@ where + num_virtual_bp_cols * D + num_int_bit_cols + num_virtual_cols; - let num_shifted_bit_slices = - uair_signature.shifted_bit_slice_specs().len() * D; + let num_shifted_bit_slices = uair_signature.shifted_bit_slice_specs().len() * D; let cpr_verifier_ancillary = CombinedPolyResolver::prepare_verifier::( &mut pcs_transcript.fs_transcript, &proof.resolver, @@ -1234,21 +1380,18 @@ where let num_binary_bit_slices = (num_total_bin - num_pub_bin) * D; let virtual_bp_specs = uair_signature.virtual_binary_poly_cols(); let virtual_specs = uair_signature.virtual_booleanity_cols(); - let public_bit_slice_evals: Vec = if !virtual_bp_specs.is_empty() - || !virtual_specs.is_empty() - { - let public_bit_slice_mles = compute_bit_slices_flat::( - &public_trace.binary_poly, - &field_cfg, - ); - public_bit_slice_mles - .into_iter() - .map(|mle| mle.evaluate_with_config(md_subclaims.point(), &field_cfg)) - .collect::, _>>() - .map_err(ProtocolError::ShiftedBitSliceEval)? - } else { - Vec::new() - }; + let public_bit_slice_evals: Vec = + if !virtual_bp_specs.is_empty() || !virtual_specs.is_empty() { + let public_bit_slice_mles = + compute_bit_slices_flat::(&public_trace.binary_poly, &field_cfg); + public_bit_slice_mles + .into_iter() + .map(|mle| mle.evaluate_with_config(md_subclaims.point(), &field_cfg)) + .collect::, _>>() + .map_err(ProtocolError::ShiftedBitSliceEval)? + } else { + Vec::new() + }; let mut closing_overrides_tail: Vec = Vec::new(); if !virtual_bp_specs.is_empty() { let virtual_bp_overrides = compute_virtual_binary_poly_closing_overrides::( @@ -1428,11 +1571,10 @@ where { let comm = &proof.commitments.0; if comm.batch_size > 0 { - let per_poly_alphas = - ZipPlus::::sample_alphas( - &mut pcs_transcript.fs_transcript, - comm.batch_size, - ); + let per_poly_alphas = ZipPlus::::sample_alphas( + &mut pcs_transcript.fs_transcript, + comm.batch_size, + ); let one = F::one_with_cfg(&field_cfg); let one_minus_gamma = one - gamma.clone(); @@ -1485,11 +1627,10 @@ where { let comm = &proof.commitments.1; if comm.batch_size > 0 { - let per_poly_alphas = - ZipPlus::::sample_alphas( - &mut pcs_transcript.fs_transcript, - comm.batch_size, - ); + let per_poly_alphas = ZipPlus::::sample_alphas( + &mut pcs_transcript.fs_transcript, + comm.batch_size, + ); let mut eval_f = F::zero_with_cfg(&field_cfg); for (bar_u, alphas) in all_lifted_evals [add!(num_total_bin, num_pub_arb)..add!(num_total_bin, num_total_arb)] @@ -1586,7 +1727,6 @@ where // witness columns. // - /// Per-region wall-time breakdown of a single [`verify_folded_4x`] run, /// populated by [`verify_folded_4x_with_timings`]. Mirrors /// [`crate::prover::FoldedProveTimings`] step-for-step; summing the @@ -1894,9 +2034,8 @@ where let projecting_element_f: F = F::from_with_cfg(&projecting_element, &field_cfg); let projected_scalars_fx = project_scalars::(|s| project_scalar(s, &field_cfg)); - let projected_scalars_f = - project_scalars_to_field(projected_scalars_fx, &projecting_element_f) - .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?; + let projected_scalars_f = project_scalars_to_field(projected_scalars_fx, &projecting_element_f) + .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?; if let Some(t) = timings.as_mut() { t.step3_eval_projection = _t_step3.elapsed(); } @@ -2081,11 +2220,10 @@ where let num_wit_arb = wit_cols.num_arbitrary_poly_cols(); let public_lifted = if add!(add!(num_pub_bin, num_pub_arb), num_pub_int) > 0 { - let projected_public = - project_trace_coeffs_row_major::, Int, D>( - public_trace, - &field_cfg, - ); + let projected_public = project_trace_coeffs_row_major::, Int, D>( + public_trace, + &field_cfg, + ); let mut lifted = crate::compute_lifted_evals::( &r_0, &public_trace.binary_poly, @@ -2093,12 +2231,11 @@ where &field_cfg, ); if num_pub_int > 0 { - let int_4coeff = - crate::compute_int_fold_4x_lifted_evals::( - &r_0, - &public_trace.int, - &field_cfg, - ); + let int_4coeff = crate::compute_int_fold_4x_lifted_evals::< + F, + INT_LIMBS, + INT_QUARTER_LIMBS, + >(&r_0, &public_trace.int, &field_cfg); let int_off = num_pub_bin + num_pub_arb; for (i, bar_u) in int_4coeff.into_iter().enumerate() { lifted[int_off + i] = bar_u; @@ -2233,7 +2370,10 @@ where // Closure: compute eval_f for the binary path's 4× fold. let bin_eval_f = |alphas: &[Vec<::Chal>]| -> F { let mut eval_f = zero.clone(); - for (bar_u, a) in all_lifted_evals[bin_range.clone()].iter().zip(alphas.iter()) { + for (bar_u, a) in all_lifted_evals[bin_range.clone()] + .iter() + .zip(alphas.iter()) + { debug_assert_eq!(a.len(), QUARTER_D); let mut c00 = zero.clone(); let mut c10 = zero.clone(); @@ -2283,7 +2423,10 @@ where // c00=α·c[0], c10=α·c[2], c01=α·c[1], c11=α·c[3]. let int_eval_f = |alphas: &[Vec<::Chal>]| -> F { let mut eval_f = zero.clone(); - for (bar_u, a) in all_lifted_evals[int_range.clone()].iter().zip(alphas.iter()) { + for (bar_u, a) in all_lifted_evals[int_range.clone()] + .iter() + .zip(alphas.iter()) + { debug_assert_eq!(a.len(), 1); let a_0: F = F::from_with_cfg(&a[0], &field_cfg); let z = || F::zero_with_cfg(&field_cfg); @@ -2326,7 +2469,10 @@ where // Closure: arb's eval_f (standard ). let arb_eval_f = |alphas: &[Vec<::Chal>]| -> F { let mut eval_f = F::zero_with_cfg(&field_cfg); - for (bar_u, a) in all_lifted_evals[arb_range.clone()].iter().zip(alphas.iter()) { + for (bar_u, a) in all_lifted_evals[arb_range.clone()] + .iter() + .zip(alphas.iter()) + { for (coeff, alpha) in bar_u.coeffs.iter().zip(a.iter()) { let mut term = F::from_with_cfg(alpha, &field_cfg); term *= coeff; @@ -2423,7 +2569,9 @@ where ZipPlus::::verify_pre_open_finalize::< F, CHECK_FOR_OVERFLOW, - >(vp_bin_split2, &field_cfg, &r0_ext, &eval_f, reads) + >( + vp_bin_split2, &field_cfg, &r0_ext, &eval_f, reads + ) .map_err(|e| ProtocolError::PcsVerification(0, e))?, )), _ => Ok(None), @@ -2448,7 +2596,9 @@ where ZipPlus::::verify_pre_open_finalize::< F, CHECK_FOR_OVERFLOW, - >(vp_int_split4, &field_cfg, &r0_ext, &eval_f, reads) + >( + vp_int_split4, &field_cfg, &r0_ext, &eval_f, reads + ) .map_err(|e| ProtocolError::PcsVerification(2, e))?, )), _ => Ok(None), @@ -2486,11 +2636,10 @@ where } else { // Per-instance fallback. if proof.commitments.0.batch_size > 0 { - let alphas = - ZipPlus::::sample_alphas( - &mut pcs_transcript.fs_transcript, - proof.commitments.0.batch_size, - ); + let alphas = ZipPlus::::sample_alphas( + &mut pcs_transcript.fs_transcript, + proof.commitments.0.batch_size, + ); let eval_f = bin_eval_f(&alphas); ZipPlus::::verify_with_alphas::( &mut pcs_transcript, @@ -2504,11 +2653,10 @@ where .map_err(|e| ProtocolError::PcsVerification(0, e))?; } if proof.commitments.1.batch_size > 0 { - let alphas = - ZipPlus::::sample_alphas( - &mut pcs_transcript.fs_transcript, - proof.commitments.1.batch_size, - ); + let alphas = ZipPlus::::sample_alphas( + &mut pcs_transcript.fs_transcript, + proof.commitments.1.batch_size, + ); let eval_f = arb_eval_f(&alphas); ZipPlus::::verify_with_alphas::< F, diff --git a/test-uair/src/ecdsa.rs b/test-uair/src/ecdsa.rs index 1ad1b121..3f0aab83 100644 --- a/test-uair/src/ecdsa.rs +++ b/test-uair/src/ecdsa.rs @@ -64,8 +64,7 @@ use rand::RngCore; use zinc_poly::{mle::DenseMultilinearExtension, univariate::dense::DensePolynomial}; use zinc_uair::{ ConstraintBuilder, PublicColumnLayout, ShiftSpec, TotalColumnLayout, TraceRow, Uair, - UairSignature, UairTrace, - ideal::ImpossibleIdeal, + UairSignature, UairTrace, ideal::ImpossibleIdeal, }; use crate::GenerateRandomTrace; @@ -315,15 +314,12 @@ where // C-D4: Y_pa − 12·X³·Y² + 3·X²·X_pa + 8·Y⁴ = 0 let x3_y_sq = x_sq.clone() * &x_y_sq; - let twelve_x3_y_sq = - mbs(&x3_y_sq, &twelve_scalar).expect("12·X³·Y² overflow"); + let twelve_x3_y_sq = mbs(&x3_y_sq, &twelve_scalar).expect("12·X³·Y² overflow"); let x_sq_x_pa = x_sq.clone() * x_pa; - let three_x2_xpa = - mbs(&x_sq_x_pa, &three_scalar).expect("3·X²·X_pa overflow"); + let three_x2_xpa = mbs(&x_sq_x_pa, &three_scalar).expect("3·X²·X_pa overflow"); let y_pow4 = y_sq.clone() * &y_sq; let eight_y_pow4 = mbs(&y_pow4, &eight_scalar).expect("8·Y⁴ overflow"); - let d4_inner = - y_pa.clone() - &twelve_x3_y_sq + &three_x2_xpa + &eight_y_pow4; + let d4_inner = y_pa.clone() - &twelve_x3_y_sq + &three_x2_xpa + &eight_y_pow4; b.assert_zero(s_active.clone() * &d4_inner); // =================================================================== @@ -376,12 +372,10 @@ where // C-O2 (Y): down.Y − Y_pa − S_ADD·(3·D·X_pa·C² + D·C³ − D³ − Y_pa·C³ − Y_pa) = 0 let d_cube = d.clone() * &d_sq; let d_x_pa_c_sq = d.clone() * &x_pa_c_sq; - let three_d_x_pa_c_sq = - mbs(&d_x_pa_c_sq, &three_scalar).expect("3·D·X_pa·C² overflow"); + let three_d_x_pa_c_sq = mbs(&d_x_pa_c_sq, &three_scalar).expect("3·D·X_pa·C² overflow"); let d_c_cube = d.clone() * &c_cube; let y_pa_c_cube = y_pa.clone() * &c_cube; - let y_add_minus_y_pa = - three_d_x_pa_c_sq + &d_c_cube - &d_cube - &y_pa_c_cube - y_pa; + let y_add_minus_y_pa = three_d_x_pa_c_sq + &d_c_cube - &d_cube - &y_pa_c_cube - y_pa; let s_add_y = s_add.clone() * &y_add_minus_y_pa; let o2_inner = down_y.clone() - y_pa - &s_add_y; b.assert_zero(s_active.clone() * &o2_inner); @@ -494,13 +488,11 @@ fn rand_nonzero_fp(rng: &mut Rng) -> CbUint) -> CbUint { let p_odd = Odd::new(SECP256K1_P_UINT).expect("p is odd"); - a.invert_odd_mod(&p_odd).expect("a has no inverse mod p (a == 0?)") + a.invert_odd_mod(&p_odd) + .expect("a has no inverse mod p (a == 0?)") } -fn mul_mod_p( - a: &CbUint, - b: &CbUint, -) -> CbUint { +fn mul_mod_p(a: &CbUint, b: &CbUint) -> CbUint { let wide: CbUint<{ EC_FP_INT_LIMBS * 2 }> = a.widening_mul(b).into(); let p_wide: CbUint<{ EC_FP_INT_LIMBS * 2 }> = SECP256K1_P_UINT.resize(); let p_wide_nz = NonZero::new(p_wide).expect("p is nonzero"); @@ -526,10 +518,7 @@ fn p_geq(a: &CbUint) -> bool { a.checked_sub(&SECP256K1_P_UINT).is_some().into() } -fn sub_mod_p( - a: &CbUint, - b: &CbUint, -) -> CbUint { +fn sub_mod_p(a: &CbUint, b: &CbUint) -> CbUint { use crypto_bigint::CheckedSub; let p_nz = NonZero::new(SECP256K1_P_UINT).expect("p is nonzero"); if a.checked_sub(b).is_some().into() { @@ -848,8 +837,15 @@ mod tests { assert_eq!(count_max_degree::(), 7); let degrees = count_constraint_degrees::(); // Spot-checks: at least one deg-7 (Y addend constraint); 3 init deg-2. - assert!(degrees.iter().any(|&d| d == 7), "expected at least one deg-7"); - assert_eq!(degrees.iter().filter(|&&d| d == 2).count(), 3, "init = 3 deg-2"); + assert!( + degrees.iter().any(|&d| d == 7), + "expected at least one deg-7" + ); + assert_eq!( + degrees.iter().filter(|&&d| d == 2).count(), + 3, + "init = 3 deg-2" + ); } /// Witness gen produces a trace where every constraint vanishes @@ -858,8 +854,10 @@ mod tests { fn witness_satisfies_constraints_mod_p() { let num_vars = 9; let mut r = rng(); - let trace = > as GenerateRandomTrace<32>>:: - generate_random_trace(num_vars, &mut r); + let trace = + > as GenerateRandomTrace<32>>::generate_random_trace( + num_vars, &mut r, + ); let n_rows = 1 << num_vars; assert_eq!(trace.int.len(), cols::NUM_INT); @@ -909,17 +907,41 @@ mod tests { // Output: down.X = next R = expected.next_x. if t + 1 < n_rows { - assert_eq!(read_uint(cols::W_X, t + 1), expected.next_x, "next X at {t}"); - assert_eq!(read_uint(cols::W_Y, t + 1), expected.next_y, "next Y at {t}"); - assert_eq!(read_uint(cols::W_Z, t + 1), expected.next_z, "next Z at {t}"); + assert_eq!( + read_uint(cols::W_X, t + 1), + expected.next_x, + "next X at {t}" + ); + assert_eq!( + read_uint(cols::W_Y, t + 1), + expected.next_y, + "next Y at {t}" + ); + assert_eq!( + read_uint(cols::W_Z, t + 1), + expected.next_z, + "next Z at {t}" + ); } } // Init boundary: row 0's R = PA_R_INIT. if init { - assert_eq!(read_uint(cols::W_X, t), read_uint(cols::PA_R_INIT_X, t), "init X at {t}"); - assert_eq!(read_uint(cols::W_Y, t), read_uint(cols::PA_R_INIT_Y, t), "init Y at {t}"); - assert_eq!(read_uint(cols::W_Z, t), read_uint(cols::PA_R_INIT_Z, t), "init Z at {t}"); + assert_eq!( + read_uint(cols::W_X, t), + read_uint(cols::PA_R_INIT_X, t), + "init X at {t}" + ); + assert_eq!( + read_uint(cols::W_Y, t), + read_uint(cols::PA_R_INIT_Y, t), + "init Y at {t}" + ); + assert_eq!( + read_uint(cols::W_Z, t), + read_uint(cols::PA_R_INIT_Z, t), + "init Z at {t}" + ); } // Final-row check: no in-circuit constraint after dropping diff --git a/test-uair/src/ecdsa_addition.rs b/test-uair/src/ecdsa_addition.rs index e0ab3c7e..4a930fa0 100644 --- a/test-uair/src/ecdsa_addition.rs +++ b/test-uair/src/ecdsa_addition.rs @@ -54,8 +54,7 @@ use rand::RngCore; use zinc_poly::{mle::DenseMultilinearExtension, univariate::dense::DensePolynomial}; use zinc_uair::{ ConstraintBuilder, PublicColumnLayout, TotalColumnLayout, TraceRow, Uair, UairSignature, - UairTrace, - ideal::ImpossibleIdeal, + UairTrace, ideal::ImpossibleIdeal, }; use crate::GenerateRandomTrace; @@ -311,10 +310,7 @@ fn rand_fp(rng: &mut Rng) -> CbUint { raw.rem_vartime(&p_nz) } -fn mul_mod_p( - a: &CbUint, - b: &CbUint, -) -> CbUint { +fn mul_mod_p(a: &CbUint, b: &CbUint) -> CbUint { let wide: CbUint<{ EC_FP_INT_LIMBS * 2 }> = a.widening_mul(b).into(); let p_wide: CbUint<{ EC_FP_INT_LIMBS * 2 }> = SECP256K1_P_UINT.resize(); let p_wide_nz = NonZero::new(p_wide).expect("p is nonzero"); @@ -339,10 +335,7 @@ fn p_geq(a: &CbUint) -> bool { a.checked_sub(&SECP256K1_P_UINT).is_some().into() } -fn sub_mod_p( - a: &CbUint, - b: &CbUint, -) -> CbUint { +fn sub_mod_p(a: &CbUint, b: &CbUint) -> CbUint { let p_nz = NonZero::new(SECP256K1_P_UINT).expect("p is nonzero"); if a.checked_sub(b).is_some().into() { a.wrapping_sub(b).rem_vartime(&p_nz) diff --git a/test-uair/src/ecdsa_affine.rs b/test-uair/src/ecdsa_affine.rs index 745fa4c9..a8d93f1d 100644 --- a/test-uair/src/ecdsa_affine.rs +++ b/test-uair/src/ecdsa_affine.rs @@ -46,8 +46,7 @@ use rand::RngCore; use zinc_poly::{mle::DenseMultilinearExtension, univariate::dense::DensePolynomial}; use zinc_uair::{ ConstraintBuilder, PublicColumnLayout, TotalColumnLayout, TraceRow, Uair, UairSignature, - UairTrace, - ideal::ImpossibleIdeal, + UairTrace, ideal::ImpossibleIdeal, }; use crate::GenerateRandomTrace; @@ -247,10 +246,7 @@ fn rand_nonzero_fp(rng: &mut Rng) -> CbUint, - b: &CbUint, -) -> CbUint { +fn mul_mod_p(a: &CbUint, b: &CbUint) -> CbUint { let wide: CbUint<{ EC_FP_INT_LIMBS * 2 }> = a.widening_mul(b).into(); let p_wide: CbUint<{ EC_FP_INT_LIMBS * 2 }> = SECP256K1_P_UINT.resize(); let p_wide_nz = NonZero::new(p_wide).expect("p is nonzero"); @@ -368,10 +364,18 @@ mod tests { // C1: Z · Z_inv = 1 let one_uint: CbUint = CbUint::ONE; - assert_eq!(mul_mod_p(&z, &z_inv), one_uint, "C1 (Z·Z_inv=1) at row {row}"); + assert_eq!( + mul_mod_p(&z, &z_inv), + one_uint, + "C1 (Z·Z_inv=1) at row {row}" + ); // C2: Z_inv_sq = Z_inv² - assert_eq!(z_inv_sq, mul_mod_p(&z_inv, &z_inv), "C2 (Z_inv²) at row {row}"); + assert_eq!( + z_inv_sq, + mul_mod_p(&z_inv, &z_inv), + "C2 (Z_inv²) at row {row}" + ); // C3: Z_inv_cube = Z_inv · Z_inv_sq assert_eq!( diff --git a/test-uair/src/ecdsa_doubling.rs b/test-uair/src/ecdsa_doubling.rs index b5c94966..5f36e545 100644 --- a/test-uair/src/ecdsa_doubling.rs +++ b/test-uair/src/ecdsa_doubling.rs @@ -66,8 +66,7 @@ use rand::RngCore; use zinc_poly::{mle::DenseMultilinearExtension, univariate::dense::DensePolynomial}; use zinc_uair::{ ConstraintBuilder, PublicColumnLayout, ShiftSpec, TotalColumnLayout, TraceRow, Uair, - UairSignature, UairTrace, - ideal::ImpossibleIdeal, + UairSignature, UairTrace, ideal::ImpossibleIdeal, }; use crate::GenerateRandomTrace; @@ -215,8 +214,7 @@ where let x_sq_x_s = x_sq.clone() * &xs; // X²·X·S = X³·S let twelve_x3s = mbs(&x_sq_x_s, &twelve_scalar).expect("12·X³·S overflow"); let x_sq_xmid = x_sq.clone() * x_mid; - let three_xsq_xmid = - mbs(&x_sq_xmid, &three_scalar).expect("3·X²·X_mid overflow"); + let three_xsq_xmid = mbs(&x_sq_xmid, &three_scalar).expect("3·X²·X_mid overflow"); let s_sq = s_w.clone() * s_w; let eight_s_sq = mbs(&s_sq, &eight_scalar).expect("8·S² overflow"); let c4_inner = y_mid.clone() - &twelve_x3s + &three_xsq_xmid + &eight_s_sq; @@ -326,10 +324,7 @@ fn rand_fp(rng: &mut Rng) -> CbUint { } /// `(a · b) mod p`. -fn mul_mod_p( - a: &CbUint, - b: &CbUint, -) -> CbUint { +fn mul_mod_p(a: &CbUint, b: &CbUint) -> CbUint { let wide: CbUint<{ EC_FP_INT_LIMBS * 2 }> = a.widening_mul(b).into(); let p_wide: CbUint<{ EC_FP_INT_LIMBS * 2 }> = SECP256K1_P_UINT.resize(); let p_wide_nz = NonZero::new(p_wide).expect("p is nonzero"); @@ -356,10 +351,7 @@ fn p_geq(a: &CbUint) -> bool { } /// `(a − b) mod p`, allowing `a < b`. -fn sub_mod_p( - a: &CbUint, - b: &CbUint, -) -> CbUint { +fn sub_mod_p(a: &CbUint, b: &CbUint) -> CbUint { let p_nz = NonZero::new(SECP256K1_P_UINT).expect("p is nonzero"); if a.checked_sub(b).is_some().into() { a.wrapping_sub(b).rem_vartime(&p_nz) diff --git a/test-uair/src/lib.rs b/test-uair/src/lib.rs index a6951178..333889ee 100644 --- a/test-uair/src/lib.rs +++ b/test-uair/src/lib.rs @@ -12,8 +12,8 @@ pub use ecdsa_addition::JacobianAdditionUair; pub use ecdsa_affine::AffineConversionUair; pub use ecdsa_doubling::{EC_FP_INT_LIMBS, EcdsaFpRing, JacobianDoublingUair}; pub use generate_trace::*; -pub use sha256::{Sha256CompressionSliceUair, Sha256Ideal}; pub use sha_ecdsa::ShaEcdsaUair; +pub use sha256::{Sha256CompressionSliceUair, Sha256Ideal}; use crypto_primitives::{ConstSemiring, FixedSemiring, Semiring, boolean::Boolean}; use num_traits::Zero; @@ -1008,10 +1008,7 @@ where { let two_ideal = ideal_from_ref(&DegreeOneIdeal::new(R::from(2))); // V[i] − Rot(7)(W[i]) ≡ 0 mod (X − 2) - b.assert_in_ideal( - up.binary_poly[1].clone() - &down.bit_op[0], - &two_ideal, - ); + b.assert_in_ideal(up.binary_poly[1].clone() - &down.bit_op[0], &two_ideal); } } diff --git a/test-uair/src/sha256.rs b/test-uair/src/sha256.rs index 8c9a5bc6..20c60eba 100644 --- a/test-uair/src/sha256.rs +++ b/test-uair/src/sha256.rs @@ -170,8 +170,7 @@ use rand::RngCore; use zinc_poly::{ mle::DenseMultilinearExtension, univariate::{ - binary::BinaryPoly, dense::DensePolynomial, - dynamic::over_field::DynamicPolynomialF, + binary::BinaryPoly, dense::DensePolynomial, dynamic::over_field::DynamicPolynomialF, }, }; use zinc_uair::{ @@ -367,7 +366,7 @@ pub mod cols { // witness and the verifier no longer needs an out-of-band // public-structure check on them. pub const S_ACTIVE_SCHED: usize = 4; // public: 1 on C7's active range [start, start+48) per compression - pub const S_ACTIVE_UPD: usize = 5; // public: 1 on C8/C9's active range [start, start+64) per compression + pub const S_ACTIVE_UPD: usize = 5; // public: 1 on C8/C9's active range [start, start+64) per compression // (For C12/C13 the active range is the junction window // [start+64, start+68), which is exactly where `S_FEEDFORWARD` is // 1 — no separate selector needed.) @@ -387,11 +386,11 @@ pub mod cols { // - SCHED (C7): k ∈ [start, start + 48) // - UPD (C8/C9): k ∈ [start, start + 64) // - JUNCTION (C12/C13): k ∈ [start + 64, start + 68) - pub const PA_C_C7: usize = 6; // witness: compensator for C7 (sched_anch) - pub const PA_C_C8: usize = 7; // witness: compensator for C8 (upd_anch a) - pub const PA_C_C9: usize = 8; // witness: compensator for C9 (upd_anch e) - pub const PA_C_FF_A: usize = 9; // witness: compensator for C12 (feed-forward a-half) - pub const PA_C_FF_E: usize = 10; // witness: compensator for C13 (feed-forward e-half) + pub const PA_C_C7: usize = 6; // witness: compensator for C7 (sched_anch) + pub const PA_C_C8: usize = 7; // witness: compensator for C8 (upd_anch a) + pub const PA_C_C9: usize = 8; // witness: compensator for C9 (upd_anch e) + pub const PA_C_FF_A: usize = 9; // witness: compensator for C12 (feed-forward a-half) + pub const PA_C_FF_E: usize = 10; // witness: compensator for C13 (feed-forward e-half) /// Total number of int columns. /// @@ -509,11 +508,11 @@ where // `down.bit_op` and the (X^32 − 1) modular lift goes away: // ROT/SHIFTR virtual columns are mod X^32 by construction. let bit_op_specs: Vec = vec![ - BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(25)), // σ_0: ROTR^7 - BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(14)), // σ_0: ROTR^18 - BitOpSpec::new(cols::FLAT_W_W, BitOp::ShiftR(3)), // σ_0: SHR^3 - BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(15)), // σ_1: ROTR^17 - BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(13)), // σ_1: ROTR^19 + BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(25)), // σ_0: ROTR^7 + BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(14)), // σ_0: ROTR^18 + BitOpSpec::new(cols::FLAT_W_W, BitOp::ShiftR(3)), // σ_0: SHR^3 + BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(15)), // σ_1: ROTR^17 + BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(13)), // σ_1: ROTR^19 BitOpSpec::new(cols::FLAT_W_W, BitOp::ShiftR(10)), // σ_1: SHR^10 // Bit-op virtuals over W_MU_PACKED for extracting each // carry from its bit slice. The C7/C8/C9/C12/C13 @@ -521,10 +520,10 @@ where // `2^32 · ShiftR(k_low) − 2^{32+w} · ShiftR(k_low+w)`. // ShiftR(10) is also assert_zero'd by C22 to pin // positions 10..31 to 0. - BitOpSpec::new(cols::FLAT_W_MU_PACKED, BitOp::ShiftR(2)), // skips mu_W - BitOpSpec::new(cols::FLAT_W_MU_PACKED, BitOp::ShiftR(5)), // skips mu_W + mu_a - BitOpSpec::new(cols::FLAT_W_MU_PACKED, BitOp::ShiftR(8)), // skips through mu_e - BitOpSpec::new(cols::FLAT_W_MU_PACKED, BitOp::ShiftR(9)), // keeps mu_ff_e + (high) + BitOpSpec::new(cols::FLAT_W_MU_PACKED, BitOp::ShiftR(2)), // skips mu_W + BitOpSpec::new(cols::FLAT_W_MU_PACKED, BitOp::ShiftR(5)), // skips mu_W + mu_a + BitOpSpec::new(cols::FLAT_W_MU_PACKED, BitOp::ShiftR(8)), // skips through mu_e + BitOpSpec::new(cols::FLAT_W_MU_PACKED, BitOp::ShiftR(9)), // keeps mu_ff_e + (high) BitOpSpec::new(cols::FLAT_W_MU_PACKED, BitOp::ShiftR(10)), // (high) — must be 0 ]; // Witness-relative col indices (post-public) for virtual specs. @@ -748,10 +747,10 @@ where let down_w_shr3 = &down.bit_op[4]; // σ_0: SHR^3 let down_w_shr10 = &down.bit_op[5]; // σ_1: SHR^10 // Bit-extraction shifts on W_MU_PACKED. - let down_w_mu_packed_shr2 = &down.bit_op[6]; // skips mu_W - let down_w_mu_packed_shr5 = &down.bit_op[7]; // skips mu_W + mu_a - let down_w_mu_packed_shr8 = &down.bit_op[8]; // skips through mu_e - let down_w_mu_packed_shr9 = &down.bit_op[9]; // keeps only mu_ff_e + (high) + let down_w_mu_packed_shr2 = &down.bit_op[6]; // skips mu_W + let down_w_mu_packed_shr5 = &down.bit_op[7]; // skips mu_W + mu_a + let down_w_mu_packed_shr8 = &down.bit_op[8]; // skips through mu_e + let down_w_mu_packed_shr9 = &down.bit_op[9]; // keeps only mu_ff_e + (high) let down_w_mu_packed_shr10 = &down.bit_op[10]; // (high) — must be 0 // Ideals. @@ -777,8 +776,7 @@ where let const_2_to_35 = const_scalar::(pow_two::(35)); // mu_X contributions (each evaluates to `2^32 · mu_X` at X=2). - let mu_w_contrib = mbs(w_mu_packed, &const_2_to_32) - .expect("2^32 · w_mu_packed overflow") + let mu_w_contrib = mbs(w_mu_packed, &const_2_to_32).expect("2^32 · w_mu_packed overflow") - &mbs(down_w_mu_packed_shr2, &const_2_to_34) .expect("2^34 · ShiftR(2)(w_mu_packed) overflow"); let mu_a_contrib = mbs(down_w_mu_packed_shr2, &const_2_to_32) @@ -807,7 +805,8 @@ where // Constraint 1: Sigma_0 rotation, Q[X]-lifted. // (a_hat · rho_sig0 − sig0_hat − 2 · ov_sig0) ∈ (X^32 − 1) b.assert_in_ideal( - mbs(w_a, &rho_sig0).expect("a · rho_sig0 overflow") - w_sig0 + mbs(w_a, &rho_sig0).expect("a · rho_sig0 overflow") + - w_sig0 - &mbs(pa_ov_sig0, &two_scalar).expect("2 · ov_sig0 overflow"), &ideal_rot_xw1, ); @@ -815,7 +814,8 @@ where // Constraint 2: Sigma_1 rotation, Q[X]-lifted. // (e_hat · rho_sig1 − sig1_hat − 2 · ov_sig1) ∈ (X^32 − 1) b.assert_in_ideal( - mbs(w_e, &rho_sig1).expect("e · rho_sig1 overflow") - w_sig1 + mbs(w_e, &rho_sig1).expect("e · rho_sig1 overflow") + - w_sig1 - &mbs(pa_ov_sig1, &two_scalar).expect("2 · ov_sig1 overflow"), &ideal_rot_xw1, ); @@ -828,14 +828,16 @@ where // coefficient sum {0..3} → bit XOR. // ROT^25(W) + ROT^14(W) + SHIFTR^3(W) − lsig0 − 2 · pa_ov_lsig0 == 0 b.assert_zero( - down_w_rot25.clone() + down_w_rot14 + down_w_shr3 - w_lsig0 + down_w_rot25.clone() + down_w_rot14 + down_w_shr3 + - w_lsig0 - &mbs(pa_ov_lsig0, &two_scalar).expect("2 · ov_lsig0 overflow"), ); // Constraint 6 (was σ_1 (X^32 − 1) ideal-lift): σ_1 analogue of C4. // ROT^15(W) + ROT^13(W) + SHIFTR^10(W) − lsig1 − 2 · pa_ov_lsig1 == 0 b.assert_zero( - down_w_rot15.clone() + down_w_rot13 + down_w_shr10 - w_lsig1 + down_w_rot15.clone() + down_w_rot13 + down_w_shr10 + - w_lsig1 - &mbs(pa_ov_lsig1, &two_scalar).expect("2 · ov_lsig1 overflow"), ); @@ -857,12 +859,9 @@ where // mu_W is now read from the up row (chained-comp re-anchoring // stores each carry at its constraint's anchor row, not at // spec-row t). `mu_w_contrib` evaluates to `2^32 · mu_W` at X=2. - let sched_inner = down_w_w_sh16.clone() - - w_big_w - - down_w_lsig0_sh1 - - down_w_w_sh9 - - down_w_lsig1_sh14 - + &mu_w_contrib; + let sched_inner = + down_w_w_sh16.clone() - w_big_w - down_w_lsig0_sh1 - down_w_w_sh9 - down_w_lsig1_sh14 + + &mu_w_contrib; b.assert_in_ideal(sched_inner + pa_c_c7, &ideal_rot_x2); // Constraint 8: Register-update for `a`, anchored at k = t − 3. @@ -891,7 +890,7 @@ where - down_w_w_sh3 // W[t] - down_w_sig0_sh3 // Sigma_0(a[t]) - down_w_maj_sh3 // Maj[t] - + &mu_a_contrib; // = 2^32 · mu_a (bits 2-4 of W_MU_PACKED) + + &mu_a_contrib; // = 2^32 · mu_a (bits 2-4 of W_MU_PACKED) b.assert_in_ideal(a_update_inner + pa_c_c8, &ideal_rot_x2); // Constraint 9: Register-update for `e`, anchored at k = t − 3. @@ -908,7 +907,7 @@ where - down_w_u_neg_e_g_sh3 - down_pa_k_sh3 - down_w_w_sh3 - + &mu_e_contrib; // = 2^32 · mu_e (bits 5-7 of W_MU_PACKED) + + &mu_e_contrib; // = 2^32 · mu_e (bits 5-7 of W_MU_PACKED) b.assert_in_ideal(e_update_inner + pa_c_c9, &ideal_rot_x2); // C13–C15 (B_1/B_2/B_3 materialization identities) are gone: @@ -958,18 +957,12 @@ where // Keeps C12 at degree 1 in the trace MLEs (preserving MLE-first // eligibility) and avoids a multiplicative selector that would // push the effective max degree to 2. - let ff_a_inner = down_w_a_sh4.clone() - - w_a - - pa_a - + &mu_ff_a_contrib; // = 2^32 · mu_ff_a (bit 8 of W_MU_PACKED) + let ff_a_inner = down_w_a_sh4.clone() - w_a - pa_a + &mu_ff_a_contrib; // = 2^32 · mu_ff_a (bit 8 of W_MU_PACKED) b.assert_in_ideal(ff_a_inner + pa_c_ff_a, &ideal_rot_x2); // Constraint 13 (feed-forward, e-family). Mirrors C12 on the // e-half via `pa_c_ff_e`. mu_ff_e from bit 9 of W_MU_PACKED. - let ff_e_inner = down_w_e_sh4.clone() - - w_e - - pa_e - + &mu_ff_e_contrib; + let ff_e_inner = down_w_e_sh4.clone() - w_e - pa_e + &mu_ff_e_contrib; b.assert_in_ideal(ff_e_inner + pa_c_ff_e, &ideal_rot_x2); // Constraint 16: message init (Table 9 row 77). For each @@ -1104,17 +1097,14 @@ fn pow_two(k: u32) -> R { /// so the C8/C9 read at anchor `k = start + j` (which references /// `down.pa_K^↓3 = pa_K[k+3]`) lands on the right round constant. pub const K_CANONICAL: [u32; 64] = [ - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, - 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, - 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, - 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, - 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, - 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, - 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, - 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, - 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, ]; #[inline] @@ -1168,12 +1158,7 @@ fn maj(x: u32, y: u32, z: u32) -> u32 { /// Per the module doc, for the rho patterns we use (3 or 2 nonzero terms) /// each per-position quotient fits in `{0, 1}`; the returned word is /// therefore a valid 32-bit bit-polynomial. -fn rotation_overflow( - input_bits: u32, - rho_positions: &[usize], - s0_bits: u32, - out_bits: u32, -) -> u32 { +fn rotation_overflow(input_bits: u32, rho_positions: &[usize], s0_bits: u32, out_bits: u32) -> u32 { // Compute the Z[X] product coefficients of `input · rho`. let mut prod = [0u32; 64]; for i in 0..32 { @@ -1452,8 +1437,14 @@ where h_e_next[j] = sum_e as u32; let carry_a = (sum_a >> 32) as u32; let carry_e = (sum_e >> 32) as u32; - debug_assert!(carry_a <= 1, "feed-forward a-carry out of {{0,1}}: {carry_a}"); - debug_assert!(carry_e <= 1, "feed-forward e-carry out of {{0,1}}: {carry_e}"); + debug_assert!( + carry_a <= 1, + "feed-forward a-carry out of {{0,1}}: {carry_a}" + ); + debug_assert!( + carry_e <= 1, + "feed-forward e-carry out of {{0,1}}: {carry_e}" + ); pa_a_vals[start + 64 + j] = prior_a; pa_e_vals[start + 64 + j] = prior_e; @@ -1489,10 +1480,22 @@ where .map(|t| if t >= 1 { e_vals[t] & e_vals[t - 1] } else { 0 }) .collect(); let u_neg_e_g_vals: Vec = (0..n) - .map(|t| if t >= 2 { (!e_vals[t]) & e_vals[t - 2] } else { 0 }) + .map(|t| { + if t >= 2 { + (!e_vals[t]) & e_vals[t - 2] + } else { + 0 + } + }) .collect(); let maj_vals: Vec = (0..n) - .map(|t| if t >= 2 { maj(a_vals[t], a_vals[t - 1], a_vals[t - 2]) } else { 0 }) + .map(|t| { + if t >= 2 { + maj(a_vals[t], a_vals[t - 1], a_vals[t - 2]) + } else { + 0 + } + }) .collect(); // ===== Tail compensators for the Ch (63) / Maj (64) virtual residuals ===== @@ -1579,9 +1582,9 @@ where v.iter().copied().map(BinaryPoly::<32>::from).collect() }; - let to_bin_mle = |col: Vec>| -> DenseMultilinearExtension< - BinaryPoly<32>, - > { col.into_iter().collect() }; + let to_bin_mle = |col: Vec>| -> DenseMultilinearExtension> { + col.into_iter().collect() + }; // Layout: 8 public bin_poly cols (PA_A, PA_E, PA_OV_SIG0, // PA_OV_SIG1, PA_OV_LSIG0, PA_OV_LSIG1, PA_R_CH2_COMP, @@ -1686,9 +1689,8 @@ where // prover, so `pa_c_cᵢ[k] = 0` automatically. On inactive rows the // compensator absorbs whatever `innerᵢ(2)` happens to be. let two_to_32: R = R::from(0x10000u32) * &R::from(0x10000u32); - let load = |arr: &[u32], idx: usize| -> R { - if idx < n { R::from(arr[idx]) } else { R::ZERO } - }; + let load = + |arr: &[u32], idx: usize| -> R { if idx < n { R::from(arr[idx]) } else { R::ZERO } }; // C7: inner(2) = w_W[k+16] − w_W[k] − lsig0[k+1] − w_W[k+9] // − lsig1[k+14] + 2^32 · mu_W[k+16] @@ -1728,14 +1730,7 @@ where // formerly stored at k+3; now at row k). let mu_a_k = load(&mu_a_vals, k); let two32_mu = two_to_32.clone() * &mu_a_k; - w_e_k - + &sig1_k3 - + &u_ef_k3 - + &u_neg_e_g_k3 - + &k_k3 - + &w_k3 - + &sig0_k3 - + &maj_k3 + w_e_k + &sig1_k3 + &u_ef_k3 + &u_neg_e_g_k3 + &k_k3 + &w_k3 + &sig0_k3 + &maj_k3 - &two32_mu - &w_a_k4 }) @@ -1757,13 +1752,7 @@ where // mu_e stored at C9-anchor row k (analogous to mu_a). let mu_e_k = load(&mu_e_vals, k); let two32_mu = two_to_32.clone() * &mu_e_k; - w_a_k - + &w_e_k - + &sig1_k3 - + &u_ef_k3 - + &u_neg_e_g_k3 - + &k_k3 - + &w_k3 + w_a_k + &w_e_k + &sig1_k3 + &u_ef_k3 + &u_neg_e_g_k3 + &k_k3 + &w_k3 - &two32_mu - &w_e_k4 }) @@ -1798,9 +1787,8 @@ where }) .collect(); - let to_int_mle = |col: Vec| -> DenseMultilinearExtension { - col.into_iter().collect() - }; + let to_int_mle = + |col: Vec| -> DenseMultilinearExtension { col.into_iter().collect() }; // Layout: public int prefix (selectors + K + active-range // selectors) followed by witness int suffix (the five linear- // constraint compensators). Order matches cols::S_INIT_PREFIX.. @@ -1863,8 +1851,8 @@ mod tests { fn k_canonical_matches_sha256_empty_string_digest() { // SHA-256 H_0 (FIPS 180-4 §5.3.3). let h_in: [u32; 8] = [ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, - 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, + 0x5be0cd19, ]; // Empty-string padded block: single 0x80 byte then 63 zero bytes. let mut m = [0u32; 16]; @@ -1890,9 +1878,13 @@ mod tests { .wrapping_add(K_CANONICAL[t]) .wrapping_add(w[t]); let t2 = big_sigma0(a).wrapping_add(maj(a, b, c)); - h = g; g = f; f = e; + h = g; + g = f; + f = e; e = d.wrapping_add(t1); - d = c; c = b; b = a; + d = c; + c = b; + b = a; a = t1.wrapping_add(t2); } @@ -1909,9 +1901,12 @@ mod tests { ]; let expected: [u32; 8] = [ - 0xe3b0c442, 0x98fc1c14, 0x9afbf4c8, 0x996fb924, - 0x27ae41e4, 0x649b934c, 0xa495991b, 0x7852b855, + 0xe3b0c442, 0x98fc1c14, 0x9afbf4c8, 0x996fb924, 0x27ae41e4, 0x649b934c, 0xa495991b, + 0x7852b855, ]; - assert_eq!(h_out, expected, "SHA-256(\"\") digest mismatch — K table or round logic drift"); + assert_eq!( + h_out, expected, + "SHA-256(\"\") digest mismatch — K table or round logic drift" + ); } } diff --git a/test-uair/src/sha_ecdsa.rs b/test-uair/src/sha_ecdsa.rs index a0a33d8c..441b4f49 100644 --- a/test-uair/src/sha_ecdsa.rs +++ b/test-uair/src/sha_ecdsa.rs @@ -67,10 +67,7 @@ use core::marker::PhantomData; use crypto_primitives::ConstSemiring; use rand::RngCore; -use zinc_poly::{ - mle::DenseMultilinearExtension, - univariate::dense::DensePolynomial, -}; +use zinc_poly::{mle::DenseMultilinearExtension, univariate::dense::DensePolynomial}; use zinc_uair::{ BitOp, BitOpSpec, ConstraintBuilder, LookupColumnSpec, PublicColumnLayout, PublicStructureError, ShiftSpec, ShiftedBitSliceSpec, TotalColumnLayout, TraceRow, Uair, @@ -281,11 +278,11 @@ where // for the full mapping (`Rot(c)` ≡ `ROTR^{32-c}` ≡ multiplication // by `X^c mod (X^32 − 1)`). All six specs target FLAT_W_W. let bit_op_specs: Vec = vec![ - BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(25)), // σ_0: ROTR^7 - BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(14)), // σ_0: ROTR^18 - BitOpSpec::new(cols::FLAT_W_W, BitOp::ShiftR(3)), // σ_0: SHR^3 - BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(15)), // σ_1: ROTR^17 - BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(13)), // σ_1: ROTR^19 + BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(25)), // σ_0: ROTR^7 + BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(14)), // σ_0: ROTR^18 + BitOpSpec::new(cols::FLAT_W_W, BitOp::ShiftR(3)), // σ_0: SHR^3 + BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(15)), // σ_1: ROTR^17 + BitOpSpec::new(cols::FLAT_W_W, BitOp::Rot(13)), // σ_1: ROTR^19 BitOpSpec::new(cols::FLAT_W_W, BitOp::ShiftR(10)), // σ_1: SHR^10 // Bit-op virtuals over W_MU_PACKED for extracting the 5 // chained-comp carries. See sha256.rs cols doc. @@ -534,8 +531,7 @@ where let const_2_to_34 = const_scalar::(pow_two::(34)); let const_2_to_35 = const_scalar::(pow_two::(35)); - let mu_w_contrib = mbs(w_mu_packed, &const_2_to_32) - .expect("2^32 · w_mu_packed overflow") + let mu_w_contrib = mbs(w_mu_packed, &const_2_to_32).expect("2^32 · w_mu_packed overflow") - &mbs(down_w_mu_packed_shr2, &const_2_to_34) .expect("2^34 · ShiftR(2)(w_mu_packed) overflow"); let mu_a_contrib = mbs(down_w_mu_packed_shr2, &const_2_to_32) @@ -557,14 +553,16 @@ where // C1: Sigma_0 rotation b.assert_in_ideal( - mbs(w_a, &rho_sig0).expect("a · rho_sig0 overflow") - w_sig0 + mbs(w_a, &rho_sig0).expect("a · rho_sig0 overflow") + - w_sig0 - &mbs(pa_ov_sig0, &two_scalar_sha).expect("2 · ov_sig0 overflow"), &ideal_rot_xw1, ); // C2: Sigma_1 rotation b.assert_in_ideal( - mbs(w_e, &rho_sig1).expect("e · rho_sig1 overflow") - w_sig1 + mbs(w_e, &rho_sig1).expect("e · rho_sig1 overflow") + - w_sig1 - &mbs(pa_ov_sig1, &two_scalar_sha).expect("2 · ov_sig1 overflow"), &ideal_rot_xw1, ); @@ -575,26 +573,25 @@ where // and the C3/C5 right-shift decompositions go away. // ROT^25(W) + ROT^14(W) + SHIFTR^3(W) − lsig0 − 2 · pa_ov_lsig0 == 0 b.assert_zero( - down_w_rot25.clone() + down_w_rot14 + down_w_shr3 - w_lsig0 + down_w_rot25.clone() + down_w_rot14 + down_w_shr3 + - w_lsig0 - &mbs(pa_ov_lsig0, &two_scalar_sha).expect("2 · ov_lsig0 overflow"), ); // C6 (was σ_1 (X^32 − 1) ideal-lift): σ_1 analogue of C4. // ROT^15(W) + ROT^13(W) + SHIFTR^10(W) − lsig1 − 2 · pa_ov_lsig1 == 0 b.assert_zero( - down_w_rot15.clone() + down_w_rot13 + down_w_shr10 - w_lsig1 + down_w_rot15.clone() + down_w_rot13 + down_w_shr10 + - w_lsig1 - &mbs(pa_ov_lsig1, &two_scalar_sha).expect("2 · ov_lsig1 overflow"), ); // C7: Message-schedule modular sum. mu_W from up.w_mu_packed // bits 0-1 via mu_w_contrib (chained-comp re-anchoring stores // each carry at its constraint's anchor row). - let sched_inner = down_w_w_sh16.clone() - - w_big_w - - down_w_lsig0_sh1 - - down_w_w_sh9 - - down_w_lsig1_sh14 - + &mu_w_contrib; + let sched_inner = + down_w_w_sh16.clone() - w_big_w - down_w_lsig0_sh1 - down_w_w_sh9 - down_w_lsig1_sh14 + + &mu_w_contrib; b.assert_in_ideal(sched_inner + pa_c_c7, &ideal_rot_x2); // C8: Register-update for `a`. mu_a from bits 2-4 of W_MU_PACKED. @@ -646,16 +643,10 @@ where // down.w_a^↓4 = w_a[k+4] = H_{i+1}, j-th component (pinned by C10) // up.sha_w_mu_junction_a = carry ∈ {0, 1} // mu_ff_a / mu_ff_e from bits 8 / 9 of W_MU_PACKED. - let ff_a_inner = down_w_a_sh4.clone() - - w_a - - pa_a - + &mu_ff_a_contrib; + let ff_a_inner = down_w_a_sh4.clone() - w_a - pa_a + &mu_ff_a_contrib; b.assert_in_ideal(ff_a_inner + pa_c_ff_a, &ideal_rot_x2); - let ff_e_inner = down_w_e_sh4.clone() - - w_e - - pa_e - + &mu_ff_e_contrib; + let ff_e_inner = down_w_e_sh4.clone() - w_e - pa_e + &mu_ff_e_contrib; b.assert_in_ideal(ff_e_inner + pa_c_ff_e, &ideal_rot_x2); // C16: message init (Table 9 row 77). Pin w_W to public message @@ -739,15 +730,12 @@ where b.assert_zero(e_s_active.clone() * &d3_inner); let x3_y_sq = x_sq.clone() * &x_y_sq; - let twelve_x3_y_sq = - mbs(&x3_y_sq, &twelve_scalar).expect("12·X³·Y² overflow"); + let twelve_x3_y_sq = mbs(&x3_y_sq, &twelve_scalar).expect("12·X³·Y² overflow"); let x_sq_x_pa = x_sq.clone() * e_x_pa; - let three_x2_xpa = - mbs(&x_sq_x_pa, &three_scalar).expect("3·X²·X_pa overflow"); + let three_x2_xpa = mbs(&x_sq_x_pa, &three_scalar).expect("3·X²·X_pa overflow"); let y_pow4 = e_y_sq.clone() * &e_y_sq; let eight_y_pow4 = mbs(&y_pow4, &eight_scalar).expect("8·Y⁴ overflow"); - let d4_inner = - e_y_pa.clone() - &twelve_x3_y_sq + &three_x2_xpa + &eight_y_pow4; + let d4_inner = e_y_pa.clone() - &twelve_x3_y_sq + &three_x2_xpa + &eight_y_pow4; b.assert_zero(e_s_active.clone() * &d4_inner); // === In-circuit affine addend selection === @@ -797,12 +785,10 @@ where // Y: down.Y − Y_pa − S_ADD·(3·D·X_pa·C² + D·C³ − D³ − Y_pa·C³ − Y_pa) = 0 let d_cube = e_d.clone() * &d_sq; let d_x_pa_c_sq = e_d.clone() * &e_x_pa_c_sq; - let three_d_x_pa_c_sq = - mbs(&d_x_pa_c_sq, &three_scalar).expect("3·D·X_pa·C² overflow"); + let three_d_x_pa_c_sq = mbs(&d_x_pa_c_sq, &three_scalar).expect("3·D·X_pa·C² overflow"); let d_c_cube = e_d.clone() * &e_c_cube; let y_pa_c_cube = e_y_pa.clone() * &e_c_cube; - let y_add_minus_y_pa = - three_d_x_pa_c_sq + &d_c_cube - &d_cube - &y_pa_c_cube - e_y_pa; + let y_add_minus_y_pa = three_d_x_pa_c_sq + &d_c_cube - &d_cube - &y_pa_c_cube - e_y_pa; let s_add_y = e_s_add.clone() * &y_add_minus_y_pa; let o2_inner = down_ecdsa_y_sh1.clone() - e_y_pa - &s_add_y; b.assert_zero(e_s_active.clone() * &o2_inner); @@ -933,10 +919,12 @@ where "ShaEcdsa UAIR needs > {FINAL_ROW} rows; got {n_rows}", ); - let sha_trace = as GenerateRandomTrace<32>>:: - generate_random_trace(num_vars, rng); - let ecdsa_trace = as GenerateRandomTrace<32>>:: - generate_random_trace(num_vars, rng); + let sha_trace = + as GenerateRandomTrace<32>>::generate_random_trace( + num_vars, rng, + ); + let ecdsa_trace = + as GenerateRandomTrace<32>>::generate_random_trace(num_vars, rng); // Sanity: column counts match the standalone UAIRs. assert_eq!(sha_trace.binary_poly.len(), sha256::cols::NUM_BIN); @@ -944,8 +932,7 @@ where assert_eq!(ecdsa_trace.int.len(), ecdsa::cols::NUM_INT); // Binary_poly: copy SHA's directly (ECDSA contributes nothing). - let binary_poly: Vec> = - sha_trace.binary_poly.into_owned(); + let binary_poly: Vec> = sha_trace.binary_poly.into_owned(); // Int section: merge per the layout in `cols`. // @@ -1016,8 +1003,14 @@ mod tests { // some deg-2 (boundaries + chaining + the compensator-zero pins), // some deg-1 (SHA C1, C2, C4, C6 — including the new row-local // σ_0/σ_1 equalities). - assert!(degrees.iter().any(|&d| d == 7), "expected deg-7 from ECDSA C-A2"); - assert!(degrees.iter().filter(|&&d| d == 2).count() >= 3, "expected ≥3 deg-2"); + assert!( + degrees.iter().any(|&d| d == 7), + "expected deg-7 from ECDSA C-A2" + ); + assert!( + degrees.iter().filter(|&&d| d == 2).count() >= 3, + "expected ≥3 deg-2" + ); } /// The merged trace builder produces a trace with the right column @@ -1027,8 +1020,10 @@ mod tests { fn merged_trace_shape() { let num_vars = 9; let mut r = rng(); - let trace = > as GenerateRandomTrace<32>>:: - generate_random_trace(num_vars, &mut r); + let trace = + > as GenerateRandomTrace<32>>::generate_random_trace( + num_vars, &mut r, + ); assert_eq!(trace.binary_poly.len(), cols::NUM_BIN); assert_eq!(trace.int.len(), cols::NUM_INT); diff --git a/uair/src/lib.rs b/uair/src/lib.rs index bdf6e6fb..dfefac82 100644 --- a/uair/src/lib.rs +++ b/uair/src/lib.rs @@ -526,9 +526,10 @@ impl UairSignature { spec.witness_col_idx, ); let flat_col = spec.witness_col_idx + num_pub_bin; - let matched = self.shifts.iter().any(|s| { - s.source_col() == flat_col && s.shift_amount() == spec.shift_amount - }); + let matched = self + .shifts + .iter() + .any(|s| s.source_col() == flat_col && s.shift_amount() == spec.shift_amount); assert!( matched, "ShiftedBitSliceSpec(col {}, shift {}) has no matching ShiftSpec", @@ -556,9 +557,7 @@ impl UairSignature { let mut bin_down_idx = 0usize; for s in &self.shifts { if s.source_col() < num_total_bin { - if s.source_col() == flat_col - && s.shift_amount() == spec.shift_amount - { + if s.source_col() == flat_col && s.shift_amount() == spec.shift_amount { return bin_down_idx; } bin_down_idx += 1; @@ -661,10 +660,7 @@ impl UairSignature { /// binary_poly cols. Per-bit closing overrides on the verifier side /// bind each bit's MLE eval to the spec residual. #[must_use] - pub fn with_virtual_binary_poly_cols( - mut self, - cols: Vec, - ) -> Self { + pub fn with_virtual_binary_poly_cols(mut self, cols: Vec) -> Self { let num_wit_bin = self.witness_cols.num_binary_poly_cols(); let num_pub_bin = self.public_cols.num_binary_poly_cols(); let num_shifted = self.shifted_bit_slice_specs.len(); @@ -1002,8 +998,5 @@ pub enum PublicStructureError { /// expected closed-form value (e.g. the tail-corrector boundary /// formula). #[error("public column '{column}' at row {row} has wrong value")] - WrongValue { - column: &'static str, - row: usize, - }, + WrongValue { column: &'static str, row: usize }, } diff --git a/zip-plus/Cargo.toml b/zip-plus/Cargo.toml index e8949a65..817aa31a 100644 --- a/zip-plus/Cargo.toml +++ b/zip-plus/Cargo.toml @@ -19,6 +19,7 @@ zinc-utils = { workspace = true } ark-ff = { version = "0.5.0", default-features = false } ark-ec = { version = "0.5.0", default-features = false } ark-poly = { version = "0.5.0", default-features = false } +ark-serialize = { version = "0.5.0", default-features = false } ark-std = { version = "0.5.0", default-features = false, features = ["std", "getrandom"] } itertools = { workspace = true } @@ -37,6 +38,7 @@ zstd = "0.13" [dev-dependencies] ark-bn254 = { version = "0.5.0", default-features = false, features = ["curve"] } +ark-secp256k1 = { version = "0.5.0", default-features = false } criterion = { workspace = true } proptest = { workspace = true } @@ -59,3 +61,7 @@ harness = false [[bench]] name = "msm_commitment_benches" harness = false + +[[bench]] +name = "hyrax_commit_breakdown" +harness = false diff --git a/zip-plus/benches/hyrax_commit_breakdown.rs b/zip-plus/benches/hyrax_commit_breakdown.rs new file mode 100644 index 00000000..f3f32346 --- /dev/null +++ b/zip-plus/benches/hyrax_commit_breakdown.rs @@ -0,0 +1,250 @@ +use ark_bn254::G1Affine; +use ark_ec::{AffineRepr, CurveGroup, PrimeGroup}; +use ark_ff::Zero as ArkZero; +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use crypto_primitives::crypto_bigint_monty::MontyField; +use std::hint::black_box; +use zinc_poly::{mle::DenseMultilinearExtension, univariate::binary::BinaryPoly}; +use zip_plus::pcs::{ + generic::PCS, + hyrax::{BinaryLanes, HyraxCommitmentKey, HyraxPCS}, + msm_commitment::{BoolSubsetMsm, MsmCommitmentEngine, MsmCommitmentKey, RowMsmStrategy}, +}; + +type F = MontyField<4>; + +fn scalar(value: usize) -> C::ScalarField { + C::ScalarField::from(u64::try_from(value).expect("benchmark value must fit into u64")) +} + +fn bases_and_h(width: usize) -> (Vec, C::Group) { + let generator = C::Group::generator(); + let bases = (1..=width) + .map(|idx| (generator * scalar::(idx)).into_affine()) + .collect(); + let h = generator * scalar::(width + 1); + (bases, h) +} + +fn msm_ck(width: usize) -> MsmCommitmentKey { + let (bases, h) = bases_and_h::(width); + MsmCommitmentEngine::::setup_from_bases(width, bases, h) + .expect("benchmark setup must be valid") + .0 +} + +fn hyrax_ck(width: usize) -> HyraxCommitmentKey { + let (bases, h) = bases_and_h::(width); + HyraxPCS::::setup_from_bases(width, bases, h) + .expect("benchmark setup must be valid") + .0 +} + +fn bool_row(width: usize) -> Vec { + (0..width) + .map(|idx| idx % 3 == 0 || idx % 11 == 1) + .collect() +} + +fn bool_values(num_lanes: usize, width: usize) -> Vec> { + (0..num_lanes) + .map(|lane| { + (0..width) + .map(|idx| (idx + lane) % 3 == 0 || (idx * 7 + lane) % 19 == 2) + .collect() + }) + .collect() +} + +fn bit_mask(bits: &[bool]) -> usize { + bits.iter().enumerate().fold( + 0usize, + |mask, (idx, bit)| { + if *bit { mask | (1usize << idx) } else { mask } + }, + ) +} + +fn subset_tables(bases: &[C], window_bits: usize) -> Vec> { + bases + .chunks(window_bits) + .map(|window| { + let table_len = 1usize << window.len(); + let mut table = vec![C::Group::zero(); table_len]; + for mask in 1..table_len { + let bit = mask.trailing_zeros() as usize; + let previous = mask & !(1usize << bit); + table[mask] = table[previous] + window[bit]; + } + table + }) + .collect() +} + +fn precomputed_bool_row( + tables: &[Vec], + values: &[bool], + window_bits: usize, +) -> C::Group { + let mut acc = C::Group::zero(); + for (window_idx, bits) in values.chunks(window_bits).enumerate() { + acc += tables[window_idx][bit_mask(bits)]; + } + acc +} + +fn binary_polys( + batch_size: usize, + num_vars: usize, +) -> Vec>> { + let n = 1usize << num_vars; + (0..batch_size) + .map(|poly_idx| { + let evals = (0..n) + .map(|row_idx| { + let mut value = (row_idx as u32).wrapping_mul(0x9e37_79b9); + value ^= (poly_idx as u32).wrapping_mul(0x85eb_ca6b); + value = value.rotate_left( + u32::try_from((row_idx + poly_idx) % 32).expect("rotation must fit"), + ); + value ^= value >> 16; + BinaryPoly::<32>::from(value) + }) + .collect(); + DenseMultilinearExtension::from_evaluations_vec(num_vars, evals, BinaryPoly::zero()) + }) + .collect() +} + +fn bench_curve( + c: &mut Criterion, + curve_name: &str, + batch_size: usize, + width: usize, + num_vars: usize, +) { + let mut group = c.benchmark_group("hyrax_commit_breakdown"); + let lanes = batch_size * 32; + let row = bool_row(width); + let lane_rows = bool_values(lanes, width); + let (bases, h) = bases_and_h::(width); + let tables = subset_tables::(&bases, 6); + let precomputed_blinds = (0..lanes) + .map(|idx| scalar::(idx + 17)) + .collect::>(); + let msm_ck = msm_ck::(width); + let blind_one = MsmCommitmentEngine::::blind(&msm_ck, width); + let hyrax_ck = hyrax_ck::(width); + let polys = binary_polys(batch_size, num_vars); + + group.bench_with_input( + BenchmarkId::new(format!("{curve_name}/bool_row_msm"), width), + &width, + |b, _| { + b.iter(|| { + as RowMsmStrategy>::msm_row( + black_box(&msm_ck), + black_box(&row), + ) + .expect("row MSM must succeed") + }); + }, + ); + + group.bench_with_input( + BenchmarkId::new(format!("{curve_name}/precomputed_bool_row_msm"), width), + &width, + |b, _| { + b.iter(|| precomputed_bool_row::(black_box(&tables), black_box(&row), 6)); + }, + ); + + group.bench_with_input( + BenchmarkId::new(format!("{curve_name}/commit_one_bool_lane"), width), + &width, + |b, _| { + b.iter(|| { + MsmCommitmentEngine::::commit_with::>( + black_box(&msm_ck), + black_box(&row), + black_box(&blind_one), + ) + .expect("one-lane commitment must succeed") + }); + }, + ); + + group.bench_with_input( + BenchmarkId::new( + format!("{curve_name}/precomputed_commit_352_bool_lanes"), + width, + ), + &width, + |b, _| { + b.iter(|| { + let mut acc = Vec::with_capacity(lane_rows.len()); + for (values, blind) in lane_rows.iter().zip(precomputed_blinds.iter()) { + let mut commitment = + precomputed_bool_row::(black_box(&tables), black_box(values), 6); + commitment += h * blind; + acc.push(commitment); + } + acc + }); + }, + ); + + group.bench_with_input( + BenchmarkId::new(format!("{curve_name}/commit_352_bool_lanes"), width), + &width, + |b, _| { + b.iter(|| { + let mut acc = Vec::with_capacity(lane_rows.len()); + for values in &lane_rows { + let blind = MsmCommitmentEngine::::blind(&msm_ck, values.len()); + let commitment = + MsmCommitmentEngine::::commit_with::>( + black_box(&msm_ck), + black_box(values), + black_box(&blind), + ) + .expect("lane commitment must succeed"); + acc.push(commitment); + } + acc + }); + }, + ); + + group.bench_with_input( + BenchmarkId::new(format!("{curve_name}/hyrax_binary_commit_batch11"), width), + &width, + |b, _| { + b.iter(|| { + as PCS, 32>>::commit( + black_box(&hyrax_ck), + black_box(&polys), + ) + .expect("Hyrax binary commit must succeed") + }); + }, + ); + + group.finish(); +} + +fn hyrax_commit_breakdown(c: &mut Criterion) { + let width = 512; + let num_vars = 9; + let batch_size = 11; + + bench_curve::(c, "bn254", batch_size, width, num_vars); + bench_curve::(c, "secp256k1", batch_size, width, num_vars); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = hyrax_commit_breakdown +} +criterion_main!(benches); diff --git a/zip-plus/benches/msm_commitment_benches.rs b/zip-plus/benches/msm_commitment_benches.rs index d17e9db9..a980f60e 100644 --- a/zip-plus/benches/msm_commitment_benches.rs +++ b/zip-plus/benches/msm_commitment_benches.rs @@ -1,24 +1,24 @@ -use ark_bn254::{Fr, G1Affine, G1Projective}; -use ark_ec::{CurveGroup, PrimeGroup}; +use ark_bn254::G1Affine; +use ark_ec::{AffineRepr, CurveGroup, PrimeGroup}; use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; use std::hint::black_box; use zip_plus::pcs::msm_commitment::{ - BoolSubsetMsm, MsmCommitmentEngine, ScalarPippengerMsm, U8BucketMsm, + BoolSubsetMsm, MsmCommitmentEngine, MsmCommitmentKey, ScalarPippengerMsm, U8BucketMsm, }; -fn fr(value: usize) -> Fr { - Fr::from(u64::try_from(value).expect("benchmark value must fit into u64")) +fn scalar(value: usize) -> C::ScalarField { + C::ScalarField::from(u64::try_from(value).expect("benchmark value must fit into u64")) } -fn setup(width: usize, n: usize) -> zip_plus::pcs::msm_commitment::MsmCommitmentKey { - let generator = G1Projective::generator(); +fn setup(width: usize, n: usize) -> MsmCommitmentKey { + let generator = C::Group::generator(); let bases = (1..=width) - .map(|idx| (generator * fr(idx)).into_affine()) + .map(|idx| (generator * scalar::(idx)).into_affine()) .collect(); - let h = generator * fr(width + 1); - let (ck, _) = MsmCommitmentEngine::::setup_from_bases(width, bases, h) + let h = generator * scalar::(width + 1); + let (ck, _) = MsmCommitmentEngine::::setup_from_bases(width, bases, h) .expect("benchmark setup must be valid"); - let _blind = MsmCommitmentEngine::::blind(&ck, n); + let _blind = MsmCommitmentEngine::::blind(&ck, n); ck } @@ -35,60 +35,74 @@ fn u8_values(n: usize, modulus: u8) -> Vec { .collect() } -fn scalar_values(values: &[u8]) -> Vec { +fn scalar_values(values: &[u8]) -> Vec { values .iter() - .map(|value| Fr::from(u64::from(*value))) + .map(|value| C::ScalarField::from(u64::from(*value))) .collect() } -fn msm_commitment_benches(c: &mut Criterion) { - let width = 64; - let n = width * 1024; - let ck = setup(width, n); - let blind = MsmCommitmentEngine::::blind(&ck, n); +fn bench_curve( + group: &mut criterion::BenchmarkGroup, + curve_name: &str, + width: usize, + n: usize, +) { + let ck = setup::(width, n); + let blind = MsmCommitmentEngine::::blind(&ck, n); let bools = bool_values(n); let u8_small = u8_values(n, 32); let u8_full = u8_values(n, 255); - let scalars = scalar_values(&u8_full); + let scalars = scalar_values::(&u8_full); - let mut group = c.benchmark_group("msm_commitment"); - group.bench_with_input(BenchmarkId::new("bool_subset", width), &width, |b, _| { - b.iter(|| { - MsmCommitmentEngine::::commit_with::>( - black_box(&ck), - black_box(&bools), - black_box(&blind), - ) - .expect("bool benchmark commit must succeed") - }); - }); - group.bench_with_input(BenchmarkId::new("u8_0_32", width), &width, |b, _| { - b.iter(|| { - MsmCommitmentEngine::::commit_with::( - black_box(&ck), - black_box(&u8_small), - black_box(&blind), - ) - .expect("u8 small benchmark commit must succeed") - }); - }); - group.bench_with_input(BenchmarkId::new("u8_0_255", width), &width, |b, _| { - b.iter(|| { - MsmCommitmentEngine::::commit_with::( - black_box(&ck), - black_box(&u8_full), - black_box(&blind), - ) - .expect("u8 full benchmark commit must succeed") - }); - }); group.bench_with_input( - BenchmarkId::new("scalar_pippenger", width), + BenchmarkId::new(format!("{curve_name}/bool_subset"), width), &width, |b, _| { b.iter(|| { - MsmCommitmentEngine::::commit_with::( + MsmCommitmentEngine::::commit_with::>( + black_box(&ck), + black_box(&bools), + black_box(&blind), + ) + .expect("bool benchmark commit must succeed") + }); + }, + ); + group.bench_with_input( + BenchmarkId::new(format!("{curve_name}/u8_0_32"), width), + &width, + |b, _| { + b.iter(|| { + MsmCommitmentEngine::::commit_with::( + black_box(&ck), + black_box(&u8_small), + black_box(&blind), + ) + .expect("u8 small benchmark commit must succeed") + }); + }, + ); + group.bench_with_input( + BenchmarkId::new(format!("{curve_name}/u8_0_255"), width), + &width, + |b, _| { + b.iter(|| { + MsmCommitmentEngine::::commit_with::( + black_box(&ck), + black_box(&u8_full), + black_box(&blind), + ) + .expect("u8 full benchmark commit must succeed") + }); + }, + ); + group.bench_with_input( + BenchmarkId::new(format!("{curve_name}/scalar_pippenger"), width), + &width, + |b, _| { + b.iter(|| { + MsmCommitmentEngine::::commit_with::( black_box(&ck), black_box(&scalars), black_box(&blind), @@ -97,6 +111,15 @@ fn msm_commitment_benches(c: &mut Criterion) { }); }, ); +} + +fn msm_commitment_benches(c: &mut Criterion) { + let width = 64; + let n = width * 1024; + + let mut group = c.benchmark_group("msm_commitment"); + bench_curve::(&mut group, "bn254", width, n); + bench_curve::(&mut group, "secp256k1", width, n); group.finish(); } diff --git a/zip-plus/src/code/iprs.rs b/zip-plus/src/code/iprs.rs index 0ed13ab4..1b2b9107 100644 --- a/zip-plus/src/code/iprs.rs +++ b/zip-plus/src/code/iprs.rs @@ -51,8 +51,7 @@ where let target_base_len = 1 << MAX_BASE_COLS_LOG2; // We want depth to be at least 1. - let base_depth = - 1.max(((1.max(row_len / target_base_len)).ilog2() as usize).div_ceil(3)); + let base_depth = 1.max(((1.max(row_len / target_base_len)).ilog2() as usize).div_ceil(3)); let extra = if REP >= 16 { 2 } else if REP >= 8 { diff --git a/zip-plus/src/merkle.rs b/zip-plus/src/merkle.rs index aee86c6c..13ed7448 100644 --- a/zip-plus/src/merkle.rs +++ b/zip-plus/src/merkle.rs @@ -98,11 +98,7 @@ impl MerkleTree { /// from all three groups in fixed order (0, 1, 2). Used by /// [`crate::pcs::multi_zip::MultiZip3`] to commit two or three /// heterogeneous Zip+ instances under a single tree. - pub fn new_combined_3( - rows0: &[&[S0]], - rows1: &[&[S1]], - rows2: &[&[S2]], - ) -> Self + pub fn new_combined_3(rows0: &[&[S0]], rows1: &[&[S1]], rows2: &[&[S2]]) -> Self where S0: ConstTranscribable + Send + Sync, S1: ConstTranscribable + Send + Sync, diff --git a/zip-plus/src/pcs.rs b/zip-plus/src/pcs.rs index 1d95af67..24c85f0a 100644 --- a/zip-plus/src/pcs.rs +++ b/zip-plus/src/pcs.rs @@ -5,6 +5,8 @@ mod phase_verify; pub use phase_prove::ZipPlusProveByteBreakdown; pub use phase_verify::{VerifyPreOpen, VerifyPreOpenReads}; pub mod folding; +pub mod generic; +pub mod hyrax; pub mod msm_commitment; pub mod multi_zip; pub mod structs; diff --git a/zip-plus/src/pcs/folding.rs b/zip-plus/src/pcs/folding.rs index 211a1365..620a1e02 100644 --- a/zip-plus/src/pcs/folding.rs +++ b/zip-plus/src/pcs/folding.rs @@ -338,11 +338,8 @@ mod tests { fn split_preserves_reconstruction() { // v[i](X=2) = u[i](2) + 2^16 * w[i](2) let val: u32 = 0xABCD_1234; - let col = DenseMultilinearExtension::from_evaluations_vec( - 0, - vec![bp32(val)], - BinaryPoly::zero(), - ); + let col = + DenseMultilinearExtension::from_evaluations_vec(0, vec![bp32(val)], BinaryPoly::zero()); let split = split_column::<32, 16>(&col); assert_eq!(split.evaluations.len(), 2); diff --git a/zip-plus/src/pcs/generic.rs b/zip-plus/src/pcs/generic.rs new file mode 100644 index 00000000..234cbbdd --- /dev/null +++ b/zip-plus/src/pcs/generic.rs @@ -0,0 +1,177 @@ +use std::{fmt::Debug, marker::PhantomData}; + +use crypto_primitives::{FromPrimitiveWithConfig, FromWithConfig, PrimeField}; +use zinc_poly::{ + mle::DenseMultilinearExtension, univariate::dynamic::over_field::DynamicPolynomialF, +}; +use zinc_transcript::traits::{GenTranscribable, Transcribable, Transcript}; +use zinc_utils::{from_ref::FromRef, mul_by_scalar::MulByScalar}; + +use crate::{ + ZipError, + code::LinearCode, + pcs::structs::{ZipPlus, ZipPlusCommitment, ZipPlusHint, ZipPlusParams, ZipTypes}, + pcs_transcript::{PcsProverTranscript, PcsVerifierTranscript}, +}; + +/// Polynomial commitment scheme interface used by the Zinc+ protocol. +/// +/// `Eval` is the unprojected witness cell type committed by the backend. +pub trait PCS: Clone + Debug + Send + Sync +where + F: PrimeField, + Eval: Clone + Debug + Send + Sync, +{ + type CommitmentKey: Clone + Debug + Send + Sync; + type VerifierKey: Clone + Debug + Send + Sync; + type Commitment: Clone + Debug + Send + Sync; + type ProverData: Clone + Debug + Send + Sync; + + fn precompute_ck(_ck: &Self::CommitmentKey) {} + + fn commit( + ck: &Self::CommitmentKey, + polys: &[DenseMultilinearExtension], + ) -> Result<(Self::ProverData, Self::Commitment), ZipError>; + + fn absorb_commitment(transcript: &mut T, commitment: &Self::Commitment); + + fn commitment_num_bytes(commitment: &Self::Commitment) -> usize; + + fn write_commitment_bytes(commitment: &Self::Commitment, buf: &mut Vec); + + fn batch_size(commitment: &Self::Commitment) -> usize; + + fn prove_open( + transcript: &mut PcsProverTranscript, + ck: &Self::CommitmentKey, + polys: &[DenseMultilinearExtension], + point: &[F], + prover_data: &Self::ProverData, + field_cfg: &F::Config, + ) -> Result<(), ZipError> + where + F::Inner: Transcribable, + F::Modulus: Transcribable; + + fn verify_open( + transcript: &mut PcsVerifierTranscript, + vk: &Self::VerifierKey, + commitment: &Self::Commitment, + point: &[F], + lifted_evals: &[DynamicPolynomialF], + field_cfg: &F::Config, + ) -> Result<(), ZipError> + where + F::Inner: Transcribable, + F::Modulus: Transcribable; +} + +#[derive(Clone, Debug)] +pub struct ZipPlusPCS>(PhantomData<(Zt, Lc)>); + +impl PCS for ZipPlusPCS +where + F: PrimeField + + FromPrimitiveWithConfig + + for<'a> FromWithConfig<&'a Zt::CombR> + + for<'a> FromWithConfig<&'a Zt::Chal> + + for<'a> MulByScalar<&'a F> + + FromRef, + Zt: ZipTypes, + Zt::Eval: Clone + Debug + Send + Sync, + Lc: LinearCode, + F::Modulus: zinc_utils::from_ref::FromRef, +{ + type CommitmentKey = ZipPlusParams; + type VerifierKey = ZipPlusParams; + type Commitment = ZipPlusCommitment; + type ProverData = Option>; + + fn commit( + ck: &Self::CommitmentKey, + polys: &[DenseMultilinearExtension], + ) -> Result<(Self::ProverData, Self::Commitment), ZipError> { + if polys.is_empty() { + return Ok((None, ZipPlusCommitment::default())); + } + let (hint, commitment) = ZipPlus::::commit(ck, polys)?; + Ok((Some(hint), commitment)) + } + + fn absorb_commitment(transcript: &mut T, commitment: &Self::Commitment) { + transcript.absorb_slice(&commitment.root); + } + + fn commitment_num_bytes(commitment: &Self::Commitment) -> usize { + commitment.get_num_bytes() + } + + fn write_commitment_bytes(commitment: &Self::Commitment, buf: &mut Vec) { + let offset = buf.len(); + buf.resize(offset + commitment.get_num_bytes(), 0); + commitment.write_transcription_bytes_exact(&mut buf[offset..]); + } + + fn batch_size(commitment: &Self::Commitment) -> usize { + commitment.batch_size + } + + fn prove_open( + transcript: &mut PcsProverTranscript, + ck: &Self::CommitmentKey, + polys: &[DenseMultilinearExtension], + point: &[F], + prover_data: &Self::ProverData, + field_cfg: &F::Config, + ) -> Result<(), ZipError> + where + F::Inner: Transcribable, + F::Modulus: Transcribable, + { + if let Some(hint) = prover_data { + let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( + transcript, ck, polys, point, hint, field_cfg, + )?; + } + Ok(()) + } + + fn verify_open( + transcript: &mut PcsVerifierTranscript, + vk: &Self::VerifierKey, + commitment: &Self::Commitment, + point: &[F], + lifted_evals: &[DynamicPolynomialF], + field_cfg: &F::Config, + ) -> Result<(), ZipError> + where + F::Inner: Transcribable, + F::Modulus: Transcribable, + { + if commitment.batch_size == 0 { + return Ok(()); + } + + let per_poly_alphas = + ZipPlus::::sample_alphas(&mut transcript.fs_transcript, commitment.batch_size); + let mut eval_f = F::zero_with_cfg(field_cfg); + for (bar_u, alphas) in lifted_evals.iter().zip(per_poly_alphas.iter()) { + for (coeff, alpha) in bar_u.coeffs.iter().zip(alphas.iter()) { + let mut term = F::from_with_cfg(alpha, field_cfg); + term *= coeff; + eval_f += &term; + } + } + + ZipPlus::::verify_with_alphas::( + transcript, + vk, + commitment, + field_cfg, + point, + &eval_f, + &per_poly_alphas, + ) + } +} diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs new file mode 100644 index 00000000..0ecc190a --- /dev/null +++ b/zip-plus/src/pcs/hyrax.rs @@ -0,0 +1,1056 @@ +#![allow(clippy::arithmetic_side_effects)] + +use std::{ + fmt::Debug, + io::{Read, Write}, + marker::PhantomData, +}; + +use ark_ec::{AffineRepr, CurveGroup}; +use ark_ff::{BigInteger, PrimeField as ArkPrimeField, Zero}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress}; +use crypto_primitives::{ + FromWithConfig, IntRing, PrimeField, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, + crypto_bigint_uint::Uint, +}; +use num_integer::Integer; +use zinc_poly::{ + mle::DenseMultilinearExtension, + univariate::{ + binary::BinaryPoly, dense::DensePolynomial, dynamic::over_field::DynamicPolynomialF, + }, +}; +use zinc_transcript::traits::{ConstTranscribable, GenTranscribable, Transcribable, Transcript}; + +use crate::{ + ZipError, + pcs::{ + generic::PCS, + msm_commitment::{ + BoolSubsetMsm, MsmCommitmentEngine, MsmCommitmentKey, MsmError, MsmVerifierKey, + RowMsmStrategy, ScalarPippengerMsm, + }, + }, + pcs_transcript::{PcsProverTranscript, PcsVerifierTranscript}, +}; + +#[derive(Clone, Debug)] +pub struct HyraxPCS(PhantomData<(C, Lanes)>); + +#[derive(Clone, Debug)] +pub struct HyraxCommitmentKey { + pub(crate) num_cols: usize, + pub(crate) bases: Vec, + pub(crate) h: C::Group, +} + +#[derive(Clone, Debug)] +pub struct HyraxVerifierKey { + pub(crate) num_cols: usize, + pub(crate) bases: Vec, + pub(crate) h: C::Group, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HyraxCommitment { + pub(crate) batch_size: usize, + pub(crate) num_lanes: usize, + pub(crate) num_rows: usize, + pub(crate) comm: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HyraxProverData { + pub(crate) batch_size: usize, + pub(crate) num_lanes: usize, + pub(crate) num_rows: usize, + pub(crate) blinds: Vec, +} + +pub trait HyraxFieldBridge: PrimeField { + fn field_to_scalar(value: &Self) -> C::ScalarField; + fn scalar_to_field(value: &C::ScalarField, cfg: &Self::Config) -> Self; +} + +impl HyraxFieldBridge for MontyField +where + C: AffineRepr, +{ + fn field_to_scalar(value: &Self) -> C::ScalarField { + assert_curve_scalar_modulus::(&value.modulus()); + + let canonical = value.retrieve(); + let mut bytes = vec![0u8; as ConstTranscribable>::NUM_BYTES]; + canonical.write_transcription_bytes_exact(&mut bytes); + C::ScalarField::from_le_bytes_mod_order(&bytes) + } + + fn scalar_to_field(value: &C::ScalarField, cfg: &Self::Config) -> Self { + let actual_modulus = Uint::::new(cfg.modulus().get()); + assert_curve_scalar_modulus::(&actual_modulus); + + let scalar_bigint: ::BigInt = value.clone().into(); + let scalar_uint = uint_from_le_bytes::(&scalar_bigint.to_bytes_le()); + MontyField::::from_with_cfg(&scalar_uint, cfg) + } +} + +pub trait HyraxLanes: Clone + Debug + Send + Sync +where + C: AffineRepr, + Eval: Clone + Debug + Send + Sync, +{ + type LaneValue: Copy + Send + Sync; + type Strategy: RowMsmStrategy; + + const NUM_LANES: usize; + + fn lane_value(eval: &Eval, lane: usize) -> Result; + + fn lane_to_scalar(value: Self::LaneValue) -> C::ScalarField; + + fn lifted_eval( + lifted_eval: &DynamicPolynomialF, + lane: usize, + field_cfg: &F::Config, + ) -> Result + where + F: PrimeField; +} + +#[derive(Clone, Debug)] +pub struct BinaryLanes; + +#[derive(Clone, Debug)] +pub struct IntScalarLane; + +#[derive(Clone, Debug)] +pub struct DensePolyScalarLanes; + +impl HyraxLanes, D> for BinaryLanes { + type LaneValue = bool; + type Strategy = BoolSubsetMsm<6>; + + const NUM_LANES: usize = D; + + fn lane_value(eval: &BinaryPoly, lane: usize) -> Result { + eval.iter() + .nth(lane) + .map(|bit| bit.inner()) + .ok_or_else(|| ZipError::InvalidPcsParam(format!("binary lane {lane} out of range"))) + } + + fn lane_to_scalar(value: Self::LaneValue) -> C::ScalarField { + if value { + C::ScalarField::from(1u64) + } else { + C::ScalarField::zero() + } + } + + fn lifted_eval( + lifted_eval: &DynamicPolynomialF, + lane: usize, + field_cfg: &F::Config, + ) -> Result + where + F: PrimeField, + { + Ok(lifted_eval + .coeffs + .get(lane) + .cloned() + .unwrap_or_else(|| F::zero_with_cfg(field_cfg))) + } +} + +impl HyraxLanes, D> + for IntScalarLane +{ + type LaneValue = C::ScalarField; + type Strategy = ScalarPippengerMsm; + + const NUM_LANES: usize = 1; + + fn lane_value(eval: &Int, lane: usize) -> Result { + if lane != 0 { + return Err(ZipError::InvalidPcsParam(format!( + "int lane {lane} out of range" + ))); + } + int_to_scalar::(eval) + } + + fn lane_to_scalar(value: Self::LaneValue) -> C::ScalarField { + value + } + + fn lifted_eval( + lifted_eval: &DynamicPolynomialF, + lane: usize, + field_cfg: &F::Config, + ) -> Result + where + F: PrimeField, + { + if lane != 0 { + return Err(ZipError::InvalidPcsParam(format!( + "lifted int lane {lane} out of range" + ))); + } + Ok(lifted_eval + .coeffs + .first() + .cloned() + .unwrap_or_else(|| F::zero_with_cfg(field_cfg))) + } +} + +impl + HyraxLanes, D>, D> for DensePolyScalarLanes +{ + type LaneValue = C::ScalarField; + type Strategy = ScalarPippengerMsm; + + const NUM_LANES: usize = D; + + fn lane_value( + eval: &DensePolynomial, D>, + lane: usize, + ) -> Result { + eval.coeffs + .get(lane) + .ok_or_else(|| ZipError::InvalidPcsParam(format!("dense lane {lane} out of range"))) + .and_then(int_to_scalar::) + } + + fn lane_to_scalar(value: Self::LaneValue) -> C::ScalarField { + value + } + + fn lifted_eval( + lifted_eval: &DynamicPolynomialF, + lane: usize, + field_cfg: &F::Config, + ) -> Result + where + F: PrimeField, + { + Ok(lifted_eval + .coeffs + .get(lane) + .cloned() + .unwrap_or_else(|| F::zero_with_cfg(field_cfg))) + } +} + +impl HyraxPCS { + pub fn setup_from_bases( + width: usize, + bases: Vec, + h: C::Group, + ) -> Result<(HyraxCommitmentKey, HyraxVerifierKey), ZipError> { + if !width.is_power_of_two() { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax row width must be a power of two, got {width}" + ))); + } + let (_ck, _vk) = msm_keys(width, bases.clone(), h)?; + Ok(( + HyraxCommitmentKey { + num_cols: width, + bases: bases.clone(), + h, + }, + HyraxVerifierKey { + num_cols: width, + bases, + h, + }, + )) + } +} + +impl PCS for HyraxPCS +where + F: HyraxFieldBridge, + C: AffineRepr, + Eval: Clone + Debug + Send + Sync, + Lanes: HyraxLanes, +{ + type CommitmentKey = HyraxCommitmentKey; + type VerifierKey = HyraxVerifierKey; + type Commitment = HyraxCommitment; + type ProverData = HyraxProverData; + + fn precompute_ck(ck: &Self::CommitmentKey) { + if let Ok((msm_ck, _)) = msm_keys(ck.num_cols, ck.bases.clone(), ck.h) { + Lanes::Strategy::precompute_ck(&msm_ck); + } + } + + fn commit( + ck: &Self::CommitmentKey, + polys: &[DenseMultilinearExtension], + ) -> Result<(Self::ProverData, Self::Commitment), ZipError> { + if polys.is_empty() { + return Ok(( + HyraxProverData { + batch_size: 0, + num_lanes: Lanes::NUM_LANES, + num_rows: 0, + blinds: Vec::new(), + }, + HyraxCommitment { + batch_size: 0, + num_lanes: Lanes::NUM_LANES, + num_rows: 0, + comm: Vec::new(), + }, + )); + } + + validate_polys(polys)?; + let n = polys[0].evaluations.len(); + let num_rows = num_rows(n, ck.num_cols)?; + let (msm_ck, _) = msm_keys(ck.num_cols, ck.bases.clone(), ck.h)?; + let mut all_comm = Vec::with_capacity(polys.len() * Lanes::NUM_LANES * num_rows); + let mut all_blinds = Vec::with_capacity(polys.len() * Lanes::NUM_LANES * num_rows); + + for poly in polys { + for lane in 0..Lanes::NUM_LANES { + let values = lane_values::(poly, lane)?; + let blind = MsmCommitmentEngine::::blind(&msm_ck, values.len()); + let commitment = MsmCommitmentEngine::::commit_with::<_, Lanes::Strategy>( + &msm_ck, &values, &blind, + ) + .map_err(msm_err)?; + all_comm.extend(commitment.comm); + all_blinds.extend(blind.blind); + } + } + + Ok(( + HyraxProverData { + batch_size: polys.len(), + num_lanes: Lanes::NUM_LANES, + num_rows, + blinds: all_blinds, + }, + HyraxCommitment { + batch_size: polys.len(), + num_lanes: Lanes::NUM_LANES, + num_rows, + comm: all_comm, + }, + )) + } + + fn absorb_commitment(transcript: &mut T, commitment: &Self::Commitment) { + transcript.absorb_slice(b"hyrax_commitment_begin"); + transcript.absorb_slice(&(commitment.batch_size as u64).to_le_bytes()); + transcript.absorb_slice(&(commitment.num_lanes as u64).to_le_bytes()); + transcript.absorb_slice(&(commitment.num_rows as u64).to_le_bytes()); + for comm in &commitment.comm { + let bytes = group_bytes::(comm).unwrap_or_default(); + transcript.absorb_slice(&bytes); + } + transcript.absorb_slice(b"hyrax_commitment_end"); + } + + fn commitment_num_bytes(commitment: &Self::Commitment) -> usize { + let group_size = C::zero().serialized_size(Compress::Yes); + 3 * core::mem::size_of::() + commitment.comm.len() * group_size + } + + fn write_commitment_bytes(commitment: &Self::Commitment, buf: &mut Vec) { + buf.extend_from_slice(&(commitment.batch_size as u64).to_le_bytes()); + buf.extend_from_slice(&(commitment.num_lanes as u64).to_le_bytes()); + buf.extend_from_slice(&(commitment.num_rows as u64).to_le_bytes()); + for comm in &commitment.comm { + let bytes = group_bytes::(comm).expect("Hyrax commitment must serialize"); + buf.extend_from_slice(&bytes); + } + } + + fn batch_size(commitment: &Self::Commitment) -> usize { + commitment.batch_size + } + + fn prove_open( + transcript: &mut PcsProverTranscript, + ck: &Self::CommitmentKey, + polys: &[DenseMultilinearExtension], + point: &[F], + prover_data: &Self::ProverData, + field_cfg: &F::Config, + ) -> Result<(), ZipError> + where + F::Inner: Transcribable, + F::Modulus: Transcribable, + { + let _ = CHECK_FOR_OVERFLOW; + if polys.is_empty() { + return Ok(()); + } + validate_polys(polys)?; + validate_hyrax_shape::(ck.num_cols, polys, prover_data)?; + + let n = polys[0].evaluations.len(); + if n != (1usize << point.len()) { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax open expected point for {n} evals, got {} variables", + point.len() + ))); + } + + let point_scalar = point.iter().map(F::field_to_scalar).collect::>(); + let row_vars = prover_data.num_rows.ilog2() as usize; + let q0_f = eq_tensor_f::(&point[..row_vars], field_cfg); + let q1_scalar = eq_tensor_scalar::(&point_scalar[row_vars..]); + let alphas = sample_scalars::( + &mut transcript.fs_transcript, + polys.len() * Lanes::NUM_LANES, + ); + + let mut b_scalar = vec![C::ScalarField::zero(); prover_data.num_rows]; + for (poly_idx, poly) in polys.iter().enumerate() { + for lane in 0..Lanes::NUM_LANES { + let alpha = alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; + for (row_idx, row) in poly.evaluations.chunks(ck.num_cols).enumerate() { + let mut row_eval = C::ScalarField::zero(); + for (col_idx, eval) in row.iter().enumerate() { + let value = Lanes::lane_to_scalar(Lanes::lane_value(eval, lane)?); + if let Some(weight) = q1_scalar.get(col_idx) { + row_eval += value * weight; + } + } + b_scalar[row_idx] += alpha * row_eval; + } + } + } + + let b_f = b_scalar + .iter() + .map(|value| F::scalar_to_field(value, field_cfg)) + .collect::>(); + transcript.write_field_elements(&b_f)?; + + let row_coeffs = if prover_data.num_rows == 1 { + vec![C::ScalarField::from(1u64)] + } else { + sample_scalars::(&mut transcript.fs_transcript, prover_data.num_rows) + }; + + let mut combined_row = vec![C::ScalarField::zero(); ck.num_cols]; + let mut rho_star = C::ScalarField::zero(); + for (poly_idx, poly) in polys.iter().enumerate() { + for lane in 0..Lanes::NUM_LANES { + let alpha = alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; + for (row_idx, row) in poly.evaluations.chunks(ck.num_cols).enumerate() { + let coeff = alpha * row_coeffs[row_idx]; + let blind_idx = commitment_index_dynamic( + Lanes::NUM_LANES, + poly_idx, + lane, + row_idx, + prover_data.num_rows, + ); + rho_star += coeff * prover_data.blinds[blind_idx]; + for (col_idx, eval) in row.iter().enumerate() { + let value = Lanes::lane_to_scalar(Lanes::lane_value(eval, lane)?); + combined_row[col_idx] += coeff * value; + } + } + } + } + + write_scalars::(transcript, &combined_row)?; + write_scalar::(transcript, &rho_star)?; + + if q0_f.len() != b_f.len() { + return Err(ZipError::InvalidPcsOpen( + "Hyrax b vector shape mismatch".to_string(), + )); + } + + Ok(()) + } + + fn verify_open( + transcript: &mut PcsVerifierTranscript, + vk: &Self::VerifierKey, + commitment: &Self::Commitment, + point: &[F], + lifted_evals: &[DynamicPolynomialF], + field_cfg: &F::Config, + ) -> Result<(), ZipError> + where + F::Inner: Transcribable, + F::Modulus: Transcribable, + { + let _ = CHECK_FOR_OVERFLOW; + if commitment.batch_size == 0 { + return Ok(()); + } + validate_commitment_shape::(commitment)?; + if lifted_evals.len() != commitment.batch_size { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax verifier expected {} lifted evals, got {}", + commitment.batch_size, + lifted_evals.len() + ))); + } + + let n = 1usize << point.len(); + let expected_rows = num_rows(n, vk.num_cols)?; + if expected_rows != commitment.num_rows { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax verifier expected {expected_rows} rows from point, commitment has {}", + commitment.num_rows + ))); + } + + let row_vars = commitment.num_rows.ilog2() as usize; + let q0_f = eq_tensor_f::(&point[..row_vars], field_cfg); + let point_scalar = point.iter().map(F::field_to_scalar).collect::>(); + let q1_scalar = eq_tensor_scalar::(&point_scalar[row_vars..]); + let alphas = sample_scalars::( + &mut transcript.fs_transcript, + commitment.batch_size * commitment.num_lanes, + ); + + let b_f = transcript.read_field_elements::(commitment.num_rows)?; + if b_f.len() != q0_f.len() { + return Err(ZipError::InvalidPcsOpen( + "Hyrax b vector shape mismatch".to_string(), + )); + } + + let mut expected_eval = F::zero_with_cfg(field_cfg); + for (poly_idx, lifted_eval) in lifted_evals.iter().enumerate() { + for lane in 0..commitment.num_lanes { + let alpha = F::scalar_to_field( + &alphas[alpha_index_dynamic(commitment.num_lanes, poly_idx, lane)], + field_cfg, + ); + let mut term = Lanes::lifted_eval::(lifted_eval, lane, field_cfg)?; + term *= α + expected_eval += &term; + } + } + + let mut b_eval = F::zero_with_cfg(field_cfg); + for (weight, b) in q0_f.iter().zip(b_f.iter()) { + let mut term = weight.clone(); + term *= b; + b_eval += &term; + } + if b_eval != expected_eval { + return Err(ZipError::InvalidPcsOpen( + "Hyrax evaluation consistency failure".to_string(), + )); + } + + let b_scalar = b_f.iter().map(F::field_to_scalar).collect::>(); + let row_coeffs = if commitment.num_rows == 1 { + vec![C::ScalarField::from(1u64)] + } else { + sample_scalars::(&mut transcript.fs_transcript, commitment.num_rows) + }; + + let combined_row = read_scalars::(transcript, vk.num_cols)?; + let rho_star = read_scalar::(transcript)?; + + let mut lhs = C::ScalarField::zero(); + for (value, weight) in combined_row.iter().zip(q1_scalar.iter()) { + lhs += *value * weight; + } + let mut rhs = C::ScalarField::zero(); + for (coeff, b) in row_coeffs.iter().zip(b_scalar.iter()) { + rhs += *coeff * b; + } + if lhs != rhs { + return Err(ZipError::InvalidPcsOpen( + "Hyrax row coherence failure".to_string(), + )); + } + + let mut comm_lc = C::Group::zero(); + for poly_idx in 0..commitment.batch_size { + for lane in 0..commitment.num_lanes { + let alpha = alphas[alpha_index_dynamic(commitment.num_lanes, poly_idx, lane)]; + for (row_idx, row_coeff) in row_coeffs.iter().enumerate() { + let idx = commitment_index_dynamic( + commitment.num_lanes, + poly_idx, + lane, + row_idx, + commitment.num_rows, + ); + comm_lc += commitment.comm[idx] * (alpha * row_coeff); + } + } + } + + let (msm_ck, _) = msm_keys(vk.num_cols, vk.bases.clone(), vk.h)?; + let mut expected = >::msm_row( + &msm_ck, + &combined_row, + ) + .map_err(msm_err)?; + expected += vk.h * rho_star; + + if comm_lc != expected { + return Err(ZipError::InvalidPcsOpen( + "Hyrax commitment opening failure".to_string(), + )); + } + + Ok(()) + } +} + +fn validate_polys(polys: &[DenseMultilinearExtension]) -> Result<(), ZipError> { + if let Some(first) = polys.first() { + for poly in polys { + if poly.num_vars != first.num_vars || poly.evaluations.len() != first.evaluations.len() + { + return Err(ZipError::InvalidPcsParam( + "Hyrax batch polynomial shape mismatch".to_string(), + )); + } + } + } + Ok(()) +} + +fn validate_hyrax_shape( + width: usize, + polys: &[DenseMultilinearExtension], + prover_data: &HyraxProverData, +) -> Result<(), ZipError> +where + C: AffineRepr, + Lanes: HyraxLanes, + Eval: Clone + Debug + Send + Sync, +{ + let n = polys[0].evaluations.len(); + let num_rows = num_rows(n, width)?; + if prover_data.batch_size != polys.len() + || prover_data.num_lanes != Lanes::NUM_LANES + || prover_data.num_rows != num_rows + || prover_data.blinds.len() != polys.len() * Lanes::NUM_LANES * num_rows + { + return Err(ZipError::InvalidPcsParam( + "Hyrax prover data shape mismatch".to_string(), + )); + } + Ok(()) +} + +fn validate_commitment_shape( + commitment: &HyraxCommitment, +) -> Result<(), ZipError> +where + C: AffineRepr, + Lanes: HyraxLanes, + Eval: Clone + Debug + Send + Sync, +{ + if commitment.num_lanes != Lanes::NUM_LANES { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax commitment lane mismatch: expected {}, got {}", + Lanes::NUM_LANES, + commitment.num_lanes + ))); + } + let expected = commitment.batch_size * commitment.num_lanes * commitment.num_rows; + if commitment.comm.len() != expected { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax commitment expected {expected} row commitments, got {}", + commitment.comm.len() + ))); + } + Ok(()) +} + +fn lane_values( + poly: &DenseMultilinearExtension, + lane: usize, +) -> Result, ZipError> +where + C: AffineRepr, + Lanes: HyraxLanes, + Eval: Clone + Debug + Send + Sync, +{ + poly.evaluations + .iter() + .map(|eval| Lanes::lane_value(eval, lane)) + .collect() +} + +fn int_to_scalar( + value: &Int, +) -> Result { + let (abs, is_negative) = if value.is_negative() { + ( + value.checked_abs().ok_or_else(|| { + ZipError::InvalidPcsParam("cannot convert minimum Int to scalar".to_string()) + })?, + true, + ) + } else { + (*value, false) + }; + let mut scalar = unsigned_int_to_scalar::(&abs); + if is_negative && !scalar.is_zero() { + scalar = -scalar; + } + Ok(scalar) +} + +fn unsigned_int_to_scalar(value: &Int) -> C::ScalarField { + let mut bytes = Vec::with_capacity(LIMBS * core::mem::size_of::()); + for word in value.as_uint().as_words() { + bytes.extend_from_slice(&word.to_le_bytes()); + } + C::ScalarField::from_le_bytes_mod_order(&bytes) +} + +fn assert_curve_scalar_modulus(actual: &Uint) +where + C: AffineRepr, +{ + let expected = + uint_from_le_bytes::(&::MODULUS.to_bytes_le()); + assert_eq!( + actual, &expected, + "Hyrax field mismatch: protocol field modulus must equal curve scalar modulus", + ); +} + +fn uint_from_le_bytes(bytes: &[u8]) -> Uint { + let num_bytes = as ConstTranscribable>::NUM_BYTES; + assert!( + bytes.len() <= num_bytes, + "integer encoding does not fit in target Uint", + ); + let mut padded = vec![0u8; num_bytes]; + padded[..bytes.len()].copy_from_slice(bytes); + Uint::::read_transcription_bytes_exact(&padded) +} + +fn msm_keys( + width: usize, + bases: Vec, + h: C::Group, +) -> Result<(MsmCommitmentKey, MsmVerifierKey), ZipError> { + MsmCommitmentEngine::::setup_from_bases(width, bases, h).map_err(msm_err) +} + +fn num_rows(n: usize, width: usize) -> Result { + if width == 0 { + return Err(ZipError::InvalidPcsParam( + "Hyrax row width must be non-zero".to_string(), + )); + } + Ok(::div_ceil(&n, &width)) +} + +fn alpha_index_dynamic(num_lanes: usize, poly_idx: usize, lane: usize) -> usize { + poly_idx * num_lanes + lane +} + +fn commitment_index_dynamic( + num_lanes: usize, + poly_idx: usize, + lane: usize, + row_idx: usize, + num_rows: usize, +) -> usize { + ((poly_idx * num_lanes + lane) * num_rows) + row_idx +} + +fn eq_tensor_f(point: &[F], cfg: &F::Config) -> Vec { + let mut tensor = vec![F::one_with_cfg(cfg)]; + for r in point { + let one_minus = { + let mut value = F::one_with_cfg(cfg); + value -= r; + value + }; + let current = tensor.clone(); + tensor.clear(); + for value in ¤t { + let mut lo = value.clone(); + lo *= &one_minus; + tensor.push(lo); + } + for value in current { + let mut hi = value; + hi *= r; + tensor.push(hi); + } + } + tensor +} + +fn eq_tensor_scalar(point: &[C::ScalarField]) -> Vec { + let mut tensor = vec![C::ScalarField::from(1u64)]; + for r in point { + let one_minus = C::ScalarField::from(1u64) - r; + let current = tensor.clone(); + tensor.clear(); + for value in ¤t { + tensor.push(*value * one_minus); + } + for value in current { + tensor.push(value * r); + } + } + tensor +} + +fn sample_scalars( + transcript: &mut impl Transcript, + n: usize, +) -> Vec { + (0..n) + .map(|_| { + let mut bytes = Vec::with_capacity(64); + for _ in 0..8 { + let word = transcript.get_challenge::(); + bytes.extend_from_slice(&word.to_le_bytes()); + } + C::ScalarField::from_le_bytes_mod_order(&bytes) + }) + .collect() +} + +fn write_scalars( + transcript: &mut PcsProverTranscript, + scalars: &[C::ScalarField], +) -> Result<(), ZipError> { + for scalar in scalars { + write_scalar::(transcript, scalar)?; + } + Ok(()) +} + +fn write_scalar( + transcript: &mut PcsProverTranscript, + scalar: &C::ScalarField, +) -> Result<(), ZipError> { + let bytes = scalar_bytes::(scalar)?; + transcript.fs_transcript.absorb_slice(&bytes); + transcript.stream.write_all(&bytes)?; + Ok(()) +} + +fn read_scalars( + transcript: &mut PcsVerifierTranscript, + n: usize, +) -> Result, ZipError> { + (0..n).map(|_| read_scalar::(transcript)).collect() +} + +fn read_scalar( + transcript: &mut PcsVerifierTranscript, +) -> Result { + let size = C::ScalarField::zero().serialized_size(Compress::Yes); + let mut bytes = vec![0u8; size]; + transcript.stream.read_exact(&mut bytes)?; + transcript.fs_transcript.absorb_slice(&bytes); + C::ScalarField::deserialize_compressed(bytes.as_slice()).map_err(ark_err) +} + +fn scalar_bytes(scalar: &C::ScalarField) -> Result, ZipError> { + let mut bytes = Vec::with_capacity(scalar.serialized_size(Compress::Yes)); + scalar.serialize_compressed(&mut bytes).map_err(ark_err)?; + Ok(bytes) +} + +fn group_bytes(group: &C::Group) -> Result, ZipError> { + let affine = group.into_affine(); + let mut bytes = Vec::with_capacity(affine.serialized_size(Compress::Yes)); + affine.serialize_compressed(&mut bytes).map_err(ark_err)?; + Ok(bytes) +} + +fn msm_err(err: MsmError) -> ZipError { + ZipError::InvalidPcsParam(err.to_string()) +} + +fn ark_err(err: ark_serialize::SerializationError) -> ZipError { + ZipError::Serialization(format!("ark serialization error: {err}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + use ark_ec::PrimeGroup; + use ark_ff::Field as ArkField; + use crypto_primitives::FromWithConfig; + + fn cfg_from_curve() -> as PrimeField>::Config { + let modulus = + uint_from_le_bytes::<4>(&::MODULUS.to_bytes_le()); + as PrimeField>::make_cfg(&modulus) + .expect("curve scalar modulus must be prime") + } + + fn assert_bridge_round_trip() { + let cfg = cfg_from_curve::(); + for value in [0u64, 1, 2, 17, 123, 1 << 20] { + let field = MontyField::<4>::from_with_cfg(value, &cfg); + let scalar = as HyraxFieldBridge>::field_to_scalar(&field); + assert_eq!(scalar, C::ScalarField::from(value)); + + let field_again = + as HyraxFieldBridge>::scalar_to_field(&scalar, &cfg); + assert_eq!(field_again, field); + } + + let large_values = [ + C::ScalarField::from(2u64).inverse().unwrap(), + -C::ScalarField::from(1u64), + C::ScalarField::from_le_bytes_mod_order(&[0xA5; 64]), + ]; + for scalar in large_values { + let field = as HyraxFieldBridge>::scalar_to_field(&scalar, &cfg); + let scalar_again = as HyraxFieldBridge>::field_to_scalar(&field); + assert_eq!(scalar_again, scalar); + } + } + + #[test] + fn bridge_round_trips_bn254_scalar_field() { + assert_bridge_round_trip::(); + } + + #[test] + fn bridge_round_trips_secp256k1_scalar_field() { + assert_bridge_round_trip::(); + } + + #[test] + #[should_panic(expected = "Hyrax field mismatch")] + fn bridge_rejects_mismatched_field_config() { + let bn_cfg = cfg_from_curve::(); + let bn_field = MontyField::<4>::from_with_cfg(1u64, &bn_cfg); + let _ = + as HyraxFieldBridge>::field_to_scalar(&bn_field); + } + + #[test] + fn binary_hyrax_open_verify_round_trip() { + type C = ark_bn254::G1Affine; + type F = MontyField<4>; + const D: usize = 32; + + fn bp(bits: u32) -> BinaryPoly { + BinaryPoly::::from(bits) + } + + let cfg = cfg_from_curve::(); + let width = 512; + let generator = ::Group::generator(); + let bases = (1..=width) + .map(|idx| (generator * ::ScalarField::from(idx as u64)).into_affine()) + .collect(); + let h = generator * ::ScalarField::from((width + 1) as u64); + let (ck, vk) = HyraxPCS::::setup_from_bases(width, bases, h).unwrap(); + + let evals0 = (0..width) + .map(|idx| bp((idx as u32).wrapping_mul(0x9E37_79B1))) + .collect::>(); + let evals1 = (0..width) + .map(|idx| bp(!((idx as u32).wrapping_mul(0x85EB_CA6B)))) + .collect::>(); + let polys = vec![ + DenseMultilinearExtension::from_evaluations_vec(9, evals0, bp(0)), + DenseMultilinearExtension::from_evaluations_vec(9, evals1, bp(0)), + ]; + let (prover_data, commitment) = + as PCS, D>>::commit(&ck, &polys).unwrap(); + + let point = [ + [0x11u8; 64], + [0x22u8; 64], + [0x33u8; 64], + [0x44u8; 64], + [0x55u8; 64], + [0x66u8; 64], + [0x77u8; 64], + [0x88u8; 64], + [0xA5u8; 64], + ] + .iter() + .map(|bytes| { + let scalar = ::ScalarField::from_le_bytes_mod_order(bytes); + >::scalar_to_field(&scalar, &cfg) + }) + .collect::>(); + let eq = eq_tensor_f::(&point, &cfg); + let lifted_evals = polys + .iter() + .map(|poly| { + let mut coeffs = vec![F::zero_with_cfg(&cfg); D]; + for (weight, eval) in eq.iter().zip(poly.evaluations.iter()) { + for (lane, bit) in eval.iter().enumerate() { + if bit.inner() { + coeffs[lane] += weight; + } + } + } + DynamicPolynomialF::new_trimmed(coeffs) + }) + .collect::>(); + + let mut prover_transcript = PcsProverTranscript { + fs_transcript: Default::default(), + stream: Default::default(), + }; + as PCS, D>>::absorb_commitment( + &mut prover_transcript.fs_transcript, + &commitment, + ); + let mut transcription_buf = vec![0u8; ::Inner::NUM_BYTES]; + for lifted_eval in &lifted_evals { + prover_transcript + .fs_transcript + .absorb_random_field_slice(&lifted_eval.coeffs, &mut transcription_buf); + } + as PCS, D>>::prove_open::( + &mut prover_transcript, + &ck, + &polys, + &point, + &prover_data, + &cfg, + ) + .unwrap(); + + let mut verifier_transcript = prover_transcript.into_verification_transcript(); + as PCS, D>>::absorb_commitment( + &mut verifier_transcript.fs_transcript, + &commitment, + ); + let mut transcription_buf = vec![0u8; ::Inner::NUM_BYTES]; + for lifted_eval in &lifted_evals { + verifier_transcript + .fs_transcript + .absorb_random_field_slice(&lifted_eval.coeffs, &mut transcription_buf); + } + as PCS, D>>::verify_open::( + &mut verifier_transcript, + &vk, + &commitment, + &point, + &lifted_evals, + &cfg, + ) + .unwrap(); + } +} diff --git a/zip-plus/src/pcs/multi_zip.rs b/zip-plus/src/pcs/multi_zip.rs index 18d39b34..da3e2d5b 100644 --- a/zip-plus/src/pcs/multi_zip.rs +++ b/zip-plus/src/pcs/multi_zip.rs @@ -50,9 +50,7 @@ pub struct MultiZipHint3 { /// Three-instance Zip+ wrapper sharing a single Merkle tree across three /// independent Zip+ commitments. -pub struct MultiZip3( - PhantomData<(Zt0, Zt1, Zt2, Lc0, Lc1, Lc2)>, -) +pub struct MultiZip3(PhantomData<(Zt0, Zt1, Zt2, Lc0, Lc1, Lc2)>) where Zt0: ZipTypes, Zt1: ZipTypes, @@ -94,8 +92,8 @@ where ), ZipError, > { - let nonempty = (!polys0.is_empty()) as u8 + (!polys1.is_empty()) as u8 - + (!polys2.is_empty()) as u8; + let nonempty = + (!polys0.is_empty()) as u8 + (!polys1.is_empty()) as u8 + (!polys2.is_empty()) as u8; assert!( nonempty >= 2, "MultiZip3::commit requires at least two non-empty batches \ @@ -239,23 +237,26 @@ where let eval0 = if polys0.is_empty() { None } else { - Some(ZipPlus::::prove_pre_open_f::( - transcript, pp0, polys0, point, field_cfg, - )?) + Some(ZipPlus::::prove_pre_open_f::< + F, + CHECK_FOR_OVERFLOW, + >(transcript, pp0, polys0, point, field_cfg)?) }; let eval1 = if polys1.is_empty() { None } else { - Some(ZipPlus::::prove_pre_open_f::( - transcript, pp1, polys1, point, field_cfg, - )?) + Some(ZipPlus::::prove_pre_open_f::< + F, + CHECK_FOR_OVERFLOW, + >(transcript, pp1, polys1, point, field_cfg)?) }; let eval2 = if polys2.is_empty() { None } else { - Some(ZipPlus::::prove_pre_open_f::( - transcript, pp2, polys2, point, field_cfg, - )?) + Some(ZipPlus::::prove_pre_open_f::< + F, + CHECK_FOR_OVERFLOW, + >(transcript, pp2, polys2, point, field_cfg)?) }; for _ in 0..Zt0::NUM_COLUMN_OPENINGS { @@ -353,9 +354,10 @@ where let eval0 = if polys0.is_empty() { None } else { - Some(ZipPlus::::prove_pre_open_f::( - transcript, pp0, polys0, point, field_cfg, - )?) + Some(ZipPlus::::prove_pre_open_f::< + F, + CHECK_FOR_OVERFLOW, + >(transcript, pp0, polys0, point, field_cfg)?) }; let p1 = pos(transcript); bd0.combined_row.extend(snapshot(transcript, p0, p1)); @@ -363,9 +365,10 @@ where let eval1 = if polys1.is_empty() { None } else { - Some(ZipPlus::::prove_pre_open_f::( - transcript, pp1, polys1, point, field_cfg, - )?) + Some(ZipPlus::::prove_pre_open_f::< + F, + CHECK_FOR_OVERFLOW, + >(transcript, pp1, polys1, point, field_cfg)?) }; let p2 = pos(transcript); bd1.combined_row.extend(snapshot(transcript, p1, p2)); @@ -373,9 +376,10 @@ where let eval2 = if polys2.is_empty() { None } else { - Some(ZipPlus::::prove_pre_open_f::( - transcript, pp2, polys2, point, field_cfg, - )?) + Some(ZipPlus::::prove_pre_open_f::< + F, + CHECK_FOR_OVERFLOW, + >(transcript, pp2, polys2, point, field_cfg)?) }; let p3 = pos(transcript); bd2.combined_row.extend(snapshot(transcript, p2, p3)); diff --git a/zip-plus/src/pcs/phase_prove.rs b/zip-plus/src/pcs/phase_prove.rs index 30f5232c..c950cadc 100644 --- a/zip-plus/src/pcs/phase_prove.rs +++ b/zip-plus/src/pcs/phase_prove.rs @@ -478,7 +478,9 @@ impl> ZipPlus { let coeffs = if pp.num_rows == 1 { vec![Zt::Chal::ONE] } else { - transcript.fs_transcript.get_challenges::(num_rows) + transcript + .fs_transcript + .get_challenges::(num_rows) }; let combined_row: Vec = { diff --git a/zip-plus/src/utils.rs b/zip-plus/src/utils.rs index cef1268a..3ae73c5e 100644 --- a/zip-plus/src/utils.rs +++ b/zip-plus/src/utils.rs @@ -35,9 +35,7 @@ pub const ZSTD_LEVEL: i32 = 3; /// compression step (excluding serialization). Useful for callers /// that want to attribute the compression cost to a step in a /// timings breakdown. -pub fn serialize_and_compress( - value: &T, -) -> (Vec, std::time::Duration) { +pub fn serialize_and_compress(value: &T) -> (Vec, std::time::Duration) { let mut buf = vec![0_u8; value.get_num_bytes()]; value.write_transcription_bytes_exact(&mut buf); let t0 = std::time::Instant::now(); @@ -75,7 +73,11 @@ pub fn eprint_bytes_size(label: impl std::fmt::Display, raw: &[u8]) { print_size!(format_args!("zstd-{ZSTD_LEVEL}"), compressed.len()); let decompressed = zstd::decode_all(&compressed[..]).expect("zstd decompression failed"); - assert_eq!(decompressed.len(), raw.len(), "zstd round-trip size mismatch"); + assert_eq!( + decompressed.len(), + raw.len(), + "zstd round-trip size mismatch" + ); } /// Prints a per-part proof size breakdown (raw + zstd-compressed) to stderr. From 92bde620f9e1eaab4ae926389359eff604a0d5da Mon Sep 17 00:00:00 2001 From: John Wu Date: Thu, 4 Jun 2026 21:05:04 -0700 Subject: [PATCH 06/49] perf: use DMR-aware field inner products --- piop/src/combined_poly_resolver.rs | 8 +- piop/src/multipoint_eval.rs | 45 ++++--- piop/src/projections.rs | 14 +-- poly/src/univariate/dynamic/over_field.rs | 11 +- protocol/src/prover.rs | 12 +- protocol/src/verifier.rs | 21 +++- utils/src/delayed_reduction.rs | 69 +++++++++- utils/src/inner_product.rs | 146 +++++++++++++++++++++- zip-plus/src/pcs/multi_zip.rs | 5 +- zip-plus/src/pcs/phase_prove.rs | 17 ++- zip-plus/src/pcs/phase_verify.rs | 13 +- 11 files changed, 309 insertions(+), 52 deletions(-) diff --git a/piop/src/combined_poly_resolver.rs b/piop/src/combined_poly_resolver.rs index 1e5e4c88..018b4c5e 100644 --- a/piop/src/combined_poly_resolver.rs +++ b/piop/src/combined_poly_resolver.rs @@ -38,8 +38,8 @@ use zinc_poly::{ use zinc_transcript::traits::{ConstTranscribable, Transcript}; use zinc_uair::{BitOp, TraceRow, Uair, ideal::ImpossibleIdeal}; use zinc_utils::{ - UNCHECKED, add, cfg_iter, from_ref::FromRef, inner_product::InnerProduct, - inner_transparent_field::InnerTransparentField, powers, + UNCHECKED, add, cfg_iter, delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, + inner_product::InnerProduct, inner_transparent_field::InnerTransparentField, powers, }; /// Materialize the bit-op virtual MLEs given by `bit_op_specs`. @@ -143,7 +143,9 @@ where pub struct CombinedPolyResolver(PhantomData); -impl CombinedPolyResolver { +impl + CombinedPolyResolver +{ /// Build the CPR sumcheck group for use in the multi-degree sumcheck. /// /// Pre-sumcheck half of the CPR prover. Samples the folding challenge `α`, diff --git a/piop/src/multipoint_eval.rs b/piop/src/multipoint_eval.rs index 610224d1..4afe22fb 100644 --- a/piop/src/multipoint_eval.rs +++ b/piop/src/multipoint_eval.rs @@ -47,7 +47,12 @@ use zinc_transcript::{ traits::{ConstTranscribable, Transcript}, }; use zinc_uair::ShiftSpec; -use zinc_utils::{cfg_into_iter, inner_transparent_field::InnerTransparentField}; +use zinc_utils::{ + UNCHECKED, cfg_into_iter, + delayed_reduction::DelayedFieldProductSum, + inner_product::{FieldFieldInnerProduct, InnerProduct}, + inner_transparent_field::InnerTransparentField, +}; // // Data structures @@ -104,7 +109,12 @@ pub struct MultipointEval(PhantomData); impl MultipointEval where - F: InnerTransparentField + FromPrimitiveWithConfig + Send + Sync + 'static, + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, F::Inner: ConstTranscribable + Zero + Default + Send + Sync, F::Modulus: ConstTranscribable, { @@ -315,13 +325,12 @@ where let zero = F::zero_with_cfg(field_cfg); - let batched_up: F = subclaim - .gammas - .iter() - .zip(open_evals.iter()) - .fold(zero.clone(), |acc, (gamma, eval)| { - acc + gamma.clone() * eval - }); + let batched_up: F = FieldFieldInnerProduct::inner_product::( + &subclaim.gammas, + open_evals, + zero.clone(), + ) + .expect("inner product cannot fail here"); // open_evals[j] = trace_col_j(r_0) for all committed (up) columns. // Shifted columns reuse the same opening: the shift is captured by @@ -351,22 +360,20 @@ where /// `expected_sum = \sum_j \gamma_j * up_eval_j + \sum_k \alpha_k * /// down_eval_k` -fn compute_expected_sum( +fn compute_expected_sum( up_evals: &[F], down_evals: &[F], gammas: &[F], alphas: &[F], zero: F, ) -> F { - let up_sum = gammas - .iter() - .zip(up_evals.iter()) - .fold(zero, |acc, (gamma, up)| acc + gamma.clone() * up); - - alphas - .iter() - .zip(down_evals.iter()) - .fold(up_sum, |acc, (alpha, down)| acc + alpha.clone() * down) + let up_sum = FieldFieldInnerProduct::inner_product::(gammas, up_evals, zero.clone()) + .expect("inner product cannot fail here"); + + let down_sum = FieldFieldInnerProduct::inner_product::(alphas, down_evals, zero) + .expect("inner product cannot fail here"); + + up_sum + down_sum } // diff --git a/piop/src/projections.rs b/piop/src/projections.rs index be5af276..97e6ce40 100644 --- a/piop/src/projections.rs +++ b/piop/src/projections.rs @@ -15,8 +15,9 @@ use zinc_poly::{ }; use zinc_uair::{Uair, UairTrace, collect_scalars::collect_scalars}; use zinc_utils::{ - UNCHECKED, cfg_extend, cfg_into_iter, cfg_iter, cfg_iter_mut, from_ref::FromRef, - inner_product::InnerProduct, powers, projectable_to_field::ProjectableToField, + UNCHECKED, cfg_extend, cfg_into_iter, cfg_iter, cfg_iter_mut, + delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, inner_product::InnerProduct, + powers, projectable_to_field::ProjectableToField, }; /// HashMap specialization used for every `projected_scalars` lookup in the @@ -240,7 +241,7 @@ where /// MLEs (`Vec>`) for sumcheck /// compatibility. Dispatches on the trace layout internally. #[allow(clippy::arithmetic_side_effects)] -pub fn evaluate_trace_to_column_mles( +pub fn evaluate_trace_to_column_mles( trace: &ProjectedTrace, projecting_element: &F, ) -> Vec> { @@ -337,9 +338,8 @@ where { let zero_inner = F::Inner::default(); - let mut result = Vec::with_capacity( - trace.binary_poly.len() + trace.arbitrary_poly.len() + trace.int.len(), - ); + let mut result = + Vec::with_capacity(trace.binary_poly.len() + trace.arbitrary_poly.len() + trace.int.len()); let bin_proj = BinaryPoly::::prepare_projection(projecting_element); cfg_extend!( @@ -408,7 +408,7 @@ pub fn project_scalars( /// Project scalars of a UAIR along F[X] -> F. #[allow(clippy::arithmetic_side_effects)] -pub fn project_scalars_to_field( +pub fn project_scalars_to_field( scalars: ScalarMap>, projecting_element: &F, ) -> Result, (R, F, EvaluationError)> { diff --git a/poly/src/univariate/dynamic/over_field.rs b/poly/src/univariate/dynamic/over_field.rs index 4d21816b..c60a62cc 100644 --- a/poly/src/univariate/dynamic/over_field.rs +++ b/poly/src/univariate/dynamic/over_field.rs @@ -8,7 +8,8 @@ use std::{ use zinc_transcript::traits::{ConstTranscribable, GenTranscribable, Transcribable}; use zinc_utils::{ UNCHECKED, add, - inner_product::{InnerProduct, InnerProductError}, + delayed_reduction::DelayedFieldProductSum, + inner_product::{FieldFieldInnerProduct, InnerProduct, InnerProductError}, mul, }; @@ -128,17 +129,13 @@ impl DynamicPolynomialF { /// Inner product for dynamic polynomials over a prime field. pub struct DynamicPolyFInnerProduct; -impl InnerProduct<[F], F, F> for DynamicPolyFInnerProduct { - #[allow(clippy::arithmetic_side_effects)] +impl InnerProduct<[F], F, F> for DynamicPolyFInnerProduct { fn inner_product( lhs: &[F], rhs: &[F], zero: F, ) -> Result { - Ok(lhs - .iter() - .zip(rhs) - .fold(zero, |acc, (coeff, power)| acc + coeff.clone() * power)) + FieldFieldInnerProduct::inner_product::(lhs, rhs, zero) } } diff --git a/protocol/src/prover.rs b/protocol/src/prover.rs index 9af6b687..1541cc36 100644 --- a/protocol/src/prover.rs +++ b/protocol/src/prover.rs @@ -31,8 +31,9 @@ use zinc_uair::{ degree_counter::count_max_degree, }; use zinc_utils::{ - add, cfg_join, from_ref::FromRef, inner_transparent_field::InnerTransparentField, - mul_by_scalar::MulByScalar, projectable_to_field::ProjectableToField, + add, cfg_join, delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, + inner_transparent_field::InnerTransparentField, mul_by_scalar::MulByScalar, + projectable_to_field::ProjectableToField, }; use zip_plus::{ pcs::{ @@ -256,6 +257,7 @@ macro_rules! impl_with_type_bounds { U: Uair + 'static, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Zt::Int> + for<'b> FromWithConfig<&'b ::CombR> @@ -977,6 +979,7 @@ where ::Eval: ProjectableToField, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Zt::Int> + for<'a> FromWithConfig<&'a ::CombR> @@ -1130,6 +1133,7 @@ where U: Uair + 'static, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a ZtF::Int> + for<'a> FromWithConfig<&'a ::CombR> @@ -1647,6 +1651,7 @@ where U: Uair, D>> + 'static, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Int> + for<'a> FromWithConfig<&'a Int> @@ -1713,6 +1718,7 @@ where U: Uair, D>> + 'static, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Int> + for<'a> FromWithConfig<&'a Int> @@ -1785,6 +1791,7 @@ where U: Uair, D>> + 'static, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Int> + for<'a> FromWithConfig<&'a Int> @@ -1853,6 +1860,7 @@ where U: Uair, D>> + 'static, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Int> + for<'a> FromWithConfig<&'a Int> diff --git a/protocol/src/verifier.rs b/protocol/src/verifier.rs index 365642c5..b9c8577a 100644 --- a/protocol/src/verifier.rs +++ b/protocol/src/verifier.rs @@ -34,7 +34,9 @@ use zinc_uair::{ ideal_collector::IdealOrZero, }; use zinc_utils::{ - add, cfg_join, delayed_reduction::MontgomeryLimbs, from_ref::FromRef, + add, cfg_join, + delayed_reduction::{DelayedFieldProductSum, MontgomeryLimbs}, + from_ref::FromRef, inner_transparent_field::InnerTransparentField, mul_by_scalar::MulByScalar, projectable_to_field::ProjectableToField, }; @@ -417,6 +419,7 @@ impl<'a, Zt, U, F, IdealOverF, const D: usize> VerifierIdealChecked<'a, Zt, U, F where Zt: ZincTypes, F: InnerTransparentField + + DelayedFieldProductSum + for<'b> FromWithConfig<&'b Zt::Chal> + FromRef + Send @@ -464,6 +467,7 @@ where Zt::Int: ProjectableToField, ::Eval: ProjectableToField, F: InnerTransparentField + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Zt::Int> + for<'b> FromWithConfig<&'b ::CombR> @@ -704,7 +708,13 @@ where impl<'a, Zt, F, IdealOverF, const D: usize> VerifierSumchecked<'a, Zt, F, IdealOverF, D> where Zt: ZincTypes, - F: InnerTransparentField + FromPrimitiveWithConfig + FromRef + Send + Sync + 'static, + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + FromRef + + Send + + Sync + + 'static, F::Inner: ConstIntSemiring + ConstTranscribable + Send + Sync + Zero + Default, F::Modulus: ConstTranscribable + FromRef, IdealOverF: Ideal, @@ -758,6 +768,7 @@ where ::Eval: ProjectableToField, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Zt::Int> + for<'b> FromWithConfig<&'b Zt::Chal> @@ -880,6 +891,7 @@ where ::Cw: ProjectableToField, ::Cw: ProjectableToField, F: InnerTransparentField + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Zt::Int> + for<'b> FromWithConfig<&'b ::CombR> @@ -995,6 +1007,7 @@ where ::Cw: ProjectableToField, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'a> FromWithConfig<&'a Zt::Int> + for<'a> FromWithConfig<&'a ::CombR> @@ -1102,6 +1115,7 @@ where U: Uair + 'static, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b ZtF::Int> + for<'b> FromWithConfig<&'b ::CombR> @@ -1677,6 +1691,7 @@ where U: Uair, D>> + 'static, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Int> + for<'b> FromWithConfig<&'b Int> @@ -1752,6 +1767,7 @@ where U: Uair, D>> + 'static, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Int> + for<'b> FromWithConfig<&'b Int> @@ -1828,6 +1844,7 @@ where U: Uair, D>> + 'static, F: InnerTransparentField + MontgomeryLimbs + + DelayedFieldProductSum + FromPrimitiveWithConfig + for<'b> FromWithConfig<&'b Int> + for<'b> FromWithConfig<&'b Int> diff --git a/utils/src/delayed_reduction.rs b/utils/src/delayed_reduction.rs index 3b57dbd8..43cd271b 100644 --- a/utils/src/delayed_reduction.rs +++ b/utils/src/delayed_reduction.rs @@ -5,7 +5,7 @@ //! reduction. The limb routines are adapted from Spartan2's MIT-licensed //! `big_num` helpers. -use crypto_bigint::modular::ConstMontyParams; +use crypto_bigint::modular::{ConstMontyForm, ConstMontyParams, MontyForm}; use crypto_primitives::{ PrimeField, crypto_bigint_const_monty::ConstMontyField, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, @@ -53,6 +53,14 @@ where fn reduce(self, cfg: &F::Config, params: &BarrettReductionParams) -> F; } +/// Field product-sum backend for delayed modular reduction-aware dot products. +pub trait DelayedFieldProductSum: PrimeField + Sized { + /// Compute `zero + sum_i lhs[i] * rhs[i]`. + /// + /// The caller is responsible for enforcing equal slice lengths. + fn delayed_sum_of_products(lhs: &[Self], rhs: &[Self], zero: Self) -> Self; +} + impl DelayedModularReduction for Uint<5> where F: MontgomeryLimbs + Send + Sync, @@ -86,6 +94,65 @@ where } } +impl DelayedFieldProductSum for MontyField { + fn delayed_sum_of_products(lhs: &[Self], rhs: &[Self], zero: Self) -> Self { + if lhs.is_empty() { + return zero; + } + + let leading_zeros = zero.cfg().modulus().as_ref().leading_zeros(); + if !lincomb_has_product_sum_headroom(leading_zeros, lhs.len()) { + return naive_sum_of_products(lhs, rhs, zero); + } + + let lhs_forms: Vec> = + lhs.iter().cloned().map(|value| value.into()).collect(); + let rhs_forms: Vec> = + rhs.iter().cloned().map(|value| value.into()).collect(); + let products: Vec<(&MontyForm, &MontyForm)> = + lhs_forms.iter().zip(&rhs_forms).collect(); + + MontyField::new(MontyForm::lincomb_vartime(&products)) + zero + } +} + +impl DelayedFieldProductSum for ConstMontyField +where + Mod: ConstMontyParams, +{ + fn delayed_sum_of_products(lhs: &[Self], rhs: &[Self], zero: Self) -> Self { + if lhs.is_empty() { + return zero; + } + + let leading_zeros = Mod::PARAMS.modulus().as_ref().leading_zeros(); + if !lincomb_has_product_sum_headroom(leading_zeros, lhs.len()) { + return naive_sum_of_products(lhs, rhs, zero); + } + + let products: Vec<(ConstMontyForm, ConstMontyForm)> = lhs + .iter() + .cloned() + .zip(rhs.iter().cloned()) + .map(|(left, right)| (left.into(), right.into())) + .collect(); + + ConstMontyField::from(ConstMontyForm::lincomb(&products)) + zero + } +} + +#[inline(always)] +fn lincomb_has_product_sum_headroom(leading_zeros: u32, len: usize) -> bool { + len > 1 && leading_zeros > 0 +} + +#[allow(clippy::arithmetic_side_effects)] +fn naive_sum_of_products(lhs: &[F], rhs: &[F], zero: F) -> F { + lhs.iter() + .zip(rhs) + .fold(zero, |acc, (left, right)| acc + left.clone() * right) +} + impl MontgomeryLimbs for ConstMontyField where Mod: ConstMontyParams<4>, diff --git a/utils/src/inner_product.rs b/utils/src/inner_product.rs index 53effe36..d6cfda75 100644 --- a/utils/src/inner_product.rs +++ b/utils/src/inner_product.rs @@ -1,4 +1,6 @@ -use crate::{from_ref::FromRef, mul_by_scalar::MulByScalar}; +use crate::{ + delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, mul_by_scalar::MulByScalar, +}; use crypto_primitives::{FromWithConfig, PrimeField, boolean::Boolean}; use num_traits::CheckedAdd; use thiserror::Error; @@ -86,6 +88,30 @@ impl MBSInnerProduct { } } +/// Field-field inner product backed by a delayed product-sum implementation. +#[derive(Clone, Debug)] +pub struct FieldFieldInnerProduct; + +impl InnerProduct<[F], F, F> for FieldFieldInnerProduct +where + F: DelayedFieldProductSum, +{ + fn inner_product( + lhs: &[F], + rhs: &[F], + zero: F, + ) -> Result { + if lhs.len() != rhs.len() { + return Err(InnerProductError::LengthMismatch { + lhs: lhs.len(), + rhs: rhs.len(), + }); + } + + Ok(F::delayed_sum_of_products(lhs, rhs, zero)) + } +} + /// The inner product for vectors of length 1 (a.k.a. scalars). /// Uses `mul_by_scalar` to multiply the only components of vectors /// to get the result. @@ -154,8 +180,11 @@ impl + CheckedAdd> InnerProduct<[Boolean], Rhs, Ou #[cfg(test)] mod test { use crate::{CHECKED, UNCHECKED}; - use crypto_bigint::{U64, const_monty_params}; - use crypto_primitives::crypto_bigint_const_monty::ConstMontyField; + use crypto_bigint::{U64, U256, const_monty_params}; + use crypto_primitives::{ + FromWithConfig, PrimeField, crypto_bigint_const_monty::ConstMontyField, + crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, + }; use num_traits::ConstZero; use super::*; @@ -198,6 +227,29 @@ mod test { } const_monty_params!(Params, U64, "0000000000000007"); + const_monty_params!( + Params256, + U256, + "00dca94d8a1ecce3b6e8755d8999787d0524d8ca1ea755e7af84fb646fa31f27" + ); + + fn dyn_field_cfg() -> as PrimeField>::Config { + let modulus = Uint::new( + crypto_bigint::Uint::<4>::from_str_radix_vartime( + "00dca94d8a1ecce3b6e8755d8999787d0524d8ca1ea755e7af84fb646fa31f27", + 16, + ) + .expect("valid modulus"), + ); + MontyField::make_cfg(&modulus).expect("valid field config") + } + + #[allow(clippy::arithmetic_side_effects)] + fn naive_field_inner_product(lhs: &[F], rhs: &[F], zero: F) -> F { + lhs.iter() + .zip(rhs) + .fold(zero, |acc, (left, right)| acc + left.clone() * right) + } #[test] fn boolean_unchecked_eq_boolean_checked() { @@ -219,4 +271,92 @@ mod test { BooleanInnerProductAdd::inner_product::(&lhs, &rhs, ConstMontyField::ZERO) ); } + + #[test] + fn field_field_inner_product_monty_matches_naive() { + type F = MontyField<4>; + let cfg = dyn_field_cfg(); + let lhs = [ + F::from_with_cfg(3u64, &cfg), + F::from_with_cfg(5u64, &cfg), + F::from_with_cfg(8u64, &cfg), + F::from_with_cfg(13u64, &cfg), + ]; + let rhs = [ + F::from_with_cfg(21u64, &cfg), + F::from_with_cfg(34u64, &cfg), + F::from_with_cfg(55u64, &cfg), + F::from_with_cfg(89u64, &cfg), + ]; + let zero = F::zero_with_cfg(&cfg); + + let got = + FieldFieldInnerProduct::inner_product::(&lhs, &rhs, zero.clone()).unwrap(); + let expected = naive_field_inner_product(&lhs, &rhs, zero); + + assert_eq!(got, expected); + } + + #[test] + fn field_field_inner_product_const_monty_matches_naive() { + type F = ConstMontyField; + let lhs = [F::from(2u64), F::from(7u64), F::from(19u64), F::from(31u64)]; + let rhs = [ + F::from(43u64), + F::from(59u64), + F::from(61u64), + F::from(71u64), + ]; + + let got = FieldFieldInnerProduct::inner_product::(&lhs, &rhs, F::ZERO).unwrap(); + let expected = naive_field_inner_product(&lhs, &rhs, F::ZERO); + + assert_eq!(got, expected); + } + + #[test] + fn field_field_inner_product_empty_returns_zero() { + type F = ConstMontyField; + let zero = F::from(99u64); + + let got = FieldFieldInnerProduct::inner_product::(&[], &[], zero).unwrap(); + + assert_eq!(got, zero); + } + + #[test] + fn field_field_inner_product_single_term_matches_naive() { + type F = ConstMontyField; + let lhs = [F::from(144u64)]; + let rhs = [F::from(233u64)]; + + let got = FieldFieldInnerProduct::inner_product::(&lhs, &rhs, F::ZERO).unwrap(); + let expected = naive_field_inner_product(&lhs, &rhs, F::ZERO); + + assert_eq!(got, expected); + } + + #[test] + fn field_field_inner_product_nonzero_seed_matches_naive() { + type F = ConstMontyField; + let lhs = [F::from(5u64), F::from(8u64), F::from(13u64)]; + let rhs = [F::from(21u64), F::from(34u64), F::from(55u64)]; + let seed = F::from(99u64); + + let got = FieldFieldInnerProduct::inner_product::(&lhs, &rhs, seed).unwrap(); + let expected = naive_field_inner_product(&lhs, &rhs, seed); + + assert_eq!(got, expected); + } + + #[test] + fn field_field_inner_product_length_mismatch() { + type F = ConstMontyField; + let lhs = [F::from(1u64)]; + + assert_eq!( + FieldFieldInnerProduct::inner_product::(&lhs, &[], F::ZERO), + Err(InnerProductError::LengthMismatch { lhs: 1, rhs: 0 }) + ); + } } diff --git a/zip-plus/src/pcs/multi_zip.rs b/zip-plus/src/pcs/multi_zip.rs index 18d39b34..36c400e0 100644 --- a/zip-plus/src/pcs/multi_zip.rs +++ b/zip-plus/src/pcs/multi_zip.rs @@ -35,7 +35,8 @@ use std::marker::PhantomData; use zinc_poly::mle::DenseMultilinearExtension; use zinc_transcript::traits::Transcribable; use zinc_utils::{ - cfg_into_iter, cfg_iter, cfg_join, from_ref::FromRef, mul_by_scalar::MulByScalar, + cfg_into_iter, cfg_iter, cfg_join, delayed_reduction::DelayedFieldProductSum, + from_ref::FromRef, mul_by_scalar::MulByScalar, }; /// Full prover-side data for a [`MultiZip3`] commitment: per-instance @@ -208,6 +209,7 @@ where ) -> Result<(Option, Option, Option), ZipError> where F: PrimeField + + DelayedFieldProductSum + for<'a> FromWithConfig<&'a Zt0::CombR> + for<'a> FromWithConfig<&'a Zt1::CombR> + for<'a> FromWithConfig<&'a Zt2::CombR> @@ -310,6 +312,7 @@ where ) -> Result<(Option, Option, Option), ZipError> where F: PrimeField + + DelayedFieldProductSum + for<'a> FromWithConfig<&'a Zt0::CombR> + for<'a> FromWithConfig<&'a Zt1::CombR> + for<'a> FromWithConfig<&'a Zt2::CombR> diff --git a/zip-plus/src/pcs/phase_prove.rs b/zip-plus/src/pcs/phase_prove.rs index 30f5232c..1bff475f 100644 --- a/zip-plus/src/pcs/phase_prove.rs +++ b/zip-plus/src/pcs/phase_prove.rs @@ -51,8 +51,9 @@ use zinc_poly::{Polynomial, mle::DenseMultilinearExtension}; use zinc_transcript::traits::{Transcribable, Transcript}; use zinc_utils::{ UNCHECKED, cfg_chunks, cfg_iter, cfg_iter_mut, + delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, - inner_product::{InnerProduct, MBSInnerProduct}, + inner_product::{FieldFieldInnerProduct, InnerProduct, MBSInnerProduct}, mul_by_scalar::MulByScalar, }; @@ -127,6 +128,7 @@ impl> ZipPlus { ) -> Result where F: PrimeField + + DelayedFieldProductSum + for<'a> FromWithConfig<&'a Zt::CombR> + for<'a> FromWithConfig<&'a Zt::Pt> + for<'a> MulByScalar<&'a F> @@ -161,6 +163,7 @@ impl> ZipPlus { ) -> Result where F: PrimeField + + DelayedFieldProductSum + for<'a> FromWithConfig<&'a Zt::CombR> + for<'a> MulByScalar<&'a F> + FromRef, @@ -194,6 +197,7 @@ impl> ZipPlus { ) -> Result where F: PrimeField + + DelayedFieldProductSum + for<'a> FromWithConfig<&'a Zt::CombR> + for<'a> MulByScalar<&'a F> + FromRef, @@ -223,6 +227,7 @@ impl> ZipPlus { ) -> Result where F: PrimeField + + DelayedFieldProductSum + for<'a> FromWithConfig<&'a Zt::CombR> + for<'a> MulByScalar<&'a F> + FromRef, @@ -297,7 +302,7 @@ impl> ZipPlus { } // Compute eval = (inner product in field), in paper // It is safe to use inner_product_unchecked because we're in a field. - let eval = MBSInnerProduct::inner_product::(&q_0, &b, zero_f.clone())?; + let eval = FieldFieldInnerProduct::inner_product::(&q_0, &b, zero_f.clone())?; // Matrix-vector product over the flat poly_comb_r layout: // Each poly is a row-major (num_rows x row_len) matrix, and coeffs is the @@ -377,6 +382,7 @@ impl> ZipPlus { ) -> Result where F: PrimeField + + DelayedFieldProductSum + for<'a> FromWithConfig<&'a Zt::CombR> + for<'a> FromWithConfig<&'a Zt::Chal> + for<'a> FromWithConfig<&'a Zt::Pt> @@ -413,6 +419,7 @@ impl> ZipPlus { ) -> Result where F: PrimeField + + DelayedFieldProductSum + for<'a> FromWithConfig<&'a Zt::CombR> + for<'a> MulByScalar<&'a F> + FromRef, @@ -473,12 +480,14 @@ impl> ZipPlus { }; transcript.write_field_elements(&b)?; - let eval = MBSInnerProduct::inner_product::(&q_0, &b, zero_f.clone())?; + let eval = FieldFieldInnerProduct::inner_product::(&q_0, &b, zero_f.clone())?; let coeffs = if pp.num_rows == 1 { vec![Zt::Chal::ONE] } else { - transcript.fs_transcript.get_challenges::(num_rows) + transcript + .fs_transcript + .get_challenges::(num_rows) }; let combined_row: Vec = { diff --git a/zip-plus/src/pcs/phase_verify.rs b/zip-plus/src/pcs/phase_verify.rs index 73ca3db9..c3cf9ede 100644 --- a/zip-plus/src/pcs/phase_verify.rs +++ b/zip-plus/src/pcs/phase_verify.rs @@ -19,8 +19,9 @@ use zinc_transcript::{ }; use zinc_utils::{ UNCHECKED, add, cfg_into_iter, + delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, - inner_product::{InnerProduct, MBSInnerProduct}, + inner_product::{FieldFieldInnerProduct, InnerProduct, MBSInnerProduct}, mul_by_scalar::MulByScalar, }; @@ -126,6 +127,7 @@ impl> ZipPlus { ) -> Result<(), ZipError> where F: FromPrimitiveWithConfig + + DelayedFieldProductSum + FromRef + for<'a> FromWithConfig<&'a Zt::CombR> + for<'a> FromWithConfig<&'a Zt::Chal> @@ -164,6 +166,7 @@ impl> ZipPlus { ) -> Result<(), ZipError> where F: FromPrimitiveWithConfig + + DelayedFieldProductSum + FromRef + for<'a> FromWithConfig<&'a Zt::CombR> + for<'a> FromWithConfig<&'a Zt::Chal> @@ -192,7 +195,8 @@ impl> ZipPlus { let b: Vec = transcript.read_field_elements(num_rows)?; // Check 1: == eval_f - if MBSInnerProduct::inner_product::(&q_0, &b, zero_f.clone())? != *eval_f { + if FieldFieldInnerProduct::inner_product::(&q_0, &b, zero_f.clone())? != *eval_f + { return Err(ZipError::InvalidPcsOpen( "Evaluation consistency failure".into(), )); @@ -273,6 +277,7 @@ impl> ZipPlus { ) -> Result, ZipError> where F: FromPrimitiveWithConfig + + DelayedFieldProductSum + FromRef + for<'a> FromWithConfig<&'a Zt::CombR> + for<'a> FromWithConfig<&'a Zt::Chal> @@ -348,6 +353,7 @@ impl> ZipPlus { ) -> Result, ZipError> where F: FromPrimitiveWithConfig + + DelayedFieldProductSum + FromRef + for<'a> FromWithConfig<&'a Zt::CombR> + for<'a> FromWithConfig<&'a Zt::Chal> @@ -374,7 +380,8 @@ impl> ZipPlus { let (q_0, q_1) = point_to_tensor(vp.num_rows, point_f, field_cfg)?; let zero_f = F::zero_with_cfg(field_cfg); - if MBSInnerProduct::inner_product::(&q_0, &b, zero_f.clone())? != *eval_f { + if FieldFieldInnerProduct::inner_product::(&q_0, &b, zero_f.clone())? != *eval_f + { return Err(ZipError::InvalidPcsOpen( "Evaluation consistency failure".into(), )); From ee890533bc8e9c176916922978f53f6ea87346f1 Mon Sep 17 00:00:00 2001 From: John Wu Date: Thu, 4 Jun 2026 21:34:35 -0700 Subject: [PATCH 07/49] perf: complete DMR follow-up integrations --- ecc-qx-commitment.md | 19 ++ piop/src/combined_poly_resolver.rs | 234 +++++++++++++++++---- piop/src/combined_poly_resolver/folder.rs | 110 +++++++++- piop/src/lookup/booleanity.rs | 239 +++++++++++++--------- zinc+.pdf | Bin 0 -> 1630176 bytes 5 files changed, 455 insertions(+), 147 deletions(-) create mode 100644 ecc-qx-commitment.md create mode 100644 zinc+.pdf diff --git a/ecc-qx-commitment.md b/ecc-qx-commitment.md new file mode 100644 index 00000000..14ee89dd --- /dev/null +++ b/ecc-qx-commitment.md @@ -0,0 +1,19 @@ +# ECC Commitment for a ℚ[X]-Valued Oracle + +So you can have a ℚ[X]-valued polynomial represented as the following + +``` +f_b(X) = Σ_{j( + trace_bin_poly: &[DenseMultilinearExtension>], + bit_op_specs: &[zinc_uair::BitOpSpec], + num_total_bin: usize, + projecting_element_f: &F, + point: &[F], + field_cfg: &F::Config, +) -> Result, ArithErrors> +where + F: InnerTransparentField + MontgomeryLimbs + DelayedFieldProductSum + Send + Sync, + F::Config: Sync, +{ + if bit_op_specs.is_empty() { + return Ok(Vec::new()); + } + + assert!( + D == 32, + "BitOpSpec virtual columns require D == 32, got D = {D}", + ); + + let zero = F::zero_with_cfg(field_cfg); + let one = F::one_with_cfg(field_cfg); + let alpha_powers: Vec = powers(projecting_element_f.clone(), one, 32); + let eq_table = build_eq_x_r_vec(point, field_cfg)?; + let reduction_params = F::barrett_reduction_params(field_cfg); + + let evals = cfg_iter!(bit_op_specs) + .map(|spec| { + assert!( + spec.source_col() < num_total_bin, + "BitOpSpec source_col {} must reference a binary_poly column \ + (num binary cols = {num_total_bin})", + spec.source_col(), + ); + + let col = &trace_bin_poly[spec.source_col()]; + let mut buckets: Vec> = vec![Uint::zero(); 32]; + for (b, cell) in col.iter().enumerate() { + let Some(eq_b) = eq_table.get(b) else { + break; + }; + for (src_bit, coeff) in cell.iter().enumerate().take(32) { + if !coeff.into_inner() { + continue; + } + if let Some(dst_bit) = bit_op_destination(spec.op(), src_bit) { + as DelayedModularReduction>::add(&mut buckets[dst_bit], eq_b); + } + } + } + + let bucket_evals: Vec = buckets + .into_iter() + .map(|acc| { + as DelayedModularReduction>::reduce( + acc, + field_cfg, + &reduction_params, + ) + }) + .collect(); + + FieldFieldInnerProduct::inner_product::( + &bucket_evals, + &alpha_powers, + zero.clone(), + ) + .expect("bit-op bucket and alpha-power lengths match") + }) + .collect(); + + Ok(evals) +} + +fn bit_op_destination(op: BitOp, src_bit: usize) -> Option { + match op { + BitOp::Rot(c) => Some((src_bit + c as usize) % 32), + BitOp::ShiftR(c) => { + let c = c as usize; + src_bit.checked_sub(c) + } + } +} + pub struct CombinedPolyResolver(PhantomData); impl @@ -285,8 +382,7 @@ impl>> = - RefCell::new(None); + let cache: RefCell>> = RefCell::new(None); let project = |scalar: &U::Scalar| -> F { if let Some(v) = cache.borrow().as_ref().and_then(|c| c.get(scalar)) { return v; @@ -315,7 +411,7 @@ impl = powers(folding_challenge, one.clone(), num_constraints); - // TODO(Alex): investigate if parallelising this is beneficial. // Compute v_0 + \alpha * v_1 + ... + \alpha ^ k * v_k. - let expected_sum = ic_check_subclaim + let expected_values: Vec = ic_check_subclaim .values .iter() - .zip(&folding_challenge_powers) - .map(|(claimed_value, random_coeff)| { + .map(|claimed_value| { let deg = claimed_value.degree().map_or(0, |d| add!(d, 1)); DynamicPolyFInnerProduct::inner_product::( &claimed_value.coeffs[..deg], @@ -479,9 +573,14 @@ impl( + &expected_values, + &folding_challenge_powers[..expected_values.len()], + zero.clone(), + ) + .expect("claimed values and folding powers have matching lengths"); if claimed_sum != expected_sum { return Err(CombinedPolyResolverError::WrongSumcheckSum { @@ -575,17 +674,13 @@ impl DenseMultilinearExtension> { + DenseMultilinearExtension::from_evaluations_vec( + patterns.len().next_power_of_two().trailing_zeros() as usize, + patterns + .iter() + .copied() + .map(BinaryPoly::<32>::from) + .collect(), + BinaryPoly::<32>::zero(), + ) + } + + fn assert_bit_op_streaming_matches_materialized(spec: BitOpSpec) { + let cfg = test_config(); + let trace_bin_poly = vec![binary_col_from_u32s(&[ + 0x0000_0001, + 0x8000_0001, + 0x0f0f_00f0, + 0xf000_00ff, + ])]; + let specs = vec![spec]; + let projecting_element = MontyField::<4>::from_with_cfg(7u64, &cfg); + let point = vec![ + MontyField::<4>::from_with_cfg(3u64, &cfg), + MontyField::<4>::from_with_cfg(5u64, &cfg), + ]; + + let materialized = build_bit_op_mles::, 32>( + &trace_bin_poly, + &specs, + 1, + &projecting_element, + point.len(), + &cfg, + ) + .into_iter() + .map(|mle| mle.evaluate_with_config(&point, &cfg)) + .collect::, _>>() + .unwrap(); + + let streaming = compute_bit_op_evals_streaming::, 32>( + &trace_bin_poly, + &specs, + 1, + &projecting_element, + &point, + &cfg, + ) + .unwrap(); + + assert_eq!(streaming, materialized); + } + + #[test] + fn bit_op_streaming_rot_matches_materialized_mle() { + assert_bit_op_streaming_matches_materialized(BitOpSpec::new(0, BitOp::Rot(7))); + } + + #[test] + fn bit_op_streaming_shift_r_matches_materialized_mle() { + assert_bit_op_streaming_matches_materialized(BitOpSpec::new(0, BitOp::ShiftR(5))); + } + fn test_successful_verification_generic< U, IdealOverF, @@ -737,22 +898,23 @@ mod tests { project_scalars_to_field(projected_scalars, &projecting_element).unwrap(); // Prover: prepare → MultiDegreeSumcheck → finalize - let (cpr_group, cpr_ancillary) = CombinedPolyResolver::prepare_sumcheck_group::( - &mut prover_transcript, - evaluate_trace_to_column_mles( - &ProjectedTrace::RowMajor(projected_trace), + let (cpr_group, cpr_ancillary) = + CombinedPolyResolver::prepare_sumcheck_group::( + &mut prover_transcript, + evaluate_trace_to_column_mles( + &ProjectedTrace::RowMajor(projected_trace), + &projecting_element, + ), + &ic_prover_state.evaluation_point, + &projected_scalars, + num_constraints, + num_vars, + max_degree, + &test_config(), + &trace.binary_poly, &projecting_element, - ), - &ic_prover_state.evaluation_point, - &projected_scalars, - num_constraints, - num_vars, - max_degree, - &test_config(), - &trace.binary_poly, - &projecting_element, - ) - .expect("CPR prepare failed"); + ) + .expect("CPR prepare failed"); let (md_proof, states) = MultiDegreeSumcheck::prove_as_subprotocol( &mut prover_transcript, diff --git a/piop/src/combined_poly_resolver/folder.rs b/piop/src/combined_poly_resolver/folder.rs index 4a9afb91..92d2d5e5 100644 --- a/piop/src/combined_poly_resolver/folder.rs +++ b/piop/src/combined_poly_resolver/folder.rs @@ -1,5 +1,10 @@ use crypto_primitives::PrimeField; use zinc_uair::{ConstraintBuilder, ideal::ImpossibleIdeal}; +use zinc_utils::{ + UNCHECKED, + delayed_reduction::DelayedFieldProductSum, + inner_product::{FieldFieldInnerProduct, InnerProduct}, +}; /// There are several situations where we need to /// compute an RLC `u_0 + \alpha * u_1 + ... + \alpha ^ k * u_k`, @@ -18,31 +23,46 @@ use zinc_uair::{ConstraintBuilder, ideal::ImpossibleIdeal}; /// /// This constraint builder handles those situations. /// It's `Expr` associated type is the field `F`, so once -/// an `assert_*` method is called it adds it to the RLC -/// with the next power of the challenge `\alpha`. +/// an `assert_*` method is called it records the residual in order. +/// Call [`ConstraintFolder::finish_folded`] to compute the RLC with the +/// DMR-aware field-field product-sum backend. pub struct ConstraintFolder<'a, F: PrimeField> { /// A reference to precomputed powers of the challenge. challenge_powers: &'a [F], - /// Index of the current constraint, - /// and therefore the current power of the challenge. - current_constraint: usize, - /// The RLC computed so far. - pub folded_constraints: F, + /// Residuals in the exact order constraints were visited. + residuals: Vec, + /// Additive identity used as the product-sum seed. + zero: F, } impl<'a, F: PrimeField> ConstraintFolder<'a, F> { pub fn new(challenge_powers: &'a [F], zero: &F) -> Self { Self { challenge_powers, - current_constraint: 0, - folded_constraints: zero.clone(), + residuals: Vec::with_capacity(challenge_powers.len()), + zero: zero.clone(), } } #[allow(clippy::arithmetic_side_effects)] fn fold_constraint(&mut self, expr: F) { - self.folded_constraints += expr * &self.challenge_powers[self.current_constraint]; - self.current_constraint += 1; + debug_assert!( + self.residuals.len() < self.challenge_powers.len(), + "more constraint residuals than challenge powers" + ); + self.residuals.push(expr); + } + + pub fn finish_folded(self) -> F + where + F: DelayedFieldProductSum, + { + FieldFieldInnerProduct::inner_product::( + &self.residuals, + &self.challenge_powers[..self.residuals.len()], + self.zero, + ) + .expect("constraint residuals and challenge powers have matching lengths") } } @@ -72,3 +92,71 @@ impl<'a, F: PrimeField> ConstraintBuilder for ConstraintFolder<'a, F> { self.fold_constraint(expr); } } + +#[cfg(test)] +mod tests { + use super::*; + use crypto_primitives::{FromWithConfig, crypto_bigint_monty::MontyField}; + use zinc_uair::ConstraintBuilder; + + type F = MontyField<4>; + + fn cfg() -> ::Config { + crate::test_utils::test_config() + } + + fn f(value: u64) -> F { + F::from_with_cfg(value, &cfg()) + } + + fn naive_fold(residuals: &[F], powers: &[F], zero: F) -> F { + residuals + .iter() + .zip(powers) + .fold(zero, |acc, (residual, power)| { + acc + residual.clone() * power + }) + } + + #[test] + fn finish_folded_matches_naive_empty() { + let cfg = cfg(); + let zero = F::zero_with_cfg(&cfg); + let powers = vec![f(1), f(7), f(49)]; + let folder = ConstraintFolder::new(&powers, &zero); + + assert_eq!(folder.finish_folded(), zero); + } + + #[test] + fn finish_folded_matches_naive_single_constraint() { + let cfg = cfg(); + let zero = F::zero_with_cfg(&cfg); + let powers = vec![f(1), f(7), f(49)]; + let residuals = vec![f(11)]; + let mut folder = ConstraintFolder::new(&powers, &zero); + folder.assert_zero(residuals[0].clone()); + + assert_eq!( + folder.finish_folded(), + naive_fold(&residuals, &powers, zero) + ); + } + + #[test] + fn finish_folded_matches_naive_multiple_constraints() { + let cfg = cfg(); + let zero = F::zero_with_cfg(&cfg); + let powers = vec![f(1), f(7), f(49), f(343)]; + let residuals = vec![f(3), f(5), f(8), f(13)]; + let mut folder = ConstraintFolder::new(&powers, &zero); + for residual in &residuals { + folder.assert_zero(residual.clone()); + } + + assert_eq!( + folder.finish_folded(), + naive_fold(&residuals, &powers, zero) + ); + } +} diff --git a/piop/src/lookup/booleanity.rs b/piop/src/lookup/booleanity.rs index 1a1f14c2..1ac1e007 100644 --- a/piop/src/lookup/booleanity.rs +++ b/piop/src/lookup/booleanity.rs @@ -33,8 +33,9 @@ use zinc_uair::{ VirtualBoolSpec, }; use zinc_utils::{ - cfg_into_iter, cfg_iter, - delayed_reduction::{DelayedModularReduction, MontgomeryLimbs}, + UNCHECKED, cfg_into_iter, cfg_iter, + delayed_reduction::{DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs}, + inner_product::{FieldFieldInnerProduct, InnerProduct}, inner_transparent_field::InnerTransparentField, powers, }; @@ -222,19 +223,15 @@ where VirtualBoolSource::SelfBitSlice { witness_col_idx, bit_idx, - } => &self_bit_slices[*witness_col_idx * D + *bit_idx] - .evaluations, + } => &self_bit_slices[*witness_col_idx * D + *bit_idx].evaluations, VirtualBoolSource::ShiftedBitSlice { shifted_spec_idx, bit_idx, - } => &shifted_bit_slice_mles - [*shifted_spec_idx * D + *bit_idx] - .evaluations, + } => &shifted_bit_slice_mles[*shifted_spec_idx * D + *bit_idx].evaluations, VirtualBoolSource::PublicBitSlice { public_col_idx, bit_idx, - } => &public_bit_slices[*public_col_idx * D + *bit_idx] - .evaluations, + } => &public_bit_slices[*public_col_idx * D + *bit_idx].evaluations, VirtualBoolSource::IntCol { witness_col_idx } => { &int_witness_cols[*witness_col_idx].evaluations } @@ -246,43 +243,38 @@ where match *coeff { 1 => { for t in 0..n { - evals[t] = - F::add_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::add_inner(&evals[t], &src[t], field_cfg); } } -1 => { for t in 0..n { - evals[t] = - F::sub_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::sub_inner(&evals[t], &src[t], field_cfg); } } 2 => { for t in 0..n { - evals[t] = - F::add_inner(&evals[t], &src[t], field_cfg); - evals[t] = - F::add_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::add_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::add_inner(&evals[t], &src[t], field_cfg); } } -2 => { for t in 0..n { - evals[t] = - F::sub_inner(&evals[t], &src[t], field_cfg); - evals[t] = - F::sub_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::sub_inner(&evals[t], &src[t], field_cfg); + evals[t] = F::sub_inner(&evals[t], &src[t], field_cfg); } } c => { for t in 0..n { - let term = - apply_coeff_inner::(c, &src[t], field_cfg); - evals[t] = - F::add_inner(&evals[t], &term, field_cfg); + let term = apply_coeff_inner::(c, &src[t], field_cfg); + evals[t] = F::add_inner(&evals[t], &term, field_cfg); } } } } - DenseMultilinearExtension { evaluations: evals, num_vars } + DenseMultilinearExtension { + evaluations: evals, + num_vars, + } }) .collect() } @@ -318,13 +310,11 @@ where VirtualBoolSource::ShiftedBitSlice { shifted_spec_idx, bit_idx, - } => &shifted_bit_slice_evals - [*shifted_spec_idx * D + *bit_idx], + } => &shifted_bit_slice_evals[*shifted_spec_idx * D + *bit_idx], VirtualBoolSource::PublicBitSlice { public_col_idx, bit_idx, - } => &public_bit_slice_evals - [*public_col_idx * D + *bit_idx], + } => &public_bit_slice_evals[*public_col_idx * D + *bit_idx], VirtualBoolSource::IntCol { witness_col_idx } => { &int_witness_up_evals[*witness_col_idx] } @@ -460,7 +450,10 @@ where } evaluations.push(BinaryPoly::::new(coeffs.as_slice())); } - DenseMultilinearExtension { evaluations, num_vars } + DenseMultilinearExtension { + evaluations, + num_vars, + } }) .collect() } @@ -733,9 +726,8 @@ where let r1_inner = r1.inner().clone(); let one_minus_r1_inner = one_minus_r1.inner().clone(); - let mut mles: Vec> = Vec::with_capacity( - 1 + self.binary_cols.len() * D + self.extra_bit_cols.len(), - ); + let mut mles: Vec> = + Vec::with_capacity(1 + self.binary_cols.len() * D + self.extra_bit_cols.len()); mles.push(eq_folded); // BinaryPoly does not impl Index on every backend @@ -871,7 +863,12 @@ pub fn prepare_booleanity_group( field_cfg: &F::Config, ) -> Result, BooleanityProverAncillary)>, BooleanityError> where - F: InnerTransparentField + FromPrimitiveWithConfig + Send + Sync + 'static, + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, F::Inner: ConstTranscribable + Send + Sync + Zero + Default + Clone, F::Modulus: ConstTranscribable, { @@ -884,8 +881,7 @@ where let one = F::one_with_cfg(field_cfg); let folding_challenge: F = transcript.get_field_challenge(field_cfg); - let folding_challenge_powers: Vec = - powers(folding_challenge, one.clone(), num_bit_slices); + let folding_challenge_powers: Vec = powers(folding_challenge, one.clone(), num_bit_slices); // Pre-build E_other = eq(b', ic_evaluation_point[1..]) for the // round-1 fast path. The full-size eq_r is only needed for rounds @@ -918,11 +914,13 @@ where // Σ_k α^k · v_k · (v_k - 1) computed as Σ_k α^k · (v_k² - v_k) to // avoid a per-iteration `(v - one)` clone. - let mut acc = zero.clone(); - for (v, coeff) in bits.iter().zip(folding_challenge_powers.iter()) { - let v_sq = v.clone() * v.clone(); - acc = acc + coeff.clone() * (v_sq - v.clone()); - } + let residuals = booleanity_residuals(bits); + let acc = FieldFieldInnerProduct::inner_product::( + &folding_challenge_powers, + &residuals, + zero.clone(), + ) + .expect("booleanity residuals and powers have matching lengths"); acc * eq_r.clone() }); @@ -1039,7 +1037,7 @@ pub fn finalize_booleanity_verifier( field_cfg: &F::Config, ) -> Result<(), BooleanityError> where - F: InnerTransparentField, + F: InnerTransparentField + DelayedFieldProductSum, F::Inner: ConstTranscribable, F::Modulus: ConstTranscribable, { @@ -1062,14 +1060,18 @@ where let eq_r_value = eq_eval(shared_point, &ancillary.ic_evaluation_point, one.clone())?; let n_proof = bit_slice_evals.len() - closing_overrides_tail.len(); - let bool_folded = bit_slice_evals[..n_proof] + let values: Vec = bit_slice_evals[..n_proof] .iter() .chain(closing_overrides_tail.iter()) - .zip(ancillary.folding_challenge_powers.iter()) - .fold(zero, |acc, (v, coeff)| { - let v_sq = v.clone() * v.clone(); - acc + coeff.clone() * (v_sq - v.clone()) - }); + .cloned() + .collect(); + let residuals = booleanity_residuals(&values); + let bool_folded = FieldFieldInnerProduct::inner_product::( + &ancillary.folding_challenge_powers, + &residuals, + zero, + ) + .expect("booleanity residuals and powers have matching lengths"); let recomputed = bool_folded * eq_r_value; @@ -1099,7 +1101,7 @@ where /// each bit-slice eval against the true bit-decomposition of the /// committed parent column. #[allow(clippy::arithmetic_side_effects)] -pub fn verify_bit_decomposition_consistency( +pub fn verify_bit_decomposition_consistency( parent_evals_per_col: &[F], bit_slice_evals: &[F], projecting_element: &F, @@ -1118,24 +1120,15 @@ pub fn verify_bit_decomposition_consistency( let zero = F::zero_with_cfg(projecting_element.cfg()); let one = F::one_with_cfg(projecting_element.cfg()); - - // Powers [1, a, a^2, ..., a^{bits_per_col - 1}]. - let mut a_powers: Vec = Vec::with_capacity(bits_per_col); - let mut acc = one; - for _ in 0..bits_per_col { - a_powers.push(acc.clone()); - acc *= projecting_element; - } + let a_powers: Vec = powers(projecting_element.clone(), one, bits_per_col); for (col_idx, parent_eval) in parent_evals_per_col.iter().enumerate() { let base = col_idx * bits_per_col; - let recombined = - bit_slice_evals[base..base + bits_per_col] - .iter() - .zip(&a_powers) - .fold(zero.clone(), |acc, (bit_eval, a_pow)| { - acc + bit_eval.clone() * a_pow - }); + let recombined = project_bit_slice_chunk( + &bit_slice_evals[base..base + bits_per_col], + &a_powers, + zero.clone(), + ); if &recombined != parent_eval { return Err(BooleanityError::ConsistencyMismatch { @@ -1149,11 +1142,28 @@ pub fn verify_bit_decomposition_consistency( Ok(()) } +fn booleanity_residuals(values: &[F]) -> Vec { + values + .iter() + .map(|v| { + let v_sq = v.clone() * v.clone(); + v_sq - v.clone() + }) + .collect() +} + +fn project_bit_slice_chunk( + bit_slice_evals: &[F], + powers: &[F], + zero: F, +) -> F { + FieldFieldInnerProduct::inner_product::(bit_slice_evals, powers, zero) + .expect("bit-slice chunk and projection powers have matching lengths") +} + #[derive(Debug, Error)] pub enum BooleanityError { - #[error( - "wrong bit-slice evaluation count: got {got}, expected {expected}" - )] + #[error("wrong bit-slice evaluation count: got {got}, expected {expected}")] WrongBitSliceEvalCount { got: usize, expected: usize }, #[error( "bit-decomposition consistency mismatch on binary_poly column {col_idx}: got Σ a^i·bᵢ = {got:?}, expected parent eval {expected:?}" @@ -1172,9 +1182,7 @@ pub enum BooleanityError { #[cfg(test)] mod tests { use super::*; - use crypto_primitives::{ - FromWithConfig, boolean::Boolean, crypto_bigint_monty::MontyField, - }; + use crypto_primitives::{FromWithConfig, boolean::Boolean, crypto_bigint_monty::MontyField}; type F = MontyField<4>; @@ -1187,8 +1195,7 @@ mod tests { let evaluations: Vec> = patterns .iter() .map(|&p| { - let coeffs: [Boolean; 8] = - array::from_fn(|i| Boolean::new((p >> i) & 1 != 0)); + let coeffs: [Boolean; 8] = array::from_fn(|i| Boolean::new((p >> i) & 1 != 0)); BinaryPoly::<8>::new(coeffs) }) .collect(); @@ -1207,20 +1214,14 @@ mod tests { ShiftedBitSliceSpec::new(0, 1), ShiftedBitSliceSpec::new(0, 2), ]; - let point = vec![ - F::from_with_cfg(3u64, &cfg), - F::from_with_cfg(5u64, &cfg), - ]; + let point = vec![F::from_with_cfg(3u64, &cfg), F::from_with_cfg(5u64, &cfg)]; - let materialized = build_shifted_bit_slice_mles::( - std::slice::from_ref(&col), - &specs, - &cfg, - ) - .into_iter() - .map(|mle| mle.evaluate_with_config(&point, &cfg)) - .collect::, _>>() - .unwrap(); + let materialized = + build_shifted_bit_slice_mles::(std::slice::from_ref(&col), &specs, &cfg) + .into_iter() + .map(|mle| mle.evaluate_with_config(&point, &cfg)) + .collect::, _>>() + .unwrap(); let streaming = compute_shifted_bit_slice_evals_streaming::( std::slice::from_ref(&col), &specs, @@ -1251,7 +1252,10 @@ mod tests { } else { zero.clone() }; - assert_eq!(bit_slices[bit].evaluations[row], want, "row {row} bit {bit}"); + assert_eq!( + bit_slices[bit].evaluations[row], want, + "row {row} bit {bit}" + ); } } } @@ -1276,13 +1280,8 @@ mod tests { a_pow = a_pow * a.clone(); } - verify_bit_decomposition_consistency( - std::slice::from_ref(&parent_eval), - &bit_evals, - &a, - 8, - ) - .expect("honest decomposition should satisfy consistency check"); + verify_bit_decomposition_consistency(std::slice::from_ref(&parent_eval), &bit_evals, &a, 8) + .expect("honest decomposition should satisfy consistency check"); } #[test] @@ -1314,7 +1313,10 @@ mod tests { &a, 4, ); - assert!(matches!(res, Err(BooleanityError::ConsistencyMismatch { .. }))); + assert!(matches!( + res, + Err(BooleanityError::ConsistencyMismatch { .. }) + )); } #[test] @@ -1326,6 +1328,32 @@ mod tests { verify_bit_decomposition_consistency(&parent_evals, &bit_evals, &one, 8).unwrap(); } + #[test] + fn bit_slice_projection_helper_matches_naive_fold() { + let cfg = test_cfg(); + let zero = F::zero_with_cfg(&cfg); + let one = F::one_with_cfg(&cfg); + let a = F::from_with_cfg(9u64, &cfg); + let bit_evals = vec![ + F::from_with_cfg(1u64, &cfg), + F::from_with_cfg(0u64, &cfg), + F::from_with_cfg(1u64, &cfg), + F::from_with_cfg(1u64, &cfg), + F::from_with_cfg(0u64, &cfg), + ]; + let powers = powers(a, one, bit_evals.len()); + + let got = project_bit_slice_chunk(&bit_evals, &powers, zero.clone()); + let want = bit_evals + .iter() + .zip(&powers) + .fold(zero, |acc, (bit_eval, power)| { + acc + bit_eval.clone() * power + }); + + assert_eq!(got, want); + } + /// Cross-validate the round-1 fast path against a faithful standard /// run of `ProverState::prove_round`. Both must produce the same /// tail evaluations and the same asserted sum (zero, since this is @@ -1343,8 +1371,14 @@ mod tests { // Mix of fully-zero, fully-one, and varying patterns to exercise all // four (A, B) cases for the XOR fold structure. let binary_cols = vec![ - col_from_u8s(&[0b00000000, 0b00010001, 0b00100010, 0b00110011, 0b01000100, 0b01010101, 0b01100110, 0b01110111]), - col_from_u8s(&[0b11110000, 0b11100001, 0b11010010, 0b11000011, 0b10110100, 0b10100101, 0b10010110, 0b10000111]), + col_from_u8s(&[ + 0b00000000, 0b00010001, 0b00100010, 0b00110011, 0b01000100, 0b01010101, 0b01100110, + 0b01110111, + ]), + col_from_u8s(&[ + 0b11110000, 0b11100001, 0b11010010, 0b11000011, 0b10110100, 0b10100101, 0b10010110, + 0b10000111, + ]), ]; let num_vars = 3; const D: usize = 8; @@ -1467,7 +1501,8 @@ mod tests { let mut std_eq_r = eq_r_full; std_eq_r.fix_variables_with_config(slice::from_ref(&r_1), &cfg); assert_eq!( - fast_mles[0].num_vars, num_vars - 1, + fast_mles[0].num_vars, + num_vars - 1, "fast-path eq_r_folded must have num_vars - 1 variables" ); assert_eq!( @@ -1478,7 +1513,8 @@ mod tests { for (idx, mut bit_mle) in bit_slices_full.into_iter().enumerate() { bit_mle.fix_variables_with_config(slice::from_ref(&r_1), &cfg); assert_eq!( - fast_mles[1 + idx].evaluations, bit_mle.evaluations, + fast_mles[1 + idx].evaluations, + bit_mle.evaluations, "fast-path bit-slice {idx} folded value must match standard fix_variables" ); } @@ -1491,6 +1527,9 @@ mod tests { let parent_evals = vec![one.clone()]; let bit_evals: Vec = vec![one.clone(), one.clone()]; let res = verify_bit_decomposition_consistency(&parent_evals, &bit_evals, &one, 8); - assert!(matches!(res, Err(BooleanityError::WrongBitSliceEvalCount { .. }))); + assert!(matches!( + res, + Err(BooleanityError::WrongBitSliceEvalCount { .. }) + )); } } diff --git a/zinc+.pdf b/zinc+.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b1c6ba02f2c01d18a17eda9359b416f437b640a3 GIT binary patch literal 1630176 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 fB9fL`J)|_auBxup&7w8 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

-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

d`-x)xf<%ByG%$UAxVdfb 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�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 ~SOlG>4 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%0A_pTfyI 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

tQv4YGELrWuV$_Pd`N;Pa|UbZ%K+}`j#N@A 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

~3eZU$==j?`=61tuZ+}7m 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�K2rs&LE+mG}K`w*HX(dagO1_pV*ZygOEPJ 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ô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 z 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%0DJrT7z`EeS{@4jH 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ŠggL$c*>YhWvr>-AtlE_K+pkU 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 PTfn$685tF{6fyq~6?1af literal 0 HcmV?d00001 From 12d46aed3c5a6f10bdbe5869a7902c1c6069edcc Mon Sep 17 00:00:00 2001 From: John Wu Date: Thu, 4 Jun 2026 22:14:31 -0700 Subject: [PATCH 08/49] Optimize unblinded Hyrax bool commits --- protocol/benches/e2e.rs | 19 +- zip-plus/benches/hyrax_commit_breakdown.rs | 13 +- zip-plus/src/pcs/hyrax.rs | 200 ++++++++++++++++----- zip-plus/src/pcs/msm_commitment.rs | 169 ++++++++++++++++- 4 files changed, 345 insertions(+), 56 deletions(-) diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index 48336b79..ca38cd02 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -55,7 +55,7 @@ use zip_plus::{ iprs::{IprsCode, PnttConfigF65537}, }, pcs::generic::{PCS, ZipPlusPCS}, - pcs::hyrax::{BinaryLanes, HyraxPCS}, + pcs::hyrax::{BinaryLanes, HyraxBlindingMode, HyraxPCS}, pcs::structs::{ZipPlus, ZipPlusCommitment, ZipPlusParams, ZipTypes}, utils::{eprint_bytes_size, eprint_bytes_size_breakdown, eprint_proof_size}, }; @@ -1255,8 +1255,13 @@ where u64::try_from(width + 1).expect("Hyrax blinding basis index must fit in u64"), ); let h = generator * h_scalar; - let (ck, vk) = HyraxPCS::::setup_from_bases(width, bases, h) - .expect("Hyrax benchmark setup must be valid"); + let (ck, vk) = HyraxPCS::::setup_from_bases_with_blinding( + width, + bases, + h, + HyraxBlindingMode::Unblinded, + ) + .expect("Hyrax benchmark setup must be valid"); ( PCSParams::, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE> { binary: ck, @@ -1392,13 +1397,13 @@ fn bench_real_sha256_pcs_e2e(group: &mut BenchmarkGroup, num_vars: usi group, num_vars, "RealSha256PCS/ZipBn254Fr", - "RealSha256PCS/HyraxBn254", + "RealSha256PCS/HyraxBn254Unblinded", ); bench_real_sha256_pcs_curve_e2e::( group, num_vars, "RealSha256PCS/ZipSecp256k1Fr", - "RealSha256PCS/HyraxSecp256k1", + "RealSha256PCS/HyraxSecp256k1Unblinded", ); } @@ -1407,13 +1412,13 @@ fn bench_real_sha256_pcs_steps(group: &mut BenchmarkGroup, num_vars: u group, num_vars, "RealSha256PCS/ZipBn254Fr", - "RealSha256PCS/HyraxBn254", + "RealSha256PCS/HyraxBn254Unblinded", ); bench_real_sha256_pcs_curve_steps::( group, num_vars, "RealSha256PCS/ZipSecp256k1Fr", - "RealSha256PCS/HyraxSecp256k1", + "RealSha256PCS/HyraxSecp256k1Unblinded", ); } diff --git a/zip-plus/benches/hyrax_commit_breakdown.rs b/zip-plus/benches/hyrax_commit_breakdown.rs index f3f32346..eb43eaa8 100644 --- a/zip-plus/benches/hyrax_commit_breakdown.rs +++ b/zip-plus/benches/hyrax_commit_breakdown.rs @@ -7,7 +7,7 @@ use std::hint::black_box; use zinc_poly::{mle::DenseMultilinearExtension, univariate::binary::BinaryPoly}; use zip_plus::pcs::{ generic::PCS, - hyrax::{BinaryLanes, HyraxCommitmentKey, HyraxPCS}, + hyrax::{BinaryLanes, HyraxBlindingMode, HyraxCommitmentKey, HyraxPCS}, msm_commitment::{BoolSubsetMsm, MsmCommitmentEngine, MsmCommitmentKey, RowMsmStrategy}, }; @@ -35,9 +35,14 @@ fn msm_ck(width: usize) -> MsmCommitmentKey { fn hyrax_ck(width: usize) -> HyraxCommitmentKey { let (bases, h) = bases_and_h::(width); - HyraxPCS::::setup_from_bases(width, bases, h) - .expect("benchmark setup must be valid") - .0 + HyraxPCS::::setup_from_bases_with_blinding( + width, + bases, + h, + HyraxBlindingMode::Unblinded, + ) + .expect("benchmark setup must be valid") + .0 } fn bool_row(width: usize) -> Vec { diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index 0ecc190a..5fecbf81 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -27,8 +27,8 @@ use crate::{ pcs::{ generic::PCS, msm_commitment::{ - BoolSubsetMsm, MsmCommitmentEngine, MsmCommitmentKey, MsmError, MsmVerifierKey, - RowMsmStrategy, ScalarPippengerMsm, + BoolSubsetMsm, MsmCommitmentEngine, MsmCommitmentKey, MsmError, RowMsmStrategy, + ScalarPippengerMsm, }, }, pcs_transcript::{PcsProverTranscript, PcsVerifierTranscript}, @@ -37,11 +37,30 @@ use crate::{ #[derive(Clone, Debug)] pub struct HyraxPCS(PhantomData<(C, Lanes)>); +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HyraxBlindingMode { + Blinded, + Unblinded, +} + +impl HyraxBlindingMode { + fn as_u8(self) -> u8 { + match self { + Self::Blinded => 1, + Self::Unblinded => 0, + } + } + + fn is_blinded(self) -> bool { + matches!(self, Self::Blinded) + } +} + #[derive(Clone, Debug)] pub struct HyraxCommitmentKey { pub(crate) num_cols: usize, - pub(crate) bases: Vec, - pub(crate) h: C::Group, + pub(crate) blinding_mode: HyraxBlindingMode, + pub(crate) msm_ck: MsmCommitmentKey, } #[derive(Clone, Debug)] @@ -49,6 +68,7 @@ pub struct HyraxVerifierKey { pub(crate) num_cols: usize, pub(crate) bases: Vec, pub(crate) h: C::Group, + pub(crate) blinding_mode: HyraxBlindingMode, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -56,6 +76,7 @@ pub struct HyraxCommitment { pub(crate) batch_size: usize, pub(crate) num_lanes: usize, pub(crate) num_rows: usize, + pub(crate) blinding_mode: HyraxBlindingMode, pub(crate) comm: Vec, } @@ -64,6 +85,7 @@ pub struct HyraxProverData { pub(crate) batch_size: usize, pub(crate) num_lanes: usize, pub(crate) num_rows: usize, + pub(crate) blinding_mode: HyraxBlindingMode, pub(crate) blinds: Vec, } @@ -249,23 +271,33 @@ impl HyraxPCS { width: usize, bases: Vec, h: C::Group, + ) -> Result<(HyraxCommitmentKey, HyraxVerifierKey), ZipError> { + Self::setup_from_bases_with_blinding(width, bases, h, HyraxBlindingMode::Blinded) + } + + pub fn setup_from_bases_with_blinding( + width: usize, + bases: Vec, + h: C::Group, + blinding_mode: HyraxBlindingMode, ) -> Result<(HyraxCommitmentKey, HyraxVerifierKey), ZipError> { if !width.is_power_of_two() { return Err(ZipError::InvalidPcsParam(format!( "Hyrax row width must be a power of two, got {width}" ))); } - let (_ck, _vk) = msm_keys(width, bases.clone(), h)?; + let msm_ck = msm_key(width, bases.clone(), h)?; Ok(( HyraxCommitmentKey { num_cols: width, - bases: bases.clone(), - h, + blinding_mode, + msm_ck, }, HyraxVerifierKey { num_cols: width, bases, h, + blinding_mode, }, )) } @@ -284,9 +316,7 @@ where type ProverData = HyraxProverData; fn precompute_ck(ck: &Self::CommitmentKey) { - if let Ok((msm_ck, _)) = msm_keys(ck.num_cols, ck.bases.clone(), ck.h) { - Lanes::Strategy::precompute_ck(&msm_ck); - } + Lanes::Strategy::precompute_ck(&ck.msm_ck); } fn commit( @@ -299,12 +329,14 @@ where batch_size: 0, num_lanes: Lanes::NUM_LANES, num_rows: 0, + blinding_mode: ck.blinding_mode, blinds: Vec::new(), }, HyraxCommitment { batch_size: 0, num_lanes: Lanes::NUM_LANES, num_rows: 0, + blinding_mode: ck.blinding_mode, comm: Vec::new(), }, )); @@ -313,20 +345,31 @@ where validate_polys(polys)?; let n = polys[0].evaluations.len(); let num_rows = num_rows(n, ck.num_cols)?; - let (msm_ck, _) = msm_keys(ck.num_cols, ck.bases.clone(), ck.h)?; let mut all_comm = Vec::with_capacity(polys.len() * Lanes::NUM_LANES * num_rows); - let mut all_blinds = Vec::with_capacity(polys.len() * Lanes::NUM_LANES * num_rows); + let mut all_blinds = if ck.blinding_mode.is_blinded() { + Vec::with_capacity(polys.len() * Lanes::NUM_LANES * num_rows) + } else { + Vec::new() + }; for poly in polys { for lane in 0..Lanes::NUM_LANES { let values = lane_values::(poly, lane)?; - let blind = MsmCommitmentEngine::::blind(&msm_ck, values.len()); - let commitment = MsmCommitmentEngine::::commit_with::<_, Lanes::Strategy>( - &msm_ck, &values, &blind, - ) - .map_err(msm_err)?; + let commitment = if ck.blinding_mode.is_blinded() { + let blind = MsmCommitmentEngine::::blind(&ck.msm_ck, values.len()); + let commitment = MsmCommitmentEngine::::commit_with::<_, Lanes::Strategy>( + &ck.msm_ck, &values, &blind, + ) + .map_err(msm_err)?; + all_blinds.extend(blind.blind); + commitment + } else { + MsmCommitmentEngine::::commit_unblinded_with::<_, Lanes::Strategy>( + &ck.msm_ck, &values, + ) + .map_err(msm_err)? + }; all_comm.extend(commitment.comm); - all_blinds.extend(blind.blind); } } @@ -335,12 +378,14 @@ where batch_size: polys.len(), num_lanes: Lanes::NUM_LANES, num_rows, + blinding_mode: ck.blinding_mode, blinds: all_blinds, }, HyraxCommitment { batch_size: polys.len(), num_lanes: Lanes::NUM_LANES, num_rows, + blinding_mode: ck.blinding_mode, comm: all_comm, }, )) @@ -351,6 +396,7 @@ where transcript.absorb_slice(&(commitment.batch_size as u64).to_le_bytes()); transcript.absorb_slice(&(commitment.num_lanes as u64).to_le_bytes()); transcript.absorb_slice(&(commitment.num_rows as u64).to_le_bytes()); + transcript.absorb_slice(&[commitment.blinding_mode.as_u8()]); for comm in &commitment.comm { let bytes = group_bytes::(comm).unwrap_or_default(); transcript.absorb_slice(&bytes); @@ -360,13 +406,14 @@ where fn commitment_num_bytes(commitment: &Self::Commitment) -> usize { let group_size = C::zero().serialized_size(Compress::Yes); - 3 * core::mem::size_of::() + commitment.comm.len() * group_size + 3 * core::mem::size_of::() + 1 + commitment.comm.len() * group_size } fn write_commitment_bytes(commitment: &Self::Commitment, buf: &mut Vec) { buf.extend_from_slice(&(commitment.batch_size as u64).to_le_bytes()); buf.extend_from_slice(&(commitment.num_lanes as u64).to_le_bytes()); buf.extend_from_slice(&(commitment.num_rows as u64).to_le_bytes()); + buf.push(commitment.blinding_mode.as_u8()); for comm in &commitment.comm { let bytes = group_bytes::(comm).expect("Hyrax commitment must serialize"); buf.extend_from_slice(&bytes); @@ -394,7 +441,12 @@ where return Ok(()); } validate_polys(polys)?; - validate_hyrax_shape::(ck.num_cols, polys, prover_data)?; + validate_hyrax_shape::( + ck.num_cols, + ck.blinding_mode, + polys, + prover_data, + )?; let n = polys[0].evaluations.len(); if n != (1usize << point.len()) { @@ -449,14 +501,16 @@ where let alpha = alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; for (row_idx, row) in poly.evaluations.chunks(ck.num_cols).enumerate() { let coeff = alpha * row_coeffs[row_idx]; - let blind_idx = commitment_index_dynamic( - Lanes::NUM_LANES, - poly_idx, - lane, - row_idx, - prover_data.num_rows, - ); - rho_star += coeff * prover_data.blinds[blind_idx]; + if ck.blinding_mode.is_blinded() { + let blind_idx = commitment_index_dynamic( + Lanes::NUM_LANES, + poly_idx, + lane, + row_idx, + prover_data.num_rows, + ); + rho_star += coeff * prover_data.blinds[blind_idx]; + } for (col_idx, eval) in row.iter().enumerate() { let value = Lanes::lane_to_scalar(Lanes::lane_value(eval, lane)?); combined_row[col_idx] += coeff * value; @@ -466,7 +520,9 @@ where } write_scalars::(transcript, &combined_row)?; - write_scalar::(transcript, &rho_star)?; + if ck.blinding_mode.is_blinded() { + write_scalar::(transcript, &rho_star)?; + } if q0_f.len() != b_f.len() { return Err(ZipError::InvalidPcsOpen( @@ -493,6 +549,11 @@ where if commitment.batch_size == 0 { return Ok(()); } + if commitment.blinding_mode != vk.blinding_mode { + return Err(ZipError::InvalidPcsParam( + "Hyrax commitment blinding mode mismatch".to_string(), + )); + } validate_commitment_shape::(commitment)?; if lifted_evals.len() != commitment.batch_size { return Err(ZipError::InvalidPcsParam(format!( @@ -560,7 +621,11 @@ where }; let combined_row = read_scalars::(transcript, vk.num_cols)?; - let rho_star = read_scalar::(transcript)?; + let rho_star = if vk.blinding_mode.is_blinded() { + Some(read_scalar::(transcript)?) + } else { + None + }; let mut lhs = C::ScalarField::zero(); for (value, weight) in combined_row.iter().zip(q1_scalar.iter()) { @@ -593,13 +658,15 @@ where } } - let (msm_ck, _) = msm_keys(vk.num_cols, vk.bases.clone(), vk.h)?; + let msm_ck = msm_key(vk.num_cols, vk.bases.clone(), vk.h)?; let mut expected = >::msm_row( &msm_ck, &combined_row, ) .map_err(msm_err)?; - expected += vk.h * rho_star; + if let Some(rho_star) = rho_star { + expected += vk.h * rho_star; + } if comm_lc != expected { return Err(ZipError::InvalidPcsOpen( @@ -627,6 +694,7 @@ fn validate_polys(polys: &[DenseMultilinearExtension]) -> Res fn validate_hyrax_shape( width: usize, + blinding_mode: HyraxBlindingMode, polys: &[DenseMultilinearExtension], prover_data: &HyraxProverData, ) -> Result<(), ZipError> @@ -637,10 +705,16 @@ where { let n = polys[0].evaluations.len(); let num_rows = num_rows(n, width)?; + let expected_blinds = if blinding_mode.is_blinded() { + polys.len() * Lanes::NUM_LANES * num_rows + } else { + 0 + }; if prover_data.batch_size != polys.len() || prover_data.num_lanes != Lanes::NUM_LANES || prover_data.num_rows != num_rows - || prover_data.blinds.len() != polys.len() * Lanes::NUM_LANES * num_rows + || prover_data.blinding_mode != blinding_mode + || prover_data.blinds.len() != expected_blinds { return Err(ZipError::InvalidPcsParam( "Hyrax prover data shape mismatch".to_string(), @@ -740,12 +814,14 @@ fn uint_from_le_bytes(bytes: &[u8]) -> Uint { Uint::::read_transcription_bytes_exact(&padded) } -fn msm_keys( +fn msm_key( width: usize, bases: Vec, h: C::Group, -) -> Result<(MsmCommitmentKey, MsmVerifierKey), ZipError> { - MsmCommitmentEngine::::setup_from_bases(width, bases, h).map_err(msm_err) +) -> Result, ZipError> { + MsmCommitmentEngine::::setup_from_bases(width, bases, h) + .map(|(ck, _)| ck) + .map_err(msm_err) } fn num_rows(n: usize, width: usize) -> Result { @@ -943,8 +1019,10 @@ mod tests { as HyraxFieldBridge>::field_to_scalar(&bn_field); } - #[test] - fn binary_hyrax_open_verify_round_trip() { + fn binary_hyrax_open_verify_round_trip_with_modes( + commit_mode: HyraxBlindingMode, + verify_mode: HyraxBlindingMode, + ) -> Result<(), ZipError> { type C = ark_bn254::G1Affine; type F = MontyField<4>; const D: usize = 32; @@ -958,9 +1036,20 @@ mod tests { let generator = ::Group::generator(); let bases = (1..=width) .map(|idx| (generator * ::ScalarField::from(idx as u64)).into_affine()) - .collect(); + .collect::>(); let h = generator * ::ScalarField::from((width + 1) as u64); - let (ck, vk) = HyraxPCS::::setup_from_bases(width, bases, h).unwrap(); + let (ck, _) = HyraxPCS::::setup_from_bases_with_blinding( + width, + bases.clone(), + h, + commit_mode, + )?; + let (_, vk) = HyraxPCS::::setup_from_bases_with_blinding( + width, + bases, + h, + verify_mode, + )?; let evals0 = (0..width) .map(|idx| bp((idx as u32).wrapping_mul(0x9E37_79B1))) @@ -973,7 +1062,7 @@ mod tests { DenseMultilinearExtension::from_evaluations_vec(9, evals1, bp(0)), ]; let (prover_data, commitment) = - as PCS, D>>::commit(&ck, &polys).unwrap(); + as PCS, D>>::commit(&ck, &polys)?; let point = [ [0x11u8; 64], @@ -1029,8 +1118,7 @@ mod tests { &point, &prover_data, &cfg, - ) - .unwrap(); + )?; let mut verifier_transcript = prover_transcript.into_verification_transcript(); as PCS, D>>::absorb_commitment( @@ -1051,6 +1139,32 @@ mod tests { &lifted_evals, &cfg, ) + } + + #[test] + fn binary_hyrax_open_verify_round_trip() { + binary_hyrax_open_verify_round_trip_with_modes( + HyraxBlindingMode::Blinded, + HyraxBlindingMode::Blinded, + ) .unwrap(); } + + #[test] + fn unblinded_binary_hyrax_open_verify_round_trip() { + binary_hyrax_open_verify_round_trip_with_modes( + HyraxBlindingMode::Unblinded, + HyraxBlindingMode::Unblinded, + ) + .unwrap(); + } + + #[test] + fn hyrax_rejects_blinding_mode_mismatch() { + let result = binary_hyrax_open_verify_round_trip_with_modes( + HyraxBlindingMode::Unblinded, + HyraxBlindingMode::Blinded, + ); + assert!(result.is_err()); + } } diff --git a/zip-plus/src/pcs/msm_commitment.rs b/zip-plus/src/pcs/msm_commitment.rs index 9f9432b8..dce478ca 100644 --- a/zip-plus/src/pcs/msm_commitment.rs +++ b/zip-plus/src/pcs/msm_commitment.rs @@ -3,18 +3,24 @@ #![allow(clippy::cast_possible_wrap)] #![allow(clippy::cast_sign_loss)] -use std::marker::PhantomData; +use std::{ + marker::PhantomData, + sync::{Arc, OnceLock}, +}; use ark_ec::{AffineRepr, CurveGroup}; use ark_ff::{AdditiveGroup, BigInteger, One, PrimeField, UniformRand, Zero}; use num_integer::Integer; use thiserror::Error; +const DEFAULT_BOOL_WINDOW_BITS: usize = 6; + #[derive(Clone, Debug)] pub struct MsmCommitmentKey { pub(crate) num_cols: usize, pub(crate) bases: Vec, pub(crate) h: C::Group, + bool_tables_6: Arc>>, } #[derive(Clone, Debug)] @@ -72,6 +78,47 @@ pub struct BoolSubsetMsm; pub struct U8BucketMsm; pub struct ScalarPippengerMsm; +#[derive(Clone, Debug)] +struct BoolWindowTable { + tables: Vec>, + lens: Vec, +} + +impl BoolWindowTable { + fn new(bases: &[C], window_bits: usize) -> Self { + let built = bases + .chunks(window_bits) + .map(|window| { + let len = window.len(); + let table_len = 1usize << len; + let mut table = vec![C::Group::zero(); table_len]; + for mask in 1..table_len { + let bit = mask.trailing_zeros() as usize; + let previous = mask & !(1usize << bit); + table[mask] = table[previous] + window[bit]; + } + (table, len) + }) + .collect::>(); + + let (tables, lens) = built.into_iter().unzip(); + Self { tables, lens } + } + + fn msm_row(&self, values: &[bool], window_bits: usize) -> C::Group { + let mut acc = C::Group::zero(); + for (window_idx, len) in self.lens.iter().copied().enumerate() { + let offset = window_idx * window_bits; + if offset >= values.len() { + break; + } + let end = (offset + len).min(values.len()); + acc += self.tables[window_idx][bit_mask(&values[offset..end])]; + } + acc + } +} + impl MsmCommitmentEngine { pub fn setup_from_bases( width: usize, @@ -97,12 +144,14 @@ impl MsmCommitmentEngine { num_cols: width, bases, h, + bool_tables_6: Arc::new(OnceLock::new()), }; Ok((ck, vk)) } pub fn precompute_ck(ck: &MsmCommitmentKey) { + as RowMsmStrategy>::precompute_ck(ck); ScalarPippengerMsm::precompute_ck(ck); } @@ -146,6 +195,35 @@ impl MsmCommitmentEngine { Ok(MsmCommitment { comm }) } + pub fn commit_unblinded_with( + ck: &MsmCommitmentKey, + values: &[V], + ) -> Result, MsmError> + where + V: Copy + Send + Sync, + S: RowMsmStrategy, + { + let expected_rows = num_rows(values.len(), ck.num_cols)?; + let mut comm = Vec::with_capacity(expected_rows); + for row in values.chunks(ck.num_cols) { + let row_comm = if row.iter().copied().all(S::is_zero) { + C::Group::zero() + } else { + S::msm_row(ck, row)? + }; + comm.push(row_comm); + } + + Ok(MsmCommitment { comm }) + } + + pub fn commit_unblinded( + ck: &MsmCommitmentKey, + values: &[C::ScalarField], + ) -> Result, MsmError> { + Self::commit_unblinded_with::(ck, values) + } + pub fn commit( ck: &MsmCommitmentKey, values: &[C::ScalarField], @@ -190,12 +268,24 @@ impl MsmCommitmentEngine { impl RowMsmStrategy for BoolSubsetMsm { - fn precompute_ck(_ck: &MsmCommitmentKey) {} + fn precompute_ck(ck: &MsmCommitmentKey) { + if WINDOW_BITS == DEFAULT_BOOL_WINDOW_BITS { + ck.bool_tables_6 + .get_or_init(|| BoolWindowTable::new(&ck.bases, DEFAULT_BOOL_WINDOW_BITS)); + } + } fn msm_row(ck: &MsmCommitmentKey, values: &[bool]) -> Result { validate_row_len(ck, values.len())?; validate_window_bits(WINDOW_BITS)?; + if WINDOW_BITS == DEFAULT_BOOL_WINDOW_BITS { + return Ok(ck + .bool_tables_6 + .get_or_init(|| BoolWindowTable::new(&ck.bases, DEFAULT_BOOL_WINDOW_BITS)) + .msm_row(values, DEFAULT_BOOL_WINDOW_BITS)); + } + let mut acc = C::Group::zero(); for (window_idx, bits) in values.chunks(WINDOW_BITS).enumerate() { let start = window_idx * WINDOW_BITS; @@ -500,6 +590,23 @@ mod tests { MsmCommitment { comm } } + fn naive_scalar_commit_unblinded( + ck: &MsmCommitmentKey, + values: &[Fr], + ) -> MsmCommitment { + let comm = values + .chunks(ck.num_cols) + .map(|row| { + let mut acc = G1Projective::zero(); + for (scalar, base) in row.iter().zip(ck.bases.iter()) { + acc += *base * scalar; + } + acc + }) + .collect(); + MsmCommitment { comm } + } + #[test] fn bool_commit_matches_scalar_commit_for_configured_widths() { for width in [8, 32, 64] { @@ -521,6 +628,64 @@ mod tests { } } + #[test] + fn precomputed_bool_commit_matches_scalar_commit_for_wide_rows() { + for width in [8, 32, 64, 512] { + let (ck, _) = setup(width); + let n = width + 5; + let values = bool_values(n); + let scalars = scalars_from_bool(&values); + let blind = blind(width, n); + + let before_precompute = MsmCommitmentEngine::::commit_with::< + bool, + BoolSubsetMsm<6>, + >(&ck, &values, &blind) + .expect("bool commit before precompute must succeed"); + MsmCommitmentEngine::::precompute_ck(&ck); + let after_precompute = MsmCommitmentEngine::::commit_with::< + bool, + BoolSubsetMsm<6>, + >(&ck, &values, &blind) + .expect("bool commit after precompute must succeed"); + let cloned_ck = ck.clone(); + let after_clone = + MsmCommitmentEngine::::commit_with::>( + &cloned_ck, &values, &blind, + ) + .expect("bool commit through cloned ck must succeed"); + let scalar_comm = MsmCommitmentEngine::::commit(&ck, &scalars, &blind) + .expect("scalar commit must succeed"); + + assert_eq!(before_precompute, scalar_comm); + assert_eq!(after_precompute, scalar_comm); + assert_eq!(after_clone, scalar_comm); + } + } + + #[test] + fn unblinded_bool_commit_matches_scalar_commit_for_wide_rows() { + for width in [8, 32, 64, 512] { + let (ck, _) = setup(width); + let n = width + 7; + let values = bool_values(n); + let scalars = scalars_from_bool(&values); + + MsmCommitmentEngine::::precompute_ck(&ck); + let bool_comm = MsmCommitmentEngine::::commit_unblinded_with::< + bool, + BoolSubsetMsm<6>, + >(&ck, &values) + .expect("unblinded bool commit must succeed"); + let scalar_comm = MsmCommitmentEngine::::commit_unblinded(&ck, &scalars) + .expect("unblinded scalar commit must succeed"); + let naive_comm = naive_scalar_commit_unblinded(&ck, &scalars); + + assert_eq!(bool_comm, scalar_comm); + assert_eq!(bool_comm, naive_comm); + } + } + #[test] fn u8_commit_matches_scalar_commit_for_configured_widths() { for width in [8, 32, 64] { From 3aff049496d39375eabfd6d0adb8f7615098ca65 Mon Sep 17 00:00:00 2001 From: John Wu Date: Fri, 5 Jun 2026 00:56:12 -0700 Subject: [PATCH 09/49] Add NeutronNova linear accumulator primitives --- piop/src/lib.rs | 1 + piop/src/neutron_nova/accumulator.rs | 337 ++++++++++++++++++++++++ piop/src/neutron_nova/mod.rs | 13 + piop/src/neutron_nova/sumfold.rs | 375 +++++++++++++++++++++++++++ utils/src/field/boxed_monty.rs | 66 ++++- zip-plus/src/pcs/test_utils.rs | 4 + 6 files changed, 794 insertions(+), 2 deletions(-) create mode 100644 piop/src/neutron_nova/accumulator.rs create mode 100644 piop/src/neutron_nova/mod.rs create mode 100644 piop/src/neutron_nova/sumfold.rs diff --git a/piop/src/lib.rs b/piop/src/lib.rs index b1475cb9..8872f821 100644 --- a/piop/src/lib.rs +++ b/piop/src/lib.rs @@ -2,6 +2,7 @@ pub mod combined_poly_resolver; pub mod ideal_check; pub mod lookup; pub mod multipoint_eval; +pub mod neutron_nova; pub mod projections; pub mod random_field_sumcheck; pub mod scalar_proj_cache; diff --git a/piop/src/neutron_nova/accumulator.rs b/piop/src/neutron_nova/accumulator.rs new file mode 100644 index 00000000..8422dd08 --- /dev/null +++ b/piop/src/neutron_nova/accumulator.rs @@ -0,0 +1,337 @@ +use crypto_primitives::{PrimeField, crypto_bigint_uint::Uint}; +use num_traits::Zero; +use std::marker::PhantomData; +use thiserror::Error; +use zinc_poly::{ + mle::DenseMultilinearExtension, + univariate::binary::BinaryPoly, + utils::{ArithErrors, build_eq_x_r_vec}, +}; +use zinc_utils::{ + UNCHECKED, + delayed_reduction::{DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs}, + inner_product::{FieldFieldInnerProduct, InnerProduct}, + powers, +}; + +/// Errors produced by NeutronNova row-space accumulation helpers. +#[derive(Clone, Debug, Error)] +pub enum AccumulatorError { + #[error("row weight length mismatch: weights={weights}, rows={rows}")] + RowWeightLengthMismatch { weights: usize, rows: usize }, + #[error("bit index {bit_idx} is out of range for degree bound {degree}")] + BitIndexOutOfRange { bit_idx: usize, degree: usize }, + #[error("projection powers length mismatch: got {got}, expected at least {expected}")] + ProjectionPowersLengthMismatch { got: usize, expected: usize }, + #[error("row-weight construction failed: {0}")] + RowWeights(#[from] ArithErrors), +} + +/// Equality weights over the Boolean row space. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RowWeights { + weights: Vec, +} + +impl RowWeights { + /// Build weights `eq(r, z)` for all Boolean rows `z`. + /// + /// The zero-variable row space is supported as a single row of weight 1. + pub fn new(row_point: &[F], field_cfg: &F::Config) -> Result { + let weights = if row_point.is_empty() { + vec![F::one_with_cfg(field_cfg)] + } else { + build_eq_x_r_vec(row_point, field_cfg)? + }; + Ok(Self { weights }) + } + + /// Build row weights and zero the final row to match current CPR parity. + pub fn new_with_last_row_zero( + row_point: &[F], + field_cfg: &F::Config, + ) -> Result { + let mut weights = Self::new(row_point, field_cfg)?; + weights.zero_last_row(field_cfg); + Ok(weights) + } + + /// Set the final row weight to zero in-place. + pub fn zero_last_row(&mut self, field_cfg: &F::Config) { + if let Some(last) = self.weights.last_mut() { + *last = F::zero_with_cfg(field_cfg); + } + } + + pub fn as_slice(&self) -> &[F] { + &self.weights + } + + pub fn len(&self) -> usize { + self.weights.len() + } + + pub fn is_empty(&self) -> bool { + self.weights.is_empty() + } +} + +/// DMR-backed bit buckets for one small-value binary-polynomial column. +#[derive(Clone, Debug)] +pub struct SmallValueBitAccumulator { + buckets: [Uint<5>; D], + _field: PhantomData, +} + +impl Default for SmallValueBitAccumulator { + fn default() -> Self { + Self::new() + } +} + +impl SmallValueBitAccumulator { + pub fn new() -> Self { + Self { + buckets: [Uint::zero(); D], + _field: PhantomData, + } + } + + pub fn buckets(&self) -> &[Uint<5>] { + &self.buckets + } +} + +impl SmallValueBitAccumulator +where + F: MontgomeryLimbs + Send + Sync, +{ + pub fn add_bit_weight(&mut self, bit_idx: usize, weight: &F) -> Result<(), AccumulatorError> { + let Some(bucket) = self.buckets.get_mut(bit_idx) else { + return Err(AccumulatorError::BitIndexOutOfRange { bit_idx, degree: D }); + }; + as DelayedModularReduction>::add(bucket, weight); + Ok(()) + } + + #[allow(clippy::arithmetic_side_effects)] + pub fn add_binary_poly( + &mut self, + poly: &BinaryPoly, + weight: &F, + ) -> Result<(), AccumulatorError> { + if D <= 64 { + let mut bits = 0u64; + for (bit_idx, coeff) in poly.iter().enumerate().take(D) { + if coeff.into_inner() { + bits |= 1u64 << bit_idx; + } + } + + while bits != 0 { + let bit_idx = + usize::try_from(bits.trailing_zeros()).expect("trailing_zeros fits usize"); + self.add_bit_weight(bit_idx, weight)?; + bits &= bits - 1; + } + } else { + for (bit_idx, coeff) in poly.iter().enumerate().take(D) { + if coeff.into_inner() { + self.add_bit_weight(bit_idx, weight)?; + } + } + } + Ok(()) + } + + pub fn reduce_buckets( + self, + field_cfg: &F::Config, + reduction_params: &zinc_utils::delayed_reduction::BarrettReductionParams, + ) -> Vec { + self.buckets + .into_iter() + .map(|bucket| { + as DelayedModularReduction>::reduce(bucket, field_cfg, reduction_params) + }) + .collect() + } +} + +impl SmallValueBitAccumulator +where + F: MontgomeryLimbs + DelayedFieldProductSum + Send + Sync, +{ + pub fn project( + self, + projection_powers: &[F], + field_cfg: &F::Config, + reduction_params: &zinc_utils::delayed_reduction::BarrettReductionParams, + ) -> Result { + if projection_powers.len() < D { + return Err(AccumulatorError::ProjectionPowersLengthMismatch { + got: projection_powers.len(), + expected: D, + }); + } + + let zero = F::zero_with_cfg(field_cfg); + let bucket_evals = self.reduce_buckets(field_cfg, reduction_params); + Ok(FieldFieldInnerProduct::inner_product::( + &bucket_evals, + &projection_powers[..D], + zero, + ) + .expect("bucket and projection-power lengths match")) + } +} + +/// Accumulate one binary-polynomial column projected by `projecting_element`. +/// +/// This computes the bucket-first form: +/// first `S_j = sum_z bit_j(col[z]) * row_weight[z]`, then +/// `sum_j S_j * projecting_element^j`. +pub fn accumulate_binary_column_projected( + column: &DenseMultilinearExtension>, + row_weights: &RowWeights, + projecting_element: &F, + field_cfg: &F::Config, +) -> Result +where + F: MontgomeryLimbs + DelayedFieldProductSum + Send + Sync, +{ + if column.evaluations.len() != row_weights.len() { + return Err(AccumulatorError::RowWeightLengthMismatch { + weights: row_weights.len(), + rows: column.evaluations.len(), + }); + } + + let one = F::one_with_cfg(field_cfg); + let projection_powers: Vec = powers(projecting_element.clone(), one, D); + let reduction_params = F::barrett_reduction_params(field_cfg); + let mut accumulator = SmallValueBitAccumulator::::new(); + + for (poly, weight) in column.iter().zip(row_weights.as_slice()) { + accumulator.add_binary_poly(poly, weight)?; + } + + accumulator.project(&projection_powers, field_cfg, &reduction_params) +} + +#[cfg(test)] +mod tests { + use super::*; + use crypto_bigint::{Odd, modular::MontyParams}; + use crypto_primitives::{FromWithConfig, crypto_bigint_monty::MontyField}; + use zinc_utils::powers; + + type F = MontyField<4>; + + fn field_cfg() -> MontyParams<4> { + let modulus = crypto_bigint::Uint::<4>::from_words([ + 0xFFFF_FFFE_FFFF_FC2F, + 0xFFFF_FFFF_FFFF_FFFF, + 0xFFFF_FFFF_FFFF_FFFF, + 0xFFFF_FFFF_FFFF_FFFF, + ]); + MontyParams::new(Odd::new(modulus).expect("secp256k1 modulus is odd")) + } + + fn f(value: u64, cfg: &MontyParams<4>) -> F { + F::from_with_cfg(value, cfg) + } + + fn binary_col(patterns: &[u32]) -> DenseMultilinearExtension> { + DenseMultilinearExtension::from_evaluations_vec( + usize::try_from(patterns.len().next_power_of_two().trailing_zeros()) + .expect("trailing_zeros fits usize"), + patterns + .iter() + .copied() + .map(BinaryPoly::<32>::from) + .collect(), + BinaryPoly::<32>::zero(), + ) + } + + #[allow(clippy::arithmetic_side_effects)] + fn naive_projected_sum( + column: &DenseMultilinearExtension>, + row_weights: &RowWeights, + projecting_element: &F, + ) -> F { + let cfg = field_cfg(); + let zero = F::zero_with_cfg(&cfg); + let one = F::one_with_cfg(&cfg); + let powers = powers(projecting_element.clone(), one, 32); + + column + .iter() + .zip(row_weights.as_slice()) + .fold(zero, |mut acc, (poly, row_weight)| { + for (bit_idx, coeff) in poly.iter().enumerate().take(32) { + if coeff.into_inner() { + acc += row_weight.clone() * &powers[bit_idx]; + } + } + acc + }) + } + + #[test] + fn projected_binary_column_matches_naive_row_space_sum() { + let cfg = field_cfg(); + let point = vec![f(3, &cfg), f(5, &cfg), f(7, &cfg)]; + let row_weights = RowWeights::new(&point, &cfg).unwrap(); + let column = binary_col(&[ + 0x0000_0001, + 0x8000_0001, + 0x0f0f_00f0, + 0xf000_00ff, + 0x0101_0101, + 0x1111_2222, + 0xdead_beef, + 0xffff_0000, + ]); + let projecting_element = f(11, &cfg); + + let got = + accumulate_binary_column_projected(&column, &row_weights, &projecting_element, &cfg) + .unwrap(); + let expected = naive_projected_sum(&column, &row_weights, &projecting_element); + + assert_eq!(got, expected); + } + + #[test] + fn last_row_zero_helper_matches_manual_zeroing() { + let cfg = field_cfg(); + let point = vec![f(3, &cfg), f(5, &cfg)]; + + let mut manual = RowWeights::new(&point, &cfg).unwrap(); + manual.zero_last_row(&cfg); + let helper = RowWeights::new_with_last_row_zero(&point, &cfg).unwrap(); + + assert_eq!(helper, manual); + assert_eq!(helper.as_slice().last().unwrap(), &F::zero_with_cfg(&cfg)); + } + + #[test] + fn projected_binary_column_rejects_row_weight_mismatch() { + let cfg = field_cfg(); + let row_weights = RowWeights::new(&[f(3, &cfg), f(5, &cfg)], &cfg).unwrap(); + let column = binary_col(&[1, 2, 3, 4, 5, 6, 7, 8]); + + let err = accumulate_binary_column_projected(&column, &row_weights, &f(11, &cfg), &cfg) + .expect_err("mismatched row weights should be rejected"); + + assert!(matches!( + err, + AccumulatorError::RowWeightLengthMismatch { + weights: 4, + rows: 8 + } + )); + } +} diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs new file mode 100644 index 00000000..8caed9f1 --- /dev/null +++ b/piop/src/neutron_nova/mod.rs @@ -0,0 +1,13 @@ +//! NeutronNova small-value accumulation helpers. +//! +//! This module is intentionally standalone for now: it provides the linear +//! row-space accumulator and SumFold prefix-table primitives without changing +//! protocol proof objects or verifier flow. + +pub mod accumulator; +pub mod sumfold; + +pub use accumulator::{ + AccumulatorError, RowWeights, SmallValueBitAccumulator, accumulate_binary_column_projected, +}; +pub use sumfold::{LinearInstanceClaims, LinearPrefixTable, SumFoldError}; diff --git a/piop/src/neutron_nova/sumfold.rs b/piop/src/neutron_nova/sumfold.rs new file mode 100644 index 00000000..2f517cca --- /dev/null +++ b/piop/src/neutron_nova/sumfold.rs @@ -0,0 +1,375 @@ +use crypto_primitives::PrimeField; +use thiserror::Error; +use zinc_poly::{ + mle::DenseMultilinearExtension, + utils::{ArithErrors, build_eq_x_r_inner, build_eq_x_r_vec}, +}; + +use crate::sumcheck::multi_degree::MultiDegreeSumcheckGroup; + +/// Errors produced by linear SumFold prefix-table helpers. +#[derive(Clone, Debug, Error)] +pub enum SumFoldError { + #[error("linear instance claims cannot be empty")] + EmptyClaims, + #[error("instance count must be a power of two, got {len}")] + InstanceCountNotPowerOfTwo { len: usize }, + #[error("instance count mismatch for ell={ell}: got {got}, expected {expected}")] + InstanceCountMismatch { + ell: usize, + got: usize, + expected: usize, + }, + #[error("domain size is too large for ell={ell}")] + DomainTooLarge { ell: usize }, + #[error("ell0={ell0} must be at most ell={ell}")] + Ell0TooLarge { ell0: usize, ell: usize }, + #[error("beta length mismatch: got {got}, expected {expected}")] + BetaLengthMismatch { got: usize, expected: usize }, + #[error("sumcheck group construction requires ell0 > 0")] + SumcheckNeedsNonzeroEll0, + #[error("equality table construction failed: {0}")] + EqTable(#[from] ArithErrors), +} + +/// Dense per-instance linear claims. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinearInstanceClaims { + claims: Vec, + ell: usize, +} + +impl LinearInstanceClaims { + pub fn new(claims: Vec) -> Result { + if claims.is_empty() { + return Err(SumFoldError::EmptyClaims); + } + if !claims.len().is_power_of_two() { + return Err(SumFoldError::InstanceCountNotPowerOfTwo { len: claims.len() }); + } + + let ell = + usize::try_from(claims.len().trailing_zeros()).expect("trailing_zeros fits usize"); + Ok(Self { claims, ell }) + } + + pub fn from_claims_for_ell(claims: Vec, ell: usize) -> Result { + let expected = checked_domain_size(ell)?; + if claims.len() != expected { + return Err(SumFoldError::InstanceCountMismatch { + ell, + got: claims.len(), + expected, + }); + } + Self::new(claims) + } + + pub fn claims(&self) -> &[F] { + &self.claims + } + + pub fn ell(&self) -> usize { + self.ell + } + + pub fn len(&self) -> usize { + self.claims.len() + } + + pub fn is_empty(&self) -> bool { + self.claims.is_empty() + } +} + +/// Prefix table over the first `ell0` instance variables. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinearPrefixTable { + values: Vec, + ell: usize, + ell0: usize, +} + +impl LinearPrefixTable { + #[allow(clippy::arithmetic_side_effects)] + pub fn build( + instance_claims: &LinearInstanceClaims, + beta: &[F], + ell0: usize, + field_cfg: &F::Config, + ) -> Result { + let ell = instance_claims.ell(); + if ell0 > ell { + return Err(SumFoldError::Ell0TooLarge { ell0, ell }); + } + if beta.len() != ell { + return Err(SumFoldError::BetaLengthMismatch { + got: beta.len(), + expected: ell, + }); + } + + let prefix_len = checked_domain_size(ell0)?; + let tail_vars = ell - ell0; + let tail_len = checked_domain_size(tail_vars)?; + let tail_weights = if tail_vars == 0 { + vec![F::one_with_cfg(field_cfg)] + } else { + build_eq_x_r_vec(&beta[ell0..], field_cfg)? + }; + + debug_assert_eq!(tail_weights.len(), tail_len); + let mut values = vec![F::zero_with_cfg(field_cfg); prefix_len]; + for (tail_weight, claims_chunk) in tail_weights + .iter() + .zip(instance_claims.claims().chunks_exact(prefix_len)) + { + for (value, claim) in values.iter_mut().zip(claims_chunk) { + *value += tail_weight.clone() * claim; + } + } + + Ok(Self { values, ell, ell0 }) + } + + pub fn values(&self) -> &[F] { + &self.values + } + + pub fn ell(&self) -> usize { + self.ell + } + + pub fn ell0(&self) -> usize { + self.ell0 + } + + pub fn len(&self) -> usize { + self.values.len() + } + + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + pub fn to_mle(&self, field_cfg: &F::Config) -> DenseMultilinearExtension { + let zero_inner = F::zero_with_cfg(field_cfg).inner().clone(); + DenseMultilinearExtension::from_evaluations_vec( + self.ell0, + self.values + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner, + ) + } +} + +impl LinearPrefixTable +where + F: PrimeField + 'static, + F::Inner: num_traits::Zero, +{ + pub fn build_sumcheck_group( + &self, + beta_prefix: &[F], + field_cfg: &F::Config, + ) -> Result, SumFoldError> { + if self.ell0 == 0 { + return Err(SumFoldError::SumcheckNeedsNonzeroEll0); + } + if beta_prefix.len() != self.ell0 { + return Err(SumFoldError::BetaLengthMismatch { + got: beta_prefix.len(), + expected: self.ell0, + }); + } + + let eq_beta_prefix = build_eq_x_r_inner(beta_prefix, field_cfg)?; + let table = self.to_mle(field_cfg); + + Ok(MultiDegreeSumcheckGroup::new( + 2, + vec![eq_beta_prefix, table], + Box::new(|values: &[F]| values[0].clone() * &values[1]), + )) + } +} + +fn checked_domain_size(ell: usize) -> Result { + let shift = u32::try_from(ell).map_err(|_| SumFoldError::DomainTooLarge { ell })?; + 1usize + .checked_shl(shift) + .ok_or(SumFoldError::DomainTooLarge { ell }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{sumcheck::multi_degree::MultiDegreeSumcheck, test_utils::test_config}; + use crypto_primitives::{FromWithConfig, crypto_bigint_monty::MontyField}; + use zinc_poly::{ + mle::MultilinearExtensionWithConfig, + utils::{build_eq_x_r_vec, eq_eval}, + }; + use zinc_transcript::Blake3Transcript; + + type F = MontyField<4>; + + fn f(value: u64) -> F { + F::from_with_cfg(value, &test_config()) + } + + fn claims_for_ell(ell: usize) -> LinearInstanceClaims { + let claims = (0..(1usize << ell)) + .map(|idx| { + let idx = u64::try_from(idx).expect("test index fits u64"); + f(idx + 2) + }) + .collect(); + LinearInstanceClaims::from_claims_for_ell(claims, ell).unwrap() + } + + #[allow(clippy::arithmetic_side_effects)] + fn direct_prefix_value( + claims: &LinearInstanceClaims, + beta: &[F], + ell0: usize, + prefix: usize, + ) -> F { + let cfg = test_config(); + let ell = claims.ell(); + let tail_vars = ell - ell0; + let tail_weights = if tail_vars == 0 { + vec![F::one_with_cfg(&cfg)] + } else { + build_eq_x_r_vec(&beta[ell0..], &cfg).unwrap() + }; + + let mut acc = F::zero_with_cfg(&cfg); + for (tail, weight) in tail_weights.iter().enumerate() { + let idx = prefix + (tail << ell0); + acc += weight.clone() * &claims.claims()[idx]; + } + acc + } + + #[allow(clippy::arithmetic_side_effects)] + fn direct_full_beta_sum(claims: &LinearInstanceClaims, beta: &[F]) -> F { + let cfg = test_config(); + let weights = build_eq_x_r_vec(beta, &cfg).unwrap(); + let mut acc = F::zero_with_cfg(&cfg); + for (weight, claim) in weights.iter().zip(claims.claims()) { + acc += weight.clone() * claim; + } + acc + } + + #[test] + fn prefix_table_matches_direct_tail_fold_for_all_ell0_cases() { + let cfg = test_config(); + let claims = claims_for_ell(3); + let beta = vec![f(3), f(5), f(7)]; + + for ell0 in 0..=3 { + let table = LinearPrefixTable::build(&claims, &beta, ell0, &cfg).unwrap(); + assert_eq!(table.ell(), 3); + assert_eq!(table.ell0(), ell0); + assert_eq!(table.len(), 1usize << ell0); + + for prefix in 0..table.len() { + assert_eq!( + table.values()[prefix], + direct_prefix_value(&claims, &beta, ell0, prefix) + ); + } + } + } + + #[test] + fn linear_sumfold_group_proves_weighted_instance_sum() { + let cfg = test_config(); + let claims = claims_for_ell(3); + let beta = vec![f(3), f(5), f(7)]; + let ell0 = 2; + let table = LinearPrefixTable::build(&claims, &beta, ell0, &cfg).unwrap(); + + let group = table + .build_sumcheck_group(&beta[..ell0], &cfg) + .expect("ell0 > 0 should build a group"); + let mut prover_transcript = Blake3Transcript::new(); + let (proof, _states) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut prover_transcript, + vec![group], + ell0, + &cfg, + ); + + assert_eq!( + proof.claimed_sums()[0], + direct_full_beta_sum(&claims, &beta) + ); + + let mut verifier_transcript = Blake3Transcript::new(); + let subclaims = MultiDegreeSumcheck::verify_as_subprotocol( + &mut verifier_transcript, + ell0, + &proof, + &cfg, + ) + .expect("linear sumfold group should verify"); + + let point = subclaims.point(); + let eq_at_point = + eq_eval(point, &beta[..ell0], F::one_with_cfg(&cfg)).expect("same length"); + let table_eval = table + .to_mle(&cfg) + .evaluate_with_config(point, &cfg) + .unwrap(); + assert_eq!( + subclaims.expected_evaluations()[0], + eq_at_point * table_eval + ); + } + + #[test] + fn linear_sumfold_validation_errors_are_reported() { + let cfg = test_config(); + + assert!(matches!( + LinearInstanceClaims::::new(Vec::new()), + Err(SumFoldError::EmptyClaims) + )); + assert!(matches!( + LinearInstanceClaims::new(vec![f(1), f(2), f(3)]), + Err(SumFoldError::InstanceCountNotPowerOfTwo { len: 3 }) + )); + assert!(matches!( + LinearInstanceClaims::from_claims_for_ell(vec![f(1), f(2), f(3)], 2), + Err(SumFoldError::InstanceCountMismatch { + ell: 2, + got: 3, + expected: 4 + }) + )); + + let claims = claims_for_ell(2); + assert!(matches!( + LinearPrefixTable::build(&claims, &[f(3), f(5)], 3, &cfg), + Err(SumFoldError::Ell0TooLarge { ell0: 3, ell: 2 }) + )); + assert!(matches!( + LinearPrefixTable::build(&claims, &[f(3)], 1, &cfg), + Err(SumFoldError::BetaLengthMismatch { + got: 1, + expected: 2 + }) + )); + + let table = LinearPrefixTable::build(&claims, &[f(3), f(5)], 0, &cfg).unwrap(); + assert!(matches!( + table.build_sumcheck_group(&[], &cfg), + Err(SumFoldError::SumcheckNeedsNonzeroEll0) + )); + } +} diff --git a/utils/src/field/boxed_monty.rs b/utils/src/field/boxed_monty.rs index a1a7296d..3032120d 100644 --- a/utils/src/field/boxed_monty.rs +++ b/utils/src/field/boxed_monty.rs @@ -1,11 +1,12 @@ -use crypto_bigint::BoxedUint; +use crypto_bigint::{BoxedUint, modular::BoxedMontyForm}; use crypto_primitives::{ FromWithConfig, IntoWithConfig, PrimeField, crypto_bigint_boxed_monty::BoxedMontyField, crypto_bigint_uint::Uint, }; use crate::{ - from_ref::FromRef, mul_by_scalar::MulByScalar, projectable_to_field::ProjectableToField, + delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, mul_by_scalar::MulByScalar, + projectable_to_field::ProjectableToField, }; impl MulByScalar<&Self> for BoxedMontyField { @@ -22,6 +23,33 @@ impl FromRef for BoxedMontyField { } } +impl DelayedFieldProductSum for BoxedMontyField { + #[allow(clippy::arithmetic_side_effects)] + fn delayed_sum_of_products(lhs: &[Self], rhs: &[Self], zero: Self) -> Self { + if lhs.is_empty() { + return zero; + } + + let leading_zeros = zero.cfg().modulus().as_ref().leading_zeros(); + if lhs.len() == 1 || leading_zeros == 0 { + return lhs + .iter() + .zip(rhs) + .fold(zero, |acc, (left, right)| acc + left.clone() * right); + } + + let forms: Vec<(BoxedMontyForm, BoxedMontyForm)> = lhs + .iter() + .zip(rhs) + .map(|(left, right)| (left.clone().into(), right.clone().into())) + .collect(); + let products: Vec<(&BoxedMontyForm, &BoxedMontyForm)> = + forms.iter().map(|(left, right)| (left, right)).collect(); + + Self::from(BoxedMontyForm::lincomb_vartime(&products)) + zero + } +} + impl FromRef> for BoxedUint { #[inline] fn from_ref(value: &Uint) -> Self { @@ -48,6 +76,7 @@ where clippy::cast_possible_wrap )] mod prop_tests { + use crate::delayed_reduction::DelayedFieldProductSum; use crypto_bigint::{BoxedUint, U256}; use crypto_primitives::{ FromWithConfig, IntoWithConfig, PrimeField, crypto_bigint_boxed_monty::BoxedMontyField, @@ -73,6 +102,39 @@ mod prop_tests { any::() } + #[test] + fn delayed_sum_of_products_matches_naive() { + let cfg = get_dyn_config(MODULUS); + let seed = F::from_with_cfg(99u64, &cfg); + let empty = ::delayed_sum_of_products(&[], &[], seed.clone()); + assert_eq!(empty, seed); + + let single_lhs = [F::from_with_cfg(17u64, &cfg)]; + let single_rhs = [F::from_with_cfg(23u64, &cfg)]; + let got = ::delayed_sum_of_products( + &single_lhs, + &single_rhs, + seed.clone(), + ); + assert_eq!(got, seed.clone() + single_lhs[0].clone() * &single_rhs[0]); + + let lhs: Vec = (0..128) + .map(|idx| F::from_with_cfg(idx + 3, &cfg)) + .collect(); + let rhs: Vec = (0..128) + .map(|idx| F::from_with_cfg(257 - idx, &cfg)) + .collect(); + let expected = lhs + .iter() + .zip(&rhs) + .fold(seed.clone(), |acc, (left, right)| { + acc + left.clone() * right + }); + let got = ::delayed_sum_of_products(&lhs, &rhs, seed); + + assert_eq!(got, expected); + } + proptest! { #[test] fn prop_from_unsigned_matches_sum_of_bits(x in any_u128()) { diff --git a/zip-plus/src/pcs/test_utils.rs b/zip-plus/src/pcs/test_utils.rs index ef27b714..012de393 100644 --- a/zip-plus/src/pcs/test_utils.rs +++ b/zip-plus/src/pcs/test_utils.rs @@ -31,6 +31,7 @@ use zinc_primality::MillerRabin; use zinc_transcript::traits::{Transcribable, Transcript}; use zinc_utils::{ CHECKED, + delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, inner_product::{MBSInnerProduct, ScalarProduct}, mul_by_scalar::MulByScalar, @@ -168,6 +169,7 @@ where + for<'a> FromWithConfig<&'a as ZipTypes>::Chal> + for<'a> FromWithConfig<&'a as ZipTypes>::CombR> + for<'a> MulByScalar<&'a F> + + DelayedFieldProductSum + FromRef, F::Inner: Transcribable, F::Modulus: FromRef< as ZipTypes>::Fmod> + Transcribable, @@ -201,6 +203,7 @@ where + for<'a> FromWithConfig<&'a as ZipTypes>::Chal> + for<'a> FromWithConfig<&'a as ZipTypes>::CombR> + for<'a> MulByScalar<&'a F> + + DelayedFieldProductSum + FromRef + 'static, F::Inner: Transcribable, @@ -231,6 +234,7 @@ where + for<'a> FromWithConfig<&'a Zt::Chal> + for<'a> FromWithConfig<&'a Zt::Pt> + for<'a> MulByScalar<&'a F> + + DelayedFieldProductSum + FromRef, F::Inner: Transcribable, F::Modulus: FromRef + Transcribable, From 032f73b29ebe796f79099c9f7e3104ffa852748e Mon Sep 17 00:00:00 2001 From: John Wu Date: Fri, 5 Jun 2026 15:47:06 -0700 Subject: [PATCH 10/49] Optimize Hyrax binary openings --- zip-plus/src/pcs/generic.rs | 34 ++- zip-plus/src/pcs/hyrax.rs | 409 ++++++++++++++++++++++++++++-------- 2 files changed, 351 insertions(+), 92 deletions(-) diff --git a/zip-plus/src/pcs/generic.rs b/zip-plus/src/pcs/generic.rs index 234cbbdd..958c1da3 100644 --- a/zip-plus/src/pcs/generic.rs +++ b/zip-plus/src/pcs/generic.rs @@ -101,6 +101,7 @@ where fn absorb_commitment(transcript: &mut T, commitment: &Self::Commitment) { transcript.absorb_slice(&commitment.root); + transcript.absorb_slice(&(commitment.batch_size as u64).to_le_bytes()); } fn commitment_num_bytes(commitment: &Self::Commitment) -> usize { @@ -129,10 +130,23 @@ where F::Inner: Transcribable, F::Modulus: Transcribable, { - if let Some(hint) = prover_data { - let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( - transcript, ck, polys, point, hint, field_cfg, - )?; + match (polys.is_empty(), prover_data) { + (true, None) => {} + (true, Some(_)) => { + return Err(ZipError::InvalidPcsParam( + "Zip+ prover data must be empty for an empty batch".to_string(), + )); + } + (false, None) => { + return Err(ZipError::InvalidPcsParam( + "Zip+ prover data missing for non-empty batch".to_string(), + )); + } + (false, Some(hint)) => { + let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( + transcript, ck, polys, point, hint, field_cfg, + )?; + } } Ok(()) } @@ -149,7 +163,19 @@ where F::Inner: Transcribable, F::Modulus: Transcribable, { + if lifted_evals.len() != commitment.batch_size { + return Err(ZipError::InvalidPcsParam(format!( + "Zip+ verifier expected {} lifted evals, got {}", + commitment.batch_size, + lifted_evals.len() + ))); + } if commitment.batch_size == 0 { + if commitment.root != Default::default() { + return Err(ZipError::InvalidPcsParam( + "Zip+ empty batch must use the canonical empty commitment".to_string(), + )); + } return Ok(()); } diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index 5fecbf81..d29a825a 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -6,7 +6,7 @@ use std::{ marker::PhantomData, }; -use ark_ec::{AffineRepr, CurveGroup}; +use ark_ec::{AffineRepr, CurveGroup, VariableBaseMSM}; use ark_ff::{BigInteger, PrimeField as ArkPrimeField, Zero}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress}; use crypto_primitives::{ @@ -90,30 +90,30 @@ pub struct HyraxProverData { } pub trait HyraxFieldBridge: PrimeField { - fn field_to_scalar(value: &Self) -> C::ScalarField; - fn scalar_to_field(value: &C::ScalarField, cfg: &Self::Config) -> Self; + fn field_to_scalar(value: &Self) -> Result; + fn scalar_to_field(value: &C::ScalarField, cfg: &Self::Config) -> Result; } impl HyraxFieldBridge for MontyField where C: AffineRepr, { - fn field_to_scalar(value: &Self) -> C::ScalarField { - assert_curve_scalar_modulus::(&value.modulus()); + fn field_to_scalar(value: &Self) -> Result { + validate_curve_scalar_modulus::(&value.modulus())?; let canonical = value.retrieve(); let mut bytes = vec![0u8; as ConstTranscribable>::NUM_BYTES]; canonical.write_transcription_bytes_exact(&mut bytes); - C::ScalarField::from_le_bytes_mod_order(&bytes) + Ok(C::ScalarField::from_le_bytes_mod_order(&bytes)) } - fn scalar_to_field(value: &C::ScalarField, cfg: &Self::Config) -> Self { + fn scalar_to_field(value: &C::ScalarField, cfg: &Self::Config) -> Result { let actual_modulus = Uint::::new(cfg.modulus().get()); - assert_curve_scalar_modulus::(&actual_modulus); + validate_curve_scalar_modulus::(&actual_modulus)?; let scalar_bigint: ::BigInt = value.clone().into(); let scalar_uint = uint_from_le_bytes::(&scalar_bigint.to_bytes_le()); - MontyField::::from_with_cfg(&scalar_uint, cfg) + Ok(MontyField::::from_with_cfg(&scalar_uint, cfg)) } } @@ -131,6 +131,53 @@ where fn lane_to_scalar(value: Self::LaneValue) -> C::ScalarField; + fn accumulate_b( + row: &[Eval], + lane: usize, + q1_scalar: &[C::ScalarField], + ) -> Result { + let mut row_eval = C::ScalarField::zero(); + for (col_idx, eval) in row.iter().enumerate() { + let value = Self::lane_to_scalar(Self::lane_value(eval, lane)?); + if let Some(weight) = q1_scalar.get(col_idx) { + row_eval += value * weight; + } + } + Ok(row_eval) + } + + fn accumulate_combined_row( + row: &[Eval], + lane: usize, + coeff: C::ScalarField, + combined_row: &mut [C::ScalarField], + ) -> Result<(), ZipError> { + for (col_idx, eval) in row.iter().enumerate() { + let value = Self::lane_to_scalar(Self::lane_value(eval, lane)?); + combined_row[col_idx] += coeff * value; + } + Ok(()) + } + + fn accumulate_single_row_opening( + row: &[Eval], + lane: usize, + alpha: C::ScalarField, + q1_scalar: &[C::ScalarField], + b_scalar: &mut C::ScalarField, + combined_row: &mut [C::ScalarField], + ) -> Result<(), ZipError> { + for (col_idx, eval) in row.iter().enumerate() { + let value = Self::lane_to_scalar(Self::lane_value(eval, lane)?); + let scaled = alpha * value; + if let Some(weight) = q1_scalar.get(col_idx) { + *b_scalar += scaled * weight; + } + combined_row[col_idx] += scaled; + } + Ok(()) + } + fn lifted_eval( lifted_eval: &DynamicPolynomialF, lane: usize, @@ -170,6 +217,57 @@ impl HyraxLanes, D> for BinaryLa } } + fn accumulate_b( + row: &[BinaryPoly], + lane: usize, + q1_scalar: &[C::ScalarField], + ) -> Result { + let mut row_eval = C::ScalarField::zero(); + for (col_idx, eval) in row.iter().enumerate() { + if , D>>::lane_value(eval, lane)? { + if let Some(weight) = q1_scalar.get(col_idx) { + row_eval += weight; + } + } + } + Ok(row_eval) + } + + fn accumulate_combined_row( + row: &[BinaryPoly], + lane: usize, + coeff: C::ScalarField, + combined_row: &mut [C::ScalarField], + ) -> Result<(), ZipError> { + for (col_idx, eval) in row.iter().enumerate() { + if , D>>::lane_value(eval, lane)? { + combined_row[col_idx] += coeff; + } + } + Ok(()) + } + + fn accumulate_single_row_opening( + row: &[BinaryPoly], + lane: usize, + alpha: C::ScalarField, + q1_scalar: &[C::ScalarField], + b_scalar: &mut C::ScalarField, + combined_row: &mut [C::ScalarField], + ) -> Result<(), ZipError> { + let mut row_eval = C::ScalarField::zero(); + for (col_idx, eval) in row.iter().enumerate() { + if , D>>::lane_value(eval, lane)? { + if let Some(weight) = q1_scalar.get(col_idx) { + row_eval += weight; + } + combined_row[col_idx] += alpha; + } + } + *b_scalar += alpha * row_eval; + Ok(()) + } + fn lifted_eval( lifted_eval: &DynamicPolynomialF, lane: usize, @@ -438,6 +536,16 @@ where { let _ = CHECK_FOR_OVERFLOW; if polys.is_empty() { + if prover_data.batch_size != 0 + || prover_data.num_lanes != Lanes::NUM_LANES + || prover_data.num_rows != 0 + || prover_data.blinding_mode != ck.blinding_mode + || !prover_data.blinds.is_empty() + { + return Err(ZipError::InvalidPcsParam( + "Hyrax prover data must be canonical for an empty batch".to_string(), + )); + } return Ok(()); } validate_polys(polys)?; @@ -456,7 +564,10 @@ where ))); } - let point_scalar = point.iter().map(F::field_to_scalar).collect::>(); + let point_scalar = point + .iter() + .map(F::field_to_scalar) + .collect::, _>>()?; let row_vars = prover_data.num_rows.ilog2() as usize; let q0_f = eq_tensor_f::(&point[..row_vars], field_cfg); let q1_scalar = eq_tensor_scalar::(&point_scalar[row_vars..]); @@ -465,66 +576,89 @@ where polys.len() * Lanes::NUM_LANES, ); - let mut b_scalar = vec![C::ScalarField::zero(); prover_data.num_rows]; - for (poly_idx, poly) in polys.iter().enumerate() { - for lane in 0..Lanes::NUM_LANES { - let alpha = alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; - for (row_idx, row) in poly.evaluations.chunks(ck.num_cols).enumerate() { - let mut row_eval = C::ScalarField::zero(); - for (col_idx, eval) in row.iter().enumerate() { - let value = Lanes::lane_to_scalar(Lanes::lane_value(eval, lane)?); - if let Some(weight) = q1_scalar.get(col_idx) { - row_eval += value * weight; - } - } - b_scalar[row_idx] += alpha * row_eval; - } - } - } - - let b_f = b_scalar - .iter() - .map(|value| F::scalar_to_field(value, field_cfg)) - .collect::>(); - transcript.write_field_elements(&b_f)?; - - let row_coeffs = if prover_data.num_rows == 1 { - vec![C::ScalarField::from(1u64)] - } else { - sample_scalars::(&mut transcript.fs_transcript, prover_data.num_rows) - }; - let mut combined_row = vec![C::ScalarField::zero(); ck.num_cols]; let mut rho_star = C::ScalarField::zero(); - for (poly_idx, poly) in polys.iter().enumerate() { - for lane in 0..Lanes::NUM_LANES { - let alpha = alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; - for (row_idx, row) in poly.evaluations.chunks(ck.num_cols).enumerate() { - let coeff = alpha * row_coeffs[row_idx]; + + let mut b_scalar = vec![C::ScalarField::zero(); prover_data.num_rows]; + if prover_data.num_rows == 1 { + for (poly_idx, poly) in polys.iter().enumerate() { + for lane in 0..Lanes::NUM_LANES { + let alpha = alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; if ck.blinding_mode.is_blinded() { let blind_idx = commitment_index_dynamic( Lanes::NUM_LANES, poly_idx, lane, - row_idx, + 0, prover_data.num_rows, ); - rho_star += coeff * prover_data.blinds[blind_idx]; + rho_star += alpha * prover_data.blinds[blind_idx]; + } + Lanes::accumulate_single_row_opening( + &poly.evaluations, + lane, + alpha, + &q1_scalar, + &mut b_scalar[0], + &mut combined_row, + )?; + } + } + } else { + for (poly_idx, poly) in polys.iter().enumerate() { + for lane in 0..Lanes::NUM_LANES { + let alpha = alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; + for (row_idx, row) in poly.evaluations.chunks(ck.num_cols).enumerate() { + let row_eval = Lanes::accumulate_b(row, lane, &q1_scalar)?; + b_scalar[row_idx] += alpha * row_eval; } - for (col_idx, eval) in row.iter().enumerate() { - let value = Lanes::lane_to_scalar(Lanes::lane_value(eval, lane)?); - combined_row[col_idx] += coeff * value; + } + } + + let b_f = b_scalar + .iter() + .map(|value| F::scalar_to_field(value, field_cfg)) + .collect::, _>>()?; + transcript.write_field_elements(&b_f)?; + + let row_coeffs = + sample_scalars::(&mut transcript.fs_transcript, prover_data.num_rows); + + for (poly_idx, poly) in polys.iter().enumerate() { + for lane in 0..Lanes::NUM_LANES { + let alpha = alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; + for (row_idx, row) in poly.evaluations.chunks(ck.num_cols).enumerate() { + let coeff = alpha * row_coeffs[row_idx]; + if ck.blinding_mode.is_blinded() { + let blind_idx = commitment_index_dynamic( + Lanes::NUM_LANES, + poly_idx, + lane, + row_idx, + prover_data.num_rows, + ); + rho_star += coeff * prover_data.blinds[blind_idx]; + } + Lanes::accumulate_combined_row(row, lane, coeff, &mut combined_row)?; } } } } + if prover_data.num_rows == 1 { + let b_f = b_scalar + .iter() + .map(|value| F::scalar_to_field(value, field_cfg)) + .collect::, _>>()?; + transcript.write_field_elements(&b_f)?; + } + write_scalars::(transcript, &combined_row)?; if ck.blinding_mode.is_blinded() { write_scalar::(transcript, &rho_star)?; } - if q0_f.len() != b_f.len() { + if q0_f.len() != b_scalar.len() { return Err(ZipError::InvalidPcsOpen( "Hyrax b vector shape mismatch".to_string(), )); @@ -546,9 +680,6 @@ where F::Modulus: Transcribable, { let _ = CHECK_FOR_OVERFLOW; - if commitment.batch_size == 0 { - return Ok(()); - } if commitment.blinding_mode != vk.blinding_mode { return Err(ZipError::InvalidPcsParam( "Hyrax commitment blinding mode mismatch".to_string(), @@ -562,6 +693,14 @@ where lifted_evals.len() ))); } + if commitment.batch_size == 0 { + if commitment.num_rows != 0 { + return Err(ZipError::InvalidPcsParam( + "Hyrax empty batch must use the canonical empty commitment".to_string(), + )); + } + return Ok(()); + } let n = 1usize << point.len(); let expected_rows = num_rows(n, vk.num_cols)?; @@ -574,7 +713,10 @@ where let row_vars = commitment.num_rows.ilog2() as usize; let q0_f = eq_tensor_f::(&point[..row_vars], field_cfg); - let point_scalar = point.iter().map(F::field_to_scalar).collect::>(); + let point_scalar = point + .iter() + .map(F::field_to_scalar) + .collect::, _>>()?; let q1_scalar = eq_tensor_scalar::(&point_scalar[row_vars..]); let alphas = sample_scalars::( &mut transcript.fs_transcript, @@ -594,7 +736,7 @@ where let alpha = F::scalar_to_field( &alphas[alpha_index_dynamic(commitment.num_lanes, poly_idx, lane)], field_cfg, - ); + )?; let mut term = Lanes::lifted_eval::(lifted_eval, lane, field_cfg)?; term *= α expected_eval += &term; @@ -613,7 +755,10 @@ where )); } - let b_scalar = b_f.iter().map(F::field_to_scalar).collect::>(); + let b_scalar = b_f + .iter() + .map(F::field_to_scalar) + .collect::, _>>()?; let row_coeffs = if commitment.num_rows == 1 { vec![C::ScalarField::from(1u64)] } else { @@ -641,29 +786,18 @@ where )); } - let mut comm_lc = C::Group::zero(); + let mut comm_lc_scalars = Vec::with_capacity(commitment.comm.len()); for poly_idx in 0..commitment.batch_size { for lane in 0..commitment.num_lanes { let alpha = alphas[alpha_index_dynamic(commitment.num_lanes, poly_idx, lane)]; - for (row_idx, row_coeff) in row_coeffs.iter().enumerate() { - let idx = commitment_index_dynamic( - commitment.num_lanes, - poly_idx, - lane, - row_idx, - commitment.num_rows, - ); - comm_lc += commitment.comm[idx] * (alpha * row_coeff); - } + comm_lc_scalars.extend(row_coeffs.iter().map(|row_coeff| alpha * row_coeff)); } } - let msm_ck = msm_key(vk.num_cols, vk.bases.clone(), vk.h)?; - let mut expected = >::msm_row( - &msm_ck, - &combined_row, - ) - .map_err(msm_err)?; + let comm_bases = C::Group::normalize_batch(&commitment.comm); + let comm_lc = msm_unchecked::(&comm_bases, &comm_lc_scalars)?; + + let mut expected = msm_unchecked::(&vk.bases[..combined_row.len()], &combined_row)?; if let Some(rho_star) = rho_star { expected += vk.h * rho_star; } @@ -791,16 +925,21 @@ fn unsigned_int_to_scalar(value: &Int) C::ScalarField::from_le_bytes_mod_order(&bytes) } -fn assert_curve_scalar_modulus(actual: &Uint) +fn validate_curve_scalar_modulus( + actual: &Uint, +) -> Result<(), ZipError> where C: AffineRepr, { let expected = uint_from_le_bytes::(&::MODULUS.to_bytes_le()); - assert_eq!( - actual, &expected, - "Hyrax field mismatch: protocol field modulus must equal curve scalar modulus", - ); + if actual != &expected { + return Err(ZipError::InvalidPcsParam( + "Hyrax field mismatch: protocol field modulus must equal curve scalar modulus" + .to_string(), + )); + } + Ok(()) } fn uint_from_le_bytes(bytes: &[u8]) -> Uint { @@ -824,6 +963,20 @@ fn msm_key( .map_err(msm_err) } +fn msm_unchecked( + bases: &[C], + scalars: &[C::ScalarField], +) -> Result { + if bases.len() != scalars.len() { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax MSM expected {} bases, got {}", + scalars.len(), + bases.len() + ))); + } + Ok(::msm_unchecked(bases, scalars)) +} + fn num_rows(n: usize, width: usize) -> Result { if width == 0 { return Err(ZipError::InvalidPcsParam( @@ -976,15 +1129,15 @@ mod tests { .expect("curve scalar modulus must be prime") } - fn assert_bridge_round_trip() { + fn assert_bridge_round_trip() -> Result<(), ZipError> { let cfg = cfg_from_curve::(); for value in [0u64, 1, 2, 17, 123, 1 << 20] { let field = MontyField::<4>::from_with_cfg(value, &cfg); - let scalar = as HyraxFieldBridge>::field_to_scalar(&field); + let scalar = as HyraxFieldBridge>::field_to_scalar(&field)?; assert_eq!(scalar, C::ScalarField::from(value)); let field_again = - as HyraxFieldBridge>::scalar_to_field(&scalar, &cfg); + as HyraxFieldBridge>::scalar_to_field(&scalar, &cfg)?; assert_eq!(field_again, field); } @@ -994,29 +1147,30 @@ mod tests { C::ScalarField::from_le_bytes_mod_order(&[0xA5; 64]), ]; for scalar in large_values { - let field = as HyraxFieldBridge>::scalar_to_field(&scalar, &cfg); - let scalar_again = as HyraxFieldBridge>::field_to_scalar(&field); + let field = as HyraxFieldBridge>::scalar_to_field(&scalar, &cfg)?; + let scalar_again = as HyraxFieldBridge>::field_to_scalar(&field)?; assert_eq!(scalar_again, scalar); } + Ok(()) } #[test] fn bridge_round_trips_bn254_scalar_field() { - assert_bridge_round_trip::(); + assert_bridge_round_trip::().unwrap(); } #[test] fn bridge_round_trips_secp256k1_scalar_field() { - assert_bridge_round_trip::(); + assert_bridge_round_trip::().unwrap(); } #[test] - #[should_panic(expected = "Hyrax field mismatch")] fn bridge_rejects_mismatched_field_config() { let bn_cfg = cfg_from_curve::(); let bn_field = MontyField::<4>::from_with_cfg(1u64, &bn_cfg); - let _ = + let result = as HyraxFieldBridge>::field_to_scalar(&bn_field); + assert!(matches!(result, Err(ZipError::InvalidPcsParam(_)))); } fn binary_hyrax_open_verify_round_trip_with_modes( @@ -1080,7 +1234,7 @@ mod tests { let scalar = ::ScalarField::from_le_bytes_mod_order(bytes); >::scalar_to_field(&scalar, &cfg) }) - .collect::>(); + .collect::, _>>()?; let eq = eq_tensor_f::(&point, &cfg); let lifted_evals = polys .iter() @@ -1167,4 +1321,83 @@ mod tests { ); assert!(result.is_err()); } + + #[test] + fn hyrax_rejects_empty_commitment_with_nonempty_lifted_evals() { + type C = ark_bn254::G1Affine; + type F = MontyField<4>; + const D: usize = 32; + + let width = 8; + let generator = ::Group::generator(); + let bases = (1..=width) + .map(|idx| (generator * ::ScalarField::from(idx as u64)).into_affine()) + .collect::>(); + let h = generator * ::ScalarField::from((width + 1) as u64); + let (_, vk) = HyraxPCS::::setup_from_bases(width, bases, h).unwrap(); + + let commitment = HyraxCommitment:: { + batch_size: 0, + num_lanes: D, + num_rows: 0, + blinding_mode: HyraxBlindingMode::Blinded, + comm: Vec::new(), + }; + let cfg = cfg_from_curve::(); + let lifted_evals = vec![DynamicPolynomialF::new_trimmed(vec![F::zero_with_cfg( + &cfg, + )])]; + let mut verifier_transcript = PcsVerifierTranscript { + fs_transcript: Default::default(), + stream: Default::default(), + }; + + let result = as PCS, D>>::verify_open::( + &mut verifier_transcript, + &vk, + &commitment, + &[], + &lifted_evals, + &cfg, + ); + assert!(matches!(result, Err(ZipError::InvalidPcsParam(_)))); + } + + #[test] + fn hyrax_rejects_noncanonical_empty_commitment() { + type C = ark_bn254::G1Affine; + type F = MontyField<4>; + const D: usize = 32; + + let width = 8; + let generator = ::Group::generator(); + let bases = (1..=width) + .map(|idx| (generator * ::ScalarField::from(idx as u64)).into_affine()) + .collect::>(); + let h = generator * ::ScalarField::from((width + 1) as u64); + let (_, vk) = HyraxPCS::::setup_from_bases(width, bases, h).unwrap(); + + let commitment = HyraxCommitment:: { + batch_size: 0, + num_lanes: D, + num_rows: 1, + blinding_mode: HyraxBlindingMode::Blinded, + comm: Vec::new(), + }; + let cfg = cfg_from_curve::(); + let mut verifier_transcript = PcsVerifierTranscript { + fs_transcript: Default::default(), + stream: Default::default(), + }; + + let result = as PCS, D>>::verify_open::( + &mut verifier_transcript, + &vk, + &commitment, + &[], + &[], + &cfg, + ); + assert!(matches!(result, Err(ZipError::InvalidPcsParam(_)))); + } } From 9bddfcd1e5db058945020058aec4bb3b75125363 Mon Sep 17 00:00:00 2001 From: John Wu Date: Fri, 5 Jun 2026 17:00:07 -0700 Subject: [PATCH 11/49] Add optimized linear CPR accumulator --- piop/src/neutron_nova/linear_cpr.rs | 968 ++++++++++++++++++++++++++++ piop/src/neutron_nova/mod.rs | 6 + piop/src/neutron_nova/sumfold.rs | 96 ++- 3 files changed, 1068 insertions(+), 2 deletions(-) create mode 100644 piop/src/neutron_nova/linear_cpr.rs diff --git a/piop/src/neutron_nova/linear_cpr.rs b/piop/src/neutron_nova/linear_cpr.rs new file mode 100644 index 00000000..0bcdf219 --- /dev/null +++ b/piop/src/neutron_nova/linear_cpr.rs @@ -0,0 +1,968 @@ +use crate::neutron_nova::{RowWeights, sumfold::checked_domain_size}; +use crate::sumcheck::multi_degree::MultiDegreeSumcheckGroup; +use crypto_primitives::{FromPrimitiveWithConfig, PrimeField, crypto_bigint_uint::Uint}; +use num_traits::Zero; +use std::array; +use thiserror::Error; +use zinc_poly::univariate::binary::BinaryPoly; +use zinc_uair::UairTrace; +use zinc_utils::{ + UNCHECKED, + delayed_reduction::{ + BarrettReductionParams, DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs, + }, + inner_product::{FieldFieldInnerProduct, InnerProduct}, +}; + +use super::{LinearPrefixTable, SumFoldError}; + +const PREFIX_TILE_SIZE: usize = 8; +const DMR_FLUSH_ADDS: usize = 1 << 20; + +/// Precomputed equality weights for the instance-axis SumFold split. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SumFoldEqWeights { + pub prefix_eq_weights: Vec, + pub tail_eq_weights: Vec, +} + +/// Precomputed multiplication weights used by the linear CPR accumulator. +#[derive(Clone, Copy, Debug)] +pub struct LinearCprWeights<'a, F: PrimeField> { + pub row_weights: &'a RowWeights, + pub tail_eq_weights: &'a [F], + pub prefix_eq_weights: &'a [F], +} + +/// Precomputed scalar weights applied after row/tail DMR reduction. +#[derive(Clone, Copy, Debug)] +pub struct LinearCprScalarWeights<'a, F: PrimeField> { + pub family_weights: &'a [F], + pub scalarization_powers: &'a [F], +} + +/// One linear CPR family described as small-coefficient binary source terms. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinearFamilySpec { + pub family_idx: usize, + pub active_rows: Vec, + pub terms: Vec>, +} + +/// One binary source term in a linear CPR family. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinearTermSpec { + pub source: LinearBinarySource, + pub coeffs_by_active_row: Vec>, +} + +/// Binary source read by a linear term. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LinearBinarySource { + Column { col_idx: usize }, + ShiftedColumn { col_idx: usize, shift: usize }, +} + +impl LinearBinarySource { + fn col_idx(&self) -> usize { + match self { + Self::Column { col_idx } | Self::ShiftedColumn { col_idx, .. } => *col_idx, + } + } +} + +/// Coefficient class for post-DMR bucket weighting. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CoeffClass { + Zero, + Small(i64), + Large(F), +} + +impl CoeffClass +where + F: FromPrimitiveWithConfig, +{ + fn is_zero(&self) -> bool { + matches!(self, Self::Zero | Self::Small(0)) + } + + fn to_field(&self, field_cfg: &F::Config) -> F { + match self { + Self::Zero => F::zero_with_cfg(field_cfg), + Self::Small(value) => F::from_with_cfg(*value, field_cfg), + Self::Large(value) => value.clone(), + } + } +} + +#[derive(Clone, Debug, Error)] +pub enum LinearCprAccumulatorError { + #[error("linear CPR accumulator needs at least one trace")] + EmptyTraces, + #[error("trace count must be a power of two, got {len}")] + TraceCountNotPowerOfTwo { len: usize }, + #[error("prefix_vars={prefix_vars} must be at most ell={ell}")] + PrefixVarsTooLarge { prefix_vars: usize, ell: usize }, + #[error("domain size is too large for {vars} variables")] + DomainTooLarge { vars: usize }, + #[error("{label} length mismatch: got {got}, expected {expected}")] + LengthMismatch { + label: &'static str, + got: usize, + expected: usize, + }, + #[error("trace {trace_idx} has {got} binary columns, expected {expected}")] + BinaryColumnCountMismatch { + trace_idx: usize, + got: usize, + expected: usize, + }, + #[error("trace {trace_idx} binary column {col_idx} has {got} rows, expected {expected}")] + BinaryColumnRowMismatch { + trace_idx: usize, + col_idx: usize, + got: usize, + expected: usize, + }, + #[error( + "family {family_idx} references family weight {weight_idx}, but only {len} weights exist" + )] + FamilyWeightOutOfRange { + family_idx: usize, + weight_idx: usize, + len: usize, + }, + #[error("family {family_idx} active row {row} is out of range for {rows} rows")] + ActiveRowOutOfRange { + family_idx: usize, + row: usize, + rows: usize, + }, + #[error("family {family_idx} term {term_idx} has {got} coefficients, expected {expected}")] + TermCoeffLengthMismatch { + family_idx: usize, + term_idx: usize, + got: usize, + expected: usize, + }, + #[error( + "family {family_idx} term {term_idx} references binary column {col_idx}, but only {cols} exist" + )] + SourceColumnOutOfRange { + family_idx: usize, + term_idx: usize, + col_idx: usize, + cols: usize, + }, + #[error("sumfold helper failed: {0}")] + SumFold(#[from] SumFoldError), + #[error("equality table construction failed: {0}")] + EqTable(#[from] zinc_poly::utils::ArithErrors), +} + +#[derive(Clone, Debug)] +struct PreparedFamily { + family_idx: usize, + active_rows: Vec, + coeff_classes: Vec>, + terms: Vec, +} + +#[derive(Clone, Debug)] +struct PreparedTerm { + source: LinearBinarySource, + coeff_indices_by_active_row: Vec>, +} + +/// Build prefix/tail equality weights for a SumFold instance-axis split. +pub fn build_sumfold_eq_weights( + beta: &[F], + prefix_vars: usize, + field_cfg: &F::Config, +) -> Result, LinearCprAccumulatorError> +where + F: PrimeField, +{ + let ell = beta.len(); + if prefix_vars > ell { + return Err(LinearCprAccumulatorError::PrefixVarsTooLarge { prefix_vars, ell }); + } + + let prefix_eq_weights = if prefix_vars == 0 { + vec![F::one_with_cfg(field_cfg)] + } else { + zinc_poly::utils::build_eq_x_r_vec(&beta[..prefix_vars], field_cfg)? + }; + let tail_vars = ell - prefix_vars; + let tail_eq_weights = if tail_vars == 0 { + vec![F::one_with_cfg(field_cfg)] + } else { + zinc_poly::utils::build_eq_x_r_vec(&beta[prefix_vars..], field_cfg)? + }; + + Ok(SumFoldEqWeights { + prefix_eq_weights, + tail_eq_weights, + }) +} + +/// Build the optimized linear CPR prefix table from small-value binary traces. +#[allow(clippy::arithmetic_side_effects, clippy::too_many_arguments)] +pub fn build_linear_cpr_prefix_table( + traces: &[UairTrace<'_, PolyCoeff, Int, D>], + prefix_vars: usize, + families: &[LinearFamilySpec], + weights: LinearCprWeights<'_, F>, + scalar_weights: LinearCprScalarWeights<'_, F>, + field_cfg: &F::Config, +) -> Result, LinearCprAccumulatorError> +where + F: MontgomeryLimbs + DelayedFieldProductSum + FromPrimitiveWithConfig + Send + Sync + 'static, + PolyCoeff: Clone, + Int: Clone, +{ + let ell = validate_trace_count(traces.len())?; + if prefix_vars > ell { + return Err(LinearCprAccumulatorError::PrefixVarsTooLarge { prefix_vars, ell }); + } + + let prefix_len = checked_domain_size(prefix_vars)?; + let tail_len = checked_domain_size(ell - prefix_vars)?; + if weights.tail_eq_weights.len() != tail_len { + return Err(LinearCprAccumulatorError::LengthMismatch { + label: "tail_eq_weights", + got: weights.tail_eq_weights.len(), + expected: tail_len, + }); + } + if weights.prefix_eq_weights.len() != prefix_len { + return Err(LinearCprAccumulatorError::LengthMismatch { + label: "prefix_eq_weights", + got: weights.prefix_eq_weights.len(), + expected: prefix_len, + }); + } + if scalar_weights.scalarization_powers.len() < D { + return Err(LinearCprAccumulatorError::LengthMismatch { + label: "scalarization_powers", + got: scalar_weights.scalarization_powers.len(), + expected: D, + }); + } + + let num_binary_cols = validate_traces(traces, weights.row_weights.len())?; + let prepared = prepare_families( + families, + scalar_weights.family_weights.len(), + num_binary_cols, + weights.row_weights.len(), + )?; + + let mut table_values = vec![F::zero_with_cfg(field_cfg); prefix_len]; + let reduction_params = F::barrett_reduction_params(field_cfg); + + for family in &prepared { + let family_weight = &scalar_weights.family_weights[family.family_idx]; + if family.coeff_classes.is_empty() { + continue; + } + + let mut tile_start = 0usize; + while tile_start < prefix_len { + let tile_len = PREFIX_TILE_SIZE.min(prefix_len - tile_start); + accumulate_family_tile::( + traces, + family, + tile_start, + tile_len, + prefix_vars, + tail_len, + &weights, + scalar_weights.scalarization_powers, + family_weight, + field_cfg, + &reduction_params, + &mut table_values, + )?; + tile_start += tile_len; + } + } + + LinearPrefixTable::from_values_for_prefix_vars(table_values, ell, prefix_vars) + .map_err(LinearCprAccumulatorError::from) +} + +#[allow(clippy::arithmetic_side_effects, clippy::too_many_arguments)] +fn accumulate_family_tile( + traces: &[UairTrace<'_, PolyCoeff, Int, D>], + family: &PreparedFamily, + tile_start: usize, + tile_len: usize, + prefix_vars: usize, + tail_len: usize, + weights: &LinearCprWeights<'_, F>, + scalarization_powers: &[F], + family_weight: &F, + field_cfg: &F::Config, + reduction_params: &BarrettReductionParams, + table_values: &mut [F], +) -> Result<(), LinearCprAccumulatorError> +where + F: MontgomeryLimbs + DelayedFieldProductSum + FromPrimitiveWithConfig + Send + Sync + 'static, + PolyCoeff: Clone, + Int: Clone, +{ + let bucket_count = tile_len + .checked_mul(family.coeff_classes.len()) + .ok_or(LinearCprAccumulatorError::DomainTooLarge { vars: prefix_vars })?; + let mut buckets: Vec<[Uint<5>; D]> = vec![[Uint::zero(); D]; bucket_count]; + let zero = F::zero_with_cfg(field_cfg); + let mut lane_accs: Vec<[F; D]> = (0..bucket_count) + .map(|_| array::from_fn(|_| zero.clone())) + .collect(); + let mut pending_adds = 0usize; + + for tail in 0..tail_len { + let tail_weight = &weights.tail_eq_weights[tail]; + for (active_pos, &row) in family.active_rows.iter().enumerate() { + let omega = tail_weight.clone() * &weights.row_weights.as_slice()[row]; + + for term in &family.terms { + let Some(coeff_idx) = term.coeff_indices_by_active_row[active_pos] else { + continue; + }; + + for prefix_offset in 0..tile_len { + let prefix = tile_start + prefix_offset; + let instance_idx = prefix + (tail << prefix_vars); + let Some(poly) = + source_poly::(&traces[instance_idx], &term.source, row) + else { + continue; + }; + let bucket_idx = prefix_offset * family.coeff_classes.len() + coeff_idx; + pending_adds = pending_adds.saturating_add(add_poly_bits_to_bucket( + poly, + &omega, + &mut buckets[bucket_idx], + )); + + if pending_adds >= DMR_FLUSH_ADDS { + flush_buckets_into_lanes( + &mut buckets, + &mut lane_accs, + field_cfg, + reduction_params, + ); + pending_adds = 0; + } + } + } + } + } + + flush_buckets_into_lanes(&mut buckets, &mut lane_accs, field_cfg, reduction_params); + + for prefix_offset in 0..tile_len { + let prefix = tile_start + prefix_offset; + let mut family_value = zero.clone(); + for (coeff_idx, coeff_class) in family.coeff_classes.iter().enumerate() { + let bucket_idx = prefix_offset * family.coeff_classes.len() + coeff_idx; + let projected = FieldFieldInnerProduct::inner_product::( + &lane_accs[bucket_idx], + &scalarization_powers[..D], + zero.clone(), + ) + .expect("lane accumulator and scalarization powers have matching lengths"); + family_value += coeff_class.to_field(field_cfg) * projected; + } + table_values[prefix] += family_weight.clone() * family_value; + } + + Ok(()) +} + +fn validate_trace_count(len: usize) -> Result { + if len == 0 { + return Err(LinearCprAccumulatorError::EmptyTraces); + } + if !len.is_power_of_two() { + return Err(LinearCprAccumulatorError::TraceCountNotPowerOfTwo { len }); + } + Ok(usize::try_from(len.trailing_zeros()).expect("trailing_zeros fits usize")) +} + +fn validate_traces( + traces: &[UairTrace<'_, PolyCoeff, Int, D>], + expected_rows: usize, +) -> Result +where + PolyCoeff: Clone, + Int: Clone, +{ + let num_binary_cols = traces + .first() + .expect("trace count was already validated as non-empty") + .binary_poly + .len(); + for (trace_idx, trace) in traces.iter().enumerate() { + if trace.binary_poly.len() != num_binary_cols { + return Err(LinearCprAccumulatorError::BinaryColumnCountMismatch { + trace_idx, + got: trace.binary_poly.len(), + expected: num_binary_cols, + }); + } + for (col_idx, column) in trace.binary_poly.iter().enumerate() { + if column.evaluations.len() != expected_rows { + return Err(LinearCprAccumulatorError::BinaryColumnRowMismatch { + trace_idx, + col_idx, + got: column.evaluations.len(), + expected: expected_rows, + }); + } + } + } + Ok(num_binary_cols) +} + +fn prepare_families( + families: &[LinearFamilySpec], + family_weight_len: usize, + num_binary_cols: usize, + row_count: usize, +) -> Result>, LinearCprAccumulatorError> +where + F: FromPrimitiveWithConfig, +{ + let mut prepared = Vec::with_capacity(families.len()); + for family in families { + if family.family_idx >= family_weight_len { + return Err(LinearCprAccumulatorError::FamilyWeightOutOfRange { + family_idx: family.family_idx, + weight_idx: family.family_idx, + len: family_weight_len, + }); + } + for &row in &family.active_rows { + if row >= row_count { + return Err(LinearCprAccumulatorError::ActiveRowOutOfRange { + family_idx: family.family_idx, + row, + rows: row_count, + }); + } + } + + let mut coeff_classes = Vec::>::new(); + let mut terms = Vec::with_capacity(family.terms.len()); + for (term_idx, term) in family.terms.iter().enumerate() { + let col_idx = term.source.col_idx(); + if col_idx >= num_binary_cols { + return Err(LinearCprAccumulatorError::SourceColumnOutOfRange { + family_idx: family.family_idx, + term_idx, + col_idx, + cols: num_binary_cols, + }); + } + if term.coeffs_by_active_row.len() != family.active_rows.len() { + return Err(LinearCprAccumulatorError::TermCoeffLengthMismatch { + family_idx: family.family_idx, + term_idx, + got: term.coeffs_by_active_row.len(), + expected: family.active_rows.len(), + }); + } + + let mut coeff_indices = Vec::with_capacity(term.coeffs_by_active_row.len()); + for coeff in &term.coeffs_by_active_row { + if coeff.is_zero() { + coeff_indices.push(None); + continue; + } + let idx = match coeff_classes.iter().position(|existing| existing == coeff) { + Some(idx) => idx, + None => { + coeff_classes.push(coeff.clone()); + coeff_classes.len() - 1 + } + }; + coeff_indices.push(Some(idx)); + } + terms.push(PreparedTerm { + source: term.source.clone(), + coeff_indices_by_active_row: coeff_indices, + }); + } + + prepared.push(PreparedFamily { + family_idx: family.family_idx, + active_rows: family.active_rows.clone(), + coeff_classes, + terms, + }); + } + Ok(prepared) +} + +fn source_poly<'a, PolyCoeff, Int, const D: usize>( + trace: &'a UairTrace<'_, PolyCoeff, Int, D>, + source: &LinearBinarySource, + row: usize, +) -> Option<&'a BinaryPoly> +where + PolyCoeff: Clone, + Int: Clone, +{ + match source { + LinearBinarySource::Column { col_idx } => trace.binary_poly[*col_idx].evaluations.get(row), + LinearBinarySource::ShiftedColumn { col_idx, shift } => row + .checked_add(*shift) + .and_then(|shifted_row| trace.binary_poly[*col_idx].evaluations.get(shifted_row)), + } +} + +#[allow(clippy::arithmetic_side_effects)] +fn add_poly_bits_to_bucket( + poly: &BinaryPoly, + weight: &F, + bucket: &mut [Uint<5>; D], +) -> usize +where + F: MontgomeryLimbs + Send + Sync, +{ + if D <= 64 { + let mut bits = 0u64; + for (bit_idx, coeff) in poly.iter().enumerate().take(D) { + if coeff.into_inner() { + bits |= 1u64 << bit_idx; + } + } + + let mut adds = 0usize; + while bits != 0 { + let bit_idx = + usize::try_from(bits.trailing_zeros()).expect("trailing_zeros fits usize"); + as DelayedModularReduction>::add(&mut bucket[bit_idx], weight); + bits &= bits - 1; + adds += 1; + } + adds + } else { + let mut adds = 0usize; + for (bit_idx, coeff) in poly.iter().enumerate().take(D) { + if coeff.into_inner() { + as DelayedModularReduction>::add(&mut bucket[bit_idx], weight); + adds += 1; + } + } + adds + } +} + +fn flush_buckets_into_lanes( + buckets: &mut [[Uint<5>; D]], + lane_accs: &mut [[F; D]], + field_cfg: &F::Config, + reduction_params: &BarrettReductionParams, +) where + F: MontgomeryLimbs + Send + Sync, +{ + for (bucket_lanes, acc_lanes) in buckets.iter_mut().zip(lane_accs.iter_mut()) { + for (bucket, acc) in bucket_lanes.iter_mut().zip(acc_lanes.iter_mut()) { + if bucket.is_zero() { + continue; + } + let pending = std::mem::replace(bucket, Uint::zero()); + *acc += as DelayedModularReduction>::reduce( + pending, + field_cfg, + reduction_params, + ); + } + } +} + +impl LinearPrefixTable +where + F: PrimeField + DelayedFieldProductSum + 'static, + F::Inner: num_traits::Zero, +{ + pub fn build_linear_cpr_sumcheck_group( + &self, + prefix_eq_weights: &[F], + field_cfg: &F::Config, + ) -> Result, SumFoldError> { + self.build_sumcheck_group_from_prefix_weights(prefix_eq_weights, field_cfg) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{sumcheck::multi_degree::MultiDegreeSumcheck, test_utils::test_config}; + use crypto_primitives::{FromWithConfig, crypto_bigint_monty::MontyField}; + use std::borrow::Cow; + use zinc_poly::{ + mle::{DenseMultilinearExtension, MultilinearExtensionWithConfig}, + utils::eq_eval, + }; + use zinc_transcript::Blake3Transcript; + use zinc_utils::powers; + + type F = MontyField<4>; + type Trace = UairTrace<'static, F, F, 32>; + + fn f(value: u64) -> F { + F::from_with_cfg(value, &test_config()) + } + + fn binary_column(patterns: &[u32]) -> DenseMultilinearExtension> { + assert!(patterns.len().is_power_of_two()); + DenseMultilinearExtension::from_evaluations_vec( + usize::try_from(patterns.len().trailing_zeros()).expect("trailing_zeros fits usize"), + patterns + .iter() + .copied() + .map(BinaryPoly::<32>::from) + .collect(), + BinaryPoly::<32>::zero(), + ) + } + + fn trace_from_columns(col0: &[u32], col1: &[u32]) -> Trace { + UairTrace { + binary_poly: Cow::Owned(vec![binary_column(col0), binary_column(col1)]), + arbitrary_poly: Cow::Owned(Vec::new()), + int: Cow::Owned(Vec::new()), + } + } + + fn sample_traces() -> Vec { + vec![ + trace_from_columns( + &[0x0000_0001, 0x0000_0002, 0x8000_0001, 0x0001_0010], + &[0x0000_0005, 0x0000_000a, 0x0000_0101, 0x0000_1000], + ), + trace_from_columns( + &[0x0000_0003, 0x0000_0010, 0x0000_00f0, 0x0000_f000], + &[0x0000_0006, 0x0000_0009, 0x0000_0f00, 0x0000_00ff], + ), + trace_from_columns( + &[0x0000_0100, 0x0000_0201, 0x0000_0402, 0x0000_0804], + &[0x0000_0011, 0x0000_0022, 0x0000_0044, 0x0000_0088], + ), + trace_from_columns( + &[0x0000_aaaa, 0x0000_5555, 0x0000_3333, 0x0000_cccc], + &[0x0000_1234, 0x0000_4321, 0x0000_00f1, 0x0000_0f10], + ), + ] + } + + fn sample_families() -> Vec> { + vec![ + LinearFamilySpec { + family_idx: 0, + active_rows: vec![0, 1, 2], + terms: vec![ + LinearTermSpec { + source: LinearBinarySource::Column { col_idx: 0 }, + coeffs_by_active_row: vec![ + CoeffClass::Small(1), + CoeffClass::Small(-1), + CoeffClass::Zero, + ], + }, + LinearTermSpec { + source: LinearBinarySource::Column { col_idx: 1 }, + coeffs_by_active_row: vec![ + CoeffClass::Small(2), + CoeffClass::Small(1), + CoeffClass::Small(-2), + ], + }, + ], + }, + LinearFamilySpec { + family_idx: 1, + active_rows: vec![1, 3], + terms: vec![ + LinearTermSpec { + source: LinearBinarySource::Column { col_idx: 0 }, + coeffs_by_active_row: vec![CoeffClass::Large(f(7)), CoeffClass::Small(-1)], + }, + LinearTermSpec { + source: LinearBinarySource::ShiftedColumn { + col_idx: 1, + shift: 1, + }, + coeffs_by_active_row: vec![CoeffClass::Small(1), CoeffClass::Small(3)], + }, + ], + }, + ] + } + + fn scalar_weights() -> Vec { + powers(f(3), F::one_with_cfg(&test_config()), 32) + } + + #[allow(clippy::arithmetic_side_effects)] + fn naive_linear_cpr_table( + traces: &[Trace], + prefix_vars: usize, + families: &[LinearFamilySpec], + weights: LinearCprWeights<'_, F>, + scalar_weights: LinearCprScalarWeights<'_, F>, + ) -> Vec { + let cfg = test_config(); + let ell = + usize::try_from(traces.len().trailing_zeros()).expect("trailing_zeros fits usize"); + let prefix_len = 1usize << prefix_vars; + let tail_len = 1usize << (ell - prefix_vars); + let mut table = vec![F::zero_with_cfg(&cfg); prefix_len]; + + for (prefix, value) in table.iter_mut().enumerate() { + for family in families { + let mut family_value = F::zero_with_cfg(&cfg); + for tail in 0..tail_len { + let instance_idx = prefix + (tail << prefix_vars); + let trace = &traces[instance_idx]; + for (active_pos, &row) in family.active_rows.iter().enumerate() { + for term in &family.terms { + let coeff = &term.coeffs_by_active_row[active_pos]; + if coeff.is_zero() { + continue; + } + let coeff_value = coeff.to_field(&cfg); + let Some(poly) = source_poly::(trace, &term.source, row) + else { + continue; + }; + + for (bit_idx, bit) in poly.iter().enumerate().take(32) { + if bit.into_inner() { + let mut contribution = weights.tail_eq_weights[tail].clone(); + contribution *= &weights.row_weights.as_slice()[row]; + contribution *= &coeff_value; + contribution *= &scalar_weights.scalarization_powers[bit_idx]; + family_value += contribution; + } + } + } + } + } + *value += scalar_weights.family_weights[family.family_idx].clone() * family_value; + } + } + + table + } + + fn build_table_for_prefix_vars( + prefix_vars: usize, + ) -> (LinearPrefixTable, SumFoldEqWeights) { + let cfg = test_config(); + let traces = sample_traces(); + let families = sample_families(); + let beta = vec![f(19), f(23)]; + let eq_weights = build_sumfold_eq_weights(&beta, prefix_vars, &cfg).unwrap(); + let row_weights = RowWeights::new(&[f(11), f(13)], &cfg).unwrap(); + let family_weights = vec![f(5), f(17)]; + let scalarization_powers = scalar_weights(); + + let table = build_linear_cpr_prefix_table( + &traces, + prefix_vars, + &families, + LinearCprWeights { + row_weights: &row_weights, + tail_eq_weights: &eq_weights.tail_eq_weights, + prefix_eq_weights: &eq_weights.prefix_eq_weights, + }, + LinearCprScalarWeights { + family_weights: &family_weights, + scalarization_powers: &scalarization_powers, + }, + &cfg, + ) + .unwrap(); + + let expected = naive_linear_cpr_table( + &traces, + prefix_vars, + &families, + LinearCprWeights { + row_weights: &row_weights, + tail_eq_weights: &eq_weights.tail_eq_weights, + prefix_eq_weights: &eq_weights.prefix_eq_weights, + }, + LinearCprScalarWeights { + family_weights: &family_weights, + scalarization_powers: &scalarization_powers, + }, + ); + assert_eq!(table.values(), expected.as_slice()); + + (table, eq_weights) + } + + #[test] + fn optimized_linear_cpr_table_matches_expanded_formula_for_live_prefix() { + let (table, eq_weights) = build_table_for_prefix_vars(2); + + assert_eq!(table.ell(), 2); + assert_eq!(table.ell0(), 2); + assert_eq!( + eq_weights.tail_eq_weights, + vec![F::one_with_cfg(&test_config())] + ); + } + + #[test] + fn optimized_linear_cpr_table_matches_expanded_formula_for_windowed_prefix() { + let (table, eq_weights) = build_table_for_prefix_vars(1); + + assert_eq!(table.ell(), 2); + assert_eq!(table.ell0(), 1); + assert_eq!(table.len(), 2); + assert_eq!(eq_weights.tail_eq_weights.len(), 2); + } + + #[test] + fn optimized_linear_cpr_table_builds_degree_two_sumcheck() { + let cfg = test_config(); + let beta = vec![f(19), f(23)]; + let (table, eq_weights) = build_table_for_prefix_vars(2); + + let claim = table + .build_sumcheck_claim(&eq_weights.prefix_eq_weights, &cfg) + .unwrap(); + let group = table + .build_sumcheck_group_from_prefix_weights(&eq_weights.prefix_eq_weights, &cfg) + .unwrap(); + + let mut prover_transcript = Blake3Transcript::new(); + let (proof, _states) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut prover_transcript, + vec![group], + table.ell0(), + &cfg, + ); + + assert_eq!(proof.claimed_sums()[0], claim); + + let mut verifier_transcript = Blake3Transcript::new(); + let subclaims = MultiDegreeSumcheck::verify_as_subprotocol( + &mut verifier_transcript, + table.ell0(), + &proof, + &cfg, + ) + .expect("optimized linear CPR sumcheck should verify"); + + let point = subclaims.point(); + let eq_at_point = + eq_eval(point, &beta, F::one_with_cfg(&cfg)).expect("same number of variables"); + let table_eval = table + .to_mle(&cfg) + .evaluate_with_config(point, &cfg) + .unwrap(); + assert_eq!( + subclaims.expected_evaluations()[0], + eq_at_point * table_eval + ); + } + + #[test] + fn optimized_linear_cpr_validation_errors_are_reported() { + let cfg = test_config(); + let traces = sample_traces(); + let families = sample_families(); + let row_weights = RowWeights::new(&[f(11), f(13)], &cfg).unwrap(); + let family_weights = vec![f(5), f(17)]; + let scalarization_powers = scalar_weights(); + + let err = build_linear_cpr_prefix_table( + &traces, + 3, + &families, + LinearCprWeights { + row_weights: &row_weights, + tail_eq_weights: &[F::one_with_cfg(&cfg)], + prefix_eq_weights: &[F::one_with_cfg(&cfg)], + }, + LinearCprScalarWeights { + family_weights: &family_weights, + scalarization_powers: &scalarization_powers, + }, + &cfg, + ) + .expect_err("too many prefix variables should be rejected"); + assert!(matches!( + err, + LinearCprAccumulatorError::PrefixVarsTooLarge { + prefix_vars: 3, + ell: 2 + } + )); + + let wrong_prefix_weights = vec![F::one_with_cfg(&cfg); 4]; + let err = build_linear_cpr_prefix_table( + &traces, + 2, + &families, + LinearCprWeights { + row_weights: &row_weights, + tail_eq_weights: &[], + prefix_eq_weights: &wrong_prefix_weights, + }, + LinearCprScalarWeights { + family_weights: &family_weights, + scalarization_powers: &scalarization_powers, + }, + &cfg, + ) + .expect_err("wrong tail weight length should be rejected"); + assert!(matches!( + err, + LinearCprAccumulatorError::LengthMismatch { + label: "tail_eq_weights", + got: 0, + expected: 1 + } + )); + + let mut bad_families = sample_families(); + bad_families[0].terms[0].coeffs_by_active_row.pop(); + let eq_weights = build_sumfold_eq_weights(&[f(19), f(23)], 2, &cfg).unwrap(); + let err = build_linear_cpr_prefix_table( + &traces, + 2, + &bad_families, + LinearCprWeights { + row_weights: &row_weights, + tail_eq_weights: &eq_weights.tail_eq_weights, + prefix_eq_weights: &eq_weights.prefix_eq_weights, + }, + LinearCprScalarWeights { + family_weights: &family_weights, + scalarization_powers: &scalarization_powers, + }, + &cfg, + ) + .expect_err("wrong coefficient vector length should be rejected"); + assert!(matches!( + err, + LinearCprAccumulatorError::TermCoeffLengthMismatch { + family_idx: 0, + term_idx: 0, + got: 2, + expected: 3 + } + )); + } +} diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index 8caed9f1..4bcaf99e 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -5,9 +5,15 @@ //! protocol proof objects or verifier flow. pub mod accumulator; +pub mod linear_cpr; pub mod sumfold; pub use accumulator::{ AccumulatorError, RowWeights, SmallValueBitAccumulator, accumulate_binary_column_projected, }; +pub use linear_cpr::{ + CoeffClass, LinearBinarySource, LinearCprAccumulatorError, LinearCprScalarWeights, + LinearCprWeights, LinearFamilySpec, LinearTermSpec, SumFoldEqWeights, + build_linear_cpr_prefix_table, build_sumfold_eq_weights, +}; pub use sumfold::{LinearInstanceClaims, LinearPrefixTable, SumFoldError}; diff --git a/piop/src/neutron_nova/sumfold.rs b/piop/src/neutron_nova/sumfold.rs index 2f517cca..40597564 100644 --- a/piop/src/neutron_nova/sumfold.rs +++ b/piop/src/neutron_nova/sumfold.rs @@ -4,6 +4,11 @@ use zinc_poly::{ mle::DenseMultilinearExtension, utils::{ArithErrors, build_eq_x_r_inner, build_eq_x_r_vec}, }; +use zinc_utils::{ + UNCHECKED, + delayed_reduction::DelayedFieldProductSum, + inner_product::{FieldFieldInnerProduct, InnerProduct}, +}; use crate::sumcheck::multi_degree::MultiDegreeSumcheckGroup; @@ -26,6 +31,12 @@ pub enum SumFoldError { Ell0TooLarge { ell0: usize, ell: usize }, #[error("beta length mismatch: got {got}, expected {expected}")] BetaLengthMismatch { got: usize, expected: usize }, + #[error("{label} length mismatch: got {got}, expected {expected}")] + WeightLengthMismatch { + label: &'static str, + got: usize, + expected: usize, + }, #[error("sumcheck group construction requires ell0 > 0")] SumcheckNeedsNonzeroEll0, #[error("equality table construction failed: {0}")] @@ -91,6 +102,32 @@ pub struct LinearPrefixTable { } impl LinearPrefixTable { + pub(crate) fn from_values_for_prefix_vars( + values: Vec, + ell: usize, + prefix_vars: usize, + ) -> Result { + if prefix_vars > ell { + return Err(SumFoldError::Ell0TooLarge { + ell0: prefix_vars, + ell, + }); + } + let expected = checked_domain_size(prefix_vars)?; + if values.len() != expected { + return Err(SumFoldError::InstanceCountMismatch { + ell: prefix_vars, + got: values.len(), + expected, + }); + } + Ok(Self { + values, + ell, + ell0: prefix_vars, + }) + } + #[allow(clippy::arithmetic_side_effects)] pub fn build( instance_claims: &LinearInstanceClaims, @@ -167,9 +204,64 @@ impl LinearPrefixTable { impl LinearPrefixTable where - F: PrimeField + 'static, + F: PrimeField + DelayedFieldProductSum + 'static, F::Inner: num_traits::Zero, { + pub fn build_sumcheck_claim( + &self, + prefix_eq_weights: &[F], + field_cfg: &F::Config, + ) -> Result { + if prefix_eq_weights.len() != self.values.len() { + return Err(SumFoldError::WeightLengthMismatch { + label: "prefix_eq_weights", + got: prefix_eq_weights.len(), + expected: self.values.len(), + }); + } + + Ok(FieldFieldInnerProduct::inner_product::( + prefix_eq_weights, + &self.values, + F::zero_with_cfg(field_cfg), + ) + .expect("prefix equality weights and table values have matching lengths")) + } + + pub fn build_sumcheck_group_from_prefix_weights( + &self, + prefix_eq_weights: &[F], + field_cfg: &F::Config, + ) -> Result, SumFoldError> { + if self.ell0 == 0 { + return Err(SumFoldError::SumcheckNeedsNonzeroEll0); + } + if prefix_eq_weights.len() != self.values.len() { + return Err(SumFoldError::WeightLengthMismatch { + label: "prefix_eq_weights", + got: prefix_eq_weights.len(), + expected: self.values.len(), + }); + } + + let zero_inner = F::zero_with_cfg(field_cfg).inner().clone(); + let eq_prefix = DenseMultilinearExtension::from_evaluations_vec( + self.ell0, + prefix_eq_weights + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner, + ); + let table = self.to_mle(field_cfg); + + Ok(MultiDegreeSumcheckGroup::new( + 2, + vec![eq_prefix, table], + Box::new(|values: &[F]| values[0].clone() * &values[1]), + )) + } + pub fn build_sumcheck_group( &self, beta_prefix: &[F], @@ -196,7 +288,7 @@ where } } -fn checked_domain_size(ell: usize) -> Result { +pub(crate) fn checked_domain_size(ell: usize) -> Result { let shift = u32::try_from(ell).map_err(|_| SumFoldError::DomainTooLarge { ell })?; 1usize .checked_shl(shift) From a9d41ded93f7e3ad4d508592cf00e1193b0e077c Mon Sep 17 00:00:00 2001 From: John Wu Date: Fri, 5 Jun 2026 21:31:24 -0700 Subject: [PATCH 12/49] Add NeutronNova booleanity accumulator --- piop/src/neutron_nova/accumulator.rs | 137 ++- piop/src/neutron_nova/booleanity.rs | 1265 ++++++++++++++++++++++++++ piop/src/neutron_nova/linear_cpr.rs | 101 +- piop/src/neutron_nova/mod.rs | 6 + 4 files changed, 1436 insertions(+), 73 deletions(-) create mode 100644 piop/src/neutron_nova/booleanity.rs diff --git a/piop/src/neutron_nova/accumulator.rs b/piop/src/neutron_nova/accumulator.rs index 8422dd08..65d7b82b 100644 --- a/piop/src/neutron_nova/accumulator.rs +++ b/piop/src/neutron_nova/accumulator.rs @@ -1,6 +1,6 @@ use crypto_primitives::{PrimeField, crypto_bigint_uint::Uint}; use num_traits::Zero; -use std::marker::PhantomData; +use std::array; use thiserror::Error; use zinc_poly::{ mle::DenseMultilinearExtension, @@ -9,11 +9,23 @@ use zinc_poly::{ }; use zinc_utils::{ UNCHECKED, - delayed_reduction::{DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs}, + delayed_reduction::{ + BarrettReductionParams, DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs, + }, inner_product::{FieldFieldInnerProduct, InnerProduct}, powers, }; +const DMR_FLUSH_ADDS: usize = 1 << 20; + +pub(crate) fn dmr_flush_adds(reduction_params: &BarrettReductionParams) -> usize { + if reduction_params.modulus[3] == 0 { + 1 + } else { + DMR_FLUSH_ADDS + } +} + /// Errors produced by NeutronNova row-space accumulation helpers. #[derive(Clone, Debug, Error)] pub enum AccumulatorError { @@ -78,39 +90,50 @@ impl RowWeights { /// DMR-backed bit buckets for one small-value binary-polynomial column. #[derive(Clone, Debug)] -pub struct SmallValueBitAccumulator { +pub struct SmallValueBitAccumulator<'a, F: PrimeField, const D: usize> { buckets: [Uint<5>; D], - _field: PhantomData, + lane_accs: [F; D], + pending_adds: usize, + flush_adds: usize, + field_cfg: &'a F::Config, + reduction_params: BarrettReductionParams, } -impl Default for SmallValueBitAccumulator { - fn default() -> Self { - Self::new() - } -} - -impl SmallValueBitAccumulator { - pub fn new() -> Self { +impl<'a, F, const D: usize> SmallValueBitAccumulator<'a, F, D> +where + F: MontgomeryLimbs + Send + Sync, +{ + pub fn new(field_cfg: &'a F::Config) -> Self { + let reduction_params = F::barrett_reduction_params(field_cfg); + let flush_adds = dmr_flush_adds(&reduction_params); + let zero = F::zero_with_cfg(field_cfg); Self { buckets: [Uint::zero(); D], - _field: PhantomData, + lane_accs: array::from_fn(|_| zero.clone()), + pending_adds: 0, + flush_adds, + field_cfg, + reduction_params, } } - pub fn buckets(&self) -> &[Uint<5>] { + /// Pending unreduced DMR buckets. + /// + /// Flushed contributions live in the reduced lane accumulators, so this is + /// only useful for low-level tests and diagnostics. + pub fn pending_buckets(&self) -> &[Uint<5>] { &self.buckets } -} -impl SmallValueBitAccumulator -where - F: MontgomeryLimbs + Send + Sync, -{ pub fn add_bit_weight(&mut self, bit_idx: usize, weight: &F) -> Result<(), AccumulatorError> { let Some(bucket) = self.buckets.get_mut(bit_idx) else { return Err(AccumulatorError::BitIndexOutOfRange { bit_idx, degree: D }); }; as DelayedModularReduction>::add(bucket, weight); + self.pending_adds = self.pending_adds.saturating_add(1); + if self.pending_adds >= self.flush_adds { + self.flush_buckets(); + } Ok(()) } @@ -144,30 +167,32 @@ where Ok(()) } - pub fn reduce_buckets( - self, - field_cfg: &F::Config, - reduction_params: &zinc_utils::delayed_reduction::BarrettReductionParams, - ) -> Vec { - self.buckets - .into_iter() - .map(|bucket| { - as DelayedModularReduction>::reduce(bucket, field_cfg, reduction_params) - }) - .collect() + pub fn reduce_buckets(mut self) -> Vec { + self.flush_buckets(); + self.lane_accs.into_iter().collect() + } + + fn flush_buckets(&mut self) { + for (bucket, acc) in self.buckets.iter_mut().zip(self.lane_accs.iter_mut()) { + if bucket.is_zero() { + continue; + } + let pending = std::mem::replace(bucket, Uint::zero()); + *acc += as DelayedModularReduction>::reduce( + pending, + self.field_cfg, + &self.reduction_params, + ); + } + self.pending_adds = 0; } } -impl SmallValueBitAccumulator +impl SmallValueBitAccumulator<'_, F, D> where F: MontgomeryLimbs + DelayedFieldProductSum + Send + Sync, { - pub fn project( - self, - projection_powers: &[F], - field_cfg: &F::Config, - reduction_params: &zinc_utils::delayed_reduction::BarrettReductionParams, - ) -> Result { + pub fn project(mut self, projection_powers: &[F]) -> Result { if projection_powers.len() < D { return Err(AccumulatorError::ProjectionPowersLengthMismatch { got: projection_powers.len(), @@ -175,10 +200,10 @@ where }); } - let zero = F::zero_with_cfg(field_cfg); - let bucket_evals = self.reduce_buckets(field_cfg, reduction_params); + self.flush_buckets(); + let zero = F::zero_with_cfg(self.field_cfg); Ok(FieldFieldInnerProduct::inner_product::( - &bucket_evals, + &self.lane_accs, &projection_powers[..D], zero, ) @@ -209,19 +234,19 @@ where let one = F::one_with_cfg(field_cfg); let projection_powers: Vec = powers(projecting_element.clone(), one, D); - let reduction_params = F::barrett_reduction_params(field_cfg); - let mut accumulator = SmallValueBitAccumulator::::new(); + let mut accumulator = SmallValueBitAccumulator::::new(field_cfg); for (poly, weight) in column.iter().zip(row_weights.as_slice()) { accumulator.add_binary_poly(poly, weight)?; } - accumulator.project(&projection_powers, field_cfg, &reduction_params) + accumulator.project(&projection_powers) } #[cfg(test)] mod tests { use super::*; + use crate::test_utils::test_config; use crypto_bigint::{Odd, modular::MontyParams}; use crypto_primitives::{FromWithConfig, crypto_bigint_monty::MontyField}; use zinc_utils::powers; @@ -260,8 +285,8 @@ mod tests { column: &DenseMultilinearExtension>, row_weights: &RowWeights, projecting_element: &F, + cfg: &MontyParams<4>, ) -> F { - let cfg = field_cfg(); let zero = F::zero_with_cfg(&cfg); let one = F::one_with_cfg(&cfg); let powers = powers(projecting_element.clone(), one, 32); @@ -299,11 +324,33 @@ mod tests { let got = accumulate_binary_column_projected(&column, &row_weights, &projecting_element, &cfg) .unwrap(); - let expected = naive_projected_sum(&column, &row_weights, &projecting_element); + let expected = naive_projected_sum(&column, &row_weights, &projecting_element, &cfg); assert_eq!(got, expected); } + #[test] + fn small_value_bit_accumulator_flushes_for_small_modulus() { + let cfg = test_config(); + let max = -F::from_with_cfg(1u64, &cfg); + let mut accumulator = SmallValueBitAccumulator::::new(&cfg); + let mut expected = F::zero_with_cfg(&cfg); + + for _ in 0..2048 { + accumulator.add_bit_weight(7, &max).unwrap(); + expected += &max; + } + + let lanes = accumulator.reduce_buckets(); + assert_eq!(lanes[7], expected); + assert!( + lanes + .iter() + .enumerate() + .all(|(lane, value)| lane == 7 || F::is_zero(value)) + ); + } + #[test] fn last_row_zero_helper_matches_manual_zeroing() { let cfg = field_cfg(); diff --git a/piop/src/neutron_nova/booleanity.rs b/piop/src/neutron_nova/booleanity.rs new file mode 100644 index 00000000..98612f7c --- /dev/null +++ b/piop/src/neutron_nova/booleanity.rs @@ -0,0 +1,1265 @@ +use crate::neutron_nova::{RowWeights, accumulator::dmr_flush_adds}; +use crypto_primitives::{FromPrimitiveWithConfig, PrimeField, crypto_bigint_uint::Uint}; +use num_traits::Zero; +use std::array; +use thiserror::Error; +use zinc_poly::univariate::binary::BinaryPoly; +use zinc_uair::UairTrace; +use zinc_utils::{ + UNCHECKED, + delayed_reduction::{ + BarrettReductionParams, DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs, + }, + inner_product::{FieldFieldInnerProduct, InnerProduct}, +}; + +const MAX_DMR_BUCKET_ARRAYS: usize = 256; +const MAX_BOOLEANITY_PREFIX_VARS: usize = 10; + +/// Precomputed equality weights used by the booleanity accumulator. +#[derive(Debug)] +pub struct BooleanityWeights<'a, F: PrimeField> { + pub row_weights: &'a RowWeights, + pub tail_eq_weights: &'a [F], +} + +impl Clone for BooleanityWeights<'_, F> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for BooleanityWeights<'_, F> {} + +/// Precomputed scalarization weights for booleanity lanes. +#[derive(Debug)] +pub struct BooleanityScalarWeights<'a, F: PrimeField> { + /// Indexed as `col_idx * D + bit_idx`. + pub rho_powers: &'a [F], +} + +impl Clone for BooleanityScalarWeights<'_, F> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for BooleanityScalarWeights<'_, F> {} + +/// One point in `{0, 1, infinity}^prefix_vars`, represented as `(S, a)`. +/// +/// `support_mask` marks coordinates set to infinity. `finite_bits` stores +/// Boolean assignments in original coordinate positions. The two masks are +/// canonical and must not overlap. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ExtendedPrefixPoint { + support_mask: usize, + finite_bits: usize, +} + +impl ExtendedPrefixPoint { + pub fn new( + support_mask: usize, + finite_bits: usize, + ) -> Result { + if support_mask & finite_bits != 0 { + return Err(BooleanityAccumulatorError::ExtendedPointNotCanonical); + } + Ok(Self { + support_mask, + finite_bits, + }) + } + + pub fn support_mask(self) -> usize { + self.support_mask + } + + pub fn finite_bits(self) -> usize { + self.finite_bits + } + + pub fn support_size(self) -> usize { + usize::try_from(self.support_mask.count_ones()).expect("count_ones fits usize") + } + + pub fn is_finite_only(self) -> bool { + self.support_mask == 0 + } +} + +/// Dense table over `{0, 1, infinity}^prefix_vars`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BooleanityPrefixTable { + values: Vec, + ell: usize, + prefix_vars: usize, + num_binary_cols: usize, +} + +impl BooleanityPrefixTable { + pub fn values(&self) -> &[F] { + &self.values + } + + pub fn ell(&self) -> usize { + self.ell + } + + pub fn prefix_vars(&self) -> usize { + self.prefix_vars + } + + pub fn num_binary_cols(&self) -> usize { + self.num_binary_cols + } + + pub fn len(&self) -> usize { + self.values.len() + } + + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + pub fn value_at_point( + &self, + point: ExtendedPrefixPoint, + ) -> Result<&F, BooleanityAccumulatorError> { + let index = extended_point_index(point, self.prefix_vars)?; + self.values + .get(index) + .ok_or(BooleanityAccumulatorError::ExtendedPointIndexOutOfRange { + index, + domain_size: self.values.len(), + }) + } +} + +#[derive(Clone, Debug, Error)] +pub enum BooleanityAccumulatorError { + #[error("booleanity accumulator needs at least one trace")] + EmptyTraces, + #[error("trace count must be a power of two, got {len}")] + TraceCountNotPowerOfTwo { len: usize }, + #[error("prefix_vars={prefix_vars} must be at most ell={ell}")] + PrefixVarsTooLarge { prefix_vars: usize, ell: usize }, + #[error("booleanity prefix_vars={prefix_vars} exceeds supported maximum {max}")] + PrefixVarsExceedsSupported { prefix_vars: usize, max: usize }, + #[error("domain size is too large for {vars} variables")] + DomainTooLarge { vars: usize }, + #[error("{label} length mismatch: got {got}, expected {expected}")] + LengthMismatch { + label: &'static str, + got: usize, + expected: usize, + }, + #[error("trace {trace_idx} has {got} binary columns, expected {expected}")] + BinaryColumnCountMismatch { + trace_idx: usize, + got: usize, + expected: usize, + }, + #[error("trace {trace_idx} binary column {col_idx} has {got} rows, expected {expected}")] + BinaryColumnRowMismatch { + trace_idx: usize, + col_idx: usize, + got: usize, + expected: usize, + }, + #[error("extended point index {index} is out of range for domain size {domain_size}")] + ExtendedPointIndexOutOfRange { index: usize, domain_size: usize }, + #[error("extended point uses bits outside prefix_vars={prefix_vars}")] + ExtendedPointOutOfRange { prefix_vars: usize }, + #[error("extended point has finite bits set inside the infinity support")] + ExtendedPointNotCanonical, + #[error("accumulator bucket count overflow for {entries} entries and stride {stride}")] + BucketCountOverflow { entries: usize, stride: usize }, +} + +#[derive(Clone, Copy, Debug)] +struct ExtendedTableEntry { + table_index: usize, + point: ExtendedPrefixPoint, +} + +/// Build the optimized booleanity prefix table from small-value binary traces. +#[allow(clippy::arithmetic_side_effects, clippy::too_many_arguments)] +pub fn build_booleanity_prefix_table( + traces: &[UairTrace<'_, PolyCoeff, Int, D>], + prefix_vars: usize, + weights: BooleanityWeights<'_, F>, + scalar_weights: BooleanityScalarWeights<'_, F>, + field_cfg: &F::Config, +) -> Result, BooleanityAccumulatorError> +where + F: MontgomeryLimbs + DelayedFieldProductSum + FromPrimitiveWithConfig + Send + Sync + 'static, + PolyCoeff: Clone, + Int: Clone, +{ + let ell = validate_trace_count(traces.len())?; + if prefix_vars > ell { + return Err(BooleanityAccumulatorError::PrefixVarsTooLarge { prefix_vars, ell }); + } + if prefix_vars > MAX_BOOLEANITY_PREFIX_VARS { + return Err(BooleanityAccumulatorError::PrefixVarsExceedsSupported { + prefix_vars, + max: MAX_BOOLEANITY_PREFIX_VARS, + }); + } + + let prefix_len = binary_domain_size(prefix_vars)?; + let tail_len = binary_domain_size(ell - prefix_vars)?; + if weights.tail_eq_weights.len() != tail_len { + return Err(BooleanityAccumulatorError::LengthMismatch { + label: "tail_eq_weights", + got: weights.tail_eq_weights.len(), + expected: tail_len, + }); + } + + let num_binary_cols = validate_traces(traces, weights.row_weights.len())?; + let expected_rho_powers = num_binary_cols + .checked_mul(D) + .ok_or(BooleanityAccumulatorError::DomainTooLarge { vars: prefix_vars })?; + if scalar_weights.rho_powers.len() < expected_rho_powers { + return Err(BooleanityAccumulatorError::LengthMismatch { + label: "rho_powers", + got: scalar_weights.rho_powers.len(), + expected: expected_rho_powers, + }); + } + + let domain_len = ternary_domain_size(prefix_vars)?; + let mut table_values = vec![F::zero_with_cfg(field_cfg); domain_len]; + let entries_by_support_size = extended_entries_by_support_size(prefix_vars)?; + let row_count = weights.row_weights.len(); + let omega = precompute_row_tail_weights(weights, field_cfg)?; + let reduction_params = F::barrett_reduction_params(field_cfg); + let word_count = bit_word_count(D); + let prefix_word_len = prefix_len + .checked_mul(word_count) + .ok_or(BooleanityAccumulatorError::DomainTooLarge { vars: prefix_vars })?; + let mut prefix_words = vec![0u64; prefix_word_len]; + + for support_size in 1..=prefix_vars { + let entries = &entries_by_support_size[support_size]; + if entries.is_empty() { + continue; + } + + let max_magnitude = max_delta_magnitude(prefix_vars, support_size)?; + let _ = max_magnitude + .checked_mul(max_magnitude) + .ok_or(BooleanityAccumulatorError::DomainTooLarge { vars: prefix_vars })?; + let tile_len = adaptive_tile_len(max_magnitude).min(entries.len()); + for tile in entries.chunks(tile_len) { + for col_idx in 0..num_binary_cols { + accumulate_column_tile::( + traces, + col_idx, + tile, + prefix_vars, + prefix_len, + tail_len, + row_count, + word_count, + max_magnitude, + &omega, + &mut prefix_words, + &scalar_weights.rho_powers[col_idx * D..col_idx * D + D], + field_cfg, + &reduction_params, + &mut table_values, + )?; + } + } + } + + Ok(BooleanityPrefixTable { + values: table_values, + ell, + prefix_vars, + num_binary_cols, + }) +} + +#[allow(clippy::arithmetic_side_effects, clippy::too_many_arguments)] +fn accumulate_column_tile( + traces: &[UairTrace<'_, PolyCoeff, Int, D>], + col_idx: usize, + tile: &[ExtendedTableEntry], + prefix_vars: usize, + prefix_len: usize, + tail_len: usize, + row_count: usize, + word_count: usize, + max_magnitude: usize, + omega: &[F], + prefix_words: &mut [u64], + rho_powers: &[F], + field_cfg: &F::Config, + reduction_params: &BarrettReductionParams, + table_values: &mut [F], +) -> Result<(), BooleanityAccumulatorError> +where + F: MontgomeryLimbs + DelayedFieldProductSum + FromPrimitiveWithConfig + Send + Sync + 'static, + PolyCoeff: Clone, + Int: Clone, +{ + let bucket_stride = max_magnitude + 1; + let bucket_count = tile.len().checked_mul(bucket_stride).ok_or( + BooleanityAccumulatorError::BucketCountOverflow { + entries: tile.len(), + stride: bucket_stride, + }, + )?; + let mut buckets: Vec<[Uint<5>; D]> = vec![[Uint::zero(); D]; bucket_count]; + let zero = F::zero_with_cfg(field_cfg); + let mut lane_accs: Vec<[F; D]> = (0..bucket_count) + .map(|_| array::from_fn(|_| zero.clone())) + .collect(); + let mut pending_adds = 0usize; + let flush_adds = dmr_flush_adds(reduction_params); + + for tail in 0..tail_len { + let omega_offset = tail * row_count; + for row in 0..row_count { + gather_prefix_words::( + traces, + col_idx, + tail, + row, + prefix_vars, + prefix_len, + word_count, + prefix_words, + ); + let weight = &omega[omega_offset + row]; + + for (entry_offset, entry) in tile.iter().enumerate() { + pending_adds = pending_adds.saturating_add(accumulate_entry_deltas::( + entry.point, + entry_offset, + bucket_stride, + word_count, + prefix_words, + weight, + &mut buckets, + )); + + if pending_adds >= flush_adds { + flush_buckets_into_lanes( + &mut buckets, + &mut lane_accs, + field_cfg, + reduction_params, + ); + pending_adds = 0; + } + } + } + } + + flush_buckets_into_lanes(&mut buckets, &mut lane_accs, field_cfg, reduction_params); + + for (entry_offset, entry) in tile.iter().enumerate() { + for magnitude in 1..=max_magnitude { + let bucket_idx = entry_offset * bucket_stride + magnitude; + let projected = FieldFieldInnerProduct::inner_product::( + &lane_accs[bucket_idx], + rho_powers, + zero.clone(), + ) + .expect("lane accumulator and rho powers have matching lengths"); + let magnitude_square = magnitude + .checked_mul(magnitude) + .ok_or(BooleanityAccumulatorError::DomainTooLarge { vars: prefix_vars })?; + let scale = F::from_with_cfg( + u64::try_from(magnitude_square).map_err(|_| { + BooleanityAccumulatorError::DomainTooLarge { vars: prefix_vars } + })?, + field_cfg, + ); + table_values[entry.table_index] += scale * projected; + } + } + Ok(()) +} + +#[allow(clippy::arithmetic_side_effects)] +fn gather_prefix_words( + traces: &[UairTrace<'_, PolyCoeff, Int, D>], + col_idx: usize, + tail: usize, + row: usize, + prefix_vars: usize, + prefix_len: usize, + word_count: usize, + out: &mut [u64], +) where + PolyCoeff: Clone, + Int: Clone, +{ + for prefix in 0..prefix_len { + let instance_idx = prefix + (tail << prefix_vars); + let poly = &traces[instance_idx].binary_poly[col_idx].evaluations[row]; + write_poly_words( + poly, + &mut out[prefix * word_count..(prefix + 1) * word_count], + ); + } +} + +#[allow(clippy::arithmetic_side_effects)] +fn accumulate_entry_deltas( + point: ExtendedPrefixPoint, + entry_offset: usize, + bucket_stride: usize, + word_count: usize, + prefix_words: &[u64], + weight: &F, + buckets: &mut [[Uint<5>; D]], +) -> usize +where + F: MontgomeryLimbs + Send + Sync, +{ + match point.support_size() { + 1 => accumulate_support_one::( + point, + entry_offset, + bucket_stride, + word_count, + prefix_words, + weight, + buckets, + ), + 2 => accumulate_support_two::( + point, + entry_offset, + bucket_stride, + word_count, + prefix_words, + weight, + buckets, + ), + _ => accumulate_support_general::( + point, + entry_offset, + bucket_stride, + word_count, + prefix_words, + weight, + buckets, + ), + } +} + +#[allow(clippy::arithmetic_side_effects)] +fn accumulate_support_one( + point: ExtendedPrefixPoint, + entry_offset: usize, + bucket_stride: usize, + word_count: usize, + prefix_words: &[u64], + weight: &F, + buckets: &mut [[Uint<5>; D]], +) -> usize +where + F: MontgomeryLimbs + Send + Sync, +{ + let support_bit = point.support_mask; + let base = point.finite_bits & !support_bit; + let idx0 = base; + let idx1 = base | support_bit; + let bucket_idx = entry_offset * bucket_stride + 1; + let mut adds = 0usize; + for word_idx in 0..word_count { + let mask = word_at(prefix_words, idx0, word_count, word_idx) + ^ word_at(prefix_words, idx1, word_count, word_idx); + adds += add_mask_word_to_bucket(mask, word_idx, weight, &mut buckets[bucket_idx]); + } + adds +} + +#[allow(clippy::arithmetic_side_effects)] +fn accumulate_support_two( + point: ExtendedPrefixPoint, + entry_offset: usize, + bucket_stride: usize, + word_count: usize, + prefix_words: &[u64], + weight: &F, + buckets: &mut [[Uint<5>; D]], +) -> usize +where + F: MontgomeryLimbs + Send + Sync, +{ + let first = point.support_mask & point.support_mask.wrapping_neg(); + let second = point.support_mask ^ first; + let base = point.finite_bits & !point.support_mask; + let idx00 = base; + let idx10 = base | first; + let idx01 = base | second; + let idx11 = base | first | second; + let bucket_1 = entry_offset * bucket_stride + 1; + let bucket_2 = entry_offset * bucket_stride + 2; + let mut adds = 0usize; + + for word_idx in 0..word_count { + let valid_mask = valid_word_mask::(word_idx); + let d00 = word_at(prefix_words, idx00, word_count, word_idx) & valid_mask; + let d10 = word_at(prefix_words, idx10, word_count, word_idx) & valid_mask; + let d01 = word_at(prefix_words, idx01, word_count, word_idx) & valid_mask; + let d11 = word_at(prefix_words, idx11, word_count, word_idx) & valid_mask; + let mask_1 = ((d11 ^ d00) ^ (d10 ^ d01)) & valid_mask; + let mask_2 = ((d11 & d00 & !d10 & !d01) | (!d11 & !d00 & d10 & d01)) & valid_mask; + adds += add_mask_word_to_bucket(mask_1, word_idx, weight, &mut buckets[bucket_1]); + adds += add_mask_word_to_bucket(mask_2, word_idx, weight, &mut buckets[bucket_2]); + } + + adds +} + +#[allow(clippy::arithmetic_side_effects)] +fn accumulate_support_general( + point: ExtendedPrefixPoint, + entry_offset: usize, + bucket_stride: usize, + word_count: usize, + prefix_words: &[u64], + weight: &F, + buckets: &mut [[Uint<5>; D]], +) -> usize +where + F: MontgomeryLimbs + Send + Sync, +{ + let mut support_bits = [0usize; usize::BITS as usize]; + let support_size = support_bit_masks_into(point.support_mask, &mut support_bits); + let base = point.finite_bits & !point.support_mask; + let mut deltas = [0i64; D]; + for vertex in 0..(1usize << support_size) { + let mut prefix = base; + for (pos, bit) in support_bits[..support_size].iter().enumerate() { + if ((vertex >> pos) & 1) == 1 { + prefix |= *bit; + } + } + let sign = if (support_size - vertex.count_ones() as usize) % 2 == 0 { + 1i64 + } else { + -1i64 + }; + for word_idx in 0..word_count { + let mut word = word_at(prefix_words, prefix, word_count, word_idx); + while word != 0 { + let bit = + usize::try_from(word.trailing_zeros()).expect("trailing_zeros fits usize"); + let lane = word_idx * 64 + bit; + if lane < D { + deltas[lane] += sign; + } + word &= word - 1; + } + } + } + + let mut adds = 0usize; + for (lane, delta) in deltas.iter().enumerate() { + let magnitude = usize::try_from(delta.unsigned_abs()).expect("delta magnitude fits usize"); + if magnitude == 0 { + continue; + } + let bucket_idx = entry_offset * bucket_stride + magnitude; + as DelayedModularReduction>::add(&mut buckets[bucket_idx][lane], weight); + adds += 1; + } + adds +} + +#[allow(clippy::arithmetic_side_effects)] +fn add_mask_word_to_bucket( + mut mask: u64, + word_idx: usize, + weight: &F, + bucket: &mut [Uint<5>; D], +) -> usize +where + F: MontgomeryLimbs + Send + Sync, +{ + let mut adds = 0usize; + while mask != 0 { + let bit = usize::try_from(mask.trailing_zeros()).expect("trailing_zeros fits usize"); + let lane = word_idx * 64 + bit; + if lane < D { + as DelayedModularReduction>::add(&mut bucket[lane], weight); + adds += 1; + } + mask &= mask - 1; + } + adds +} + +fn flush_buckets_into_lanes( + buckets: &mut [[Uint<5>; D]], + lane_accs: &mut [[F; D]], + field_cfg: &F::Config, + reduction_params: &BarrettReductionParams, +) where + F: MontgomeryLimbs + Send + Sync, +{ + for (bucket_lanes, acc_lanes) in buckets.iter_mut().zip(lane_accs.iter_mut()) { + for (bucket, acc) in bucket_lanes.iter_mut().zip(acc_lanes.iter_mut()) { + if bucket.is_zero() { + continue; + } + let pending = std::mem::replace(bucket, Uint::zero()); + *acc += as DelayedModularReduction>::reduce( + pending, + field_cfg, + reduction_params, + ); + } + } +} + +fn validate_trace_count(len: usize) -> Result { + if len == 0 { + return Err(BooleanityAccumulatorError::EmptyTraces); + } + if !len.is_power_of_two() { + return Err(BooleanityAccumulatorError::TraceCountNotPowerOfTwo { len }); + } + Ok(usize::try_from(len.trailing_zeros()).expect("trailing_zeros fits usize")) +} + +fn validate_traces( + traces: &[UairTrace<'_, PolyCoeff, Int, D>], + expected_rows: usize, +) -> Result +where + PolyCoeff: Clone, + Int: Clone, +{ + let num_binary_cols = traces + .first() + .expect("trace count was already validated as non-empty") + .binary_poly + .len(); + for (trace_idx, trace) in traces.iter().enumerate() { + if trace.binary_poly.len() != num_binary_cols { + return Err(BooleanityAccumulatorError::BinaryColumnCountMismatch { + trace_idx, + got: trace.binary_poly.len(), + expected: num_binary_cols, + }); + } + for (col_idx, column) in trace.binary_poly.iter().enumerate() { + if column.evaluations.len() != expected_rows { + return Err(BooleanityAccumulatorError::BinaryColumnRowMismatch { + trace_idx, + col_idx, + got: column.evaluations.len(), + expected: expected_rows, + }); + } + } + } + Ok(num_binary_cols) +} + +fn precompute_row_tail_weights( + weights: BooleanityWeights<'_, F>, + field_cfg: &F::Config, +) -> Result, BooleanityAccumulatorError> +where + F: PrimeField, +{ + let row_count = weights.row_weights.len(); + let total = weights.tail_eq_weights.len().checked_mul(row_count).ok_or( + BooleanityAccumulatorError::DomainTooLarge { + vars: weights.tail_eq_weights.len(), + }, + )?; + let mut omega = Vec::with_capacity(total); + for tail_weight in weights.tail_eq_weights { + for row_weight in weights.row_weights.as_slice() { + omega.push(tail_weight.clone() * row_weight); + } + } + if omega.is_empty() { + omega.push(F::zero_with_cfg(field_cfg)); + } + Ok(omega) +} + +fn adaptive_tile_len(max_magnitude: usize) -> usize { + (MAX_DMR_BUCKET_ARRAYS / (max_magnitude + 1)).max(1) +} + +fn max_delta_magnitude( + prefix_vars: usize, + support_size: usize, +) -> Result { + let shift = support_size + .checked_sub(1) + .ok_or(BooleanityAccumulatorError::DomainTooLarge { vars: prefix_vars })?; + let shift = u32::try_from(shift) + .map_err(|_| BooleanityAccumulatorError::DomainTooLarge { vars: prefix_vars })?; + 1usize + .checked_shl(shift) + .ok_or(BooleanityAccumulatorError::DomainTooLarge { vars: prefix_vars }) +} + +fn binary_domain_size(vars: usize) -> Result { + let shift = + u32::try_from(vars).map_err(|_| BooleanityAccumulatorError::DomainTooLarge { vars })?; + 1usize + .checked_shl(shift) + .ok_or(BooleanityAccumulatorError::DomainTooLarge { vars }) +} + +pub fn ternary_domain_size(vars: usize) -> Result { + let mut size = 1usize; + for _ in 0..vars { + size = size + .checked_mul(3) + .ok_or(BooleanityAccumulatorError::DomainTooLarge { vars })?; + } + Ok(size) +} + +#[allow(clippy::arithmetic_side_effects)] +pub fn extended_point_from_index( + mut index: usize, + prefix_vars: usize, +) -> Result { + let domain_size = ternary_domain_size(prefix_vars)?; + if index >= domain_size { + return Err(BooleanityAccumulatorError::ExtendedPointIndexOutOfRange { + index, + domain_size, + }); + } + + let mut support_mask = 0usize; + let mut finite_bits = 0usize; + for var in 0..prefix_vars { + let digit = index % 3; + index /= 3; + match digit { + 0 => {} + 1 => finite_bits |= 1usize << var, + 2 => support_mask |= 1usize << var, + _ => unreachable!("ternary digit must be 0, 1, or 2"), + } + } + ExtendedPrefixPoint::new(support_mask, finite_bits) +} + +#[allow(clippy::arithmetic_side_effects)] +pub fn extended_point_index( + point: ExtendedPrefixPoint, + prefix_vars: usize, +) -> Result { + let _ = ternary_domain_size(prefix_vars)?; + let allowed_bits = binary_domain_size(prefix_vars)?.saturating_sub(1); + if point.support_mask & !allowed_bits != 0 || point.finite_bits & !allowed_bits != 0 { + return Err(BooleanityAccumulatorError::ExtendedPointOutOfRange { prefix_vars }); + } + if point.support_mask & point.finite_bits != 0 { + return Err(BooleanityAccumulatorError::ExtendedPointNotCanonical); + } + + let mut index = 0usize; + let mut scale = 1usize; + for var in 0..prefix_vars { + let bit = 1usize << var; + let digit = if point.support_mask & bit != 0 { + 2 + } else if point.finite_bits & bit != 0 { + 1 + } else { + 0 + }; + index = index + .checked_add(digit * scale) + .ok_or(BooleanityAccumulatorError::DomainTooLarge { vars: prefix_vars })?; + scale = scale + .checked_mul(3) + .ok_or(BooleanityAccumulatorError::DomainTooLarge { vars: prefix_vars })?; + } + Ok(index) +} + +fn extended_entries_by_support_size( + prefix_vars: usize, +) -> Result>, BooleanityAccumulatorError> { + let domain_len = ternary_domain_size(prefix_vars)?; + let mut entries = vec![Vec::new(); prefix_vars + 1]; + for table_index in 0..domain_len { + let point = extended_point_from_index(table_index, prefix_vars)?; + let support_size = point.support_size(); + if support_size == 0 { + continue; + } + entries[support_size].push(ExtendedTableEntry { table_index, point }); + } + Ok(entries) +} + +fn support_bit_masks_into(mut support_mask: usize, out: &mut [usize]) -> usize { + let mut len = 0usize; + while support_mask != 0 { + let bit = support_mask & support_mask.wrapping_neg(); + out[len] = bit; + len += 1; + support_mask ^= bit; + } + len +} + +#[cfg(test)] +fn support_bit_masks(support_mask: usize) -> Vec { + let mut bits = + vec![0usize; usize::try_from(support_mask.count_ones()).expect("count_ones fits usize")]; + let len = support_bit_masks_into(support_mask, &mut bits); + debug_assert_eq!(len, bits.len()); + bits +} + +fn bit_word_count(degree: usize) -> usize { + degree.div_ceil(64) +} + +#[allow(clippy::arithmetic_side_effects)] +fn write_poly_words(poly: &BinaryPoly, out: &mut [u64]) { + out.fill(0); + for (bit_idx, coeff) in poly.iter().enumerate().take(D) { + if coeff.into_inner() { + out[bit_idx / 64] |= 1u64 << (bit_idx % 64); + } + } +} + +fn word_at(prefix_words: &[u64], prefix: usize, word_count: usize, word_idx: usize) -> u64 { + prefix_words[prefix * word_count + word_idx] +} + +#[allow(clippy::arithmetic_side_effects)] +fn valid_word_mask(word_idx: usize) -> u64 { + let remaining = D.saturating_sub(word_idx * 64); + match remaining { + 0 => 0, + 1..=63 => (1u64 << remaining) - 1, + _ => u64::MAX, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{neutron_nova::build_sumfold_eq_weights, test_utils::test_config}; + use crypto_primitives::{FromWithConfig, crypto_bigint_monty::MontyField}; + use std::borrow::Cow; + use zinc_poly::mle::DenseMultilinearExtension; + use zinc_utils::powers; + + type F = MontyField<4>; + type Trace = UairTrace<'static, F, F, 32>; + + fn f(value: u64) -> F { + F::from_with_cfg(value, &test_config()) + } + + fn binary_column(patterns: &[u32]) -> DenseMultilinearExtension> { + assert!(patterns.len().is_power_of_two()); + DenseMultilinearExtension::from_evaluations_vec( + usize::try_from(patterns.len().trailing_zeros()).expect("trailing_zeros fits usize"), + patterns + .iter() + .copied() + .map(BinaryPoly::<32>::from) + .collect(), + BinaryPoly::<32>::zero(), + ) + } + + fn trace_from_columns(col0: &[u32], col1: &[u32]) -> Trace { + UairTrace { + binary_poly: Cow::Owned(vec![binary_column(col0), binary_column(col1)]), + arbitrary_poly: Cow::Owned(Vec::new()), + int: Cow::Owned(Vec::new()), + } + } + + fn sample_traces_ell3() -> Vec { + (0..8u32) + .map(|i| { + trace_from_columns( + &[ + 0x0000_0001 ^ i, + 0x0000_00f0 ^ (i << 4), + 0x0000_3333 ^ (i * 0x1111), + 0x8000_0001 ^ (i << 8), + ], + &[ + 0x0000_0005 ^ (i << 1), + 0x0000_0a0a ^ (i << 5), + 0x0000_f00f ^ (i * 3), + 0x0001_0010 ^ (i << 9), + ], + ) + }) + .collect() + } + + #[allow(clippy::arithmetic_side_effects)] + fn naive_table( + traces: &[Trace], + prefix_vars: usize, + weights: BooleanityWeights<'_, F>, + scalar_weights: BooleanityScalarWeights<'_, F>, + ) -> Vec { + let cfg = test_config(); + let ell = + usize::try_from(traces.len().trailing_zeros()).expect("trailing_zeros fits usize"); + let tail_len = 1usize << (ell - prefix_vars); + let domain_len = ternary_domain_size(prefix_vars).unwrap(); + let mut out = vec![F::zero_with_cfg(&cfg); domain_len]; + + for (index, value) in out.iter_mut().enumerate() { + let point = extended_point_from_index(index, prefix_vars).unwrap(); + if point.is_finite_only() { + continue; + } + for tail in 0..tail_len { + for row in 0..weights.row_weights.len() { + for col_idx in 0..traces[0].binary_poly.len() { + for bit_idx in 0..32 { + let delta = naive_delta_bit( + traces, + col_idx, + bit_idx, + tail, + row, + prefix_vars, + point, + ); + if delta == 0 { + continue; + } + let mut contribution = weights.tail_eq_weights[tail].clone(); + contribution *= &weights.row_weights.as_slice()[row]; + contribution *= &scalar_weights.rho_powers[col_idx * 32 + bit_idx]; + let delta_square = + u64::try_from(delta * delta).expect("delta square fits u64"); + contribution *= F::from_with_cfg(delta_square, &cfg); + *value += contribution; + } + } + } + } + } + out + } + + #[allow(clippy::arithmetic_side_effects)] + fn naive_delta_bit( + traces: &[Trace], + col_idx: usize, + bit_idx: usize, + tail: usize, + row: usize, + prefix_vars: usize, + point: ExtendedPrefixPoint, + ) -> i64 { + let support_bits = support_bit_masks(point.support_mask); + let support_size = support_bits.len(); + let base = point.finite_bits & !point.support_mask; + let mut delta = 0i64; + for vertex in 0..(1usize << support_size) { + let mut prefix = base; + for (pos, bit) in support_bits.iter().enumerate() { + if ((vertex >> pos) & 1) == 1 { + prefix |= *bit; + } + } + let sign = if (support_size - vertex.count_ones() as usize) % 2 == 0 { + 1i64 + } else { + -1i64 + }; + let instance_idx = prefix + (tail << prefix_vars); + let bit = traces[instance_idx].binary_poly[col_idx].evaluations[row] + .iter() + .nth(bit_idx) + .expect("bit index in range") + .into_inner(); + if bit { + delta += sign; + } + } + delta + } + + fn rho_powers() -> Vec { + powers(f(7), F::one_with_cfg(&test_config()), 64) + } + + #[test] + fn extended_point_index_round_trips() { + for prefix_vars in 0..=4 { + let domain_len = ternary_domain_size(prefix_vars).unwrap(); + for index in 0..domain_len { + let point = extended_point_from_index(index, prefix_vars).unwrap(); + assert_eq!(extended_point_index(point, prefix_vars).unwrap(), index); + } + } + } + + #[test] + fn extended_point_rejects_noncanonical_overlap() { + assert!(matches!( + ExtendedPrefixPoint::new(0b01, 0b01), + Err(BooleanityAccumulatorError::ExtendedPointNotCanonical) + )); + + let point = ExtendedPrefixPoint { + support_mask: 0b01, + finite_bits: 0b01, + }; + assert!(matches!( + extended_point_index(point, 1), + Err(BooleanityAccumulatorError::ExtendedPointNotCanonical) + )); + } + + #[test] + fn optimized_booleanity_table_matches_naive_for_support_one_and_two() { + let cfg = test_config(); + let traces = sample_traces_ell3(); + let beta = vec![f(3), f(5), f(11)]; + let eq_weights = build_sumfold_eq_weights(&beta, 2, &cfg).unwrap(); + let row_weights = RowWeights::new(&[f(13), f(17)], &cfg).unwrap(); + let rho = rho_powers(); + let weights = BooleanityWeights { + row_weights: &row_weights, + tail_eq_weights: &eq_weights.tail_eq_weights, + }; + let scalar_weights = BooleanityScalarWeights { rho_powers: &rho }; + + let table = build_booleanity_prefix_table(&traces, 2, weights, scalar_weights, &cfg) + .expect("booleanity table should build"); + let expected = naive_table(&traces, 2, weights, scalar_weights); + assert_eq!(table.values(), expected.as_slice()); + } + + #[test] + fn optimized_booleanity_table_matches_naive_for_general_support() { + let cfg = test_config(); + let traces = sample_traces_ell3(); + let eq_weights = build_sumfold_eq_weights(&[f(3), f(5), f(11)], 3, &cfg).unwrap(); + let row_weights = RowWeights::new(&[f(13), f(17)], &cfg).unwrap(); + let rho = rho_powers(); + let weights = BooleanityWeights { + row_weights: &row_weights, + tail_eq_weights: &eq_weights.tail_eq_weights, + }; + let scalar_weights = BooleanityScalarWeights { rho_powers: &rho }; + + let table = build_booleanity_prefix_table(&traces, 3, weights, scalar_weights, &cfg) + .expect("booleanity table should build"); + let expected = naive_table(&traces, 3, weights, scalar_weights); + assert_eq!(table.values(), expected.as_slice()); + } + + #[test] + fn finite_only_entries_are_zero_initially() { + let cfg = test_config(); + let traces = sample_traces_ell3(); + let eq_weights = build_sumfold_eq_weights(&[f(3), f(5), f(11)], 2, &cfg).unwrap(); + let row_weights = RowWeights::new(&[f(13), f(17)], &cfg).unwrap(); + let rho = rho_powers(); + let table = build_booleanity_prefix_table( + &traces, + 2, + BooleanityWeights { + row_weights: &row_weights, + tail_eq_weights: &eq_weights.tail_eq_weights, + }, + BooleanityScalarWeights { rho_powers: &rho }, + &cfg, + ) + .unwrap(); + + for index in 0..table.len() { + let point = extended_point_from_index(index, table.prefix_vars()).unwrap(); + if point.is_finite_only() { + assert_eq!(table.values()[index], F::zero_with_cfg(&cfg)); + } + } + } + + #[test] + fn booleanity_validation_errors_are_reported() { + let cfg = test_config(); + let traces = sample_traces_ell3(); + let row_weights = RowWeights::new(&[f(13), f(17)], &cfg).unwrap(); + let rho = rho_powers(); + let empty_traces: &[Trace] = &[]; + + let err = build_booleanity_prefix_table( + empty_traces, + 0, + BooleanityWeights { + row_weights: &row_weights, + tail_eq_weights: &[F::one_with_cfg(&cfg)], + }, + BooleanityScalarWeights { rho_powers: &rho }, + &cfg, + ) + .expect_err("empty traces should be rejected"); + assert!(matches!(err, BooleanityAccumulatorError::EmptyTraces)); + + let err = build_booleanity_prefix_table( + &traces[..3], + 1, + BooleanityWeights { + row_weights: &row_weights, + tail_eq_weights: &[F::one_with_cfg(&cfg), F::one_with_cfg(&cfg)], + }, + BooleanityScalarWeights { rho_powers: &rho }, + &cfg, + ) + .expect_err("non-power-of-two trace count should be rejected"); + assert!(matches!( + err, + BooleanityAccumulatorError::TraceCountNotPowerOfTwo { len: 3 } + )); + + let err = build_booleanity_prefix_table( + &traces, + 4, + BooleanityWeights { + row_weights: &row_weights, + tail_eq_weights: &[F::one_with_cfg(&cfg)], + }, + BooleanityScalarWeights { rho_powers: &rho }, + &cfg, + ) + .expect_err("too many prefix vars should be rejected"); + assert!(matches!( + err, + BooleanityAccumulatorError::PrefixVarsTooLarge { + prefix_vars: 4, + ell: 3 + } + )); + + let oversized_prefix_traces = + vec![traces[0].clone(); 1usize << (MAX_BOOLEANITY_PREFIX_VARS + 1)]; + let err = build_booleanity_prefix_table( + &oversized_prefix_traces, + MAX_BOOLEANITY_PREFIX_VARS + 1, + BooleanityWeights { + row_weights: &row_weights, + tail_eq_weights: &[F::one_with_cfg(&cfg)], + }, + BooleanityScalarWeights { rho_powers: &rho }, + &cfg, + ) + .expect_err("unsupported prefix var count should be rejected before allocation"); + assert!(matches!( + err, + BooleanityAccumulatorError::PrefixVarsExceedsSupported { + prefix_vars, + max + } if prefix_vars == MAX_BOOLEANITY_PREFIX_VARS + 1 + && max == MAX_BOOLEANITY_PREFIX_VARS + )); + + let err = build_booleanity_prefix_table( + &traces, + 2, + BooleanityWeights { + row_weights: &row_weights, + tail_eq_weights: &[], + }, + BooleanityScalarWeights { rho_powers: &rho }, + &cfg, + ) + .expect_err("wrong tail weight length should be rejected"); + assert!(matches!( + err, + BooleanityAccumulatorError::LengthMismatch { + label: "tail_eq_weights", + got: 0, + expected: 2 + } + )); + + let short_rho = vec![F::one_with_cfg(&cfg); 3]; + let eq_weights = build_sumfold_eq_weights(&[f(3), f(5), f(11)], 2, &cfg).unwrap(); + let err = build_booleanity_prefix_table( + &traces, + 2, + BooleanityWeights { + row_weights: &row_weights, + tail_eq_weights: &eq_weights.tail_eq_weights, + }, + BooleanityScalarWeights { + rho_powers: &short_rho, + }, + &cfg, + ) + .expect_err("short rho powers should be rejected"); + assert!(matches!( + err, + BooleanityAccumulatorError::LengthMismatch { + label: "rho_powers", + got: 3, + expected: 64 + } + )); + + let mut mismatched_cols = traces.clone(); + mismatched_cols[1].binary_poly.to_mut().pop(); + let err = build_booleanity_prefix_table( + &mismatched_cols, + 2, + BooleanityWeights { + row_weights: &row_weights, + tail_eq_weights: &eq_weights.tail_eq_weights, + }, + BooleanityScalarWeights { rho_powers: &rho }, + &cfg, + ) + .expect_err("wrong binary column count should be rejected"); + assert!(matches!( + err, + BooleanityAccumulatorError::BinaryColumnCountMismatch { + trace_idx: 1, + got: 1, + expected: 2 + } + )); + + let bad_row_weights = RowWeights::new(&[f(13)], &cfg).unwrap(); + let err = build_booleanity_prefix_table( + &traces, + 2, + BooleanityWeights { + row_weights: &bad_row_weights, + tail_eq_weights: &eq_weights.tail_eq_weights, + }, + BooleanityScalarWeights { rho_powers: &rho }, + &cfg, + ) + .expect_err("wrong row count should be rejected"); + assert!(matches!( + err, + BooleanityAccumulatorError::BinaryColumnRowMismatch { .. } + )); + } +} diff --git a/piop/src/neutron_nova/linear_cpr.rs b/piop/src/neutron_nova/linear_cpr.rs index 0bcdf219..4a3a0fe0 100644 --- a/piop/src/neutron_nova/linear_cpr.rs +++ b/piop/src/neutron_nova/linear_cpr.rs @@ -1,4 +1,4 @@ -use crate::neutron_nova::{RowWeights, sumfold::checked_domain_size}; +use crate::neutron_nova::{RowWeights, accumulator::dmr_flush_adds, sumfold::checked_domain_size}; use crate::sumcheck::multi_degree::MultiDegreeSumcheckGroup; use crypto_primitives::{FromPrimitiveWithConfig, PrimeField, crypto_bigint_uint::Uint}; use num_traits::Zero; @@ -17,8 +17,6 @@ use zinc_utils::{ use super::{LinearPrefixTable, SumFoldError}; const PREFIX_TILE_SIZE: usize = 8; -const DMR_FLUSH_ADDS: usize = 1 << 20; - /// Precomputed equality weights for the instance-axis SumFold split. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SumFoldEqWeights { @@ -27,20 +25,35 @@ pub struct SumFoldEqWeights { } /// Precomputed multiplication weights used by the linear CPR accumulator. -#[derive(Clone, Copy, Debug)] +#[derive(Debug)] pub struct LinearCprWeights<'a, F: PrimeField> { pub row_weights: &'a RowWeights, pub tail_eq_weights: &'a [F], - pub prefix_eq_weights: &'a [F], } +impl Clone for LinearCprWeights<'_, F> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for LinearCprWeights<'_, F> {} + /// Precomputed scalar weights applied after row/tail DMR reduction. -#[derive(Clone, Copy, Debug)] +#[derive(Debug)] pub struct LinearCprScalarWeights<'a, F: PrimeField> { pub family_weights: &'a [F], pub scalarization_powers: &'a [F], } +impl Clone for LinearCprScalarWeights<'_, F> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for LinearCprScalarWeights<'_, F> {} + /// One linear CPR family described as small-coefficient binary source terms. #[derive(Clone, Debug, PartialEq, Eq)] pub struct LinearFamilySpec { @@ -165,7 +178,7 @@ pub enum LinearCprAccumulatorError { struct PreparedFamily { family_idx: usize, active_rows: Vec, - coeff_classes: Vec>, + coeff_values: Vec, terms: Vec, } @@ -236,13 +249,6 @@ where expected: tail_len, }); } - if weights.prefix_eq_weights.len() != prefix_len { - return Err(LinearCprAccumulatorError::LengthMismatch { - label: "prefix_eq_weights", - got: weights.prefix_eq_weights.len(), - expected: prefix_len, - }); - } if scalar_weights.scalarization_powers.len() < D { return Err(LinearCprAccumulatorError::LengthMismatch { label: "scalarization_powers", @@ -257,6 +263,7 @@ where scalar_weights.family_weights.len(), num_binary_cols, weights.row_weights.len(), + field_cfg, )?; let mut table_values = vec![F::zero_with_cfg(field_cfg); prefix_len]; @@ -264,7 +271,7 @@ where for family in &prepared { let family_weight = &scalar_weights.family_weights[family.family_idx]; - if family.coeff_classes.is_empty() { + if family.coeff_values.is_empty() { continue; } @@ -314,7 +321,7 @@ where Int: Clone, { let bucket_count = tile_len - .checked_mul(family.coeff_classes.len()) + .checked_mul(family.coeff_values.len()) .ok_or(LinearCprAccumulatorError::DomainTooLarge { vars: prefix_vars })?; let mut buckets: Vec<[Uint<5>; D]> = vec![[Uint::zero(); D]; bucket_count]; let zero = F::zero_with_cfg(field_cfg); @@ -322,6 +329,7 @@ where .map(|_| array::from_fn(|_| zero.clone())) .collect(); let mut pending_adds = 0usize; + let flush_adds = dmr_flush_adds(reduction_params); for tail in 0..tail_len { let tail_weight = &weights.tail_eq_weights[tail]; @@ -341,14 +349,14 @@ where else { continue; }; - let bucket_idx = prefix_offset * family.coeff_classes.len() + coeff_idx; + let bucket_idx = prefix_offset * family.coeff_values.len() + coeff_idx; pending_adds = pending_adds.saturating_add(add_poly_bits_to_bucket( poly, &omega, &mut buckets[bucket_idx], )); - if pending_adds >= DMR_FLUSH_ADDS { + if pending_adds >= flush_adds { flush_buckets_into_lanes( &mut buckets, &mut lane_accs, @@ -367,15 +375,15 @@ where for prefix_offset in 0..tile_len { let prefix = tile_start + prefix_offset; let mut family_value = zero.clone(); - for (coeff_idx, coeff_class) in family.coeff_classes.iter().enumerate() { - let bucket_idx = prefix_offset * family.coeff_classes.len() + coeff_idx; + for (coeff_idx, coeff_value) in family.coeff_values.iter().enumerate() { + let bucket_idx = prefix_offset * family.coeff_values.len() + coeff_idx; let projected = FieldFieldInnerProduct::inner_product::( &lane_accs[bucket_idx], &scalarization_powers[..D], zero.clone(), ) .expect("lane accumulator and scalarization powers have matching lengths"); - family_value += coeff_class.to_field(field_cfg) * projected; + family_value += coeff_value.clone() * projected; } table_values[prefix] += family_weight.clone() * family_value; } @@ -433,6 +441,7 @@ fn prepare_families( family_weight_len: usize, num_binary_cols: usize, row_count: usize, + field_cfg: &F::Config, ) -> Result>, LinearCprAccumulatorError> where F: FromPrimitiveWithConfig, @@ -498,10 +507,15 @@ where }); } + let coeff_values = coeff_classes + .iter() + .map(|coeff| coeff.to_field(field_cfg)) + .collect(); + prepared.push(PreparedFamily { family_idx: family.family_idx, active_rows: family.active_rows.clone(), - coeff_classes, + coeff_values, terms, }); } @@ -781,7 +795,6 @@ mod tests { LinearCprWeights { row_weights: &row_weights, tail_eq_weights: &eq_weights.tail_eq_weights, - prefix_eq_weights: &eq_weights.prefix_eq_weights, }, LinearCprScalarWeights { family_weights: &family_weights, @@ -798,7 +811,6 @@ mod tests { LinearCprWeights { row_weights: &row_weights, tail_eq_weights: &eq_weights.tail_eq_weights, - prefix_eq_weights: &eq_weights.prefix_eq_weights, }, LinearCprScalarWeights { family_weights: &family_weights, @@ -877,6 +889,43 @@ mod tests { ); } + #[test] + fn optimized_linear_cpr_flushes_dmr_for_small_modulus() { + let cfg = test_config(); + let trace_count = 2048usize; + let traces: Vec<_> = (0..trace_count) + .map(|_| trace_from_columns(&[u32::MAX], &[0])) + .collect(); + let families = vec![LinearFamilySpec { + family_idx: 0, + active_rows: vec![0], + terms: vec![LinearTermSpec { + source: LinearBinarySource::Column { col_idx: 0 }, + coeffs_by_active_row: vec![CoeffClass::Small(1)], + }], + }]; + let row_weights = RowWeights::new(&[], &cfg).unwrap(); + let max = -F::from_with_cfg(1u64, &cfg); + let tail_eq_weights = vec![max; trace_count]; + let family_weights = vec![F::one_with_cfg(&cfg)]; + let scalarization_powers = scalar_weights(); + let weights = LinearCprWeights { + row_weights: &row_weights, + tail_eq_weights: &tail_eq_weights, + }; + let scalar_weights = LinearCprScalarWeights { + family_weights: &family_weights, + scalarization_powers: &scalarization_powers, + }; + + let table = + build_linear_cpr_prefix_table(&traces, 0, &families, weights, scalar_weights, &cfg) + .unwrap(); + let expected = naive_linear_cpr_table(&traces, 0, &families, weights, scalar_weights); + + assert_eq!(table.values(), expected.as_slice()); + } + #[test] fn optimized_linear_cpr_validation_errors_are_reported() { let cfg = test_config(); @@ -893,7 +942,6 @@ mod tests { LinearCprWeights { row_weights: &row_weights, tail_eq_weights: &[F::one_with_cfg(&cfg)], - prefix_eq_weights: &[F::one_with_cfg(&cfg)], }, LinearCprScalarWeights { family_weights: &family_weights, @@ -910,7 +958,6 @@ mod tests { } )); - let wrong_prefix_weights = vec![F::one_with_cfg(&cfg); 4]; let err = build_linear_cpr_prefix_table( &traces, 2, @@ -918,7 +965,6 @@ mod tests { LinearCprWeights { row_weights: &row_weights, tail_eq_weights: &[], - prefix_eq_weights: &wrong_prefix_weights, }, LinearCprScalarWeights { family_weights: &family_weights, @@ -946,7 +992,6 @@ mod tests { LinearCprWeights { row_weights: &row_weights, tail_eq_weights: &eq_weights.tail_eq_weights, - prefix_eq_weights: &eq_weights.prefix_eq_weights, }, LinearCprScalarWeights { family_weights: &family_weights, diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index 4bcaf99e..ff3da139 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -5,12 +5,18 @@ //! protocol proof objects or verifier flow. pub mod accumulator; +pub mod booleanity; pub mod linear_cpr; pub mod sumfold; pub use accumulator::{ AccumulatorError, RowWeights, SmallValueBitAccumulator, accumulate_binary_column_projected, }; +pub use booleanity::{ + BooleanityAccumulatorError, BooleanityPrefixTable, BooleanityScalarWeights, BooleanityWeights, + ExtendedPrefixPoint, build_booleanity_prefix_table, extended_point_from_index, + extended_point_index, ternary_domain_size, +}; pub use linear_cpr::{ CoeffClass, LinearBinarySource, LinearCprAccumulatorError, LinearCprScalarWeights, LinearCprWeights, LinearFamilySpec, LinearTermSpec, SumFoldEqWeights, From a038a0b025f41553a5a46023665b4913c7d2d71a Mon Sep 17 00:00:00 2001 From: John Wu Date: Fri, 5 Jun 2026 23:35:20 -0700 Subject: [PATCH 13/49] Harden Hyrax PCS setup and openings --- protocol/benches/e2e.rs | 20 +- protocol/src/lib.rs | 86 +++++- protocol/src/verifier.rs | 15 + zip-plus/benches/hyrax_commit_breakdown.rs | 6 +- zip-plus/src/pcs/hyrax.rs | 337 ++++++++++++++++++--- zip-plus/src/pcs/msm_commitment.rs | 70 ++--- 6 files changed, 417 insertions(+), 117 deletions(-) diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index ca38cd02..27f657ad 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -1,6 +1,6 @@ #![allow(clippy::arithmetic_side_effects)] -use ark_ec::{AffineRepr, CurveGroup, PrimeGroup}; +use ark_ec::AffineRepr; use criterion::{ BatchSize, BenchmarkGroup, BenchmarkId, Criterion, criterion_group, criterion_main, measurement::WallTime, @@ -1242,23 +1242,9 @@ where { let pp = setup_pp_real_ecdsa(num_vars); let width = pp.0.linear_code.row_len(); - let generator = C::Group::generator(); - let bases = (1..=width) - .map(|idx| { - let scalar = C::ScalarField::from( - u64::try_from(idx).expect("Hyrax basis index must fit in u64"), - ); - (generator * scalar).into_affine() - }) - .collect(); - let h_scalar = C::ScalarField::from( - u64::try_from(width + 1).expect("Hyrax blinding basis index must fit in u64"), - ); - let h = generator * h_scalar; - let (ck, vk) = HyraxPCS::::setup_from_bases_with_blinding( + let (ck, vk) = HyraxPCS::::setup( width, - bases, - h, + b"zinc-plus-bench-real-sha256-hyrax", HyraxBlindingMode::Unblinded, ) .expect("Hyrax benchmark setup must be valid"); diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index cfe5ca46..456ee885 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -473,6 +473,8 @@ pub enum ProtocolError { Pcs(#[from] ZipError), #[error("PCS verification failed at column {0}: {1}")] PcsVerification(usize, ZipError), + #[error("PCS proof has trailing bytes: consumed {consumed} of {total}")] + PcsProofTrailingBytes { consumed: usize, total: usize }, } // @@ -784,7 +786,7 @@ mod tests { fixed_prime::field_cfg_from_curve_scalar, pcs::{AllZipPCSTypes, BinaryHyraxZipRest, PCSParams, PCSVerifierParams, ZincPCSTypes}, }; - use ark_ec::{AffineRepr, CurveGroup, PrimeGroup}; + use ark_ec::AffineRepr; use crypto_bigint::U64; use crypto_primitives::{ Field, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, @@ -819,7 +821,7 @@ mod tests { }, pcs::{ generic::ZipPlusPCS, - hyrax::{BinaryLanes, HyraxPCS}, + hyrax::{BinaryLanes, HyraxBlindingMode, HyraxPCS}, structs::{ZipPlus, ZipPlusParams}, }, pcs_transcript::PcsProverTranscript, @@ -1883,19 +1885,12 @@ mod tests { ), ); let width = pp.0.linear_code.row_len(); - let generator = C::Group::generator(); - let bases = (1..=width) - .map(|idx| { - let scalar = - C::ScalarField::from(u64::try_from(idx).expect("basis index fits in u64")); - (generator * scalar).into_affine() - }) - .collect(); - let h_scalar = - C::ScalarField::from(u64::try_from(width + 1).expect("basis index fits in u64")); - let (binary, binary_vk) = - HyraxPCS::::setup_from_bases(width, bases, generator * h_scalar) - .expect("Hyrax setup must be valid"); + let (binary, binary_vk) = HyraxPCS::::setup( + width, + b"zinc-plus-test-sha256-hyrax", + HyraxBlindingMode::Unblinded, + ) + .expect("Hyrax setup must be valid"); ( PCSParams::, TestShaEcdsaZincTypes, F, DEGREE_PLUS_ONE> { binary, @@ -1950,7 +1945,7 @@ mod tests { } #[test] - fn test_real_sha256_pcs_variants_round_trip() { + fn test_real_sha256_pcs_zip_bn_round_trip() { const NUM_VARS: usize = 9; let bn_field_cfg = field_cfg_from_curve_scalar::< @@ -1960,6 +1955,11 @@ mod tests { >(); let (zip_bn_pp, zip_bn_vp) = sha256_zip_pcs_params(NUM_VARS); run_sha256_pcs_round_trip::(&zip_bn_pp, &zip_bn_vp, bn_field_cfg); + } + + #[test] + fn test_real_sha256_pcs_zip_secp256k1_round_trip() { + const NUM_VARS: usize = 9; let secp_field_cfg = field_cfg_from_curve_scalar::< F, @@ -1968,6 +1968,11 @@ mod tests { >(); let (zip_secp_pp, zip_secp_vp) = sha256_zip_pcs_params(NUM_VARS); run_sha256_pcs_round_trip::(&zip_secp_pp, &zip_secp_vp, secp_field_cfg); + } + + #[test] + fn test_real_sha256_pcs_hyrax_bn_round_trip() { + const NUM_VARS: usize = 9; let bn_field_cfg = field_cfg_from_curve_scalar::< F, @@ -1980,6 +1985,11 @@ mod tests { &hyrax_bn_vp, bn_field_cfg, ); + } + + #[test] + fn test_real_sha256_pcs_hyrax_secp256k1_round_trip() { + const NUM_VARS: usize = 9; let secp_field_cfg = field_cfg_from_curve_scalar::< F, @@ -1995,6 +2005,50 @@ mod tests { ); } + #[test] + fn test_real_sha256_rejects_trailing_pcs_bytes() { + type U = Sha256CompressionSliceUair; + const NUM_VARS: usize = 9; + + let field_cfg = field_cfg_from_curve_scalar::< + F, + >::Fmod, + ark_bn254::G1Affine, + >(); + let (pp, vp) = sha256_zip_pcs_params(NUM_VARS); + let mut rng = rng(); + let trace = U::generate_random_trace(NUM_VARS, &mut rng); + let public_trace = trace.public(&U::signature()); + + let mut proof = + ZincPlusPiop::::prove_with_pcs_and_field_cfg::< + AllZipPCSTypes, + false, + CHECKED, + >(&pp, &trace, NUM_VARS, project_scalar_fn, field_cfg.clone()) + .expect("SHA PCS prover failed"); + proof.zip.extend_from_slice(b"trailing pcs bytes"); + + let result = + ZincPlusPiop::::verify_with_pcs_and_field_cfg::< + AllZipPCSTypes, + Sha256Ideal, + CHECKED, + >( + &vp, + proof, + &public_trace, + NUM_VARS, + project_scalar_fn, + sha256_test_project_ideal, + field_cfg, + ); + assert!(matches!( + result, + Err(ProtocolError::PcsProofTrailingBytes { .. }) + )); + } + /// `num_vars` for SHA-ECDSA tests. ECDSA's Shamir scalar /// multiplication needs `n_rows > 256`, so `num_vars >= 9`. const SHA_ECDSA_NUM_VARS: usize = 9; diff --git a/protocol/src/verifier.rs b/protocol/src/verifier.rs index 4bdd6da8..36afee67 100644 --- a/protocol/src/verifier.rs +++ b/protocol/src/verifier.rs @@ -67,6 +67,17 @@ fn filter_skipped_parent_evals( .collect() } +fn ensure_pcs_stream_consumed( + pcs_transcript: &PcsVerifierTranscript, +) -> Result<(), ProtocolError> { + let consumed = usize::try_from(pcs_transcript.stream.position()).unwrap_or(usize::MAX); + let total = pcs_transcript.stream.get_ref().len(); + if consumed != total { + return Err(ProtocolError::PcsProofTrailingBytes { consumed, total }); + } + Ok(()) +} + // // Shared base // @@ -1046,6 +1057,8 @@ where ) .map_err(|e| ProtocolError::PcsVerification(2, e))?; + ensure_pcs_stream_consumed::(pcs_transcript)?; + Ok(VerifierPcsVerified { _phantom: PhantomData, }) @@ -1692,6 +1705,7 @@ where } } + ensure_pcs_stream_consumed::(&pcs_transcript)?; Ok(()) } @@ -2694,5 +2708,6 @@ where t.step7_pcs_verify = _t_step7.elapsed(); } + ensure_pcs_stream_consumed::(&pcs_transcript)?; Ok(()) } diff --git a/zip-plus/benches/hyrax_commit_breakdown.rs b/zip-plus/benches/hyrax_commit_breakdown.rs index eb43eaa8..3382c001 100644 --- a/zip-plus/benches/hyrax_commit_breakdown.rs +++ b/zip-plus/benches/hyrax_commit_breakdown.rs @@ -34,11 +34,9 @@ fn msm_ck(width: usize) -> MsmCommitmentKey { } fn hyrax_ck(width: usize) -> HyraxCommitmentKey { - let (bases, h) = bases_and_h::(width); - HyraxPCS::::setup_from_bases_with_blinding( + HyraxPCS::::setup( width, - bases, - h, + b"zinc-plus-hyrax-commit-breakdown-bench", HyraxBlindingMode::Unblinded, ) .expect("benchmark setup must be valid") diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index d29a825a..9f63d4c2 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -1,6 +1,7 @@ #![allow(clippy::arithmetic_side_effects)] use std::{ + collections::HashSet, fmt::Debug, io::{Read, Write}, marker::PhantomData, @@ -43,6 +44,12 @@ pub enum HyraxBlindingMode { Unblinded, } +impl Default for HyraxBlindingMode { + fn default() -> Self { + Self::Unblinded + } +} + impl HyraxBlindingMode { fn as_u8(self) -> u8 { match self { @@ -131,6 +138,14 @@ where fn lane_to_scalar(value: Self::LaneValue) -> C::ScalarField; + fn commit_poly( + _ck: &HyraxCommitmentKey, + _poly: &DenseMultilinearExtension, + _num_rows: usize, + ) -> Option, Vec), ZipError>> { + None + } + fn accumulate_b( row: &[Eval], lane: usize, @@ -217,6 +232,61 @@ impl HyraxLanes, D> for BinaryLa } } + fn commit_poly( + ck: &HyraxCommitmentKey, + poly: &DenseMultilinearExtension>, + num_rows: usize, + ) -> Option, Vec), ZipError>> { + let expected_comm = , D>>::NUM_LANES * num_rows; + let mut comm = Vec::with_capacity(expected_comm); + let mut blinds = if ck.blinding_mode.is_blinded() { + Vec::with_capacity(expected_comm) + } else { + Vec::new() + }; + + Some((|| { + for lane in 0.., D>>::NUM_LANES { + let lane_blinds = if ck.blinding_mode.is_blinded() { + Some(MsmCommitmentEngine::::blind( + &ck.msm_ck, + poly.evaluations.len(), + )) + } else { + None + }; + + for (row_idx, row) in poly.evaluations.chunks(ck.num_cols).enumerate() { + let values = row + .iter() + .map(|eval| { + , D>>::lane_value(eval, lane) + }) + .collect::, _>>()?; + + let mut row_comm = if values.iter().copied().any(|bit| bit) { + , D>>::Strategy::msm_row( + &ck.msm_ck, &values, + ) + .map_err(msm_err)? + } else { + C::Group::zero() + }; + + if let Some(lane_blinds) = lane_blinds.as_ref() { + row_comm += ck.msm_ck.h * lane_blinds.blind[row_idx]; + } + comm.push(row_comm); + } + + if let Some(lane_blinds) = lane_blinds { + blinds.extend(lane_blinds.blind); + } + } + Ok((comm, blinds)) + })()) + } + fn accumulate_b( row: &[BinaryPoly], lane: usize, @@ -365,25 +435,26 @@ impl } impl HyraxPCS { - pub fn setup_from_bases( + pub fn setup( width: usize, - bases: Vec, - h: C::Group, + domain: impl AsRef<[u8]>, + blinding_mode: HyraxBlindingMode, ) -> Result<(HyraxCommitmentKey, HyraxVerifierKey), ZipError> { - Self::setup_from_bases_with_blinding(width, bases, h, HyraxBlindingMode::Blinded) + let domain = domain.as_ref(); + let bases = (0..width) + .map(|idx| hash_to_curve::(domain, b"basis", idx)) + .collect::, _>>()?; + let h = hash_to_curve::(domain, b"blinding", 0)?.into_group(); + Self::setup_from_trusted_bases(width, bases, h, blinding_mode) } - pub fn setup_from_bases_with_blinding( + pub fn setup_from_trusted_bases( width: usize, bases: Vec, h: C::Group, blinding_mode: HyraxBlindingMode, ) -> Result<(HyraxCommitmentKey, HyraxVerifierKey), ZipError> { - if !width.is_power_of_two() { - return Err(ZipError::InvalidPcsParam(format!( - "Hyrax row width must be a power of two, got {width}" - ))); - } + validate_trusted_bases(width, &bases, &h)?; let msm_ck = msm_key(width, bases.clone(), h)?; Ok(( HyraxCommitmentKey { @@ -451,6 +522,12 @@ where }; for poly in polys { + if let Some(result) = Lanes::commit_poly(ck, poly, num_rows) { + let (comm, blinds) = result?; + all_comm.extend(comm); + all_blinds.extend(blinds); + continue; + } for lane in 0..Lanes::NUM_LANES { let values = lane_values::(poly, lane)?; let commitment = if ck.blinding_mode.is_blinded() { @@ -826,6 +903,52 @@ fn validate_polys(polys: &[DenseMultilinearExtension]) -> Res Ok(()) } +fn validate_trusted_bases( + width: usize, + bases: &[C], + h: &C::Group, +) -> Result<(), ZipError> { + if !width.is_power_of_two() { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax row width must be a power of two, got {width}" + ))); + } + if bases.len() != width { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax expected {width} bases, got {}", + bases.len() + ))); + } + + let mut seen = HashSet::with_capacity(bases.len()); + for (idx, base) in bases.iter().copied().enumerate() { + if base.is_zero() { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax base {idx} is the identity" + ))); + } + if !seen.insert(base) { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax base {idx} duplicates an earlier base" + ))); + } + } + + let h_affine = h.clone().into_affine(); + if h_affine.is_zero() { + return Err(ZipError::InvalidPcsParam( + "Hyrax blinding base is the identity".to_string(), + )); + } + if seen.contains(&h_affine) { + return Err(ZipError::InvalidPcsParam( + "Hyrax blinding base duplicates a witness base".to_string(), + )); + } + + Ok(()) +} + fn validate_hyrax_shape( width: usize, blinding_mode: HyraxBlindingMode, @@ -897,6 +1020,51 @@ where .collect() } +fn hash_to_curve(domain: &[u8], label: &[u8], index: usize) -> Result { + let point_bytes = C::zero().serialized_size(Compress::Yes); + let mut counter = 0u64; + loop { + let mut hasher = blake3::Hasher::new(); + absorb_hash_part(&mut hasher, b"zinc-plus-hyrax-setup-v1")?; + absorb_hash_part(&mut hasher, domain)?; + absorb_hash_part(&mut hasher, label)?; + hasher.update( + &u64::try_from(index) + .map_err(|_| { + ZipError::InvalidPcsParam("Hyrax setup index does not fit u64".to_string()) + })? + .to_le_bytes(), + ); + hasher.update(&counter.to_le_bytes()); + + let mut bytes = vec![0u8; point_bytes]; + hasher.finalize_xof().fill(&mut bytes); + if let Some(point) = C::from_random_bytes(&bytes).map(|point| point.clear_cofactor()) { + if !point.is_zero() { + return Ok(point); + } + } + + counter = counter.checked_add(1).ok_or_else(|| { + ZipError::InvalidPcsParam("Hyrax hash-to-curve setup exhausted counters".to_string()) + })?; + } +} + +fn absorb_hash_part(hasher: &mut blake3::Hasher, part: &[u8]) -> Result<(), ZipError> { + hasher.update( + &u64::try_from(part.len()) + .map_err(|_| { + ZipError::InvalidPcsParam( + "Hyrax setup domain component length does not fit u64".to_string(), + ) + })? + .to_le_bytes(), + ); + hasher.update(part); + Ok(()) +} + fn int_to_scalar( value: &Int, ) -> Result { @@ -1173,6 +1341,112 @@ mod tests { assert!(matches!(result, Err(ZipError::InvalidPcsParam(_)))); } + #[test] + fn setup_derives_distinct_deterministic_bases() { + type C = ark_bn254::G1Affine; + let width = 32; + let (ck_0, vk_0) = HyraxPCS::::setup( + width, + b"zinc-plus-hyrax-setup-test", + HyraxBlindingMode::Unblinded, + ) + .unwrap(); + let (ck_1, vk_1) = HyraxPCS::::setup( + width, + b"zinc-plus-hyrax-setup-test", + HyraxBlindingMode::Unblinded, + ) + .unwrap(); + + assert_eq!(ck_0.msm_ck.bases, ck_1.msm_ck.bases); + assert_eq!(vk_0.bases, vk_1.bases); + assert_eq!(ck_0.msm_ck.h, ck_1.msm_ck.h); + assert_eq!(vk_0.h, vk_1.h); + assert_eq!(ck_0.blinding_mode, HyraxBlindingMode::Unblinded); + assert_eq!(vk_0.blinding_mode, HyraxBlindingMode::Unblinded); + assert!(ck_0.msm_ck.bases.iter().all(|base| !base.is_zero())); + assert!(!ck_0.msm_ck.h.is_zero()); + + let seen = ck_0.msm_ck.bases.iter().copied().collect::>(); + assert_eq!(seen.len(), width); + assert!(!seen.contains(&ck_0.msm_ck.h.into_affine())); + } + + #[test] + fn trusted_setup_rejects_bad_bases() { + type C = ark_bn254::G1Affine; + let width = 8; + let generator = ::Group::generator(); + let bases = (1..=width) + .map(|idx| (generator * ::ScalarField::from(idx as u64)).into_affine()) + .collect::>(); + let h = generator * ::ScalarField::from((width + 1) as u64); + + assert!(matches!( + HyraxPCS::::setup_from_trusted_bases( + 0, + Vec::new(), + ::Group::zero(), + HyraxBlindingMode::Unblinded, + ), + Err(ZipError::InvalidPcsParam(_)) + )); + + assert!(matches!( + HyraxPCS::::setup_from_trusted_bases( + width, + bases[..width - 1].to_vec(), + h, + HyraxBlindingMode::Unblinded, + ), + Err(ZipError::InvalidPcsParam(_)) + )); + + let mut identity_bases = bases.clone(); + identity_bases[0] = C::zero(); + assert!(matches!( + HyraxPCS::::setup_from_trusted_bases( + width, + identity_bases, + h, + HyraxBlindingMode::Unblinded, + ), + Err(ZipError::InvalidPcsParam(_)) + )); + + let mut duplicate_bases = bases.clone(); + duplicate_bases[1] = duplicate_bases[0]; + assert!(matches!( + HyraxPCS::::setup_from_trusted_bases( + width, + duplicate_bases, + h, + HyraxBlindingMode::Unblinded, + ), + Err(ZipError::InvalidPcsParam(_)) + )); + + assert!(matches!( + HyraxPCS::::setup_from_trusted_bases( + width, + bases.clone(), + ::Group::zero(), + HyraxBlindingMode::Unblinded, + ), + Err(ZipError::InvalidPcsParam(_)) + )); + + assert!(matches!( + HyraxPCS::::setup_from_trusted_bases( + width, + bases.clone(), + bases[0].into_group(), + HyraxBlindingMode::Unblinded, + ), + Err(ZipError::InvalidPcsParam(_)) + )); + } + fn binary_hyrax_open_verify_round_trip_with_modes( commit_mode: HyraxBlindingMode, verify_mode: HyraxBlindingMode, @@ -1187,21 +1461,14 @@ mod tests { let cfg = cfg_from_curve::(); let width = 512; - let generator = ::Group::generator(); - let bases = (1..=width) - .map(|idx| (generator * ::ScalarField::from(idx as u64)).into_affine()) - .collect::>(); - let h = generator * ::ScalarField::from((width + 1) as u64); - let (ck, _) = HyraxPCS::::setup_from_bases_with_blinding( + let (ck, _) = HyraxPCS::::setup( width, - bases.clone(), - h, + b"zinc-plus-hyrax-round-trip-test", commit_mode, )?; - let (_, vk) = HyraxPCS::::setup_from_bases_with_blinding( + let (_, vk) = HyraxPCS::::setup( width, - bases, - h, + b"zinc-plus-hyrax-round-trip-test", verify_mode, )?; @@ -1329,18 +1596,18 @@ mod tests { const D: usize = 32; let width = 8; - let generator = ::Group::generator(); - let bases = (1..=width) - .map(|idx| (generator * ::ScalarField::from(idx as u64)).into_affine()) - .collect::>(); - let h = generator * ::ScalarField::from((width + 1) as u64); - let (_, vk) = HyraxPCS::::setup_from_bases(width, bases, h).unwrap(); + let (_, vk) = HyraxPCS::::setup( + width, + b"zinc-plus-hyrax-empty-reject-test", + HyraxBlindingMode::Unblinded, + ) + .unwrap(); let commitment = HyraxCommitment:: { batch_size: 0, num_lanes: D, num_rows: 0, - blinding_mode: HyraxBlindingMode::Blinded, + blinding_mode: HyraxBlindingMode::Unblinded, comm: Vec::new(), }; let cfg = cfg_from_curve::(); @@ -1370,18 +1637,18 @@ mod tests { const D: usize = 32; let width = 8; - let generator = ::Group::generator(); - let bases = (1..=width) - .map(|idx| (generator * ::ScalarField::from(idx as u64)).into_affine()) - .collect::>(); - let h = generator * ::ScalarField::from((width + 1) as u64); - let (_, vk) = HyraxPCS::::setup_from_bases(width, bases, h).unwrap(); + let (_, vk) = HyraxPCS::::setup( + width, + b"zinc-plus-hyrax-empty-reject-test-2", + HyraxBlindingMode::Unblinded, + ) + .unwrap(); let commitment = HyraxCommitment:: { batch_size: 0, num_lanes: D, num_rows: 1, - blinding_mode: HyraxBlindingMode::Blinded, + blinding_mode: HyraxBlindingMode::Unblinded, comm: Vec::new(), }; let cfg = cfg_from_curve::(); diff --git a/zip-plus/src/pcs/msm_commitment.rs b/zip-plus/src/pcs/msm_commitment.rs index dce478ca..192513cd 100644 --- a/zip-plus/src/pcs/msm_commitment.rs +++ b/zip-plus/src/pcs/msm_commitment.rs @@ -9,7 +9,7 @@ use std::{ }; use ark_ec::{AffineRepr, CurveGroup}; -use ark_ff::{AdditiveGroup, BigInteger, One, PrimeField, UniformRand, Zero}; +use ark_ff::{AdditiveGroup, One, PrimeField, UniformRand, Zero}; use num_integer::Integer; use thiserror::Error; @@ -435,54 +435,27 @@ fn signed_window_pippenger( let num_bits = C::ScalarField::MODULUS_BIT_SIZE as usize; let segments = ::div_ceil(&num_bits, &window_bits); - let total_segments = segments + 1; - let n = scalars.len(); - let half = 1usize << (window_bits - 1); - let full = 1usize << window_bits; - let mut signed_digits = vec![0isize; total_segments * n]; - let mut carries = vec![0usize; n]; - - for (j, scalar) in scalars.iter().enumerate() { - let bits = scalar.into_bigint().to_bits_le(); - for segment in 0..segments { - let offset = segment * n; - let raw = window_value(&bits, segment * window_bits, window_bits) + carries[j]; - carries[j] = 0; - if raw >= half { - signed_digits[offset + j] = -isize::try_from(full - raw) - .map_err(|_| MsmError::InvalidWindowBits(window_bits))?; - carries[j] = 1; - } else { - signed_digits[offset + j] = - isize::try_from(raw).map_err(|_| MsmError::InvalidWindowBits(window_bits))?; - } - } - } - - let mut highest_segment = segments; - let carry_offset = segments * n; - for (j, carry) in carries.iter().copied().enumerate() { - if carry != 0 { - signed_digits[carry_offset + j] = - isize::try_from(carry).map_err(|_| MsmError::InvalidWindowBits(window_bits))?; - highest_segment = segments + 1; - } - } + let bucket_len = (1usize << window_bits) - 1; + let bigints = scalars + .iter() + .map(|scalar| scalar.into_bigint()) + .collect::>(); + let mut buckets = vec![C::Group::zero(); bucket_len]; let mut acc = C::Group::zero(); - for segment in (0..highest_segment).rev() { + for segment in (0..segments).rev() { for _ in 0..window_bits { acc.double_in_place(); } - let mut buckets = vec![C::Group::zero(); half]; - let offset = segment * n; - for j in 0..n { - let digit = signed_digits[offset + j]; - if digit > 0 { - buckets[digit as usize - 1] += bases[j]; - } else if digit < 0 { - buckets[(-digit) as usize - 1] -= bases[j]; + let offset = segment * window_bits; + for bucket in &mut buckets { + *bucket = C::Group::zero(); + } + for (j, scalar) in bigints.iter().enumerate() { + let digit = window_value_from_limbs(scalar.as_ref(), offset, window_bits); + if digit != 0 { + buckets[digit - 1] += bases[j]; } } @@ -502,9 +475,16 @@ fn scalar_window_bits(n: usize) -> usize { } } -fn window_value(bits: &[bool], start: usize, width: usize) -> usize { +fn window_value_from_limbs(limbs: &[u64], start: usize, width: usize) -> usize { (0..width).fold(0usize, |value, bit_idx| { - if bits.get(start + bit_idx).copied().unwrap_or(false) { + let absolute_bit = start + bit_idx; + let limb_idx = absolute_bit / u64::BITS as usize; + let limb_bit = absolute_bit % u64::BITS as usize; + if limbs + .get(limb_idx) + .map(|limb| ((limb >> limb_bit) & 1) == 1) + .unwrap_or(false) + { value | (1usize << bit_idx) } else { value From 7bec4b55852d57338db49c86cd0b9dc11aa40062 Mon Sep 17 00:00:00 2001 From: John Wu Date: Sat, 6 Jun 2026 00:22:39 -0700 Subject: [PATCH 14/49] Add production ProjectionFold SHA helpers --- piop/src/ideal_check.rs | 41 + piop/src/neutron_nova/mod.rs | 15 + piop/src/neutron_nova/projection_sha.rs | 1839 +++++++++++++++++++++++ 3 files changed, 1895 insertions(+) create mode 100644 piop/src/neutron_nova/projection_sha.rs diff --git a/piop/src/ideal_check.rs b/piop/src/ideal_check.rs index 9c8d51fc..68c1a717 100644 --- a/piop/src/ideal_check.rs +++ b/piop/src/ideal_check.rs @@ -3,6 +3,7 @@ mod batched_ideal_check; mod combined_poly_builder; mod structs; +pub(crate) use batched_ideal_check::batched_ideal_check; pub use structs::*; #[cfg(feature = "parallel")] @@ -406,6 +407,12 @@ where let mut transcription_buf: Vec = vec![0; F::Inner::NUM_BYTES]; let combined_mle_values = proof.combined_mle_values; + if combined_mle_values.len() != num_constraints { + return Err(IdealCheckError::ProofValueCount { + got: combined_mle_values.len(), + expected: num_constraints, + }); + } let evaluation_point = transcript.get_field_challenges(num_vars, field_cfg); @@ -445,6 +452,8 @@ pub enum IdealCheckError { IdealCollectorError(#[from] BatchedIdealCheckError, I>), #[error("`eq` polynomial construction failure: {0}")] EqPolyConstructionError(#[from] PolyArithErrors), + #[error("ideal-check proof value count mismatch: got {got}, expected {expected}")] + ProofValueCount { got: usize, expected: usize }, } #[cfg(test)] @@ -578,4 +587,36 @@ mod tests { |_ideal_over_ring| IdealOrZero::>::zero(), ); } + + #[test] + fn verifier_rejects_truncated_proof_values() { + let field_cfg = test_config(); + let num_vars = 2; + let mut rng = rng(); + let transcript = Blake3Transcript::new(); + type U = TestUairNoMultiplication>; + + let (mut proof, ..) = run_ideal_check_prover_linear::( + num_vars, + &U::generate_random_trace(num_vars, &mut rng), + &mut transcript.clone(), + ); + let num_constraints = count_constraints::(); + proof.combined_mle_values.pop(); + + let result = U::verify_as_subprotocol( + &mut transcript.clone(), + proof, + num_constraints, + num_vars, + |ideal_over_ring| ideal_over_ring.map(|i| DegreeOneIdeal::from_with_cfg(i, &field_cfg)), + &field_cfg, + ); + + assert!(matches!( + result, + Err(IdealCheckError::ProofValueCount { got, expected }) + if got + 1 == expected && expected == num_constraints + )); + } } diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index ff3da139..0dd832bd 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -7,6 +7,7 @@ pub mod accumulator; pub mod booleanity; pub mod linear_cpr; +pub mod projection_sha; pub mod sumfold; pub use accumulator::{ @@ -22,4 +23,18 @@ pub use linear_cpr::{ LinearCprWeights, LinearFamilySpec, LinearTermSpec, SumFoldEqWeights, build_linear_cpr_prefix_table, build_sumfold_eq_weights, }; +pub use projection_sha::{ + FoldedCommitments, FoldedShaAccumulator, FoldedShaWitness, FreshShaIdealCache, + NUM_NONZERO_SHA_FAMILIES, NUM_SHA_RESIDUAL_FAMILIES, ProjectedShaPublic, ProjectedShaTrace, + SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, ShaBitSliceColumns, ShaBooleanitySource, ShaIntCol, + ShaIntColumns, ShaProductionIdeal, ShaProjectionError, ShaPublicCol, ShaPublicColumns, + ShaResidualFamily, ShaScalarizedRows, ShaSumFoldOutput, ShaWordCol, VirtualChMajValues, + build_folded_row_sumcheck_group, build_fresh_sha_ideal_cache, build_sha_ideal_values_at_point, + check_fresh_sha_ideal_cache, check_sha_ideal_values, evaluate_fresh_sha_targets, + finalize_sha_sumfold, fold_projected_sha_traces, folded_row_integrand_sum, + folded_row_integrand_values, production_sha_nonzero_families, production_sha_nonzero_ideals, + reconstruct_virtual_ch_maj_at_row, scalarize_trace_words, verify_folded_row_sumcheck_claim, + verify_folded_scalarization_links, verify_folded_scalarization_links_at_point, + verify_folded_shifted_scalarization_link_at_point, +}; pub use sumfold::{LinearInstanceClaims, LinearPrefixTable, SumFoldError}; diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs new file mode 100644 index 00000000..9adb5bb6 --- /dev/null +++ b/piop/src/neutron_nova/projection_sha.rs @@ -0,0 +1,1839 @@ +//! Production SHA-256 ProjectionFold helpers. +//! +//! This module implements the SHA-specific data model and reference +//! computations used by the production ProjectionFold flow: +//! +//! fresh ideal checks -> SumFold over instances -> post-SumFold folding -> +//! folded row check over the 128-row SHA domain. + +use crate::ideal_check::batched_ideal_check; +use crate::neutron_nova::SumFoldError; +use crate::sumcheck::multi_degree::MultiDegreeSumcheckGroup; +use crypto_primitives::PrimeField; +use num_traits::{ConstZero, Zero}; +use thiserror::Error; +use zinc_poly::{ + EvaluatablePolynomial, + mle::DenseMultilinearExtension, + univariate::dynamic::over_field::DynamicPolynomialF, + utils::{ArithErrors, build_eq_x_r_vec, eq_eval}, +}; +use zinc_uair::{ + ideal::{Ideal, IdealCheck, IdealCheckError, rotation::RotationIdeal}, + ideal_collector::IdealOrZero, +}; +use zinc_utils::{ + delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, + inner_transparent_field::InnerTransparentField, powers, +}; + +pub const SHA_ROW_VARS: usize = 7; +pub const SHA_ROW_COUNT: usize = 128; +pub const SHA_WORD_BITS: usize = 32; +pub const NUM_SHA_RESIDUAL_FAMILIES: usize = 18; +pub const NUM_NONZERO_SHA_FAMILIES: usize = 7; + +const NONZERO_SHA_FAMILIES: [ShaResidualFamily; NUM_NONZERO_SHA_FAMILIES] = [ + ShaResidualFamily::R0BigSigmaA, + ShaResidualFamily::R1BigSigmaE, + ShaResidualFamily::R4Schedule, + ShaResidualFamily::R5UpdateA, + ShaResidualFamily::R6UpdateE, + ShaResidualFamily::R9FeedForwardA, + ShaResidualFamily::R10FeedForwardE, +]; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ShaResidualFamily { + R0BigSigmaA, + R1BigSigmaE, + R2SmallSigma0, + R3SmallSigma1, + R4Schedule, + R5UpdateA, + R6UpdateE, + R7PinA, + R8PinE, + R9FeedForwardA, + R10FeedForwardE, + R11MessagePin, + R12CompSchedule, + R13CompUpdateA, + R14CompUpdateE, + R15CompFeedForwardA, + R16CompFeedForwardE, + R17CarryHighBits, +} + +impl ShaResidualFamily { + pub const ALL: [Self; NUM_SHA_RESIDUAL_FAMILIES] = [ + Self::R0BigSigmaA, + Self::R1BigSigmaE, + Self::R2SmallSigma0, + Self::R3SmallSigma1, + Self::R4Schedule, + Self::R5UpdateA, + Self::R6UpdateE, + Self::R7PinA, + Self::R8PinE, + Self::R9FeedForwardA, + Self::R10FeedForwardE, + Self::R11MessagePin, + Self::R12CompSchedule, + Self::R13CompUpdateA, + Self::R14CompUpdateE, + Self::R15CompFeedForwardA, + Self::R16CompFeedForwardE, + Self::R17CarryHighBits, + ]; + + pub fn index(self) -> usize { + match self { + Self::R0BigSigmaA => 0, + Self::R1BigSigmaE => 1, + Self::R2SmallSigma0 => 2, + Self::R3SmallSigma1 => 3, + Self::R4Schedule => 4, + Self::R5UpdateA => 5, + Self::R6UpdateE => 6, + Self::R7PinA => 7, + Self::R8PinE => 8, + Self::R9FeedForwardA => 9, + Self::R10FeedForwardE => 10, + Self::R11MessagePin => 11, + Self::R12CompSchedule => 12, + Self::R13CompUpdateA => 13, + Self::R14CompUpdateE => 14, + Self::R15CompFeedForwardA => 15, + Self::R16CompFeedForwardE => 16, + Self::R17CarryHighBits => 17, + } + } + + pub fn is_nonzero_ideal(self) -> bool { + matches!( + self, + Self::R0BigSigmaA + | Self::R1BigSigmaE + | Self::R4Schedule + | Self::R5UpdateA + | Self::R6UpdateE + | Self::R9FeedForwardA + | Self::R10FeedForwardE + ) + } +} + +#[repr(usize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ShaWordCol { + A = 0, + E = 1, + Sigma0 = 2, + Sigma1 = 3, + W = 4, + SmallSigma0 = 5, + SmallSigma1 = 6, + Uef = 7, + UNegEg = 8, + Maj = 9, + MuPacked = 10, + OvSigma0 = 11, + OvSigma1 = 12, + OvSmallSigma0 = 13, + OvSmallSigma1 = 14, + Ch2Comp = 15, + MajComp = 16, +} + +impl ShaWordCol { + pub const COUNT: usize = 17; + + pub fn index(self) -> usize { + self as usize + } +} + +#[repr(usize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ShaIntCol { + CompSchedule = 0, + CompUpdateA = 1, + CompUpdateE = 2, + CompFeedForwardA = 3, + CompFeedForwardE = 4, +} + +impl ShaIntCol { + pub const COUNT: usize = 5; + + pub fn index(self) -> usize { + self as usize + } +} + +#[repr(usize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ShaPublicCol { + K = 0, + PAIn = 1, + PEIn = 2, + PAOut = 3, + PEOut = 4, + Message = 5, + SInit = 6, + SMsg = 7, + SSched = 8, + SUpd = 9, + SFf = 10, + SOut = 11, +} + +impl ShaPublicCol { + pub const COUNT: usize = 12; + + pub fn index(self) -> usize { + self as usize + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShaBitSliceColumns { + /// Indexed as `[word_col][row][bit]`. + pub columns: Vec>>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShaScalarizedRows { + /// Indexed as `[word_col][row]`. + pub words: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShaIntColumns { + /// Indexed as `[int_col][row]`. + pub columns: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShaPublicColumns { + /// Indexed as `[public_col][row]`. + pub columns: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProjectedShaTrace { + pub rows: usize, + pub bit_slices: ShaBitSliceColumns, + pub scalarized_words: ShaScalarizedRows, + pub int_columns: ShaIntColumns, + pub public_columns: ShaPublicColumns, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProjectedShaPublic { + pub columns: ShaPublicColumns, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FreshShaIdealCache { + pub r_ic: [F; SHA_ROW_VARS], + pub ideal_polys: Vec<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]>, + pub taus_at_a: Vec<[F; NUM_NONZERO_SHA_FAMILIES]>, + pub fresh_targets: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShaSumFoldOutput { + r_b: Vec, + c_sf: F, + t_prime: F, + theta: Vec, +} + +impl ShaSumFoldOutput { + pub fn r_b(&self) -> &[F] { + &self.r_b + } + + pub fn c_sf(&self) -> &F { + &self.c_sf + } + + pub fn t_prime(&self) -> &F { + &self.t_prime + } + + pub fn theta(&self) -> &[F] { + &self.theta + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FoldedCommitments { + pub commitments: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FoldedShaAccumulator { + pub t_prime: F, + pub folded_commitments: FoldedCommitments, + pub folded_public: ProjectedShaPublic, + pub r_b: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FoldedShaWitness { + pub trace: ProjectedShaTrace, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ShaBooleanitySource { + WordBit { col: ShaWordCol, bit: usize }, + VirtualCh1 { bit: usize }, + VirtualCh2 { bit: usize }, + VirtualMaj { bit: usize }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VirtualChMajValues { + pub ch1: [F; SHA_WORD_BITS], + pub ch2: [F; SHA_WORD_BITS], + pub maj: [F; SHA_WORD_BITS], +} + +#[derive(Clone, Debug)] +pub enum ShaProductionIdeal { + RotX2(RotationIdeal), + RotXw1, +} + +impl FromRef> for ShaProductionIdeal { + fn from_ref(value: &ShaProductionIdeal) -> Self { + value.clone() + } +} + +impl Ideal for ShaProductionIdeal {} + +impl IdealCheck> for ShaProductionIdeal { + fn contains(&self, value: &DynamicPolynomialF) -> Result { + match self { + ShaProductionIdeal::RotX2(ideal) => IdealOrZero::NonZero(ideal.clone()).contains(value), + ShaProductionIdeal::RotXw1 => { + if value.coeffs.is_empty() { + return Ok(true); + } + let one = F::one_with_cfg(value.coeffs[0].cfg()); + IdealOrZero::NonZero(RotationIdeal::::new(one)).contains(value) + } + } + } +} + +#[derive(Clone, Debug, Error)] +pub enum ShaProjectionError { + #[error("expected {expected} rows, got {got}")] + RowCount { expected: usize, got: usize }, + #[error("row index out of range: {row}")] + RowIndexOutOfRange { row: usize }, + #[error("{kind} column {col} is missing")] + MissingColumn { kind: &'static str, col: usize }, + #[error("{kind} column {col} row length mismatch: got {got}, expected {expected}")] + ColumnRowCount { + kind: &'static str, + col: usize, + got: usize, + expected: usize, + }, + #[error("word column {col} row {row} bit length mismatch: got {got}, expected {expected}")] + BitCount { + col: usize, + row: usize, + got: usize, + expected: usize, + }, + #[error("row-batching point length mismatch: got {got}, expected 7")] + RowPointLength { got: usize }, + #[error("instance count must be a power of two, got {got}")] + InstanceCountNotPowerOfTwo { got: usize }, + #[error("instance count mismatch: got {got}, expected {expected}")] + InstanceCountMismatch { got: usize, expected: usize }, + #[error("folding weight count mismatch: got {got}, expected {expected}")] + FoldingWeightCount { got: usize, expected: usize }, + #[error("SumFold denominator eq(beta, r_b) is zero")] + ZeroSumFoldDenominator, + #[error("scalarization mismatch for word column {col}")] + ScalarizationMismatch { col: usize }, + #[error("folded row sumcheck claim does not match SumFold target")] + FoldedRowClaimMismatch, + #[error("booleanity bit index out of range: {bit}")] + BitIndexOutOfRange { bit: usize }, + #[error("ideal membership check failed")] + IdealMembership, + #[error("polynomial evaluation failed: {0}")] + PolynomialEvaluation(#[from] zinc_poly::EvaluationError), + #[error("equality table construction failed: {0}")] + EqTable(#[from] ArithErrors), + #[error("sumfold helper failed: {0}")] + SumFold(#[from] SumFoldError), +} + +pub fn production_sha_nonzero_families() -> &'static [ShaResidualFamily] { + &NONZERO_SHA_FAMILIES +} + +pub fn production_sha_nonzero_ideals( + field_cfg: &F::Config, +) -> [ShaProductionIdeal; NUM_NONZERO_SHA_FAMILIES] { + let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); + [ + ShaProductionIdeal::RotXw1, + ShaProductionIdeal::RotXw1, + ShaProductionIdeal::RotX2(RotationIdeal::new(two.clone())), + ShaProductionIdeal::RotX2(RotationIdeal::new(two.clone())), + ShaProductionIdeal::RotX2(RotationIdeal::new(two.clone())), + ShaProductionIdeal::RotX2(RotationIdeal::new(two.clone())), + ShaProductionIdeal::RotX2(RotationIdeal::new(two)), + ] +} + +pub fn build_sha_ideal_values_at_point( + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + r_ic: &[F; SHA_ROW_VARS], + field_cfg: &F::Config, +) -> Result<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], ShaProjectionError> +where + F: PrimeField, +{ + validate_trace(trace)?; + validate_public(public)?; + + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + let mut out: [DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES] = + std::array::from_fn(|_| DynamicPolynomialF::ZERO); + + for (row, row_weight) in row_weights.iter().enumerate().take(SHA_ROW_COUNT) { + let residuals = residual_polys_at_row(trace, public, row, field_cfg)?; + for (slot, family) in NONZERO_SHA_FAMILIES.iter().enumerate() { + let weighted = scale_poly(&residuals[family.index()], row_weight); + out[slot] += &weighted; + } + } + out.iter_mut().for_each(DynamicPolynomialF::trim); + Ok(out) +} + +pub fn check_sha_ideal_values( + values: &[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], + field_cfg: &F::Config, +) -> Result<(), ShaProjectionError> +where + F: PrimeField, +{ + let ideals = production_sha_nonzero_ideals(field_cfg); + batched_ideal_check(&ideals, values).map_err(|_err| ShaProjectionError::IdealMembership) +} + +pub fn build_fresh_sha_ideal_cache( + traces: &[ProjectedShaTrace], + publics: &[ProjectedShaPublic], + r_ic: [F; SHA_ROW_VARS], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + if traces.len() != publics.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: publics.len(), + expected: traces.len(), + }); + } + let ideal_polys = traces + .iter() + .zip(publics) + .map(|(trace, public)| build_sha_ideal_values_at_point(trace, public, &r_ic, field_cfg)) + .collect::, _>>()?; + + Ok(FreshShaIdealCache { + r_ic, + ideal_polys, + taus_at_a: Vec::new(), + fresh_targets: Vec::new(), + }) +} + +pub fn check_fresh_sha_ideal_cache( + cache: &FreshShaIdealCache, + field_cfg: &F::Config, +) -> Result<(), ShaProjectionError> +where + F: PrimeField, +{ + for values in &cache.ideal_polys { + check_sha_ideal_values(values, field_cfg)?; + } + Ok(()) +} + +pub fn evaluate_fresh_sha_targets( + cache: &mut FreshShaIdealCache, + a: &F, + lambda: &F, + field_cfg: &F::Config, +) -> Result<(), ShaProjectionError> +where + F: PrimeField, +{ + let one = F::one_with_cfg(field_cfg); + let zero = F::zero_with_cfg(field_cfg); + let lambda_powers = powers(lambda.clone(), one, NUM_SHA_RESIDUAL_FAMILIES); + + cache.taus_at_a.clear(); + cache.fresh_targets.clear(); + + for ideal_polys in &cache.ideal_polys { + let taus: [F; NUM_NONZERO_SHA_FAMILIES] = std::array::from_fn(|idx| { + ideal_polys[idx] + .evaluate_at_point(a) + .expect("field polynomial evaluation cannot fail") + }); + let mut target = zero.clone(); + for (slot, family) in NONZERO_SHA_FAMILIES.iter().enumerate() { + target += lambda_powers[family.index()].clone() * &taus[slot]; + } + cache.taus_at_a.push(taus); + cache.fresh_targets.push(target); + } + Ok(()) +} + +pub fn finalize_sha_sumfold( + beta: &[F], + r_b: Vec, + c_sf: F, + instance_count: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + if !instance_count.is_power_of_two() { + return Err(ShaProjectionError::InstanceCountNotPowerOfTwo { + got: instance_count, + }); + } + let ell = usize::try_from(instance_count.trailing_zeros()).expect("ell fits usize"); + if beta.len() != ell { + return Err(ShaProjectionError::InstanceCountMismatch { + got: beta.len(), + expected: ell, + }); + } + if r_b.len() != ell { + return Err(ShaProjectionError::InstanceCountMismatch { + got: r_b.len(), + expected: ell, + }); + } + + let one = F::one_with_cfg(field_cfg); + let d = eq_eval(beta, &r_b, one)?; + if F::is_zero(&d) { + return Err(ShaProjectionError::ZeroSumFoldDenominator); + } + + let theta = build_eq_x_r_vec(&r_b, field_cfg)?; + debug_assert_eq!(theta.len(), instance_count); + let t_prime = c_sf.clone() / d; + Ok(ShaSumFoldOutput { + r_b, + c_sf, + t_prime, + theta, + }) +} + +pub fn fold_projected_sha_traces( + traces: &[ProjectedShaTrace], + publics: &[ProjectedShaPublic], + sumfold: &ShaSumFoldOutput, + field_cfg: &F::Config, +) -> Result<(FoldedShaWitness, ProjectedShaPublic), ShaProjectionError> +where + F: PrimeField, +{ + if traces.len() != publics.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: publics.len(), + expected: traces.len(), + }); + } + if sumfold.theta.len() != traces.len() { + return Err(ShaProjectionError::FoldingWeightCount { + got: sumfold.theta.len(), + expected: traces.len(), + }); + } + for trace in traces { + validate_trace(trace)?; + } + for public in publics { + validate_public(public)?; + } + + let folded_trace = ProjectedShaTrace { + rows: SHA_ROW_COUNT, + bit_slices: ShaBitSliceColumns { + columns: fold_3d( + traces.iter().map(|trace| &trace.bit_slices.columns), + &sumfold.theta, + field_cfg, + )?, + }, + scalarized_words: ShaScalarizedRows { + words: fold_2d( + traces.iter().map(|trace| &trace.scalarized_words.words), + &sumfold.theta, + field_cfg, + )?, + }, + int_columns: ShaIntColumns { + columns: fold_2d( + traces.iter().map(|trace| &trace.int_columns.columns), + &sumfold.theta, + field_cfg, + )?, + }, + public_columns: ShaPublicColumns { + columns: fold_2d( + traces.iter().map(|trace| &trace.public_columns.columns), + &sumfold.theta, + field_cfg, + )?, + }, + }; + let folded_public = ProjectedShaPublic { + columns: ShaPublicColumns { + columns: fold_2d( + publics.iter().map(|public| &public.columns.columns), + &sumfold.theta, + field_cfg, + )?, + }, + }; + + Ok(( + FoldedShaWitness { + trace: folded_trace, + }, + folded_public, + )) +} + +pub fn scalarize_trace_words( + bit_slices: &ShaBitSliceColumns, + a: &F, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + let powers = powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); + let mut words = Vec::with_capacity(bit_slices.columns.len()); + for (col_idx, col) in bit_slices.columns.iter().enumerate() { + if col.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "bit_slices", + col: col_idx, + got: col.len(), + expected: SHA_ROW_COUNT, + }); + } + let mut out_col = Vec::with_capacity(SHA_ROW_COUNT); + for (row, bits) in col.iter().enumerate() { + if bits.len() != SHA_WORD_BITS { + return Err(ShaProjectionError::BitCount { + col: col_idx, + row, + got: bits.len(), + expected: SHA_WORD_BITS, + }); + } + out_col.push(project_bits(bits, &powers, field_cfg)); + } + words.push(out_col); + } + Ok(ShaScalarizedRows { words }) +} + +pub fn verify_folded_scalarization_links( + trace: &ProjectedShaTrace, + a: &F, + word_cols: &[ShaWordCol], + field_cfg: &F::Config, +) -> Result<(), ShaProjectionError> +where + F: PrimeField + DelayedFieldProductSum, +{ + validate_trace(trace)?; + let powers = powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); + for col in word_cols { + let col_idx = col.index(); + let bit_col = + trace + .bit_slices + .columns + .get(col_idx) + .ok_or(ShaProjectionError::MissingColumn { + kind: "bit_slices", + col: col_idx, + })?; + let scalar_col = + trace + .scalarized_words + .words + .get(col_idx) + .ok_or(ShaProjectionError::MissingColumn { + kind: "scalarized_words", + col: col_idx, + })?; + for row in 0..SHA_ROW_COUNT { + let recombined = project_bits(&bit_col[row], &powers, field_cfg); + if recombined != scalar_col[row] { + return Err(ShaProjectionError::ScalarizationMismatch { col: col_idx }); + } + } + } + Ok(()) +} + +pub fn verify_folded_scalarization_links_at_point( + trace: &ProjectedShaTrace, + a: &F, + r_star: &[F; SHA_ROW_VARS], + word_cols: &[ShaWordCol], + field_cfg: &F::Config, +) -> Result<(), ShaProjectionError> +where + F: PrimeField + DelayedFieldProductSum, +{ + for col in word_cols { + verify_folded_shifted_scalarization_link_at_point(trace, a, r_star, *col, 0, field_cfg)?; + } + Ok(()) +} + +pub fn verify_folded_shifted_scalarization_link_at_point( + trace: &ProjectedShaTrace, + a: &F, + r_star: &[F; SHA_ROW_VARS], + col: ShaWordCol, + shift: usize, + field_cfg: &F::Config, +) -> Result<(), ShaProjectionError> +where + F: PrimeField + DelayedFieldProductSum, +{ + validate_trace(trace)?; + let row_weights = build_eq_x_r_vec(r_star, field_cfg)?; + let powers = powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); + let mut word_eval = F::zero_with_cfg(field_cfg); + let mut bit_eval = F::zero_with_cfg(field_cfg); + + for (row, row_weight) in row_weights.iter().enumerate() { + word_eval += row_weight.clone() + * scalarized_word_at_shifted_or_zero(trace, col, row, shift, field_cfg)?; + for (bit, power) in powers.iter().enumerate() { + let source_bit = bit_at_shifted_or_zero(trace, col, row, shift, bit, field_cfg)?; + bit_eval += row_weight.clone() * power.clone() * source_bit; + } + } + + if word_eval != bit_eval { + return Err(ShaProjectionError::ScalarizationMismatch { col: col.index() }); + } + Ok(()) +} + +pub fn reconstruct_virtual_ch_maj_at_row( + trace: &ProjectedShaTrace, + row: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + validate_trace(trace)?; + if row >= SHA_ROW_COUNT { + return Err(ShaProjectionError::RowIndexOutOfRange { row }); + } + let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); + let ch1 = build_virtual_bit_array(|bit| { + Ok( + bit_at_shifted_or_zero(trace, ShaWordCol::E, row, 2, bit, field_cfg)? + + bit_at_shifted_or_zero(trace, ShaWordCol::E, row, 1, bit, field_cfg)? + - two.clone() + * bit_at_shifted_or_zero(trace, ShaWordCol::Uef, row, 2, bit, field_cfg)?, + ) + }); + let ch2 = build_virtual_bit_array(|bit| { + Ok( + bit_at_shifted_or_zero(trace, ShaWordCol::E, row, 2, bit, field_cfg)? + - bit_at_shifted_or_zero(trace, ShaWordCol::E, row, 0, bit, field_cfg)? + + two.clone() + * bit_at_shifted_or_zero(trace, ShaWordCol::UNegEg, row, 2, bit, field_cfg)? + + two.clone() + * bit_at_shifted_or_zero(trace, ShaWordCol::Ch2Comp, row, 0, bit, field_cfg)?, + ) + }); + let maj = build_virtual_bit_array(|bit| { + Ok( + bit_at_shifted_or_zero(trace, ShaWordCol::A, row, 0, bit, field_cfg)? + + bit_at_shifted_or_zero(trace, ShaWordCol::A, row, 1, bit, field_cfg)? + + bit_at_shifted_or_zero(trace, ShaWordCol::A, row, 2, bit, field_cfg)? + - two.clone() + * bit_at_shifted_or_zero(trace, ShaWordCol::Maj, row, 2, bit, field_cfg)? + - two.clone() + * bit_at_shifted_or_zero(trace, ShaWordCol::MajComp, row, 0, bit, field_cfg)?, + ) + }); + + Ok(VirtualChMajValues { + ch1: ch1?, + ch2: ch2?, + maj: maj?, + }) +} + +pub fn folded_row_integrand_values( + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum, + F::Inner: Zero, +{ + validate_trace(trace)?; + validate_public(public)?; + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + let lambda_powers = powers( + lambda.clone(), + F::one_with_cfg(field_cfg), + NUM_SHA_RESIDUAL_FAMILIES, + ); + let rho_powers = powers( + rho.clone(), + F::one_with_cfg(field_cfg), + booleanity_sources.len(), + ); + + let mut out = Vec::with_capacity(SHA_ROW_COUNT); + for row in 0..SHA_ROW_COUNT { + let residuals = residual_values_at_row(trace, public, row, a, field_cfg)?; + let mut linear = F::zero_with_cfg(field_cfg); + for family in ShaResidualFamily::ALL { + linear += lambda_powers[family.index()].clone() * &residuals[family.index()]; + } + + let mut bool_sum = F::zero_with_cfg(field_cfg); + for (idx, source) in booleanity_sources.iter().enumerate() { + let d = booleanity_source_value_at_row(trace, row, source, field_cfg)?; + let term = d.clone() * (d - F::one_with_cfg(field_cfg)); + bool_sum += rho_powers[idx].clone() * term; + } + out.push(row_weights[row].clone() * (linear + xi.clone() * bool_sum)); + } + Ok(out) +} + +pub fn build_folded_row_sumcheck_group( + row_integrand_values: &[F], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + if row_integrand_values.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_integrand", + col: 0, + got: row_integrand_values.len(), + expected: SHA_ROW_COUNT, + }); + } + let zero_inner = F::zero_with_cfg(field_cfg).inner().clone(); + let integrand = DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + row_integrand_values + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner, + ); + Ok(MultiDegreeSumcheckGroup::new( + 1, + vec![integrand], + Box::new(|values: &[F]| values[0].clone()), + )) +} + +pub fn folded_row_integrand_sum( + row_integrand_values: &[F], + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + if row_integrand_values.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_integrand", + col: 0, + got: row_integrand_values.len(), + expected: SHA_ROW_COUNT, + }); + } + Ok(row_integrand_values + .iter() + .fold(F::zero_with_cfg(field_cfg), |acc, value| acc + value)) +} + +pub fn verify_folded_row_sumcheck_claim( + claimed_sum: &F, + t_prime: &F, +) -> Result<(), ShaProjectionError> +where + F: PrimeField, +{ + if claimed_sum != t_prime { + return Err(ShaProjectionError::FoldedRowClaimMismatch); + } + Ok(()) +} + +pub fn residual_polys_at_row( + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + row: usize, + field_cfg: &F::Config, +) -> Result<[DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES], ShaProjectionError> +where + F: PrimeField, +{ + let zero = F::zero_with_cfg(field_cfg); + let one = F::one_with_cfg(field_cfg); + let two = one.clone() + &one; + let rho_sig0 = sparse_poly::(&[10, 19, 30], field_cfg); + let rho_sig1 = sparse_poly::(&[7, 21, 26], field_cfg); + + let a = word_poly(trace, ShaWordCol::A, row, field_cfg)?; + let e = word_poly(trace, ShaWordCol::E, row, field_cfg)?; + let sigma0 = word_poly(trace, ShaWordCol::Sigma0, row, field_cfg)?; + let sigma1 = word_poly(trace, ShaWordCol::Sigma1, row, field_cfg)?; + let w = word_poly(trace, ShaWordCol::W, row, field_cfg)?; + let small_sigma0 = word_poly(trace, ShaWordCol::SmallSigma0, row, field_cfg)?; + let small_sigma1 = word_poly(trace, ShaWordCol::SmallSigma1, row, field_cfg)?; + let ov_sigma0 = word_poly(trace, ShaWordCol::OvSigma0, row, field_cfg)?; + let ov_sigma1 = word_poly(trace, ShaWordCol::OvSigma1, row, field_cfg)?; + let ov_small_sigma0 = word_poly(trace, ShaWordCol::OvSmallSigma0, row, field_cfg)?; + let ov_small_sigma1 = word_poly(trace, ShaWordCol::OvSmallSigma1, row, field_cfg)?; + + let r0 = (&a * &rho_sig0) - &sigma0 - &scale_poly(&ov_sigma0, &two); + let r1 = (&e * &rho_sig1) - &sigma1 - &scale_poly(&ov_sigma1, &two); + let r2 = word_poly(trace, ShaWordCol::W, row, field_cfg)?.rot_c(25) + + &word_poly(trace, ShaWordCol::W, row, field_cfg)?.rot_c(14) + + &word_poly(trace, ShaWordCol::W, row, field_cfg)?.shift_r_c(3) + - &small_sigma0 + - &scale_poly(&ov_small_sigma0, &two); + let r3 = word_poly(trace, ShaWordCol::W, row, field_cfg)?.rot_c(15) + + &word_poly(trace, ShaWordCol::W, row, field_cfg)?.rot_c(13) + + &word_poly(trace, ShaWordCol::W, row, field_cfg)?.shift_r_c(10) + - &small_sigma1 + - &scale_poly(&ov_small_sigma1, &two); + + let mu_w = mu_contribution(trace, row, 0, 2, field_cfg)?; + let mu_a = mu_contribution(trace, row, 2, 5, field_cfg)?; + let mu_e = mu_contribution(trace, row, 5, 8, field_cfg)?; + let mu_ff_a = mu_contribution(trace, row, 8, 9, field_cfg)?; + let mu_ff_e = mu_contribution(trace, row, 9, 10, field_cfg)?; + + let r4 = word_poly_shifted(trace, ShaWordCol::W, row, 16, field_cfg)? + - &w + - &word_poly_shifted(trace, ShaWordCol::SmallSigma0, row, 1, field_cfg)? + - &word_poly_shifted(trace, ShaWordCol::W, row, 9, field_cfg)? + - &word_poly_shifted(trace, ShaWordCol::SmallSigma1, row, 14, field_cfg)? + + &mu_w + + &int_const_poly(trace, ShaIntCol::CompSchedule, row, field_cfg)?; + + let r5 = word_poly_shifted(trace, ShaWordCol::A, row, 4, field_cfg)? + - &e + - &word_poly_shifted(trace, ShaWordCol::Sigma1, row, 3, field_cfg)? + - &word_poly_shifted(trace, ShaWordCol::Uef, row, 3, field_cfg)? + - &word_poly_shifted(trace, ShaWordCol::UNegEg, row, 3, field_cfg)? + - &public_const_poly(public, ShaPublicCol::K, row + 3, field_cfg)? + - &word_poly_shifted(trace, ShaWordCol::W, row, 3, field_cfg)? + - &word_poly_shifted(trace, ShaWordCol::Sigma0, row, 3, field_cfg)? + - &word_poly_shifted(trace, ShaWordCol::Maj, row, 3, field_cfg)? + + &mu_a + + &int_const_poly(trace, ShaIntCol::CompUpdateA, row, field_cfg)?; + + let r6 = word_poly_shifted(trace, ShaWordCol::E, row, 4, field_cfg)? + - &a + - &e + - &word_poly_shifted(trace, ShaWordCol::Sigma1, row, 3, field_cfg)? + - &word_poly_shifted(trace, ShaWordCol::Uef, row, 3, field_cfg)? + - &word_poly_shifted(trace, ShaWordCol::UNegEg, row, 3, field_cfg)? + - &public_const_poly(public, ShaPublicCol::K, row + 3, field_cfg)? + - &word_poly_shifted(trace, ShaWordCol::W, row, 3, field_cfg)? + + &mu_e + + &int_const_poly(trace, ShaIntCol::CompUpdateE, row, field_cfg)?; + + let s_init = public_scalar(public, ShaPublicCol::SInit, row, field_cfg)?; + let s_msg = public_scalar(public, ShaPublicCol::SMsg, row, field_cfg)?; + let s_sched = public_scalar(public, ShaPublicCol::SSched, row, field_cfg)?; + let s_upd = public_scalar(public, ShaPublicCol::SUpd, row, field_cfg)?; + let s_ff = public_scalar(public, ShaPublicCol::SFf, row, field_cfg)?; + let s_out = public_scalar(public, ShaPublicCol::SOut, row, field_cfg)?; + + let r7 = scale_poly( + &(a.clone() - &public_const_poly(public, ShaPublicCol::PAIn, row, field_cfg)?), + &s_init, + ) + &scale_poly( + &(a.clone() - &public_const_poly(public, ShaPublicCol::PAOut, row, field_cfg)?), + &s_out, + ); + let r8 = scale_poly( + &(e.clone() - &public_const_poly(public, ShaPublicCol::PEIn, row, field_cfg)?), + &s_init, + ) + &scale_poly( + &(e.clone() - &public_const_poly(public, ShaPublicCol::PEOut, row, field_cfg)?), + &s_out, + ); + + let r9 = word_poly_shifted(trace, ShaWordCol::A, row, 4, field_cfg)? + - &a + - &public_const_poly(public, ShaPublicCol::PAIn, row, field_cfg)? + + &mu_ff_a + + &int_const_poly(trace, ShaIntCol::CompFeedForwardA, row, field_cfg)?; + let r10 = word_poly_shifted(trace, ShaWordCol::E, row, 4, field_cfg)? + - &e + - &public_const_poly(public, ShaPublicCol::PEIn, row, field_cfg)? + + &mu_ff_e + + &int_const_poly(trace, ShaIntCol::CompFeedForwardE, row, field_cfg)?; + let r11 = scale_poly( + &(w - &public_const_poly(public, ShaPublicCol::Message, row, field_cfg)?), + &s_msg, + ); + + let comp_schedule = int_const_poly(trace, ShaIntCol::CompSchedule, row, field_cfg)?; + let comp_update_a = int_const_poly(trace, ShaIntCol::CompUpdateA, row, field_cfg)?; + let comp_update_e = int_const_poly(trace, ShaIntCol::CompUpdateE, row, field_cfg)?; + let comp_ff_a = int_const_poly(trace, ShaIntCol::CompFeedForwardA, row, field_cfg)?; + let comp_ff_e = int_const_poly(trace, ShaIntCol::CompFeedForwardE, row, field_cfg)?; + + let r12 = scale_poly(&comp_schedule, &s_sched); + let r13 = scale_poly(&comp_update_a, &s_upd); + let r14 = scale_poly(&comp_update_e, &s_upd); + let r15 = scale_poly(&comp_ff_a, &s_ff); + let r16 = scale_poly(&comp_ff_e, &s_ff); + let r17 = word_poly(trace, ShaWordCol::MuPacked, row, field_cfg)?.shift_r_c(10); + + let mut residuals = [ + r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, + ]; + residuals.iter_mut().for_each(DynamicPolynomialF::trim); + debug_assert_eq!(residuals.len(), NUM_SHA_RESIDUAL_FAMILIES); + let _ = zero; + Ok(residuals) +} + +fn residual_values_at_row( + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + row: usize, + a: &F, + field_cfg: &F::Config, +) -> Result<[F; NUM_SHA_RESIDUAL_FAMILIES], ShaProjectionError> +where + F: PrimeField, +{ + let polies = residual_polys_at_row(trace, public, row, field_cfg)?; + let mut out: [F; NUM_SHA_RESIDUAL_FAMILIES] = + std::array::from_fn(|_| F::zero_with_cfg(field_cfg)); + for (idx, poly) in polies.iter().enumerate() { + out[idx] = poly.evaluate_at_point(a)?; + } + Ok(out) +} + +fn validate_trace(trace: &ProjectedShaTrace) -> Result<(), ShaProjectionError> { + if trace.rows != SHA_ROW_COUNT { + return Err(ShaProjectionError::RowCount { + expected: SHA_ROW_COUNT, + got: trace.rows, + }); + } + validate_bit_columns(&trace.bit_slices)?; + validate_matrix( + "scalarized_words", + &trace.scalarized_words.words, + SHA_ROW_COUNT, + )?; + validate_matrix("int_columns", &trace.int_columns.columns, SHA_ROW_COUNT)?; + validate_matrix( + "public_columns", + &trace.public_columns.columns, + SHA_ROW_COUNT, + ) +} + +fn validate_public(public: &ProjectedShaPublic) -> Result<(), ShaProjectionError> { + validate_matrix("public.columns", &public.columns.columns, SHA_ROW_COUNT) +} + +fn validate_matrix( + kind: &'static str, + columns: &[Vec], + rows: usize, +) -> Result<(), ShaProjectionError> { + for (col, values) in columns.iter().enumerate() { + if values.len() != rows { + return Err(ShaProjectionError::ColumnRowCount { + kind, + col, + got: values.len(), + expected: rows, + }); + } + } + Ok(()) +} + +fn validate_bit_columns(bit_slices: &ShaBitSliceColumns) -> Result<(), ShaProjectionError> { + for (col, rows) in bit_slices.columns.iter().enumerate() { + if rows.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "bit_slices", + col, + got: rows.len(), + expected: SHA_ROW_COUNT, + }); + } + for (row, bits) in rows.iter().enumerate() { + if bits.len() != SHA_WORD_BITS { + return Err(ShaProjectionError::BitCount { + col, + row, + got: bits.len(), + expected: SHA_WORD_BITS, + }); + } + } + } + Ok(()) +} + +fn word_poly( + trace: &ProjectedShaTrace, + col: ShaWordCol, + row: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + if row >= SHA_ROW_COUNT { + return Ok(DynamicPolynomialF::ZERO); + } + let col_idx = col.index(); + let rows = trace + .bit_slices + .columns + .get(col_idx) + .ok_or(ShaProjectionError::MissingColumn { + kind: "bit_slices", + col: col_idx, + })?; + let bits = rows.get(row).ok_or(ShaProjectionError::ColumnRowCount { + kind: "bit_slices", + col: col_idx, + got: rows.len(), + expected: SHA_ROW_COUNT, + })?; + if bits.len() != SHA_WORD_BITS { + return Err(ShaProjectionError::BitCount { + col: col_idx, + row, + got: bits.len(), + expected: SHA_WORD_BITS, + }); + } + let mut coeffs = bits.clone(); + coeffs.resize(SHA_WORD_BITS, F::zero_with_cfg(field_cfg)); + Ok(DynamicPolynomialF::new_trimmed(coeffs)) +} + +fn word_poly_shifted( + trace: &ProjectedShaTrace, + col: ShaWordCol, + row: usize, + shift: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + match row.checked_add(shift) { + Some(shifted) if shifted < SHA_ROW_COUNT => word_poly(trace, col, shifted, field_cfg), + _ => Ok(DynamicPolynomialF::ZERO), + } +} + +fn bit_at_shifted_or_zero( + trace: &ProjectedShaTrace, + col: ShaWordCol, + row: usize, + shift: usize, + bit: usize, + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + if bit >= SHA_WORD_BITS { + return Err(ShaProjectionError::BitIndexOutOfRange { bit }); + } + let Some(shifted) = row.checked_add(shift) else { + return Ok(F::zero_with_cfg(field_cfg)); + }; + if shifted >= SHA_ROW_COUNT { + return Ok(F::zero_with_cfg(field_cfg)); + } + let col_idx = col.index(); + let rows = trace + .bit_slices + .columns + .get(col_idx) + .ok_or(ShaProjectionError::MissingColumn { + kind: "bit_slices", + col: col_idx, + })?; + if rows.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "bit_slices", + col: col_idx, + got: rows.len(), + expected: SHA_ROW_COUNT, + }); + } + let bits = &rows[shifted]; + if bits.len() != SHA_WORD_BITS { + return Err(ShaProjectionError::BitCount { + col: col_idx, + row: shifted, + got: bits.len(), + expected: SHA_WORD_BITS, + }); + } + Ok(bits[bit].clone()) +} + +fn int_const_poly( + trace: &ProjectedShaTrace, + col: ShaIntCol, + row: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + Ok(const_poly( + int_scalar(trace, col, row, field_cfg)?, + field_cfg, + )) +} + +fn public_const_poly( + public: &ProjectedShaPublic, + col: ShaPublicCol, + row: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + Ok(const_poly( + public_scalar(public, col, row, field_cfg)?, + field_cfg, + )) +} + +fn int_scalar( + trace: &ProjectedShaTrace, + col: ShaIntCol, + row: usize, + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + if row >= SHA_ROW_COUNT { + return Ok(F::zero_with_cfg(field_cfg)); + } + scalar_from_matrix( + "int_columns", + &trace.int_columns.columns, + col.index(), + row, + field_cfg, + ) +} + +fn public_scalar( + public: &ProjectedShaPublic, + col: ShaPublicCol, + row: usize, + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + if row >= SHA_ROW_COUNT { + return Ok(F::zero_with_cfg(field_cfg)); + } + scalar_from_matrix( + "public.columns", + &public.columns.columns, + col.index(), + row, + field_cfg, + ) +} + +fn scalar_from_matrix( + kind: &'static str, + columns: &[Vec], + col: usize, + row: usize, + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + let values = columns + .get(col) + .ok_or(ShaProjectionError::MissingColumn { kind, col })?; + Ok(values + .get(row) + .cloned() + .unwrap_or_else(|| F::zero_with_cfg(field_cfg))) +} + +fn const_poly(value: F, _field_cfg: &F::Config) -> DynamicPolynomialF { + DynamicPolynomialF::new_trimmed([value]) +} + +fn sparse_poly(indices: &[usize], field_cfg: &F::Config) -> DynamicPolynomialF { + let mut coeffs = vec![F::zero_with_cfg(field_cfg); SHA_WORD_BITS]; + for &idx in indices { + coeffs[idx] = F::one_with_cfg(field_cfg); + } + DynamicPolynomialF::new_trimmed(coeffs) +} + +fn scale_poly(poly: &DynamicPolynomialF, scalar: &F) -> DynamicPolynomialF { + if poly.is_zero() || F::is_zero(scalar) { + return DynamicPolynomialF::ZERO; + } + DynamicPolynomialF::new_trimmed( + poly.coeffs + .iter() + .map(|coeff| coeff.clone() * scalar) + .collect::>(), + ) +} + +fn pow_two(exp: usize, field_cfg: &F::Config) -> F { + let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); + let mut out = F::one_with_cfg(field_cfg); + for _ in 0..exp { + out *= &two; + } + out +} + +fn mu_contribution( + trace: &ProjectedShaTrace, + row: usize, + low: usize, + high: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + let packed = word_poly(trace, ShaWordCol::MuPacked, row, field_cfg)?.shift_r_c(low as u32); + let tail = word_poly(trace, ShaWordCol::MuPacked, row, field_cfg)?.shift_r_c(high as u32); + let low_coeff = pow_two(32, field_cfg); + let high_coeff = pow_two(32 + high - low, field_cfg); + Ok(scale_poly(&packed, &low_coeff) - &scale_poly(&tail, &high_coeff)) +} + +fn project_bits(bits: &[F], powers: &[F], field_cfg: &F::Config) -> F { + let mut acc = F::zero_with_cfg(field_cfg); + for (bit, power) in bits.iter().zip(powers.iter()) { + acc += bit.clone() * power; + } + acc +} + +fn build_virtual_bit_array(mut f: G) -> Result<[F; SHA_WORD_BITS], ShaProjectionError> +where + G: FnMut(usize) -> Result, +{ + let mut values = Vec::with_capacity(SHA_WORD_BITS); + for bit in 0..SHA_WORD_BITS { + values.push(f(bit)?); + } + Ok(values + .try_into() + .unwrap_or_else(|_| unreachable!("exactly 32 virtual bits were built"))) +} + +fn scalarized_word_at_shifted_or_zero( + trace: &ProjectedShaTrace, + col: ShaWordCol, + row: usize, + shift: usize, + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + let Some(shifted) = row.checked_add(shift) else { + return Ok(F::zero_with_cfg(field_cfg)); + }; + if shifted >= SHA_ROW_COUNT { + return Ok(F::zero_with_cfg(field_cfg)); + } + let col_idx = col.index(); + let rows = + trace + .scalarized_words + .words + .get(col_idx) + .ok_or(ShaProjectionError::MissingColumn { + kind: "scalarized_words", + col: col_idx, + })?; + if rows.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "scalarized_words", + col: col_idx, + got: rows.len(), + expected: SHA_ROW_COUNT, + }); + } + Ok(rows[shifted].clone()) +} + +fn booleanity_source_value_at_row( + trace: &ProjectedShaTrace, + row: usize, + source: &ShaBooleanitySource, + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + match source { + ShaBooleanitySource::WordBit { col, bit } => { + bit_at_shifted_or_zero(trace, *col, row, 0, *bit, field_cfg) + } + ShaBooleanitySource::VirtualCh1 { bit } => virtual_bit_at( + &reconstruct_virtual_ch_maj_at_row(trace, row, field_cfg)?.ch1, + *bit, + ), + ShaBooleanitySource::VirtualCh2 { bit } => virtual_bit_at( + &reconstruct_virtual_ch_maj_at_row(trace, row, field_cfg)?.ch2, + *bit, + ), + ShaBooleanitySource::VirtualMaj { bit } => virtual_bit_at( + &reconstruct_virtual_ch_maj_at_row(trace, row, field_cfg)?.maj, + *bit, + ), + } +} + +fn virtual_bit_at( + bits: &[F; SHA_WORD_BITS], + bit: usize, +) -> Result { + bits.get(bit) + .cloned() + .ok_or(ShaProjectionError::BitIndexOutOfRange { bit }) +} + +fn fold_2d<'a, F, I>( + matrices: I, + theta: &[F], + field_cfg: &F::Config, +) -> Result>, ShaProjectionError> +where + F: PrimeField + 'a, + I: IntoIterator>>, +{ + let matrices = matrices.into_iter().collect::>(); + if matrices.len() != theta.len() { + return Err(ShaProjectionError::FoldingWeightCount { + got: theta.len(), + expected: matrices.len(), + }); + } + let Some(first) = matrices.first() else { + return Ok(Vec::new()); + }; + let mut out = vec![vec![F::zero_with_cfg(field_cfg); SHA_ROW_COUNT]; first.len()]; + for (matrix, weight) in matrices.iter().zip(theta) { + if matrix.len() != first.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: matrix.len(), + expected: first.len(), + }); + } + for (col_idx, col) in matrix.iter().enumerate() { + if col.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "fold_2d", + col: col_idx, + got: col.len(), + expected: SHA_ROW_COUNT, + }); + } + for row in 0..SHA_ROW_COUNT { + out[col_idx][row] += weight.clone() * &col[row]; + } + } + } + Ok(out) +} + +fn fold_3d<'a, F, I>( + tensors: I, + theta: &[F], + field_cfg: &F::Config, +) -> Result>>, ShaProjectionError> +where + F: PrimeField + 'a, + I: IntoIterator>>>, +{ + let tensors = tensors.into_iter().collect::>(); + if tensors.len() != theta.len() { + return Err(ShaProjectionError::FoldingWeightCount { + got: theta.len(), + expected: tensors.len(), + }); + } + let Some(first) = tensors.first() else { + return Ok(Vec::new()); + }; + let mut out = + vec![vec![vec![F::zero_with_cfg(field_cfg); SHA_WORD_BITS]; SHA_ROW_COUNT]; first.len()]; + for (tensor, weight) in tensors.iter().zip(theta) { + if tensor.len() != first.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: tensor.len(), + expected: first.len(), + }); + } + for (col_idx, col) in tensor.iter().enumerate() { + if col.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "fold_3d", + col: col_idx, + got: col.len(), + expected: SHA_ROW_COUNT, + }); + } + for row in 0..SHA_ROW_COUNT { + if col[row].len() != SHA_WORD_BITS { + return Err(ShaProjectionError::BitCount { + col: col_idx, + row, + got: col[row].len(), + expected: SHA_WORD_BITS, + }); + } + for bit in 0..SHA_WORD_BITS { + out[col_idx][row][bit] += weight.clone() * &col[row][bit]; + } + } + } + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sumcheck::multi_degree::MultiDegreeSumcheck; + use crate::test_utils::test_config; + use crypto_primitives::{FromWithConfig, crypto_bigint_monty::MontyField}; + use zinc_transcript::Blake3Transcript; + + type F = MontyField<4>; + + fn f(value: u64) -> F { + F::from_with_cfg(value, &test_config()) + } + + fn zero_trace() -> ProjectedShaTrace { + let cfg = test_config(); + let zero = F::zero_with_cfg(&cfg); + let bits = vec![vec![vec![zero.clone(); SHA_WORD_BITS]; SHA_ROW_COUNT]; ShaWordCol::COUNT]; + let bit_slices = ShaBitSliceColumns { columns: bits }; + let scalarized_words = scalarize_trace_words(&bit_slices, &f(5), &cfg).unwrap(); + ProjectedShaTrace { + rows: SHA_ROW_COUNT, + bit_slices, + scalarized_words, + int_columns: ShaIntColumns { + columns: vec![vec![zero.clone(); SHA_ROW_COUNT]; ShaIntCol::COUNT], + }, + public_columns: ShaPublicColumns { + columns: vec![vec![zero; SHA_ROW_COUNT]; ShaPublicCol::COUNT], + }, + } + } + + fn zero_public() -> ProjectedShaPublic { + let cfg = test_config(); + ProjectedShaPublic { + columns: ShaPublicColumns { + columns: vec![vec![F::zero_with_cfg(&cfg); SHA_ROW_COUNT]; ShaPublicCol::COUNT], + }, + } + } + + #[test] + fn zero_trace_ideal_cache_checks_and_targets_are_zero() { + let cfg = test_config(); + let trace = zero_trace(); + let public = zero_public(); + let mut r_ic = std::array::from_fn(|_| F::zero_with_cfg(&cfg)); + r_ic[0] = f(3); + r_ic[1] = f(7); + + let mut cache = + build_fresh_sha_ideal_cache(&[trace], &[public], r_ic, &cfg).expect("cache builds"); + check_fresh_sha_ideal_cache(&cache, &cfg).expect("zero ideals pass"); + evaluate_fresh_sha_targets(&mut cache, &f(11), &f(13), &cfg).unwrap(); + + assert_eq!(cache.ideal_polys.len(), 1); + for poly in &cache.ideal_polys[0] { + assert!(poly.is_zero()); + } + for tau in &cache.taus_at_a[0] { + assert_eq!(tau, &F::zero_with_cfg(&cfg)); + } + assert_eq!(cache.fresh_targets[0], F::zero_with_cfg(&cfg)); + } + + #[test] + fn tampered_ideal_cache_fails_membership() { + let cfg = test_config(); + let trace = zero_trace(); + let public = zero_public(); + let r_ic = std::array::from_fn(|_| F::zero_with_cfg(&cfg)); + let mut values = build_sha_ideal_values_at_point(&trace, &public, &r_ic, &cfg).unwrap(); + values[0] = DynamicPolynomialF::new_trimmed([f(1)]); + + assert!(matches!( + check_sha_ideal_values(&values, &cfg), + Err(ShaProjectionError::IdealMembership) + )); + } + + #[test] + fn scalarization_links_check_folded_words() { + let cfg = test_config(); + let mut trace = zero_trace(); + trace.bit_slices.columns[ShaWordCol::A.index()][0][0] = f(1); + trace.bit_slices.columns[ShaWordCol::A.index()][0][3] = f(1); + trace.scalarized_words = scalarize_trace_words(&trace.bit_slices, &f(5), &cfg).unwrap(); + + verify_folded_scalarization_links(&trace, &f(5), &[ShaWordCol::A], &cfg) + .expect("scalarization should pass"); + + trace.scalarized_words.words[ShaWordCol::A.index()][0] += f(1); + assert!(matches!( + verify_folded_scalarization_links(&trace, &f(5), &[ShaWordCol::A], &cfg), + Err(ShaProjectionError::ScalarizationMismatch { .. }) + )); + } + + #[test] + fn scalarization_links_check_endpoint_and_shifted_sources() { + let cfg = test_config(); + let mut trace = zero_trace(); + trace.bit_slices.columns[ShaWordCol::A.index()][0][1] = f(1); + trace.bit_slices.columns[ShaWordCol::A.index()][1][0] = f(1); + trace.bit_slices.columns[ShaWordCol::A.index()][1][2] = f(1); + trace.scalarized_words = scalarize_trace_words(&trace.bit_slices, &f(3), &cfg).unwrap(); + let r_star = std::array::from_fn(|_| F::zero_with_cfg(&cfg)); + + verify_folded_scalarization_links_at_point(&trace, &f(3), &r_star, &[ShaWordCol::A], &cfg) + .expect("endpoint scalarization should pass"); + verify_folded_shifted_scalarization_link_at_point( + &trace, + &f(3), + &r_star, + ShaWordCol::A, + 1, + &cfg, + ) + .expect("shifted endpoint scalarization should pass"); + + trace.scalarized_words.words[ShaWordCol::A.index()][0] += f(1); + assert!(matches!( + verify_folded_scalarization_links_at_point( + &trace, + &f(3), + &r_star, + &[ShaWordCol::A], + &cfg + ), + Err(ShaProjectionError::ScalarizationMismatch { .. }) + )); + } + + #[test] + fn sumfold_output_derives_theta_after_endpoint() { + let cfg = test_config(); + let beta = vec![f(2), f(3)]; + let r_b = vec![f(5), f(7)]; + let c_sf = f(11); + let out = finalize_sha_sumfold(&beta, r_b.clone(), c_sf.clone(), 4, &cfg).unwrap(); + let d = eq_eval(&beta, &r_b, F::one_with_cfg(&cfg)).unwrap(); + + assert_eq!(out.t_prime(), &(c_sf / d)); + assert_eq!(out.theta(), build_eq_x_r_vec(&r_b, &cfg).unwrap()); + } + + #[test] + fn folding_uses_sumfold_theta() { + let cfg = test_config(); + let beta = vec![f(2)]; + let r_b = vec![f(3)]; + let out = finalize_sha_sumfold(&beta, r_b, f(9), 2, &cfg).unwrap(); + + let mut left = zero_trace(); + let mut right = zero_trace(); + left.bit_slices.columns[ShaWordCol::A.index()][0][0] = f(1); + right.bit_slices.columns[ShaWordCol::A.index()][0][0] = f(2); + left.scalarized_words = scalarize_trace_words(&left.bit_slices, &f(5), &cfg).unwrap(); + right.scalarized_words = scalarize_trace_words(&right.bit_slices, &f(5), &cfg).unwrap(); + + let (folded, _public) = fold_projected_sha_traces( + &[left.clone(), right.clone()], + &[zero_public(), zero_public()], + &out, + &cfg, + ) + .unwrap(); + let expected = out.theta()[0].clone() * &left.bit_slices.columns[0][0][0] + + out.theta()[1].clone() * &right.bit_slices.columns[0][0][0]; + assert_eq!(folded.trace.bit_slices.columns[0][0][0], expected); + } + + #[test] + fn virtual_ch_maj_reconstructs_from_source_bits() { + let cfg = test_config(); + let mut trace = zero_trace(); + trace.bit_slices.columns[ShaWordCol::E.index()][2][0] = f(1); + trace.bit_slices.columns[ShaWordCol::E.index()][1][0] = f(1); + trace.bit_slices.columns[ShaWordCol::Uef.index()][2][0] = f(1); + trace.bit_slices.columns[ShaWordCol::A.index()][0][1] = f(1); + trace.bit_slices.columns[ShaWordCol::A.index()][1][1] = f(1); + trace.bit_slices.columns[ShaWordCol::A.index()][2][1] = f(1); + trace.bit_slices.columns[ShaWordCol::Maj.index()][2][1] = f(1); + + let virtuals = reconstruct_virtual_ch_maj_at_row(&trace, 0, &cfg).unwrap(); + + assert_eq!(virtuals.ch1[0], F::zero_with_cfg(&cfg)); + assert_eq!(virtuals.maj[1], f(1)); + } + + #[test] + fn malformed_virtual_sources_return_errors() { + let cfg = test_config(); + let trace = zero_trace(); + assert!(matches!( + reconstruct_virtual_ch_maj_at_row(&trace, SHA_ROW_COUNT, &cfg), + Err(ShaProjectionError::RowIndexOutOfRange { .. }) + )); + + let public = zero_public(); + let r_ic = std::array::from_fn(|_| F::zero_with_cfg(&cfg)); + assert!(matches!( + folded_row_integrand_values( + &trace, + &public, + &r_ic, + &f(3), + &f(5), + &f(7), + &f(11), + &[ShaBooleanitySource::VirtualMaj { bit: SHA_WORD_BITS }], + &cfg, + ), + Err(ShaProjectionError::BitIndexOutOfRange { .. }) + )); + } + + #[test] + fn folded_row_group_claims_row_integrand_sum() { + let cfg = test_config(); + let trace = zero_trace(); + let public = zero_public(); + let r_ic = std::array::from_fn(|_| F::zero_with_cfg(&cfg)); + let values = folded_row_integrand_values( + &trace, + &public, + &r_ic, + &f(3), + &f(5), + &f(7), + &f(11), + &[], + &cfg, + ) + .unwrap(); + let group = build_folded_row_sumcheck_group(&values, &cfg).unwrap(); + let mut transcript = Blake3Transcript::new(); + let (proof, _) = + MultiDegreeSumcheck::prove_as_subprotocol(&mut transcript, vec![group], 7, &cfg); + + assert_eq!(proof.claimed_sums()[0], F::zero_with_cfg(&cfg)); + verify_folded_row_sumcheck_claim(&proof.claimed_sums()[0], &F::zero_with_cfg(&cfg)) + .expect("row claim matches T'"); + assert!(matches!( + verify_folded_row_sumcheck_claim(&proof.claimed_sums()[0], &f(1)), + Err(ShaProjectionError::FoldedRowClaimMismatch) + )); + assert_eq!( + folded_row_integrand_sum(&values, &cfg).unwrap(), + F::zero_with_cfg(&cfg) + ); + } +} From 80960f1c09f3fd9ac95dffeb07a1caf7519ce54f Mon Sep 17 00:00:00 2001 From: John Wu Date: Sat, 6 Jun 2026 01:06:18 -0700 Subject: [PATCH 15/49] Add production SHA ProjectionFold folding path --- piop/src/neutron_nova/linear_cpr.rs | 627 +++++++++++++++--- piop/src/neutron_nova/mod.rs | 1 + piop/src/neutron_nova/sumfold.rs | 405 +++++++++--- piop/src/sumcheck/multi_degree.rs | 226 +++++-- protocol/src/lib.rs | 4 +- protocol/src/pcs.rs | 23 +- protocol/src/production_sha.rs | 955 ++++++++++++++++++++++++++++ zip-plus/src/pcs/generic.rs | 29 + zip-plus/src/pcs/hyrax.rs | 489 +++++++++++++- 9 files changed, 2518 insertions(+), 241 deletions(-) create mode 100644 protocol/src/production_sha.rs diff --git a/piop/src/neutron_nova/linear_cpr.rs b/piop/src/neutron_nova/linear_cpr.rs index 4a3a0fe0..4f4173cd 100644 --- a/piop/src/neutron_nova/linear_cpr.rs +++ b/piop/src/neutron_nova/linear_cpr.rs @@ -1,10 +1,17 @@ use crate::neutron_nova::{RowWeights, accumulator::dmr_flush_adds, sumfold::checked_domain_size}; -use crate::sumcheck::multi_degree::MultiDegreeSumcheckGroup; +use crate::sumcheck::{ + multi_degree::{MultiDegreeSumcheckGroup, PrefixFastPath, PrefixRoundOutput}, + prover::ProverState as SumcheckProverState, +}; use crypto_primitives::{FromPrimitiveWithConfig, PrimeField, crypto_bigint_uint::Uint}; use num_traits::Zero; use std::array; use thiserror::Error; -use zinc_poly::univariate::binary::BinaryPoly; +use zinc_poly::{ + mle::DenseMultilinearExtension, + univariate::binary::BinaryPoly, + utils::{build_eq_x_r_inner, build_eq_x_r_vec, eq_eval}, +}; use zinc_uair::UairTrace; use zinc_utils::{ UNCHECKED, @@ -12,6 +19,7 @@ use zinc_utils::{ BarrettReductionParams, DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs, }, inner_product::{FieldFieldInnerProduct, InnerProduct}, + inner_transparent_field::InnerTransparentField, }; use super::{LinearPrefixTable, SumFoldError}; @@ -300,6 +308,367 @@ where .map_err(LinearCprAccumulatorError::from) } +/// Build the post-prefix CPR claim table `claim(r_prefix, tail)`. +/// +/// Unlike [`build_linear_cpr_prefix_table`], this does not apply tail equality +/// weights. The resumed ordinary sumcheck keeps +/// `eq(beta_prefix, r_prefix) * eq(beta_tail, tail)` as its separate equality +/// MLE, so this table only binds the CPR claim MLE along the prefix variables. +#[allow(clippy::arithmetic_side_effects, clippy::too_many_arguments)] +pub fn build_linear_cpr_prefix_bound_tail_table( + traces: &[UairTrace<'_, PolyCoeff, Int, D>], + prefix_vars: usize, + prefix_point: &[F], + families: &[LinearFamilySpec], + row_weights: &RowWeights, + scalar_weights: LinearCprScalarWeights<'_, F>, + field_cfg: &F::Config, +) -> Result, LinearCprAccumulatorError> +where + F: InnerTransparentField + FromPrimitiveWithConfig, + F::Inner: Zero, + PolyCoeff: Clone, + Int: Clone, +{ + let ell = validate_trace_count(traces.len())?; + if prefix_vars > ell { + return Err(LinearCprAccumulatorError::PrefixVarsTooLarge { prefix_vars, ell }); + } + if prefix_point.len() != prefix_vars { + return Err(LinearCprAccumulatorError::LengthMismatch { + label: "prefix_point", + got: prefix_point.len(), + expected: prefix_vars, + }); + } + if scalar_weights.scalarization_powers.len() < D { + return Err(LinearCprAccumulatorError::LengthMismatch { + label: "scalarization_powers", + got: scalar_weights.scalarization_powers.len(), + expected: D, + }); + } + + let prefix_len = checked_domain_size(prefix_vars)?; + let tail_vars = ell - prefix_vars; + let tail_len = checked_domain_size(tail_vars)?; + let prefix_weights = if prefix_vars == 0 { + vec![F::one_with_cfg(field_cfg)] + } else { + zinc_poly::utils::build_eq_x_r_vec(prefix_point, field_cfg)? + }; + + let num_binary_cols = validate_traces(traces, row_weights.len())?; + let prepared = prepare_families( + families, + scalar_weights.family_weights.len(), + num_binary_cols, + row_weights.len(), + field_cfg, + )?; + + let zero = F::zero_with_cfg(field_cfg); + let mut tail_values = vec![zero.clone(); tail_len]; + for (tail, tail_value) in tail_values.iter_mut().enumerate() { + for (prefix, prefix_weight) in prefix_weights.iter().enumerate().take(prefix_len) { + let instance_idx = prefix + (tail << prefix_vars); + let trace = &traces[instance_idx]; + + for family in &prepared { + let family_weight = &scalar_weights.family_weights[family.family_idx]; + let mut family_value = zero.clone(); + + for (active_pos, &row) in family.active_rows.iter().enumerate() { + let row_weight = &row_weights.as_slice()[row]; + for term in &family.terms { + let Some(coeff_idx) = term.coeff_indices_by_active_row[active_pos] else { + continue; + }; + let Some(poly) = source_poly::(trace, &term.source, row) + else { + continue; + }; + let coeff_value = &family.coeff_values[coeff_idx]; + + for (bit_idx, bit) in poly.iter().enumerate().take(D) { + if bit.into_inner() { + let mut contribution = row_weight.clone(); + contribution *= coeff_value; + contribution *= &scalar_weights.scalarization_powers[bit_idx]; + family_value += contribution; + } + } + } + } + + *tail_value += prefix_weight.clone() * family_weight * family_value; + } + } + } + + let zero_inner = zero.inner().clone(); + Ok(DenseMultilinearExtension::from_evaluations_vec( + tail_vars, + tail_values + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner, + )) +} + +struct LinearCprPrefixFastPath< + F: PrimeField, + PolyCoeff: Clone + 'static, + Int: Clone + 'static, + const D: usize, +> { + traces: Vec>, + families: Vec>, + row_weights: RowWeights, + family_weights: Vec, + scalarization_powers: Vec, + beta: Vec, + prefix_vars: usize, + prefix_state: SumcheckProverState, +} + +impl LinearCprPrefixFastPath +where + F: MontgomeryLimbs + + InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Zero, + PolyCoeff: Clone + Send + Sync + 'static, + Int: Clone + Send + Sync + 'static, +{ + #[allow(clippy::too_many_arguments)] + fn new( + traces: Vec>, + prefix_vars: usize, + beta: Vec, + families: Vec>, + row_weights: RowWeights, + family_weights: Vec, + scalarization_powers: Vec, + field_cfg: &F::Config, + ) -> Result { + let eq_weights = build_sumfold_eq_weights(&beta, prefix_vars, field_cfg)?; + let table = build_linear_cpr_prefix_table( + &traces, + prefix_vars, + &families, + LinearCprWeights { + row_weights: &row_weights, + tail_eq_weights: &eq_weights.tail_eq_weights, + }, + LinearCprScalarWeights { + family_weights: &family_weights, + scalarization_powers: &scalarization_powers, + }, + field_cfg, + )?; + let eq_prefix = build_eq_x_r_inner(&beta[..prefix_vars], field_cfg)?; + let prefix_state = + SumcheckProverState::new(vec![eq_prefix, table.to_mle(field_cfg)], prefix_vars, 2); + + Ok(Self { + traces, + families, + row_weights, + family_weights, + scalarization_powers, + beta, + prefix_vars, + prefix_state, + }) + } + + #[allow(clippy::arithmetic_side_effects)] + fn finish_tail_mles( + self, + prefix_challenges: &[F], + field_cfg: &F::Config, + ) -> Vec> { + debug_assert_eq!(prefix_challenges.len(), self.prefix_vars); + let tail_vars = self.beta.len() - self.prefix_vars; + let beta_tail_weights = build_eq_x_r_vec(&self.beta[self.prefix_vars..], field_cfg) + .expect("tail beta equality table should build"); + let eq_prefix_at_r = eq_eval( + prefix_challenges, + &self.beta[..self.prefix_vars], + F::one_with_cfg(field_cfg), + ) + .expect("prefix challenge and beta prefix lengths match"); + + let tail_claims = build_linear_cpr_prefix_bound_tail_table( + &self.traces, + self.prefix_vars, + prefix_challenges, + &self.families, + &self.row_weights, + LinearCprScalarWeights { + family_weights: &self.family_weights, + scalarization_powers: &self.scalarization_powers, + }, + field_cfg, + ) + .expect("validated CPR tail table should build"); + + let zero_inner = F::zero_with_cfg(field_cfg).inner().clone(); + let scaled_eq_tail = DenseMultilinearExtension::from_evaluations_vec( + tail_vars, + beta_tail_weights + .iter() + .map(|weight| (eq_prefix_at_r.clone() * weight).inner().clone()) + .collect(), + zero_inner, + ); + + vec![scaled_eq_tail, tail_claims] + } +} + +impl PrefixFastPath + for LinearCprPrefixFastPath +where + F: MontgomeryLimbs + + InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Zero, + PolyCoeff: Clone + Send + Sync + 'static, + Int: Clone + Send + Sync + 'static, +{ + fn prefix_len(&self) -> usize { + self.prefix_vars + } + + fn prove_prefix_round( + &mut self, + verifier_msg: &Option, + config: &F::Config, + ) -> PrefixRoundOutput { + let msg = self.prefix_state.prove_round( + verifier_msg, + |values: &[F]| values[0].clone() * &values[1], + config, + ); + let asserted_sum = if self.prefix_state.round == 1 { + self.prefix_state.asserted_sum.clone() + } else { + None + }; + + PrefixRoundOutput { + asserted_sum, + tail_evaluations: msg.0.tail_evaluations, + } + } + + fn finish_prefix( + self: Box, + prefix_challenges: &[F], + config: &F::Config, + ) -> Vec> { + self.finish_tail_mles(prefix_challenges, config) + } +} + +/// Build a hybrid SumFold group for linear CPR over owned traces. +/// +/// `prefix_vars = 0` falls back to an ordinary full sumcheck group over dense +/// CPR claims. Positive prefixes emit CPR prefix-table sumcheck messages, bind +/// the prefix at the sampled challenges, and resume the ordinary degree-2 +/// sumcheck over the tail variables. +#[allow(clippy::too_many_arguments)] +pub fn build_linear_cpr_hybrid_sumcheck_group( + traces: Vec>, + prefix_vars: usize, + beta: &[F], + families: Vec>, + row_weights: RowWeights, + family_weights: Vec, + scalarization_powers: Vec, + field_cfg: &F::Config, +) -> Result, LinearCprAccumulatorError> +where + F: MontgomeryLimbs + + InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Zero, + PolyCoeff: Clone + Send + Sync + 'static, + Int: Clone + Send + Sync + 'static, +{ + let ell = validate_trace_count(traces.len())?; + if beta.len() != ell { + return Err(LinearCprAccumulatorError::LengthMismatch { + label: "beta", + got: beta.len(), + expected: ell, + }); + } + if prefix_vars > ell { + return Err(LinearCprAccumulatorError::PrefixVarsTooLarge { prefix_vars, ell }); + } + if prefix_vars == 0 { + let claims = build_linear_cpr_prefix_bound_tail_table( + &traces, + 0, + &[], + &families, + &row_weights, + LinearCprScalarWeights { + family_weights: &family_weights, + scalarization_powers: &scalarization_powers, + }, + field_cfg, + )?; + let eq_beta = build_eq_x_r_inner(beta, field_cfg)?; + return Ok(MultiDegreeSumcheckGroup::new( + 2, + vec![eq_beta, claims], + Box::new(|values: &[F]| values[0].clone() * &values[1]), + )); + } + if prefix_vars >= ell { + return Err(LinearCprAccumulatorError::SumFold( + SumFoldError::HybridPrefixNeedsTail { + ell0: prefix_vars, + ell, + }, + )); + } + + let fast_path = LinearCprPrefixFastPath::new( + traces, + prefix_vars, + beta.to_vec(), + families, + row_weights, + family_weights, + scalarization_powers, + field_cfg, + )?; + Ok(MultiDegreeSumcheckGroup::with_prefix_fast( + 2, + Vec::new(), + Box::new(|values: &[F]| values[0].clone() * &values[1]), + Box::new(fast_path), + )) +} + #[allow(clippy::arithmetic_side_effects, clippy::too_many_arguments)] fn accumulate_family_tile( traces: &[UairTrace<'_, PolyCoeff, Int, D>], @@ -600,29 +969,18 @@ fn flush_buckets_into_lanes( } } -impl LinearPrefixTable -where - F: PrimeField + DelayedFieldProductSum + 'static, - F::Inner: num_traits::Zero, -{ - pub fn build_linear_cpr_sumcheck_group( - &self, - prefix_eq_weights: &[F], - field_cfg: &F::Config, - ) -> Result, SumFoldError> { - self.build_sumcheck_group_from_prefix_weights(prefix_eq_weights, field_cfg) - } -} - #[cfg(test)] mod tests { use super::*; - use crate::{sumcheck::multi_degree::MultiDegreeSumcheck, test_utils::test_config}; + use crate::{ + neutron_nova::LinearInstanceClaims, sumcheck::multi_degree::MultiDegreeSumcheck, + test_utils::test_config, + }; use crypto_primitives::{FromWithConfig, crypto_bigint_monty::MontyField}; use std::borrow::Cow; use zinc_poly::{ mle::{DenseMultilinearExtension, MultilinearExtensionWithConfig}, - utils::eq_eval, + utils::{build_eq_x_r_vec, eq_eval}, }; use zinc_transcript::Blake3Transcript; use zinc_utils::powers; @@ -724,6 +1082,47 @@ mod tests { powers(f(3), F::one_with_cfg(&test_config()), 32) } + #[allow(clippy::arithmetic_side_effects)] + fn naive_linear_cpr_claims( + traces: &[Trace], + families: &[LinearFamilySpec], + row_weights: &RowWeights, + scalar_weights: LinearCprScalarWeights<'_, F>, + ) -> Vec { + let cfg = test_config(); + let mut claims = vec![F::zero_with_cfg(&cfg); traces.len()]; + + for (trace, claim) in traces.iter().zip(claims.iter_mut()) { + for family in families { + let mut family_value = F::zero_with_cfg(&cfg); + for (active_pos, &row) in family.active_rows.iter().enumerate() { + for term in &family.terms { + let coeff = &term.coeffs_by_active_row[active_pos]; + if coeff.is_zero() { + continue; + } + let coeff_value = coeff.to_field(&cfg); + let Some(poly) = source_poly::(trace, &term.source, row) else { + continue; + }; + + for (bit_idx, bit) in poly.iter().enumerate().take(32) { + if bit.into_inner() { + let mut contribution = row_weights.as_slice()[row].clone(); + contribution *= &coeff_value; + contribution *= &scalar_weights.scalarization_powers[bit_idx]; + family_value += contribution; + } + } + } + } + *claim += scalar_weights.family_weights[family.family_idx].clone() * family_value; + } + } + + claims + } + #[allow(clippy::arithmetic_side_effects)] fn naive_linear_cpr_table( traces: &[Trace], @@ -737,39 +1136,13 @@ mod tests { usize::try_from(traces.len().trailing_zeros()).expect("trailing_zeros fits usize"); let prefix_len = 1usize << prefix_vars; let tail_len = 1usize << (ell - prefix_vars); + let claims = naive_linear_cpr_claims(traces, families, weights.row_weights, scalar_weights); let mut table = vec![F::zero_with_cfg(&cfg); prefix_len]; for (prefix, value) in table.iter_mut().enumerate() { - for family in families { - let mut family_value = F::zero_with_cfg(&cfg); - for tail in 0..tail_len { - let instance_idx = prefix + (tail << prefix_vars); - let trace = &traces[instance_idx]; - for (active_pos, &row) in family.active_rows.iter().enumerate() { - for term in &family.terms { - let coeff = &term.coeffs_by_active_row[active_pos]; - if coeff.is_zero() { - continue; - } - let coeff_value = coeff.to_field(&cfg); - let Some(poly) = source_poly::(trace, &term.source, row) - else { - continue; - }; - - for (bit_idx, bit) in poly.iter().enumerate().take(32) { - if bit.into_inner() { - let mut contribution = weights.tail_eq_weights[tail].clone(); - contribution *= &weights.row_weights.as_slice()[row]; - contribution *= &coeff_value; - contribution *= &scalar_weights.scalarization_powers[bit_idx]; - family_value += contribution; - } - } - } - } - } - *value += scalar_weights.family_weights[family.family_idx].clone() * family_value; + for tail in 0..tail_len { + let instance_idx = prefix + (tail << prefix_vars); + *value += weights.tail_eq_weights[tail].clone() * &claims[instance_idx]; } } @@ -845,47 +1218,157 @@ mod tests { } #[test] - fn optimized_linear_cpr_table_builds_degree_two_sumcheck() { + fn optimized_linear_cpr_prefix_bound_tail_table_matches_naive_claim_binding() { let cfg = test_config(); - let beta = vec![f(19), f(23)]; - let (table, eq_weights) = build_table_for_prefix_vars(2); + let traces = sample_traces(); + let families = sample_families(); + let prefix_vars = 1; + let prefix_point = vec![f(29)]; + let row_weights = RowWeights::new(&[f(11), f(13)], &cfg).unwrap(); + let family_weights = vec![f(5), f(17)]; + let scalarization_powers = scalar_weights(); + let scalar_weights = LinearCprScalarWeights { + family_weights: &family_weights, + scalarization_powers: &scalarization_powers, + }; - let claim = table - .build_sumcheck_claim(&eq_weights.prefix_eq_weights, &cfg) - .unwrap(); - let group = table - .build_sumcheck_group_from_prefix_weights(&eq_weights.prefix_eq_weights, &cfg) - .unwrap(); + let tail_mle = build_linear_cpr_prefix_bound_tail_table( + &traces, + prefix_vars, + &prefix_point, + &families, + &row_weights, + scalar_weights, + &cfg, + ) + .unwrap(); + let dense_claims = + naive_linear_cpr_claims(&traces, &families, &row_weights, scalar_weights); + let prefix_weights = build_eq_x_r_vec(&prefix_point, &cfg).unwrap(); + + let tail_len = 1usize << (2 - prefix_vars); + let mut expected = vec![F::zero_with_cfg(&cfg); tail_len]; + for (tail, value) in expected.iter_mut().enumerate() { + for (prefix, weight) in prefix_weights.iter().enumerate() { + let instance_idx = prefix + (tail << prefix_vars); + *value += weight.clone() * &dense_claims[instance_idx]; + } + } + + assert_eq!(tail_mle.num_vars, 1); + assert_eq!( + tail_mle.evaluations, + expected + .iter() + .map(|value| value.inner().clone()) + .collect::>() + ); + } + + #[test] + fn optimized_linear_cpr_hybrid_sumfold_matches_full_ordinary_sumcheck() { + let cfg = test_config(); + let traces = sample_traces(); + let families = sample_families(); + let beta = vec![f(19), f(23)]; + let row_weights = RowWeights::new(&[f(11), f(13)], &cfg).unwrap(); + let family_weights = vec![f(5), f(17)]; + let scalarization_powers = scalar_weights(); + let scalar_weights = LinearCprScalarWeights { + family_weights: &family_weights, + scalarization_powers: &scalarization_powers, + }; + let dense_claims = + naive_linear_cpr_claims(&traces, &families, &row_weights, scalar_weights); + let claims = LinearInstanceClaims::from_claims_for_ell(dense_claims, 2).unwrap(); + + let full_group = claims.build_full_sumcheck_group(&beta, &cfg).unwrap(); + let optimized_group = build_linear_cpr_hybrid_sumcheck_group( + traces.clone(), + 1, + &beta, + families.clone(), + row_weights.clone(), + family_weights.clone(), + scalarization_powers.clone(), + &cfg, + ) + .unwrap(); + let fallback_group = build_linear_cpr_hybrid_sumcheck_group( + traces, + 0, + &beta, + families, + row_weights, + family_weights, + scalarization_powers, + &cfg, + ) + .unwrap(); let mut prover_transcript = Blake3Transcript::new(); - let (proof, _states) = MultiDegreeSumcheck::prove_as_subprotocol( + let (full_proof, _states) = MultiDegreeSumcheck::prove_as_subprotocol( &mut prover_transcript, - vec![group], - table.ell0(), + vec![full_group], + 2, &cfg, ); + let mut prover_transcript = Blake3Transcript::new(); + let (optimized_proof, _states) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut prover_transcript, + vec![optimized_group], + 2, + &cfg, + ); + assert_eq!(optimized_proof, full_proof); + let mut prover_transcript = Blake3Transcript::new(); + let (fallback_proof, _states) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut prover_transcript, + vec![fallback_group], + 2, + &cfg, + ); + assert_eq!(fallback_proof, full_proof); - assert_eq!(proof.claimed_sums()[0], claim); - let mut verifier_transcript = Blake3Transcript::new(); - let subclaims = MultiDegreeSumcheck::verify_as_subprotocol( + let full_subclaims = MultiDegreeSumcheck::verify_as_subprotocol( + &mut verifier_transcript, + 2, + &full_proof, + &cfg, + ) + .expect("full linear CPR sumcheck should verify"); + let mut verifier_transcript = Blake3Transcript::new(); + let optimized_subclaims = MultiDegreeSumcheck::verify_as_subprotocol( &mut verifier_transcript, - table.ell0(), - &proof, + 2, + &optimized_proof, &cfg, ) .expect("optimized linear CPR sumcheck should verify"); + assert_eq!(optimized_subclaims.point(), full_subclaims.point()); + assert_eq!( + optimized_subclaims.expected_evaluations(), + full_subclaims.expected_evaluations() + ); - let point = subclaims.point(); + let point = full_subclaims.point(); let eq_at_point = eq_eval(point, &beta, F::one_with_cfg(&cfg)).expect("same number of variables"); - let table_eval = table - .to_mle(&cfg) - .evaluate_with_config(point, &cfg) - .unwrap(); + let zero_inner = F::zero_with_cfg(&cfg).inner().clone(); + let claims_mle = DenseMultilinearExtension::from_evaluations_vec( + 2, + claims + .claims() + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner, + ); + let claim_eval = claims_mle.evaluate_with_config(point, &cfg).unwrap(); assert_eq!( - subclaims.expected_evaluations()[0], - eq_at_point * table_eval + full_subclaims.expected_evaluations()[0], + eq_at_point * claim_eval ); } diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index 0dd832bd..c2ff439e 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -21,6 +21,7 @@ pub use booleanity::{ pub use linear_cpr::{ CoeffClass, LinearBinarySource, LinearCprAccumulatorError, LinearCprScalarWeights, LinearCprWeights, LinearFamilySpec, LinearTermSpec, SumFoldEqWeights, + build_linear_cpr_hybrid_sumcheck_group, build_linear_cpr_prefix_bound_tail_table, build_linear_cpr_prefix_table, build_sumfold_eq_weights, }; pub use projection_sha::{ diff --git a/piop/src/neutron_nova/sumfold.rs b/piop/src/neutron_nova/sumfold.rs index 40597564..1112637b 100644 --- a/piop/src/neutron_nova/sumfold.rs +++ b/piop/src/neutron_nova/sumfold.rs @@ -2,15 +2,16 @@ use crypto_primitives::PrimeField; use thiserror::Error; use zinc_poly::{ mle::DenseMultilinearExtension, - utils::{ArithErrors, build_eq_x_r_inner, build_eq_x_r_vec}, + utils::{ArithErrors, build_eq_x_r_inner, build_eq_x_r_vec, eq_eval}, }; use zinc_utils::{ - UNCHECKED, - delayed_reduction::DelayedFieldProductSum, - inner_product::{FieldFieldInnerProduct, InnerProduct}, + delayed_reduction::DelayedFieldProductSum, inner_transparent_field::InnerTransparentField, }; -use crate::sumcheck::multi_degree::MultiDegreeSumcheckGroup; +use crate::sumcheck::{ + multi_degree::{MultiDegreeSumcheckGroup, PrefixFastPath, PrefixRoundOutput}, + prover::ProverState as SumcheckProverState, +}; /// Errors produced by linear SumFold prefix-table helpers. #[derive(Clone, Debug, Error)] @@ -31,14 +32,8 @@ pub enum SumFoldError { Ell0TooLarge { ell0: usize, ell: usize }, #[error("beta length mismatch: got {got}, expected {expected}")] BetaLengthMismatch { got: usize, expected: usize }, - #[error("{label} length mismatch: got {got}, expected {expected}")] - WeightLengthMismatch { - label: &'static str, - got: usize, - expected: usize, - }, - #[error("sumcheck group construction requires ell0 > 0")] - SumcheckNeedsNonzeroEll0, + #[error("hybrid sumfold requires ell0 < ell, got ell0={ell0}, ell={ell}")] + HybridPrefixNeedsTail { ell0: usize, ell: usize }, #[error("equality table construction failed: {0}")] EqTable(#[from] ArithErrors), } @@ -202,88 +197,194 @@ impl LinearPrefixTable { } } -impl LinearPrefixTable +struct LinearSumFoldPrefixFastPath { + instance_claims: LinearInstanceClaims, + beta: Vec, + ell0: usize, + prefix_state: SumcheckProverState, +} + +impl LinearSumFoldPrefixFastPath where - F: PrimeField + DelayedFieldProductSum + 'static, + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: num_traits::Zero, { - pub fn build_sumcheck_claim( - &self, - prefix_eq_weights: &[F], + fn new( + instance_claims: LinearInstanceClaims, + beta: Vec, + ell0: usize, field_cfg: &F::Config, - ) -> Result { - if prefix_eq_weights.len() != self.values.len() { - return Err(SumFoldError::WeightLengthMismatch { - label: "prefix_eq_weights", - got: prefix_eq_weights.len(), - expected: self.values.len(), - }); - } + ) -> Result { + let table = LinearPrefixTable::build(&instance_claims, &beta, ell0, field_cfg)?; + let eq_prefix = build_eq_x_r_inner(&beta[..ell0], field_cfg)?; + let table_mle = table.to_mle(field_cfg); + let prefix_state = SumcheckProverState::new(vec![eq_prefix, table_mle], ell0, 2); - Ok(FieldFieldInnerProduct::inner_product::( - prefix_eq_weights, - &self.values, - F::zero_with_cfg(field_cfg), + Ok(Self { + instance_claims, + beta, + ell0, + prefix_state, + }) + } + + #[allow(clippy::arithmetic_side_effects)] + fn finish_tail_mles( + self, + prefix_challenges: &[F], + field_cfg: &F::Config, + ) -> Vec> { + debug_assert_eq!(prefix_challenges.len(), self.ell0); + let ell = self.instance_claims.ell(); + let tail_vars = ell - self.ell0; + let prefix_len = checked_domain_size(self.ell0).expect("validated ell0 fits usize"); + let tail_len = checked_domain_size(tail_vars).expect("validated tail vars fit usize"); + let prefix_weights = build_eq_x_r_vec(prefix_challenges, field_cfg) + .expect("prefix challenge equality table should build"); + let beta_tail_weights = build_eq_x_r_vec(&self.beta[self.ell0..], field_cfg) + .expect("tail beta equality table should build"); + let eq_prefix_at_r = eq_eval( + prefix_challenges, + &self.beta[..self.ell0], + F::one_with_cfg(field_cfg), ) - .expect("prefix equality weights and table values have matching lengths")) + .expect("prefix challenge and beta prefix lengths match"); + + let mut bound_claims = vec![F::zero_with_cfg(field_cfg); tail_len]; + for (tail, value) in bound_claims.iter_mut().enumerate() { + for (prefix, weight) in prefix_weights.iter().enumerate().take(prefix_len) { + let idx = prefix + (tail << self.ell0); + *value += weight.clone() * &self.instance_claims.claims()[idx]; + } + } + + let zero_inner = F::zero_with_cfg(field_cfg).inner().clone(); + let scaled_eq_tail = DenseMultilinearExtension::from_evaluations_vec( + tail_vars, + beta_tail_weights + .iter() + .map(|weight| (eq_prefix_at_r.clone() * weight).inner().clone()) + .collect(), + zero_inner.clone(), + ); + let bound_claims = DenseMultilinearExtension::from_evaluations_vec( + tail_vars, + bound_claims + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner, + ); + + vec![scaled_eq_tail, bound_claims] + } +} + +impl PrefixFastPath for LinearSumFoldPrefixFastPath +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: num_traits::Zero, +{ + fn prefix_len(&self) -> usize { + self.ell0 + } + + fn prove_prefix_round( + &mut self, + verifier_msg: &Option, + config: &F::Config, + ) -> PrefixRoundOutput { + let msg = self.prefix_state.prove_round( + verifier_msg, + |values: &[F]| values[0].clone() * &values[1], + config, + ); + let asserted_sum = if self.prefix_state.round == 1 { + self.prefix_state.asserted_sum.clone() + } else { + None + }; + + PrefixRoundOutput { + asserted_sum, + tail_evaluations: msg.0.tail_evaluations, + } } - pub fn build_sumcheck_group_from_prefix_weights( + fn finish_prefix( + self: Box, + prefix_challenges: &[F], + config: &F::Config, + ) -> Vec> { + self.finish_tail_mles(prefix_challenges, config) + } +} + +impl LinearInstanceClaims +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: num_traits::Zero, +{ + pub fn build_full_sumcheck_group( &self, - prefix_eq_weights: &[F], + beta: &[F], field_cfg: &F::Config, ) -> Result, SumFoldError> { - if self.ell0 == 0 { - return Err(SumFoldError::SumcheckNeedsNonzeroEll0); - } - if prefix_eq_weights.len() != self.values.len() { - return Err(SumFoldError::WeightLengthMismatch { - label: "prefix_eq_weights", - got: prefix_eq_weights.len(), - expected: self.values.len(), + if beta.len() != self.ell { + return Err(SumFoldError::BetaLengthMismatch { + got: beta.len(), + expected: self.ell, }); } let zero_inner = F::zero_with_cfg(field_cfg).inner().clone(); - let eq_prefix = DenseMultilinearExtension::from_evaluations_vec( - self.ell0, - prefix_eq_weights + let eq_beta = build_eq_x_r_inner(beta, field_cfg)?; + let claims = DenseMultilinearExtension::from_evaluations_vec( + self.ell, + self.claims .iter() .map(|value| value.inner().clone()) .collect(), zero_inner, ); - let table = self.to_mle(field_cfg); Ok(MultiDegreeSumcheckGroup::new( 2, - vec![eq_prefix, table], + vec![eq_beta, claims], Box::new(|values: &[F]| values[0].clone() * &values[1]), )) } - pub fn build_sumcheck_group( + pub fn build_hybrid_sumcheck_group( &self, - beta_prefix: &[F], + beta: &[F], + ell0: usize, field_cfg: &F::Config, ) -> Result, SumFoldError> { - if self.ell0 == 0 { - return Err(SumFoldError::SumcheckNeedsNonzeroEll0); - } - if beta_prefix.len() != self.ell0 { + if beta.len() != self.ell { return Err(SumFoldError::BetaLengthMismatch { - got: beta_prefix.len(), - expected: self.ell0, + got: beta.len(), + expected: self.ell, + }); + } + if ell0 == 0 { + return self.build_full_sumcheck_group(beta, field_cfg); + } + if ell0 >= self.ell { + return Err(SumFoldError::HybridPrefixNeedsTail { + ell0, + ell: self.ell, }); } - let eq_beta_prefix = build_eq_x_r_inner(beta_prefix, field_cfg)?; - let table = self.to_mle(field_cfg); + let fast_path = + LinearSumFoldPrefixFastPath::new(self.clone(), beta.to_vec(), ell0, field_cfg)?; - Ok(MultiDegreeSumcheckGroup::new( + Ok(MultiDegreeSumcheckGroup::with_prefix_fast( 2, - vec![eq_beta_prefix, table], + Vec::new(), Box::new(|values: &[F]| values[0].clone() * &values[1]), + Box::new(fast_path), )) } } @@ -298,10 +399,13 @@ pub(crate) fn checked_domain_size(ell: usize) -> Result { #[cfg(test)] mod tests { use super::*; - use crate::{sumcheck::multi_degree::MultiDegreeSumcheck, test_utils::test_config}; + use crate::{ + sumcheck::multi_degree::{MultiDegreeSumcheck, MultiDegreeSumcheckProof}, + test_utils::test_config, + }; use crypto_primitives::{FromWithConfig, crypto_bigint_monty::MontyField}; use zinc_poly::{ - mle::MultilinearExtensionWithConfig, + mle::{DenseMultilinearExtension, MultilinearExtensionWithConfig}, utils::{build_eq_x_r_vec, eq_eval}, }; use zinc_transcript::Blake3Transcript; @@ -357,6 +461,105 @@ mod tests { acc } + fn claims_mle( + claims: &LinearInstanceClaims, + ) -> DenseMultilinearExtension<::Inner> { + let cfg = test_config(); + let zero_inner = F::zero_with_cfg(&cfg).inner().clone(); + DenseMultilinearExtension::from_evaluations_vec( + claims.ell(), + claims + .claims() + .iter() + .map(|claim| claim.inner().clone()) + .collect(), + zero_inner, + ) + } + + fn prove_and_verify( + group: MultiDegreeSumcheckGroup, + ell: usize, + ) -> (MultiDegreeSumcheckProof, Vec, Vec) { + let cfg = test_config(); + let mut prover_transcript = Blake3Transcript::new(); + let (proof, _states) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut prover_transcript, + vec![group], + ell, + &cfg, + ); + + let mut verifier_transcript = Blake3Transcript::new(); + let subclaims = + MultiDegreeSumcheck::verify_as_subprotocol(&mut verifier_transcript, ell, &proof, &cfg) + .expect("sumcheck proof should verify"); + + ( + proof, + subclaims.point().to_vec(), + subclaims.expected_evaluations().to_vec(), + ) + } + + fn proof_satisfies_dense_claim( + proof: &MultiDegreeSumcheckProof, + ell: usize, + beta: &[F], + claims: &LinearInstanceClaims, + ) -> bool { + let cfg = test_config(); + let mut verifier_transcript = Blake3Transcript::new(); + let Ok(subclaims) = + MultiDegreeSumcheck::verify_as_subprotocol(&mut verifier_transcript, ell, proof, &cfg) + else { + return false; + }; + + let eq_at_point = + eq_eval(subclaims.point(), beta, F::one_with_cfg(&cfg)).expect("same length"); + let claim_at_point = claims_mle(claims) + .evaluate_with_config(subclaims.point(), &cfg) + .unwrap(); + subclaims.expected_evaluations()[0] == eq_at_point * claim_at_point + } + + #[allow(clippy::arithmetic_side_effects)] + fn beta_for_ell(ell: usize) -> Vec { + (0..ell) + .map(|idx| f(3 + 2 * u64::try_from(idx).expect("test index fits u64"))) + .collect() + } + + fn assert_hybrid_matches_full_sumcheck(ell: usize, ell0: usize) { + let cfg = test_config(); + let claims = claims_for_ell(ell); + let beta = beta_for_ell(ell); + + let full_group = claims.build_full_sumcheck_group(&beta, &cfg).unwrap(); + let optimized_group = claims + .build_hybrid_sumcheck_group(&beta, ell0, &cfg) + .unwrap(); + + let (full_proof, full_point, full_expected) = prove_and_verify(full_group, ell); + let (optimized_proof, optimized_point, optimized_expected) = + prove_and_verify(optimized_group, ell); + + assert_eq!(optimized_proof, full_proof); + assert_eq!(optimized_point, full_point); + assert_eq!(optimized_expected, full_expected); + assert_eq!( + full_proof.claimed_sums()[0], + direct_full_beta_sum(&claims, &beta) + ); + + let eq_at_point = eq_eval(&full_point, &beta, F::one_with_cfg(&cfg)).expect("same length"); + let claim_at_point = claims_mle(&claims) + .evaluate_with_config(&full_point, &cfg) + .unwrap(); + assert_eq!(full_expected[0], eq_at_point * claim_at_point); + } + #[test] fn prefix_table_matches_direct_tail_fold_for_all_ell0_cases() { let cfg = test_config(); @@ -379,49 +582,46 @@ mod tests { } #[test] - fn linear_sumfold_group_proves_weighted_instance_sum() { + fn hybrid_sumfold_proof_matches_full_ordinary_sumcheck() { + for (ell, ell0) in [(3, 1), (4, 2), (5, 3), (4, 0), (5, 4)] { + assert_hybrid_matches_full_sumcheck(ell, ell0); + } + } + + #[test] + fn hybrid_sumfold_rejects_tampered_prefix_and_tail_messages() { let cfg = test_config(); - let claims = claims_for_ell(3); - let beta = vec![f(3), f(5), f(7)]; + let ell = 4; let ell0 = 2; - let table = LinearPrefixTable::build(&claims, &beta, ell0, &cfg).unwrap(); - - let group = table - .build_sumcheck_group(&beta[..ell0], &cfg) - .expect("ell0 > 0 should build a group"); - let mut prover_transcript = Blake3Transcript::new(); - let (proof, _states) = MultiDegreeSumcheck::prove_as_subprotocol( - &mut prover_transcript, - vec![group], - ell0, - &cfg, - ); - - assert_eq!( - proof.claimed_sums()[0], - direct_full_beta_sum(&claims, &beta) - ); - - let mut verifier_transcript = Blake3Transcript::new(); - let subclaims = MultiDegreeSumcheck::verify_as_subprotocol( - &mut verifier_transcript, - ell0, - &proof, - &cfg, - ) - .expect("linear sumfold group should verify"); + let claims = claims_for_ell(ell); + let beta = beta_for_ell(ell); - let point = subclaims.point(); - let eq_at_point = - eq_eval(point, &beta[..ell0], F::one_with_cfg(&cfg)).expect("same length"); - let table_eval = table - .to_mle(&cfg) - .evaluate_with_config(point, &cfg) + let group = claims + .build_hybrid_sumcheck_group(&beta, ell0, &cfg) .unwrap(); - assert_eq!( - subclaims.expected_evaluations()[0], - eq_at_point * table_eval - ); + let (proof, _point, _expected) = prove_and_verify(group, ell); + + let mut prefix_tampered = proof.clone(); + prefix_tampered.group_messages_mut_for_testing()[0][0] + .0 + .tail_evaluations[0] += f(1); + assert!(!proof_satisfies_dense_claim( + &prefix_tampered, + ell, + &beta, + &claims + )); + + let mut tail_tampered = proof; + tail_tampered.group_messages_mut_for_testing()[0][ell0] + .0 + .tail_evaluations[0] += f(1); + assert!(!proof_satisfies_dense_claim( + &tail_tampered, + ell, + &beta, + &claims + )); } #[test] @@ -458,10 +658,11 @@ mod tests { }) )); - let table = LinearPrefixTable::build(&claims, &[f(3), f(5)], 0, &cfg).unwrap(); assert!(matches!( - table.build_sumcheck_group(&[], &cfg), - Err(SumFoldError::SumcheckNeedsNonzeroEll0) + claims + .build_hybrid_sumcheck_group(&[f(3), f(5)], 2, &cfg) + .err(), + Some(SumFoldError::HybridPrefixNeedsTail { ell0: 2, ell: 2 }) )); } } diff --git a/piop/src/sumcheck/multi_degree.rs b/piop/src/sumcheck/multi_degree.rs index 98bcedfb..1172617b 100644 --- a/piop/src/sumcheck/multi_degree.rs +++ b/piop/src/sumcheck/multi_degree.rs @@ -15,14 +15,10 @@ use crypto_primitives::{FromPrimitiveWithConfig, PrimeField}; use num_traits::Zero; -#[cfg(feature = "parallel")] -use rayon::prelude::*; use std::marker::PhantomData; use zinc_poly::mle::DenseMultilinearExtension; use zinc_transcript::traits::{ConstTranscribable, GenTranscribable, Transcribable, Transcript}; -use zinc_utils::{ - add, cfg_iter, cfg_iter_mut, inner_transparent_field::InnerTransparentField, mul, -}; +use zinc_utils::{add, inner_transparent_field::InnerTransparentField, mul}; use crate::CombFn; @@ -50,6 +46,14 @@ pub struct Round1Output { pub tail_evaluations: Vec, } +/// Output of one prefix fast-path round. +pub struct PrefixRoundOutput { + /// Set only on round 1, matching [`Round1Output::asserted_sum`]. + pub asserted_sum: Option, + /// `[p_i(1), p_i(2), ..., p_i(degree)]` for this round. + pub tail_evaluations: Vec, +} + /// Optional per-group hook that lets a degree group bypass the standard /// round-1 sumcheck loop. Used when the round-1 polynomial has a closed /// form (e.g. booleanity zerocheck on bit-slice MLEs that are 0/1 @@ -74,13 +78,68 @@ pub trait Round1FastPath: Send + Sync { ) -> Vec>; } +/// Optional per-group hook that emits a prefix of ordinary sumcheck messages. +/// +/// A prefix fast path must be transcript-equivalent to running +/// [`SumcheckProverState::prove_round`] for rounds `1..=prefix_len`. Once the +/// last prefix challenge is sampled, it returns the MLEs already bound at the +/// full prefix challenge point, so the standard prover can continue with the +/// remaining variables. +pub trait PrefixFastPath: Send + Sync { + fn prefix_len(&self) -> usize; + + fn prove_prefix_round( + &mut self, + verifier_msg: &Option, + config: &F::Config, + ) -> PrefixRoundOutput; + + fn finish_prefix( + self: Box, + prefix_challenges: &[F], + config: &F::Config, + ) -> Vec>; +} + +struct Round1PrefixAdapter { + inner: Box>, +} + +impl PrefixFastPath for Round1PrefixAdapter { + fn prefix_len(&self) -> usize { + 1 + } + + fn prove_prefix_round( + &mut self, + verifier_msg: &Option, + config: &F::Config, + ) -> PrefixRoundOutput { + debug_assert!(verifier_msg.is_none()); + let out = self.inner.round_1_message(config); + PrefixRoundOutput { + asserted_sum: Some(out.asserted_sum), + tail_evaluations: out.tail_evaluations, + } + } + + fn finish_prefix( + self: Box, + prefix_challenges: &[F], + config: &F::Config, + ) -> Vec> { + debug_assert_eq!(prefix_challenges.len(), 1); + self.inner.fold_with_r1(&prefix_challenges[0], config) + } +} + /// A single degree group for the multi-degree sumcheck: (degree, mles, /// comb_fn). pub struct MultiDegreeSumcheckGroup { degree: usize, poly: Vec>, comb_fn: CombFn, - round_1_fast: Option>>, + prefix_fast: Option>>, } impl MultiDegreeSumcheckGroup { @@ -93,7 +152,7 @@ impl MultiDegreeSumcheckGroup { degree, poly, comb_fn, - round_1_fast: None, + prefix_fast: None, } } @@ -105,12 +164,34 @@ impl MultiDegreeSumcheckGroup { poly: Vec>, comb_fn: CombFn, round_1_fast: Box>, + ) -> Self + where + F: 'static, + { + Self { + degree, + poly, + comb_fn, + prefix_fast: Some(Box::new(Round1PrefixAdapter { + inner: round_1_fast, + })), + } + } + + /// Construct a group whose initial rounds are produced by a custom + /// [`PrefixFastPath`]. `poly` may be empty here — the fast path supplies + /// post-prefix MLEs via [`PrefixFastPath::finish_prefix`]. + pub fn with_prefix_fast( + degree: usize, + poly: Vec>, + comb_fn: CombFn, + prefix_fast: Box>, ) -> Self { Self { degree, poly, comb_fn, - round_1_fast: Some(round_1_fast), + prefix_fast: Some(prefix_fast), } } } @@ -135,6 +216,15 @@ impl MultiDegreeSumcheckProof { pub fn claimed_sums(&self) -> &[F] { &self.claimed_sums } + + pub fn degrees(&self) -> &[usize] { + &self.degrees + } + + #[cfg(test)] + pub(crate) fn group_messages_mut_for_testing(&mut self) -> &mut [Vec>] { + &mut self.group_messages + } } impl GenTranscribable for MultiDegreeSumcheckProof @@ -341,79 +431,89 @@ impl MultiDegreeSumcheck { let mut prover_states: Vec> = Vec::with_capacity(num_groups); let mut comb_fns: Vec> = Vec::with_capacity(num_groups); - let mut fast_paths: Vec>>> = + let mut fast_paths: Vec>>> = Vec::with_capacity(num_groups); for group in groups { let degree_field = F::from_with_cfg(group.degree as u64, config); transcript.absorb_random_field(°ree_field, &mut buf); + if let Some(ref fp) = group.prefix_fast { + assert!( + fp.prefix_len() > 0 && fp.prefix_len() <= num_vars, + "prefix fast path length must be in 1..=num_vars" + ); + } prover_states.push(SumcheckProverState::new(group.poly, num_vars, group.degree)); comb_fns.push(group.comb_fn); - fast_paths.push(group.round_1_fast); + fast_paths.push(group.prefix_fast); } - // ---- Round 1 --------------------------------------------------- - let mut round_1_msgs: Vec> = Vec::with_capacity(num_groups); - for ((state, comb_fn), fp_slot) in prover_states - .iter_mut() - .zip(comb_fns.iter()) - .zip(fast_paths.iter()) - { - let msg = if let Some(fp) = fp_slot { - let out = fp.round_1_message(config); - debug_assert_eq!( - out.tail_evaluations.len(), - state.max_degree, - "fast-path round-1 tail must have length equal to group's degree" - ); - state.asserted_sum = Some(out.asserted_sum); - state.round = 1; - SumcheckProverMsg(NatEvaluatedPolyWithoutConstant::new(out.tail_evaluations)) - } else { - state.prove_round(&None, comb_fn, config) - }; - round_1_msgs.push(msg); - } - for msg in &round_1_msgs { - transcript.absorb_random_field_slice(&msg.0.tail_evaluations, &mut buf); - } - for (j, msg) in round_1_msgs.into_iter().enumerate() { - group_messages[j].push(msg); - } - let r_1: F = transcript.get_field_challenge(config); - transcript.absorb_random_field(&r_1, &mut buf); - let mut verifier_msg = Some(r_1.clone()); - - // For fast-path groups, materialize the round-1-folded MLEs and - // mark the next standard fold to be skipped. - for (state, fp_slot) in prover_states.iter_mut().zip(fast_paths.iter_mut()) { - if let Some(fp) = fp_slot.take() { - let folded = fp.fold_with_r1(&r_1, config); - state.mles = folded; - state.skip_next_fold = true; + let mut verifier_msg = None; + let mut challenges = Vec::with_capacity(num_vars); + for round_idx in 0..num_vars { + let mut round_msgs: Vec> = Vec::with_capacity(num_groups); + for group_idx in 0..num_groups { + let use_fast = fast_paths[group_idx] + .as_ref() + .is_some_and(|fp| round_idx < fp.prefix_len()); + let msg = if use_fast { + let fp = fast_paths[group_idx] + .as_mut() + .expect("fast path must exist when use_fast is true"); + let out = fp.prove_prefix_round(&verifier_msg, config); + debug_assert_eq!( + out.tail_evaluations.len(), + prover_states[group_idx].max_degree, + "prefix fast-path tail must match the group's degree" + ); + if round_idx == 0 { + prover_states[group_idx].asserted_sum = Some( + out.asserted_sum + .expect("prefix fast path must provide the first asserted sum"), + ); + } else { + debug_assert!(out.asserted_sum.is_none()); + } + prover_states[group_idx].round = round_idx + 1; + SumcheckProverMsg(NatEvaluatedPolyWithoutConstant::new(out.tail_evaluations)) + } else { + prover_states[group_idx].prove_round( + &verifier_msg, + &comb_fns[group_idx], + config, + ) + }; + round_msgs.push(msg); } - } - // ---- Rounds 2..num_vars --------------------------------------- - for _ in 1..num_vars { - // Parallel: each group computes its round polynomial independently - let round_msgs: Vec> = cfg_iter_mut!(prover_states) - .zip(cfg_iter!(comb_fns)) - .map(|(state, comb_fn)| state.prove_round(&verifier_msg, comb_fn, config)) - .collect(); - - // Sequential: absorb in deterministic order, sample one shared challenge for msg in &round_msgs { transcript.absorb_random_field_slice(&msg.0.tail_evaluations, &mut buf); } - for (j, msg) in round_msgs.into_iter().enumerate() { group_messages[j].push(msg); } - let next_verifier_msg = transcript.get_field_challenge(config); - transcript.absorb_random_field(&next_verifier_msg, &mut buf); + let challenge: F = transcript.get_field_challenge(config); + transcript.absorb_random_field(&challenge, &mut buf); + + for group_idx in 0..num_groups { + let should_finish = fast_paths[group_idx] + .as_ref() + .is_some_and(|fp| fp.prefix_len() == round_idx + 1); + if should_finish { + let fp = fast_paths[group_idx] + .take() + .expect("fast path must exist when should_finish is true"); + let mut prefix_challenges = challenges.clone(); + prefix_challenges.push(challenge.clone()); + prover_states[group_idx].mles = fp.finish_prefix(&prefix_challenges, config); + prover_states[group_idx].randomness = challenges.clone(); + prover_states[group_idx].round = round_idx + 1; + prover_states[group_idx].skip_next_fold = round_idx + 1 < num_vars; + } + } - verifier_msg = Some(next_verifier_msg); + verifier_msg = Some(challenge.clone()); + challenges.push(challenge); } prover_states.iter_mut().for_each(|state| { diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index dbdf3663..7f78d0a6 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -19,6 +19,7 @@ pub mod fixed_prime; pub mod pcs; +pub mod production_sha; pub mod prover; pub mod verifier; @@ -26,8 +27,7 @@ pub mod verifier; use rayon::prelude::*; use crypto_primitives::{ - ConstIntRing, ConstIntSemiring, FromWithConfig, PrimeField, Semiring, - crypto_bigint_uint::Uint, + ConstIntRing, ConstIntSemiring, FromWithConfig, PrimeField, Semiring, crypto_bigint_uint::Uint, }; use std::{fmt::Debug, marker::PhantomData}; use thiserror::Error; diff --git a/protocol/src/pcs.rs b/protocol/src/pcs.rs index a447108d..c23f1b14 100644 --- a/protocol/src/pcs.rs +++ b/protocol/src/pcs.rs @@ -5,7 +5,7 @@ use crypto_primitives::PrimeField; use zinc_poly::univariate::{binary::BinaryPoly, dense::DensePolynomial}; use zip_plus::pcs::{ generic::{PCS, ZipPlusPCS}, - hyrax::{BinaryLanes, HyraxPCS}, + hyrax::{BinaryLanes, DensePolyScalarLanes, HyraxPCS, IntScalarLane}, structs::ZipPlusCommitment, }; @@ -56,6 +56,27 @@ where type IntPCS = ZipPlusPCS; } +/// Homomorphic PCS bundle for production ProjectionFold paths. +/// +/// Every witness domain uses Hyrax/MSM commitments so verifier-derived +/// instance-axis folds can be opened against folded prover data. +#[derive(Clone, Debug)] +pub struct AllHyraxPCSTypes(PhantomData); + +impl ZincPCSTypes for AllHyraxPCSTypes +where + Zt: ZincTypes, + F: PrimeField, + C: AffineRepr, + HyraxPCS: PCS, D>, + HyraxPCS: PCS, D>, + HyraxPCS: PCS, +{ + type BinaryPCS = HyraxPCS; + type ArbitraryPCS = HyraxPCS; + type IntPCS = HyraxPCS; +} + #[derive(Clone, Debug)] pub struct PCSParams where diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs new file mode 100644 index 00000000..871015c3 --- /dev/null +++ b/protocol/src/production_sha.rs @@ -0,0 +1,955 @@ +//! Production SHA ProjectionFold protocol helpers. +//! +//! This module is intentionally separate from the existing single-instance +//! `Proof`: production ProjectionFold has a different transcript order and +//! derives folded commitments only after SumFold fixes the instance-axis point. + +use crate::{ + ZincTypes, + pcs::{PCSCommitments, PCSProverData, ZincPCSTypes}, +}; +use crypto_primitives::{FromPrimitiveWithConfig, PrimeField}; +use thiserror::Error; +use zinc_piop::{ + multipoint_eval::Proof as MultipointEvalProof, + neutron_nova::SumFoldError, + neutron_nova::{ + NUM_NONZERO_SHA_FAMILIES, SHA_ROW_VARS, ShaBooleanitySource, ShaProjectionError, + ShaResidualFamily, ShaSumFoldOutput, ShaWordCol, build_folded_row_sumcheck_group, + finalize_sha_sumfold, folded_row_integrand_sum, production_sha_nonzero_ideals, + verify_folded_row_sumcheck_claim, + }, + sumcheck::{ + SumCheckError, + multi_degree::{MultiDegreeSumcheck, MultiDegreeSumcheckProof}, + }, +}; +use zinc_poly::{ + univariate::{ + binary::BinaryPoly, dense::DensePolynomial, dynamic::over_field::DynamicPolynomialF, + }, + utils::{ArithErrors, build_eq_x_r_vec, eq_eval}, +}; +use zinc_transcript::traits::{ConstTranscribable, Transcribable, Transcript}; +use zinc_uair::ideal::IdealCheck; +use zinc_utils::{ + delayed_reduction::DelayedFieldProductSum, inner_transparent_field::InnerTransparentField, +}; +use zip_plus::{ZipError, pcs::generic::FoldablePCS}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProductionShaProof { + pub instance_commitments: Vec, + pub fresh_ideal_polys: Vec<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]>, + pub sumfold_proof: MultiDegreeSumcheckProof, + pub folded_row_sumcheck: MultiDegreeSumcheckProof, + pub endpoint_evals: ShaEndpointEvals, + pub multipoint_eval: MultipointEvalProof, + pub folded_lifted_evals: Vec>, + pub pcs_opening_bytes: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShaEndpointEvals { + pub sources: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShaSourceEndpointEval { + pub col: ShaWordCol, + pub shift: usize, + pub scalarized: F, + pub bits: [F; 32], +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VirtualChMajEndpoint { + pub ch1: [F; 32], + pub ch2: [F; 32], + pub maj: [F; 32], +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProductionShaChallenges { + pub r_ic: [F; SHA_ROW_VARS], + pub a: F, + pub lambda: F, + pub rho: F, + pub xi: F, + pub beta: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FoldedRowSumcheckOutput { + pub r_star: Vec, + pub terminal_value: F, +} + +#[derive(Debug, Error)] +pub enum ProductionShaError { + #[error("instance count must be a power of two, got {0}")] + InstanceCountNotPowerOfTwo(usize), + #[error("length mismatch for {label}: got {got}, expected {expected}")] + LengthMismatch { + label: &'static str, + got: usize, + expected: usize, + }, + #[error("{label} expected exactly one sumcheck group, got {got}")] + UnexpectedSumcheckGroupCount { label: &'static str, got: usize }, + #[error("SumFold proof has degree {degree}, expected at most 3")] + SumFoldDegreeTooHigh { degree: usize }, + #[error("SumFold terminal evaluation mismatch")] + SumFoldTerminalMismatch, + #[error("row sumcheck proof has degree {degree}, expected at most 3")] + RowSumcheckDegreeTooHigh { degree: usize }, + #[error("row sumcheck terminal evaluation mismatch")] + RowSumcheckTerminalMismatch, + #[error("endpoint scalarization mismatch for {col:?} shift {shift}")] + EndpointScalarizationMismatch { col: ShaWordCol, shift: usize }, + #[error("missing endpoint eval for {col:?} shift {shift}")] + MissingEndpointEval { col: ShaWordCol, shift: usize }, + #[error("ideal membership failed")] + IdealMembership, + #[error("PCS error: {0}")] + Pcs(#[from] ZipError), + #[error("sumcheck error: {0}")] + Sumcheck(#[from] SumCheckError), + #[error("SumFold error: {0}")] + SumFold(#[from] SumFoldError), + #[error("SHA projection error: {0}")] + ShaProjection(#[from] ShaProjectionError), + #[error("equality polynomial error: {0}")] + Eq(#[from] ArithErrors), +} + +pub fn absorb_projected_sha_publics( + transcript: &mut impl Transcript, + publics: &[zinc_piop::neutron_nova::ProjectedShaPublic], +) where + F: PrimeField, + F::Inner: ConstTranscribable + Transcribable, + F::Modulus: Transcribable, +{ + let mut buf = vec![0; F::Inner::NUM_BYTES]; + transcript.absorb_slice(b"production_sha_publics_begin"); + transcript.absorb_slice(&(publics.len() as u64).to_le_bytes()); + for (instance_idx, public) in publics.iter().enumerate() { + transcript.absorb_slice(&(instance_idx as u64).to_le_bytes()); + transcript.absorb_slice(&(public.columns.columns.len() as u64).to_le_bytes()); + for (col_idx, col) in public.columns.columns.iter().enumerate() { + transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); + transcript.absorb_slice(&(col.len() as u64).to_le_bytes()); + transcript.absorb_random_field_slice(col, &mut buf); + } + } + transcript.absorb_slice(b"production_sha_publics_end"); +} + +pub fn absorb_fresh_sha_ideal_polys( + transcript: &mut impl Transcript, + ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], +) where + F: PrimeField, + F::Inner: ConstTranscribable + Transcribable, + F::Modulus: Transcribable, +{ + let mut buf = vec![0; F::Inner::NUM_BYTES]; + transcript.absorb_slice(b"production_sha_fresh_ideals_begin"); + transcript.absorb_slice(&(ideal_polys.len() as u64).to_le_bytes()); + for (instance_idx, instance) in ideal_polys.iter().enumerate() { + transcript.absorb_slice(&(instance_idx as u64).to_le_bytes()); + transcript.absorb_slice(&(instance.len() as u64).to_le_bytes()); + for (family_idx, poly) in instance.iter().enumerate() { + transcript.absorb_slice(&(family_idx as u64).to_le_bytes()); + transcript.absorb_slice(&(poly.coeffs.len() as u64).to_le_bytes()); + transcript.absorb_random_field_slice(&poly.coeffs, &mut buf); + } + } + transcript.absorb_slice(b"production_sha_fresh_ideals_end"); +} + +pub fn sample_pre_ideal_challenge( + transcript: &mut impl Transcript, + field_cfg: &F::Config, +) -> [F; SHA_ROW_VARS] +where + F: PrimeField, + F::Inner: ConstTranscribable, +{ + std::array::from_fn(|_| transcript.get_field_challenge(field_cfg)) +} + +pub fn sample_post_ideal_challenges( + transcript: &mut impl Transcript, + instance_count: usize, + field_cfg: &F::Config, +) -> Result<(F, F, F, F, Vec), ProductionShaError> +where + F: PrimeField, + F::Inner: ConstTranscribable, +{ + if !instance_count.is_power_of_two() { + return Err(ProductionShaError::InstanceCountNotPowerOfTwo( + instance_count, + )); + } + let ell = usize::try_from(instance_count.trailing_zeros()).expect("ell fits usize"); + Ok(( + transcript.get_field_challenge(field_cfg), + transcript.get_field_challenge(field_cfg), + transcript.get_field_challenge(field_cfg), + transcript.get_field_challenge(field_cfg), + transcript.get_field_challenges(ell, field_cfg), + )) +} + +pub fn check_fresh_sha_ideal_membership( + ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], + field_cfg: &F::Config, +) -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + let ideals = production_sha_nonzero_ideals(field_cfg); + for values in ideal_polys { + for (ideal, value) in ideals.iter().zip(values.iter()) { + if !ideal + .contains(value) + .map_err(|_| ProductionShaError::IdealMembership)? + { + return Err(ProductionShaError::IdealMembership); + } + } + } + Ok(()) +} + +pub fn prove_sha_sumfold_targets( + transcript: &mut impl Transcript, + fresh_targets: &[F], + beta: &[F], + prefix_vars: usize, + field_cfg: &F::Config, +) -> Result<(MultiDegreeSumcheckProof, ShaSumFoldOutput), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + let claims = zinc_piop::neutron_nova::LinearInstanceClaims::new(fresh_targets.to_vec())?; + let group = claims.build_hybrid_sumcheck_group(beta, prefix_vars, field_cfg)?; + let (proof, states) = + MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], claims.ell(), field_cfg); + let r_b = states + .first() + .ok_or(ProductionShaError::LengthMismatch { + label: "sumfold states", + got: 0, + expected: 1, + })? + .randomness + .clone(); + let c_sf = sumfold_expected_eval(beta, fresh_targets, &r_b, field_cfg)?; + let output = finalize_sha_sumfold(beta, r_b, c_sf, fresh_targets.len(), field_cfg)?; + Ok((proof, output)) +} + +pub fn verify_sha_sumfold_targets( + transcript: &mut impl Transcript, + proof: &MultiDegreeSumcheckProof, + fresh_targets: &[F], + beta: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + require_single_sumcheck_group(proof, "SHA SumFold")?; + for °ree in proof.degrees() { + if degree > 3 { + return Err(ProductionShaError::SumFoldDegreeTooHigh { degree }); + } + } + let claims = zinc_piop::neutron_nova::LinearInstanceClaims::new(fresh_targets.to_vec())?; + let subclaims = + MultiDegreeSumcheck::verify_as_subprotocol(transcript, claims.ell(), proof, field_cfg)?; + let r_b = subclaims.point().to_vec(); + let c_sf = subclaims.expected_evaluations()[0].clone(); + if c_sf != sumfold_expected_eval(beta, fresh_targets, &r_b, field_cfg)? { + return Err(ProductionShaError::SumFoldTerminalMismatch); + } + Ok(finalize_sha_sumfold( + beta, + r_b, + c_sf, + fresh_targets.len(), + field_cfg, + )?) +} + +pub fn fold_pcs_commitments( + commitments: &[PCSCommitments], + theta: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + if commitments.len() != theta.len() { + return Err(ProductionShaError::LengthMismatch { + label: "commitments/theta", + got: commitments.len(), + expected: theta.len(), + }); + } + let binary = commitments + .iter() + .map(|commitment| commitment.binary.clone()) + .collect::>(); + let arbitrary = commitments + .iter() + .map(|commitment| commitment.arbitrary.clone()) + .collect::>(); + let int = commitments + .iter() + .map(|commitment| commitment.int.clone()) + .collect::>(); + Ok(PCSCommitments { + binary: P::BinaryPCS::fold_commitments(&binary, theta, field_cfg)?, + arbitrary: P::ArbitraryPCS::fold_commitments(&arbitrary, theta, field_cfg)?, + int: P::IntPCS::fold_commitments(&int, theta, field_cfg)?, + }) +} + +pub fn fold_pcs_prover_data( + prover_data: &[PCSProverData], + theta: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + if prover_data.len() != theta.len() { + return Err(ProductionShaError::LengthMismatch { + label: "prover_data/theta", + got: prover_data.len(), + expected: theta.len(), + }); + } + let binary = prover_data + .iter() + .map(|data| data.binary.clone()) + .collect::>(); + let arbitrary = prover_data + .iter() + .map(|data| data.arbitrary.clone()) + .collect::>(); + let int = prover_data + .iter() + .map(|data| data.int.clone()) + .collect::>(); + Ok(PCSProverData { + binary: P::BinaryPCS::fold_prover_data(&binary, theta, field_cfg)?, + arbitrary: P::ArbitraryPCS::fold_prover_data(&arbitrary, theta, field_cfg)?, + int: P::IntPCS::fold_prover_data(&int, theta, field_cfg)?, + }) +} + +pub fn prove_folded_row_sumcheck( + transcript: &mut impl Transcript, + row_integrand_values: &[F], + t_prime: &F, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + let claimed = folded_row_integrand_sum(row_integrand_values, field_cfg)?; + verify_folded_row_sumcheck_claim(&claimed, t_prime)?; + let group = build_folded_row_sumcheck_group(row_integrand_values, field_cfg)?; + let (proof, _) = + MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], SHA_ROW_VARS, field_cfg); + Ok(proof) +} + +pub fn verify_folded_row_sumcheck( + transcript: &mut impl Transcript, + proof: &MultiDegreeSumcheckProof, + t_prime: &F, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + require_single_sumcheck_group(proof, "folded row sumcheck")?; + for °ree in proof.degrees() { + if degree > 3 { + return Err(ProductionShaError::RowSumcheckDegreeTooHigh { degree }); + } + } + let Some(claimed_sum) = proof.claimed_sums().first() else { + return Err(ProductionShaError::LengthMismatch { + label: "folded row claimed sums", + got: 0, + expected: 1, + }); + }; + verify_folded_row_sumcheck_claim(claimed_sum, t_prime)?; + let subclaims = + MultiDegreeSumcheck::verify_as_subprotocol(transcript, SHA_ROW_VARS, proof, field_cfg)?; + Ok(FoldedRowSumcheckOutput { + r_star: subclaims.point().to_vec(), + terminal_value: subclaims.expected_evaluations()[0].clone(), + }) +} + +pub fn verify_folded_row_terminal_value( + output: &FoldedRowSumcheckOutput, + reconstructed_terminal: &F, +) -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + if &output.terminal_value != reconstructed_terminal { + return Err(ProductionShaError::RowSumcheckTerminalMismatch); + } + Ok(()) +} + +pub fn verify_endpoint_scalarization( + endpoint_evals: &ShaEndpointEvals, + a: &F, + field_cfg: &F::Config, +) -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + let powers = zinc_utils::powers(a.clone(), F::one_with_cfg(field_cfg), 32); + for source in &endpoint_evals.sources { + let recombined = source + .bits + .iter() + .zip(powers.iter()) + .fold(F::zero_with_cfg(field_cfg), |acc, (bit, power)| { + acc + bit.clone() * power + }); + if recombined != source.scalarized { + return Err(ProductionShaError::EndpointScalarizationMismatch { + col: source.col, + shift: source.shift, + }); + } + } + Ok(()) +} + +pub fn reconstruct_virtual_ch_maj_endpoint( + endpoint_evals: &ShaEndpointEvals, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); + let bits = |col, shift| source_bits(endpoint_evals, col, shift); + let e0 = bits(ShaWordCol::E, 0)?; + let e1 = bits(ShaWordCol::E, 1)?; + let e2 = bits(ShaWordCol::E, 2)?; + let a0 = bits(ShaWordCol::A, 0)?; + let a1 = bits(ShaWordCol::A, 1)?; + let a2 = bits(ShaWordCol::A, 2)?; + let uef2 = bits(ShaWordCol::Uef, 2)?; + let uneg_eg2 = bits(ShaWordCol::UNegEg, 2)?; + let ch2_comp0 = bits(ShaWordCol::Ch2Comp, 0)?; + let maj2 = bits(ShaWordCol::Maj, 2)?; + let maj_comp0 = bits(ShaWordCol::MajComp, 0)?; + + Ok(VirtualChMajEndpoint { + ch1: std::array::from_fn(|idx| e2[idx].clone() + &e1[idx] - two.clone() * &uef2[idx]), + ch2: std::array::from_fn(|idx| { + e2[idx].clone() - &e0[idx] + + two.clone() * &uneg_eg2[idx] + + two.clone() * &ch2_comp0[idx] + }), + maj: std::array::from_fn(|idx| { + a0[idx].clone() + &a1[idx] + &a2[idx] + - two.clone() * &maj2[idx] + - two.clone() * &maj_comp0[idx] + }), + }) +} + +pub fn booleanity_endpoint_value( + endpoint_evals: &ShaEndpointEvals, + source: &ShaBooleanitySource, + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + match source { + ShaBooleanitySource::WordBit { col, bit } => Ok(source_bits(endpoint_evals, *col, 0)? + .get(*bit) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "endpoint bit", + got: *bit, + expected: 32, + })?), + ShaBooleanitySource::VirtualCh1 { bit } => Ok(reconstruct_virtual_ch_maj_endpoint( + endpoint_evals, + field_cfg, + )? + .ch1 + .get(*bit) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "virtual Ch1 bit", + got: *bit, + expected: 32, + })?), + ShaBooleanitySource::VirtualCh2 { bit } => Ok(reconstruct_virtual_ch_maj_endpoint( + endpoint_evals, + field_cfg, + )? + .ch2 + .get(*bit) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "virtual Ch2 bit", + got: *bit, + expected: 32, + })?), + ShaBooleanitySource::VirtualMaj { bit } => Ok(reconstruct_virtual_ch_maj_endpoint( + endpoint_evals, + field_cfg, + )? + .maj + .get(*bit) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "virtual Maj bit", + got: *bit, + expected: 32, + })?), + } +} + +fn source_bits( + endpoint_evals: &ShaEndpointEvals, + col: ShaWordCol, + shift: usize, +) -> Result<&[F; 32], ProductionShaError> +where + F: PrimeField, +{ + endpoint_evals + .sources + .iter() + .find(|source| source.col == col && source.shift == shift) + .map(|source| &source.bits) + .ok_or(ProductionShaError::MissingEndpointEval { col, shift }) +} + +fn sumfold_expected_eval( + beta: &[F], + fresh_targets: &[F], + r_b: &[F], + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + let d = eq_eval(beta, r_b, F::one_with_cfg(field_cfg))?; + let weights = build_eq_x_r_vec(r_b, field_cfg)?; + if weights.len() != fresh_targets.len() { + return Err(ProductionShaError::LengthMismatch { + label: "sumfold target weights", + got: weights.len(), + expected: fresh_targets.len(), + }); + } + let claim_at_r = weights + .iter() + .zip(fresh_targets) + .fold(F::zero_with_cfg(field_cfg), |acc, (weight, target)| { + acc + weight.clone() * target + }); + Ok(d * claim_at_r) +} + +fn require_single_sumcheck_group( + proof: &MultiDegreeSumcheckProof, + label: &'static str, +) -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + let group_count = proof.degrees().len(); + if group_count != 1 { + return Err(ProductionShaError::UnexpectedSumcheckGroupCount { + label, + got: group_count, + }); + } + let claimed_count = proof.claimed_sums().len(); + if claimed_count != 1 { + return Err(ProductionShaError::LengthMismatch { + label: "sumcheck claimed sums", + got: claimed_count, + expected: 1, + }); + } + Ok(()) +} + +#[allow(dead_code)] +fn family_weight_index(family: ShaResidualFamily) -> usize { + family.index() +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::fixed_prime; + use crypto_primitives::{ + FromWithConfig, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, + }; + use zinc_piop::neutron_nova::SHA_WORD_BITS; + use zinc_transcript::{Blake3Transcript, traits::Transcript}; + + type F = MontyField<4>; + + fn cfg() -> ::Config { + fixed_prime::secp256k1_field_cfg::>() + } + + fn f(value: u64) -> F { + F::from_with_cfg(value, &cfg()) + } + + fn endpoint_source(col: ShaWordCol, shift: usize, seed: u64) -> ShaSourceEndpointEval { + let field_cfg = cfg(); + let bits = std::array::from_fn(|idx| f(seed + idx as u64 + 1)); + let powers = zinc_utils::powers(f(7), F::one_with_cfg(&field_cfg), 32); + let scalarized = bits + .iter() + .zip(powers.iter()) + .fold(F::zero_with_cfg(&field_cfg), |acc, (bit, power)| { + acc + bit.clone() * power + }); + ShaSourceEndpointEval { + col, + shift, + scalarized, + bits, + } + } + + fn endpoint_evals_for_virtuals() -> ShaEndpointEvals { + ShaEndpointEvals { + sources: vec![ + endpoint_source(ShaWordCol::E, 0, 10), + endpoint_source(ShaWordCol::E, 1, 20), + endpoint_source(ShaWordCol::E, 2, 30), + endpoint_source(ShaWordCol::A, 0, 40), + endpoint_source(ShaWordCol::A, 1, 50), + endpoint_source(ShaWordCol::A, 2, 60), + endpoint_source(ShaWordCol::Uef, 2, 70), + endpoint_source(ShaWordCol::UNegEg, 2, 80), + endpoint_source(ShaWordCol::Ch2Comp, 0, 90), + endpoint_source(ShaWordCol::Maj, 2, 100), + endpoint_source(ShaWordCol::MajComp, 0, 110), + ], + } + } + + #[test] + fn fresh_ideal_coefficients_are_bound_before_a() { + let field_cfg = cfg(); + let ideals = vec![std::array::from_fn(|idx| { + DynamicPolynomialF::new_trimmed(vec![f(idx as u64 + 1), f(99)]) + })]; + let mut tampered = ideals.clone(); + tampered[0][0].coeffs[0] += f(1); + + let sample_a = |values: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]]| { + let mut transcript = Blake3Transcript::new(); + transcript.absorb_slice(b"fresh-commitments-and-public-inputs"); + let _r_ic = sample_pre_ideal_challenge::(&mut transcript, &field_cfg); + absorb_fresh_sha_ideal_polys(&mut transcript, values); + let (a, _, _, _, _) = + sample_post_ideal_challenges::(&mut transcript, 1, &field_cfg).unwrap(); + a + }; + + assert_ne!(sample_a(&ideals), sample_a(&tampered)); + } + + #[test] + fn fresh_ideal_absorption_binds_polynomial_slot_structure() { + let field_cfg = cfg(); + let mut packed = vec![std::array::from_fn(|_| DynamicPolynomialF::new(Vec::::new()))]; + packed[0][0] = DynamicPolynomialF::new(vec![f(1), f(2)]); + + let mut split = vec![std::array::from_fn(|_| DynamicPolynomialF::new(Vec::::new()))]; + split[0][0] = DynamicPolynomialF::new(vec![f(1)]); + split[0][1] = DynamicPolynomialF::new(vec![f(2)]); + + let sample_a = |values: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]]| { + let mut transcript = Blake3Transcript::new(); + transcript.absorb_slice(b"fresh-commitments-and-public-inputs"); + let _r_ic = sample_pre_ideal_challenge::(&mut transcript, &field_cfg); + absorb_fresh_sha_ideal_polys(&mut transcript, values); + let (a, _, _, _, _) = + sample_post_ideal_challenges::(&mut transcript, 1, &field_cfg).unwrap(); + a + }; + + assert_ne!(sample_a(&packed), sample_a(&split)); + } + + #[test] + fn sumfold_outputs_instance_fold_point_before_theta() { + let field_cfg = cfg(); + let fresh_targets = vec![f(2), f(5), f(7), f(11)]; + let beta = vec![f(13), f(17)]; + + let mut prover_transcript = Blake3Transcript::new(); + prover_transcript.absorb_slice(b"bound-before-sumfold"); + let (proof, prover_output) = + prove_sha_sumfold_targets(&mut prover_transcript, &fresh_targets, &beta, 1, &field_cfg) + .unwrap(); + + let mut verifier_transcript = Blake3Transcript::new(); + verifier_transcript.absorb_slice(b"bound-before-sumfold"); + let verifier_output = verify_sha_sumfold_targets( + &mut verifier_transcript, + &proof, + &fresh_targets, + &beta, + &field_cfg, + ) + .unwrap(); + + assert_eq!(verifier_output, prover_output); + assert_eq!( + prover_output.theta(), + build_eq_x_r_vec(prover_output.r_b(), &field_cfg).unwrap() + ); + assert_eq!(prover_output.theta().len(), fresh_targets.len()); + + let d = eq_eval(&beta, prover_output.r_b(), F::one_with_cfg(&field_cfg)).unwrap(); + assert_eq!(prover_output.c_sf(), &(d * prover_output.t_prime())); + + let mut bad_targets = fresh_targets; + bad_targets[0] += f(1); + let mut bad_transcript = Blake3Transcript::new(); + bad_transcript.absorb_slice(b"bound-before-sumfold"); + assert!( + verify_sha_sumfold_targets( + &mut bad_transcript, + &proof, + &bad_targets, + &beta, + &field_cfg + ) + .is_err() + ); + } + + #[test] + fn sumfold_verifier_rejects_extra_groups() { + let field_cfg = cfg(); + let fresh_targets = vec![f(2), f(5), f(7), f(11)]; + let beta = vec![f(13), f(17)]; + let claims = + zinc_piop::neutron_nova::LinearInstanceClaims::new(fresh_targets.clone()).unwrap(); + let group_0 = claims + .build_hybrid_sumcheck_group(&beta, 1, &field_cfg) + .unwrap(); + let group_1 = claims + .build_hybrid_sumcheck_group(&beta, 1, &field_cfg) + .unwrap(); + + let mut prover_transcript = Blake3Transcript::new(); + prover_transcript.absorb_slice(b"bound-before-sumfold"); + let (proof, _) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut prover_transcript, + vec![group_0, group_1], + claims.ell(), + &field_cfg, + ); + + let mut verifier_transcript = Blake3Transcript::new(); + verifier_transcript.absorb_slice(b"bound-before-sumfold"); + assert!(matches!( + verify_sha_sumfold_targets( + &mut verifier_transcript, + &proof, + &fresh_targets, + &beta, + &field_cfg, + ), + Err(ProductionShaError::UnexpectedSumcheckGroupCount { + label: "SHA SumFold", + got: 2 + }) + )); + } + + #[test] + fn folded_row_sumcheck_claim_is_t_prime() { + let field_cfg = cfg(); + let row_integrand_values = (0..(1usize << SHA_ROW_VARS)) + .map(|idx| f((idx as u64).wrapping_mul(3) + 1)) + .collect::>(); + let t_prime = folded_row_integrand_sum(&row_integrand_values, &field_cfg).unwrap(); + + let mut prover_transcript = Blake3Transcript::new(); + prover_transcript.absorb_slice(b"folded-row-context"); + let proof = prove_folded_row_sumcheck( + &mut prover_transcript, + &row_integrand_values, + &t_prime, + &field_cfg, + ) + .unwrap(); + assert_eq!(proof.claimed_sums(), &[t_prime.clone()]); + + let mut verifier_transcript = Blake3Transcript::new(); + verifier_transcript.absorb_slice(b"folded-row-context"); + let output = + verify_folded_row_sumcheck(&mut verifier_transcript, &proof, &t_prime, &field_cfg) + .unwrap(); + assert_eq!(output.r_star.len(), SHA_ROW_VARS); + + let row_weights = build_eq_x_r_vec(&output.r_star, &field_cfg).unwrap(); + let terminal = row_weights + .iter() + .zip(row_integrand_values.iter()) + .fold(F::zero_with_cfg(&field_cfg), |acc, (weight, value)| { + acc + weight.clone() * value + }); + verify_folded_row_terminal_value(&output, &terminal).unwrap(); + + let mut bad_terminal = terminal; + bad_terminal += f(1); + assert!(verify_folded_row_terminal_value(&output, &bad_terminal).is_err()); + + let mut bad_t = t_prime; + bad_t += f(1); + let mut bad_transcript = Blake3Transcript::new(); + bad_transcript.absorb_slice(b"folded-row-context"); + assert!( + verify_folded_row_sumcheck(&mut bad_transcript, &proof, &bad_t, &field_cfg).is_err() + ); + } + + #[test] + fn folded_row_verifier_rejects_extra_groups() { + let field_cfg = cfg(); + let row_integrand_values = (0..(1usize << SHA_ROW_VARS)) + .map(|idx| f((idx as u64).wrapping_mul(5) + 9)) + .collect::>(); + let t_prime = folded_row_integrand_sum(&row_integrand_values, &field_cfg).unwrap(); + let group_0 = build_folded_row_sumcheck_group(&row_integrand_values, &field_cfg).unwrap(); + let group_1 = build_folded_row_sumcheck_group(&row_integrand_values, &field_cfg).unwrap(); + + let mut prover_transcript = Blake3Transcript::new(); + prover_transcript.absorb_slice(b"folded-row-context"); + let (proof, _) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut prover_transcript, + vec![group_0, group_1], + SHA_ROW_VARS, + &field_cfg, + ); + + let mut verifier_transcript = Blake3Transcript::new(); + verifier_transcript.absorb_slice(b"folded-row-context"); + assert!(matches!( + verify_folded_row_sumcheck(&mut verifier_transcript, &proof, &t_prime, &field_cfg), + Err(ProductionShaError::UnexpectedSumcheckGroupCount { + label: "folded row sumcheck", + got: 2 + }) + )); + } + + #[test] + fn scalarization_and_virtual_endpoints_use_source_bits_only() { + let field_cfg = cfg(); + let mut endpoint_evals = endpoint_evals_for_virtuals(); + verify_endpoint_scalarization(&endpoint_evals, &f(7), &field_cfg).unwrap(); + + let virtuals = reconstruct_virtual_ch_maj_endpoint(&endpoint_evals, &field_cfg).unwrap(); + let two = f(2); + for bit in 0..SHA_WORD_BITS { + let e0 = source_bits(&endpoint_evals, ShaWordCol::E, 0).unwrap()[bit].clone(); + let e1 = source_bits(&endpoint_evals, ShaWordCol::E, 1).unwrap()[bit].clone(); + let e2 = source_bits(&endpoint_evals, ShaWordCol::E, 2).unwrap()[bit].clone(); + let a0 = source_bits(&endpoint_evals, ShaWordCol::A, 0).unwrap()[bit].clone(); + let a1 = source_bits(&endpoint_evals, ShaWordCol::A, 1).unwrap()[bit].clone(); + let a2 = source_bits(&endpoint_evals, ShaWordCol::A, 2).unwrap()[bit].clone(); + let uef2 = source_bits(&endpoint_evals, ShaWordCol::Uef, 2).unwrap()[bit].clone(); + let uneg_eg2 = + source_bits(&endpoint_evals, ShaWordCol::UNegEg, 2).unwrap()[bit].clone(); + let ch2_comp0 = + source_bits(&endpoint_evals, ShaWordCol::Ch2Comp, 0).unwrap()[bit].clone(); + let maj2 = source_bits(&endpoint_evals, ShaWordCol::Maj, 2).unwrap()[bit].clone(); + let maj_comp0 = + source_bits(&endpoint_evals, ShaWordCol::MajComp, 0).unwrap()[bit].clone(); + + assert_eq!(virtuals.ch1[bit], e2.clone() + e1 - two.clone() * uef2); + assert_eq!( + virtuals.ch2[bit], + e2 - e0 + two.clone() * uneg_eg2 + two.clone() * ch2_comp0 + ); + assert_eq!( + virtuals.maj[bit], + a0 + a1 + a2 - two.clone() * maj2 - two.clone() * maj_comp0 + ); + } + + endpoint_evals.sources[0].scalarized += f(1); + assert!(verify_endpoint_scalarization(&endpoint_evals, &f(7), &field_cfg).is_err()); + } +} diff --git a/zip-plus/src/pcs/generic.rs b/zip-plus/src/pcs/generic.rs index b1429cff..6bb7782e 100644 --- a/zip-plus/src/pcs/generic.rs +++ b/zip-plus/src/pcs/generic.rs @@ -69,6 +69,35 @@ where F::Modulus: Transcribable; } +/// Homomorphic extension of [`PCS`] used by instance-axis folding protocols. +/// +/// Implementations must satisfy: +/// +/// ```text +/// fold_commitments([Com(w_i; eta_i)], theta) +/// = Com(sum_i theta_i w_i; sum_i theta_i eta_i) +/// ``` +/// +/// Non-homomorphic commitments, such as Merkle roots, must not implement this +/// trait. +pub trait FoldablePCS: PCS +where + F: PrimeField, + Eval: Clone + Debug + Send + Sync, +{ + fn fold_commitments( + commitments: &[Self::Commitment], + theta: &[F], + field_cfg: &F::Config, + ) -> Result; + + fn fold_prover_data( + prover_data: &[Self::ProverData], + theta: &[F], + field_cfg: &F::Config, + ) -> Result; +} + #[derive(Clone, Debug)] pub struct ZipPlusPCS>(PhantomData<(Zt, Lc)>); diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index 5fecbf81..4566a7d8 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -25,7 +25,7 @@ use zinc_transcript::traits::{ConstTranscribable, GenTranscribable, Transcribabl use crate::{ ZipError, pcs::{ - generic::PCS, + generic::{FoldablePCS, PCS}, msm_commitment::{ BoolSubsetMsm, MsmCommitmentEngine, MsmCommitmentKey, MsmError, RowMsmStrategy, ScalarPippengerMsm, @@ -37,6 +37,114 @@ use crate::{ #[derive(Clone, Debug)] pub struct HyraxPCS(PhantomData<(C, Lanes)>); +impl HyraxPCS +where + C: AffineRepr, +{ + /// Open a folded Hyrax commitment whose lane values are already scalar + /// field elements. + /// + /// This is needed for instance-axis folds of binary commitments: after + /// folding by transcript-derived weights, each bit lane is a scalar field + /// linear combination of bits, not a `bool`. + #[allow(clippy::arithmetic_side_effects)] + pub fn prove_open_scalar_lanes( + transcript: &mut PcsProverTranscript, + ck: &HyraxCommitmentKey, + scalar_lanes: &[Vec>], + point: &[F], + prover_data: &HyraxProverData, + field_cfg: &F::Config, + ) -> Result<(), ZipError> + where + F: HyraxFieldBridge, + F::Inner: Transcribable, + F::Modulus: Transcribable, + Lanes: Clone + Debug + Send + Sync, + { + let _ = CHECK_FOR_OVERFLOW; + if scalar_lanes.is_empty() { + return Ok(()); + } + validate_scalar_lanes::(ck, scalar_lanes, point.len(), prover_data)?; + + let n = scalar_lanes[0][0].len(); + let point_scalar = point.iter().map(F::field_to_scalar).collect::>(); + let row_vars = prover_data.num_rows.ilog2() as usize; + let q0_f = eq_tensor_f::(&point[..row_vars], field_cfg); + let q1_scalar = eq_tensor_scalar::(&point_scalar[row_vars..]); + let alphas = sample_scalars::( + &mut transcript.fs_transcript, + scalar_lanes.len() * prover_data.num_lanes, + ); + + let mut b_scalar = vec![C::ScalarField::zero(); prover_data.num_rows]; + for (poly_idx, lanes) in scalar_lanes.iter().enumerate() { + for (lane, values) in lanes.iter().enumerate() { + let alpha = alphas[alpha_index_dynamic(prover_data.num_lanes, poly_idx, lane)]; + for (row_idx, row) in values.chunks(ck.num_cols).enumerate() { + let mut row_eval = C::ScalarField::zero(); + for (col_idx, value) in row.iter().enumerate() { + if let Some(weight) = q1_scalar.get(col_idx) { + row_eval += *value * weight; + } + } + b_scalar[row_idx] += alpha * row_eval; + } + } + } + + let b_f = b_scalar + .iter() + .map(|value| F::scalar_to_field(value, field_cfg)) + .collect::>(); + transcript.write_field_elements(&b_f)?; + + let row_coeffs = if prover_data.num_rows == 1 { + vec![C::ScalarField::from(1u64)] + } else { + sample_scalars::(&mut transcript.fs_transcript, prover_data.num_rows) + }; + + let mut combined_row = vec![C::ScalarField::zero(); ck.num_cols]; + let mut rho_star = C::ScalarField::zero(); + for (poly_idx, lanes) in scalar_lanes.iter().enumerate() { + for (lane, values) in lanes.iter().enumerate() { + let alpha = alphas[alpha_index_dynamic(prover_data.num_lanes, poly_idx, lane)]; + for (row_idx, row) in values.chunks(ck.num_cols).enumerate() { + let coeff = alpha * row_coeffs[row_idx]; + if ck.blinding_mode.is_blinded() { + let blind_idx = commitment_index_dynamic( + prover_data.num_lanes, + poly_idx, + lane, + row_idx, + prover_data.num_rows, + ); + rho_star += coeff * prover_data.blinds[blind_idx]; + } + for (col_idx, value) in row.iter().enumerate() { + combined_row[col_idx] += coeff * value; + } + } + } + } + + write_scalars::(transcript, &combined_row)?; + if ck.blinding_mode.is_blinded() { + write_scalar::(transcript, &rho_star)?; + } + + if q0_f.len() != b_f.len() || n != (1usize << point.len()) { + return Err(ZipError::InvalidPcsOpen( + "Hyrax folded scalar-lane opening shape mismatch".to_string(), + )); + } + + Ok(()) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum HyraxBlindingMode { Blinded, @@ -117,6 +225,21 @@ where } } +fn validate_fold_inputs(values: &[T], theta_len: usize, label: &str) -> Result<(), ZipError> { + if values.is_empty() { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax cannot fold empty {label}" + ))); + } + if values.len() != theta_len { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax fold {label} count mismatch: got {}, expected {theta_len}", + values.len() + ))); + } + Ok(()) +} + pub trait HyraxLanes: Clone + Debug + Send + Sync where C: AffineRepr, @@ -678,6 +801,78 @@ where } } +impl FoldablePCS for HyraxPCS +where + F: HyraxFieldBridge, + C: AffineRepr, + Eval: Clone + Debug + Send + Sync, + Lanes: HyraxLanes, +{ + fn fold_commitments( + commitments: &[Self::Commitment], + theta: &[F], + field_cfg: &F::Config, + ) -> Result { + let _ = field_cfg; + validate_fold_inputs(commitments, theta.len(), "commitments")?; + let first = &commitments[0]; + validate_commitment_shape::(first)?; + + let mut folded = vec![C::Group::zero(); first.comm.len()]; + for (commitment, weight) in commitments.iter().zip(theta) { + validate_commitment_shape::(commitment)?; + if !same_commitment_shape(first, commitment) { + return Err(ZipError::InvalidPcsParam( + "Hyrax commitment fold shape mismatch".to_string(), + )); + } + let scalar = F::field_to_scalar(weight); + for (out, value) in folded.iter_mut().zip(commitment.comm.iter()) { + *out += value.clone() * scalar; + } + } + + Ok(HyraxCommitment { + batch_size: first.batch_size, + num_lanes: first.num_lanes, + num_rows: first.num_rows, + blinding_mode: first.blinding_mode, + comm: folded, + }) + } + + fn fold_prover_data( + prover_data: &[Self::ProverData], + theta: &[F], + field_cfg: &F::Config, + ) -> Result { + let _ = field_cfg; + validate_fold_inputs(prover_data, theta.len(), "prover data")?; + let first = &prover_data[0]; + + let mut folded_blinds = vec![C::ScalarField::zero(); first.blinds.len()]; + for (data, weight) in prover_data.iter().zip(theta) { + if !same_prover_data_shape(first, data) { + return Err(ZipError::InvalidPcsParam( + "Hyrax prover-data fold shape mismatch".to_string(), + )); + } + let scalar = F::field_to_scalar(weight); + for (out, blind) in folded_blinds.iter_mut().zip(data.blinds.iter()) { + *out += *blind * scalar; + } + } + + Ok(HyraxProverData { + batch_size: first.batch_size, + num_lanes: first.num_lanes, + num_rows: first.num_rows, + blinding_mode: first.blinding_mode, + blinds: folded_blinds, + }) + } +} + fn validate_polys(polys: &[DenseMultilinearExtension]) -> Result<(), ZipError> { if let Some(first) = polys.first() { for poly in polys { @@ -692,6 +887,81 @@ fn validate_polys(polys: &[DenseMultilinearExtension]) -> Res Ok(()) } +fn validate_scalar_lanes( + ck: &HyraxCommitmentKey, + scalar_lanes: &[Vec>], + point_len: usize, + prover_data: &HyraxProverData, +) -> Result<(), ZipError> +where + C: AffineRepr, +{ + let expected_n = 1usize + .checked_shl(u32::try_from(point_len).map_err(|_| { + ZipError::InvalidPcsParam(format!("Hyrax point length {point_len} is too large")) + })?) + .ok_or_else(|| { + ZipError::InvalidPcsParam(format!("Hyrax point length {point_len} is too large")) + })?; + let expected_rows = num_rows(expected_n, ck.num_cols)?; + if prover_data.batch_size != scalar_lanes.len() + || prover_data.num_rows != expected_rows + || prover_data.blinding_mode != ck.blinding_mode + { + return Err(ZipError::InvalidPcsParam( + "Hyrax scalar-lane prover data shape mismatch".to_string(), + )); + } + let expected_blinds = if ck.blinding_mode.is_blinded() { + prover_data.batch_size * prover_data.num_lanes * prover_data.num_rows + } else { + 0 + }; + if prover_data.blinds.len() != expected_blinds { + return Err(ZipError::InvalidPcsParam( + "Hyrax scalar-lane blind count mismatch".to_string(), + )); + } + for lanes in scalar_lanes { + if lanes.len() != prover_data.num_lanes { + return Err(ZipError::InvalidPcsParam( + "Hyrax scalar-lane count mismatch".to_string(), + )); + } + for values in lanes { + if values.len() != expected_n { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax scalar-lane length mismatch: got {}, expected {expected_n}", + values.len() + ))); + } + } + } + Ok(()) +} + +fn same_commitment_shape( + lhs: &HyraxCommitment, + rhs: &HyraxCommitment, +) -> bool { + lhs.batch_size == rhs.batch_size + && lhs.num_lanes == rhs.num_lanes + && lhs.num_rows == rhs.num_rows + && lhs.blinding_mode == rhs.blinding_mode + && lhs.comm.len() == rhs.comm.len() +} + +fn same_prover_data_shape( + lhs: &HyraxProverData, + rhs: &HyraxProverData, +) -> bool { + lhs.batch_size == rhs.batch_size + && lhs.num_lanes == rhs.num_lanes + && lhs.num_rows == rhs.num_rows + && lhs.blinding_mode == rhs.blinding_mode + && lhs.blinds.len() == rhs.blinds.len() +} + fn validate_hyrax_shape( width: usize, blinding_mode: HyraxBlindingMode, @@ -1167,4 +1437,221 @@ mod tests { ); assert!(result.is_err()); } + + #[test] + fn folded_binary_hyrax_commitment_opens_from_scalar_lanes() { + type C = ark_bn254::G1Affine; + type F = MontyField<4>; + const D: usize = 32; + + fn bp(bits: u32) -> BinaryPoly { + BinaryPoly::::from(bits) + } + + let cfg = cfg_from_curve::(); + let n = 8; + let width = n; + let generator = ::Group::generator(); + let bases = (1..=width) + .map(|idx| (generator * ::ScalarField::from(idx as u64)).into_affine()) + .collect::>(); + let h = generator * ::ScalarField::from((width + 1) as u64); + let (ck, vk) = HyraxPCS::::setup_from_bases_with_blinding( + width, + bases, + h, + HyraxBlindingMode::Blinded, + ) + .unwrap(); + + let instance_polys = [ + vec![ + DenseMultilinearExtension::from_evaluations_vec( + 3, + (0..n).map(|idx| bp((idx as u32) * 17 + 3)).collect(), + bp(0), + ), + DenseMultilinearExtension::from_evaluations_vec( + 3, + (0..n).map(|idx| bp(!((idx as u32) * 11))).collect(), + bp(0), + ), + ], + vec![ + DenseMultilinearExtension::from_evaluations_vec( + 3, + (0..n).map(|idx| bp((idx as u32) * 23 + 9)).collect(), + bp(0), + ), + DenseMultilinearExtension::from_evaluations_vec( + 3, + (0..n).map(|idx| bp(!((idx as u32) * 5 + 7))).collect(), + bp(0), + ), + ], + ]; + + let mut prover_data = Vec::new(); + let mut commitments = Vec::new(); + for polys in &instance_polys { + let (data, commitment) = + as PCS, D>>::commit(&ck, polys).unwrap(); + prover_data.push(data); + commitments.push(commitment); + } + + let theta = [F::from_with_cfg(3u64, &cfg), F::from_with_cfg(5u64, &cfg)]; + let folded_commitment = + as FoldablePCS, D>>::fold_commitments( + &commitments, + &theta, + &cfg, + ) + .unwrap(); + let folded_data = + as FoldablePCS, D>>::fold_prover_data( + &prover_data, + &theta, + &cfg, + ) + .unwrap(); + + let theta_scalar = theta + .iter() + .map(>::field_to_scalar) + .collect::>(); + let mut scalar_lanes = + vec![vec![vec![::ScalarField::zero(); n]; D]; instance_polys[0].len()]; + for (instance_idx, polys) in instance_polys.iter().enumerate() { + for (poly_idx, poly) in polys.iter().enumerate() { + for (eval_idx, eval) in poly.evaluations.iter().enumerate() { + for (lane, bit) in eval.iter().enumerate() { + if bit.inner() { + scalar_lanes[poly_idx][lane][eval_idx] += theta_scalar[instance_idx]; + } + } + } + } + } + + let point = [[0x11u8; 64], [0x22u8; 64], [0x33u8; 64]] + .iter() + .map(|bytes| { + let scalar = ::ScalarField::from_le_bytes_mod_order(bytes); + >::scalar_to_field(&scalar, &cfg) + }) + .collect::>(); + let eq = eq_tensor_f::(&point, &cfg); + let folded_lifted_evals = scalar_lanes + .iter() + .map(|lanes| { + let coeffs = lanes + .iter() + .map(|values| { + values.iter().zip(eq.iter()).fold( + F::zero_with_cfg(&cfg), + |mut acc, (value, weight)| { + acc += >::scalar_to_field(value, &cfg) + * weight; + acc + }, + ) + }) + .collect::>(); + DynamicPolynomialF::new_trimmed(coeffs) + }) + .collect::>(); + + let mut prover_transcript = PcsProverTranscript { + fs_transcript: Default::default(), + stream: Default::default(), + }; + as PCS, D>>::absorb_commitment( + &mut prover_transcript.fs_transcript, + &folded_commitment, + ); + let mut transcription_buf = vec![0u8; ::Inner::NUM_BYTES]; + for lifted_eval in &folded_lifted_evals { + prover_transcript + .fs_transcript + .absorb_random_field_slice(&lifted_eval.coeffs, &mut transcription_buf); + } + HyraxPCS::::prove_open_scalar_lanes::( + &mut prover_transcript, + &ck, + &scalar_lanes, + &point, + &folded_data, + &cfg, + ) + .unwrap(); + + let mut verifier_transcript = prover_transcript.into_verification_transcript(); + as PCS, D>>::absorb_commitment( + &mut verifier_transcript.fs_transcript, + &folded_commitment, + ); + let mut transcription_buf = vec![0u8; ::Inner::NUM_BYTES]; + for lifted_eval in &folded_lifted_evals { + verifier_transcript + .fs_transcript + .absorb_random_field_slice(&lifted_eval.coeffs, &mut transcription_buf); + } + as PCS, D>>::verify_open::( + &mut verifier_transcript, + &vk, + &folded_commitment, + &point, + &folded_lifted_evals, + &cfg, + ) + .unwrap(); + } + + #[test] + fn hyrax_fold_rejects_commitment_shape_mismatch() { + type C = ark_bn254::G1Affine; + type F = MontyField<4>; + const D: usize = 32; + + let cfg = cfg_from_curve::(); + let generator = ::Group::generator(); + let bases = (1..=4) + .map(|idx| (generator * ::ScalarField::from(idx as u64)).into_affine()) + .collect::>(); + let h = generator * ::ScalarField::from(5u64); + let (ck, _) = HyraxPCS::::setup_from_bases_with_blinding( + 4, + bases, + h, + HyraxBlindingMode::Unblinded, + ) + .unwrap(); + let polys_one = vec![DenseMultilinearExtension::from_evaluations_vec( + 2, + vec![BinaryPoly::::from(1u32); 4], + BinaryPoly::::from(0u32), + )]; + let polys_two = vec![DenseMultilinearExtension::from_evaluations_vec( + 3, + vec![BinaryPoly::::from(2u32); 8], + BinaryPoly::::from(0u32), + )]; + let (_, c0) = + as PCS, D>>::commit(&ck, &polys_one) + .unwrap(); + let (_, c1) = + as PCS, D>>::commit(&ck, &polys_two) + .unwrap(); + + let theta = [F::from_with_cfg(1u64, &cfg), F::from_with_cfg(2u64, &cfg)]; + assert!(matches!( + as FoldablePCS, D>>::fold_commitments( + &[c0, c1], + &theta, + &cfg, + ), + Err(ZipError::InvalidPcsParam(_)) + )); + } } From f44b9825cc3b2f9c45eca6f9e53ff5a41ff0fd79 Mon Sep 17 00:00:00 2001 From: John Wu Date: Sat, 6 Jun 2026 08:50:35 -0700 Subject: [PATCH 16/49] Implement production SHA ProjectionFold core --- piop/src/neutron_nova/mod.rs | 10 +- piop/src/neutron_nova/projection_sha.rs | 444 ++++ protocol/src/pcs.rs | 29 +- protocol/src/production_sha.rs | 3188 +++++++++++++++++++++-- 4 files changed, 3503 insertions(+), 168 deletions(-) diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index c2ff439e..caefaa0b 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -30,11 +30,15 @@ pub use projection_sha::{ SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, ShaBitSliceColumns, ShaBooleanitySource, ShaIntCol, ShaIntColumns, ShaProductionIdeal, ShaProjectionError, ShaPublicCol, ShaPublicColumns, ShaResidualFamily, ShaScalarizedRows, ShaSumFoldOutput, ShaWordCol, VirtualChMajValues, + build_dense_sha_sumfold_group, build_expression_folded_row_sumcheck_group, build_folded_row_sumcheck_group, build_fresh_sha_ideal_cache, build_sha_ideal_values_at_point, check_fresh_sha_ideal_cache, check_sha_ideal_values, evaluate_fresh_sha_targets, - finalize_sha_sumfold, fold_projected_sha_traces, folded_row_integrand_sum, - folded_row_integrand_values, production_sha_nonzero_families, production_sha_nonzero_ideals, - reconstruct_virtual_ch_maj_at_row, scalarize_trace_words, verify_folded_row_sumcheck_claim, + expression_folded_row_sum, finalize_sha_sumfold, fold_projected_sha_traces, + folded_row_integrand_sum, folded_row_integrand_values, production_sha_booleanity_sources, + production_sha_nonzero_families, production_sha_nonzero_ideals, + reconstruct_virtual_ch_maj_at_row, scalarize_trace_words, sha_int_at_point, + sha_linear_residual_row_value, sha_linear_residual_sum, sha_public_at_point, + sha_scalarized_word_at_point, sha_word_bits_at_point, verify_folded_row_sumcheck_claim, verify_folded_scalarization_links, verify_folded_scalarization_links_at_point, verify_folded_shifted_scalarization_link_at_point, }; diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 9adb5bb6..723e61a4 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -147,6 +147,26 @@ pub enum ShaWordCol { } impl ShaWordCol { + pub const ALL: [Self; 17] = [ + Self::A, + Self::E, + Self::Sigma0, + Self::Sigma1, + Self::W, + Self::SmallSigma0, + Self::SmallSigma1, + Self::Uef, + Self::UNegEg, + Self::Maj, + Self::MuPacked, + Self::OvSigma0, + Self::OvSigma1, + Self::OvSmallSigma0, + Self::OvSmallSigma1, + Self::Ch2Comp, + Self::MajComp, + ]; + pub const COUNT: usize = 17; pub fn index(self) -> usize { @@ -165,6 +185,14 @@ pub enum ShaIntCol { } impl ShaIntCol { + pub const ALL: [Self; 5] = [ + Self::CompSchedule, + Self::CompUpdateA, + Self::CompUpdateE, + Self::CompFeedForwardA, + Self::CompFeedForwardE, + ]; + pub const COUNT: usize = 5; pub fn index(self) -> usize { @@ -190,6 +218,21 @@ pub enum ShaPublicCol { } impl ShaPublicCol { + pub const ALL: [Self; 12] = [ + Self::K, + Self::PAIn, + Self::PEIn, + Self::PAOut, + Self::PEOut, + Self::Message, + Self::SInit, + Self::SMsg, + Self::SSched, + Self::SUpd, + Self::SFf, + Self::SOut, + ]; + pub const COUNT: usize = 12; pub fn index(self) -> usize { @@ -908,6 +951,407 @@ where .fold(F::zero_with_cfg(field_cfg), |acc, value| acc + value)) } +/// Canonical booleanity sources for the production SHA ProjectionFold flow. +/// +/// This includes every committed binary-polynomial SHA source bit and the +/// three virtual Ch/Maj residual families. The virtual values are reconstructed +/// from source bit slices; they are never independent witness columns. +pub fn production_sha_booleanity_sources() -> Vec { + let mut sources = Vec::with_capacity(ShaWordCol::COUNT * SHA_WORD_BITS + 3 * SHA_WORD_BITS); + for col_idx in 0..ShaWordCol::COUNT { + let col = ShaWordCol::ALL[col_idx]; + for bit in 0..SHA_WORD_BITS { + sources.push(ShaBooleanitySource::WordBit { col, bit }); + } + } + for bit in 0..SHA_WORD_BITS { + sources.push(ShaBooleanitySource::VirtualCh1 { bit }); + sources.push(ShaBooleanitySource::VirtualCh2 { bit }); + sources.push(ShaBooleanitySource::VirtualMaj { bit }); + } + sources +} + +/// Evaluate the linear SHA residual scalarization at one row. +pub fn sha_linear_residual_row_value( + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + row: usize, + a: &F, + lambda: &F, + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + let residuals = residual_values_at_row(trace, public, row, a, field_cfg)?; + let lambda_powers = powers( + lambda.clone(), + F::one_with_cfg(field_cfg), + NUM_SHA_RESIDUAL_FAMILIES, + ); + Ok(residuals + .iter() + .zip(lambda_powers.iter()) + .fold(F::zero_with_cfg(field_cfg), |acc, (residual, weight)| { + acc + weight.clone() * residual + })) +} + +/// Evaluate the row-weighted linear SHA residual scalarization for one +/// instance. +pub fn sha_linear_residual_sum( + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + validate_trace(trace)?; + validate_public(public)?; + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + let mut sum = F::zero_with_cfg(field_cfg); + for (row, row_weight) in row_weights.iter().enumerate() { + sum += row_weight.clone() + * sha_linear_residual_row_value(trace, public, row, a, lambda, field_cfg)?; + } + Ok(sum) +} + +/// Build the production SHA SumFold group over the instance axis. +/// +/// The group proves the expression +/// +/// `eq(beta, b) * (L(b) + xi * B(b))` +/// +/// where `L` is the row-weighted linear SHA residual scalarization and `B` +/// is built from source booleanity MLEs. Unlike a table of fresh targets, the +/// booleanity part is evaluated from source MLEs, so the terminal at `r_b` +/// is the folded booleanity expression. +#[allow(clippy::too_many_arguments)] +pub fn build_dense_sha_sumfold_group( + traces: &[ProjectedShaTrace], + publics: &[ProjectedShaPublic], + beta: &[F], + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + if traces.is_empty() { + return Err(ShaProjectionError::InstanceCountNotPowerOfTwo { got: 0 }); + } + if traces.len() != publics.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: publics.len(), + expected: traces.len(), + }); + } + if !traces.len().is_power_of_two() { + return Err(ShaProjectionError::InstanceCountNotPowerOfTwo { got: traces.len() }); + } + let ell = usize::try_from(traces.len().trailing_zeros()).expect("trailing_zeros fits usize"); + if beta.len() != ell { + return Err(ShaProjectionError::InstanceCountMismatch { + got: beta.len(), + expected: ell, + }); + } + for trace in traces { + validate_trace(trace)?; + } + for public in publics { + validate_public(public)?; + } + + let zero = F::zero_with_cfg(field_cfg); + let zero_inner = zero.inner().clone(); + let mut mles = Vec::with_capacity(2 + booleanity_sources.len() * SHA_ROW_COUNT); + + let eq_beta = build_eq_x_r_vec(beta, field_cfg)?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + ell, + eq_beta.iter().map(|value| value.inner().clone()).collect(), + zero_inner.clone(), + )); + + let linear_values = traces + .iter() + .zip(publics.iter()) + .map(|(trace, public)| sha_linear_residual_sum(trace, public, r_ic, a, lambda, field_cfg)) + .collect::, _>>()?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + ell, + linear_values + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner.clone(), + )); + + for source in booleanity_sources { + for row in 0..SHA_ROW_COUNT { + let values = traces + .iter() + .map(|trace| booleanity_source_value_at_row(trace, row, source, field_cfg)) + .collect::, _>>()?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + ell, + values.iter().map(|value| value.inner().clone()).collect(), + zero_inner.clone(), + )); + } + } + + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + let rho_powers = powers( + rho.clone(), + F::one_with_cfg(field_cfg), + booleanity_sources.len(), + ); + let xi = xi.clone(); + let zero_for_comb = F::zero_with_cfg(field_cfg); + let one_for_comb = F::one_with_cfg(field_cfg); + Ok(MultiDegreeSumcheckGroup::new( + 3, + mles, + Box::new(move |values: &[F]| { + let eq_beta = values[0].clone(); + let linear = values[1].clone(); + let mut bool_sum = zero_for_comb.clone(); + let mut cursor = 2usize; + for rho_power in &rho_powers { + for row_weight in &row_weights { + let d = values[cursor].clone(); + cursor += 1; + let term = d.clone() * (d - one_for_comb.clone()); + bool_sum += row_weight.clone() * rho_power * term; + } + } + eq_beta * (linear + xi.clone() * bool_sum) + }), + )) +} + +/// Build the expression-backed folded row sumcheck group. +/// +/// The terminal at the verifier challenge is tied to source MLE endpoint +/// values, including booleanity sources, rather than to an opaque MLE of +/// precomputed row-integrand values. +#[allow(clippy::too_many_arguments)] +pub fn build_expression_folded_row_sumcheck_group( + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + validate_trace(trace)?; + validate_public(public)?; + + let zero = F::zero_with_cfg(field_cfg); + let zero_inner = zero.inner().clone(); + let mut mles = Vec::with_capacity(2 + booleanity_sources.len()); + + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + row_weights + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner.clone(), + )); + + let linear_values = (0..SHA_ROW_COUNT) + .map(|row| sha_linear_residual_row_value(trace, public, row, a, lambda, field_cfg)) + .collect::, _>>()?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + linear_values + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner.clone(), + )); + + for source in booleanity_sources { + let values = (0..SHA_ROW_COUNT) + .map(|row| booleanity_source_value_at_row(trace, row, source, field_cfg)) + .collect::, _>>()?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + values.iter().map(|value| value.inner().clone()).collect(), + zero_inner.clone(), + )); + } + + let rho_powers = powers( + rho.clone(), + F::one_with_cfg(field_cfg), + booleanity_sources.len(), + ); + let xi = xi.clone(); + let zero_for_comb = F::zero_with_cfg(field_cfg); + let one_for_comb = F::one_with_cfg(field_cfg); + Ok(MultiDegreeSumcheckGroup::new( + 3, + mles, + Box::new(move |values: &[F]| { + let row_weight = values[0].clone(); + let linear = values[1].clone(); + let mut bool_sum = zero_for_comb.clone(); + for (d, rho_power) in values[2..].iter().zip(rho_powers.iter()) { + let term = d.clone() * (d.clone() - one_for_comb.clone()); + bool_sum += rho_power.clone() * term; + } + row_weight * (linear + xi.clone() * bool_sum) + }), + )) +} + +/// Claimed sum for the expression-backed folded row check. +#[allow(clippy::too_many_arguments)] +pub fn expression_folded_row_sum( + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result +where + F: InnerTransparentField + DelayedFieldProductSum, + F::Inner: Zero, +{ + let values = folded_row_integrand_values( + trace, + public, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + )?; + folded_row_integrand_sum(&values, field_cfg) +} + +pub fn sha_word_bits_at_point( + trace: &ProjectedShaTrace, + col: ShaWordCol, + shift: usize, + point: &[F], + field_cfg: &F::Config, +) -> Result<[F; SHA_WORD_BITS], ShaProjectionError> +where + F: PrimeField, +{ + if point.len() != SHA_ROW_VARS { + return Err(ShaProjectionError::RowPointLength { got: point.len() }); + } + validate_trace(trace)?; + let row_weights = build_eq_x_r_vec(point, field_cfg)?; + let mut bits: [F; SHA_WORD_BITS] = std::array::from_fn(|_| F::zero_with_cfg(field_cfg)); + for (row, row_weight) in row_weights.iter().enumerate() { + for (bit, out) in bits.iter_mut().enumerate() { + *out += row_weight.clone() + * bit_at_shifted_or_zero(trace, col, row, shift, bit, field_cfg)?; + } + } + Ok(bits) +} + +pub fn sha_scalarized_word_at_point( + trace: &ProjectedShaTrace, + col: ShaWordCol, + shift: usize, + point: &[F], + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + if point.len() != SHA_ROW_VARS { + return Err(ShaProjectionError::RowPointLength { got: point.len() }); + } + validate_trace(trace)?; + let row_weights = build_eq_x_r_vec(point, field_cfg)?; + let mut value = F::zero_with_cfg(field_cfg); + for (row, row_weight) in row_weights.iter().enumerate() { + value += row_weight.clone() + * scalarized_word_at_shifted_or_zero(trace, col, row, shift, field_cfg)?; + } + Ok(value) +} + +pub fn sha_int_at_point( + trace: &ProjectedShaTrace, + col: ShaIntCol, + point: &[F], + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + if point.len() != SHA_ROW_VARS { + return Err(ShaProjectionError::RowPointLength { got: point.len() }); + } + validate_trace(trace)?; + let row_weights = build_eq_x_r_vec(point, field_cfg)?; + let mut value = F::zero_with_cfg(field_cfg); + for (row, row_weight) in row_weights.iter().enumerate() { + value += row_weight.clone() * int_scalar(trace, col, row, field_cfg)?; + } + Ok(value) +} + +pub fn sha_public_at_point( + public: &ProjectedShaPublic, + col: ShaPublicCol, + shift: usize, + point: &[F], + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + if point.len() != SHA_ROW_VARS { + return Err(ShaProjectionError::RowPointLength { got: point.len() }); + } + validate_public(public)?; + let row_weights = build_eq_x_r_vec(point, field_cfg)?; + let mut value = F::zero_with_cfg(field_cfg); + for (row, row_weight) in row_weights.iter().enumerate() { + let shifted = row.checked_add(shift).unwrap_or(SHA_ROW_COUNT); + value += row_weight.clone() * public_scalar(public, col, shifted, field_cfg)?; + } + Ok(value) +} + pub fn verify_folded_row_sumcheck_claim( claimed_sum: &F, t_prime: &F, diff --git a/protocol/src/pcs.rs b/protocol/src/pcs.rs index c23f1b14..4f0f87b9 100644 --- a/protocol/src/pcs.rs +++ b/protocol/src/pcs.rs @@ -4,7 +4,7 @@ use ark_ec::AffineRepr; use crypto_primitives::PrimeField; use zinc_poly::univariate::{binary::BinaryPoly, dense::DensePolynomial}; use zip_plus::pcs::{ - generic::{PCS, ZipPlusPCS}, + generic::{FoldablePCS, PCS, ZipPlusPCS}, hyrax::{BinaryLanes, DensePolyScalarLanes, HyraxPCS, IntScalarLane}, structs::ZipPlusCommitment, }; @@ -23,6 +23,21 @@ where type IntPCS: PCS; } +/// PCS bundle allowed by production SHA ProjectionFold. +/// +/// Production ProjectionFold folds instance commitments after SumFold, so all +/// committed witness domains must be homomorphic. `AllHyraxPCSTypes` satisfies +/// this today; Zip+/Merkle-based bundles intentionally do not. +pub trait ProductionShaPCS: ZincPCSTypes +where + Zt: ZincTypes, + F: PrimeField, + Self::BinaryPCS: FoldablePCS, D>, + Self::ArbitraryPCS: FoldablePCS, D>, + Self::IntPCS: FoldablePCS, +{ +} + #[derive(Clone, Debug)] pub struct AllZipPCSTypes; @@ -77,6 +92,18 @@ where type IntPCS = HyraxPCS; } +impl ProductionShaPCS for AllHyraxPCSTypes +where + Zt: ZincTypes, + F: PrimeField, + C: AffineRepr, + HyraxPCS: PCS, D> + FoldablePCS, D>, + HyraxPCS: + PCS, D> + FoldablePCS, D>, + HyraxPCS: PCS + FoldablePCS, +{ +} + #[derive(Clone, Debug)] pub struct PCSParams where diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 871015c3..66e36ee1 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -4,20 +4,35 @@ //! `Proof`: production ProjectionFold has a different transcript order and //! derives folded commitments only after SumFold fixes the instance-axis point. +use std::io::Cursor; + use crate::{ ZincTypes, - pcs::{PCSCommitments, PCSProverData, ZincPCSTypes}, + pcs::{ + AllHyraxPCSTypes, PCSCommitments, PCSParams, PCSProverData, PCSVerifierParams, + ProductionShaPCS, ZincPCSTypes, + }, }; +use ark_ec::AffineRepr; use crypto_primitives::{FromPrimitiveWithConfig, PrimeField}; +use num_traits::{ConstZero, Zero}; use thiserror::Error; use zinc_piop::{ - multipoint_eval::Proof as MultipointEvalProof, + multipoint_eval::{ + MultipointEval, MultipointEvalError, Proof as MultipointEvalProof, + Subclaim as MultipointSubclaim, + }, neutron_nova::SumFoldError, neutron_nova::{ - NUM_NONZERO_SHA_FAMILIES, SHA_ROW_VARS, ShaBooleanitySource, ShaProjectionError, - ShaResidualFamily, ShaSumFoldOutput, ShaWordCol, build_folded_row_sumcheck_group, - finalize_sha_sumfold, folded_row_integrand_sum, production_sha_nonzero_ideals, - verify_folded_row_sumcheck_claim, + NUM_NONZERO_SHA_FAMILIES, NUM_SHA_RESIDUAL_FAMILIES, ProjectedShaPublic, ProjectedShaTrace, + SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, ShaBooleanitySource, ShaIntCol, + ShaProjectionError, ShaPublicCol, ShaResidualFamily, ShaSumFoldOutput, ShaWordCol, + build_dense_sha_sumfold_group, build_expression_folded_row_sumcheck_group, + build_folded_row_sumcheck_group, build_fresh_sha_ideal_cache, evaluate_fresh_sha_targets, + finalize_sha_sumfold, fold_projected_sha_traces, folded_row_integrand_sum, + production_sha_booleanity_sources, production_sha_nonzero_families, + production_sha_nonzero_ideals, sha_int_at_point, sha_public_at_point, + sha_scalarized_word_at_point, sha_word_bits_at_point, verify_folded_row_sumcheck_claim, }, sumcheck::{ SumCheckError, @@ -25,17 +40,31 @@ use zinc_piop::{ }, }; use zinc_poly::{ + EvaluatablePolynomial, EvaluationError, + mle::DenseMultilinearExtension, univariate::{ binary::BinaryPoly, dense::DensePolynomial, dynamic::over_field::DynamicPolynomialF, }, utils::{ArithErrors, build_eq_x_r_vec, eq_eval}, }; +use zinc_transcript::Blake3Transcript; use zinc_transcript::traits::{ConstTranscribable, Transcribable, Transcript}; +use zinc_uair::ShiftSpec; use zinc_uair::ideal::IdealCheck; use zinc_utils::{ delayed_reduction::DelayedFieldProductSum, inner_transparent_field::InnerTransparentField, }; -use zip_plus::{ZipError, pcs::generic::FoldablePCS}; +use zip_plus::{ + ZipError, + pcs::{ + generic::{FoldablePCS, PCS}, + hyrax::{ + BinaryLanes, DensePolyScalarLanes, HyraxCommitment, HyraxCommitmentKey, + HyraxFieldBridge, HyraxPCS, HyraxProverData, HyraxVerifierKey, IntScalarLane, + }, + }, + pcs_transcript::{PcsProverTranscript, PcsVerifierTranscript}, +}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct ProductionShaProof { @@ -49,9 +78,31 @@ pub struct ProductionShaProof { pub pcs_opening_bytes: Vec, } +#[derive(Clone, Debug)] +pub struct ProductionShaWitnessPolys +where + Zt: ZincTypes, +{ + pub binary: Vec>>, + pub arbitrary: Vec>>, + pub int: Vec>, +} + +#[derive(Clone, Debug)] +pub struct ProductionShaProverInstance +where + Zt: ZincTypes, + F: PrimeField, +{ + pub trace: ProjectedShaTrace, + pub public: ProjectedShaPublic, + pub witness_polys: ProductionShaWitnessPolys, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ShaEndpointEvals { pub sources: Vec>, + pub int_sources: Vec>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -62,6 +113,38 @@ pub struct ShaSourceEndpointEval { pub bits: [F; 32], } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShaIntEndpointEval { + pub col: ShaIntCol, + pub scalar: F, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ShaMpSource { + WordBit { col: ShaWordCol, bit: usize }, + Int { col: ShaIntCol }, + Public { col: ShaPublicCol }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ShaMpShiftSource { + WordBit { + col: ShaWordCol, + bit: usize, + shift: usize, + }, + Public { + col: ShaPublicCol, + shift: usize, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShaMultipointLayout { + pub sources: Vec, + pub shifts: Vec, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct VirtualChMajEndpoint { pub ch1: [F; 32], @@ -79,6 +162,19 @@ pub struct ProductionShaChallenges { pub beta: Vec, } +const SHA256_ROUND_CONSTANTS: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +]; + +const PRODUCTION_SHA_FRESH_BATCH_DOMAIN: &[u8] = b"PF_CONCISE_SHA256_FRESH_BATCH_V1"; + #[derive(Clone, Debug, PartialEq, Eq)] pub struct FoldedRowSumcheckOutput { pub r_star: Vec, @@ -87,6 +183,8 @@ pub struct FoldedRowSumcheckOutput { #[derive(Debug, Error)] pub enum ProductionShaError { + #[error("production SHA requires at least two fresh instances, got {0}")] + InstanceCountTooSmall(usize), #[error("instance count must be a power of two, got {0}")] InstanceCountNotPowerOfTwo(usize), #[error("length mismatch for {label}: got {got}, expected {expected}")] @@ -95,6 +193,24 @@ pub enum ProductionShaError { got: usize, expected: usize, }, + #[error("non-canonical proof object: {0}")] + NonCanonicalProofObject(&'static str), + #[error("production SHA public selector column {col:?} is not boolean at row {row}")] + NonBooleanPublicSelector { col: ShaPublicCol, row: usize }, + #[error("production SHA public selector column {col:?} is all zero")] + EmptyPublicSelector { col: ShaPublicCol }, + #[error( + "production SHA public selector column {col:?} does not match the fixed row layout at row {row}" + )] + InvalidPublicSelector { col: ShaPublicCol, row: usize }, + #[error("production SHA public K column does not match SHA-256 constants at row {row}")] + InvalidRoundConstant { row: usize }, + #[error("production SHA requires {expected}-bit word polynomials, got D={got}")] + UnsupportedProductionShaWordDegree { got: usize, expected: usize }, + #[error("unsupported production SHA PCS shape: {0}")] + UnsupportedProductionShaPcsShape(&'static str), + #[error("PCS opening transcript has trailing bytes")] + TrailingPcsOpeningBytes, #[error("{label} expected exactly one sumcheck group, got {got}")] UnexpectedSumcheckGroupCount { label: &'static str, got: usize }, #[error("SumFold proof has degree {degree}, expected at most 3")] @@ -115,12 +231,51 @@ pub enum ProductionShaError { Pcs(#[from] ZipError), #[error("sumcheck error: {0}")] Sumcheck(#[from] SumCheckError), + #[error("multipoint evaluation error: {0}")] + Multipoint(#[from] MultipointEvalError), #[error("SumFold error: {0}")] SumFold(#[from] SumFoldError), #[error("SHA projection error: {0}")] ShaProjection(#[from] ShaProjectionError), #[error("equality polynomial error: {0}")] Eq(#[from] ArithErrors), + #[error("polynomial evaluation error: {0}")] + PolyEval(#[from] EvaluationError), +} + +pub trait ProductionShaOpeningPCS: + ProductionShaPCS + Sized +where + Zt: ZincTypes, + F: PrimeField, + Self::BinaryPCS: FoldablePCS, D>, + Self::ArbitraryPCS: FoldablePCS, D>, + Self::IntPCS: FoldablePCS, +{ + fn prove_folded_sha_opening( + pcs_params: &PCSParams, + folded_prover_data: &PCSProverData, + folded_commitments: &PCSCommitments, + folded_trace: &ProjectedShaTrace, + r_0: &[F], + folded_lifted_evals: &[DynamicPolynomialF], + field_cfg: &F::Config, + ) -> Result, ProductionShaError> + where + F::Inner: ConstTranscribable + Transcribable, + F::Modulus: ConstTranscribable + Transcribable; + + fn verify_folded_sha_opening( + pcs_params: &PCSVerifierParams, + folded_commitments: &PCSCommitments, + r_0: &[F], + folded_lifted_evals: &[DynamicPolynomialF], + pcs_opening_bytes: &[u8], + field_cfg: &F::Config, + ) -> Result<(), ProductionShaError> + where + F::Inner: ConstTranscribable + Transcribable, + F::Modulus: ConstTranscribable + Transcribable; } pub fn absorb_projected_sha_publics( @@ -146,6 +301,77 @@ pub fn absorb_projected_sha_publics( transcript.absorb_slice(b"production_sha_publics_end"); } +fn absorb_production_sha_statement_metadata(transcript: &mut impl Transcript) { + transcript.absorb_slice(PRODUCTION_SHA_FRESH_BATCH_DOMAIN); + transcript.absorb_slice(b"production_sha_statement_metadata_begin"); + + transcript.absorb_slice(b"row_layout"); + transcript.absorb_slice(&(SHA_ROW_VARS as u64).to_le_bytes()); + transcript.absorb_slice(&(SHA_ROW_COUNT as u64).to_le_bytes()); + for (start, end) in [(0u64, 3u64), (0, 15), (0, 47), (0, 63), (64, 67), (68, 71)] { + transcript.absorb_slice(&start.to_le_bytes()); + transcript.absorb_slice(&end.to_le_bytes()); + } + + transcript.absorb_slice(b"sha_word_column_order"); + for col in ShaWordCol::ALL { + transcript.absorb_slice(&(col.index() as u64).to_le_bytes()); + } + transcript.absorb_slice(b"sha_int_column_order"); + for col in ShaIntCol::ALL { + transcript.absorb_slice(&(col.index() as u64).to_le_bytes()); + } + transcript.absorb_slice(b"sha_public_column_order"); + for col in ShaPublicCol::ALL { + transcript.absorb_slice(&(col.index() as u64).to_le_bytes()); + } + + transcript.absorb_slice(b"sha_residual_family_order"); + for family in ShaResidualFamily::ALL { + transcript.absorb_slice(&(family.index() as u64).to_le_bytes()); + } + transcript.absorb_slice(b"sha_nonzero_ideal_ids"); + for family in production_sha_nonzero_families() { + transcript.absorb_slice(&(family.index() as u64).to_le_bytes()); + let ideal_id: &[u8] = match family { + ShaResidualFamily::R0BigSigmaA | ShaResidualFamily::R1BigSigmaE => b"X32_MINUS_1", + ShaResidualFamily::R4Schedule + | ShaResidualFamily::R5UpdateA + | ShaResidualFamily::R6UpdateE + | ShaResidualFamily::R9FeedForwardA + | ShaResidualFamily::R10FeedForwardE => b"X_MINUS_2", + _ => b"UNEXPECTED_NONZERO_IDEAL", + }; + transcript.absorb_slice(ideal_id); + } + + transcript.absorb_slice(b"sha256_k_constants"); + for constant in SHA256_ROUND_CONSTANTS { + transcript.absorb_slice(&(constant as u64).to_le_bytes()); + } + + transcript.absorb_slice(b"production_sha_statement_metadata_end"); +} + +pub fn absorb_production_sha_commitments( + transcript: &mut impl Transcript, + label: &'static [u8], + commitments: &[PCSCommitments], +) where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + transcript.absorb_slice(label); + transcript.absorb_slice(&(commitments.len() as u64).to_le_bytes()); + for (instance_idx, commitment) in commitments.iter().enumerate() { + transcript.absorb_slice(&(instance_idx as u64).to_le_bytes()); + P::BinaryPCS::absorb_commitment(transcript, &commitment.binary); + P::ArbitraryPCS::absorb_commitment(transcript, &commitment.arbitrary); + P::IntPCS::absorb_commitment(transcript, &commitment.int); + } +} + pub fn absorb_fresh_sha_ideal_polys( transcript: &mut impl Transcript, ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], @@ -169,6 +395,50 @@ pub fn absorb_fresh_sha_ideal_polys( transcript.absorb_slice(b"production_sha_fresh_ideals_end"); } +pub fn absorb_sha_endpoint_evals( + transcript: &mut impl Transcript, + endpoint_evals: &ShaEndpointEvals, +) where + F: PrimeField, + F::Inner: ConstTranscribable + Transcribable, + F::Modulus: Transcribable, +{ + let mut buf = vec![0; F::Inner::NUM_BYTES]; + transcript.absorb_slice(b"production_sha_endpoint_evals_begin"); + transcript.absorb_slice(&(endpoint_evals.sources.len() as u64).to_le_bytes()); + for source in &endpoint_evals.sources { + transcript.absorb_slice(&(source.col.index() as u64).to_le_bytes()); + transcript.absorb_slice(&(source.shift as u64).to_le_bytes()); + transcript.absorb_random_field(&source.scalarized, &mut buf); + transcript.absorb_random_field_slice(&source.bits, &mut buf); + } + transcript.absorb_slice(&(endpoint_evals.int_sources.len() as u64).to_le_bytes()); + for source in &endpoint_evals.int_sources { + transcript.absorb_slice(&(source.col.index() as u64).to_le_bytes()); + transcript.absorb_random_field(&source.scalar, &mut buf); + } + transcript.absorb_slice(b"production_sha_endpoint_evals_end"); +} + +pub fn absorb_folded_lifted_evals( + transcript: &mut impl Transcript, + lifted_evals: &[DynamicPolynomialF], +) where + F: PrimeField, + F::Inner: ConstTranscribable + Transcribable, + F::Modulus: Transcribable, +{ + let mut buf = vec![0; F::Inner::NUM_BYTES]; + transcript.absorb_slice(b"production_sha_folded_lifted_evals_begin"); + transcript.absorb_slice(&(lifted_evals.len() as u64).to_le_bytes()); + for (idx, lifted_eval) in lifted_evals.iter().enumerate() { + transcript.absorb_slice(&(idx as u64).to_le_bytes()); + transcript.absorb_slice(&(lifted_eval.coeffs.len() as u64).to_le_bytes()); + transcript.absorb_random_field_slice(&lifted_eval.coeffs, &mut buf); + } + transcript.absorb_slice(b"production_sha_folded_lifted_evals_end"); +} + pub fn sample_pre_ideal_challenge( transcript: &mut impl Transcript, field_cfg: &F::Config, @@ -211,6 +481,7 @@ pub fn check_fresh_sha_ideal_membership( where F: PrimeField, { + validate_fresh_sha_ideal_polys_canonical(ideal_polys)?; let ideals = production_sha_nonzero_ideals(field_cfg); for values in ideal_polys { for (ideal, value) in ideals.iter().zip(values.iter()) { @@ -225,102 +496,1250 @@ where Ok(()) } -pub fn prove_sha_sumfold_targets( - transcript: &mut impl Transcript, - fresh_targets: &[F], - beta: &[F], - prefix_vars: usize, - field_cfg: &F::Config, -) -> Result<(MultiDegreeSumcheckProof, ShaSumFoldOutput), ProductionShaError> +impl ProductionShaOpeningPCS for AllHyraxPCSTypes where - F: InnerTransparentField - + DelayedFieldProductSum - + FromPrimitiveWithConfig - + Send - + Sync - + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, - F::Modulus: ConstTranscribable, + Zt: ZincTypes, + F: HyraxFieldBridge, + C: AffineRepr, + HyraxPCS: PCS< + F, + BinaryPoly, + D, + CommitmentKey = HyraxCommitmentKey, + VerifierKey = HyraxVerifierKey, + Commitment = HyraxCommitment, + ProverData = HyraxProverData, + > + FoldablePCS, D>, + HyraxPCS: PCS< + F, + DensePolynomial, + D, + CommitmentKey = HyraxCommitmentKey, + VerifierKey = HyraxVerifierKey, + Commitment = HyraxCommitment, + ProverData = HyraxProverData, + > + FoldablePCS, D>, + HyraxPCS: PCS< + F, + Zt::Int, + D, + CommitmentKey = HyraxCommitmentKey, + VerifierKey = HyraxVerifierKey, + Commitment = HyraxCommitment, + ProverData = HyraxProverData, + > + FoldablePCS, { - let claims = zinc_piop::neutron_nova::LinearInstanceClaims::new(fresh_targets.to_vec())?; - let group = claims.build_hybrid_sumcheck_group(beta, prefix_vars, field_cfg)?; - let (proof, states) = - MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], claims.ell(), field_cfg); - let r_b = states - .first() - .ok_or(ProductionShaError::LengthMismatch { - label: "sumfold states", - got: 0, - expected: 1, - })? - .randomness - .clone(); - let c_sf = sumfold_expected_eval(beta, fresh_targets, &r_b, field_cfg)?; - let output = finalize_sha_sumfold(beta, r_b, c_sf, fresh_targets.len(), field_cfg)?; - Ok((proof, output)) + fn prove_folded_sha_opening( + pcs_params: &PCSParams, + folded_prover_data: &PCSProverData, + folded_commitments: &PCSCommitments, + folded_trace: &ProjectedShaTrace, + r_0: &[F], + folded_lifted_evals: &[DynamicPolynomialF], + field_cfg: &F::Config, + ) -> Result, ProductionShaError> + where + F::Inner: ConstTranscribable + Transcribable, + F::Modulus: ConstTranscribable + Transcribable, + { + ensure_production_sha_word_degree::()?; + validate_production_sha_batch_sizes::( + , D>>::batch_size(&folded_commitments.binary), + , D>>::batch_size( + &folded_commitments.arbitrary, + ), + >::batch_size(&folded_commitments.int), + )?; + let (binary_lifted, int_lifted) = split_folded_sha_pcs_lifted_evals(folded_lifted_evals)?; + + let mut transcript = PcsProverTranscript { + fs_transcript: Blake3Transcript::default(), + stream: Cursor::default(), + }; + let mut transcription_buf = vec![0u8; F::Inner::NUM_BYTES]; + + as PCS, D>>::absorb_commitment( + &mut transcript.fs_transcript, + &folded_commitments.binary, + ); + absorb_pcs_lifted_evals( + &mut transcript.fs_transcript, + binary_lifted, + &mut transcription_buf, + ); + let binary_scalar_lanes = folded_sha_binary_scalar_lanes::(folded_trace); + HyraxPCS::::prove_open_scalar_lanes::( + &mut transcript, + &pcs_params.binary, + &binary_scalar_lanes, + r_0, + &folded_prover_data.binary, + field_cfg, + )?; + + as PCS>::absorb_commitment( + &mut transcript.fs_transcript, + &folded_commitments.int, + ); + absorb_pcs_lifted_evals( + &mut transcript.fs_transcript, + int_lifted, + &mut transcription_buf, + ); + let int_scalar_lanes = folded_sha_int_scalar_lanes::(folded_trace); + HyraxPCS::::prove_open_scalar_lanes::( + &mut transcript, + &pcs_params.int, + &int_scalar_lanes, + r_0, + &folded_prover_data.int, + field_cfg, + )?; + + Ok(transcript.stream.into_inner()) + } + + fn verify_folded_sha_opening( + pcs_params: &PCSVerifierParams, + folded_commitments: &PCSCommitments, + r_0: &[F], + folded_lifted_evals: &[DynamicPolynomialF], + pcs_opening_bytes: &[u8], + field_cfg: &F::Config, + ) -> Result<(), ProductionShaError> + where + F::Inner: ConstTranscribable + Transcribable, + F::Modulus: ConstTranscribable + Transcribable, + { + ensure_production_sha_word_degree::()?; + validate_production_sha_batch_sizes::( + , D>>::batch_size(&folded_commitments.binary), + , D>>::batch_size( + &folded_commitments.arbitrary, + ), + >::batch_size(&folded_commitments.int), + )?; + let (binary_lifted, int_lifted) = split_folded_sha_pcs_lifted_evals(folded_lifted_evals)?; + + let mut transcript = PcsVerifierTranscript { + fs_transcript: Blake3Transcript::default(), + stream: Cursor::new(pcs_opening_bytes.to_vec()), + }; + let mut transcription_buf = vec![0u8; F::Inner::NUM_BYTES]; + + as PCS, D>>::absorb_commitment( + &mut transcript.fs_transcript, + &folded_commitments.binary, + ); + absorb_pcs_lifted_evals( + &mut transcript.fs_transcript, + binary_lifted, + &mut transcription_buf, + ); + as PCS, D>>::verify_open::( + &mut transcript, + &pcs_params.binary, + &folded_commitments.binary, + r_0, + binary_lifted, + field_cfg, + )?; + + as PCS>::absorb_commitment( + &mut transcript.fs_transcript, + &folded_commitments.int, + ); + absorb_pcs_lifted_evals( + &mut transcript.fs_transcript, + int_lifted, + &mut transcription_buf, + ); + as PCS>::verify_open::( + &mut transcript, + &pcs_params.int, + &folded_commitments.int, + r_0, + int_lifted, + field_cfg, + )?; + + if transcript.stream.position() != pcs_opening_bytes.len() as u64 { + return Err(ProductionShaError::TrailingPcsOpeningBytes); + } + Ok(()) + } } -pub fn verify_sha_sumfold_targets( +fn validate_fresh_sha_ideal_polys_canonical( + ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], +) -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + for instance in ideal_polys { + for (slot, poly) in instance.iter().enumerate() { + if poly.coeffs.last().is_some_and(F::is_zero) { + return Err(ProductionShaError::NonCanonicalProofObject( + "fresh ideal polynomial has trailing zero coefficients", + )); + } + let family = production_sha_nonzero_families()[slot]; + let max_degree = match family { + ShaResidualFamily::R0BigSigmaA | ShaResidualFamily::R1BigSigmaE => 61, + ShaResidualFamily::R4Schedule + | ShaResidualFamily::R5UpdateA + | ShaResidualFamily::R6UpdateE + | ShaResidualFamily::R9FeedForwardA + | ShaResidualFamily::R10FeedForwardE => 31, + _ => { + return Err(ProductionShaError::NonCanonicalProofObject( + "unexpected nonzero SHA ideal family", + )); + } + }; + if poly.coeffs.len() > max_degree + 1 { + return Err(ProductionShaError::NonCanonicalProofObject( + "fresh ideal polynomial exceeds production degree cap", + )); + } + } + } + Ok(()) +} + +fn ensure_production_sha_word_degree() -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + if D != SHA_WORD_BITS { + return Err(ProductionShaError::UnsupportedProductionShaWordDegree { + got: D, + expected: SHA_WORD_BITS, + }); + } + Ok(()) +} + +fn validate_production_sha_batch_sizes( + binary: usize, + arbitrary: usize, + int: usize, +) -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + if binary != ShaWordCol::COUNT { + return Err(ProductionShaError::UnsupportedProductionShaPcsShape( + "production SHA expects one binary commitment batch per SHA word column", + )); + } + if arbitrary != 0 { + return Err(ProductionShaError::UnsupportedProductionShaPcsShape( + "production SHA expects no arbitrary witness columns", + )); + } + if int != ShaIntCol::COUNT { + return Err(ProductionShaError::UnsupportedProductionShaPcsShape( + "production SHA expects one int commitment batch per SHA int column", + )); + } + Ok(()) +} + +pub fn commit_production_sha_instance( + pcs_params: &PCSParams, + witness_polys: &ProductionShaWitnessPolys, +) -> Result<(PCSProverData, PCSCommitments), ProductionShaError> +where + Zt: ZincTypes, + F: PrimeField, + P: ProductionShaPCS, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + ensure_production_sha_word_degree::()?; + validate_production_sha_batch_sizes::( + witness_polys.binary.len(), + witness_polys.arbitrary.len(), + witness_polys.int.len(), + )?; + let (binary_data, binary_commitment) = + P::BinaryPCS::commit(&pcs_params.binary, &witness_polys.binary)?; + let (arbitrary_data, arbitrary_commitment) = + P::ArbitraryPCS::commit(&pcs_params.arbitrary, &witness_polys.arbitrary)?; + let (int_data, int_commitment) = P::IntPCS::commit(&pcs_params.int, &witness_polys.int)?; + Ok(( + PCSProverData { + binary: binary_data, + arbitrary: arbitrary_data, + int: int_data, + }, + PCSCommitments { + binary: binary_commitment, + arbitrary: arbitrary_commitment, + int: int_commitment, + }, + )) +} + +#[allow(clippy::too_many_arguments)] +pub fn prove_production_sha_core( transcript: &mut impl Transcript, - proof: &MultiDegreeSumcheckProof, - fresh_targets: &[F], - beta: &[F], + pcs_params: &PCSParams, + instances: &[ProductionShaProverInstance], field_cfg: &F::Config, -) -> Result, ProductionShaError> +) -> Result>, ProductionShaError> where + Zt: ZincTypes, F: InnerTransparentField + DelayedFieldProductSum + FromPrimitiveWithConfig + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, - F::Modulus: ConstTranscribable, + F::Inner: ConstTranscribable + Transcribable + num_traits::Zero + Default + Send + Sync, + F::Modulus: ConstTranscribable + Transcribable, + P: ProductionShaOpeningPCS, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, { - require_single_sumcheck_group(proof, "SHA SumFold")?; - for °ree in proof.degrees() { - if degree > 3 { - return Err(ProductionShaError::SumFoldDegreeTooHigh { degree }); - } + ensure_production_sha_word_degree::()?; + if instances.len() < 2 { + return Err(ProductionShaError::InstanceCountTooSmall(instances.len())); } - let claims = zinc_piop::neutron_nova::LinearInstanceClaims::new(fresh_targets.to_vec())?; - let subclaims = - MultiDegreeSumcheck::verify_as_subprotocol(transcript, claims.ell(), proof, field_cfg)?; - let r_b = subclaims.point().to_vec(); - let c_sf = subclaims.expected_evaluations()[0].clone(); - if c_sf != sumfold_expected_eval(beta, fresh_targets, &r_b, field_cfg)? { - return Err(ProductionShaError::SumFoldTerminalMismatch); + if !instances.len().is_power_of_two() { + return Err(ProductionShaError::InstanceCountNotPowerOfTwo( + instances.len(), + )); } - Ok(finalize_sha_sumfold( - beta, - r_b, - c_sf, - fresh_targets.len(), + let booleanity_sources = production_sha_booleanity_sources(); + absorb_production_sha_statement_metadata(transcript); + + let mut prover_data = Vec::with_capacity(instances.len()); + let mut instance_commitments = Vec::with_capacity(instances.len()); + for instance in instances { + let (data, commitment) = + commit_production_sha_instance::(pcs_params, &instance.witness_polys)?; + prover_data.push(data); + instance_commitments.push(commitment); + } + + let traces = instances + .iter() + .map(|instance| instance.trace.clone()) + .collect::>(); + let publics = instances + .iter() + .map(|instance| instance.public.clone()) + .collect::>(); + validate_production_sha_publics(&publics, field_cfg)?; + + absorb_production_sha_commitments::( + transcript, + b"production_sha_fresh_commitments", + &instance_commitments, + ); + absorb_projected_sha_publics(transcript, &publics); + + let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); + let mut ideal_cache = build_fresh_sha_ideal_cache(&traces, &publics, r_ic.clone(), field_cfg)?; + absorb_fresh_sha_ideal_polys(transcript, &ideal_cache.ideal_polys); + check_fresh_sha_ideal_membership(&ideal_cache.ideal_polys, field_cfg)?; + + let (a, lambda, rho, xi, beta) = + sample_post_ideal_challenges(transcript, instances.len(), field_cfg)?; + evaluate_fresh_sha_targets(&mut ideal_cache, &a, &lambda, field_cfg)?; + let initial_claim = eq_weighted_sum(&beta, &ideal_cache.fresh_targets, field_cfg)?; + + let (sumfold_proof, sumfold_output) = prove_full_sha_sumfold( + transcript, + &traces, + &publics, + &initial_claim, + &beta, + &r_ic, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, field_cfg, - )?) + )?; + + let (folded, folded_public) = + fold_projected_sha_traces(&traces, &publics, &sumfold_output, field_cfg)?; + let folded_commitments = fold_pcs_commitments::( + &instance_commitments, + sumfold_output.theta(), + field_cfg, + )?; + let folded_prover_data = + fold_pcs_prover_data::(&prover_data, sumfold_output.theta(), field_cfg)?; + absorb_production_sha_commitments::( + transcript, + b"production_sha_derived_folded_commitments", + std::slice::from_ref(&folded_commitments), + ); + + let (folded_row_sumcheck, row_output) = prove_expression_folded_row_sumcheck_with_output( + transcript, + &folded.trace, + &folded_public, + &r_ic, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + sumfold_output.t_prime(), + field_cfg, + )?; + let endpoint_evals = + build_sha_endpoint_evals_from_trace(&folded.trace, &row_output.r_star, field_cfg)?; + absorb_sha_endpoint_evals(transcript, &endpoint_evals); + let terminal = reconstruct_folded_row_terminal_from_endpoints( + &endpoint_evals, + &folded_public, + &r_ic, + &row_output.r_star, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + field_cfg, + )?; + verify_folded_row_terminal_value(&row_output, &terminal)?; + + let (multipoint_eval, r_0) = prove_sha_endpoint_multipoint( + transcript, + &folded.trace, + &folded_public, + &endpoint_evals, + &row_output.r_star, + field_cfg, + )?; + let folded_lifted_evals = build_folded_sha_pcs_lifted_evals(&folded.trace, &r_0, field_cfg)?; + absorb_folded_lifted_evals(transcript, &folded_lifted_evals); + let pcs_opening_bytes = P::prove_folded_sha_opening( + pcs_params, + &folded_prover_data, + &folded_commitments, + &folded.trace, + &r_0, + &folded_lifted_evals, + field_cfg, + )?; + transcript.absorb_slice(b"production_sha_pcs_opening_bytes"); + transcript.absorb_slice(&(pcs_opening_bytes.len() as u64).to_le_bytes()); + transcript.absorb_slice(&pcs_opening_bytes); + + Ok(ProductionShaProof { + instance_commitments, + fresh_ideal_polys: ideal_cache.ideal_polys, + sumfold_proof, + folded_row_sumcheck, + endpoint_evals, + multipoint_eval, + folded_lifted_evals, + pcs_opening_bytes, + }) } -pub fn fold_pcs_commitments( - commitments: &[PCSCommitments], - theta: &[F], +pub fn verify_production_sha_core( + transcript: &mut impl Transcript, + pcs_params: &PCSVerifierParams, + proof: &ProductionShaProof>, + publics: &[ProjectedShaPublic], field_cfg: &F::Config, -) -> Result, ProductionShaError> +) -> Result<(), ProductionShaError> where Zt: ZincTypes, - F: PrimeField, - P: ZincPCSTypes, + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + Transcribable + num_traits::Zero + Default + Send + Sync, + F::Modulus: ConstTranscribable + Transcribable, + P: ProductionShaOpeningPCS, P::BinaryPCS: FoldablePCS, D>, P::ArbitraryPCS: FoldablePCS, D>, P::IntPCS: FoldablePCS, { - if commitments.len() != theta.len() { + ensure_production_sha_word_degree::()?; + if publics.len() < 2 { + return Err(ProductionShaError::InstanceCountTooSmall(publics.len())); + } + if !publics.len().is_power_of_two() { + return Err(ProductionShaError::InstanceCountNotPowerOfTwo( + publics.len(), + )); + } + if proof.instance_commitments.len() != publics.len() { return Err(ProductionShaError::LengthMismatch { - label: "commitments/theta", - got: commitments.len(), - expected: theta.len(), + label: "commitments/publics", + got: proof.instance_commitments.len(), + expected: publics.len(), }); } - let binary = commitments + if proof.fresh_ideal_polys.len() != publics.len() { + return Err(ProductionShaError::LengthMismatch { + label: "fresh ideals/publics", + got: proof.fresh_ideal_polys.len(), + expected: publics.len(), + }); + } + validate_production_sha_publics(publics, field_cfg)?; + let booleanity_sources = production_sha_booleanity_sources(); + absorb_production_sha_statement_metadata(transcript); + + absorb_production_sha_commitments::( + transcript, + b"production_sha_fresh_commitments", + &proof.instance_commitments, + ); + absorb_projected_sha_publics(transcript, publics); + + let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); + absorb_fresh_sha_ideal_polys(transcript, &proof.fresh_ideal_polys); + check_fresh_sha_ideal_membership(&proof.fresh_ideal_polys, field_cfg)?; + + let (a, lambda, rho, xi, beta) = + sample_post_ideal_challenges(transcript, publics.len(), field_cfg)?; + let fresh_targets = + evaluate_fresh_targets_from_ideal_polys(&proof.fresh_ideal_polys, &a, &lambda, field_cfg)?; + let initial_claim = eq_weighted_sum(&beta, &fresh_targets, field_cfg)?; + + let sumfold_output = verify_full_sha_sumfold( + transcript, + &proof.sumfold_proof, + &initial_claim, + &beta, + publics.len(), + field_cfg, + )?; + let folded_commitments = fold_pcs_commitments::( + &proof.instance_commitments, + sumfold_output.theta(), + field_cfg, + )?; + absorb_production_sha_commitments::( + transcript, + b"production_sha_derived_folded_commitments", + std::slice::from_ref(&folded_commitments), + ); + + let row_output = verify_folded_row_sumcheck( + transcript, + &proof.folded_row_sumcheck, + sumfold_output.t_prime(), + field_cfg, + )?; + absorb_sha_endpoint_evals(transcript, &proof.endpoint_evals); + let folded_public = fold_projected_sha_publics(publics, sumfold_output.theta(), field_cfg)?; + let terminal = reconstruct_folded_row_terminal_from_endpoints( + &proof.endpoint_evals, + &folded_public, + &r_ic, + &row_output.r_star, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + field_cfg, + )?; + verify_folded_row_terminal_value(&row_output, &terminal)?; + + let (subclaim, shift_specs) = verify_sha_endpoint_multipoint( + transcript, + &proof.multipoint_eval, + &proof.endpoint_evals, + &folded_public, + &row_output.r_star, + field_cfg, + )?; + let open_evals = multipoint_open_evals_from_pcs_lifted( + &proof.folded_lifted_evals, + &production_sha_multipoint_layout(), + &folded_public, + &subclaim.sumcheck_subclaim.point, + field_cfg, + )?; + verify_sha_endpoint_multipoint_open_evals(&subclaim, &open_evals, &shift_specs, field_cfg)?; + absorb_folded_lifted_evals(transcript, &proof.folded_lifted_evals); + P::verify_folded_sha_opening( + pcs_params, + &folded_commitments, + &subclaim.sumcheck_subclaim.point, + &proof.folded_lifted_evals, + &proof.pcs_opening_bytes, + field_cfg, + )?; + transcript.absorb_slice(b"production_sha_pcs_opening_bytes"); + transcript.absorb_slice(&(proof.pcs_opening_bytes.len() as u64).to_le_bytes()); + transcript.absorb_slice(&proof.pcs_opening_bytes); + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub fn prove_production_sha( + transcript: &mut impl Transcript, + pcs_params: &PCSParams, + instances: &[ProductionShaProverInstance], + field_cfg: &F::Config, +) -> Result>, ProductionShaError> +where + Zt: ZincTypes, + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + Transcribable + num_traits::Zero + Default + Send + Sync, + F::Modulus: ConstTranscribable + Transcribable, + P: ProductionShaOpeningPCS, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + prove_production_sha_core::(transcript, pcs_params, instances, field_cfg) +} + +pub fn verify_production_sha( + transcript: &mut impl Transcript, + pcs_params: &PCSVerifierParams, + proof: &ProductionShaProof>, + publics: &[ProjectedShaPublic], + field_cfg: &F::Config, +) -> Result<(), ProductionShaError> +where + Zt: ZincTypes, + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + Transcribable + num_traits::Zero + Default + Send + Sync, + F::Modulus: ConstTranscribable + Transcribable, + P: ProductionShaOpeningPCS, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + verify_production_sha_core::(transcript, pcs_params, proof, publics, field_cfg) +} + +fn evaluate_fresh_targets_from_ideal_polys( + ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], + a: &F, + lambda: &F, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + let lambda_powers = zinc_utils::powers( + lambda.clone(), + F::one_with_cfg(field_cfg), + NUM_SHA_RESIDUAL_FAMILIES, + ); + ideal_polys + .iter() + .map(|instance| { + let mut target = F::zero_with_cfg(field_cfg); + for (slot, family) in production_sha_nonzero_families().iter().enumerate() { + target += + lambda_powers[family.index()].clone() * instance[slot].evaluate_at_point(a)?; + } + Ok(target) + }) + .collect() +} + +fn eq_weighted_sum( + point: &[F], + values: &[F], + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + let expected = 1usize + .checked_shl(u32::try_from(point.len()).map_err(|_| { + ProductionShaError::LengthMismatch { + label: "eq point", + got: point.len(), + expected: usize::BITS as usize, + } + })?) + .ok_or(ProductionShaError::LengthMismatch { + label: "eq point", + got: point.len(), + expected: usize::BITS as usize, + })?; + if values.len() != expected { + return Err(ProductionShaError::LengthMismatch { + label: "eq-weighted values", + got: values.len(), + expected, + }); + } + let weights = build_eq_x_r_vec(point, field_cfg)?; + Ok(weights + .iter() + .zip(values.iter()) + .fold(F::zero_with_cfg(field_cfg), |acc, (weight, value)| { + acc + weight.clone() * value + })) +} + +fn fold_projected_sha_publics( + publics: &[ProjectedShaPublic], + theta: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + if publics.len() != theta.len() { + return Err(ProductionShaError::LengthMismatch { + label: "publics/theta", + got: publics.len(), + expected: theta.len(), + }); + } + let first = publics.first().ok_or(ProductionShaError::LengthMismatch { + label: "publics", + got: 0, + expected: 1, + })?; + let col_count = first.columns.columns.len(); + let row_count = first + .columns + .columns + .first() + .map(|col| col.len()) + .unwrap_or(0); + let mut columns = vec![vec![F::zero_with_cfg(field_cfg); row_count]; col_count]; + for (public, weight) in publics.iter().zip(theta.iter()) { + if public.columns.columns.len() != col_count { + return Err(ProductionShaError::LengthMismatch { + label: "public column count", + got: public.columns.columns.len(), + expected: col_count, + }); + } + for (col_idx, col) in public.columns.columns.iter().enumerate() { + if col.len() != row_count { + return Err(ProductionShaError::LengthMismatch { + label: "public row count", + got: col.len(), + expected: row_count, + }); + } + for (out, value) in columns[col_idx].iter_mut().zip(col.iter()) { + *out += weight.clone() * value; + } + } + } + Ok(ProjectedShaPublic { + columns: zinc_piop::neutron_nova::ShaPublicColumns { columns }, + }) +} + +fn validate_production_sha_publics( + publics: &[ProjectedShaPublic], + field_cfg: &F::Config, +) -> Result<(), ProductionShaError> +where + F: PrimeField + FromPrimitiveWithConfig, +{ + for public in publics { + if public.columns.columns.len() != ShaPublicCol::COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "SHA public column count", + got: public.columns.columns.len(), + expected: ShaPublicCol::COUNT, + }); + } + for col in &public.columns.columns { + if col.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "SHA public row count", + got: col.len(), + expected: SHA_ROW_COUNT, + }); + } + } + for selector in [ + ShaPublicCol::SInit, + ShaPublicCol::SMsg, + ShaPublicCol::SSched, + ShaPublicCol::SUpd, + ShaPublicCol::SFf, + ShaPublicCol::SOut, + ] { + let col = &public.columns.columns[selector.index()]; + for (row, value) in col.iter().enumerate() { + let expected = production_sha_selector_expected(selector, row, field_cfg); + if value != &expected { + if *value != F::zero_with_cfg(field_cfg) && *value != F::one_with_cfg(field_cfg) + { + return Err(ProductionShaError::NonBooleanPublicSelector { + col: selector, + row, + }); + } + return Err(ProductionShaError::InvalidPublicSelector { col: selector, row }); + } + } + } + + let k_col = &public.columns.columns[ShaPublicCol::K.index()]; + for (row, value) in k_col.iter().enumerate() { + let expected = production_sha_k_expected(row, field_cfg); + if value != &expected { + return Err(ProductionShaError::InvalidRoundConstant { row }); + } + } + } + Ok(()) +} + +fn production_sha_selector_expected( + selector: ShaPublicCol, + row: usize, + field_cfg: &F::Config, +) -> F +where + F: PrimeField, +{ + let active = match selector { + ShaPublicCol::SInit => row < 4, + ShaPublicCol::SMsg => row < 16, + ShaPublicCol::SSched => row < 48, + ShaPublicCol::SUpd => row < 64, + ShaPublicCol::SFf => (64..68).contains(&row), + ShaPublicCol::SOut => (68..72).contains(&row), + _ => false, + }; + if active { + F::one_with_cfg(field_cfg) + } else { + F::zero_with_cfg(field_cfg) + } +} + +fn production_sha_k_expected(row: usize, field_cfg: &F::Config) -> F +where + F: PrimeField + FromPrimitiveWithConfig, +{ + if (3..67).contains(&row) { + F::from_with_cfg(SHA256_ROUND_CONSTANTS[row - 3] as u64, field_cfg) + } else { + F::zero_with_cfg(field_cfg) + } +} + +fn build_folded_sha_pcs_lifted_evals( + folded_trace: &ProjectedShaTrace, + r_0: &[F], + field_cfg: &F::Config, +) -> Result>, ProductionShaError> +where + F: PrimeField + DelayedFieldProductSum, +{ + let mut lifted = Vec::with_capacity(ShaWordCol::COUNT + ShaIntCol::COUNT); + for col in ShaWordCol::ALL { + let coeffs = sha_word_bits_at_point(folded_trace, col, 0, r_0, field_cfg)?.to_vec(); + lifted.push(DynamicPolynomialF::new_trimmed(coeffs)); + } + for col in ShaIntCol::ALL { + lifted.push(DynamicPolynomialF::new_trimmed([sha_int_at_point( + folded_trace, + col, + r_0, + field_cfg, + )?])); + } + Ok(lifted) +} + +fn split_folded_sha_pcs_lifted_evals( + lifted_evals: &[DynamicPolynomialF], +) -> Result<(&[DynamicPolynomialF], &[DynamicPolynomialF]), ProductionShaError> +where + F: PrimeField, +{ + let expected = ShaWordCol::COUNT + ShaIntCol::COUNT; + if lifted_evals.len() != expected { + return Err(ProductionShaError::LengthMismatch { + label: "folded SHA PCS lifted evals", + got: lifted_evals.len(), + expected, + }); + } + validate_folded_sha_pcs_lifted_evals_canonical(lifted_evals)?; + Ok(lifted_evals.split_at(ShaWordCol::COUNT)) +} + +fn validate_folded_sha_pcs_lifted_evals_canonical( + lifted_evals: &[DynamicPolynomialF], +) -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + for (idx, lifted_eval) in lifted_evals.iter().enumerate() { + let max_len = if idx < ShaWordCol::COUNT { + SHA_WORD_BITS + } else { + 1 + }; + if lifted_eval.coeffs.len() > max_len { + return Err(ProductionShaError::NonCanonicalProofObject( + "folded SHA lifted eval has too many coefficients", + )); + } + if lifted_eval.coeffs.last().is_some_and(F::is_zero) { + return Err(ProductionShaError::NonCanonicalProofObject( + "folded SHA lifted eval has trailing zero coefficients", + )); + } + } + Ok(()) +} + +fn folded_sha_binary_scalar_lanes( + folded_trace: &ProjectedShaTrace, +) -> Vec>> +where + C: AffineRepr, + F: HyraxFieldBridge, +{ + ShaWordCol::ALL + .iter() + .map(|col| { + (0..32) + .map(|bit| { + (0..SHA_ROW_COUNT) + .map(|row| { + F::field_to_scalar( + &folded_trace.bit_slices.columns[col.index()][row][bit], + ) + }) + .collect::>() + }) + .collect::>() + }) + .collect() +} + +fn folded_sha_int_scalar_lanes( + folded_trace: &ProjectedShaTrace, +) -> Vec>> +where + C: AffineRepr, + F: HyraxFieldBridge, +{ + ShaIntCol::ALL + .iter() + .map(|col| { + vec![ + (0..SHA_ROW_COUNT) + .map(|row| { + F::field_to_scalar(&folded_trace.int_columns.columns[col.index()][row]) + }) + .collect::>(), + ] + }) + .collect() +} + +fn absorb_pcs_lifted_evals( + transcript: &mut impl Transcript, + lifted_evals: &[DynamicPolynomialF], + transcription_buf: &mut Vec, +) where + F: PrimeField, + F::Inner: ConstTranscribable + Transcribable, + F::Modulus: Transcribable, +{ + for lifted_eval in lifted_evals { + transcript.absorb_random_field_slice(&lifted_eval.coeffs, transcription_buf); + } +} + +fn multipoint_open_evals_from_pcs_lifted( + lifted_evals: &[DynamicPolynomialF], + layout: &ShaMultipointLayout, + folded_public: &ProjectedShaPublic, + r_0: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + split_folded_sha_pcs_lifted_evals(lifted_evals)?; + layout + .sources + .iter() + .map(|source| match *source { + ShaMpSource::Public { col } => { + sha_public_at_point(folded_public, col, 0, r_0, field_cfg) + .map_err(ProductionShaError::from) + } + ShaMpSource::WordBit { col, bit } => Ok(lifted_evals[col.index()] + .coeffs + .get(bit) + .cloned() + .unwrap_or_else(|| F::zero_with_cfg(field_cfg))), + ShaMpSource::Int { col } => Ok(lifted_evals[ShaWordCol::COUNT + col.index()] + .coeffs + .first() + .cloned() + .unwrap_or_else(|| F::zero_with_cfg(field_cfg))), + }) + .collect() +} + +pub fn prove_sha_sumfold_targets( + transcript: &mut impl Transcript, + fresh_targets: &[F], + beta: &[F], + prefix_vars: usize, + field_cfg: &F::Config, +) -> Result<(MultiDegreeSumcheckProof, ShaSumFoldOutput), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + let claims = zinc_piop::neutron_nova::LinearInstanceClaims::new(fresh_targets.to_vec())?; + let group = claims.build_hybrid_sumcheck_group(beta, prefix_vars, field_cfg)?; + let (proof, states) = + MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], claims.ell(), field_cfg); + let r_b = states + .first() + .ok_or(ProductionShaError::LengthMismatch { + label: "sumfold states", + got: 0, + expected: 1, + })? + .randomness + .clone(); + let c_sf = sumfold_expected_eval(beta, fresh_targets, &r_b, field_cfg)?; + let output = finalize_sha_sumfold(beta, r_b, c_sf, fresh_targets.len(), field_cfg)?; + Ok((proof, output)) +} + +pub fn verify_sha_sumfold_targets( + transcript: &mut impl Transcript, + proof: &MultiDegreeSumcheckProof, + fresh_targets: &[F], + beta: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + require_single_sumcheck_group(proof, "SHA SumFold")?; + for °ree in proof.degrees() { + if degree > 3 { + return Err(ProductionShaError::SumFoldDegreeTooHigh { degree }); + } + } + let claims = zinc_piop::neutron_nova::LinearInstanceClaims::new(fresh_targets.to_vec())?; + let subclaims = + MultiDegreeSumcheck::verify_as_subprotocol(transcript, claims.ell(), proof, field_cfg)?; + let r_b = subclaims.point().to_vec(); + let c_sf = subclaims.expected_evaluations()[0].clone(); + if c_sf != sumfold_expected_eval(beta, fresh_targets, &r_b, field_cfg)? { + return Err(ProductionShaError::SumFoldTerminalMismatch); + } + Ok(finalize_sha_sumfold( + beta, + r_b, + c_sf, + fresh_targets.len(), + field_cfg, + )?) +} + +#[allow(clippy::too_many_arguments)] +pub fn prove_full_sha_sumfold( + transcript: &mut impl Transcript, + traces: &[ProjectedShaTrace], + publics: &[ProjectedShaPublic], + initial_claim: &F, + beta: &[F], + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result<(MultiDegreeSumcheckProof, ShaSumFoldOutput), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + let group = build_dense_sha_sumfold_group( + traces, + publics, + beta, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + )?; + let ell = beta.len(); + let (proof, states) = + MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], ell, field_cfg); + require_single_sumcheck_group(&proof, "SHA SumFold")?; + if proof.claimed_sums()[0] != *initial_claim { + return Err(ProductionShaError::SumFoldTerminalMismatch); + } + + let r_b = states + .first() + .ok_or(ProductionShaError::LengthMismatch { + label: "sumfold states", + got: 0, + expected: 1, + })? + .randomness + .clone(); + let provisional = finalize_sha_sumfold( + beta, + r_b.clone(), + F::one_with_cfg(field_cfg), + traces.len(), + field_cfg, + )?; + let (folded, folded_public) = zinc_piop::neutron_nova::fold_projected_sha_traces( + traces, + publics, + &provisional, + field_cfg, + )?; + let t_prime = zinc_piop::neutron_nova::expression_folded_row_sum( + &folded.trace, + &folded_public, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + )?; + let d = eq_eval(beta, &r_b, F::one_with_cfg(field_cfg))?; + let c_sf = d * t_prime; + Ok(( + proof, + finalize_sha_sumfold(beta, r_b, c_sf, traces.len(), field_cfg)?, + )) +} + +pub fn verify_full_sha_sumfold( + transcript: &mut impl Transcript, + proof: &MultiDegreeSumcheckProof, + initial_claim: &F, + beta: &[F], + instance_count: usize, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + require_single_sumcheck_group(proof, "SHA SumFold")?; + for °ree in proof.degrees() { + if degree > 3 { + return Err(ProductionShaError::SumFoldDegreeTooHigh { degree }); + } + } + let Some(claimed_sum) = proof.claimed_sums().first() else { + return Err(ProductionShaError::LengthMismatch { + label: "SHA SumFold claimed sums", + got: 0, + expected: 1, + }); + }; + if claimed_sum != initial_claim { + return Err(ProductionShaError::SumFoldTerminalMismatch); + } + + let subclaims = + MultiDegreeSumcheck::verify_as_subprotocol(transcript, beta.len(), proof, field_cfg)?; + let r_b = subclaims.point().to_vec(); + let c_sf = subclaims.expected_evaluations()[0].clone(); + Ok(finalize_sha_sumfold( + beta, + r_b, + c_sf, + instance_count, + field_cfg, + )?) +} + +pub fn fold_pcs_commitments( + commitments: &[PCSCommitments], + theta: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + if commitments.len() != theta.len() { + return Err(ProductionShaError::LengthMismatch { + label: "commitments/theta", + got: commitments.len(), + expected: theta.len(), + }); + } + let binary = commitments .iter() .map(|commitment| commitment.binary.clone()) .collect::>(); @@ -378,12 +1797,367 @@ where }) } -pub fn prove_folded_row_sumcheck( +pub fn prove_folded_row_sumcheck( + transcript: &mut impl Transcript, + row_integrand_values: &[F], + t_prime: &F, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + let claimed = folded_row_integrand_sum(row_integrand_values, field_cfg)?; + verify_folded_row_sumcheck_claim(&claimed, t_prime)?; + let group = build_folded_row_sumcheck_group(row_integrand_values, field_cfg)?; + let (proof, _) = + MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], SHA_ROW_VARS, field_cfg); + Ok(proof) +} + +#[allow(clippy::too_many_arguments)] +pub fn prove_expression_folded_row_sumcheck( + transcript: &mut impl Transcript, + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + t_prime: &F, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + let claimed = zinc_piop::neutron_nova::expression_folded_row_sum( + trace, + public, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + )?; + verify_folded_row_sumcheck_claim(&claimed, t_prime)?; + let group = build_expression_folded_row_sumcheck_group( + trace, + public, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + )?; + let (proof, _) = + MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], SHA_ROW_VARS, field_cfg); + Ok(proof) +} + +#[allow(clippy::too_many_arguments)] +pub fn prove_expression_folded_row_sumcheck_with_output( + transcript: &mut impl Transcript, + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + t_prime: &F, + field_cfg: &F::Config, +) -> Result<(MultiDegreeSumcheckProof, FoldedRowSumcheckOutput), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + let claimed = zinc_piop::neutron_nova::expression_folded_row_sum( + trace, + public, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + )?; + verify_folded_row_sumcheck_claim(&claimed, t_prime)?; + let group = build_expression_folded_row_sumcheck_group( + trace, + public, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + )?; + let (proof, states) = + MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], SHA_ROW_VARS, field_cfg); + let r_star = states + .first() + .ok_or(ProductionShaError::LengthMismatch { + label: "folded row states", + got: 0, + expected: 1, + })? + .randomness + .clone(); + let endpoint_evals = build_sha_endpoint_evals_from_trace(trace, &r_star, field_cfg)?; + let terminal_value = reconstruct_folded_row_terminal_from_endpoints( + &endpoint_evals, + public, + r_ic, + &r_star, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + )?; + Ok(( + proof, + FoldedRowSumcheckOutput { + r_star, + terminal_value, + }, + )) +} + +pub fn verify_folded_row_sumcheck( + transcript: &mut impl Transcript, + proof: &MultiDegreeSumcheckProof, + t_prime: &F, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero, + F::Modulus: ConstTranscribable, +{ + require_single_sumcheck_group(proof, "folded row sumcheck")?; + for °ree in proof.degrees() { + if degree > 3 { + return Err(ProductionShaError::RowSumcheckDegreeTooHigh { degree }); + } + } + let Some(claimed_sum) = proof.claimed_sums().first() else { + return Err(ProductionShaError::LengthMismatch { + label: "folded row claimed sums", + got: 0, + expected: 1, + }); + }; + verify_folded_row_sumcheck_claim(claimed_sum, t_prime)?; + let subclaims = + MultiDegreeSumcheck::verify_as_subprotocol(transcript, SHA_ROW_VARS, proof, field_cfg)?; + Ok(FoldedRowSumcheckOutput { + r_star: subclaims.point().to_vec(), + terminal_value: subclaims.expected_evaluations()[0].clone(), + }) +} + +pub fn verify_folded_row_terminal_value( + output: &FoldedRowSumcheckOutput, + reconstructed_terminal: &F, +) -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + if &output.terminal_value != reconstructed_terminal { + return Err(ProductionShaError::RowSumcheckTerminalMismatch); + } + Ok(()) +} + +pub fn production_sha_endpoint_word_sources() -> Vec<(ShaWordCol, usize)> { + let mut sources = Vec::new(); + let mut push = |col, shift| { + if !sources.contains(&(col, shift)) { + sources.push((col, shift)); + } + }; + + for col in ShaWordCol::ALL { + push(col, 0); + } + for (col, shifts) in [ + (ShaWordCol::A, &[1usize, 2, 4][..]), + (ShaWordCol::E, &[1usize, 2, 4][..]), + (ShaWordCol::Sigma0, &[3usize][..]), + (ShaWordCol::Sigma1, &[3usize][..]), + (ShaWordCol::W, &[3usize, 9, 16][..]), + (ShaWordCol::SmallSigma0, &[1usize][..]), + (ShaWordCol::SmallSigma1, &[14usize][..]), + (ShaWordCol::Uef, &[2usize, 3][..]), + (ShaWordCol::UNegEg, &[2usize, 3][..]), + (ShaWordCol::Maj, &[2usize, 3][..]), + ] { + for &shift in shifts { + push(col, shift); + } + } + sources +} + +pub fn production_sha_endpoint_int_sources() -> Vec { + ShaIntCol::ALL.to_vec() +} + +pub fn production_sha_multipoint_layout() -> ShaMultipointLayout { + let mut sources = Vec::new(); + let mut push_source = |source| { + if !sources.contains(&source) { + sources.push(source); + } + }; + + for (col, _) in production_sha_endpoint_word_sources() { + for bit in 0..32 { + push_source(ShaMpSource::WordBit { col, bit }); + } + } + for col in production_sha_endpoint_int_sources() { + push_source(ShaMpSource::Int { col }); + } + for col in ShaPublicCol::ALL { + push_source(ShaMpSource::Public { col }); + } + + let mut shifts = Vec::new(); + let mut push_shift = |shift| { + if !shifts.contains(&shift) { + shifts.push(shift); + } + }; + for (col, shift) in production_sha_endpoint_word_sources() { + if shift == 0 { + continue; + } + for bit in 0..32 { + push_shift(ShaMpShiftSource::WordBit { col, bit, shift }); + } + } + push_shift(ShaMpShiftSource::Public { + col: ShaPublicCol::K, + shift: 3, + }); + + ShaMultipointLayout { sources, shifts } +} + +pub fn build_sha_endpoint_evals_from_trace( + trace: &ProjectedShaTrace, + r_star: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField + DelayedFieldProductSum, +{ + let mut sources = Vec::new(); + for (col, shift) in production_sha_endpoint_word_sources() { + sources.push(ShaSourceEndpointEval { + col, + shift, + scalarized: sha_scalarized_word_at_point(trace, col, shift, r_star, field_cfg)?, + bits: sha_word_bits_at_point(trace, col, shift, r_star, field_cfg)?, + }); + } + let mut int_sources = Vec::new(); + for col in production_sha_endpoint_int_sources() { + int_sources.push(ShaIntEndpointEval { + col, + scalar: sha_int_at_point(trace, col, r_star, field_cfg)?, + }); + } + Ok(ShaEndpointEvals { + sources, + int_sources, + }) +} + +pub fn validate_sha_endpoint_layout( + endpoint_evals: &ShaEndpointEvals, +) -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + let word_sources = production_sha_endpoint_word_sources(); + if endpoint_evals.sources.len() != word_sources.len() { + return Err(ProductionShaError::LengthMismatch { + label: "SHA endpoint word-source count", + got: endpoint_evals.sources.len(), + expected: word_sources.len(), + }); + } + for (got, expected) in endpoint_evals.sources.iter().zip(word_sources.iter()) { + if (got.col, got.shift) != *expected { + return Err(ProductionShaError::NonCanonicalProofObject( + "SHA endpoint word sources are not in canonical order", + )); + } + } + + let int_sources = production_sha_endpoint_int_sources(); + if endpoint_evals.int_sources.len() != int_sources.len() { + return Err(ProductionShaError::LengthMismatch { + label: "SHA endpoint int-source count", + got: endpoint_evals.int_sources.len(), + expected: int_sources.len(), + }); + } + for (got, expected) in endpoint_evals.int_sources.iter().zip(int_sources.iter()) { + if got.col != *expected { + return Err(ProductionShaError::NonCanonicalProofObject( + "SHA endpoint int sources are not in canonical order", + )); + } + } + Ok(()) +} + +pub fn prove_sha_endpoint_multipoint( transcript: &mut impl Transcript, - row_integrand_values: &[F], - t_prime: &F, + folded_trace: &ProjectedShaTrace, + folded_public: &ProjectedShaPublic, + endpoint_evals: &ShaEndpointEvals, + r_star: &[F], field_cfg: &F::Config, -) -> Result, ProductionShaError> +) -> Result<(MultipointEvalProof, Vec), ProductionShaError> where F: InnerTransparentField + DelayedFieldProductSum @@ -391,23 +2165,41 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, + F::Inner: ConstTranscribable + num_traits::Zero + Default + Send + Sync, F::Modulus: ConstTranscribable, { - let claimed = folded_row_integrand_sum(row_integrand_values, field_cfg)?; - verify_folded_row_sumcheck_claim(&claimed, t_prime)?; - let group = build_folded_row_sumcheck_group(row_integrand_values, field_cfg)?; - let (proof, _) = - MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], SHA_ROW_VARS, field_cfg); - Ok(proof) + validate_sha_endpoint_layout(endpoint_evals)?; + let layout = production_sha_multipoint_layout(); + let trace_mles = sha_multipoint_trace_mles(folded_trace, folded_public, &layout, field_cfg)?; + let up_evals = + sha_multipoint_up_evals(endpoint_evals, folded_public, r_star, &layout, field_cfg)?; + let (shift_specs, down_evals) = sha_multipoint_shift_specs_and_down_evals( + endpoint_evals, + folded_public, + r_star, + &layout, + field_cfg, + )?; + let (proof, state) = MultipointEval::prove_as_subprotocol( + transcript, + &trace_mles, + r_star, + &up_evals, + &down_evals, + &shift_specs, + field_cfg, + )?; + Ok((proof, state.eval_point)) } -pub fn verify_folded_row_sumcheck( +pub fn verify_sha_endpoint_multipoint( transcript: &mut impl Transcript, - proof: &MultiDegreeSumcheckProof, - t_prime: &F, + proof: &MultipointEvalProof, + endpoint_evals: &ShaEndpointEvals, + folded_public: &ProjectedShaPublic, + r_star: &[F], field_cfg: &F::Config, -) -> Result, ProductionShaError> +) -> Result<(MultipointSubclaim, Vec), ProductionShaError> where F: InnerTransparentField + DelayedFieldProductSum @@ -415,42 +2207,106 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, + F::Inner: ConstTranscribable + num_traits::Zero + Default + Send + Sync, F::Modulus: ConstTranscribable, { - require_single_sumcheck_group(proof, "folded row sumcheck")?; - for °ree in proof.degrees() { - if degree > 3 { - return Err(ProductionShaError::RowSumcheckDegreeTooHigh { degree }); - } - } - let Some(claimed_sum) = proof.claimed_sums().first() else { - return Err(ProductionShaError::LengthMismatch { - label: "folded row claimed sums", - got: 0, - expected: 1, - }); - }; - verify_folded_row_sumcheck_claim(claimed_sum, t_prime)?; - let subclaims = - MultiDegreeSumcheck::verify_as_subprotocol(transcript, SHA_ROW_VARS, proof, field_cfg)?; - Ok(FoldedRowSumcheckOutput { - r_star: subclaims.point().to_vec(), - terminal_value: subclaims.expected_evaluations()[0].clone(), - }) + validate_sha_endpoint_layout(endpoint_evals)?; + let layout = production_sha_multipoint_layout(); + let up_evals = + sha_multipoint_up_evals(endpoint_evals, folded_public, r_star, &layout, field_cfg)?; + let (shift_specs, down_evals) = sha_multipoint_shift_specs_and_down_evals( + endpoint_evals, + folded_public, + r_star, + &layout, + field_cfg, + )?; + let subclaim = MultipointEval::verify_as_subprotocol( + transcript, + proof.clone(), + r_star, + &up_evals, + &down_evals, + &shift_specs, + SHA_ROW_VARS, + field_cfg, + )?; + Ok((subclaim, shift_specs)) } -pub fn verify_folded_row_terminal_value( - output: &FoldedRowSumcheckOutput, - reconstructed_terminal: &F, +pub fn verify_sha_endpoint_multipoint_open_evals( + subclaim: &MultipointSubclaim, + open_evals: &[F], + shift_specs: &[ShiftSpec], + field_cfg: &F::Config, ) -> Result<(), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + num_traits::Zero + Default + Send + Sync, + F::Modulus: ConstTranscribable, +{ + Ok(MultipointEval::verify_subclaim( + subclaim, + open_evals, + shift_specs, + field_cfg, + )?) +} + +#[allow(clippy::too_many_arguments)] +pub fn reconstruct_folded_row_terminal_from_endpoints( + endpoint_evals: &ShaEndpointEvals, + folded_public: &ProjectedShaPublic, + r_ic: &[F; SHA_ROW_VARS], + r_star: &[F], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result> where F: PrimeField, { - if &output.terminal_value != reconstructed_terminal { - return Err(ProductionShaError::RowSumcheckTerminalMismatch); + if r_star.len() != SHA_ROW_VARS { + return Err(ProductionShaError::LengthMismatch { + label: "r_star", + got: r_star.len(), + expected: SHA_ROW_VARS, + }); } - Ok(()) + + validate_sha_endpoint_layout(endpoint_evals)?; + verify_endpoint_scalarization(endpoint_evals, a, field_cfg)?; + + let residuals = + residual_polys_from_endpoints(endpoint_evals, folded_public, r_star, field_cfg)?; + let lambda_powers = + zinc_utils::powers(lambda.clone(), F::one_with_cfg(field_cfg), residuals.len()); + let mut linear = F::zero_with_cfg(field_cfg); + for (residual, weight) in residuals.iter().zip(lambda_powers.iter()) { + linear += weight.clone() * residual.evaluate_at_point(a)?; + } + + let rho_powers = zinc_utils::powers( + rho.clone(), + F::one_with_cfg(field_cfg), + booleanity_sources.len(), + ); + let mut bool_sum = F::zero_with_cfg(field_cfg); + for (source, rho_power) in booleanity_sources.iter().zip(rho_powers.iter()) { + let d = booleanity_endpoint_value(endpoint_evals, source, field_cfg)?; + bool_sum += rho_power.clone() * d.clone() * (d - F::one_with_cfg(field_cfg)); + } + + let row_weight = eq_eval(r_ic, r_star, F::one_with_cfg(field_cfg))?; + Ok(row_weight * (linear + xi.clone() * bool_sum)) } pub fn verify_endpoint_scalarization( @@ -524,52 +2380,528 @@ pub fn booleanity_endpoint_value( where F: PrimeField, { - match source { - ShaBooleanitySource::WordBit { col, bit } => Ok(source_bits(endpoint_evals, *col, 0)? - .get(*bit) - .cloned() - .ok_or(ProductionShaError::LengthMismatch { - label: "endpoint bit", - got: *bit, - expected: 32, - })?), - ShaBooleanitySource::VirtualCh1 { bit } => Ok(reconstruct_virtual_ch_maj_endpoint( - endpoint_evals, - field_cfg, - )? - .ch1 - .get(*bit) - .cloned() - .ok_or(ProductionShaError::LengthMismatch { - label: "virtual Ch1 bit", - got: *bit, - expected: 32, - })?), - ShaBooleanitySource::VirtualCh2 { bit } => Ok(reconstruct_virtual_ch_maj_endpoint( - endpoint_evals, - field_cfg, - )? - .ch2 - .get(*bit) - .cloned() - .ok_or(ProductionShaError::LengthMismatch { - label: "virtual Ch2 bit", - got: *bit, - expected: 32, - })?), - ShaBooleanitySource::VirtualMaj { bit } => Ok(reconstruct_virtual_ch_maj_endpoint( - endpoint_evals, - field_cfg, - )? - .maj - .get(*bit) - .cloned() - .ok_or(ProductionShaError::LengthMismatch { - label: "virtual Maj bit", - got: *bit, - expected: 32, - })?), + match source { + ShaBooleanitySource::WordBit { col, bit } => Ok(source_bits(endpoint_evals, *col, 0)? + .get(*bit) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "endpoint bit", + got: *bit, + expected: 32, + })?), + ShaBooleanitySource::VirtualCh1 { bit } => Ok(reconstruct_virtual_ch_maj_endpoint( + endpoint_evals, + field_cfg, + )? + .ch1 + .get(*bit) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "virtual Ch1 bit", + got: *bit, + expected: 32, + })?), + ShaBooleanitySource::VirtualCh2 { bit } => Ok(reconstruct_virtual_ch_maj_endpoint( + endpoint_evals, + field_cfg, + )? + .ch2 + .get(*bit) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "virtual Ch2 bit", + got: *bit, + expected: 32, + })?), + ShaBooleanitySource::VirtualMaj { bit } => Ok(reconstruct_virtual_ch_maj_endpoint( + endpoint_evals, + field_cfg, + )? + .maj + .get(*bit) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "virtual Maj bit", + got: *bit, + expected: 32, + })?), + } +} + +fn sha_multipoint_trace_mles( + folded_trace: &ProjectedShaTrace, + folded_public: &ProjectedShaPublic, + layout: &ShaMultipointLayout, + field_cfg: &F::Config, +) -> Result>, ProductionShaError> +where + F: InnerTransparentField, +{ + let zero_inner = F::zero_with_cfg(field_cfg).inner().clone(); + layout + .sources + .iter() + .map(|source| { + let values = (0..SHA_ROW_COUNT) + .map(|row| { + sha_mp_source_row_value(folded_trace, folded_public, *source, row, field_cfg) + }) + .collect::, _>>()?; + Ok(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + values.iter().map(|value| value.inner().clone()).collect(), + zero_inner.clone(), + )) + }) + .collect() +} + +fn sha_multipoint_up_evals( + endpoint_evals: &ShaEndpointEvals, + folded_public: &ProjectedShaPublic, + r_star: &[F], + layout: &ShaMultipointLayout, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + layout + .sources + .iter() + .map(|source| { + sha_mp_source_endpoint_value(endpoint_evals, folded_public, r_star, *source, field_cfg) + }) + .collect() +} + +fn sha_multipoint_shift_specs_and_down_evals( + endpoint_evals: &ShaEndpointEvals, + folded_public: &ProjectedShaPublic, + r_star: &[F], + layout: &ShaMultipointLayout, + field_cfg: &F::Config, +) -> Result<(Vec, Vec), ProductionShaError> +where + F: PrimeField, +{ + let mut specs = Vec::with_capacity(layout.shifts.len()); + let mut evals = Vec::with_capacity(layout.shifts.len()); + for shift in &layout.shifts { + let (source, amount, value) = match *shift { + ShaMpShiftSource::WordBit { col, bit, shift } => ( + ShaMpSource::WordBit { col, bit }, + shift, + source_bits(endpoint_evals, col, shift)? + .get(bit) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "shifted word bit", + got: bit, + expected: 32, + })?, + ), + ShaMpShiftSource::Public { col, shift } => ( + ShaMpSource::Public { col }, + shift, + sha_public_at_point(folded_public, col, shift, r_star, field_cfg)?, + ), + }; + let source_idx = layout + .sources + .iter() + .position(|candidate| *candidate == source) + .ok_or(ProductionShaError::LengthMismatch { + label: "multipoint shift source", + got: 0, + expected: 1, + })?; + specs.push(ShiftSpec::new(source_idx, amount)); + evals.push(value); + } + Ok((specs, evals)) +} + +fn sha_mp_source_row_value( + folded_trace: &ProjectedShaTrace, + folded_public: &ProjectedShaPublic, + source: ShaMpSource, + row: usize, + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + match source { + ShaMpSource::WordBit { col, bit } => folded_trace + .bit_slices + .columns + .get(col.index()) + .and_then(|rows| rows.get(row)) + .and_then(|bits| bits.get(bit)) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "multipoint word bit source", + got: row, + expected: SHA_ROW_COUNT, + }), + ShaMpSource::Int { col } => folded_trace + .int_columns + .columns + .get(col.index()) + .and_then(|rows| rows.get(row)) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "multipoint int source", + got: row, + expected: SHA_ROW_COUNT, + }), + ShaMpSource::Public { col } => folded_public + .columns + .columns + .get(col.index()) + .and_then(|rows| rows.get(row)) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "multipoint public source", + got: row, + expected: SHA_ROW_COUNT, + }), + } + .map(|value| { + let _ = field_cfg; + value + }) +} + +fn sha_mp_source_endpoint_value( + endpoint_evals: &ShaEndpointEvals, + folded_public: &ProjectedShaPublic, + r_star: &[F], + source: ShaMpSource, + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + match source { + ShaMpSource::WordBit { col, bit } => source_bits(endpoint_evals, col, 0)? + .get(bit) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "endpoint word bit", + got: bit, + expected: 32, + }), + ShaMpSource::Int { col } => endpoint_evals + .int_sources + .iter() + .find(|source| source.col == col) + .map(|source| source.scalar.clone()) + .ok_or(ProductionShaError::LengthMismatch { + label: "endpoint int source", + got: endpoint_evals.int_sources.len(), + expected: ShaIntCol::COUNT, + }), + ShaMpSource::Public { col } => Ok(sha_public_at_point( + folded_public, + col, + 0, + r_star, + field_cfg, + )?), + } +} + +fn residual_polys_from_endpoints( + endpoint_evals: &ShaEndpointEvals, + folded_public: &ProjectedShaPublic, + r_star: &[F], + field_cfg: &F::Config, +) -> Result>, ProductionShaError> +where + F: PrimeField, +{ + let one = F::one_with_cfg(field_cfg); + let two = one.clone() + &one; + let rho_sig0 = sparse_endpoint_poly::(&[10, 19, 30], field_cfg); + let rho_sig1 = sparse_endpoint_poly::(&[7, 21, 26], field_cfg); + + let a = endpoint_word_poly(endpoint_evals, ShaWordCol::A, 0, field_cfg)?; + let e = endpoint_word_poly(endpoint_evals, ShaWordCol::E, 0, field_cfg)?; + let sigma0 = endpoint_word_poly(endpoint_evals, ShaWordCol::Sigma0, 0, field_cfg)?; + let sigma1 = endpoint_word_poly(endpoint_evals, ShaWordCol::Sigma1, 0, field_cfg)?; + let w = endpoint_word_poly(endpoint_evals, ShaWordCol::W, 0, field_cfg)?; + let small_sigma0 = endpoint_word_poly(endpoint_evals, ShaWordCol::SmallSigma0, 0, field_cfg)?; + let small_sigma1 = endpoint_word_poly(endpoint_evals, ShaWordCol::SmallSigma1, 0, field_cfg)?; + let ov_sigma0 = endpoint_word_poly(endpoint_evals, ShaWordCol::OvSigma0, 0, field_cfg)?; + let ov_sigma1 = endpoint_word_poly(endpoint_evals, ShaWordCol::OvSigma1, 0, field_cfg)?; + let ov_small_sigma0 = + endpoint_word_poly(endpoint_evals, ShaWordCol::OvSmallSigma0, 0, field_cfg)?; + let ov_small_sigma1 = + endpoint_word_poly(endpoint_evals, ShaWordCol::OvSmallSigma1, 0, field_cfg)?; + + let r0 = (&a * &rho_sig0) - &sigma0 - &scale_endpoint_poly(&ov_sigma0, &two); + let r1 = (&e * &rho_sig1) - &sigma1 - &scale_endpoint_poly(&ov_sigma1, &two); + let r2 = endpoint_word_poly(endpoint_evals, ShaWordCol::W, 0, field_cfg)?.rot_c(25) + + &endpoint_word_poly(endpoint_evals, ShaWordCol::W, 0, field_cfg)?.rot_c(14) + + &endpoint_word_poly(endpoint_evals, ShaWordCol::W, 0, field_cfg)?.shift_r_c(3) + - &small_sigma0 + - &scale_endpoint_poly(&ov_small_sigma0, &two); + let r3 = endpoint_word_poly(endpoint_evals, ShaWordCol::W, 0, field_cfg)?.rot_c(15) + + &endpoint_word_poly(endpoint_evals, ShaWordCol::W, 0, field_cfg)?.rot_c(13) + + &endpoint_word_poly(endpoint_evals, ShaWordCol::W, 0, field_cfg)?.shift_r_c(10) + - &small_sigma1 + - &scale_endpoint_poly(&ov_small_sigma1, &two); + + let mu_w = endpoint_mu_contribution(endpoint_evals, 0, 2, field_cfg)?; + let mu_a = endpoint_mu_contribution(endpoint_evals, 2, 5, field_cfg)?; + let mu_e = endpoint_mu_contribution(endpoint_evals, 5, 8, field_cfg)?; + let mu_ff_a = endpoint_mu_contribution(endpoint_evals, 8, 9, field_cfg)?; + let mu_ff_e = endpoint_mu_contribution(endpoint_evals, 9, 10, field_cfg)?; + + let r4 = endpoint_word_poly(endpoint_evals, ShaWordCol::W, 16, field_cfg)? + - &w + - &endpoint_word_poly(endpoint_evals, ShaWordCol::SmallSigma0, 1, field_cfg)? + - &endpoint_word_poly(endpoint_evals, ShaWordCol::W, 9, field_cfg)? + - &endpoint_word_poly(endpoint_evals, ShaWordCol::SmallSigma1, 14, field_cfg)? + + &mu_w + + &endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompSchedule, field_cfg)?; + + let r5 = endpoint_word_poly(endpoint_evals, ShaWordCol::A, 4, field_cfg)? + - &e + - &endpoint_word_poly(endpoint_evals, ShaWordCol::Sigma1, 3, field_cfg)? + - &endpoint_word_poly(endpoint_evals, ShaWordCol::Uef, 3, field_cfg)? + - &endpoint_word_poly(endpoint_evals, ShaWordCol::UNegEg, 3, field_cfg)? + - &endpoint_public_const_poly(folded_public, ShaPublicCol::K, 3, r_star, field_cfg)? + - &endpoint_word_poly(endpoint_evals, ShaWordCol::W, 3, field_cfg)? + - &endpoint_word_poly(endpoint_evals, ShaWordCol::Sigma0, 3, field_cfg)? + - &endpoint_word_poly(endpoint_evals, ShaWordCol::Maj, 3, field_cfg)? + + &mu_a + + &endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompUpdateA, field_cfg)?; + + let r6 = endpoint_word_poly(endpoint_evals, ShaWordCol::E, 4, field_cfg)? + - &a + - &e + - &endpoint_word_poly(endpoint_evals, ShaWordCol::Sigma1, 3, field_cfg)? + - &endpoint_word_poly(endpoint_evals, ShaWordCol::Uef, 3, field_cfg)? + - &endpoint_word_poly(endpoint_evals, ShaWordCol::UNegEg, 3, field_cfg)? + - &endpoint_public_const_poly(folded_public, ShaPublicCol::K, 3, r_star, field_cfg)? + - &endpoint_word_poly(endpoint_evals, ShaWordCol::W, 3, field_cfg)? + + &mu_e + + &endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompUpdateE, field_cfg)?; + + let s_init = sha_public_at_point(folded_public, ShaPublicCol::SInit, 0, r_star, field_cfg)?; + let s_msg = sha_public_at_point(folded_public, ShaPublicCol::SMsg, 0, r_star, field_cfg)?; + let s_sched = sha_public_at_point(folded_public, ShaPublicCol::SSched, 0, r_star, field_cfg)?; + let s_upd = sha_public_at_point(folded_public, ShaPublicCol::SUpd, 0, r_star, field_cfg)?; + let s_ff = sha_public_at_point(folded_public, ShaPublicCol::SFf, 0, r_star, field_cfg)?; + let s_out = sha_public_at_point(folded_public, ShaPublicCol::SOut, 0, r_star, field_cfg)?; + + let r7 = scale_endpoint_poly( + &(a.clone() + - &endpoint_public_const_poly( + folded_public, + ShaPublicCol::PAIn, + 0, + r_star, + field_cfg, + )?), + &s_init, + ) + &scale_endpoint_poly( + &(a.clone() + - &endpoint_public_const_poly( + folded_public, + ShaPublicCol::PAOut, + 0, + r_star, + field_cfg, + )?), + &s_out, + ); + let r8 = scale_endpoint_poly( + &(e.clone() + - &endpoint_public_const_poly( + folded_public, + ShaPublicCol::PEIn, + 0, + r_star, + field_cfg, + )?), + &s_init, + ) + &scale_endpoint_poly( + &(e.clone() + - &endpoint_public_const_poly( + folded_public, + ShaPublicCol::PEOut, + 0, + r_star, + field_cfg, + )?), + &s_out, + ); + + let r9 = endpoint_word_poly(endpoint_evals, ShaWordCol::A, 4, field_cfg)? + - &a + - &endpoint_public_const_poly(folded_public, ShaPublicCol::PAIn, 0, r_star, field_cfg)? + + &mu_ff_a + + &endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompFeedForwardA, field_cfg)?; + let r10 = endpoint_word_poly(endpoint_evals, ShaWordCol::E, 4, field_cfg)? + - &e + - &endpoint_public_const_poly(folded_public, ShaPublicCol::PEIn, 0, r_star, field_cfg)? + + &mu_ff_e + + &endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompFeedForwardE, field_cfg)?; + let r11 = scale_endpoint_poly( + &(w - &endpoint_public_const_poly( + folded_public, + ShaPublicCol::Message, + 0, + r_star, + field_cfg, + )?), + &s_msg, + ); + + let comp_schedule = + endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompSchedule, field_cfg)?; + let comp_update_a = endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompUpdateA, field_cfg)?; + let comp_update_e = endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompUpdateE, field_cfg)?; + let comp_ff_a = + endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompFeedForwardA, field_cfg)?; + let comp_ff_e = + endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompFeedForwardE, field_cfg)?; + + let r12 = scale_endpoint_poly(&comp_schedule, &s_sched); + let r13 = scale_endpoint_poly(&comp_update_a, &s_upd); + let r14 = scale_endpoint_poly(&comp_update_e, &s_upd); + let r15 = scale_endpoint_poly(&comp_ff_a, &s_ff); + let r16 = scale_endpoint_poly(&comp_ff_e, &s_ff); + let r17 = endpoint_word_poly(endpoint_evals, ShaWordCol::MuPacked, 0, field_cfg)?.shift_r_c(10); + + let mut residuals = vec![ + r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, + ]; + residuals.iter_mut().for_each(DynamicPolynomialF::trim); + Ok(residuals) +} + +fn endpoint_word_poly( + endpoint_evals: &ShaEndpointEvals, + col: ShaWordCol, + shift: usize, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + let mut coeffs = source_bits(endpoint_evals, col, shift)?.to_vec(); + coeffs.resize(32, F::zero_with_cfg(field_cfg)); + Ok(DynamicPolynomialF::new_trimmed(coeffs)) +} + +fn endpoint_int_const_poly( + endpoint_evals: &ShaEndpointEvals, + col: ShaIntCol, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + let value = endpoint_evals + .int_sources + .iter() + .find(|source| source.col == col) + .map(|source| source.scalar.clone()) + .ok_or(ProductionShaError::LengthMismatch { + label: "endpoint int source", + got: endpoint_evals.int_sources.len(), + expected: ShaIntCol::COUNT, + })?; + Ok(endpoint_const_poly(value, field_cfg)) +} + +fn endpoint_public_const_poly( + folded_public: &ProjectedShaPublic, + col: ShaPublicCol, + shift: usize, + r_star: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + Ok(endpoint_const_poly( + sha_public_at_point(folded_public, col, shift, r_star, field_cfg)?, + field_cfg, + )) +} + +fn endpoint_mu_contribution( + endpoint_evals: &ShaEndpointEvals, + low: usize, + high: usize, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + let packed = endpoint_word_poly(endpoint_evals, ShaWordCol::MuPacked, 0, field_cfg)? + .shift_r_c(low as u32); + let tail = endpoint_word_poly(endpoint_evals, ShaWordCol::MuPacked, 0, field_cfg)? + .shift_r_c(high as u32); + let low_coeff = endpoint_pow_two(32, field_cfg); + let high_coeff = endpoint_pow_two(32 + high - low, field_cfg); + Ok(scale_endpoint_poly(&packed, &low_coeff) - &scale_endpoint_poly(&tail, &high_coeff)) +} + +fn sparse_endpoint_poly(positions: &[usize], field_cfg: &F::Config) -> DynamicPolynomialF +where + F: PrimeField, +{ + let mut coeffs = vec![F::zero_with_cfg(field_cfg); 32]; + for &pos in positions { + coeffs[pos] = F::one_with_cfg(field_cfg); + } + DynamicPolynomialF::new_trimmed(coeffs) +} + +fn scale_endpoint_poly(poly: &DynamicPolynomialF, scalar: &F) -> DynamicPolynomialF +where + F: PrimeField, +{ + if poly.is_zero() || F::is_zero(scalar) { + return DynamicPolynomialF::ZERO; + } + DynamicPolynomialF::new_trimmed( + poly.coeffs + .iter() + .map(|coeff| coeff.clone() * scalar) + .collect::>(), + ) +} + +fn endpoint_const_poly(value: F, field_cfg: &F::Config) -> DynamicPolynomialF +where + F: PrimeField, +{ + if F::is_zero(&value) { + DynamicPolynomialF::ZERO + } else { + let _ = field_cfg; + DynamicPolynomialF::constant_poly(value) + } +} + +fn endpoint_pow_two(exp: usize, field_cfg: &F::Config) -> F +where + F: PrimeField, +{ + let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); + let mut out = F::one_with_cfg(field_cfg); + for _ in 0..exp { + out *= &two; } + out } fn source_bits( @@ -653,7 +2985,11 @@ mod tests { use crypto_primitives::{ FromWithConfig, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, }; - use zinc_piop::neutron_nova::SHA_WORD_BITS; + use zinc_piop::neutron_nova::{ + SHA_ROW_COUNT, SHA_WORD_BITS, ShaBitSliceColumns, ShaIntColumns, ShaPublicColumns, + expression_folded_row_sum, fold_projected_sha_traces, scalarize_trace_words, + }; + use zinc_poly::mle::MultilinearExtensionWithConfig; use zinc_transcript::{Blake3Transcript, traits::Transcript}; type F = MontyField<4>; @@ -666,6 +3002,80 @@ mod tests { F::from_with_cfg(value, &cfg()) } + fn zero_trace_with_scalar_challenge(a: &F) -> ProjectedShaTrace { + let field_cfg = cfg(); + let zero = F::zero_with_cfg(&field_cfg); + let bit_slices = ShaBitSliceColumns { + columns: vec![ + vec![vec![zero.clone(); SHA_WORD_BITS]; SHA_ROW_COUNT]; + ShaWordCol::COUNT + ], + }; + let scalarized_words = scalarize_trace_words(&bit_slices, a, &field_cfg).unwrap(); + ProjectedShaTrace { + rows: SHA_ROW_COUNT, + bit_slices, + scalarized_words, + int_columns: ShaIntColumns { + columns: vec![vec![zero.clone(); SHA_ROW_COUNT]; ShaIntCol::COUNT], + }, + public_columns: ShaPublicColumns { + columns: vec![vec![zero; SHA_ROW_COUNT]; ShaPublicCol::COUNT], + }, + } + } + + fn zero_public() -> ProjectedShaPublic { + let field_cfg = cfg(); + ProjectedShaPublic { + columns: ShaPublicColumns { + columns: vec![ + vec![F::zero_with_cfg(&field_cfg); SHA_ROW_COUNT]; + ShaPublicCol::COUNT + ], + }, + } + } + + fn fixed_layout_public() -> ProjectedShaPublic { + let field_cfg = cfg(); + let mut public = zero_public(); + for selector in [ + ShaPublicCol::SInit, + ShaPublicCol::SMsg, + ShaPublicCol::SSched, + ShaPublicCol::SUpd, + ShaPublicCol::SFf, + ShaPublicCol::SOut, + ] { + for row in 0..SHA_ROW_COUNT { + public.columns.columns[selector.index()][row] = + production_sha_selector_expected(selector, row, &field_cfg); + } + } + for row in 0..SHA_ROW_COUNT { + public.columns.columns[ShaPublicCol::K.index()][row] = + production_sha_k_expected(row, &field_cfg); + } + public + } + + fn sparse_r_ic() -> [F; SHA_ROW_VARS] { + std::array::from_fn(|idx| f(idx as u64 + 2)) + } + + fn rescalarize_endpoint_source(source: &mut ShaSourceEndpointEval, a: &F) { + let field_cfg = cfg(); + let powers = zinc_utils::powers(a.clone(), F::one_with_cfg(&field_cfg), 32); + source.scalarized = source + .bits + .iter() + .zip(powers.iter()) + .fold(F::zero_with_cfg(&field_cfg), |acc, (bit, power)| { + acc + bit.clone() * power + }); + } + fn endpoint_source(col: ShaWordCol, shift: usize, seed: u64) -> ShaSourceEndpointEval { let field_cfg = cfg(); let bits = std::array::from_fn(|idx| f(seed + idx as u64 + 1)); @@ -699,6 +3109,7 @@ mod tests { endpoint_source(ShaWordCol::Maj, 2, 100), endpoint_source(ShaWordCol::MajComp, 0, 110), ], + int_sources: Vec::new(), } } @@ -727,10 +3138,14 @@ mod tests { #[test] fn fresh_ideal_absorption_binds_polynomial_slot_structure() { let field_cfg = cfg(); - let mut packed = vec![std::array::from_fn(|_| DynamicPolynomialF::new(Vec::::new()))]; + let mut packed = vec![std::array::from_fn(|_| { + DynamicPolynomialF::new(Vec::::new()) + })]; packed[0][0] = DynamicPolynomialF::new(vec![f(1), f(2)]); - let mut split = vec![std::array::from_fn(|_| DynamicPolynomialF::new(Vec::::new()))]; + let mut split = vec![std::array::from_fn(|_| { + DynamicPolynomialF::new(Vec::::new()) + })]; split[0][0] = DynamicPolynomialF::new(vec![f(1)]); split[0][1] = DynamicPolynomialF::new(vec![f(2)]); @@ -747,6 +3162,40 @@ mod tests { assert_ne!(sample_a(&packed), sample_a(&split)); } + #[test] + fn production_public_validation_requires_fixed_selectors_and_k() { + let field_cfg = cfg(); + let valid = fixed_layout_public(); + validate_production_sha_publics(std::slice::from_ref(&valid), &field_cfg).unwrap(); + + let mut bad_selector = valid.clone(); + bad_selector.columns.columns[ShaPublicCol::SOut.index()][0] = f(1); + assert!(matches!( + validate_production_sha_publics(&[bad_selector], &field_cfg), + Err(ProductionShaError::InvalidPublicSelector { + col: ShaPublicCol::SOut, + row: 0 + }) + )); + + let mut non_boolean_selector = valid.clone(); + non_boolean_selector.columns.columns[ShaPublicCol::SInit.index()][0] = f(2); + assert!(matches!( + validate_production_sha_publics(&[non_boolean_selector], &field_cfg), + Err(ProductionShaError::NonBooleanPublicSelector { + col: ShaPublicCol::SInit, + row: 0 + }) + )); + + let mut bad_k = valid; + bad_k.columns.columns[ShaPublicCol::K.index()][3] += f(1); + assert!(matches!( + validate_production_sha_publics(&[bad_k], &field_cfg), + Err(ProductionShaError::InvalidRoundConstant { row: 3 }) + )); + } + #[test] fn sumfold_outputs_instance_fold_point_before_theta() { let field_cfg = cfg(); @@ -836,6 +3285,97 @@ mod tests { )); } + #[test] + fn full_sha_sumfold_derives_fold_weights_after_instance_sumcheck() { + let field_cfg = cfg(); + let a = f(5); + let traces = vec![ + zero_trace_with_scalar_challenge(&a), + zero_trace_with_scalar_challenge(&a), + ]; + let publics = vec![zero_public(), zero_public()]; + let beta = vec![f(13)]; + let r_ic = sparse_r_ic(); + let lambda = f(17); + let rho = f(19); + let xi = f(23); + let booleanity_sources = vec![ + ShaBooleanitySource::WordBit { + col: ShaWordCol::A, + bit: 0, + }, + ShaBooleanitySource::VirtualMaj { bit: 0 }, + ]; + let initial_claim = F::zero_with_cfg(&field_cfg); + + let mut prover_transcript = Blake3Transcript::new(); + prover_transcript.absorb_slice(b"full-sha-sumfold-context"); + let (proof, prover_output) = prove_full_sha_sumfold( + &mut prover_transcript, + &traces, + &publics, + &initial_claim, + &beta, + &r_ic, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + &field_cfg, + ) + .unwrap(); + + let mut verifier_transcript = Blake3Transcript::new(); + verifier_transcript.absorb_slice(b"full-sha-sumfold-context"); + let verifier_output = verify_full_sha_sumfold( + &mut verifier_transcript, + &proof, + &initial_claim, + &beta, + traces.len(), + &field_cfg, + ) + .unwrap(); + + assert_eq!(verifier_output, prover_output); + assert_eq!( + prover_output.theta(), + build_eq_x_r_vec(prover_output.r_b(), &field_cfg).unwrap() + ); + assert_eq!(prover_output.theta().len(), traces.len()); + + let (folded, folded_public) = + fold_projected_sha_traces(&traces, &publics, &prover_output, &field_cfg).unwrap(); + let folded_sum = expression_folded_row_sum( + &folded.trace, + &folded_public, + &r_ic, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + &field_cfg, + ) + .unwrap(); + assert_eq!(prover_output.t_prime(), &folded_sum); + + let mut bad_transcript = Blake3Transcript::new(); + bad_transcript.absorb_slice(b"full-sha-sumfold-context"); + assert!( + verify_full_sha_sumfold( + &mut bad_transcript, + &proof, + &f(1), + &beta, + traces.len(), + &field_cfg + ) + .is_err() + ); + } + #[test] fn folded_row_sumcheck_claim_is_t_prime() { let field_cfg = cfg(); @@ -914,6 +3454,326 @@ mod tests { )); } + #[test] + fn expression_folded_row_terminal_is_reconstructed_from_endpoints() { + let field_cfg = cfg(); + let a = f(5); + let trace = zero_trace_with_scalar_challenge(&a); + let public = zero_public(); + let r_ic = sparse_r_ic(); + let lambda = f(7); + let rho = f(11); + let xi = f(13); + let booleanity_sources = vec![ + ShaBooleanitySource::WordBit { + col: ShaWordCol::A, + bit: 0, + }, + ShaBooleanitySource::VirtualCh1 { bit: 0 }, + ]; + let t_prime = expression_folded_row_sum( + &trace, + &public, + &r_ic, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + &field_cfg, + ) + .unwrap(); + + let mut prover_transcript = Blake3Transcript::new(); + prover_transcript.absorb_slice(b"expression-row-context"); + let proof = prove_expression_folded_row_sumcheck( + &mut prover_transcript, + &trace, + &public, + &r_ic, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + &t_prime, + &field_cfg, + ) + .unwrap(); + + let mut verifier_transcript = Blake3Transcript::new(); + verifier_transcript.absorb_slice(b"expression-row-context"); + let output = + verify_folded_row_sumcheck(&mut verifier_transcript, &proof, &t_prime, &field_cfg) + .unwrap(); + let endpoint_evals = + build_sha_endpoint_evals_from_trace(&trace, &output.r_star, &field_cfg).unwrap(); + let terminal = reconstruct_folded_row_terminal_from_endpoints( + &endpoint_evals, + &public, + &r_ic, + &output.r_star, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + &field_cfg, + ) + .unwrap(); + verify_folded_row_terminal_value(&output, &terminal).unwrap(); + + let mut bad_terminal = terminal; + bad_terminal += f(1); + assert!(verify_folded_row_terminal_value(&output, &bad_terminal).is_err()); + + let mut bad_endpoints = endpoint_evals; + bad_endpoints.sources[0].bits[0] += f(1); + assert!( + reconstruct_folded_row_terminal_from_endpoints( + &bad_endpoints, + &public, + &r_ic, + &output.r_star, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + &field_cfg + ) + .is_err() + ); + } + + #[test] + fn endpoint_multipoint_reduces_all_sources_and_rejects_bad_openings() { + let field_cfg = cfg(); + let a = f(5); + let trace = zero_trace_with_scalar_challenge(&a); + let public = zero_public(); + let r_star = vec![f(2), f(3), f(5), f(7), f(11), f(13), f(17)]; + let endpoint_evals = + build_sha_endpoint_evals_from_trace(&trace, &r_star, &field_cfg).unwrap(); + + let mut prover_transcript = Blake3Transcript::new(); + prover_transcript.absorb_slice(b"endpoint-multipoint-context"); + let (proof, r_0) = prove_sha_endpoint_multipoint( + &mut prover_transcript, + &trace, + &public, + &endpoint_evals, + &r_star, + &field_cfg, + ) + .unwrap(); + + let mut verifier_transcript = Blake3Transcript::new(); + verifier_transcript.absorb_slice(b"endpoint-multipoint-context"); + let (subclaim, shift_specs) = verify_sha_endpoint_multipoint( + &mut verifier_transcript, + &proof, + &endpoint_evals, + &public, + &r_star, + &field_cfg, + ) + .unwrap(); + assert_eq!(subclaim.sumcheck_subclaim.point, r_0); + + let layout = production_sha_multipoint_layout(); + let trace_mles = sha_multipoint_trace_mles(&trace, &public, &layout, &field_cfg).unwrap(); + let open_evals = trace_mles + .iter() + .map(|mle| mle.clone().evaluate_with_config(&r_0, &field_cfg).unwrap()) + .collect::>(); + verify_sha_endpoint_multipoint_open_evals(&subclaim, &open_evals, &shift_specs, &field_cfg) + .unwrap(); + + let mut bad_open_evals = open_evals.clone(); + bad_open_evals[0] += f(1); + assert!( + verify_sha_endpoint_multipoint_open_evals( + &subclaim, + &bad_open_evals, + &shift_specs, + &field_cfg + ) + .is_err() + ); + + let mut bad_endpoint_evals = endpoint_evals; + bad_endpoint_evals.sources[0].bits[0] += f(1); + rescalarize_endpoint_source(&mut bad_endpoint_evals.sources[0], &a); + let mut bad_verifier_transcript = Blake3Transcript::new(); + bad_verifier_transcript.absorb_slice(b"endpoint-multipoint-context"); + assert!( + verify_sha_endpoint_multipoint( + &mut bad_verifier_transcript, + &proof, + &bad_endpoint_evals, + &public, + &r_star, + &field_cfg + ) + .is_err() + ); + } + + #[test] + fn fresh_ideal_objects_must_be_trimmed_and_degree_capped() { + let field_cfg = cfg(); + let mut ideals = vec![std::array::from_fn(|_| { + DynamicPolynomialF::new(Vec::::new()) + })]; + ideals[0][0] = DynamicPolynomialF::new(vec![f(1), F::zero_with_cfg(&field_cfg)]); + assert!(matches!( + check_fresh_sha_ideal_membership(&ideals, &field_cfg), + Err(ProductionShaError::NonCanonicalProofObject(_)) + )); + + let mut high_degree = vec![std::array::from_fn(|_| { + DynamicPolynomialF::new(Vec::::new()) + })]; + high_degree[0][2] = DynamicPolynomialF::new(vec![f(1); 33]); + assert!(matches!( + check_fresh_sha_ideal_membership(&high_degree, &field_cfg), + Err(ProductionShaError::NonCanonicalProofObject(_)) + )); + } + + #[test] + fn endpoint_layout_must_be_exact_and_canonical() { + let field_cfg = cfg(); + let trace = zero_trace_with_scalar_challenge(&f(5)); + let r_star = vec![f(2), f(3), f(5), f(7), f(11), f(13), f(17)]; + let endpoint_evals = + build_sha_endpoint_evals_from_trace(&trace, &r_star, &field_cfg).unwrap(); + validate_sha_endpoint_layout(&endpoint_evals).unwrap(); + + let mut missing = endpoint_evals.clone(); + missing.sources.pop(); + assert!(validate_sha_endpoint_layout(&missing).is_err()); + + let mut reordered = endpoint_evals; + reordered.sources.swap(0, 1); + assert!(matches!( + validate_sha_endpoint_layout(&reordered), + Err(ProductionShaError::NonCanonicalProofObject(_)) + )); + } + + #[test] + fn pcs_lifted_evals_drive_multipoint_sources_and_recompute_publics() { + let field_cfg = cfg(); + let mut public = zero_public(); + public.columns.columns[ShaPublicCol::K.index()][0] = f(99); + let r_0 = vec![F::zero_with_cfg(&field_cfg); SHA_ROW_VARS]; + let layout = production_sha_multipoint_layout(); + + let mut lifted = vec![ + DynamicPolynomialF::new_trimmed(vec![F::zero_with_cfg(&field_cfg)]); + ShaWordCol::COUNT + ShaIntCol::COUNT + ]; + lifted[ShaWordCol::A.index()] = DynamicPolynomialF::new_trimmed(vec![f(3)]); + lifted[ShaWordCol::COUNT + ShaIntCol::CompSchedule.index()] = + DynamicPolynomialF::new_trimmed(vec![f(7)]); + + let open_evals = + multipoint_open_evals_from_pcs_lifted(&lifted, &layout, &public, &r_0, &field_cfg) + .unwrap(); + let a0_idx = layout + .sources + .iter() + .position(|source| { + *source + == ShaMpSource::WordBit { + col: ShaWordCol::A, + bit: 0, + } + }) + .unwrap(); + let int_idx = layout + .sources + .iter() + .position(|source| { + *source + == ShaMpSource::Int { + col: ShaIntCol::CompSchedule, + } + }) + .unwrap(); + let public_idx = layout + .sources + .iter() + .position(|source| { + *source + == ShaMpSource::Public { + col: ShaPublicCol::K, + } + }) + .unwrap(); + + assert_eq!(open_evals[a0_idx], f(3)); + assert_eq!(open_evals[int_idx], f(7)); + assert_eq!(open_evals[public_idx], f(99)); + } + + #[test] + fn folded_lifted_evals_must_be_canonical_and_32_bit() { + let field_cfg = cfg(); + let mut lifted = vec![DynamicPolynomialF::ZERO; ShaWordCol::COUNT + ShaIntCol::COUNT]; + split_folded_sha_pcs_lifted_evals(&lifted).unwrap(); + ensure_production_sha_word_degree::().unwrap(); + assert!(matches!( + ensure_production_sha_word_degree::(), + Err(ProductionShaError::UnsupportedProductionShaWordDegree { + got: 8, + expected: 32 + }) + )); + + lifted[ShaWordCol::A.index()] = + DynamicPolynomialF::new(vec![F::zero_with_cfg(&field_cfg); SHA_WORD_BITS + 1]); + assert!(matches!( + split_folded_sha_pcs_lifted_evals(&lifted), + Err(ProductionShaError::NonCanonicalProofObject(_)) + )); + + lifted[ShaWordCol::A.index()] = + DynamicPolynomialF::new(vec![f(1), F::zero_with_cfg(&field_cfg)]); + assert!(matches!( + split_folded_sha_pcs_lifted_evals(&lifted), + Err(ProductionShaError::NonCanonicalProofObject(_)) + )); + + lifted[ShaWordCol::A.index()] = DynamicPolynomialF::ZERO; + lifted[ShaWordCol::COUNT + ShaIntCol::CompSchedule.index()] = + DynamicPolynomialF::new(vec![f(1), f(2)]); + assert!(matches!( + split_folded_sha_pcs_lifted_evals(&lifted), + Err(ProductionShaError::NonCanonicalProofObject(_)) + )); + } + + #[test] + fn production_sha_requires_exact_commitment_batch_sizes() { + validate_production_sha_batch_sizes::(ShaWordCol::COUNT, 0, ShaIntCol::COUNT).unwrap(); + + assert!(matches!( + validate_production_sha_batch_sizes::(0, 0, ShaIntCol::COUNT), + Err(ProductionShaError::UnsupportedProductionShaPcsShape(_)) + )); + assert!(matches!( + validate_production_sha_batch_sizes::(ShaWordCol::COUNT, 1, ShaIntCol::COUNT), + Err(ProductionShaError::UnsupportedProductionShaPcsShape(_)) + )); + assert!(matches!( + validate_production_sha_batch_sizes::(ShaWordCol::COUNT, 0, 0), + Err(ProductionShaError::UnsupportedProductionShaPcsShape(_)) + )); + } + #[test] fn scalarization_and_virtual_endpoints_use_source_bits_only() { let field_cfg = cfg(); From 965a8db1bbbc213dab5d7e9ffb81c137dd55dfb6 Mon Sep 17 00:00:00 2001 From: John Wu Date: Sat, 6 Jun 2026 16:17:37 -0700 Subject: [PATCH 17/49] Optimize Hyrax SHA commitments --- poly/src/univariate/binary_ref.rs | 6 + poly/src/univariate/binary_u64.rs | 7 + protocol/Cargo.toml | 2 +- protocol/benches/e2e.rs | 67 ++--- protocol/src/pcs.rs | 19 +- zip-plus/src/pcs/hyrax.rs | 401 +++++++++++++++++++++-------- zip-plus/src/pcs/msm_commitment.rs | 293 ++++++++++++++++++--- 7 files changed, 624 insertions(+), 171 deletions(-) diff --git a/poly/src/univariate/binary_ref.rs b/poly/src/univariate/binary_ref.rs index 1370a5cc..d72d22b4 100644 --- a/poly/src/univariate/binary_ref.rs +++ b/poly/src/univariate/binary_ref.rs @@ -51,6 +51,12 @@ impl BinaryRefPoly { pub const fn inner(&self) -> &DensePolynomial { &self.0 } + + #[inline(always)] + pub fn coeff(&self, idx: usize) -> bool { + assert!(idx < DEGREE_PLUS_ONE); + self.0.coeffs[idx].inner() + } } impl From> diff --git a/poly/src/univariate/binary_u64.rs b/poly/src/univariate/binary_u64.rs index 8d5f031a..b36cf46a 100644 --- a/poly/src/univariate/binary_u64.rs +++ b/poly/src/univariate/binary_u64.rs @@ -32,6 +32,13 @@ impl BinaryU64Poly { pub const fn inner(&self) -> &u64 { &self.0 } + + #[inline(always)] + #[allow(clippy::arithmetic_side_effects)] + pub fn coeff(&self, idx: usize) -> bool { + assert!(idx < DEGREE_PLUS_ONE && idx < u64::BITS as usize); + !(self.0 & (1 << idx)).is_zero() + } } impl From> for u64 { diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index f3429c01..dd06112d 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -40,7 +40,7 @@ workspace = true [features] -parallel = ["dep:rayon", "zinc-piop/parallel", "zinc-poly/parallel", "zinc-uair/parallel", "zinc-utils/parallel"] +parallel = ["dep:rayon", "zip-plus/parallel", "zinc-piop/parallel", "zinc-poly/parallel", "zinc-uair/parallel", "zinc-utils/parallel"] simd = ["zinc-poly/simd", "zinc-piop/simd", "zip-plus/simd"] unchecked = [] # Switch the IPRS bench code from inverse-rate 4 (rate 1/4) to inverse-rate 8 diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index 27f657ad..8322a325 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -26,7 +26,7 @@ use zinc_protocol::{ FoldedZincTypes, IntFoldedZincTypes4x, Proof, ZincPlusPiop, ZincTypes, fixed_prime::field_cfg_from_curve_scalar, pcs::{ - AllZipPCSTypes, BinaryHyraxZipRest, PCSCommitments, PCSParams, PCSVerifierParams, + AllZipPCSTypes, BinaryIntHyraxZipArbitrary, PCSCommitments, PCSParams, PCSVerifierParams, ZincPCSTypes, }, }; @@ -55,7 +55,7 @@ use zip_plus::{ iprs::{IprsCode, PnttConfigF65537}, }, pcs::generic::{PCS, ZipPlusPCS}, - pcs::hyrax::{BinaryLanes, HyraxBlindingMode, HyraxPCS}, + pcs::hyrax::{BinaryLanes, HyraxBlindingMode, HyraxPCS, IntScalarLane}, pcs::structs::{ZipPlus, ZipPlusCommitment, ZipPlusParams, ZipTypes}, utils::{eprint_bytes_size, eprint_bytes_size_breakdown, eprint_proof_size}, }; @@ -1221,11 +1221,11 @@ fn zip_pcs_params( fn hyrax_pcs_params( num_vars: usize, ) -> ( - PCSParams, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>, - PCSVerifierParams, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>, + PCSParams, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>, + PCSVerifierParams, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>, ) where - BinaryHyraxZipRest: ZincPCSTypes< + BinaryIntHyraxZipArbitrary: ZincPCSTypes< RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE, @@ -1234,30 +1234,39 @@ where >::ArbitraryZt, >::ArbitraryLc, >, - IntPCS = ZipPlusPCS< - >::IntZt, - >::IntLc, - >, + IntPCS = HyraxPCS, >, { let pp = setup_pp_real_ecdsa(num_vars); - let width = pp.0.linear_code.row_len(); - let (ck, vk) = HyraxPCS::::setup( - width, - b"zinc-plus-bench-real-sha256-hyrax", + let binary_width = pp.0.linear_code.row_len(); + let int_width = pp.2.linear_code.row_len(); + let (binary, binary_vk) = HyraxPCS::::setup( + binary_width, + b"zinc-plus-bench-real-sha256-hyrax-binary", + HyraxBlindingMode::Unblinded, + ) + .expect("Hyrax binary benchmark setup must be valid"); + let (int, int_vk) = HyraxPCS::::setup( + int_width, + b"zinc-plus-bench-real-sha256-hyrax-int", HyraxBlindingMode::Unblinded, ) - .expect("Hyrax benchmark setup must be valid"); + .expect("Hyrax int benchmark setup must be valid"); ( - PCSParams::, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE> { - binary: ck, + PCSParams::, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE> { + binary, arbitrary: pp.1.clone(), - int: pp.2.clone(), + int, }, - PCSVerifierParams::, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE> { - binary: vk, + PCSVerifierParams::< + BinaryIntHyraxZipArbitrary, + RealEcdsaBenchZincTypes, + F, + DEGREE_PLUS_ONE, + > { + binary: binary_vk, arbitrary: pp.1, - int: pp.2, + int: int_vk, }, ) } @@ -1268,7 +1277,7 @@ fn bench_real_sha256_pcs_curve_e2e( zip_label: &str, hyrax_label: &str, ) where - BinaryHyraxZipRest: ZincPCSTypes< + BinaryIntHyraxZipArbitrary: ZincPCSTypes< RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE, @@ -1277,10 +1286,7 @@ fn bench_real_sha256_pcs_curve_e2e( >::ArbitraryZt, >::ArbitraryLc, >, - IntPCS = ZipPlusPCS< - >::IntZt, - >::IntLc, - >, + IntPCS = HyraxPCS, >, { type U = Sha256CompressionSliceUair; @@ -1307,7 +1313,7 @@ fn bench_real_sha256_pcs_curve_e2e( ); let (hyrax_pp, hyrax_vp) = hyrax_pcs_params::(num_vars); - do_bench_pcs_e2e::>( + do_bench_pcs_e2e::>( group, hyrax_label, num_vars, @@ -1326,7 +1332,7 @@ fn bench_real_sha256_pcs_curve_steps( zip_label: &str, hyrax_label: &str, ) where - BinaryHyraxZipRest: ZincPCSTypes< + BinaryIntHyraxZipArbitrary: ZincPCSTypes< RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE, @@ -1335,10 +1341,7 @@ fn bench_real_sha256_pcs_curve_steps( >::ArbitraryZt, >::ArbitraryLc, >, - IntPCS = ZipPlusPCS< - >::IntZt, - >::IntLc, - >, + IntPCS = HyraxPCS, >, { type U = Sha256CompressionSliceUair; @@ -1365,7 +1368,7 @@ fn bench_real_sha256_pcs_curve_steps( ); let (hyrax_pp, hyrax_vp) = hyrax_pcs_params::(num_vars); - do_bench_pcs_steps::>( + do_bench_pcs_steps::>( group, hyrax_label, num_vars, diff --git a/protocol/src/pcs.rs b/protocol/src/pcs.rs index a447108d..765393c7 100644 --- a/protocol/src/pcs.rs +++ b/protocol/src/pcs.rs @@ -5,7 +5,7 @@ use crypto_primitives::PrimeField; use zinc_poly::univariate::{binary::BinaryPoly, dense::DensePolynomial}; use zip_plus::pcs::{ generic::{PCS, ZipPlusPCS}, - hyrax::{BinaryLanes, HyraxPCS}, + hyrax::{BinaryLanes, HyraxPCS, IntScalarLane}, structs::ZipPlusCommitment, }; @@ -56,6 +56,23 @@ where type IntPCS = ZipPlusPCS; } +#[derive(Clone, Debug)] +pub struct BinaryIntHyraxZipArbitrary(PhantomData); + +impl ZincPCSTypes for BinaryIntHyraxZipArbitrary +where + Zt: ZincTypes, + F: PrimeField, + C: AffineRepr, + HyraxPCS: PCS, D>, + ZipPlusPCS: PCS, D>, + HyraxPCS: PCS, +{ + type BinaryPCS = HyraxPCS; + type ArbitraryPCS = ZipPlusPCS; + type IntPCS = HyraxPCS; +} + #[derive(Clone, Debug)] pub struct PCSParams where diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index 9f63d4c2..a3d1ec58 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -8,7 +8,7 @@ use std::{ }; use ark_ec::{AffineRepr, CurveGroup, VariableBaseMSM}; -use ark_ff::{BigInteger, PrimeField as ArkPrimeField, Zero}; +use ark_ff::{BigInteger, PrimeField as ArkPrimeField, UniformRand, Zero}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress}; use crypto_primitives::{ FromWithConfig, IntRing, PrimeField, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, @@ -22,6 +22,10 @@ use zinc_poly::{ }, }; use zinc_transcript::traits::{ConstTranscribable, GenTranscribable, Transcribable, Transcript}; +use zinc_utils::{cfg_into_iter, cfg_iter}; + +#[cfg(feature = "parallel")] +use rayon::prelude::*; use crate::{ ZipError, @@ -29,7 +33,7 @@ use crate::{ generic::PCS, msm_commitment::{ BoolSubsetMsm, MsmCommitmentEngine, MsmCommitmentKey, MsmError, RowMsmStrategy, - ScalarPippengerMsm, + ScalarPippengerMsm, SignedIntPippengerMsm, }, }, pcs_transcript::{PcsProverTranscript, PcsVerifierTranscript}, @@ -218,10 +222,12 @@ impl HyraxLanes, D> for BinaryLa const NUM_LANES: usize = D; fn lane_value(eval: &BinaryPoly, lane: usize) -> Result { - eval.iter() - .nth(lane) - .map(|bit| bit.inner()) - .ok_or_else(|| ZipError::InvalidPcsParam(format!("binary lane {lane} out of range"))) + if lane >= D { + return Err(ZipError::InvalidPcsParam(format!( + "binary lane {lane} out of range" + ))); + } + Ok(eval.coeff(lane)) } fn lane_to_scalar(value: Self::LaneValue) -> C::ScalarField { @@ -238,25 +244,22 @@ impl HyraxLanes, D> for BinaryLa num_rows: usize, ) -> Option, Vec), ZipError>> { let expected_comm = , D>>::NUM_LANES * num_rows; - let mut comm = Vec::with_capacity(expected_comm); - let mut blinds = if ck.blinding_mode.is_blinded() { - Vec::with_capacity(expected_comm) + , D>>::Strategy::precompute_ck(&ck.msm_ck); + let blinds = if ck.blinding_mode.is_blinded() { + random_scalars::(expected_comm) } else { Vec::new() }; Some((|| { - for lane in 0.., D>>::NUM_LANES { - let lane_blinds = if ck.blinding_mode.is_blinded() { - Some(MsmCommitmentEngine::::blind( - &ck.msm_ck, - poly.evaluations.len(), - )) - } else { - None - }; - - for (row_idx, row) in poly.evaluations.chunks(ck.num_cols).enumerate() { + let use_inner_parallelism = use_inner_bool_parallelism(expected_comm); + let comm = cfg_into_iter!(0..expected_comm) + .map(|job_idx| { + let lane = job_idx / num_rows; + let row_idx = job_idx % num_rows; + let lower = row_idx * ck.num_cols; + let upper = (lower + ck.num_cols).min(poly.evaluations.len()); + let row = &poly.evaluations[lower..upper]; let values = row .iter() .map(|eval| { @@ -265,24 +268,18 @@ impl HyraxLanes, D> for BinaryLa .collect::, _>>()?; let mut row_comm = if values.iter().copied().any(|bit| bit) { - , D>>::Strategy::msm_row( - &ck.msm_ck, &values, - ) - .map_err(msm_err)? + BoolSubsetMsm::<6>::msm_bool_row(&ck.msm_ck, &values, use_inner_parallelism) + .map_err(msm_err)? } else { C::Group::zero() }; - if let Some(lane_blinds) = lane_blinds.as_ref() { - row_comm += ck.msm_ck.h * lane_blinds.blind[row_idx]; + if ck.blinding_mode.is_blinded() { + row_comm += ck.msm_ck.h * blinds[job_idx]; } - comm.push(row_comm); - } - - if let Some(lane_blinds) = lane_blinds { - blinds.extend(lane_blinds.blind); - } - } + Ok::(row_comm) + }) + .collect::, _>>()?; Ok((comm, blinds)) })()) } @@ -357,8 +354,8 @@ impl HyraxLanes, D> for BinaryLa impl HyraxLanes, D> for IntScalarLane { - type LaneValue = C::ScalarField; - type Strategy = ScalarPippengerMsm; + type LaneValue = Int; + type Strategy = SignedIntPippengerMsm; const NUM_LANES: usize = 1; @@ -368,11 +365,11 @@ impl HyraxLanes "int lane {lane} out of range" ))); } - int_to_scalar::(eval) + Ok(*eval) } fn lane_to_scalar(value: Self::LaneValue) -> C::ScalarField { - value + int_to_scalar::(&value).expect("int lane value must convert to scalar") } fn lifted_eval( @@ -514,38 +511,23 @@ where validate_polys(polys)?; let n = polys[0].evaluations.len(); let num_rows = num_rows(n, ck.num_cols)?; - let mut all_comm = Vec::with_capacity(polys.len() * Lanes::NUM_LANES * num_rows); - let mut all_blinds = if ck.blinding_mode.is_blinded() { - Vec::with_capacity(polys.len() * Lanes::NUM_LANES * num_rows) + Lanes::Strategy::precompute_ck(&ck.msm_ck); + + let per_poly = cfg_iter!(polys) + .map(|poly| commit_hyrax_poly::(ck, poly, num_rows)) + .collect::, _>>()?; + + let expected_comm = polys.len() * Lanes::NUM_LANES * num_rows; + let expected_blinds = if ck.blinding_mode.is_blinded() { + expected_comm } else { - Vec::new() + 0 }; - - for poly in polys { - if let Some(result) = Lanes::commit_poly(ck, poly, num_rows) { - let (comm, blinds) = result?; - all_comm.extend(comm); - all_blinds.extend(blinds); - continue; - } - for lane in 0..Lanes::NUM_LANES { - let values = lane_values::(poly, lane)?; - let commitment = if ck.blinding_mode.is_blinded() { - let blind = MsmCommitmentEngine::::blind(&ck.msm_ck, values.len()); - let commitment = MsmCommitmentEngine::::commit_with::<_, Lanes::Strategy>( - &ck.msm_ck, &values, &blind, - ) - .map_err(msm_err)?; - all_blinds.extend(blind.blind); - commitment - } else { - MsmCommitmentEngine::::commit_unblinded_with::<_, Lanes::Strategy>( - &ck.msm_ck, &values, - ) - .map_err(msm_err)? - }; - all_comm.extend(commitment.comm); - } + let mut all_comm = Vec::with_capacity(expected_comm); + let mut all_blinds = Vec::with_capacity(expected_blinds); + for (comm, blinds) in per_poly { + all_comm.extend(comm); + all_blinds.extend(blinds); } Ok(( @@ -682,15 +664,23 @@ where } } } else { - for (poly_idx, poly) in polys.iter().enumerate() { - for lane in 0..Lanes::NUM_LANES { - let alpha = alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; - for (row_idx, row) in poly.evaluations.chunks(ck.num_cols).enumerate() { - let row_eval = Lanes::accumulate_b(row, lane, &q1_scalar)?; - b_scalar[row_idx] += alpha * row_eval; + b_scalar = cfg_into_iter!(0..prover_data.num_rows) + .map(|row_idx| { + let lower = row_idx * ck.num_cols; + let mut acc = C::ScalarField::zero(); + for (poly_idx, poly) in polys.iter().enumerate() { + let upper = (lower + ck.num_cols).min(poly.evaluations.len()); + let row = &poly.evaluations[lower..upper]; + for lane in 0..Lanes::NUM_LANES { + let alpha = + alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; + let row_eval = Lanes::accumulate_b(row, lane, &q1_scalar)?; + acc += alpha * row_eval; + } } - } - } + Ok::(acc) + }) + .collect::, _>>()?; let b_f = b_scalar .iter() @@ -701,24 +691,43 @@ where let row_coeffs = sample_scalars::(&mut transcript.fs_transcript, prover_data.num_rows); - for (poly_idx, poly) in polys.iter().enumerate() { - for lane in 0..Lanes::NUM_LANES { - let alpha = alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; - for (row_idx, row) in poly.evaluations.chunks(ck.num_cols).enumerate() { - let coeff = alpha * row_coeffs[row_idx]; - if ck.blinding_mode.is_blinded() { - let blind_idx = commitment_index_dynamic( - Lanes::NUM_LANES, - poly_idx, - lane, - row_idx, - prover_data.num_rows, - ); - rho_star += coeff * prover_data.blinds[blind_idx]; + combined_row = cfg_into_iter!(0..ck.num_cols) + .map(|col_idx| { + let mut acc = C::ScalarField::zero(); + for (poly_idx, poly) in polys.iter().enumerate() { + for lane in 0..Lanes::NUM_LANES { + let alpha = + alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; + for (row_idx, row_coeff) in row_coeffs.iter().copied().enumerate() { + let eval_idx = row_idx * ck.num_cols + col_idx; + if let Some(eval) = poly.evaluations.get(eval_idx) { + let value = + Lanes::lane_to_scalar(Lanes::lane_value(eval, lane)?); + acc += alpha * row_coeff * value; + } + } } - Lanes::accumulate_combined_row(row, lane, coeff, &mut combined_row)?; } - } + Ok::(acc) + }) + .collect::, _>>()?; + + if ck.blinding_mode.is_blinded() { + let total_jobs = polys.len() * Lanes::NUM_LANES * prover_data.num_rows; + let rho_terms = cfg_into_iter!(0..total_jobs) + .map(|job_idx| { + let poly_stride = Lanes::NUM_LANES * prover_data.num_rows; + let poly_idx = job_idx / poly_stride; + let lane_row_idx = job_idx % poly_stride; + let lane = lane_row_idx / prover_data.num_rows; + let row_idx = lane_row_idx % prover_data.num_rows; + let alpha = alphas[alpha_index_dynamic(Lanes::NUM_LANES, poly_idx, lane)]; + alpha * row_coeffs[row_idx] * prover_data.blinds[job_idx] + }) + .collect::>(); + rho_star = rho_terms + .into_iter() + .fold(C::ScalarField::zero(), |acc, term| acc + term); } } @@ -863,16 +872,19 @@ where )); } - let mut comm_lc_scalars = Vec::with_capacity(commitment.comm.len()); - for poly_idx in 0..commitment.batch_size { - for lane in 0..commitment.num_lanes { - let alpha = alphas[alpha_index_dynamic(commitment.num_lanes, poly_idx, lane)]; - comm_lc_scalars.extend(row_coeffs.iter().map(|row_coeff| alpha * row_coeff)); - } - } - let comm_bases = C::Group::normalize_batch(&commitment.comm); - let comm_lc = msm_unchecked::(&comm_bases, &comm_lc_scalars)?; + let comm_lc = if commitment.num_rows == 1 { + msm_unchecked::(&comm_bases, &alphas)? + } else { + let mut comm_lc_scalars = Vec::with_capacity(commitment.comm.len()); + for poly_idx in 0..commitment.batch_size { + for lane in 0..commitment.num_lanes { + let alpha = alphas[alpha_index_dynamic(commitment.num_lanes, poly_idx, lane)]; + comm_lc_scalars.extend(row_coeffs.iter().map(|row_coeff| alpha * row_coeff)); + } + } + msm_unchecked::(&comm_bases, &comm_lc_scalars)? + }; let mut expected = msm_unchecked::(&vk.bases[..combined_row.len()], &combined_row)?; if let Some(rho_star) = rho_star { @@ -1005,6 +1017,54 @@ where Ok(()) } +fn commit_hyrax_poly( + ck: &HyraxCommitmentKey, + poly: &DenseMultilinearExtension, + num_rows: usize, +) -> Result<(Vec, Vec), ZipError> +where + C: AffineRepr, + Lanes: HyraxLanes, + Eval: Clone + Debug + Send + Sync, +{ + if let Some(result) = Lanes::commit_poly(ck, poly, num_rows) { + return result; + } + + let per_lane = cfg_into_iter!(0..Lanes::NUM_LANES) + .map(|lane| { + let values = lane_values::(poly, lane)?; + if ck.blinding_mode.is_blinded() { + let blind = MsmCommitmentEngine::::blind(&ck.msm_ck, values.len()); + let commitment = MsmCommitmentEngine::::commit_with::<_, Lanes::Strategy>( + &ck.msm_ck, &values, &blind, + ) + .map_err(msm_err)?; + Ok::<(Vec, Vec), ZipError>((commitment.comm, blind.blind)) + } else { + let commitment = MsmCommitmentEngine::::commit_unblinded_with::< + _, + Lanes::Strategy, + >(&ck.msm_ck, &values) + .map_err(msm_err)?; + Ok::<(Vec, Vec), ZipError>((commitment.comm, Vec::new())) + } + }) + .collect::, _>>()?; + + let mut comm = Vec::with_capacity(Lanes::NUM_LANES * num_rows); + let mut blinds = if ck.blinding_mode.is_blinded() { + Vec::with_capacity(Lanes::NUM_LANES * num_rows) + } else { + Vec::new() + }; + for (lane_comm, lane_blinds) in per_lane { + comm.extend(lane_comm); + blinds.extend(lane_blinds); + } + Ok((comm, blinds)) +} + fn lane_values( poly: &DenseMultilinearExtension, lane: usize, @@ -1020,6 +1080,24 @@ where .collect() } +fn random_scalars(n: usize) -> Vec { + let mut rng = ark_std::rand::thread_rng(); + (0..n).map(|_| C::ScalarField::rand(&mut rng)).collect() +} + +fn use_inner_bool_parallelism(outer_jobs: usize) -> bool { + #[cfg(feature = "parallel")] + { + outer_jobs < rayon::current_num_threads() + } + + #[cfg(not(feature = "parallel"))] + { + let _ = outer_jobs; + false + } +} + fn hash_to_curve(domain: &[u8], label: &[u8], index: usize) -> Result { let point_bytes = C::zero().serialized_size(Compress::Yes); let mut counter = 0u64; @@ -1142,7 +1220,28 @@ fn msm_unchecked( bases.len() ))); } - Ok(::msm_unchecked(bases, scalars)) + if !scalars.iter().any(|scalar| scalar.is_zero()) { + return Ok(::msm_unchecked(bases, scalars)); + } + + let non_zero = scalars + .iter() + .enumerate() + .filter(|(_, scalar)| !scalar.is_zero()); + let mut filtered_bases = Vec::new(); + let mut filtered_scalars = Vec::new(); + for (idx, scalar) in non_zero { + filtered_bases.push(bases[idx]); + filtered_scalars.push(*scalar); + } + if filtered_scalars.is_empty() { + return Ok(C::Group::zero()); + } + + Ok(::msm_unchecked( + &filtered_bases, + &filtered_scalars, + )) } fn num_rows(n: usize, width: usize) -> Result { @@ -1580,6 +1679,106 @@ mod tests { .unwrap(); } + #[test] + fn binary_hyrax_commitment_order_is_poly_lane_row() { + type C = ark_bn254::G1Affine; + type F = MontyField<4>; + const D: usize = 32; + + fn bp(bits: u32) -> BinaryPoly { + BinaryPoly::::from(bits) + } + + let width = 8; + let (ck, _) = HyraxPCS::::setup( + width, + b"zinc-plus-hyrax-order-test", + HyraxBlindingMode::Unblinded, + ) + .unwrap(); + let polys = vec![ + DenseMultilinearExtension::from_evaluations_vec( + 4, + (0..16).map(|idx| bp((idx * 13 + 7) as u32)).collect(), + bp(0), + ), + DenseMultilinearExtension::from_evaluations_vec( + 4, + (0..16) + .map(|idx| bp(((idx * 29 + 3) as u32).reverse_bits())) + .collect(), + bp(0), + ), + ]; + + let (prover_data, commitment) = + as PCS, D>>::commit(&ck, &polys).unwrap(); + + let mut expected = Vec::new(); + BoolSubsetMsm::<6>::precompute_ck(&ck.msm_ck); + for poly in &polys { + for lane in 0..D { + for row in poly.evaluations.chunks(width) { + let values = row.iter().map(|eval| eval.coeff(lane)).collect::>(); + let row_comm = if values.iter().copied().any(|bit| bit) { + BoolSubsetMsm::<6>::msm_bool_row(&ck.msm_ck, &values, false).unwrap() + } else { + ::Group::zero() + }; + expected.push(row_comm); + } + } + } + + assert_eq!(prover_data.blinds.len(), 0); + assert_eq!(commitment.num_rows, 2); + assert_eq!(commitment.comm, expected); + } + + #[test] + fn binary_hyrax_commitment_supports_partial_single_row() { + type C = ark_bn254::G1Affine; + type F = MontyField<4>; + const D: usize = 32; + + fn bp(bits: u32) -> BinaryPoly { + BinaryPoly::::from(bits) + } + + let width = 32; + let (ck, _) = HyraxPCS::::setup( + width, + b"zinc-plus-hyrax-partial-row-test", + HyraxBlindingMode::Unblinded, + ) + .unwrap(); + let polys = vec![DenseMultilinearExtension::from_evaluations_vec( + 4, + (0..16).map(|idx| bp((idx * 17 + 11) as u32)).collect(), + bp(0), + )]; + + let (prover_data, commitment) = + as PCS, D>>::commit(&ck, &polys).unwrap(); + + assert_eq!(prover_data.num_rows, 1); + assert_eq!(commitment.num_rows, 1); + assert_eq!(commitment.comm.len(), D); + for (lane, comm) in commitment.comm.iter().enumerate() { + let values = polys[0] + .evaluations + .iter() + .map(|eval| eval.coeff(lane)) + .collect::>(); + let expected = if values.iter().copied().any(|bit| bit) { + BoolSubsetMsm::<6>::msm_bool_row(&ck.msm_ck, &values, false).unwrap() + } else { + ::Group::zero() + }; + assert_eq!(*comm, expected); + } + } + #[test] fn hyrax_rejects_blinding_mode_mismatch() { let result = binary_hyrax_open_verify_round_trip_with_modes( diff --git a/zip-plus/src/pcs/msm_commitment.rs b/zip-plus/src/pcs/msm_commitment.rs index 192513cd..569b63d7 100644 --- a/zip-plus/src/pcs/msm_commitment.rs +++ b/zip-plus/src/pcs/msm_commitment.rs @@ -10,8 +10,14 @@ use std::{ use ark_ec::{AffineRepr, CurveGroup}; use ark_ff::{AdditiveGroup, One, PrimeField, UniformRand, Zero}; +use crypto_primitives::{IntRing, crypto_bigint_int::Int}; use num_integer::Integer; +use num_traits::Zero as NumZero; use thiserror::Error; +use zinc_utils::{cfg_chunks, cfg_iter}; + +#[cfg(feature = "parallel")] +use rayon::prelude::*; const DEFAULT_BOOL_WINDOW_BITS: usize = 6; @@ -58,6 +64,8 @@ pub enum MsmError { CommitmentShapeMismatch { expected: usize, actual: usize }, #[error("MSM window size must be in 1..usize::BITS, got {0}")] InvalidWindowBits(usize), + #[error("cannot commit minimum signed integer value")] + SignedIntegerMinimum, } pub trait RowMsmStrategy @@ -77,6 +85,7 @@ where pub struct BoolSubsetMsm; pub struct U8BucketMsm; pub struct ScalarPippengerMsm; +pub struct SignedIntPippengerMsm; #[derive(Clone, Debug)] struct BoolWindowTable { @@ -86,8 +95,7 @@ struct BoolWindowTable { impl BoolWindowTable { fn new(bases: &[C], window_bits: usize) -> Self { - let built = bases - .chunks(window_bits) + let built = cfg_chunks!(bases, window_bits) .map(|window| { let len = window.len(); let table_len = 1usize << len; @@ -105,7 +113,30 @@ impl BoolWindowTable { Self { tables, lens } } - fn msm_row(&self, values: &[bool], window_bits: usize) -> C::Group { + fn msm_row( + &self, + values: &[bool], + window_bits: usize, + _use_parallelism_internally: bool, + ) -> C::Group { + #[cfg(feature = "parallel")] + if _use_parallelism_internally && self.lens.len() > 1 { + return self + .lens + .par_iter() + .copied() + .enumerate() + .map(|(window_idx, len)| { + let offset = window_idx * window_bits; + if offset >= values.len() { + return C::Group::zero(); + } + let end = (offset + len).min(values.len()); + self.tables[window_idx][bit_mask(&values[offset..end])] + }) + .reduce(C::Group::zero, |acc, point| acc + point); + } + let mut acc = C::Group::zero(); for (window_idx, len) in self.lens.iter().copied().enumerate() { let offset = window_idx * window_bits; @@ -181,16 +212,15 @@ impl MsmCommitmentEngine { }); } - let mut comm = Vec::with_capacity(expected_rows); - for (row_idx, row) in values.chunks(ck.num_cols).enumerate() { - let mut row_comm = if row.iter().copied().all(S::is_zero) { - C::Group::zero() - } else { - S::msm_row(ck, row)? - }; - row_comm += ck.h * blind.blind[row_idx]; - comm.push(row_comm); - } + S::precompute_ck(ck); + let comm = cfg_chunks!(values, ck.num_cols) + .enumerate() + .map(|(row_idx, row)| { + let mut row_comm = commit_row::(ck, row)?; + row_comm += ck.h * blind.blind[row_idx]; + Ok(row_comm) + }) + .collect::, _>>()?; Ok(MsmCommitment { comm }) } @@ -204,15 +234,11 @@ impl MsmCommitmentEngine { S: RowMsmStrategy, { let expected_rows = num_rows(values.len(), ck.num_cols)?; - let mut comm = Vec::with_capacity(expected_rows); - for row in values.chunks(ck.num_cols) { - let row_comm = if row.iter().copied().all(S::is_zero) { - C::Group::zero() - } else { - S::msm_row(ck, row)? - }; - comm.push(row_comm); - } + S::precompute_ck(ck); + let comm = cfg_chunks!(values, ck.num_cols) + .map(|row| commit_row::(ck, row)) + .collect::, _>>()?; + debug_assert_eq!(comm.len(), expected_rows); Ok(MsmCommitment { comm }) } @@ -245,7 +271,7 @@ impl MsmCommitmentEngine { }); } - let comm = blind.blind.iter().map(|r| ck.h * r).collect(); + let comm = cfg_iter!(blind.blind).map(|r| ck.h * r).collect(); Ok(MsmCommitment { comm }) } @@ -276,6 +302,28 @@ impl RowMsmStrategy } fn msm_row(ck: &MsmCommitmentKey, values: &[bool]) -> Result { + Self::msm_bool_row(ck, values, false) + } + + fn is_zero(value: bool) -> bool { + !value + } + + fn to_scalar(value: bool) -> C::ScalarField { + if value { + C::ScalarField::one() + } else { + C::ScalarField::zero() + } + } +} + +impl BoolSubsetMsm { + pub(crate) fn msm_bool_row( + ck: &MsmCommitmentKey, + values: &[bool], + use_parallelism_internally: bool, + ) -> Result { validate_row_len(ck, values.len())?; validate_window_bits(WINDOW_BITS)?; @@ -283,7 +331,7 @@ impl RowMsmStrategy return Ok(ck .bool_tables_6 .get_or_init(|| BoolWindowTable::new(&ck.bases, DEFAULT_BOOL_WINDOW_BITS)) - .msm_row(values, DEFAULT_BOOL_WINDOW_BITS)); + .msm_row(values, DEFAULT_BOOL_WINDOW_BITS, use_parallelism_internally)); } let mut acc = C::Group::zero(); @@ -295,18 +343,6 @@ impl RowMsmStrategy } Ok(acc) } - - fn is_zero(value: bool) -> bool { - !value - } - - fn to_scalar(value: bool) -> C::ScalarField { - if value { - C::ScalarField::one() - } else { - C::ScalarField::zero() - } - } } impl RowMsmStrategy for U8BucketMsm { @@ -356,6 +392,24 @@ impl RowMsmStrategy for ScalarPippengerMsm { } } +impl RowMsmStrategy> for SignedIntPippengerMsm { + fn precompute_ck(_ck: &MsmCommitmentKey) {} + + fn msm_row(ck: &MsmCommitmentKey, values: &[Int]) -> Result { + validate_row_len(ck, values.len())?; + signed_int_window_pippenger::(values, &ck.bases[..values.len()]) + } + + fn is_zero(value: Int) -> bool { + NumZero::is_zero(&value) + } + + fn to_scalar(value: Int) -> C::ScalarField { + signed_int_to_scalar::(&value) + .expect("signed integer lane value must fit scalar conversion") + } +} + fn num_rows(n: usize, width: usize) -> Result { if width == 0 { return Err(MsmError::InvalidWidth); @@ -383,6 +437,23 @@ fn validate_window_bits(window_bits: usize) -> Result<(), MsmError> { Ok(()) } +fn commit_row(ck: &MsmCommitmentKey, row: &[V]) -> Result +where + C: AffineRepr, + V: Copy + Send + Sync, + S: RowMsmStrategy, +{ + let effective_len = row + .iter() + .rposition(|value| !S::is_zero(*value)) + .map_or(0, |pos| pos + 1); + if effective_len == 0 { + Ok(C::Group::zero()) + } else { + S::msm_row(ck, &row[..effective_len]) + } +} + fn bit_mask(bits: &[bool]) -> usize { bits.iter().enumerate().fold( 0usize, @@ -465,6 +536,94 @@ fn signed_window_pippenger( Ok(acc) } +fn signed_int_window_pippenger( + values: &[Int], + bases: &[C], +) -> Result { + if values.len() != bases.len() { + return Err(MsmError::BaseCountMismatch { + expected: values.len(), + actual: bases.len(), + }); + } + if values.is_empty() { + return Ok(C::Group::zero()); + } + + let mut max_bits = 0usize; + for value in values { + let (abs, _) = signed_int_abs(value)?; + max_bits = max_bits.max(bit_len_from_words(abs.as_uint().as_words())); + } + if max_bits == 0 { + return Ok(C::Group::zero()); + } + + let window_bits = scalar_window_bits(values.len()).min(max_bits).max(1); + validate_window_bits(window_bits)?; + + let segments = ::div_ceil(&max_bits, &window_bits); + let bucket_len = (1usize << window_bits) - 1; + let mut positive_buckets = vec![C::Group::zero(); bucket_len]; + let mut negative_buckets = vec![C::Group::zero(); bucket_len]; + + let mut acc = C::Group::zero(); + for segment in (0..segments).rev() { + for _ in 0..window_bits { + acc.double_in_place(); + } + + for bucket in &mut positive_buckets { + *bucket = C::Group::zero(); + } + for bucket in &mut negative_buckets { + *bucket = C::Group::zero(); + } + + let offset = segment * window_bits; + for (value, base) in values.iter().zip(bases.iter()) { + let (abs, is_negative) = signed_int_abs(value)?; + let digit = window_value_from_words(abs.as_uint().as_words(), offset, window_bits); + if digit != 0 { + if is_negative { + negative_buckets[digit - 1] += base; + } else { + positive_buckets[digit - 1] += base; + } + } + } + + acc += bucket_running_sum(&positive_buckets); + acc -= bucket_running_sum(&negative_buckets); + } + + Ok(acc) +} + +fn signed_int_abs(value: &Int) -> Result<(Int, bool), MsmError> { + if value.is_negative() { + let abs = value.checked_abs().ok_or(MsmError::SignedIntegerMinimum)?; + Ok((abs, true)) + } else { + Ok((*value, false)) + } +} + +fn signed_int_to_scalar( + value: &Int, +) -> Result { + let (abs, is_negative) = signed_int_abs(value)?; + let mut bytes = Vec::with_capacity(LIMBS * core::mem::size_of::()); + for word in abs.as_uint().as_words() { + bytes.extend_from_slice(&word.to_le_bytes()); + } + let mut scalar = C::ScalarField::from_le_bytes_mod_order(&bytes); + if is_negative && !scalar.is_zero() { + scalar = -scalar; + } + Ok(scalar) +} + fn scalar_window_bits(n: usize) -> usize { if n < 4 { 1 @@ -475,6 +634,16 @@ fn scalar_window_bits(n: usize) -> usize { } } +fn bit_len_from_words(words: &[crypto_bigint::Word]) -> usize { + let word_bits = core::mem::size_of::() * 8; + for (idx, word) in words.iter().copied().enumerate().rev() { + if word != 0 { + return idx * word_bits + word_bits - word.leading_zeros() as usize; + } + } + 0 +} + fn window_value_from_limbs(limbs: &[u64], start: usize, width: usize) -> usize { (0..width).fold(0usize, |value, bit_idx| { let absolute_bit = start + bit_idx; @@ -492,6 +661,28 @@ fn window_value_from_limbs(limbs: &[u64], start: usize, width: usize) -> usize { }) } +fn window_value_from_words( + words: &[crypto_bigint::Word], + start: usize, + width: usize, +) -> usize { + let word_bits = core::mem::size_of::() * 8; + (0..width).fold(0usize, |value, bit_idx| { + let absolute_bit = start + bit_idx; + let word_idx = absolute_bit / word_bits; + let word_bit = absolute_bit % word_bits; + if words + .get(word_idx) + .map(|word| ((word >> word_bit) & 1) == 1) + .unwrap_or(false) + { + value | (1usize << bit_idx) + } else { + value + } + }) +} + #[cfg(test)] mod tests { use super::*; @@ -550,6 +741,13 @@ mod tests { .collect() } + fn scalars_from_int(values: &[Int]) -> Vec { + values + .iter() + .map(|value| signed_int_to_scalar::(value).expect("valid test int")) + .collect() + } + fn naive_scalar_commit( ck: &MsmCommitmentKey, values: &[Fr], @@ -706,6 +904,29 @@ mod tests { } } + #[test] + fn signed_int_commit_matches_scalar_commit_for_small_values() { + for width in [8, 32, 64] { + let (ck, _) = setup(width); + let n = width * 2 + 5; + let values = (0..n) + .map(|idx| Int::<1>::from((idx as i64 % 31) - 15)) + .collect::>(); + let scalars = scalars_from_int(&values); + let blind = blind(width, n); + + let int_comm = + MsmCommitmentEngine::::commit_with::, SignedIntPippengerMsm>( + &ck, &values, &blind, + ) + .expect("signed int commit must succeed"); + let scalar_comm = MsmCommitmentEngine::::commit(&ck, &scalars, &blind) + .expect("scalar commit must succeed"); + + assert_eq!(int_comm, scalar_comm); + } + } + #[test] fn commit_zeros_matches_strategy_zero_paths() { let width = 32; From 6be419a936a16db5e1fc22800632e99585bb3d2a Mon Sep 17 00:00:00 2001 From: John Wu Date: Sat, 6 Jun 2026 19:28:54 -0700 Subject: [PATCH 18/49] Refactor production SHA ideal and multipoint helpers --- piop/src/ideal_check.rs | 3 +- piop/src/neutron_nova/mod.rs | 7 +- piop/src/neutron_nova/projection_sha.rs | 93 +++++++++++++++++++++++-- protocol/src/lib.rs | 1 + protocol/src/multipoint_reduction.rs | 62 +++++++++++++++++ protocol/src/production_sha.rs | 74 ++++---------------- protocol/src/prover.rs | 17 ++--- protocol/src/verifier.rs | 14 ++-- test-uair/src/sha_ecdsa.rs | 7 +- 9 files changed, 193 insertions(+), 85 deletions(-) create mode 100644 protocol/src/multipoint_reduction.rs diff --git a/piop/src/ideal_check.rs b/piop/src/ideal_check.rs index 68c1a717..4f1ba7d4 100644 --- a/piop/src/ideal_check.rs +++ b/piop/src/ideal_check.rs @@ -3,14 +3,13 @@ mod batched_ideal_check; mod combined_poly_builder; mod structs; -pub(crate) use batched_ideal_check::batched_ideal_check; +pub use batched_ideal_check::{BatchedIdealCheckError, batched_ideal_check}; pub use structs::*; #[cfg(feature = "parallel")] use rayon::prelude::*; use crate::projections::{ColumnMajorTrace, RowMajorTrace, ScalarMap}; -use batched_ideal_check::*; use crypto_primitives::PrimeField; use num_traits::ConstZero; use thiserror::Error; diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index caefaa0b..caf2ff79 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -38,8 +38,9 @@ pub use projection_sha::{ production_sha_nonzero_families, production_sha_nonzero_ideals, reconstruct_virtual_ch_maj_at_row, scalarize_trace_words, sha_int_at_point, sha_linear_residual_row_value, sha_linear_residual_sum, sha_public_at_point, - sha_scalarized_word_at_point, sha_word_bits_at_point, verify_folded_row_sumcheck_claim, - verify_folded_scalarization_links, verify_folded_scalarization_links_at_point, - verify_folded_shifted_scalarization_link_at_point, + sha_scalarized_word_at_point, sha_word_bits_at_point, validate_fresh_sha_ideal_polys_canonical, + verify_folded_row_sumcheck_claim, verify_folded_scalarization_links, + verify_folded_scalarization_links_at_point, verify_folded_shifted_scalarization_link_at_point, + verify_fresh_sha_ideal_polys, }; pub use sumfold::{LinearInstanceClaims, LinearPrefixTable, SumFoldError}; diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 723e61a4..76cc8e64 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -412,6 +412,8 @@ pub enum ShaProjectionError { FoldedRowClaimMismatch, #[error("booleanity bit index out of range: {bit}")] BitIndexOutOfRange { bit: usize }, + #[error("non-canonical proof object: {0}")] + NonCanonicalProofObject(&'static str), #[error("ideal membership check failed")] IdealMembership, #[error("polynomial evaluation failed: {0}")] @@ -441,6 +443,62 @@ pub fn production_sha_nonzero_ideals( ] } +fn production_sha_ideal_max_degree(family: ShaResidualFamily) -> Result { + match family { + ShaResidualFamily::R0BigSigmaA | ShaResidualFamily::R1BigSigmaE => Ok(61), + ShaResidualFamily::R4Schedule + | ShaResidualFamily::R5UpdateA + | ShaResidualFamily::R6UpdateE + | ShaResidualFamily::R9FeedForwardA + | ShaResidualFamily::R10FeedForwardE => Ok(31), + _ => Err(ShaProjectionError::NonCanonicalProofObject( + "unexpected nonzero SHA ideal family", + )), + } +} + +pub fn validate_fresh_sha_ideal_polys_canonical( + ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], +) -> Result<(), ShaProjectionError> +where + F: PrimeField, +{ + for instance in ideal_polys { + for (slot, poly) in instance.iter().enumerate() { + if poly.coeffs.last().is_some_and(F::is_zero) { + return Err(ShaProjectionError::NonCanonicalProofObject( + "fresh ideal polynomial has trailing zero coefficients", + )); + } + + let family = production_sha_nonzero_families()[slot]; + let max_degree = production_sha_ideal_max_degree(family)?; + if poly.coeffs.len() > max_degree + 1 { + return Err(ShaProjectionError::NonCanonicalProofObject( + "fresh ideal polynomial exceeds production degree cap", + )); + } + } + } + Ok(()) +} + +pub fn verify_fresh_sha_ideal_polys( + ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], + field_cfg: &F::Config, +) -> Result<(), ShaProjectionError> +where + F: PrimeField, +{ + validate_fresh_sha_ideal_polys_canonical(ideal_polys)?; + + let ideals = production_sha_nonzero_ideals(field_cfg); + for values in ideal_polys { + batched_ideal_check(&ideals, values).map_err(|_err| ShaProjectionError::IdealMembership)?; + } + Ok(()) +} + pub fn build_sha_ideal_values_at_point( trace: &ProjectedShaTrace, public: &ProjectedShaPublic, @@ -515,10 +573,7 @@ pub fn check_fresh_sha_ideal_cache( where F: PrimeField, { - for values in &cache.ideal_polys { - check_sha_ideal_values(values, field_cfg)?; - } - Ok(()) + verify_fresh_sha_ideal_polys(&cache.ideal_polys, field_cfg) } pub fn evaluate_fresh_sha_targets( @@ -2108,6 +2163,36 @@ mod tests { )); } + #[test] + fn fresh_sha_ideal_polys_are_verified_by_reusable_helper() { + let cfg = test_config(); + let valid_zero = vec![std::array::from_fn(|_| { + DynamicPolynomialF::new(Vec::::new()) + })]; + verify_fresh_sha_ideal_polys(&valid_zero, &cfg).expect("zero ideal set passes"); + + let mut tampered_x_minus_two = valid_zero.clone(); + tampered_x_minus_two[0][2] = DynamicPolynomialF::new_trimmed([f(1)]); + assert!(matches!( + verify_fresh_sha_ideal_polys(&tampered_x_minus_two, &cfg), + Err(ShaProjectionError::IdealMembership) + )); + + let mut trailing_zero = valid_zero.clone(); + trailing_zero[0][0] = DynamicPolynomialF::new(vec![f(1), F::zero_with_cfg(&cfg)]); + assert!(matches!( + verify_fresh_sha_ideal_polys(&trailing_zero, &cfg), + Err(ShaProjectionError::NonCanonicalProofObject(_)) + )); + + let mut high_degree = valid_zero; + high_degree[0][2] = DynamicPolynomialF::new(vec![f(1); 33]); + assert!(matches!( + verify_fresh_sha_ideal_polys(&high_degree, &cfg), + Err(ShaProjectionError::NonCanonicalProofObject(_)) + )); + } + #[test] fn scalarization_links_check_folded_words() { let cfg = test_config(); diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 7f78d0a6..bf7eb9e0 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -18,6 +18,7 @@ //! - Step 7: Zip+ PCS open/verify at r_0 pub mod fixed_prime; +mod multipoint_reduction; pub mod pcs; pub mod production_sha; pub mod prover; diff --git a/protocol/src/multipoint_reduction.rs b/protocol/src/multipoint_reduction.rs new file mode 100644 index 00000000..e4afb911 --- /dev/null +++ b/protocol/src/multipoint_reduction.rs @@ -0,0 +1,62 @@ +use crypto_primitives::FromPrimitiveWithConfig; +use num_traits::Zero; +use zinc_piop::multipoint_eval::{ + MultipointEval, MultipointEvalError, Proof as MultipointEvalProof, + Subclaim as MultipointSubclaim, +}; +use zinc_poly::mle::DenseMultilinearExtension; +use zinc_transcript::traits::{ConstTranscribable, Transcript}; +use zinc_uair::ShiftSpec; +use zinc_utils::{ + delayed_reduction::DelayedFieldProductSum, inner_transparent_field::InnerTransparentField, +}; + +pub(crate) fn prove_multipoint_reduction( + transcript: &mut impl Transcript, + trace_mles: &[DenseMultilinearExtension], + eval_point: &[F], + up_evals: &[F], + down_evals: &[F], + shifts: &[ShiftSpec], + field_cfg: &F::Config, +) -> Result<(MultipointEvalProof, Vec), MultipointEvalError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + Zero + Default + Send + Sync, + F::Modulus: ConstTranscribable, +{ + let (proof, state) = MultipointEval::prove_as_subprotocol( + transcript, trace_mles, eval_point, up_evals, down_evals, shifts, field_cfg, + )?; + Ok((proof, state.eval_point)) +} + +pub(crate) fn verify_multipoint_reduction( + transcript: &mut impl Transcript, + proof: MultipointEvalProof, + eval_point: &[F], + up_evals: &[F], + down_evals: &[F], + shifts: &[ShiftSpec], + num_vars: usize, + field_cfg: &F::Config, +) -> Result, MultipointEvalError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + Zero + Default + Send + Sync, + F::Modulus: ConstTranscribable, +{ + MultipointEval::verify_as_subprotocol( + transcript, proof, eval_point, up_evals, down_evals, shifts, num_vars, field_cfg, + ) +} diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 66e36ee1..79026998 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -8,6 +8,7 @@ use std::io::Cursor; use crate::{ ZincTypes, + multipoint_reduction::{prove_multipoint_reduction, verify_multipoint_reduction}, pcs::{ AllHyraxPCSTypes, PCSCommitments, PCSParams, PCSProverData, PCSVerifierParams, ProductionShaPCS, ZincPCSTypes, @@ -30,9 +31,9 @@ use zinc_piop::{ build_dense_sha_sumfold_group, build_expression_folded_row_sumcheck_group, build_folded_row_sumcheck_group, build_fresh_sha_ideal_cache, evaluate_fresh_sha_targets, finalize_sha_sumfold, fold_projected_sha_traces, folded_row_integrand_sum, - production_sha_booleanity_sources, production_sha_nonzero_families, - production_sha_nonzero_ideals, sha_int_at_point, sha_public_at_point, - sha_scalarized_word_at_point, sha_word_bits_at_point, verify_folded_row_sumcheck_claim, + production_sha_booleanity_sources, production_sha_nonzero_families, sha_int_at_point, + sha_public_at_point, sha_scalarized_word_at_point, sha_word_bits_at_point, + verify_folded_row_sumcheck_claim, verify_fresh_sha_ideal_polys, }, sumcheck::{ SumCheckError, @@ -50,7 +51,6 @@ use zinc_poly::{ use zinc_transcript::Blake3Transcript; use zinc_transcript::traits::{ConstTranscribable, Transcribable, Transcript}; use zinc_uair::ShiftSpec; -use zinc_uair::ideal::IdealCheck; use zinc_utils::{ delayed_reduction::DelayedFieldProductSum, inner_transparent_field::InnerTransparentField, }; @@ -481,18 +481,7 @@ pub fn check_fresh_sha_ideal_membership( where F: PrimeField, { - validate_fresh_sha_ideal_polys_canonical(ideal_polys)?; - let ideals = production_sha_nonzero_ideals(field_cfg); - for values in ideal_polys { - for (ideal, value) in ideals.iter().zip(values.iter()) { - if !ideal - .contains(value) - .map_err(|_| ProductionShaError::IdealMembership)? - { - return Err(ProductionShaError::IdealMembership); - } - } - } + verify_fresh_sha_ideal_polys(ideal_polys, field_cfg)?; Ok(()) } @@ -670,43 +659,6 @@ where } } -fn validate_fresh_sha_ideal_polys_canonical( - ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], -) -> Result<(), ProductionShaError> -where - F: PrimeField, -{ - for instance in ideal_polys { - for (slot, poly) in instance.iter().enumerate() { - if poly.coeffs.last().is_some_and(F::is_zero) { - return Err(ProductionShaError::NonCanonicalProofObject( - "fresh ideal polynomial has trailing zero coefficients", - )); - } - let family = production_sha_nonzero_families()[slot]; - let max_degree = match family { - ShaResidualFamily::R0BigSigmaA | ShaResidualFamily::R1BigSigmaE => 61, - ShaResidualFamily::R4Schedule - | ShaResidualFamily::R5UpdateA - | ShaResidualFamily::R6UpdateE - | ShaResidualFamily::R9FeedForwardA - | ShaResidualFamily::R10FeedForwardE => 31, - _ => { - return Err(ProductionShaError::NonCanonicalProofObject( - "unexpected nonzero SHA ideal family", - )); - } - }; - if poly.coeffs.len() > max_degree + 1 { - return Err(ProductionShaError::NonCanonicalProofObject( - "fresh ideal polynomial exceeds production degree cap", - )); - } - } - } - Ok(()) -} - fn ensure_production_sha_word_degree() -> Result<(), ProductionShaError> where F: PrimeField, @@ -2180,7 +2132,7 @@ where &layout, field_cfg, )?; - let (proof, state) = MultipointEval::prove_as_subprotocol( + prove_multipoint_reduction( transcript, &trace_mles, r_star, @@ -2188,8 +2140,8 @@ where &down_evals, &shift_specs, field_cfg, - )?; - Ok((proof, state.eval_point)) + ) + .map_err(ProductionShaError::from) } pub fn verify_sha_endpoint_multipoint( @@ -2221,7 +2173,7 @@ where &layout, field_cfg, )?; - let subclaim = MultipointEval::verify_as_subprotocol( + let subclaim = verify_multipoint_reduction( transcript, proof.clone(), r_star, @@ -3629,7 +3581,9 @@ mod tests { ideals[0][0] = DynamicPolynomialF::new(vec![f(1), F::zero_with_cfg(&field_cfg)]); assert!(matches!( check_fresh_sha_ideal_membership(&ideals, &field_cfg), - Err(ProductionShaError::NonCanonicalProofObject(_)) + Err(ProductionShaError::ShaProjection( + ShaProjectionError::NonCanonicalProofObject(_) + )) )); let mut high_degree = vec![std::array::from_fn(|_| { @@ -3638,7 +3592,9 @@ mod tests { high_degree[0][2] = DynamicPolynomialF::new(vec![f(1); 33]); assert!(matches!( check_fresh_sha_ideal_membership(&high_degree, &field_cfg), - Err(ProductionShaError::NonCanonicalProofObject(_)) + Err(ProductionShaError::ShaProjection( + ShaProjectionError::NonCanonicalProofObject(_) + )) )); } diff --git a/protocol/src/prover.rs b/protocol/src/prover.rs index 79f07fb5..294d2a65 100644 --- a/protocol/src/prover.rs +++ b/protocol/src/prover.rs @@ -13,7 +13,7 @@ use zinc_piop::{ compute_shifted_bit_slice_evals_streaming, finalize_booleanity_prover, prepare_booleanity_group, }, - multipoint_eval::{MultipointEval, Proof as MultipointEvalProof}, + multipoint_eval::Proof as MultipointEvalProof, projections::{ ColumnMajorTrace, ProjectedTrace, RowMajorTrace, ScalarMap, evaluate_trace_to_column_mles_fast, project_scalars, project_scalars_to_field, @@ -48,7 +48,10 @@ use zip_plus::{ pcs_transcript::PcsProverTranscript, }; -use crate::pcs::{AllZipPCSTypes, PCSCommitments, PCSParams, PCSProverData, ZincPCSTypes}; +use crate::{ + multipoint_reduction::prove_multipoint_reduction, + pcs::{AllZipPCSTypes, PCSCommitments, PCSParams, PCSProverData, ZincPCSTypes}, +}; /// Drop the witness binary_poly columns the UAIR opted out of (sorted, /// dedup'd `skip_indices` relative to `witness_cols`) and return the @@ -994,7 +997,7 @@ impl_with_type_bounds!(ProverSumchecked let mut up_evals = self.cpr_proof.up_evals.clone(); up_evals.extend(self.cpr_proof.bit_op_down_evals.iter().cloned()); - let (mp_proof, mp_prover_state) = MultipointEval::prove_as_subprotocol( + let (mp_proof, r_0) = prove_multipoint_reduction( &mut self.base.pcs_transcript.fs_transcript, &sources, &self.cpr_eval_point, @@ -1013,7 +1016,7 @@ impl_with_type_bounds!(ProverSumchecked combined_sumcheck: self.combined_sumcheck, lookup_proof: self.lookup_proof, mp_proof, - r_0: mp_prover_state.eval_point, + r_0, }) } }); @@ -1658,7 +1661,7 @@ where sources.extend(bit_op_mles); let mut up_evals_with_bit_op = cpr_proof.up_evals.clone(); up_evals_with_bit_op.extend(cpr_proof.bit_op_down_evals.iter().cloned()); - let (mp_proof, mp_prover_state) = MultipointEval::prove_as_subprotocol( + let (mp_proof, r_0) = prove_multipoint_reduction( &mut pcs_transcript.fs_transcript, &sources, &cpr_eval_point, @@ -1667,7 +1670,6 @@ where uair_signature.shifts(), &field_cfg, )?; - let r_0 = mp_prover_state.eval_point; // ── Step 6: Lift-and-project + sample γ for folding ───────────────── let lifted_evals = @@ -2441,7 +2443,7 @@ where sources.extend(bit_op_mles); let mut up_evals_with_bit_op = cpr_proof.up_evals.clone(); up_evals_with_bit_op.extend(cpr_proof.bit_op_down_evals.iter().cloned()); - let (mp_proof, mp_prover_state) = MultipointEval::prove_as_subprotocol( + let (mp_proof, r_0) = prove_multipoint_reduction( &mut pcs_transcript.fs_transcript, &sources, &cpr_eval_point, @@ -2450,7 +2452,6 @@ where uair_signature.shifts(), &field_cfg, )?; - let r_0 = mp_prover_state.eval_point; if let Some(t) = timings.as_mut() { t.step5_multipoint_eval = _t_step5.elapsed(); } diff --git a/protocol/src/verifier.rs b/protocol/src/verifier.rs index fd30aaaf..fd86a249 100644 --- a/protocol/src/verifier.rs +++ b/protocol/src/verifier.rs @@ -37,7 +37,8 @@ use zinc_utils::{ add, cfg_join, delayed_reduction::{DelayedFieldProductSum, MontgomeryLimbs}, from_ref::FromRef, - inner_transparent_field::InnerTransparentField, mul_by_scalar::MulByScalar, + inner_transparent_field::InnerTransparentField, + mul_by_scalar::MulByScalar, projectable_to_field::ProjectableToField, }; use zip_plus::{ @@ -48,7 +49,10 @@ use zip_plus::{ pcs_transcript::PcsVerifierTranscript, }; -use crate::pcs::{AllZipPCSTypes, PCSCommitments, PCSVerifierParams, ZincPCSTypes}; +use crate::{ + multipoint_reduction::verify_multipoint_reduction, + pcs::{AllZipPCSTypes, PCSCommitments, PCSVerifierParams, ZincPCSTypes}, +}; /// Drop the witness binary_poly column evals the UAIR opted out of /// (sorted, dedup'd `skip_indices` relative to the witness slice). The @@ -840,7 +844,7 @@ where let mut up_evals_with_bit_op = self.cpr_subclaim.up_evals.clone(); up_evals_with_bit_op.extend(self.cpr_subclaim.bit_op_down_evals.iter().cloned()); - let mp_subclaim = MultipointEval::verify_as_subprotocol( + let mp_subclaim = verify_multipoint_reduction( &mut self.base.pcs_transcript.fs_transcript, self.proof_multipoint_eval, &cpr_eval_point, @@ -1486,7 +1490,7 @@ where let cpr_eval_point = cpr_subclaim.evaluation_point.clone(); let mut up_evals_with_bit_op = cpr_subclaim.up_evals.clone(); up_evals_with_bit_op.extend(cpr_subclaim.bit_op_down_evals.iter().cloned()); - let mp_subclaim = MultipointEval::verify_as_subprotocol( + let mp_subclaim = verify_multipoint_reduction( &mut pcs_transcript.fs_transcript, proof.multipoint_eval, &cpr_eval_point, @@ -2219,7 +2223,7 @@ where let cpr_eval_point = cpr_subclaim.evaluation_point.clone(); let mut up_evals_with_bit_op = cpr_subclaim.up_evals.clone(); up_evals_with_bit_op.extend(cpr_subclaim.bit_op_down_evals.iter().cloned()); - let mp_subclaim = MultipointEval::verify_as_subprotocol( + let mp_subclaim = verify_multipoint_reduction( &mut pcs_transcript.fs_transcript, proof.multipoint_eval, &cpr_eval_point, diff --git a/test-uair/src/sha_ecdsa.rs b/test-uair/src/sha_ecdsa.rs index 1ecf94f8..5aea66b8 100644 --- a/test-uair/src/sha_ecdsa.rs +++ b/test-uair/src/sha_ecdsa.rs @@ -75,14 +75,13 @@ use zinc_uair::{ ideal::rotation::RotationIdeal, }; +#[cfg(test)] +use crate::ecdsa::FINAL_ROW as ECDSA_FINAL_ROW; use crate::{ - GenerateRandomTrace, - ecdsa, + GenerateRandomTrace, ecdsa, ecdsa_doubling::{EC_FP_INT_LIMBS, EcdsaFpRing}, sha256::{self, Sha256CompressionSliceUair, Sha256Ideal}, }; -#[cfg(test)] -use crate::ecdsa::FINAL_ROW as ECDSA_FINAL_ROW; use crypto_primitives::crypto_bigint_int::Int; From eaba4cc0f93fc3530911d639ba7804a76d6837de Mon Sep 17 00:00:00 2001 From: John Wu Date: Sat, 6 Jun 2026 23:07:24 -0700 Subject: [PATCH 19/49] Optimize production SHA SumFold hot paths --- piop/Cargo.toml | 3 + piop/benches/neutron_nova_sumfold.rs | 209 +++ piop/src/neutron_nova/mod.rs | 15 +- piop/src/neutron_nova/projection_sha.rs | 1735 +++++++++++++++++++++-- protocol/src/production_sha.rs | 108 +- 5 files changed, 1893 insertions(+), 177 deletions(-) create mode 100644 piop/benches/neutron_nova_sumfold.rs diff --git a/piop/Cargo.toml b/piop/Cargo.toml index 0512764e..2d7ab24f 100644 --- a/piop/Cargo.toml +++ b/piop/Cargo.toml @@ -54,3 +54,6 @@ harness = false name = "multipoint_eval" harness = false +[[bench]] +name = "neutron_nova_sumfold" +harness = false diff --git a/piop/benches/neutron_nova_sumfold.rs b/piop/benches/neutron_nova_sumfold.rs new file mode 100644 index 00000000..9df0303d --- /dev/null +++ b/piop/benches/neutron_nova_sumfold.rs @@ -0,0 +1,209 @@ +#![allow(non_local_definitions)] + +use std::hint::black_box; + +use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; +use crypto_primitives::{Field, FromWithConfig, PrimeField, crypto_bigint_monty::MontyField}; +use zinc_piop::{ + neutron_nova::{ + ProjectedShaPublic, ProjectedShaTrace, SHA_ROW_COUNT, SHA_WORD_BITS, ShaBitSliceColumns, + ShaBooleanitySource, ShaIntCol, ShaIntColumns, ShaPublicCol, ShaPublicColumns, ShaWordCol, + build_dense_sha_sumfold_group, build_production_sha_sumfold_group_owned, + scalarize_trace_words, + }, + sumcheck::multi_degree::MultiDegreeSumcheck, +}; +use zinc_primality::{MillerRabin, PrimalityTest}; +use zinc_transcript::{Blake3Transcript, traits::Transcript}; + +type F = MontyField<4>; + +fn bench_config() -> ::Config +where + MillerRabin: PrimalityTest<::Modulus>, +{ + let mut transcript = Blake3Transcript::new(); + transcript.get_random_field_cfg::::Modulus, MillerRabin>() +} + +fn f(value: u64, cfg: &::Config) -> F { + F::from_with_cfg(value, cfg) +} + +fn synthetic_boolean_trace( + instance_idx: u64, + a: &F, + cfg: &::Config, +) -> ProjectedShaTrace { + let zero = F::zero_with_cfg(cfg); + let mut bits = vec![vec![vec![zero.clone(); SHA_WORD_BITS]; SHA_ROW_COUNT]; ShaWordCol::COUNT]; + for (col_idx, col) in bits.iter_mut().enumerate() { + for (row_idx, row) in col.iter_mut().enumerate() { + for (bit_idx, bit) in row.iter_mut().enumerate() { + let selector = instance_idx + + u64::try_from(col_idx * 17 + row_idx * 3 + bit_idx) + .expect("bench selector fits u64"); + if selector % 2 == 1 { + *bit = f(1, cfg); + } + } + } + } + let bit_slices = ShaBitSliceColumns { columns: bits }; + let scalarized_words = scalarize_trace_words(&bit_slices, a, cfg).unwrap(); + ProjectedShaTrace { + rows: SHA_ROW_COUNT, + bit_slices, + scalarized_words, + int_columns: ShaIntColumns { + columns: vec![vec![zero.clone(); SHA_ROW_COUNT]; ShaIntCol::COUNT], + }, + public_columns: ShaPublicColumns { + columns: vec![vec![zero; SHA_ROW_COUNT]; ShaPublicCol::COUNT], + }, + } +} + +fn zero_public(cfg: &::Config) -> ProjectedShaPublic { + ProjectedShaPublic { + columns: ShaPublicColumns { + columns: vec![vec![F::zero_with_cfg(cfg); SHA_ROW_COUNT]; ShaPublicCol::COUNT], + }, + } +} + +fn booleanity_sources() -> Vec { + vec![ + ShaBooleanitySource::WordBit { + col: ShaWordCol::A, + bit: 0, + }, + ShaBooleanitySource::WordBit { + col: ShaWordCol::A, + bit: 7, + }, + ShaBooleanitySource::WordBit { + col: ShaWordCol::E, + bit: 1, + }, + ShaBooleanitySource::WordBit { + col: ShaWordCol::E, + bit: 9, + }, + ShaBooleanitySource::WordBit { + col: ShaWordCol::W, + bit: 2, + }, + ShaBooleanitySource::WordBit { + col: ShaWordCol::W, + bit: 13, + }, + ShaBooleanitySource::WordBit { + col: ShaWordCol::Sigma0, + bit: 3, + }, + ShaBooleanitySource::WordBit { + col: ShaWordCol::Sigma1, + bit: 5, + }, + ] +} + +#[allow(clippy::too_many_lines)] +fn neutron_nova_sumfold_benches(c: &mut Criterion) { + let cfg = bench_config(); + let ell = 7usize; + let prefix_vars = 2usize; + let a = f(3, &cfg); + let traces = (0..(1usize << ell)) + .map(|idx| synthetic_boolean_trace(u64::try_from(idx).unwrap(), &a, &cfg)) + .collect::>(); + let publics = vec![zero_public(&cfg); traces.len()]; + let beta = vec![ + f(5, &cfg), + f(7, &cfg), + f(11, &cfg), + f(13, &cfg), + f(17, &cfg), + f(19, &cfg), + f(37, &cfg), + ]; + let r_ic = [ + f(2, &cfg), + f(3, &cfg), + f(5, &cfg), + f(7, &cfg), + f(11, &cfg), + f(13, &cfg), + f(17, &cfg), + ]; + let lambda = f(23, &cfg); + let rho = f(29, &cfg); + let xi = f(31, &cfg); + let sources = booleanity_sources(); + + let mut group = c.benchmark_group("NeutronNova SHA SumFold"); + group.sample_size(10); + + group.bench_function(BenchmarkId::new("dense_build_and_prove", ell), |bench| { + bench.iter_batched( + Blake3Transcript::new, + |mut transcript| { + let group = build_dense_sha_sumfold_group( + &traces, &publics, &beta, &r_ic, &a, &lambda, &rho, &xi, &sources, &cfg, + ) + .unwrap(); + let (proof, _) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut transcript, + vec![group], + ell, + &cfg, + ); + black_box(proof.claimed_sums()[0].clone()) + }, + BatchSize::SmallInput, + ); + }); + + group.bench_function( + BenchmarkId::new("production_prefix_tail_build_and_prove", prefix_vars), + |bench| { + bench.iter_batched( + || (traces.clone().into_boxed_slice(), Blake3Transcript::new()), + |(owned_traces, mut transcript)| { + let group = build_production_sha_sumfold_group_owned( + owned_traces, + &publics, + &beta, + &r_ic, + &a, + &lambda, + &rho, + &xi, + &sources, + prefix_vars, + &cfg, + ) + .unwrap(); + let (proof, _) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut transcript, + vec![group], + ell, + &cfg, + ); + black_box(proof.claimed_sums()[0].clone()) + }, + BatchSize::SmallInput, + ); + }, + ); + + group.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = neutron_nova_sumfold_benches +} +criterion_main!(benches); diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index caf2ff79..75b0193a 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -31,13 +31,14 @@ pub use projection_sha::{ ShaIntColumns, ShaProductionIdeal, ShaProjectionError, ShaPublicCol, ShaPublicColumns, ShaResidualFamily, ShaScalarizedRows, ShaSumFoldOutput, ShaWordCol, VirtualChMajValues, build_dense_sha_sumfold_group, build_expression_folded_row_sumcheck_group, - build_folded_row_sumcheck_group, build_fresh_sha_ideal_cache, build_sha_ideal_values_at_point, - check_fresh_sha_ideal_cache, check_sha_ideal_values, evaluate_fresh_sha_targets, - expression_folded_row_sum, finalize_sha_sumfold, fold_projected_sha_traces, - folded_row_integrand_sum, folded_row_integrand_values, production_sha_booleanity_sources, - production_sha_nonzero_families, production_sha_nonzero_ideals, - reconstruct_virtual_ch_maj_at_row, scalarize_trace_words, sha_int_at_point, - sha_linear_residual_row_value, sha_linear_residual_sum, sha_public_at_point, + build_folded_row_sumcheck_group, build_fresh_sha_ideal_cache, + build_production_sha_sumfold_group, build_production_sha_sumfold_group_owned, + build_sha_ideal_values_at_point, check_fresh_sha_ideal_cache, check_sha_ideal_values, + evaluate_fresh_sha_targets, expression_folded_row_sum, finalize_sha_sumfold, + fold_projected_sha_traces, folded_row_integrand_sum, folded_row_integrand_values, + production_sha_booleanity_sources, production_sha_nonzero_families, + production_sha_nonzero_ideals, reconstruct_virtual_ch_maj_at_row, scalarize_trace_words, + sha_int_at_point, sha_linear_residual_row_value, sha_linear_residual_sum, sha_public_at_point, sha_scalarized_word_at_point, sha_word_bits_at_point, validate_fresh_sha_ideal_polys_canonical, verify_folded_row_sumcheck_claim, verify_folded_scalarization_links, verify_folded_scalarization_links_at_point, verify_folded_shifted_scalarization_link_at_point, diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 76cc8e64..4ae0f882 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -7,15 +7,17 @@ //! folded row check over the 128-row SHA domain. use crate::ideal_check::batched_ideal_check; -use crate::neutron_nova::SumFoldError; -use crate::sumcheck::multi_degree::MultiDegreeSumcheckGroup; -use crypto_primitives::PrimeField; +use crate::neutron_nova::{SumFoldError, accumulator::dmr_flush_adds}; +use crate::{ + CombFn, + sumcheck::multi_degree::{MultiDegreeSumcheckGroup, PrefixFastPath, PrefixRoundOutput}, +}; +use crypto_primitives::{PrimeField, crypto_bigint_uint::Uint}; use num_traits::{ConstZero, Zero}; use thiserror::Error; use zinc_poly::{ - EvaluatablePolynomial, mle::DenseMultilinearExtension, - univariate::dynamic::over_field::DynamicPolynomialF, + univariate::dynamic::over_field::{DynamicPolyFInnerProduct, DynamicPolynomialF}, utils::{ArithErrors, build_eq_x_r_vec, eq_eval}, }; use zinc_uair::{ @@ -23,8 +25,12 @@ use zinc_uair::{ ideal_collector::IdealOrZero, }; use zinc_utils::{ - delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, - inner_transparent_field::InnerTransparentField, powers, + UNCHECKED, + delayed_reduction::{DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs}, + from_ref::FromRef, + inner_product::{FieldFieldInnerProduct, InnerProduct}, + inner_transparent_field::InnerTransparentField, + powers, }; pub const SHA_ROW_VARS: usize = 7; @@ -32,6 +38,7 @@ pub const SHA_ROW_COUNT: usize = 128; pub const SHA_WORD_BITS: usize = 32; pub const NUM_SHA_RESIDUAL_FAMILIES: usize = 18; pub const NUM_NONZERO_SHA_FAMILIES: usize = 7; +const SHA_RESIDUAL_EVAL_POWER_COUNT: usize = 62; const NONZERO_SHA_FAMILIES: [ShaResidualFamily; NUM_NONZERO_SHA_FAMILIES] = [ ShaResidualFamily::R0BigSigmaA, @@ -418,6 +425,8 @@ pub enum ShaProjectionError { IdealMembership, #[error("polynomial evaluation failed: {0}")] PolynomialEvaluation(#[from] zinc_poly::EvaluationError), + #[error("inner product failed: {0}")] + InnerProduct(#[from] zinc_utils::inner_product::InnerProductError), #[error("equality table construction failed: {0}")] EqTable(#[from] ArithErrors), #[error("sumfold helper failed: {0}")] @@ -583,21 +592,28 @@ pub fn evaluate_fresh_sha_targets( field_cfg: &F::Config, ) -> Result<(), ShaProjectionError> where - F: PrimeField, + F: DelayedFieldProductSum, { let one = F::one_with_cfg(field_cfg); let zero = F::zero_with_cfg(field_cfg); let lambda_powers = powers(lambda.clone(), one, NUM_SHA_RESIDUAL_FAMILIES); + let a_powers = powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_RESIDUAL_EVAL_POWER_COUNT, + ); cache.taus_at_a.clear(); cache.fresh_targets.clear(); for ideal_polys in &cache.ideal_polys { - let taus: [F; NUM_NONZERO_SHA_FAMILIES] = std::array::from_fn(|idx| { - ideal_polys[idx] - .evaluate_at_point(a) - .expect("field polynomial evaluation cannot fail") - }); + let mut tau_values = Vec::with_capacity(NUM_NONZERO_SHA_FAMILIES); + for poly in ideal_polys { + tau_values.push(evaluate_poly_at_powers_dmr(poly, &a_powers, field_cfg)?); + } + let taus: [F; NUM_NONZERO_SHA_FAMILIES] = tau_values + .try_into() + .unwrap_or_else(|_| unreachable!("exactly seven SHA ideal values were evaluated")); let mut target = zero.clone(); for (slot, family) in NONZERO_SHA_FAMILIES.iter().enumerate() { target += lambda_powers[family.index()].clone() * &taus[slot]; @@ -737,7 +753,7 @@ pub fn scalarize_trace_words( field_cfg: &F::Config, ) -> Result, ShaProjectionError> where - F: PrimeField, + F: MontgomeryLimbs + DelayedFieldProductSum + Send + Sync, { let powers = powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); let mut words = Vec::with_capacity(bit_slices.columns.len()); @@ -760,7 +776,9 @@ where expected: SHA_WORD_BITS, }); } - out_col.push(project_bits(bits, &powers, field_cfg)); + out_col.push(project_binary_bits_conditional_add_dmr( + bits, &powers, field_cfg, + )?); } words.push(out_col); } @@ -799,7 +817,7 @@ where col: col_idx, })?; for row in 0..SHA_ROW_COUNT { - let recombined = project_bits(&bit_col[row], &powers, field_cfg); + let recombined = project_bits_dmr(&bit_col[row], &powers, field_cfg)?; if recombined != scalar_col[row] { return Err(ShaProjectionError::ScalarizationMismatch { col: col_idx }); } @@ -839,16 +857,24 @@ where let row_weights = build_eq_x_r_vec(r_star, field_cfg)?; let powers = powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); let mut word_eval = F::zero_with_cfg(field_cfg); - let mut bit_eval = F::zero_with_cfg(field_cfg); + let mut bit_rows = Vec::with_capacity(SHA_ROW_COUNT); for (row, row_weight) in row_weights.iter().enumerate() { word_eval += row_weight.clone() * scalarized_word_at_shifted_or_zero(trace, col, row, shift, field_cfg)?; - for (bit, power) in powers.iter().enumerate() { - let source_bit = bit_at_shifted_or_zero(trace, col, row, shift, bit, field_cfg)?; - bit_eval += row_weight.clone() * power.clone() * source_bit; + let mut bits = Vec::with_capacity(SHA_WORD_BITS); + for bit in 0..SHA_WORD_BITS { + bits.push(bit_at_shifted_or_zero( + trace, col, row, shift, bit, field_cfg, + )?); } + bit_rows.push(project_bits_dmr(&bits, &powers, field_cfg)?); } + let bit_eval = FieldFieldInnerProduct::inner_product::( + &row_weights, + &bit_rows, + F::zero_with_cfg(field_cfg), + )?; if word_eval != bit_eval { return Err(ShaProjectionError::ScalarizationMismatch { col: col.index() }); @@ -865,6 +891,17 @@ where F: PrimeField, { validate_trace(trace)?; + reconstruct_virtual_ch_maj_at_row_unchecked(trace, row, field_cfg) +} + +fn reconstruct_virtual_ch_maj_at_row_unchecked( + trace: &ProjectedShaTrace, + row: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ if row >= SHA_ROW_COUNT { return Err(ShaProjectionError::RowIndexOutOfRange { row }); } @@ -929,23 +966,45 @@ where F::one_with_cfg(field_cfg), NUM_SHA_RESIDUAL_FAMILIES, ); + let a_powers = powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_RESIDUAL_EVAL_POWER_COUNT, + ); let rho_powers = powers( rho.clone(), F::one_with_cfg(field_cfg), booleanity_sources.len(), ); + let needs_virtuals = sources_need_virtuals(booleanity_sources); let mut out = Vec::with_capacity(SHA_ROW_COUNT); for row in 0..SHA_ROW_COUNT { - let residuals = residual_values_at_row(trace, public, row, a, field_cfg)?; - let mut linear = F::zero_with_cfg(field_cfg); - for family in ShaResidualFamily::ALL { - linear += lambda_powers[family.index()].clone() * &residuals[family.index()]; - } + let linear = sha_linear_residual_row_value_with_powers( + trace, + public, + row, + &a_powers, + &lambda_powers, + field_cfg, + )?; let mut bool_sum = F::zero_with_cfg(field_cfg); + let virtuals = if needs_virtuals { + Some(reconstruct_virtual_ch_maj_at_row_unchecked( + trace, row, field_cfg, + )?) + } else { + None + }; for (idx, source) in booleanity_sources.iter().enumerate() { - let d = booleanity_source_value_at_row(trace, row, source, field_cfg)?; + let d = booleanity_source_value_at_row_with_virtuals( + trace, + row, + source, + virtuals.as_ref(), + field_cfg, + )?; let term = d.clone() * (d - F::one_with_cfg(field_cfg)); bool_sum += rho_powers[idx].clone() * term; } @@ -1037,20 +1096,26 @@ pub fn sha_linear_residual_row_value( field_cfg: &F::Config, ) -> Result where - F: PrimeField, + F: DelayedFieldProductSum, { - let residuals = residual_values_at_row(trace, public, row, a, field_cfg)?; let lambda_powers = powers( lambda.clone(), F::one_with_cfg(field_cfg), NUM_SHA_RESIDUAL_FAMILIES, ); - Ok(residuals - .iter() - .zip(lambda_powers.iter()) - .fold(F::zero_with_cfg(field_cfg), |acc, (residual, weight)| { - acc + weight.clone() * residual - })) + let a_powers = powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_RESIDUAL_EVAL_POWER_COUNT, + ); + sha_linear_residual_row_value_with_powers( + trace, + public, + row, + &a_powers, + &lambda_powers, + field_cfg, + ) } /// Evaluate the row-weighted linear SHA residual scalarization for one @@ -1064,19 +1129,510 @@ pub fn sha_linear_residual_sum( field_cfg: &F::Config, ) -> Result where - F: PrimeField, + F: DelayedFieldProductSum, { validate_trace(trace)?; validate_public(public)?; let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + let lambda_powers = powers( + lambda.clone(), + F::one_with_cfg(field_cfg), + NUM_SHA_RESIDUAL_FAMILIES, + ); + let a_powers = powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_RESIDUAL_EVAL_POWER_COUNT, + ); + sha_linear_residual_sum_with_weights( + trace, + public, + &row_weights, + &a_powers, + &lambda_powers, + field_cfg, + ) +} + +fn sha_linear_residual_sum_with_weights( + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + row_weights: &[F], + a_powers: &[F], + lambda_powers: &[F], + field_cfg: &F::Config, +) -> Result +where + F: DelayedFieldProductSum, +{ let mut sum = F::zero_with_cfg(field_cfg); for (row, row_weight) in row_weights.iter().enumerate() { sum += row_weight.clone() - * sha_linear_residual_row_value(trace, public, row, a, lambda, field_cfg)?; + * sha_linear_residual_row_value_with_powers( + trace, + public, + row, + a_powers, + lambda_powers, + field_cfg, + )?; } Ok(sum) } +fn sha_linear_residual_row_value_with_powers( + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + row: usize, + a_powers: &[F], + lambda_powers: &[F], + field_cfg: &F::Config, +) -> Result +where + F: DelayedFieldProductSum, +{ + let residuals = residual_values_at_row_with_powers(trace, public, row, a_powers, field_cfg)?; + FieldFieldInnerProduct::inner_product::( + &residuals, + lambda_powers, + F::zero_with_cfg(field_cfg), + ) + .map_err(ShaProjectionError::from) +} + +#[derive(Clone, Debug)] +struct BinaryPrefixTailTable { + values: Vec, + prefix_vars: usize, + tail_len: usize, +} + +impl BinaryPrefixTailTable +where + F: PrimeField, +{ + fn new(values: Vec, prefix_vars: usize, tail_len: usize) -> Self { + debug_assert_eq!(values.len(), binary_len(prefix_vars) * tail_len); + Self { + values, + prefix_vars, + tail_len, + } + } + + #[allow(clippy::arithmetic_side_effects)] + fn bind_first_axis(&mut self, r: &F, field_cfg: &F::Config) { + debug_assert!(self.prefix_vars > 0); + let rest_len = binary_len(self.prefix_vars - 1); + let old_prefix_len = binary_len(self.prefix_vars); + let one = F::one_with_cfg(field_cfg); + let one_minus_r = one - r; + let mut next = vec![F::zero_with_cfg(field_cfg); rest_len * self.tail_len]; + + for tail in 0..self.tail_len { + let old_tail_offset = tail * old_prefix_len; + let new_tail_offset = tail * rest_len; + for rest in 0..rest_len { + let base = old_tail_offset + (rest << 1); + next[new_tail_offset + rest] = + self.values[base].clone() * &one_minus_r + self.values[base + 1].clone() * r; + } + } + + self.values = next; + self.prefix_vars -= 1; + } + + #[allow(clippy::arithmetic_side_effects)] + fn value_with_first_axis(&self, rest: usize, tail: usize, x: &F, field_cfg: &F::Config) -> F { + debug_assert!(self.prefix_vars > 0); + let prefix_len = binary_len(self.prefix_vars); + let base = tail * prefix_len + (rest << 1); + let one = F::one_with_cfg(field_cfg); + self.values[base].clone() * (one - x) + self.values[base + 1].clone() * x + } +} + +#[derive(Clone, Debug)] +struct TernaryPrefixTailTable { + values: Vec, + prefix_vars: usize, + tail_len: usize, +} + +impl TernaryPrefixTailTable +where + F: PrimeField, +{ + fn new( + values: Vec, + prefix_vars: usize, + tail_len: usize, + ) -> Result { + debug_assert_eq!(values.len(), checked_ternary_len(prefix_vars)? * tail_len); + Ok(Self { + values, + prefix_vars, + tail_len, + }) + } + + #[allow(clippy::arithmetic_side_effects)] + fn bind_first_axis(&mut self, r: &F, field_cfg: &F::Config) -> Result<(), ShaProjectionError> { + debug_assert!(self.prefix_vars > 0); + let rest_len = checked_ternary_len(self.prefix_vars - 1)?; + let old_prefix_len = checked_ternary_len(self.prefix_vars)?; + let one = F::one_with_cfg(field_cfg); + let one_minus_r = one - r; + let quadratic = r.clone() * (r.clone() - F::one_with_cfg(field_cfg)); + let mut next = vec![F::zero_with_cfg(field_cfg); rest_len * self.tail_len]; + + for tail in 0..self.tail_len { + let old_tail_offset = tail * old_prefix_len; + let new_tail_offset = tail * rest_len; + for rest in 0..rest_len { + let base = old_tail_offset + rest * 3; + next[new_tail_offset + rest] = self.values[base].clone() * &one_minus_r + + self.values[base + 1].clone() * r + + self.values[base + 2].clone() * &quadratic; + } + } + + self.values = next; + self.prefix_vars -= 1; + Ok(()) + } + + #[allow(clippy::arithmetic_side_effects)] + fn value_with_first_axis( + &self, + rest: usize, + tail: usize, + x: &F, + field_cfg: &F::Config, + ) -> Result { + debug_assert!(self.prefix_vars > 0); + let prefix_len = checked_ternary_len(self.prefix_vars)?; + let base = tail * prefix_len + rest * 3; + let one = F::one_with_cfg(field_cfg); + Ok(self.values[base].clone() * (one - x) + + self.values[base + 1].clone() * x + + self.values[base + 2].clone() + * (x.clone() * (x.clone() - F::one_with_cfg(field_cfg)))) + } +} + +struct ShaSumFoldPrefixFastPath { + traces: Box<[ProjectedShaTrace]>, + beta: Vec, + xi: F, + booleanity_sources: Vec, + linear: BinaryPrefixTailTable, + booleanity: TernaryPrefixTailTable, + tail_eq_weights: Vec, + prefix_suffix_eq_weights: Vec>, + total_prefix_vars: usize, + round: usize, + eq_bound: F, +} + +#[derive(Clone, Debug)] +struct TernaryCoeffPlan { + support_mask: usize, + finite_bits: usize, + vertices: Vec<(usize, bool)>, +} + +impl ShaSumFoldPrefixFastPath +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + #[allow(clippy::too_many_arguments)] + fn new( + traces: &[ProjectedShaTrace], + publics: &[ProjectedShaPublic], + beta: &[F], + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + field_cfg: &F::Config, + ) -> Result { + Self::new_owned( + traces.to_vec().into_boxed_slice(), + publics, + beta, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + prefix_vars, + field_cfg, + ) + } + + #[allow(clippy::too_many_arguments)] + fn new_owned( + traces: Box<[ProjectedShaTrace]>, + publics: &[ProjectedShaPublic], + beta: &[F], + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + field_cfg: &F::Config, + ) -> Result { + let ell = validate_sha_sumfold_inputs(&traces, publics, beta)?; + if prefix_vars == 0 || prefix_vars > ell { + return Err(SumFoldError::Ell0TooLarge { + ell0: prefix_vars, + ell, + } + .into()); + } + + let tail_vars = ell - prefix_vars; + let tail_len = binary_len(tail_vars); + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + let lambda_powers = powers( + lambda.clone(), + F::one_with_cfg(field_cfg), + NUM_SHA_RESIDUAL_FAMILIES, + ); + let a_powers = powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_RESIDUAL_EVAL_POWER_COUNT, + ); + let rho_powers = powers( + rho.clone(), + F::one_with_cfg(field_cfg), + booleanity_sources.len(), + ); + + let linear_values = traces + .iter() + .zip(publics.iter()) + .map(|(trace, public)| { + sha_linear_residual_sum_with_weights( + trace, + public, + &row_weights, + &a_powers, + &lambda_powers, + field_cfg, + ) + }) + .collect::, _>>()?; + let linear = BinaryPrefixTailTable::new(linear_values, prefix_vars, tail_len); + let booleanity = TernaryPrefixTailTable::new( + build_sha_booleanity_prefix_tail_table( + &traces, + booleanity_sources, + prefix_vars, + tail_len, + &row_weights, + &rho_powers, + field_cfg, + )?, + prefix_vars, + tail_len, + )?; + + let tail_eq_weights = eq_weights_or_one(&beta[prefix_vars..], field_cfg)?; + let mut prefix_suffix_eq_weights = Vec::with_capacity(prefix_vars); + for round in 0..prefix_vars { + prefix_suffix_eq_weights + .push(eq_weights_or_one(&beta[round + 1..prefix_vars], field_cfg)?); + } + + Ok(Self { + traces, + beta: beta.to_vec(), + xi: xi.clone(), + booleanity_sources: booleanity_sources.to_vec(), + linear, + booleanity, + tail_eq_weights, + prefix_suffix_eq_weights, + total_prefix_vars: prefix_vars, + round: 0, + eq_bound: F::one_with_cfg(field_cfg), + }) + } + + #[allow(clippy::arithmetic_side_effects)] + fn bind_previous_round( + &mut self, + r: &F, + field_cfg: &F::Config, + ) -> Result<(), ShaProjectionError> { + let beta_idx = self.round - 1; + self.eq_bound *= eq_one_var(&self.beta[beta_idx], r, field_cfg); + self.linear.bind_first_axis(r, field_cfg); + self.booleanity.bind_first_axis(r, field_cfg) + } + + fn round_value_at(&self, x: &F, field_cfg: &F::Config) -> Result { + debug_assert!(self.round < self.total_prefix_vars); + let suffix_weights = &self.prefix_suffix_eq_weights[self.round]; + let rest_len = suffix_weights.len(); + let mut acc = F::zero_with_cfg(field_cfg); + + for tail in 0..self.tail_eq_weights.len() { + for (rest, suffix_weight) in suffix_weights.iter().enumerate().take(rest_len) { + let linear = self.linear.value_with_first_axis(rest, tail, x, field_cfg); + let ternary_rest = binary_bits_to_ternary_index(rest, self.linear.prefix_vars - 1); + let booleanity = + self.booleanity + .value_with_first_axis(ternary_rest, tail, x, field_cfg)?; + acc += self.tail_eq_weights[tail].clone() + * suffix_weight + * (linear + self.xi.clone() * booleanity); + } + } + + Ok(self.eq_bound.clone() * eq_one_var(&self.beta[self.round], x, field_cfg) * acc) + } + + #[allow(clippy::arithmetic_side_effects)] + fn finish_tail_mles( + mut self, + prefix_challenges: &[F], + field_cfg: &F::Config, + ) -> Result>, ShaProjectionError> { + debug_assert_eq!(prefix_challenges.len(), self.total_prefix_vars); + let tail_vars = self.beta.len() - self.total_prefix_vars; + if tail_vars == 0 { + return Ok(Vec::new()); + } + + while self.linear.prefix_vars > 0 { + let next_axis = self.total_prefix_vars - self.linear.prefix_vars; + let r = &prefix_challenges[next_axis]; + self.linear.bind_first_axis(r, field_cfg); + } + + let tail_len = binary_len(tail_vars); + debug_assert_eq!(self.linear.values.len(), tail_len); + + let prefix_weights = eq_weights_or_one(prefix_challenges, field_cfg)?; + let eq_prefix_at_r = eq_eval( + prefix_challenges, + &self.beta[..self.total_prefix_vars], + F::one_with_cfg(field_cfg), + )?; + let zero_inner = F::zero_with_cfg(field_cfg).inner().clone(); + + let mut mles = Vec::with_capacity(2 + self.booleanity_sources.len() * SHA_ROW_COUNT); + mles.push(DenseMultilinearExtension::from_evaluations_vec( + tail_vars, + self.tail_eq_weights + .iter() + .map(|tail_weight| (eq_prefix_at_r.clone() * tail_weight).inner().clone()) + .collect(), + zero_inner.clone(), + )); + mles.push(DenseMultilinearExtension::from_evaluations_vec( + tail_vars, + self.linear + .values + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner.clone(), + )); + + let source_tail_values = bind_sha_booleanity_sources_to_prefix( + &self.traces, + &self.booleanity_sources, + self.total_prefix_vars, + tail_len, + &prefix_weights, + field_cfg, + )?; + + for values in source_tail_values { + mles.push(DenseMultilinearExtension::from_evaluations_vec( + tail_vars, + values.iter().map(|value| value.inner().clone()).collect(), + zero_inner.clone(), + )); + } + + Ok(mles) + } +} + +impl PrefixFastPath for ShaSumFoldPrefixFastPath +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + fn prefix_len(&self) -> usize { + self.total_prefix_vars + } + + fn prove_prefix_round( + &mut self, + verifier_msg: &Option, + config: &F::Config, + ) -> PrefixRoundOutput { + if let Some(r) = verifier_msg { + self.bind_previous_round(r, config) + .expect("validated SHA prefix table should bind"); + } + + let zero = F::zero_with_cfg(config); + let one = F::one_with_cfg(config); + let two = one.clone() + &one; + let three = two.clone() + &one; + + let p0 = self + .round_value_at(&zero, config) + .expect("validated SHA prefix table should evaluate at 0"); + let p1 = self + .round_value_at(&one, config) + .expect("validated SHA prefix table should evaluate at 1"); + let p2 = self + .round_value_at(&two, config) + .expect("validated SHA prefix table should evaluate at 2"); + let p3 = self + .round_value_at(&three, config) + .expect("validated SHA prefix table should evaluate at 3"); + + let asserted_sum = if self.round == 0 { + Some(p0 + &p1) + } else { + None + }; + self.round += 1; + + PrefixRoundOutput { + asserted_sum, + tail_evaluations: vec![p1, p2, p3], + } + } + + fn finish_prefix( + self: Box, + prefix_challenges: &[F], + config: &F::Config, + ) -> Vec> { + self.finish_tail_mles(prefix_challenges, config) + .expect("validated SHA prefix fast path should finish") + } +} + /// Build the production SHA SumFold group over the instance axis. /// /// The group proves the expression @@ -1104,35 +1660,22 @@ where F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: Zero, { - if traces.is_empty() { - return Err(ShaProjectionError::InstanceCountNotPowerOfTwo { got: 0 }); - } - if traces.len() != publics.len() { - return Err(ShaProjectionError::InstanceCountMismatch { - got: publics.len(), - expected: traces.len(), - }); - } - if !traces.len().is_power_of_two() { - return Err(ShaProjectionError::InstanceCountNotPowerOfTwo { got: traces.len() }); - } - let ell = usize::try_from(traces.len().trailing_zeros()).expect("trailing_zeros fits usize"); - if beta.len() != ell { - return Err(ShaProjectionError::InstanceCountMismatch { - got: beta.len(), - expected: ell, - }); - } - for trace in traces { - validate_trace(trace)?; - } - for public in publics { - validate_public(public)?; - } + let ell = validate_sha_sumfold_inputs(traces, publics, beta)?; let zero = F::zero_with_cfg(field_cfg); let zero_inner = zero.inner().clone(); let mut mles = Vec::with_capacity(2 + booleanity_sources.len() * SHA_ROW_COUNT); + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + let lambda_powers = powers( + lambda.clone(), + F::one_with_cfg(field_cfg), + NUM_SHA_RESIDUAL_FAMILIES, + ); + let a_powers = powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_RESIDUAL_EVAL_POWER_COUNT, + ); let eq_beta = build_eq_x_r_vec(beta, field_cfg)?; mles.push(DenseMultilinearExtension::from_evaluations_vec( @@ -1144,7 +1687,16 @@ where let linear_values = traces .iter() .zip(publics.iter()) - .map(|(trace, public)| sha_linear_residual_sum(trace, public, r_ic, a, lambda, field_cfg)) + .map(|(trace, public)| { + sha_linear_residual_sum_with_weights( + trace, + public, + &row_weights, + &a_powers, + &lambda_powers, + field_cfg, + ) + }) .collect::, _>>()?; mles.push(DenseMultilinearExtension::from_evaluations_vec( ell, @@ -1169,90 +1721,597 @@ where } } - let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; - let rho_powers = powers( - rho.clone(), - F::one_with_cfg(field_cfg), - booleanity_sources.len(), - ); - let xi = xi.clone(); - let zero_for_comb = F::zero_with_cfg(field_cfg); - let one_for_comb = F::one_with_cfg(field_cfg); Ok(MultiDegreeSumcheckGroup::new( 3, mles, - Box::new(move |values: &[F]| { - let eq_beta = values[0].clone(); - let linear = values[1].clone(); - let mut bool_sum = zero_for_comb.clone(); - let mut cursor = 2usize; - for rho_power in &rho_powers { - for row_weight in &row_weights { - let d = values[cursor].clone(); - cursor += 1; - let term = d.clone() * (d - one_for_comb.clone()); - bool_sum += row_weight.clone() * rho_power * term; - } - } - eq_beta * (linear + xi.clone() * bool_sum) - }), + sha_sumfold_comb_fn( + row_weights, + powers( + rho.clone(), + F::one_with_cfg(field_cfg), + booleanity_sources.len(), + ), + xi.clone(), + field_cfg, + ), )) } -/// Build the expression-backed folded row sumcheck group. -/// -/// The terminal at the verifier challenge is tied to source MLE endpoint -/// values, including booleanity sources, rather than to an opaque MLE of -/// precomputed row-integrand values. #[allow(clippy::too_many_arguments)] -pub fn build_expression_folded_row_sumcheck_group( - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, +pub fn build_production_sha_sumfold_group( + traces: &[ProjectedShaTrace], + publics: &[ProjectedShaPublic], + beta: &[F], r_ic: &[F; SHA_ROW_VARS], a: &F, lambda: &F, rho: &F, xi: &F, booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, field_cfg: &F::Config, ) -> Result, ShaProjectionError> where F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: Zero, { - validate_trace(trace)?; - validate_public(public)?; + let ell = validate_sha_sumfold_inputs(traces, publics, beta)?; + if prefix_vars > ell { + return Err(SumFoldError::Ell0TooLarge { + ell0: prefix_vars, + ell, + } + .into()); + } + if prefix_vars == 0 { + return build_dense_sha_sumfold_group( + traces, + publics, + beta, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + ); + } - let zero = F::zero_with_cfg(field_cfg); - let zero_inner = zero.inner().clone(); - let mut mles = Vec::with_capacity(2 + booleanity_sources.len()); + let fast_path = ShaSumFoldPrefixFastPath::new( + traces, + publics, + beta, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + prefix_vars, + field_cfg, + ); - let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; - mles.push(DenseMultilinearExtension::from_evaluations_vec( - SHA_ROW_VARS, - row_weights - .iter() - .map(|value| value.inner().clone()) - .collect(), - zero_inner.clone(), - )); + Ok(MultiDegreeSumcheckGroup::with_prefix_fast( + 3, + Vec::new(), + sha_sumfold_comb_fn( + build_eq_x_r_vec(r_ic, field_cfg)?, + powers( + rho.clone(), + F::one_with_cfg(field_cfg), + booleanity_sources.len(), + ), + xi.clone(), + field_cfg, + ), + Box::new(fast_path?), + )) +} - let linear_values = (0..SHA_ROW_COUNT) - .map(|row| sha_linear_residual_row_value(trace, public, row, a, lambda, field_cfg)) - .collect::, _>>()?; - mles.push(DenseMultilinearExtension::from_evaluations_vec( - SHA_ROW_VARS, - linear_values - .iter() +#[allow(clippy::too_many_arguments)] +pub fn build_production_sha_sumfold_group_owned( + traces: Box<[ProjectedShaTrace]>, + publics: &[ProjectedShaPublic], + beta: &[F], + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + let ell = validate_sha_sumfold_inputs(&traces, publics, beta)?; + if prefix_vars > ell { + return Err(SumFoldError::Ell0TooLarge { + ell0: prefix_vars, + ell, + } + .into()); + } + if prefix_vars == 0 { + return build_dense_sha_sumfold_group( + &traces, + publics, + beta, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + ); + } + + let fast_path = ShaSumFoldPrefixFastPath::new_owned( + traces, + publics, + beta, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + prefix_vars, + field_cfg, + ); + + Ok(MultiDegreeSumcheckGroup::with_prefix_fast( + 3, + Vec::new(), + sha_sumfold_comb_fn( + build_eq_x_r_vec(r_ic, field_cfg)?, + powers( + rho.clone(), + F::one_with_cfg(field_cfg), + booleanity_sources.len(), + ), + xi.clone(), + field_cfg, + ), + Box::new(fast_path?), + )) +} + +fn sha_sumfold_comb_fn( + row_weights: Vec, + rho_powers: Vec, + xi: F, + field_cfg: &F::Config, +) -> CombFn +where + F: PrimeField + Send + Sync + 'static, +{ + let zero_for_comb = F::zero_with_cfg(field_cfg); + let one_for_comb = F::one_with_cfg(field_cfg); + Box::new(move |values: &[F]| { + let eq_beta = values[0].clone(); + let linear = values[1].clone(); + let mut bool_sum = zero_for_comb.clone(); + let mut cursor = 2usize; + for rho_power in &rho_powers { + for row_weight in &row_weights { + let d = values[cursor].clone(); + cursor += 1; + let term = d.clone() * (d - one_for_comb.clone()); + bool_sum += row_weight.clone() * rho_power * term; + } + } + eq_beta * (linear + xi.clone() * bool_sum) + }) +} + +fn validate_sha_sumfold_inputs( + traces: &[ProjectedShaTrace], + publics: &[ProjectedShaPublic], + beta: &[F], +) -> Result { + if traces.is_empty() { + return Err(ShaProjectionError::InstanceCountNotPowerOfTwo { got: 0 }); + } + if traces.len() != publics.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: publics.len(), + expected: traces.len(), + }); + } + if !traces.len().is_power_of_two() { + return Err(ShaProjectionError::InstanceCountNotPowerOfTwo { got: traces.len() }); + } + let ell = usize::try_from(traces.len().trailing_zeros()).expect("trailing_zeros fits usize"); + if beta.len() != ell { + return Err(ShaProjectionError::InstanceCountMismatch { + got: beta.len(), + expected: ell, + }); + } + for trace in traces { + validate_trace(trace)?; + } + for public in publics { + validate_public(public)?; + } + Ok(ell) +} + +fn binary_len(vars: usize) -> usize { + 1usize + .checked_shl(u32::try_from(vars).expect("vars fits u32")) + .expect("binary domain size fits usize") +} + +fn checked_ternary_len(vars: usize) -> Result { + let mut size = 1usize; + for _ in 0..vars { + size = size + .checked_mul(3) + .ok_or(SumFoldError::DomainTooLarge { ell: vars })?; + } + Ok(size) +} + +fn eq_weights_or_one(point: &[F], field_cfg: &F::Config) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + if point.is_empty() { + Ok(vec![F::one_with_cfg(field_cfg)]) + } else { + Ok(build_eq_x_r_vec(point, field_cfg)?) + } +} + +fn eq_one_var(beta: &F, x: &F, field_cfg: &F::Config) -> F +where + F: PrimeField, +{ + let one = F::one_with_cfg(field_cfg); + x.clone() * beta + (one.clone() - x) * (one - beta) +} + +#[allow(clippy::arithmetic_side_effects)] +fn build_sha_booleanity_prefix_tail_table( + traces: &[ProjectedShaTrace], + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + tail_len: usize, + row_weights: &[F], + rho_powers: &[F], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + let prefix_len = binary_len(prefix_vars); + let ternary_len = checked_ternary_len(prefix_vars)?; + let mut table = vec![F::zero_with_cfg(field_cfg); ternary_len * tail_len]; + if booleanity_sources.is_empty() { + return Ok(table); + } + + let needs_virtuals = sources_need_virtuals(booleanity_sources); + let coeff_plans = ternary_coeff_plans(prefix_vars)?; + let mut source_values = + vec![F::zero_with_cfg(field_cfg); prefix_len * booleanity_sources.len()]; + for tail in 0..tail_len { + for (row, row_weight) in row_weights.iter().enumerate().take(SHA_ROW_COUNT) { + fill_booleanity_source_prefix_values( + traces, + booleanity_sources, + prefix_vars, + tail, + row, + needs_virtuals, + &mut source_values, + field_cfg, + )?; + + for (source_idx, rho_power) in rho_powers.iter().enumerate() { + let source_weight = row_weight.clone() * rho_power; + for (ternary_idx, plan) in coeff_plans.iter().enumerate() { + let coeff = booleanity_degree_two_coeff( + &source_values, + booleanity_sources.len(), + source_idx, + plan, + field_cfg, + ); + table[tail * ternary_len + ternary_idx] += source_weight.clone() * coeff; + } + } + } + } + Ok(table) +} + +#[allow(clippy::arithmetic_side_effects)] +fn bind_sha_booleanity_sources_to_prefix( + traces: &[ProjectedShaTrace], + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + tail_len: usize, + prefix_weights: &[F], + field_cfg: &F::Config, +) -> Result>, ShaProjectionError> +where + F: PrimeField, +{ + let prefix_len = binary_len(prefix_vars); + let source_count = booleanity_sources.len(); + let needs_virtuals = sources_need_virtuals(booleanity_sources); + let mut source_values = vec![F::zero_with_cfg(field_cfg); prefix_len * source_count]; + let mut out = vec![vec![F::zero_with_cfg(field_cfg); tail_len]; source_count * SHA_ROW_COUNT]; + + for tail in 0..tail_len { + for row in 0..SHA_ROW_COUNT { + fill_booleanity_source_prefix_values( + traces, + booleanity_sources, + prefix_vars, + tail, + row, + needs_virtuals, + &mut source_values, + field_cfg, + )?; + for source_idx in 0..source_count { + let mut acc = F::zero_with_cfg(field_cfg); + for (prefix, weight) in prefix_weights.iter().enumerate().take(prefix_len) { + acc += weight.clone() * &source_values[prefix * source_count + source_idx]; + } + out[source_idx * SHA_ROW_COUNT + row][tail] = acc; + } + } + } + + Ok(out) +} + +#[allow(clippy::arithmetic_side_effects)] +fn fill_booleanity_source_prefix_values( + traces: &[ProjectedShaTrace], + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + tail: usize, + row: usize, + needs_virtuals: bool, + out: &mut [F], + field_cfg: &F::Config, +) -> Result<(), ShaProjectionError> +where + F: PrimeField, +{ + let prefix_len = binary_len(prefix_vars); + let source_count = booleanity_sources.len(); + debug_assert_eq!(out.len(), prefix_len * source_count); + + for prefix in 0..prefix_len { + let instance_idx = prefix + (tail << prefix_vars); + let trace = &traces[instance_idx]; + let virtuals = if needs_virtuals { + Some(reconstruct_virtual_ch_maj_at_row_unchecked( + trace, row, field_cfg, + )?) + } else { + None + }; + + for (source_idx, source) in booleanity_sources.iter().enumerate() { + let value = match source { + ShaBooleanitySource::WordBit { col, bit } => { + bit_at_shifted_or_zero(trace, *col, row, 0, *bit, field_cfg)? + } + ShaBooleanitySource::VirtualCh1 { bit } => { + virtual_bit_at(&virtuals.as_ref().expect("virtuals computed").ch1, *bit)? + } + ShaBooleanitySource::VirtualCh2 { bit } => { + virtual_bit_at(&virtuals.as_ref().expect("virtuals computed").ch2, *bit)? + } + ShaBooleanitySource::VirtualMaj { bit } => { + virtual_bit_at(&virtuals.as_ref().expect("virtuals computed").maj, *bit)? + } + }; + out[prefix * source_count + source_idx] = value; + } + } + + Ok(()) +} + +#[allow(clippy::arithmetic_side_effects)] +fn booleanity_degree_two_coeff( + source_values: &[F], + source_count: usize, + source_idx: usize, + plan: &TernaryCoeffPlan, + field_cfg: &F::Config, +) -> F +where + F: PrimeField, +{ + let value_at = + |prefix: usize| -> F { source_values[prefix * source_count + source_idx].clone() }; + if plan.support_mask == 0 { + let d = value_at(plan.finite_bits); + return d.clone() * (d - F::one_with_cfg(field_cfg)); + } + + let mut delta = F::zero_with_cfg(field_cfg); + for (prefix, positive) in &plan.vertices { + if *positive { + delta += value_at(*prefix); + } else { + delta -= value_at(*prefix); + } + } + + delta.clone() * delta +} + +#[allow(clippy::arithmetic_side_effects)] +fn ternary_point_parts(mut index: usize, prefix_vars: usize) -> (usize, usize) { + let mut support_mask = 0usize; + let mut finite_bits = 0usize; + for var in 0..prefix_vars { + let digit = index % 3; + index /= 3; + match digit { + 0 => {} + 1 => finite_bits |= 1usize << var, + 2 => support_mask |= 1usize << var, + _ => unreachable!("ternary digit is always 0, 1, or 2"), + } + } + (support_mask, finite_bits) +} + +fn sources_need_virtuals(booleanity_sources: &[ShaBooleanitySource]) -> bool { + booleanity_sources.iter().any(|source| { + matches!( + source, + ShaBooleanitySource::VirtualCh1 { .. } + | ShaBooleanitySource::VirtualCh2 { .. } + | ShaBooleanitySource::VirtualMaj { .. } + ) + }) +} + +#[allow(clippy::arithmetic_side_effects)] +fn ternary_coeff_plans(prefix_vars: usize) -> Result, ShaProjectionError> { + let ternary_len = checked_ternary_len(prefix_vars)?; + let mut plans = Vec::with_capacity(ternary_len); + for ternary_idx in 0..ternary_len { + let (support_mask, finite_bits) = ternary_point_parts(ternary_idx, prefix_vars); + let mut vertices = Vec::new(); + if support_mask != 0 { + let mut support_bits = [0usize; usize::BITS as usize]; + let mut mask = support_mask; + let mut support_size = 0usize; + while mask != 0 { + let bit = mask & mask.wrapping_neg(); + support_bits[support_size] = bit; + support_size += 1; + mask ^= bit; + } + vertices.reserve(1usize << support_size); + for vertex in 0..(1usize << support_size) { + let mut prefix = finite_bits; + for (pos, bit) in support_bits[..support_size].iter().enumerate() { + if ((vertex >> pos) & 1) == 1 { + prefix |= *bit; + } + } + let positive = (support_size + - usize::try_from(vertex.count_ones()).expect("count_ones fits usize")) + % 2 + == 0; + vertices.push((prefix, positive)); + } + } + plans.push(TernaryCoeffPlan { + support_mask, + finite_bits, + vertices, + }); + } + Ok(plans) +} + +#[allow(clippy::arithmetic_side_effects)] +fn binary_bits_to_ternary_index(mut bits: usize, vars: usize) -> usize { + let mut index = 0usize; + let mut scale = 1usize; + for _ in 0..vars { + if bits & 1 == 1 { + index += scale; + } + bits >>= 1; + scale *= 3; + } + index +} + +/// Build the expression-backed folded row sumcheck group. +/// +/// The terminal at the verifier challenge is tied to source MLE endpoint +/// values, including booleanity sources, rather than to an opaque MLE of +/// precomputed row-integrand values. +#[allow(clippy::too_many_arguments)] +pub fn build_expression_folded_row_sumcheck_group( + trace: &ProjectedShaTrace, + public: &ProjectedShaPublic, + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + validate_trace(trace)?; + validate_public(public)?; + + let zero = F::zero_with_cfg(field_cfg); + let zero_inner = zero.inner().clone(); + let mut mles = Vec::with_capacity(2 + booleanity_sources.len()); + + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + row_weights + .iter() .map(|value| value.inner().clone()) .collect(), zero_inner.clone(), )); - for source in booleanity_sources { - let values = (0..SHA_ROW_COUNT) - .map(|row| booleanity_source_value_at_row(trace, row, source, field_cfg)) - .collect::, _>>()?; + let linear_values = (0..SHA_ROW_COUNT) + .map(|row| sha_linear_residual_row_value(trace, public, row, a, lambda, field_cfg)) + .collect::, _>>()?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + linear_values + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner.clone(), + )); + + let needs_virtuals = sources_need_virtuals(booleanity_sources); + let mut source_values = (0..booleanity_sources.len()) + .map(|_| Vec::with_capacity(SHA_ROW_COUNT)) + .collect::>(); + for row in 0..SHA_ROW_COUNT { + let virtuals = if needs_virtuals { + Some(reconstruct_virtual_ch_maj_at_row_unchecked( + trace, row, field_cfg, + )?) + } else { + None + }; + for (source_idx, source) in booleanity_sources.iter().enumerate() { + source_values[source_idx].push(booleanity_source_value_at_row_with_virtuals( + trace, + row, + source, + virtuals.as_ref(), + field_cfg, + )?); + } + } + for values in source_values { mles.push(DenseMultilinearExtension::from_evaluations_vec( SHA_ROW_VARS, values.iter().map(|value| value.inner().clone()).collect(), @@ -1556,21 +2615,21 @@ where Ok(residuals) } -fn residual_values_at_row( +fn residual_values_at_row_with_powers( trace: &ProjectedShaTrace, public: &ProjectedShaPublic, row: usize, - a: &F, + a_powers: &[F], field_cfg: &F::Config, ) -> Result<[F; NUM_SHA_RESIDUAL_FAMILIES], ShaProjectionError> where - F: PrimeField, + F: DelayedFieldProductSum, { let polies = residual_polys_at_row(trace, public, row, field_cfg)?; let mut out: [F; NUM_SHA_RESIDUAL_FAMILIES] = std::array::from_fn(|_| F::zero_with_cfg(field_cfg)); for (idx, poly) in polies.iter().enumerate() { - out[idx] = poly.evaluate_at_point(a)?; + out[idx] = evaluate_poly_at_powers_dmr(poly, a_powers, field_cfg)?; } Ok(out) } @@ -1888,12 +2947,97 @@ where Ok(scale_poly(&packed, &low_coeff) - &scale_poly(&tail, &high_coeff)) } -fn project_bits(bits: &[F], powers: &[F], field_cfg: &F::Config) -> F { +fn evaluate_poly_at_powers_dmr( + poly: &DynamicPolynomialF, + powers: &[F], + field_cfg: &F::Config, +) -> Result +where + F: DelayedFieldProductSum, +{ + if poly.coeffs.is_empty() { + return Ok(F::zero_with_cfg(field_cfg)); + } + if poly.coeffs.len() > powers.len() { + return Err(ShaProjectionError::NonCanonicalProofObject( + "SHA polynomial exceeds precomputed scalarization power bound", + )); + } + DynamicPolyFInnerProduct::inner_product::( + &poly.coeffs, + &powers[..poly.coeffs.len()], + F::zero_with_cfg(field_cfg), + ) + .map_err(ShaProjectionError::from) +} + +fn project_bits_dmr( + bits: &[F], + powers: &[F], + field_cfg: &F::Config, +) -> Result +where + F: DelayedFieldProductSum, +{ + if bits.len() > powers.len() { + return Err(ShaProjectionError::NonCanonicalProofObject( + "SHA bit projection exceeds precomputed scalarization power bound", + )); + } + FieldFieldInnerProduct::inner_product::( + bits, + &powers[..bits.len()], + F::zero_with_cfg(field_cfg), + ) + .map_err(ShaProjectionError::from) +} + +fn project_binary_bits_conditional_add_dmr( + bits: &[F], + powers: &[F], + field_cfg: &F::Config, +) -> Result +where + F: MontgomeryLimbs + DelayedFieldProductSum + Send + Sync, +{ + if bits.len() > powers.len() { + return Err(ShaProjectionError::NonCanonicalProofObject( + "SHA binary bit projection exceeds precomputed scalarization power bound", + )); + } + let one = F::one_with_cfg(field_cfg); + let reduction_params = F::barrett_reduction_params(field_cfg); + let flush_adds = dmr_flush_adds(&reduction_params); + let mut bucket = Uint::<5>::zero(); + let mut pending_adds = 0usize; let mut acc = F::zero_with_cfg(field_cfg); + for (bit, power) in bits.iter().zip(powers.iter()) { - acc += bit.clone() * power; + if F::is_zero(bit) { + continue; + } + if bit != &one { + return project_bits_dmr(bits, powers, field_cfg); + } + + as DelayedModularReduction>::add(&mut bucket, power); + pending_adds = pending_adds.saturating_add(1); + if pending_adds >= flush_adds { + let pending = std::mem::replace(&mut bucket, Uint::zero()); + acc += as DelayedModularReduction>::reduce( + pending, + field_cfg, + &reduction_params, + ); + pending_adds = 0; + } + } + + if !bucket.is_zero() { + acc += + as DelayedModularReduction>::reduce(bucket, field_cfg, &reduction_params); } - acc + Ok(acc) } fn build_virtual_bit_array(mut f: G) -> Result<[F; SHA_WORD_BITS], ShaProjectionError> @@ -1952,6 +3096,29 @@ fn booleanity_source_value_at_row( source: &ShaBooleanitySource, field_cfg: &F::Config, ) -> Result +where + F: PrimeField, +{ + let virtuals = if matches!( + source, + ShaBooleanitySource::VirtualCh1 { .. } + | ShaBooleanitySource::VirtualCh2 { .. } + | ShaBooleanitySource::VirtualMaj { .. } + ) { + Some(reconstruct_virtual_ch_maj_at_row(trace, row, field_cfg)?) + } else { + None + }; + booleanity_source_value_at_row_with_virtuals(trace, row, source, virtuals.as_ref(), field_cfg) +} + +fn booleanity_source_value_at_row_with_virtuals( + trace: &ProjectedShaTrace, + row: usize, + source: &ShaBooleanitySource, + virtuals: Option<&VirtualChMajValues>, + field_cfg: &F::Config, +) -> Result where F: PrimeField, { @@ -1959,18 +3126,15 @@ where ShaBooleanitySource::WordBit { col, bit } => { bit_at_shifted_or_zero(trace, *col, row, 0, *bit, field_cfg) } - ShaBooleanitySource::VirtualCh1 { bit } => virtual_bit_at( - &reconstruct_virtual_ch_maj_at_row(trace, row, field_cfg)?.ch1, - *bit, - ), - ShaBooleanitySource::VirtualCh2 { bit } => virtual_bit_at( - &reconstruct_virtual_ch_maj_at_row(trace, row, field_cfg)?.ch2, - *bit, - ), - ShaBooleanitySource::VirtualMaj { bit } => virtual_bit_at( - &reconstruct_virtual_ch_maj_at_row(trace, row, field_cfg)?.maj, - *bit, - ), + ShaBooleanitySource::VirtualCh1 { bit } => { + virtual_bit_at(&virtuals.expect("virtual source needs row cache").ch1, *bit) + } + ShaBooleanitySource::VirtualCh2 { bit } => { + virtual_bit_at(&virtuals.expect("virtual source needs row cache").ch2, *bit) + } + ShaBooleanitySource::VirtualMaj { bit } => { + virtual_bit_at(&virtuals.expect("virtual source needs row cache").maj, *bit) + } } } @@ -2088,6 +3252,7 @@ mod tests { use crate::sumcheck::multi_degree::MultiDegreeSumcheck; use crate::test_utils::test_config; use crypto_primitives::{FromWithConfig, crypto_bigint_monty::MontyField}; + use zinc_poly::EvaluatablePolynomial; use zinc_transcript::Blake3Transcript; type F = MontyField<4>; @@ -2124,6 +3289,161 @@ mod tests { } } + fn synthetic_boolean_trace(instance_idx: u64, a: &F) -> ProjectedShaTrace { + let cfg = test_config(); + let zero = F::zero_with_cfg(&cfg); + let mut bits = + vec![vec![vec![zero.clone(); SHA_WORD_BITS]; SHA_ROW_COUNT]; ShaWordCol::COUNT]; + for (col_idx, col) in bits.iter_mut().enumerate() { + for (row_idx, row) in col.iter_mut().enumerate() { + for (bit_idx, bit) in row.iter_mut().enumerate() { + let selector = instance_idx + + u64::try_from(col_idx * 17 + row_idx * 3 + bit_idx) + .expect("test selector fits u64"); + if selector % 2 == 1 { + *bit = f(1); + } + } + } + } + let bit_slices = ShaBitSliceColumns { columns: bits }; + let scalarized_words = scalarize_trace_words(&bit_slices, a, &cfg).unwrap(); + ProjectedShaTrace { + rows: SHA_ROW_COUNT, + bit_slices, + scalarized_words, + int_columns: ShaIntColumns { + columns: vec![vec![zero.clone(); SHA_ROW_COUNT]; ShaIntCol::COUNT], + }, + public_columns: ShaPublicColumns { + columns: vec![vec![zero; SHA_ROW_COUNT]; ShaPublicCol::COUNT], + }, + } + } + + fn prove_and_verify_sumfold( + group: MultiDegreeSumcheckGroup, + ell: usize, + ) -> ( + crate::sumcheck::multi_degree::MultiDegreeSumcheckProof, + Vec, + Vec, + ) { + let cfg = test_config(); + let mut prover_transcript = Blake3Transcript::new(); + let (proof, _) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut prover_transcript, + vec![group], + ell, + &cfg, + ); + + let mut verifier_transcript = Blake3Transcript::new(); + let subclaims = + MultiDegreeSumcheck::verify_as_subprotocol(&mut verifier_transcript, ell, &proof, &cfg) + .expect("sumcheck proof should verify"); + + ( + proof, + subclaims.point().to_vec(), + subclaims.expected_evaluations().to_vec(), + ) + } + + fn naive_project_bits(bits: &[F], powers: &[F]) -> F { + bits.iter() + .zip(powers.iter()) + .fold(F::zero_with_cfg(&test_config()), |acc, (bit, power)| { + acc + bit.clone() * power + }) + } + + #[test] + fn dmr_bit_projection_matches_naive_for_binary_and_field_bits() { + let cfg = test_config(); + let a = f(7); + let powers = powers(a, F::one_with_cfg(&cfg), SHA_WORD_BITS); + let zero = F::zero_with_cfg(&cfg); + let mut binary_bits = vec![zero.clone(); SHA_WORD_BITS]; + binary_bits[0] = f(1); + binary_bits[5] = f(1); + binary_bits[31] = f(1); + + let binary_expected = naive_project_bits(&binary_bits, &powers); + assert_eq!( + project_binary_bits_conditional_add_dmr(&binary_bits, &powers, &cfg).unwrap(), + binary_expected + ); + assert_eq!( + project_bits_dmr(&binary_bits, &powers, &cfg).unwrap(), + binary_expected + ); + + let mut field_bits = vec![zero; SHA_WORD_BITS]; + field_bits[3] = f(2); + field_bits[9] = f(11); + let field_expected = naive_project_bits(&field_bits, &powers); + assert_eq!( + project_binary_bits_conditional_add_dmr(&field_bits, &powers, &cfg).unwrap(), + field_expected + ); + assert_eq!( + project_bits_dmr(&field_bits, &powers, &cfg).unwrap(), + field_expected + ); + } + + #[test] + fn dmr_residual_evaluation_matches_polynomial_evaluation() { + let cfg = test_config(); + let a = f(5); + let trace = synthetic_boolean_trace(3, &a); + let public = zero_public(); + let row = 17usize; + let a_powers = powers( + a.clone(), + F::one_with_cfg(&cfg), + SHA_RESIDUAL_EVAL_POWER_COUNT, + ); + let residuals = + residual_values_at_row_with_powers(&trace, &public, row, &a_powers, &cfg).unwrap(); + let polies = residual_polys_at_row(&trace, &public, row, &cfg).unwrap(); + + for (value, poly) in residuals.iter().zip(polies.iter()) { + assert_eq!(value, &poly.evaluate_at_point(&a).unwrap()); + } + } + + #[test] + fn dmr_fresh_sha_targets_match_reference_evaluation() { + let cfg = test_config(); + let a = f(13); + let lambda = f(17); + let mut cache = FreshShaIdealCache { + r_ic: std::array::from_fn(|_| F::zero_with_cfg(&cfg)), + ideal_polys: vec![std::array::from_fn(|slot| { + DynamicPolynomialF::new_trimmed([ + f(u64::try_from(slot + 1).unwrap()), + f(u64::try_from(slot + 2).unwrap()), + f(u64::try_from(slot + 3).unwrap()), + ]) + })], + taus_at_a: Vec::new(), + fresh_targets: Vec::new(), + }; + + evaluate_fresh_sha_targets(&mut cache, &a, &lambda, &cfg).unwrap(); + + let lambda_powers = powers(lambda, F::one_with_cfg(&cfg), NUM_SHA_RESIDUAL_FAMILIES); + let mut expected_target = F::zero_with_cfg(&cfg); + for (slot, family) in NONZERO_SHA_FAMILIES.iter().enumerate() { + let expected_tau = cache.ideal_polys[0][slot].evaluate_at_point(&a).unwrap(); + assert_eq!(cache.taus_at_a[0][slot], expected_tau); + expected_target += lambda_powers[family.index()].clone() * expected_tau; + } + assert_eq!(cache.fresh_targets[0], expected_target); + } + #[test] fn zero_trace_ideal_cache_checks_and_targets_are_zero() { let cfg = test_config(); @@ -2330,6 +3650,145 @@ mod tests { )); } + #[test] + fn production_sha_sumfold_prefix_tail_matches_dense_sumcheck() { + let cfg = test_config(); + let ell = 3usize; + let a = f(3); + let traces = (0..(1usize << ell)) + .map(|idx| synthetic_boolean_trace(u64::try_from(idx).unwrap(), &a)) + .collect::>(); + let publics = vec![zero_public(); traces.len()]; + let beta = vec![f(5), f(7), f(11)]; + let r_ic = [f(2), f(3), f(5), f(7), f(11), f(13), f(17)]; + let lambda = f(19); + let rho = f(23); + let xi = f(29); + let sources = vec![ + ShaBooleanitySource::WordBit { + col: ShaWordCol::A, + bit: 0, + }, + ShaBooleanitySource::WordBit { + col: ShaWordCol::E, + bit: 1, + }, + ShaBooleanitySource::WordBit { + col: ShaWordCol::W, + bit: 2, + }, + ]; + + for prefix_vars in [1usize, 2, 3] { + let dense = build_dense_sha_sumfold_group( + &traces, &publics, &beta, &r_ic, &a, &lambda, &rho, &xi, &sources, &cfg, + ) + .unwrap(); + let optimized = build_production_sha_sumfold_group( + &traces, + &publics, + &beta, + &r_ic, + &a, + &lambda, + &rho, + &xi, + &sources, + prefix_vars, + &cfg, + ) + .unwrap(); + + let (dense_proof, dense_point, dense_expected) = prove_and_verify_sumfold(dense, ell); + let (optimized_proof, optimized_point, optimized_expected) = + prove_and_verify_sumfold(optimized, ell); + + assert_eq!(optimized_proof, dense_proof); + assert_eq!(optimized_point, dense_point); + assert_eq!(optimized_expected, dense_expected); + } + } + + #[test] + fn production_sha_sumfold_feeds_folded_row_sumcheck() { + let cfg = test_config(); + let ell = 2usize; + let a = f(3); + let traces = (0..(1usize << ell)) + .map(|idx| synthetic_boolean_trace(u64::try_from(idx).unwrap(), &a)) + .collect::>(); + let publics = vec![zero_public(); traces.len()]; + let beta = vec![f(5), f(7)]; + let r_ic = [f(2), f(3), f(5), f(7), f(11), f(13), f(17)]; + let lambda = f(19); + let rho = f(23); + let xi = f(29); + let sources = vec![ + ShaBooleanitySource::WordBit { + col: ShaWordCol::A, + bit: 0, + }, + ShaBooleanitySource::WordBit { + col: ShaWordCol::E, + bit: 1, + }, + ]; + + let sumfold_group = build_production_sha_sumfold_group( + &traces, &publics, &beta, &r_ic, &a, &lambda, &rho, &xi, &sources, 1, &cfg, + ) + .unwrap(); + let (_proof, r_b, expected) = prove_and_verify_sumfold(sumfold_group, ell); + let sumfold = + finalize_sha_sumfold(&beta, r_b, expected[0].clone(), traces.len(), &cfg).unwrap(); + let (folded_witness, folded_public) = + fold_projected_sha_traces(&traces, &publics, &sumfold, &cfg).unwrap(); + + let folded_claim = expression_folded_row_sum( + &folded_witness.trace, + &folded_public, + &r_ic, + &a, + &lambda, + &rho, + &xi, + &sources, + &cfg, + ) + .unwrap(); + assert_eq!(&folded_claim, sumfold.t_prime()); + + let row_group = build_expression_folded_row_sumcheck_group( + &folded_witness.trace, + &folded_public, + &r_ic, + &a, + &lambda, + &rho, + &xi, + &sources, + &cfg, + ) + .unwrap(); + let mut row_prover_transcript = Blake3Transcript::new(); + let (row_proof, _) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut row_prover_transcript, + vec![row_group], + SHA_ROW_VARS, + &cfg, + ); + let mut row_verifier_transcript = Blake3Transcript::new(); + MultiDegreeSumcheck::verify_as_subprotocol( + &mut row_verifier_transcript, + SHA_ROW_VARS, + &row_proof, + &cfg, + ) + .expect("folded row sumcheck proof should verify"); + verify_folded_row_sumcheck_claim(&row_proof.claimed_sums()[0], sumfold.t_prime()) + .expect("folded row claim matches T'"); + } + #[test] fn folded_row_group_claims_row_integrand_sum() { let cfg = test_config(); diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 79026998..69c646db 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -41,10 +41,12 @@ use zinc_piop::{ }, }; use zinc_poly::{ - EvaluatablePolynomial, EvaluationError, + EvaluationError, mle::DenseMultilinearExtension, univariate::{ - binary::BinaryPoly, dense::DensePolynomial, dynamic::over_field::DynamicPolynomialF, + binary::BinaryPoly, + dense::DensePolynomial, + dynamic::over_field::{DynamicPolyFInnerProduct, DynamicPolynomialF}, }, utils::{ArithErrors, build_eq_x_r_vec, eq_eval}, }; @@ -52,7 +54,8 @@ use zinc_transcript::Blake3Transcript; use zinc_transcript::traits::{ConstTranscribable, Transcribable, Transcript}; use zinc_uair::ShiftSpec; use zinc_utils::{ - delayed_reduction::DelayedFieldProductSum, inner_transparent_field::InnerTransparentField, + UNCHECKED, delayed_reduction::DelayedFieldProductSum, inner_product::InnerProduct, + inner_transparent_field::InnerTransparentField, }; use zip_plus::{ ZipError, @@ -172,6 +175,7 @@ const SHA256_ROUND_CONSTANTS: [u32; 64] = [ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, ]; +const SHA_IDEAL_EVAL_POWER_COUNT: usize = 62; const PRODUCTION_SHA_FRESH_BATCH_DOMAIN: &[u8] = b"PF_CONCISE_SHA256_FRESH_BATCH_V1"; @@ -444,7 +448,7 @@ pub fn sample_pre_ideal_challenge( field_cfg: &F::Config, ) -> [F; SHA_ROW_VARS] where - F: PrimeField, + F: DelayedFieldProductSum, F::Inner: ConstTranscribable, { std::array::from_fn(|_| transcript.get_field_challenge(field_cfg)) @@ -750,7 +754,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Transcribable + num_traits::Zero + Default + Send + Sync, + F::Inner: ConstTranscribable + Transcribable + Zero + Default + Send + Sync, F::Modulus: ConstTranscribable + Transcribable, P: ProductionShaOpeningPCS, P::BinaryPCS: FoldablePCS, D>, @@ -915,7 +919,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Transcribable + num_traits::Zero + Default + Send + Sync, + F::Inner: ConstTranscribable + Transcribable + Zero + Default + Send + Sync, F::Modulus: ConstTranscribable + Transcribable, P: ProductionShaOpeningPCS, P::BinaryPCS: FoldablePCS, D>, @@ -1054,7 +1058,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Transcribable + num_traits::Zero + Default + Send + Sync, + F::Inner: ConstTranscribable + Transcribable + Zero + Default + Send + Sync, F::Modulus: ConstTranscribable + Transcribable, P: ProductionShaOpeningPCS, P::BinaryPCS: FoldablePCS, D>, @@ -1079,7 +1083,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Transcribable + num_traits::Zero + Default + Send + Sync, + F::Inner: ConstTranscribable + Transcribable + Zero + Default + Send + Sync, F::Modulus: ConstTranscribable + Transcribable, P: ProductionShaOpeningPCS, P::BinaryPCS: FoldablePCS, D>, @@ -1096,26 +1100,61 @@ fn evaluate_fresh_targets_from_ideal_polys( field_cfg: &F::Config, ) -> Result, ProductionShaError> where - F: PrimeField, + F: DelayedFieldProductSum, { let lambda_powers = zinc_utils::powers( lambda.clone(), F::one_with_cfg(field_cfg), NUM_SHA_RESIDUAL_FAMILIES, ); + let a_powers = zinc_utils::powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_IDEAL_EVAL_POWER_COUNT, + ); ideal_polys .iter() .map(|instance| { let mut target = F::zero_with_cfg(field_cfg); for (slot, family) in production_sha_nonzero_families().iter().enumerate() { - target += - lambda_powers[family.index()].clone() * instance[slot].evaluate_at_point(a)?; + target += lambda_powers[family.index()].clone() + * evaluate_production_sha_poly_at_powers( + &instance[slot], + &a_powers, + field_cfg, + )?; } Ok(target) }) .collect() } +fn evaluate_production_sha_poly_at_powers( + poly: &DynamicPolynomialF, + powers: &[F], + field_cfg: &F::Config, +) -> Result> +where + F: DelayedFieldProductSum, +{ + if poly.coeffs.is_empty() { + return Ok(F::zero_with_cfg(field_cfg)); + } + if poly.coeffs.len() > powers.len() { + return Err(ProductionShaError::NonCanonicalProofObject( + "production SHA polynomial exceeds scalarization power bound", + )); + } + DynamicPolyFInnerProduct::inner_product::( + &poly.coeffs, + &powers[..poly.coeffs.len()], + F::zero_with_cfg(field_cfg), + ) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject("production SHA polynomial dot product failed") + }) +} + fn eq_weighted_sum( point: &[F], values: &[F], @@ -1476,7 +1515,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, + F::Inner: ConstTranscribable + Zero, F::Modulus: ConstTranscribable, { let claims = zinc_piop::neutron_nova::LinearInstanceClaims::new(fresh_targets.to_vec())?; @@ -1511,7 +1550,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, + F::Inner: ConstTranscribable + Zero, F::Modulus: ConstTranscribable, { require_single_sumcheck_group(proof, "SHA SumFold")?; @@ -1559,7 +1598,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, + F::Inner: ConstTranscribable + Zero, F::Modulus: ConstTranscribable, { let group = build_dense_sha_sumfold_group( @@ -1638,7 +1677,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, + F::Inner: ConstTranscribable + Zero, F::Modulus: ConstTranscribable, { require_single_sumcheck_group(proof, "SHA SumFold")?; @@ -1762,7 +1801,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, + F::Inner: ConstTranscribable + Zero, F::Modulus: ConstTranscribable, { let claimed = folded_row_integrand_sum(row_integrand_values, field_cfg)?; @@ -1794,7 +1833,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, + F::Inner: ConstTranscribable + Zero, F::Modulus: ConstTranscribable, { let claimed = zinc_piop::neutron_nova::expression_folded_row_sum( @@ -1846,7 +1885,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, + F::Inner: ConstTranscribable + Zero, F::Modulus: ConstTranscribable, { let claimed = zinc_piop::neutron_nova::expression_folded_row_sum( @@ -1918,7 +1957,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero, + F::Inner: ConstTranscribable + Zero, F::Modulus: ConstTranscribable, { require_single_sumcheck_group(proof, "folded row sumcheck")?; @@ -2117,7 +2156,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero + Default + Send + Sync, + F::Inner: ConstTranscribable + Zero + Default + Send + Sync, F::Modulus: ConstTranscribable, { validate_sha_endpoint_layout(endpoint_evals)?; @@ -2159,7 +2198,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero + Default + Send + Sync, + F::Inner: ConstTranscribable + Zero + Default + Send + Sync, F::Modulus: ConstTranscribable, { validate_sha_endpoint_layout(endpoint_evals)?; @@ -2199,7 +2238,7 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + num_traits::Zero + Default + Send + Sync, + F::Inner: ConstTranscribable + Zero + Default + Send + Sync, F::Modulus: ConstTranscribable, { Ok(MultipointEval::verify_subclaim( @@ -2224,7 +2263,7 @@ pub fn reconstruct_folded_row_terminal_from_endpoints( field_cfg: &F::Config, ) -> Result> where - F: PrimeField, + F: DelayedFieldProductSum, { if r_star.len() != SHA_ROW_VARS { return Err(ProductionShaError::LengthMismatch { @@ -2241,9 +2280,15 @@ where residual_polys_from_endpoints(endpoint_evals, folded_public, r_star, field_cfg)?; let lambda_powers = zinc_utils::powers(lambda.clone(), F::one_with_cfg(field_cfg), residuals.len()); + let a_powers = zinc_utils::powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_IDEAL_EVAL_POWER_COUNT, + ); let mut linear = F::zero_with_cfg(field_cfg); for (residual, weight) in residuals.iter().zip(lambda_powers.iter()) { - linear += weight.clone() * residual.evaluate_at_point(a)?; + linear += weight.clone() + * evaluate_production_sha_poly_at_powers(residual, &a_powers, field_cfg)?; } let rho_powers = zinc_utils::powers( @@ -2267,17 +2312,16 @@ pub fn verify_endpoint_scalarization( field_cfg: &F::Config, ) -> Result<(), ProductionShaError> where - F: PrimeField, + F: DelayedFieldProductSum, { let powers = zinc_utils::powers(a.clone(), F::one_with_cfg(field_cfg), 32); for source in &endpoint_evals.sources { - let recombined = source - .bits - .iter() - .zip(powers.iter()) - .fold(F::zero_with_cfg(field_cfg), |acc, (bit, power)| { - acc + bit.clone() * power - }); + let recombined = zinc_utils::inner_product::FieldFieldInnerProduct::inner_product::< + UNCHECKED, + >(&source.bits, &powers, F::zero_with_cfg(field_cfg)) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject("endpoint scalarization dot product failed") + })?; if recombined != source.scalarized { return Err(ProductionShaError::EndpointScalarizationMismatch { col: source.col, From 203db94c7b12f4603204977c50c207d20353ad76 Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 00:34:16 -0700 Subject: [PATCH 20/49] Add generic SHA witness synthesis --- protocol/src/lib.rs | 62 +- test-uair/Cargo.toml | 3 + test-uair/src/lib.rs | 6 +- test-uair/src/sha256.rs | 1373 +++++++++++++++++++++++---------------- uair/src/lib.rs | 10 + 5 files changed, 893 insertions(+), 561 deletions(-) diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index bf7eb9e0..6d9bfc30 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -826,8 +826,9 @@ mod tests { use zinc_test_uair::{ BigLinearUair, BigLinearUairWithPublicInput, BinaryDecompositionUair, BitOpRotUair, EC_FP_INT_LIMBS, GenerateRandomTrace, Sha256CompressionSliceUair, Sha256Ideal, - ShaEcdsaUair, TestUairMixedDegrees, TestUairMixedShifts, TestUairNoMultiplication, - TestUairSimpleMultiplication, + Sha256MessageBlock, Sha256State, ShaEcdsaUair, TestUairMixedDegrees, TestUairMixedShifts, + TestUairNoMultiplication, TestUairSimpleMultiplication, sha256_compress_native, + synthesize_sha256_chain_witnesses, }; use zinc_uair::{ ideal::{DegreeOneIdeal, rotation::RotationIdeal}, @@ -2037,6 +2038,63 @@ mod tests { .expect("SHA PCS verifier rejected an honest proof"); } + #[test] + fn test_synthesized_sha256_single_witness_round_trip() { + type U = Sha256CompressionSliceUair; + + const NUM_VARS: usize = 9; + let initial_state: Sha256State = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, + 0x5be0cd19, + ]; + let message_blocks: [Sha256MessageBlock; 1] = [std::array::from_fn(|idx| { + 0x0102_0304u32.wrapping_mul(idx as u32 + 1) + })]; + + let (witnesses, final_state) = + synthesize_sha256_chain_witnesses::(initial_state, message_blocks) + .expect("synthesized SHA witness generation should succeed"); + assert_eq!( + final_state, + sha256_compress_native(initial_state, message_blocks[0]) + ); + + let (pp, vp) = sha256_zip_pcs_params(NUM_VARS); + let field_cfg = field_cfg_from_curve_scalar::< + F, + >::Fmod, + ark_bn254::G1Affine, + >(); + let public_trace = witnesses[0].trace.public(&U::signature()); + let proof = ZincPlusPiop::::prove_with_pcs_and_field_cfg::< + AllZipPCSTypes, + false, + CHECKED, + >( + &pp, + &witnesses[0].trace, + NUM_VARS, + project_scalar_fn, + field_cfg.clone(), + ) + .expect("synthesized SHA PCS prover failed"); + + ZincPlusPiop::::verify_with_pcs_and_field_cfg::< + AllZipPCSTypes, + Sha256Ideal, + CHECKED, + >( + &vp, + proof, + &public_trace, + NUM_VARS, + project_scalar_fn, + sha256_test_project_ideal, + field_cfg, + ) + .expect("SHA PCS verifier rejected a synthesized witness proof"); + } + #[test] fn test_real_sha256_pcs_variants_round_trip() { const NUM_VARS: usize = 9; diff --git a/test-uair/Cargo.toml b/test-uair/Cargo.toml index 3e89df0e..e9cf7b91 100644 --- a/test-uair/Cargo.toml +++ b/test-uair/Cargo.toml @@ -16,6 +16,7 @@ crypto-primitives = { workspace = true } itertools = { workspace = true } num-traits = { workspace = true } rand = { workspace = true } +rayon = { workspace = true, optional = true } zinc-poly = { workspace = true } zinc-uair = { workspace = true } zinc-utils = { workspace = true } @@ -23,4 +24,6 @@ zinc-utils = { workspace = true } [lints] workspace = true +[features] +parallel = ["dep:rayon", "zinc-utils/parallel"] diff --git a/test-uair/src/lib.rs b/test-uair/src/lib.rs index 333889ee..776023a7 100644 --- a/test-uair/src/lib.rs +++ b/test-uair/src/lib.rs @@ -13,7 +13,11 @@ pub use ecdsa_affine::AffineConversionUair; pub use ecdsa_doubling::{EC_FP_INT_LIMBS, EcdsaFpRing, JacobianDoublingUair}; pub use generate_trace::*; pub use sha_ecdsa::ShaEcdsaUair; -pub use sha256::{Sha256CompressionSliceUair, Sha256Ideal}; +pub use sha256::{ + Sha256CompressionSliceUair, Sha256Ideal, Sha256MessageBlock, Sha256State, Sha256WitnessError, + sha256_compress_native, synthesize_one_sha256_compression_trace, + synthesize_sha256_chain_witnesses, +}; use crypto_primitives::{ConstSemiring, FixedSemiring, Semiring, boolean::Boolean}; use num_traits::Zero; diff --git a/test-uair/src/sha256.rs b/test-uair/src/sha256.rs index 20c60eba..72f496af 100644 --- a/test-uair/src/sha256.rs +++ b/test-uair/src/sha256.rs @@ -163,10 +163,12 @@ //! these would let a verifier pin the initial compression state. The //! init boundary currently only constrains `a[0] = pa_a[0]`. -use core::marker::PhantomData; +use core::{fmt, marker::PhantomData}; use crypto_primitives::{ConstSemiring, PrimeField, Semiring}; use rand::RngCore; +#[cfg(feature = "parallel")] +use rayon::prelude::*; use zinc_poly::{ mle::DenseMultilinearExtension, univariate::{ @@ -176,10 +178,10 @@ use zinc_poly::{ use zinc_uair::{ BitOp, BitOpSpec, ConstraintBuilder, LookupColumnSpec, PublicColumnLayout, PublicStructureError, ShiftSpec, ShiftedBitSliceSpec, TotalColumnLayout, TraceRow, Uair, - UairSignature, UairTrace, VirtualBinaryPolySource, VirtualBinaryPolySpec, + UairSignature, UairTrace, UairWitness, VirtualBinaryPolySource, VirtualBinaryPolySpec, ideal::{Ideal, IdealCheck, IdealCheckError, rotation::RotationIdeal}, }; -use zinc_utils::from_ref::FromRef; +use zinc_utils::{cfg_into_iter, from_ref::FromRef}; use crate::GenerateRandomTrace; @@ -247,6 +249,60 @@ where #[derive(Clone, Debug)] pub struct Sha256CompressionSliceUair(PhantomData); +/// Canonical SHA-256 compression state `[a, b, c, d, e, f, g, h]`. +pub type Sha256State = [u32; 8]; + +/// One 512-bit SHA-256 message block as sixteen big-endian words. +pub type Sha256MessageBlock = [u32; 16]; + +/// Errors surfaced by deterministic SHA-256 witness synthesis. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Sha256WitnessError { + /// The requested packed trace would not fit in the requested MLE size. + TraceTooSmall { + num_compressions: usize, + active_rows: usize, + num_vars: usize, + }, + /// A synthesized compression trace did not return the expected terminal state. + FinalStateMismatch { + index: usize, + expected: Sha256State, + got: Sha256State, + }, + /// Internal conversion from `Vec` to fixed-size array failed. + InternalLengthMismatch { expected: usize, got: usize }, +} + +impl fmt::Display for Sha256WitnessError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TraceTooSmall { + num_compressions, + active_rows, + num_vars, + } => write!( + f, + "trace too small for {num_compressions} SHA-256 compression(s): \ + {active_rows} active rows do not fit in 2^{num_vars} rows" + ), + Self::FinalStateMismatch { + index, + expected, + got, + } => write!( + f, + "SHA-256 witness {index} ended at {got:08x?}, expected {expected:08x?}" + ), + Self::InternalLengthMismatch { expected, got } => { + write!(f, "expected {expected} synthesized witness(es), got {got}") + } + } + } +} + +impl std::error::Error for Sha256WitnessError {} + /// Column indices within the flat trace (binary || arbitrary || int). /// /// All polynomial columns are bit-polynomials (stored as `binary_poly`); @@ -463,7 +519,8 @@ where ShiftSpec::new(cols::FLAT_W_E, 4), // w_sig1: Sigma_1(e[t]) at anchor t-3. ShiftSpec::new(cols::FLAT_W_SIG1, 3), - // w_W: message-schedule 9, 16 AND register-update 3. + // w_W: message-schedule shifts 9 and 16. Shift 3 is retained + // for signature-slot stability; round updates consume up.w_W. ShiftSpec::new(cols::FLAT_W_W, 3), ShiftSpec::new(cols::FLAT_W_W, 9), ShiftSpec::new(cols::FLAT_W_W, 16), @@ -721,7 +778,7 @@ where let _down_w_e_sh2 = &down.binary_poly[5]; let down_w_e_sh4 = &down.binary_poly[6]; let down_w_sig1_sh3 = &down.binary_poly[7]; - let down_w_w_sh3 = &down.binary_poly[8]; + let _down_w_w_sh3 = &down.binary_poly[8]; let down_w_w_sh9 = &down.binary_poly[9]; let down_w_w_sh16 = &down.binary_poly[10]; let down_w_lsig0_sh1 = &down.binary_poly[11]; @@ -877,7 +934,7 @@ where // a[t] = down.w_a^↓3 Sigma_1(e[t]) = down.w_sig1^↓3 // Sigma_0(a[t]) = down.w_sig0^↓3 u_ef[t] = down.w_u_ef^↓3 // u_{¬e,g}[t] = down.w_u_neg_e_g^↓3 - // Maj[t] = down.w_maj^↓3 W[t] = down.w_W^↓3 + // Maj[t] = down.w_maj^↓3 W[t] = up.w_W // K[t] = down.pa_K^↓3 mu_a[t] = down.w_mu_a^↓3 // pa_c_c8 is the witness compensator (see C7 note); zero-on-active // pinned in-circuit by C19: `pa_c_c8 · S_ACTIVE_UPD = 0`. @@ -887,7 +944,7 @@ where - down_w_u_ef_sh3 // Ch[t] = u_ef + u_{¬e,g} - down_w_u_neg_e_g_sh3 - down_pa_k_sh3 // K[t] - - down_w_w_sh3 // W[t] + - w_big_w // W[t] - down_w_sig0_sh3 // Sigma_0(a[t]) - down_w_maj_sh3 // Maj[t] + &mu_a_contrib; // = 2^32 · mu_a (bits 2-4 of W_MU_PACKED) @@ -906,7 +963,7 @@ where - down_w_u_ef_sh3 // Ch[t] = u_ef + u_{¬e,g} - down_w_u_neg_e_g_sh3 - down_pa_k_sh3 - - down_w_w_sh3 + - w_big_w + &mu_e_contrib; // = 2^32 · mu_e (bits 5-7 of W_MU_PACKED) b.assert_in_ideal(e_update_inner + pa_c_c9, &ideal_rot_x2); @@ -1231,588 +1288,713 @@ fn lsig1_overflow(w_val: u32, lsig1_val: u32) -> u32 { rotation_overflow(w_val, &[13, 15], shr10, lsig1_val) } -// --------------------------------------------------------------------------- -// GenerateRandomTrace for the slice. -// --------------------------------------------------------------------------- +fn state_to_trace_halves(state: Sha256State) -> ([u32; 4], [u32; 4]) { + ( + [state[3], state[2], state[1], state[0]], + [state[7], state[6], state[5], state[4]], + ) +} -impl GenerateRandomTrace<32> for Sha256CompressionSliceUair +fn trace_halves_to_state(h_a: [u32; 4], h_e: [u32; 4]) -> Sha256State { + [ + h_a[3], h_a[2], h_a[1], h_a[0], h_e[3], h_e[2], h_e[1], h_e[0], + ] +} + +/// Native SHA-256 compression of one 512-bit message block. +/// +/// The returned state is `initial_state + round_state` componentwise, in +/// canonical `[a, b, c, d, e, f, g, h]` order. +pub fn sha256_compress_native( + initial_state: Sha256State, + message_block: Sha256MessageBlock, +) -> Sha256State { + let mut w = [0u32; cols::ROUNDS_PER_COMP]; + w[..16].copy_from_slice(&message_block); + for t in 16..cols::ROUNDS_PER_COMP { + w[t] = w[t - 16] + .wrapping_add(small_sigma0(w[t - 15])) + .wrapping_add(w[t - 7]) + .wrapping_add(small_sigma1(w[t - 2])); + } + + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = initial_state; + for t in 0..cols::ROUNDS_PER_COMP { + let t1 = h + .wrapping_add(big_sigma1(e)) + .wrapping_add(ch(e, f, g)) + .wrapping_add(K_CANONICAL[t]) + .wrapping_add(w[t]); + let t2 = big_sigma0(a).wrapping_add(maj(a, b, c)); + h = g; + g = f; + f = e; + e = d.wrapping_add(t1); + d = c; + c = b; + b = a; + a = t1.wrapping_add(t2); + } + + [ + initial_state[0].wrapping_add(a), + initial_state[1].wrapping_add(b), + initial_state[2].wrapping_add(c), + initial_state[3].wrapping_add(d), + initial_state[4].wrapping_add(e), + initial_state[5].wrapping_add(f), + initial_state[6].wrapping_add(g), + initial_state[7].wrapping_add(h), + ] +} + +/// Synthesize one fresh SHA-256 compression UAIR trace. +pub fn synthesize_one_sha256_compression_trace( + initial_state: Sha256State, + message_block: Sha256MessageBlock, +) -> Result<(UairTrace<'static, R, R, 32>, Sha256State), Sha256WitnessError> where - R: ConstSemiring + From + 'static, + R: ConstSemiring + From + Clone + Send + Sync + 'static, { - type PolyCoeff = R; - type Int = R; + synthesize_sha256_compression_chain_trace::( + cols::MIN_NUM_VARS, + initial_state, + &[message_block], + ) +} - fn generate_random_trace( - num_vars: usize, - rng: &mut Rng, - ) -> UairTrace<'static, R, R, 32> { - let n = 1usize << num_vars; - assert!( - num_vars >= cols::MIN_NUM_VARS, - "trace too small for {} chained compressions: need num_vars ≥ {}, got {num_vars}", - cols::NUM_COMPRESSIONS, - cols::MIN_NUM_VARS, - ); +/// Synthesize `N` ordered fresh SHA-256 compression witnesses for a chain. +/// +/// State computation is sequential (`H_{i+1} = compress(H_i, M_i)`); once all +/// states are known, trace synthesis is parallelized when the crate's +/// `parallel` feature is enabled. +pub fn synthesize_sha256_chain_witnesses( + initial_state: Sha256State, + message_blocks: [Sha256MessageBlock; N], +) -> Result<([UairWitness<'static, R, R, 32>; N], Sha256State), Sha256WitnessError> +where + R: ConstSemiring + From + Clone + Send + Sync + 'static, +{ + let mut states = Vec::with_capacity(N + 1); + states.push(initial_state); + let mut state = initial_state; + for message_block in &message_blocks { + state = sha256_compress_native(state, *message_block); + states.push(state); + } - // ===== Chained-compression layout ===== - // - // Run NUM_COMPRESSIONS independent SHA-256 compressions chained - // via the spec's feed-forward addition `H_{i+1} = compress(H_i, - // M_i) + H_i mod 2^32` componentwise. Compression i ∈ [0, N) uses - // rows [i·RPC, (i+1)·RPC) where RPC = ROWS_PER_COMP = 68: - // - rows [start, start+4): init prefix (= H_i, pinned to pa_a/pa_e - // by S_INIT_PREFIX). Under the shift- - // register convention, w_a[start+j] holds - // H_i's (d, c, b, a) for j=0..3, w_e[start+j] - // holds H_i's (h, g, f, e). - // - rows [start+4, start+68): 64 round-update outputs. - // - rows [start+64, start+68): "junction window" — w_a/w_e hold - // internal_final_i; pa_a/pa_e hold a SECOND - // copy of H_i so the feed-forward constraint - // can read the prior init via `up.pa_a`. - // After the last compression, rows [N·RPC, N·RPC+4) hold the H_N output - // prefix, pinned by S_INIT_PREFIX in the same way. - // - // Slack rows [N·RPC + 4, n) are zero-padded; all SHA constraints - // are inactive there (compensators absorb C7/C8/C9; selectors - // gate off C13–C15 and the boundary/junction families). - let big_n = cols::NUM_COMPRESSIONS; - let rpc = cols::ROWS_PER_COMP; - let rounds = cols::ROUNDS_PER_COMP; - - // Trace-row buffers, all length n, zero-initialized. - let mut a_vals = vec![0u32; n]; - let mut e_vals = vec![0u32; n]; - let mut w_vals = vec![0u32; n]; - let mut k_vals = vec![0u32; n]; - let mut mu_w_vals = vec![0u32; n]; - let mut mu_a_vals = vec![0u32; n]; - let mut mu_e_vals = vec![0u32; n]; - let mut mu_junction_a_vals = vec![0u32; n]; - let mut mu_junction_e_vals = vec![0u32; n]; - - // pa_a / pa_e: H_i values at init-prefix rows (gated by - // S_INIT_PREFIX) AND at junction rows (read by the feed-forward - // constraint). Both copies hold the same H_i values; they live - // at different rows for different constraint uses. - let mut pa_a_vals = vec![0u32; n]; - let mut pa_e_vals = vec![0u32; n]; - // pa_m: per-compression message-block words. Holds M_i[0..16] - // at rows [start, start+16) for compression i; zero elsewhere. - // Pinned to w_W at those rows by C16 (s_msg_init). - let mut pa_m_vals = vec![0u32; n]; - - // H_0: random initial state for testing. Stored as two 4-arrays - // (d, c, b, a) for the a-half and (h, g, f, e) for the e-half, in - // the order they appear at the init prefix rows (so index j → row - // `start + j` directly). - let mut h_a: [u32; 4] = [ - rng.next_u32(), - rng.next_u32(), - rng.next_u32(), - rng.next_u32(), - ]; - let mut h_e: [u32; 4] = [ - rng.next_u32(), - rng.next_u32(), - rng.next_u32(), - rng.next_u32(), - ]; + let witnesses_vec = cfg_into_iter!(0..N) + .map(|index| { + let (trace, got) = + synthesize_one_sha256_compression_trace::(states[index], message_blocks[index])?; + let expected = states[index + 1]; + if got != expected { + return Err(Sha256WitnessError::FinalStateMismatch { + index, + expected, + got, + }); + } + Ok(UairWitness { trace }) + }) + .collect::, Sha256WitnessError>>()?; + + let got = witnesses_vec.len(); + let witnesses = witnesses_vec + .try_into() + .map_err(|_| Sha256WitnessError::InternalLengthMismatch { expected: N, got })?; + Ok((witnesses, states[N])) +} - for i in 0..big_n { - let start = i * rpc; +fn synthesize_sha256_compression_chain_trace( + num_vars: usize, + initial_state: Sha256State, + message_blocks: &[Sha256MessageBlock], +) -> Result<(UairTrace<'static, R, R, 32>, Sha256State), Sha256WitnessError> +where + R: ConstSemiring + From + Clone + 'static, +{ + let n = 1usize << num_vars; + let big_n = message_blocks.len(); + let rpc = cols::ROWS_PER_COMP; + let rounds = cols::ROUNDS_PER_COMP; + let active_rows = big_n * rpc + 4; + if active_rows > n { + return Err(Sha256WitnessError::TraceTooSmall { + num_compressions: big_n, + active_rows, + num_vars, + }); + } - // 1) Init prefix [start, start+4): pin to H_i. - for j in 0..4 { - a_vals[start + j] = h_a[j]; - e_vals[start + j] = h_e[j]; - pa_a_vals[start + j] = h_a[j]; - pa_e_vals[start + j] = h_e[j]; - } + // ===== Chained-compression layout ===== + // + // Run NUM_COMPRESSIONS independent SHA-256 compressions chained + // via the spec's feed-forward addition `H_{i+1} = compress(H_i, + // M_i) + H_i mod 2^32` componentwise. Compression i ∈ [0, N) uses + // rows [i·RPC, (i+1)·RPC) where RPC = ROWS_PER_COMP = 68: + // - rows [start, start+4): init prefix (= H_i, pinned to pa_a/pa_e + // by S_INIT_PREFIX). Under the shift- + // register convention, w_a[start+j] holds + // H_i's (d, c, b, a) for j=0..3, w_e[start+j] + // holds H_i's (h, g, f, e). + // - rows [start+4, start+68): 64 round-update outputs. + // - rows [start+64, start+68): "junction window" — w_a/w_e hold + // internal_final_i; pa_a/pa_e hold a SECOND + // copy of H_i so the feed-forward constraint + // can read the prior init via `up.pa_a`. + // After the last compression, rows [N·RPC, N·RPC+4) hold the H_N output + // prefix, pinned by S_INIT_PREFIX in the same way. + // + // Slack rows [N·RPC + 4, n) are zero-padded; all SHA constraints + // are inactive there (compensators absorb C7/C8/C9; selectors + // gate off C13–C15 and the boundary/junction families). + // Trace-row buffers, all length n, zero-initialized. + let mut a_vals = vec![0u32; n]; + let mut e_vals = vec![0u32; n]; + let mut w_vals = vec![0u32; n]; + let mut k_vals = vec![0u32; n]; + let mut mu_w_vals = vec![0u32; n]; + let mut mu_a_vals = vec![0u32; n]; + let mut mu_e_vals = vec![0u32; n]; + let mut mu_junction_a_vals = vec![0u32; n]; + let mut mu_junction_e_vals = vec![0u32; n]; + + // pa_a / pa_e: H_i values at init-prefix rows (gated by + // S_INIT_PREFIX) AND at junction rows (read by the feed-forward + // constraint). Both copies hold the same H_i values; they live + // at different rows for different constraint uses. + let mut pa_a_vals = vec![0u32; n]; + let mut pa_e_vals = vec![0u32; n]; + // pa_m: per-compression message-block words. Holds M_i[0..16] + // at rows [start, start+16) for compression i; zero elsewhere. + // Pinned to w_W at those rows by C16 (s_msg_init). + let mut pa_m_vals = vec![0u32; n]; + + // H_0: caller-supplied initial state. Stored as two 4-arrays + // (d, c, b, a) for the a-half and (h, g, f, e) for the e-half, in + // the order they appear at the init prefix rows (so index j → row + // `start + j` directly). + let (mut h_a, mut h_e) = state_to_trace_halves(initial_state); + + for i in 0..big_n { + let start = i * rpc; + + // 1) Init prefix [start, start+4): pin to H_i. + for j in 0..4 { + a_vals[start + j] = h_a[j]; + e_vals[start + j] = h_e[j]; + pa_a_vals[start + j] = h_a[j]; + pa_e_vals[start + j] = h_e[j]; + } - // 2) Per-compression message block. 16 random seeds (which - // also populate the public pa_m column so C16 pins them), - // then 48 derived via the SHA-256 message-schedule - // recurrence — contained entirely within compression i's - // window. - for j in 0..16 { - let m_word = rng.next_u32(); - w_vals[start + j] = m_word; - pa_m_vals[start + j] = m_word; - } - for j in 16..rpc { - let t = start + j; - let sum_u64: u64 = (w_vals[t - 16] as u64) - + (small_sigma0(w_vals[t - 15]) as u64) - + (w_vals[t - 7] as u64) - + (small_sigma1(w_vals[t - 2]) as u64); - w_vals[t] = sum_u64 as u32; - let carry = (sum_u64 >> 32) as u32; - debug_assert!(carry <= 3, "message-schedule carry out of [0,3]: {carry}"); - // Store mu_W at the C7 anchor row k = t − 16 (not at - // spec row t) so C7 reads it via `up.w_mu_packed` bits - // 0-1 with no shift. - mu_w_vals[t - 16] = carry; - } + // 2) Per-compression message block. 16 caller-supplied seeds + // (which also populate the public pa_m column so C16 pins them), + // then 48 derived via the SHA-256 message-schedule + // recurrence — contained entirely within compression i's + // window. + for j in 0..16 { + let m_word = message_blocks[i][j]; + w_vals[start + j] = m_word; + pa_m_vals[start + j] = m_word; + } + for j in 16..rpc { + let t = start + j; + let sum_u64: u64 = (w_vals[t - 16] as u64) + + (small_sigma0(w_vals[t - 15]) as u64) + + (w_vals[t - 7] as u64) + + (small_sigma1(w_vals[t - 2]) as u64); + w_vals[t] = sum_u64 as u32; + let carry = (sum_u64 >> 32) as u32; + debug_assert!(carry <= 3, "message-schedule carry out of [0,3]: {carry}"); + // Store mu_W at the C7 anchor row k = t − 16 (not at + // spec row t) so C7 reads it via `up.w_mu_packed` bits + // 0-1 with no shift. + mu_w_vals[t - 16] = carry; + } - // 3) Per-compression round constants. Cycle the canonical - // SHA-256 K table per compression at rows - // `[start + 3, start + 67)` so that C8/C9 at active - // anchors `k ∈ [start, start + 64)` (which read - // `down.pa_K^↓3 = pa_K[k+3]`) see `K_CANONICAL[k - start]`. - // Rows `start..start+3` and `start+67` are not read by - // any active anchor of compression i, so they're left - // as zero. (The compensator pa_c_c8/c9 absorbs whatever - // those rows contain.) - for j in 0..cols::ROUNDS_PER_COMP { - k_vals[start + 3 + j] = K_CANONICAL[j]; - } + // 3) Per-compression round constants. Cycle the canonical + // SHA-256 K table per compression at rows + // `[start + 3, start + 67)` so that C8/C9 at active + // anchors `k ∈ [start, start + 64)` (which read + // `down.pa_K^↓3 = pa_K[k+3]`) see `K_CANONICAL[k - start]`. + // Rows `start..start+3` and `start+67` are not read by + // any active anchor of compression i, so they're left + // as zero. (The compensator pa_c_c8/c9 absorbs whatever + // those rows contain.) + for j in 0..cols::ROUNDS_PER_COMP { + k_vals[start + 3 + j] = K_CANONICAL[j]; + } - // 4) Round-update: 64 rounds, anchor k = start+0..=start+63 - // produces a[k+4]/e[k+4] from the 4-row window a[k..=k+3] - // / e[k..=k+3]. All back-references stay within - // compression i (the first round's reads land on the init - // prefix at [start, start+4); the last round's reads land - // on rows [start+60, start+64)). - // - // Bounds: T1 = h + Σ_1(e) + Ch + K + W (5 terms of <2^32). - // T2 = Σ_0(a) + Maj (2 terms). - // a_sum = T1 + T2 (7 terms ⇒ mu_a ∈ {0..=6}). - // e_sum = d + T1 (6 terms ⇒ mu_e ∈ {0..=5}). - for j in 0..rounds { - let k = start + j; - let t = k + 3; // spec round number under the t = k+3 anchor convention - - let a_t = a_vals[k + 3]; // a[t] - let a_t1 = a_vals[k + 2]; // a[t-1] = b - let a_t2 = a_vals[k + 1]; // a[t-2] = c - let e_t = e_vals[k + 3]; // e[t] - let e_t1 = e_vals[k + 2]; // e[t-1] = f - let e_t2 = e_vals[k + 1]; // e[t-2] = g - - let sig0_a_t = big_sigma0(a_t); - let sig1_e_t = big_sigma1(e_t); - let ch_t = ch(e_t, e_t1, e_t2); - let maj_t = maj(a_t, a_t1, a_t2); - - let t1: u64 = (e_vals[k] as u64) // h = e[t-3] + // 4) Round-update: 64 rounds, anchor k = start+0..=start+63 + // produces a[k+4]/e[k+4] from the 4-row window a[k..=k+3] + // / e[k..=k+3]. All back-references stay within + // compression i (the first round's reads land on the init + // prefix at [start, start+4); the last round's reads land + // on rows [start+60, start+64)). + // + // Bounds: T1 = h + Σ_1(e) + Ch + K + W (5 terms of <2^32). + // T2 = Σ_0(a) + Maj (2 terms). + // a_sum = T1 + T2 (7 terms ⇒ mu_a ∈ {0..=6}). + // e_sum = d + T1 (6 terms ⇒ mu_e ∈ {0..=5}). + for j in 0..rounds { + let k = start + j; + let t = k + 3; // register/K row under the t = k+3 anchor convention + + let a_t = a_vals[k + 3]; // a[t] + let a_t1 = a_vals[k + 2]; // a[t-1] = b + let a_t2 = a_vals[k + 1]; // a[t-2] = c + let e_t = e_vals[k + 3]; // e[t] + let e_t1 = e_vals[k + 2]; // e[t-1] = f + let e_t2 = e_vals[k + 1]; // e[t-2] = g + + let sig0_a_t = big_sigma0(a_t); + let sig1_e_t = big_sigma1(e_t); + let ch_t = ch(e_t, e_t1, e_t2); + let maj_t = maj(a_t, a_t1, a_t2); + + let t1: u64 = (e_vals[k] as u64) // h = e[t-3] + (sig1_e_t as u64) + (ch_t as u64) + (k_vals[t] as u64) - + (w_vals[t] as u64); - let t2: u64 = (sig0_a_t as u64) + (maj_t as u64); - let a_sum: u64 = t1 + t2; - let e_sum: u64 = (a_vals[k] as u64) + t1; // d + T1, d = a[t-3] - - a_vals[k + 4] = a_sum as u32; - e_vals[k + 4] = e_sum as u32; - let mu_a_t = (a_sum >> 32) as u32; - let mu_e_t = (e_sum >> 32) as u32; - debug_assert!(mu_a_t <= 6, "mu_a out of [0,6]: {mu_a_t}"); - debug_assert!(mu_e_t <= 5, "mu_e out of [0,5]: {mu_e_t}"); - // Store mu_a/mu_e at the C8/C9 anchor row k (not at - // spec row t = k+3) so C8/C9 read via `up.w_mu_packed` - // bits 2-4 / 5-7 with no shift. - mu_a_vals[k] = mu_a_t; - mu_e_vals[k] = mu_e_t; - } - - // 5) Feed-forward: H_{i+1} = internal_final_i + H_i mod 2^32 - // componentwise. internal_final_i lives at rows - // [start+64, start+68); we place a second copy of H_i in - // pa_a/pa_e at the same rows (so the feed-forward - // constraint can read the prior init via `up.pa_a`), and - // record the per-component carry in w_mu_junction_{a,e}. - // Each carry is in {0, 1} since both summands are < 2^32. - let mut h_a_next: [u32; 4] = [0; 4]; - let mut h_e_next: [u32; 4] = [0; 4]; - for j in 0..4 { - let internal_a = a_vals[start + 64 + j]; - let internal_e = e_vals[start + 64 + j]; - let prior_a = h_a[j]; - let prior_e = h_e[j]; - let sum_a: u64 = (internal_a as u64) + (prior_a as u64); - let sum_e: u64 = (internal_e as u64) + (prior_e as u64); - h_a_next[j] = sum_a as u32; - h_e_next[j] = sum_e as u32; - let carry_a = (sum_a >> 32) as u32; - let carry_e = (sum_e >> 32) as u32; - debug_assert!( - carry_a <= 1, - "feed-forward a-carry out of {{0,1}}: {carry_a}" - ); - debug_assert!( - carry_e <= 1, - "feed-forward e-carry out of {{0,1}}: {carry_e}" - ); - - pa_a_vals[start + 64 + j] = prior_a; - pa_e_vals[start + 64 + j] = prior_e; - mu_junction_a_vals[start + 64 + j] = carry_a; - mu_junction_e_vals[start + 64 + j] = carry_e; - } - h_a = h_a_next; - h_e = h_e_next; + + (w_vals[k] as u64); + let t2: u64 = (sig0_a_t as u64) + (maj_t as u64); + let a_sum: u64 = t1 + t2; + let e_sum: u64 = (a_vals[k] as u64) + t1; // d + T1, d = a[t-3] + + a_vals[k + 4] = a_sum as u32; + e_vals[k + 4] = e_sum as u32; + let mu_a_t = (a_sum >> 32) as u32; + let mu_e_t = (e_sum >> 32) as u32; + debug_assert!(mu_a_t <= 6, "mu_a out of [0,6]: {mu_a_t}"); + debug_assert!(mu_e_t <= 5, "mu_e out of [0,5]: {mu_e_t}"); + // Store mu_a/mu_e at the C8/C9 anchor row k (not at + // spec row t = k+3) so C8/C9 read via `up.w_mu_packed` + // bits 2-4 / 5-7 with no shift. + mu_a_vals[k] = mu_a_t; + mu_e_vals[k] = mu_e_t; } - // 6) H_N output prefix at rows [big_n·rpc, big_n·rpc + 4): pin - // to H_N (the final compression's output) so the verifier can - // read the digest from the public columns. - let h_out_start = big_n * rpc; + // 5) Feed-forward: H_{i+1} = internal_final_i + H_i mod 2^32 + // componentwise. internal_final_i lives at rows + // [start+64, start+68); we place a second copy of H_i in + // pa_a/pa_e at the same rows (so the feed-forward + // constraint can read the prior init via `up.pa_a`), and + // record the per-component carry in w_mu_junction_{a,e}. + // Each carry is in {0, 1} since both summands are < 2^32. + let mut h_a_next: [u32; 4] = [0; 4]; + let mut h_e_next: [u32; 4] = [0; 4]; for j in 0..4 { - a_vals[h_out_start + j] = h_a[j]; - e_vals[h_out_start + j] = h_e[j]; - pa_a_vals[h_out_start + j] = h_a[j]; - pa_e_vals[h_out_start + j] = h_e[j]; + let internal_a = a_vals[start + 64 + j]; + let internal_e = e_vals[start + 64 + j]; + let prior_a = h_a[j]; + let prior_e = h_e[j]; + let sum_a: u64 = (internal_a as u64) + (prior_a as u64); + let sum_e: u64 = (internal_e as u64) + (prior_e as u64); + h_a_next[j] = sum_a as u32; + h_e_next[j] = sum_e as u32; + let carry_a = (sum_a >> 32) as u32; + let carry_e = (sum_e >> 32) as u32; + debug_assert!( + carry_a <= 1, + "feed-forward a-carry out of {{0,1}}: {carry_a}" + ); + debug_assert!( + carry_e <= 1, + "feed-forward e-carry out of {{0,1}}: {carry_e}" + ); + + pa_a_vals[start + 64 + j] = prior_a; + pa_e_vals[start + 64 + j] = prior_e; + mu_junction_a_vals[start + 64 + j] = carry_a; + mu_junction_e_vals[start + 64 + j] = carry_e; } + h_a = h_a_next; + h_e = h_e_next; + } - // ===== Per-row Ch / Maj operand witnesses ===== - // - // Computed honestly on every row from a_vals / e_vals contents. - // The truth-table values must hold on every row (not only - // SHA-active ones) to keep the Ch/Maj virtual residuals - // (`r_ch1` / `r_ch2` / `r_maj`, declared in `signature()`'s - // `with_virtual_binary_poly_cols`) bit-valid per coefficient - // across compression-junction boundaries: the booleanity - // sumcheck checks every row, including ones the spec doesn't - // care about. - let u_ef_vals: Vec = (0..n) - .map(|t| if t >= 1 { e_vals[t] & e_vals[t - 1] } else { 0 }) - .collect(); - let u_neg_e_g_vals: Vec = (0..n) - .map(|t| { - if t >= 2 { - (!e_vals[t]) & e_vals[t - 2] - } else { - 0 - } - }) - .collect(); - let maj_vals: Vec = (0..n) - .map(|t| { - if t >= 2 { - maj(a_vals[t], a_vals[t - 1], a_vals[t - 2]) - } else { - 0 - } - }) - .collect(); + // 6) H_N output prefix at rows [big_n·rpc, big_n·rpc + 4): pin + // to H_N (the final compression's output) so the verifier can + // read the digest from the public columns. + let h_out_start = big_n * rpc; + for j in 0..4 { + a_vals[h_out_start + j] = h_a[j]; + e_vals[h_out_start + j] = h_e[j]; + pa_a_vals[h_out_start + j] = h_a[j]; + pa_e_vals[h_out_start + j] = h_e[j]; + } - // ===== Tail compensators for the Ch (63) / Maj (64) virtual residuals ===== - // - // Zero on every row except `k ∈ {n−2, n−1}` where the length-2 - // forward shifts in r_ch2 / r_maj read into off-trace zero- - // padding and the residual would slip outside `{0,1}` per - // coefficient. Match the compensator logic in option-a-virtual- - // residuals (8787cbd): - // r_ch2 (alt complement form) at boundary k = n-2 / n-1: - // u_{¬e,g}[k+2] = 0 (off-trace), e[k+2] = 0, e[k] real. - // residual = -e[k] + 2·comp_ch2 ∈ {0,1} ⇒ comp_ch2[k] = e[k]. - // r_maj at boundary k = n-2: - // a[k+2] = Maj[k+2] = 0 (off-trace), a[k+1] real. - // residual = a[k] + a[k+1] − 2·comp_maj ∈ {0,1} - // ⇒ comp_maj[k] = AND(a[k], a[k+1]). - // r_maj at k = n-1: a[k+1] = a[k+2] = 0, residual = a[k] ∈ - // {0,1} already; comp_maj = 0. - let mut pa_r_ch2_comp_vals: Vec = vec![0; n]; - let mut pa_r_maj_comp_vals: Vec = vec![0; n]; - for k in 0..n { - let off_kp1 = k + 1 >= n; - let off_kp2 = k + 2 >= n; - if off_kp2 { - pa_r_ch2_comp_vals[k] = e_vals[k]; + // ===== Per-row Ch / Maj operand witnesses ===== + // + // Computed honestly on every row from a_vals / e_vals contents. + // The truth-table values must hold on every row (not only + // SHA-active ones) to keep the Ch/Maj virtual residuals + // (`r_ch1` / `r_ch2` / `r_maj`, declared in `signature()`'s + // `with_virtual_binary_poly_cols`) bit-valid per coefficient + // across compression-junction boundaries: the booleanity + // sumcheck checks every row, including ones the spec doesn't + // care about. + let u_ef_vals: Vec = (0..n) + .map(|t| if t >= 1 { e_vals[t] & e_vals[t - 1] } else { 0 }) + .collect(); + let u_neg_e_g_vals: Vec = (0..n) + .map(|t| { + if t >= 2 { + (!e_vals[t]) & e_vals[t - 2] + } else { + 0 } - if off_kp2 && !off_kp1 { - pa_r_maj_comp_vals[k] = a_vals[k] & a_vals[k + 1]; + }) + .collect(); + let maj_vals: Vec = (0..n) + .map(|t| { + if t >= 2 { + maj(a_vals[t], a_vals[t - 1], a_vals[t - 2]) + } else { + 0 } - } + }) + .collect(); - // Derived values. - let sig0_vals: Vec = a_vals.iter().copied().map(big_sigma0).collect(); - let sig1_vals: Vec = e_vals.iter().copied().map(big_sigma1).collect(); - let lsig0_vals: Vec = w_vals.iter().copied().map(small_sigma0).collect(); - let lsig1_vals: Vec = w_vals.iter().copied().map(small_sigma1).collect(); - - let ov_sig0_vals: Vec = a_vals - .iter() - .zip(&sig0_vals) - .map(|(&a, &s)| sigma0_overflow(a, s)) - .collect(); - let ov_sig1_vals: Vec = e_vals - .iter() - .zip(&sig1_vals) - .map(|(&e, &s)| sigma1_overflow(e, s)) - .collect(); - let ov_lsig0_vals: Vec = w_vals - .iter() - .zip(&lsig0_vals) - .map(|(&w, &l)| lsig0_overflow(w, l)) - .collect(); - let ov_lsig1_vals: Vec = w_vals - .iter() - .zip(&lsig1_vals) - .map(|(&w, &l)| lsig1_overflow(w, l)) - .collect(); - - // The σ_0/σ_1 right-shift decomposition columns S_i / T_i are - // gone — their role (carrying SHR(W, k) for the F_2[X] sum) is - // taken over by the `BitOp::ShiftR(k)` virtual columns over W. - // `lsig0_overflow` / `lsig1_overflow` already compute the - // matching `pa_ov_lsig{0,1}` per-bit values for the new - // constraint (the algebraic identity is unchanged). - - // Pack all 5 carries per row into the W_MU_PACKED binary_poly - // column. Each carry was stored at its constraint's anchor row - // (mu_W at C7-anchor k = t-16, mu_a/mu_e at C8/C9-anchor k = - // t-3, mu_ff_a/e at junction-anchor k). Bit layout: - // bits 0-1: mu_W, 2-4: mu_a, 5-7: mu_e, 8: mu_ff_a, 9: mu_ff_e. - // Positions 10..31 stay 0 (pinned by C22's high-bits-zero - // assert_zero on ShiftR(10)(W_MU_PACKED)). - let w_mu_packed_vals: Vec = (0..n) - .map(|k| { - (mu_w_vals[k] & 0b11) - | ((mu_a_vals[k] & 0b111) << 2) - | ((mu_e_vals[k] & 0b111) << 5) - | ((mu_junction_a_vals[k] & 0b1) << 8) - | ((mu_junction_e_vals[k] & 0b1) << 9) - }) - .collect(); - - let to_bits = |v: &[u32]| -> Vec> { - v.iter().copied().map(BinaryPoly::<32>::from).collect() - }; - - let to_bin_mle = |col: Vec>| -> DenseMultilinearExtension> { - col.into_iter().collect() - }; - - // Layout: 8 public bin_poly cols (PA_A, PA_E, PA_OV_SIG0, - // PA_OV_SIG1, PA_OV_LSIG0, PA_OV_LSIG1, PA_R_CH2_COMP, - // PA_R_MAJ_COMP) + 10 witness cols. pa_a / pa_e were populated - // above with H_i values at init-prefix rows (for compression i - // and the H_N output block) AND at junction rows (where the - // feed-forward constraint reads the prior H_i). The two - // PA_R_*_COMP columns are zero except on the trace tail. - let binary_poly = vec![ - to_bin_mle(to_bits(&pa_a_vals)), - to_bin_mle(to_bits(&pa_e_vals)), - to_bin_mle(to_bits(&ov_sig0_vals)), - to_bin_mle(to_bits(&ov_sig1_vals)), - to_bin_mle(to_bits(&ov_lsig0_vals)), - to_bin_mle(to_bits(&ov_lsig1_vals)), - to_bin_mle(to_bits(&pa_r_ch2_comp_vals)), - to_bin_mle(to_bits(&pa_r_maj_comp_vals)), - to_bin_mle(to_bits(&pa_m_vals)), - to_bin_mle(to_bits(&a_vals)), - to_bin_mle(to_bits(&sig0_vals)), - to_bin_mle(to_bits(&e_vals)), - to_bin_mle(to_bits(&sig1_vals)), - to_bin_mle(to_bits(&w_vals)), - to_bin_mle(to_bits(&lsig0_vals)), - to_bin_mle(to_bits(&lsig1_vals)), - to_bin_mle(to_bits(&u_ef_vals)), - to_bin_mle(to_bits(&u_neg_e_g_vals)), - to_bin_mle(to_bits(&maj_vals)), - to_bin_mle(to_bits(&w_mu_packed_vals)), - ]; + // ===== Tail compensators for the Ch (63) / Maj (64) virtual residuals ===== + // + // Zero on every row except `k ∈ {n−2, n−1}` where the length-2 + // forward shifts in r_ch2 / r_maj read into off-trace zero- + // padding and the residual would slip outside `{0,1}` per + // coefficient. Match the compensator logic in option-a-virtual- + // residuals (8787cbd): + // r_ch2 (alt complement form) at boundary k = n-2 / n-1: + // u_{¬e,g}[k+2] = 0 (off-trace), e[k+2] = 0, e[k] real. + // residual = -e[k] + 2·comp_ch2 ∈ {0,1} ⇒ comp_ch2[k] = e[k]. + // r_maj at boundary k = n-2: + // a[k+2] = Maj[k+2] = 0 (off-trace), a[k+1] real. + // residual = a[k] + a[k+1] − 2·comp_maj ∈ {0,1} + // ⇒ comp_maj[k] = AND(a[k], a[k+1]). + // r_maj at k = n-1: a[k+1] = a[k+2] = 0, residual = a[k] ∈ + // {0,1} already; comp_maj = 0. + let mut pa_r_ch2_comp_vals: Vec = vec![0; n]; + let mut pa_r_maj_comp_vals: Vec = vec![0; n]; + for k in 0..n { + let off_kp1 = k + 1 >= n; + let off_kp2 = k + 2 >= n; + if off_kp2 { + pa_r_ch2_comp_vals[k] = e_vals[k]; + } + if off_kp2 && !off_kp1 { + pa_r_maj_comp_vals[k] = a_vals[k] & a_vals[k + 1]; + } + } - // ===== Selectors ===== - // - // s_init_prefix: 1 on the init-prefix windows for every compression - // (4 rows × NUM_COMPRESSIONS) plus the H_N output - // block (4 more rows). Pins w_a / w_e to pa_a / pa_e. - // s_feedforward: 1 on the junction windows [start+64, start+68) for - // every compression. Gates the SHA-256 inter- - // compression addition constraint. - let mut s_init_prefix_col: Vec = (0..n).map(|_| R::ZERO).collect(); - for i in 0..=big_n { - // i = big_n: the H_N output block. - for j in 0..4 { - s_init_prefix_col[i * rpc + j] = R::ONE; - } + // Derived values. + let sig0_vals: Vec = a_vals.iter().copied().map(big_sigma0).collect(); + let sig1_vals: Vec = e_vals.iter().copied().map(big_sigma1).collect(); + let lsig0_vals: Vec = w_vals.iter().copied().map(small_sigma0).collect(); + let lsig1_vals: Vec = w_vals.iter().copied().map(small_sigma1).collect(); + + let ov_sig0_vals: Vec = a_vals + .iter() + .zip(&sig0_vals) + .map(|(&a, &s)| sigma0_overflow(a, s)) + .collect(); + let ov_sig1_vals: Vec = e_vals + .iter() + .zip(&sig1_vals) + .map(|(&e, &s)| sigma1_overflow(e, s)) + .collect(); + let ov_lsig0_vals: Vec = w_vals + .iter() + .zip(&lsig0_vals) + .map(|(&w, &l)| lsig0_overflow(w, l)) + .collect(); + let ov_lsig1_vals: Vec = w_vals + .iter() + .zip(&lsig1_vals) + .map(|(&w, &l)| lsig1_overflow(w, l)) + .collect(); + + // The σ_0/σ_1 right-shift decomposition columns S_i / T_i are + // gone — their role (carrying SHR(W, k) for the F_2[X] sum) is + // taken over by the `BitOp::ShiftR(k)` virtual columns over W. + // `lsig0_overflow` / `lsig1_overflow` already compute the + // matching `pa_ov_lsig{0,1}` per-bit values for the new + // constraint (the algebraic identity is unchanged). + + // Pack all 5 carries per row into the W_MU_PACKED binary_poly + // column. Each carry was stored at its constraint's anchor row + // (mu_W at C7-anchor k = t-16, mu_a/mu_e at C8/C9-anchor k = + // t-3, mu_ff_a/e at junction-anchor k). Bit layout: + // bits 0-1: mu_W, 2-4: mu_a, 5-7: mu_e, 8: mu_ff_a, 9: mu_ff_e. + // Positions 10..31 stay 0 (pinned by C22's high-bits-zero + // assert_zero on ShiftR(10)(W_MU_PACKED)). + let w_mu_packed_vals: Vec = (0..n) + .map(|k| { + (mu_w_vals[k] & 0b11) + | ((mu_a_vals[k] & 0b111) << 2) + | ((mu_e_vals[k] & 0b111) << 5) + | ((mu_junction_a_vals[k] & 0b1) << 8) + | ((mu_junction_e_vals[k] & 0b1) << 9) + }) + .collect(); + + let to_bits = |v: &[u32]| -> Vec> { + v.iter().copied().map(BinaryPoly::<32>::from).collect() + }; + + let to_bin_mle = |col: Vec>| -> DenseMultilinearExtension> { + col.into_iter().collect() + }; + + // Layout: 8 public bin_poly cols (PA_A, PA_E, PA_OV_SIG0, + // PA_OV_SIG1, PA_OV_LSIG0, PA_OV_LSIG1, PA_R_CH2_COMP, + // PA_R_MAJ_COMP) + 10 witness cols. pa_a / pa_e were populated + // above with H_i values at init-prefix rows (for compression i + // and the H_N output block) AND at junction rows (where the + // feed-forward constraint reads the prior H_i). The two + // PA_R_*_COMP columns are zero except on the trace tail. + let binary_poly = vec![ + to_bin_mle(to_bits(&pa_a_vals)), + to_bin_mle(to_bits(&pa_e_vals)), + to_bin_mle(to_bits(&ov_sig0_vals)), + to_bin_mle(to_bits(&ov_sig1_vals)), + to_bin_mle(to_bits(&ov_lsig0_vals)), + to_bin_mle(to_bits(&ov_lsig1_vals)), + to_bin_mle(to_bits(&pa_r_ch2_comp_vals)), + to_bin_mle(to_bits(&pa_r_maj_comp_vals)), + to_bin_mle(to_bits(&pa_m_vals)), + to_bin_mle(to_bits(&a_vals)), + to_bin_mle(to_bits(&sig0_vals)), + to_bin_mle(to_bits(&e_vals)), + to_bin_mle(to_bits(&sig1_vals)), + to_bin_mle(to_bits(&w_vals)), + to_bin_mle(to_bits(&lsig0_vals)), + to_bin_mle(to_bits(&lsig1_vals)), + to_bin_mle(to_bits(&u_ef_vals)), + to_bin_mle(to_bits(&u_neg_e_g_vals)), + to_bin_mle(to_bits(&maj_vals)), + to_bin_mle(to_bits(&w_mu_packed_vals)), + ]; + + // ===== Selectors ===== + // + // s_init_prefix: 1 on the init-prefix windows for every compression + // (4 rows × NUM_COMPRESSIONS) plus the H_N output + // block (4 more rows). Pins w_a / w_e to pa_a / pa_e. + // s_feedforward: 1 on the junction windows [start+64, start+68) for + // every compression. Gates the SHA-256 inter- + // compression addition constraint. + let mut s_init_prefix_col: Vec = (0..n).map(|_| R::ZERO).collect(); + for i in 0..=big_n { + // i = big_n: the H_N output block. + for j in 0..4 { + s_init_prefix_col[i * rpc + j] = R::ONE; } - let mut s_feedforward_col: Vec = (0..n).map(|_| R::ZERO).collect(); - for i in 0..big_n { - for j in 0..4 { - s_feedforward_col[i * rpc + 64 + j] = R::ONE; - } + } + let mut s_feedforward_col: Vec = (0..n).map(|_| R::ZERO).collect(); + for i in 0..big_n { + for j in 0..4 { + s_feedforward_col[i * rpc + 64 + j] = R::ONE; } - // s_msg_init: 1 on the 16 message-block-seed rows of every - // compression, 0 elsewhere. Gates C16 (`w_W − pa_m == 0`). - let mut s_msg_init_col: Vec = (0..n).map(|_| R::ZERO).collect(); - for i in 0..big_n { - for j in 0..16 { - s_msg_init_col[i * rpc + j] = R::ONE; - } + } + // s_msg_init: 1 on the 16 message-block-seed rows of every + // compression, 0 elsewhere. Gates C16 (`w_W − pa_m == 0`). + let mut s_msg_init_col: Vec = (0..n).map(|_| R::ZERO).collect(); + for i in 0..big_n { + for j in 0..16 { + s_msg_init_col[i * rpc + j] = R::ONE; } - // s_active_sched / s_active_upd: pin each compensator to 0 on - // its constraint's honest active range. Read by the - // `pa_c_* · s_active_* == 0` zero-ideal constraints in - // `constrain_general`. - // - // s_active_sched: 1 on C7's 48 anchors per compression - // [start, start + ROUNDS_PER_COMP - 16), 0 elsewhere. - // s_active_upd: 1 on C8/C9's 64 anchors per compression - // [start, start + ROUNDS_PER_COMP), 0 elsewhere. - let mut s_active_sched_col: Vec = (0..n).map(|_| R::ZERO).collect(); - let mut s_active_upd_col: Vec = (0..n).map(|_| R::ZERO).collect(); - for i in 0..big_n { - let start = i * rpc; - for j in 0..(rounds - 16) { - s_active_sched_col[start + j] = R::ONE; - } - for j in 0..rounds { - s_active_upd_col[start + j] = R::ONE; - } + } + // s_active_sched / s_active_upd: pin each compensator to 0 on + // its constraint's honest active range. Read by the + // `pa_c_* · s_active_* == 0` zero-ideal constraints in + // `constrain_general`. + // + // s_active_sched: 1 on C7's 48 anchors per compression + // [start, start + ROUNDS_PER_COMP - 16), 0 elsewhere. + // s_active_upd: 1 on C8/C9's 64 anchors per compression + // [start, start + ROUNDS_PER_COMP), 0 elsewhere. + let mut s_active_sched_col: Vec = (0..n).map(|_| R::ZERO).collect(); + let mut s_active_upd_col: Vec = (0..n).map(|_| R::ZERO).collect(); + for i in 0..big_n { + let start = i * rpc; + for j in 0..(rounds - 16) { + s_active_sched_col[start + j] = R::ONE; } - // (PA_C_FF_{A,E} reuse `s_feedforward_col` as their - // compensator-zero selector — it is already 1 exactly on the - // junction window where the feed-forward addition holds - // honestly.) - - let k_col: Vec = k_vals.iter().copied().map(R::from).collect(); - // mu_w_vals / mu_a_vals / mu_e_vals / mu_junction_{a,e}_vals - // are no longer materialized as separate int columns — they're - // packed into the W_MU_PACKED binary_poly column above. + for j in 0..rounds { + s_active_upd_col[start + j] = R::ONE; + } + } + // (PA_C_FF_{A,E} reuse `s_feedforward_col` as their + // compensator-zero selector — it is already 1 exactly on the + // junction window where the feed-forward addition holds + // honestly.) - // ----- Compensator columns (replace s_sched_anch / s_upd_anch). ----- - // - // For each constraint Cᵢ ∈ {C7, C8, C9}, we publish a public column - // `pa_c_cᵢ[k]` with the property that (innerᵢ + pa_c_cᵢ) ∈ (X − 2) - // on every row k. Concretely we pick `pa_c_cᵢ[k] = −innerᵢ(2)` mod - // R's modulus; the protocol projects R into the random field, so - // the negation lands as `−innerᵢ(2) mod p` — exactly the value - // needed for the constraint to lie in (X − 2). - // - // On the corresponding active range (where the original selector - // was 1), the SHA recurrence makes `innerᵢ(2) = 0` for an honest - // prover, so `pa_c_cᵢ[k] = 0` automatically. On inactive rows the - // compensator absorbs whatever `innerᵢ(2)` happens to be. - let two_to_32: R = R::from(0x10000u32) * &R::from(0x10000u32); - let load = - |arr: &[u32], idx: usize| -> R { if idx < n { R::from(arr[idx]) } else { R::ZERO } }; - - // C7: inner(2) = w_W[k+16] − w_W[k] − lsig0[k+1] − w_W[k+9] - // − lsig1[k+14] + 2^32 · mu_W[k+16] - let pa_c_c7_col: Vec = (0..n) - .map(|k| { - let w_k16 = load(&w_vals, k + 16); - let w_k = load(&w_vals, k); - let lsig0_k1 = load(&lsig0_vals, k + 1); - let w_k9 = load(&w_vals, k + 9); - let lsig1_k14 = load(&lsig1_vals, k + 14); - // mu_W stored at C7-anchor row k (= round t = k+16 was - // formerly stored at k+16; with chained-comp re-anchoring - // it's now at row k). - let mu_k = load(&mu_w_vals, k); - let two32_mu = two_to_32.clone() * &mu_k; - // comp = −inner(2) = w_k + lsig0_k1 + w_k9 + lsig1_k14 - // − 2^32·mu_k16 − w_k16 - w_k + &lsig0_k1 + &w_k9 + &lsig1_k14 - &two32_mu - &w_k16 - }) - .collect(); - - // C8: inner(2) = w_a[k+4] − w_e[k] − sig1[k+3] − Ch[k+3] − K[k+3] - // − W[k+3] − sig0[k+3] − maj[k+3] + 2^32 · mu_a[k+3] - // with Ch[k+3] = u_ef[k+3] + u_{¬e,g}[k+3]. - let pa_c_c8_col: Vec = (0..n) - .map(|k| { - let w_a_k4 = load(&a_vals, k + 4); - let w_e_k = load(&e_vals, k); - let sig1_k3 = load(&sig1_vals, k + 3); - let u_ef_k3 = load(&u_ef_vals, k + 3); - let u_neg_e_g_k3 = load(&u_neg_e_g_vals, k + 3); - let k_k3 = load(&k_vals, k + 3); - let w_k3 = load(&w_vals, k + 3); - let sig0_k3 = load(&sig0_vals, k + 3); - let maj_k3 = load(&maj_vals, k + 3); - // mu_a stored at C8-anchor row k (= round t = k+3 was - // formerly stored at k+3; now at row k). - let mu_a_k = load(&mu_a_vals, k); - let two32_mu = two_to_32.clone() * &mu_a_k; - w_e_k + &sig1_k3 + &u_ef_k3 + &u_neg_e_g_k3 + &k_k3 + &w_k3 + &sig0_k3 + &maj_k3 - - &two32_mu - - &w_a_k4 - }) - .collect(); - - // C9: inner(2) = w_e[k+4] − w_a[k] − w_e[k] − sig1[k+3] − Ch[k+3] - // − K[k+3] − W[k+3] + 2^32 · mu_e[k+3] - // with Ch[k+3] = u_ef[k+3] + u_{¬e,g}[k+3]. - let pa_c_c9_col: Vec = (0..n) - .map(|k| { - let w_e_k4 = load(&e_vals, k + 4); - let w_a_k = load(&a_vals, k); - let w_e_k = load(&e_vals, k); - let sig1_k3 = load(&sig1_vals, k + 3); - let u_ef_k3 = load(&u_ef_vals, k + 3); - let u_neg_e_g_k3 = load(&u_neg_e_g_vals, k + 3); - let k_k3 = load(&k_vals, k + 3); - let w_k3 = load(&w_vals, k + 3); - // mu_e stored at C9-anchor row k (analogous to mu_a). - let mu_e_k = load(&mu_e_vals, k); - let two32_mu = two_to_32.clone() * &mu_e_k; - w_a_k + &w_e_k + &sig1_k3 + &u_ef_k3 + &u_neg_e_g_k3 + &k_k3 + &w_k3 - - &two32_mu - - &w_e_k4 - }) - .collect(); - - // C12/C13 feed-forward compensators. inner_a(2) at row k = - // w_a[k+4] − w_a[k] − pa_a[k] + 2^32 · mu_junction_a[k] - // (e-half symmetric). On junction rows the SHA-256 feed-forward - // makes inner = 0 honestly, so the compensator is 0. Off- - // junction (init prefix straddle, round-update windows, slack) - // it absorbs whatever inner happens to be so that - // `(inner + pa_c_ff) ∈ (X − 2)` everywhere. - let pa_c_ff_a_col: Vec = (0..n) - .map(|k| { - let w_a_k4 = load(&a_vals, k + 4); - let w_a_k = load(&a_vals, k); - let pa_a_k = load(&pa_a_vals, k); - let mu_ff_k = load(&mu_junction_a_vals, k); - let two32_mu = two_to_32.clone() * &mu_ff_k; - // comp = −inner(2) = w_a_k + pa_a_k − 2^32·mu_ff_k − w_a_k4 - w_a_k + &pa_a_k - &two32_mu - &w_a_k4 - }) - .collect(); - let pa_c_ff_e_col: Vec = (0..n) - .map(|k| { - let w_e_k4 = load(&e_vals, k + 4); - let w_e_k = load(&e_vals, k); - let pa_e_k = load(&pa_e_vals, k); - let mu_ff_k = load(&mu_junction_e_vals, k); - let two32_mu = two_to_32.clone() * &mu_ff_k; - w_e_k + &pa_e_k - &two32_mu - &w_e_k4 - }) - .collect(); - - let to_int_mle = - |col: Vec| -> DenseMultilinearExtension { col.into_iter().collect() }; - // Layout: public int prefix (selectors + K + active-range - // selectors) followed by witness int suffix (the five linear- - // constraint compensators). Order matches cols::S_INIT_PREFIX.. - // PA_C_FF_E. The 5 prior int carry columns (mu_W/a/e/ - // junction_a/e) are gone — packed into W_MU_PACKED above. - let int = vec![ - to_int_mle(s_init_prefix_col), - to_int_mle(s_feedforward_col), - to_int_mle(s_msg_init_col), - to_int_mle(k_col), - to_int_mle(s_active_sched_col), - to_int_mle(s_active_upd_col), - to_int_mle(pa_c_c7_col), - to_int_mle(pa_c_c8_col), - to_int_mle(pa_c_c9_col), - to_int_mle(pa_c_ff_a_col), - to_int_mle(pa_c_ff_e_col), - ]; + let k_col: Vec = k_vals.iter().copied().map(R::from).collect(); + // mu_w_vals / mu_a_vals / mu_e_vals / mu_junction_{a,e}_vals + // are no longer materialized as separate int columns — they're + // packed into the W_MU_PACKED binary_poly column above. + // ----- Compensator columns (replace s_sched_anch / s_upd_anch). ----- + // + // For each constraint Cᵢ ∈ {C7, C8, C9}, we publish a public column + // `pa_c_cᵢ[k]` with the property that (innerᵢ + pa_c_cᵢ) ∈ (X − 2) + // on every row k. Concretely we pick `pa_c_cᵢ[k] = −innerᵢ(2)` mod + // R's modulus; the protocol projects R into the random field, so + // the negation lands as `−innerᵢ(2) mod p` — exactly the value + // needed for the constraint to lie in (X − 2). + // + // On the corresponding active range (where the original selector + // was 1), the SHA recurrence makes `innerᵢ(2) = 0` for an honest + // prover, so `pa_c_cᵢ[k] = 0` automatically. On inactive rows the + // compensator absorbs whatever `innerᵢ(2)` happens to be. + let two_to_32: R = R::from(0x10000u32) * &R::from(0x10000u32); + let load = |arr: &[u32], idx: usize| -> R { if idx < n { R::from(arr[idx]) } else { R::ZERO } }; + + // C7: inner(2) = w_W[k+16] − w_W[k] − lsig0[k+1] − w_W[k+9] + // − lsig1[k+14] + 2^32 · mu_W[k+16] + let pa_c_c7_col: Vec = (0..n) + .map(|k| { + let w_k16 = load(&w_vals, k + 16); + let w_k = load(&w_vals, k); + let lsig0_k1 = load(&lsig0_vals, k + 1); + let w_k9 = load(&w_vals, k + 9); + let lsig1_k14 = load(&lsig1_vals, k + 14); + // mu_W stored at C7-anchor row k (= round t = k+16 was + // formerly stored at k+16; with chained-comp re-anchoring + // it's now at row k). + let mu_k = load(&mu_w_vals, k); + let two32_mu = two_to_32.clone() * &mu_k; + // comp = −inner(2) = w_k + lsig0_k1 + w_k9 + lsig1_k14 + // − 2^32·mu_k16 − w_k16 + w_k + &lsig0_k1 + &w_k9 + &lsig1_k14 - &two32_mu - &w_k16 + }) + .collect(); + + // C8: inner(2) = w_a[k+4] − w_e[k] − sig1[k+3] − Ch[k+3] − K[k+3] + // − W[k] − sig0[k+3] − maj[k+3] + 2^32 · mu_a[k] + // with Ch[k+3] = u_ef[k+3] + u_{¬e,g}[k+3]. + let pa_c_c8_col: Vec = (0..n) + .map(|k| { + let w_a_k4 = load(&a_vals, k + 4); + let w_e_k = load(&e_vals, k); + let sig1_k3 = load(&sig1_vals, k + 3); + let u_ef_k3 = load(&u_ef_vals, k + 3); + let u_neg_e_g_k3 = load(&u_neg_e_g_vals, k + 3); + let k_k3 = load(&k_vals, k + 3); + let w_k = load(&w_vals, k); + let sig0_k3 = load(&sig0_vals, k + 3); + let maj_k3 = load(&maj_vals, k + 3); + // mu_a stored at C8-anchor row k (= round t = k+3 was + // formerly stored at k+3; now at row k). + let mu_a_k = load(&mu_a_vals, k); + let two32_mu = two_to_32.clone() * &mu_a_k; + w_e_k + &sig1_k3 + &u_ef_k3 + &u_neg_e_g_k3 + &k_k3 + &w_k + &sig0_k3 + &maj_k3 + - &two32_mu + - &w_a_k4 + }) + .collect(); + + // C9: inner(2) = w_e[k+4] − w_a[k] − w_e[k] − sig1[k+3] − Ch[k+3] + // − K[k+3] − W[k] + 2^32 · mu_e[k] + // with Ch[k+3] = u_ef[k+3] + u_{¬e,g}[k+3]. + let pa_c_c9_col: Vec = (0..n) + .map(|k| { + let w_e_k4 = load(&e_vals, k + 4); + let w_a_k = load(&a_vals, k); + let w_e_k = load(&e_vals, k); + let sig1_k3 = load(&sig1_vals, k + 3); + let u_ef_k3 = load(&u_ef_vals, k + 3); + let u_neg_e_g_k3 = load(&u_neg_e_g_vals, k + 3); + let k_k3 = load(&k_vals, k + 3); + let w_k = load(&w_vals, k); + // mu_e stored at C9-anchor row k (analogous to mu_a). + let mu_e_k = load(&mu_e_vals, k); + let two32_mu = two_to_32.clone() * &mu_e_k; + w_a_k + &w_e_k + &sig1_k3 + &u_ef_k3 + &u_neg_e_g_k3 + &k_k3 + &w_k + - &two32_mu + - &w_e_k4 + }) + .collect(); + + // C12/C13 feed-forward compensators. inner_a(2) at row k = + // w_a[k+4] − w_a[k] − pa_a[k] + 2^32 · mu_junction_a[k] + // (e-half symmetric). On junction rows the SHA-256 feed-forward + // makes inner = 0 honestly, so the compensator is 0. Off- + // junction (init prefix straddle, round-update windows, slack) + // it absorbs whatever inner happens to be so that + // `(inner + pa_c_ff) ∈ (X − 2)` everywhere. + let pa_c_ff_a_col: Vec = (0..n) + .map(|k| { + let w_a_k4 = load(&a_vals, k + 4); + let w_a_k = load(&a_vals, k); + let pa_a_k = load(&pa_a_vals, k); + let mu_ff_k = load(&mu_junction_a_vals, k); + let two32_mu = two_to_32.clone() * &mu_ff_k; + // comp = −inner(2) = w_a_k + pa_a_k − 2^32·mu_ff_k − w_a_k4 + w_a_k + &pa_a_k - &two32_mu - &w_a_k4 + }) + .collect(); + let pa_c_ff_e_col: Vec = (0..n) + .map(|k| { + let w_e_k4 = load(&e_vals, k + 4); + let w_e_k = load(&e_vals, k); + let pa_e_k = load(&pa_e_vals, k); + let mu_ff_k = load(&mu_junction_e_vals, k); + let two32_mu = two_to_32.clone() * &mu_ff_k; + w_e_k + &pa_e_k - &two32_mu - &w_e_k4 + }) + .collect(); + + let to_int_mle = |col: Vec| -> DenseMultilinearExtension { col.into_iter().collect() }; + // Layout: public int prefix (selectors + K + active-range + // selectors) followed by witness int suffix (the five linear- + // constraint compensators). Order matches cols::S_INIT_PREFIX.. + // PA_C_FF_E. The 5 prior int carry columns (mu_W/a/e/ + // junction_a/e) are gone — packed into W_MU_PACKED above. + let int = vec![ + to_int_mle(s_init_prefix_col), + to_int_mle(s_feedforward_col), + to_int_mle(s_msg_init_col), + to_int_mle(k_col), + to_int_mle(s_active_sched_col), + to_int_mle(s_active_upd_col), + to_int_mle(pa_c_c7_col), + to_int_mle(pa_c_c8_col), + to_int_mle(pa_c_c9_col), + to_int_mle(pa_c_ff_a_col), + to_int_mle(pa_c_ff_e_col), + ]; + + Ok(( UairTrace { binary_poly: binary_poly.into(), int: int.into(), ..Default::default() - } + }, + trace_halves_to_state(h_a, h_e), + )) +} + +// --------------------------------------------------------------------------- +// GenerateRandomTrace for the slice. +// --------------------------------------------------------------------------- + +impl GenerateRandomTrace<32> for Sha256CompressionSliceUair +where + R: ConstSemiring + From + Clone + 'static, +{ + type PolyCoeff = R; + type Int = R; + + fn generate_random_trace( + num_vars: usize, + rng: &mut Rng, + ) -> UairTrace<'static, R, R, 32> { + let initial_state: Sha256State = std::array::from_fn(|_| rng.next_u32()); + let message_blocks: [Sha256MessageBlock; cols::NUM_COMPRESSIONS] = + std::array::from_fn(|_| std::array::from_fn(|_| rng.next_u32())); + synthesize_sha256_compression_chain_trace::(num_vars, initial_state, &message_blocks) + .map(|(trace, _)| trace) + .expect("random SHA-256 trace synthesis failed") } } @@ -1823,7 +2005,52 @@ where mod tests { use super::*; use crypto_primitives::crypto_bigint_int::Int; - use zinc_uair::degree_counter::{count_effective_max_degree, count_max_degree}; + use zinc_uair::{ + Uair, + degree_counter::{count_effective_max_degree, count_max_degree}, + }; + + fn test_initial_state() -> Sha256State { + [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, + 0x5be0cd19, + ] + } + + fn test_message_blocks() -> [Sha256MessageBlock; N] { + std::array::from_fn(|i| { + std::array::from_fn(|j| { + ((i as u32) << 24) ^ ((j as u32) << 16) ^ 0xa5a5_0000u32.wrapping_add(j as u32) + }) + }) + } + + fn native_chain( + initial_state: Sha256State, + message_blocks: &[Sha256MessageBlock; N], + ) -> Sha256State { + message_blocks.iter().fold(initial_state, |state, block| { + sha256_compress_native(state, *block) + }) + } + + fn assert_sha_trace_splits_cleanly(trace: &UairTrace<'static, Int<5>, Int<5>, 32>) { + let sig = > as Uair>::signature(); + let public = trace.public(&sig); + let witness = trace.witness(&sig); + + assert_eq!(public.binary_poly.len(), cols::NUM_BIN_PUB); + assert_eq!(public.arbitrary_poly.len(), 0); + assert_eq!(public.int.len(), cols::NUM_INT_PUB); + assert_eq!(witness.binary_poly.len(), cols::NUM_BIN - cols::NUM_BIN_PUB); + assert_eq!(witness.arbitrary_poly.len(), 0); + assert_eq!(witness.int.len(), cols::NUM_INT - cols::NUM_INT_PUB); + > as Uair>::verify_public_structure( + &public, + cols::MIN_NUM_VARS, + ) + .expect("generated SHA public trace should satisfy public structure checks"); + } /// All non-zero-ideal SHA constraints (C1, C2, C4, C6, C7, C8, C9, /// and the new feed-forward C12/C13) must stay degree-1 in the @@ -1840,6 +2067,36 @@ mod tests { assert!(count_max_degree::() >= 2); } + #[test] + fn synthesize_sha256_chain_witnesses_n1_matches_native() { + let initial_state = test_initial_state(); + let message_blocks = test_message_blocks::<1>(); + let expected = sha256_compress_native(initial_state, message_blocks[0]); + + let (witnesses, final_state) = + synthesize_sha256_chain_witnesses::, 1>(initial_state, message_blocks) + .expect("N=1 SHA witness synthesis should succeed"); + + assert_eq!(final_state, expected); + assert_sha_trace_splits_cleanly(&witnesses[0].trace); + } + + #[test] + fn synthesize_sha256_chain_witnesses_n8_matches_native() { + let initial_state = test_initial_state(); + let message_blocks = test_message_blocks::<8>(); + let expected = native_chain(initial_state, &message_blocks); + + let (witnesses, final_state) = + synthesize_sha256_chain_witnesses::, 8>(initial_state, message_blocks) + .expect("N=8 SHA witness synthesis should succeed"); + + assert_eq!(final_state, expected); + for witness in &witnesses { + assert_sha_trace_splits_cleanly(&witness.trace); + } + } + /// Cross-check the K_CANONICAL table against the canonical SHA-256 /// initial hash values H_0 — running one full compression of the /// empty-padding block (with H_0 as input) must produce the diff --git a/uair/src/lib.rs b/uair/src/lib.rs index dfefac82..91e89bcb 100644 --- a/uair/src/lib.rs +++ b/uair/src/lib.rs @@ -782,6 +782,16 @@ pub struct UairTrace<'a, PolyCoeff: Clone, Int: Clone, const D: usize> { pub int: Cow<'a, [DenseMultilinearExtension]>, } +/// Prover-private UAIR witness data. +/// +/// The wrapped trace may still contain public-prefix columns; callers can split +/// it with [`UairTrace::public`] and [`UairTrace::witness`] using the UAIR +/// signature. +#[derive(Debug, Clone)] +pub struct UairWitness<'a, PolyCoeff: Clone, Int: Clone, const D: usize> { + pub trace: UairTrace<'a, PolyCoeff, Int, D>, +} + impl UairTrace<'static, PolyCoeff, Int, D> { /// Returns a sub-trace containing only public columns. /// Returned trace is borrowed from the full trace. From c7dfd979cbec4b855d1eceffd96d0a8736cb6ca1 Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 10:47:07 -0700 Subject: [PATCH 21/49] Update SHA projection object model --- .../sha-uair-doc/uair-object-model.md | 539 ++++++ piop/src/neutron_nova/mod.rs | 8 +- piop/src/neutron_nova/projection_sha.rs | 506 ++++- protocol/src/production_sha.rs | 1632 +++++++++++++++-- 4 files changed, 2497 insertions(+), 188 deletions(-) create mode 100644 documentation/sha-uair-doc/uair-object-model.md diff --git a/documentation/sha-uair-doc/uair-object-model.md b/documentation/sha-uair-doc/uair-object-model.md new file mode 100644 index 00000000..8fcf3d25 --- /dev/null +++ b/documentation/sha-uair-doc/uair-object-model.md @@ -0,0 +1,539 @@ +# UAIR Object Model for Production SHA-256 + +This note records the naming and lifecycle decisions for the generic UAIR +objects we want to use before specializing them to production SHA-256. + +## Core Principle + +Keep these roles separate: + +- `Shape`: static relation metadata and layout. +- `Witness`: the prover's full pre-projection assignment. +- `Instance`: verifier-visible public data plus commitments. +- `FoldedWitness`: prover-private folded state after folding. +- `FoldedInstance`: verifier-visible folded accumulator. + +Do not put projected/evaluated data in the initial instance object. Projection +and evaluation happen later after transcript challenges are known. + +## Existing e2e.rs Lifecycle + +The generic path in `protocol/benches/e2e.rs` works like this: + +```rust +let trace = U::generate_random_trace(num_vars, &mut rng); +let sig = U::signature(); +let public_trace = trace.public(&sig); +``` + +The prover receives the full `UairTrace`, splits it with `UairSignature`, commits +only the witness columns, and absorbs the unprojected public columns into the +transcript: + +```rust +let public_trace = trace.public(&uair_signature); +let witness_trace = trace.witness(&uair_signature); + +commit(witness_trace); +absorb(public_trace); +``` + +The verifier receives the proof and the unprojected `public_trace`. It absorbs +that same public trace before later projection/evaluation steps. + +So the instance's public data is an unprojected public UAIR trace, not a +`ProjectedPublicTrace`. + +## UairSignature + +`UairSignature` is the layout contract for a UAIR. It defines: + +- total column counts for `binary_poly`, `arbitrary_poly`, and `int` +- public column prefix counts +- witness column suffix counts +- shifted columns +- virtual columns +- lookup and booleanity metadata + +`UairTrace::public(sig)` and `UairTrace::witness(sig)` use this signature to +split a full trace into public and witness subtraces. + +## Generic Objects + +`UairShape` is useful as a value-level handle for the static UAIR relation plus +the trace length. + +```rust +pub struct UairShape { + pub num_vars: usize, + pub signature: UairSignature, + _marker: PhantomData, +} +``` + +`num_vars` should stay on the shape or protocol input. It is the log trace +length, so the row domain has size `1 << num_vars`. The prover and verifier use +it for MLE sizes, sumcheck rounds, public-structure checks, and PCS parameters. + +Do not add `shape_digest` in the first pass. A shape digest may be useful later +for serialization or cached transcript binding, but it should be derived from +the shape rather than treated as fundamental state. + +The witness is the full pre-projection prover assignment. It includes public +columns and private columns because `UairTrace` itself stores both; public +columns are the prefix determined by `UairSignature`. + +```rust +pub struct UairWitness<'a, PolyCoeff: Clone, Int: Clone, const D: usize> { + pub trace: UairTrace<'a, PolyCoeff, Int, D>, +} +``` + +Document this clearly: `UairWitness` means full prover assignment, not +private-only data. + +The fresh verifier-visible instance contains the unprojected public trace and +commitments to the witness columns. + +```rust +pub struct UairInstance<'a, PolyCoeff: Clone, Int: Clone, Commitments, const D: usize> { + pub public_trace: UairTrace<'a, PolyCoeff, Int, D>, + pub commitments: Commitments, +} +``` + +The folded objects should mirror the fresh split: + +```rust +pub struct FoldedUairWitness { + // prover-private folded evaluations, residuals, and randomness +} + +pub struct FoldedUairInstance { + pub commitments: Commitments, + pub public_evals: Vec, + pub u: F, +} +``` + +The exact fields of the folded objects should be chosen by the folding protocol, +but the boundary remains the same: witness is private, instance is +verifier-visible. + +## LinearIdealFold Proof Objects + +`LinearIdealFold` is the generic folding layer for UAIRs whose projected +residue constraints live in linear ideals. The proof object should contain only +verifier messages and claimed evaluations. It should not contain prover-side +caches such as PCS prover data, and it should not contain NeutronNova-specific +objects such as `comm_E` for a committed power-vector witness. + +The family association for ideal polynomials is part of the proof contract. A +bare `Vec>` is not enough unless `UairShape` defines the +exact canonical order. Prefer an explicit family-tagged form: + +```rust +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct IdealFamilyId(pub u16); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct IdealPolySlot(pub u16); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IdealFamilyPolys { + pub family: IdealFamilyId, + pub polys: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IdealFamilyPoly { + pub slot: IdealPolySlot, + pub poly: DynamicPolynomialF, +} +``` + +The verifier must check that these families and slots are in the canonical +shape-defined order, with no missing or duplicate entries. For SHA-256, this +corresponds to the current `production_sha_nonzero_families()` order, but the +generic API should make that association shape-level data. + +The folded verifier-visible instance is the result of folding fresh instances. +It contains the folded target claim, folded commitments, and any folded public +values needed by the later checks: + +```rust +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FoldedLinearIdealInstance { + pub target: F, + pub commitments: Commitments, + pub public: Public, +} +``` + +The folded witness is prover-private. Its concrete representation can be an +owned folded trace, folded source MLEs, or another protocol-specific witness +bundle: + +```rust +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FoldedLinearIdealWitness { + pub witness: Witness, +} +``` + +The proof object is: + +```rust +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinearIdealFoldProof { + pub ideal_family_polys: Vec>, + pub nifs: MultiDegreeSumcheckProof, + pub folded_claim_sumcheck: MultiDegreeSumcheckProof, + pub terminal_evals: TerminalEvals, + pub multipoint: MultipointEvalProof, + pub opening_evals: OpeningEvals, + pub pcs_opening_bytes: Vec, +} +``` + +`ideal_family_polys` contains the nonzero ideal-witness polynomials, grouped by +ideal family. These prove ideal membership and define the scalar targets used by +the instance-axis folding claim. + +For fresh instances \(b \in \{0,\dots,B-1\}\), ideal families +\(f \in \mathcal F\), and family slots \(k\): + +$$ +E_{b,f,k}(X) \in I_f +$$ + +The beta-aggregated family polynomial is: + +$$ +\bar E^{\beta}_{f,k}(X) += +\sum_{b=0}^{B-1} \operatorname{eq}(\beta,b)\,E_{b,f,k}(X) +$$ + +Because the ideals are linear: + +$$ +E_{b,f,k}(X) \in I_f +\quad\Longrightarrow\quad +\bar E^{\beta}_{f,k}(X) \in I_f +$$ + +The fresh scalar target for instance \(b\) is: + +$$ +T_b += +\sum_{f \in \mathcal F} +\lambda_f +\sum_k E_{b,f,k}(a) +$$ + +The initial SumFold claim is: + +$$ +C_0 += +\sum_{b=0}^{B-1} +\operatorname{eq}(\beta,b)\,T_b +$$ + +`nifs` is the instance-axis `MultiDegreeSumcheckProof`. It proves the SumFold +transition from \(C_0\) to the folded target. The verifier derives \(r_b\), +folding weights \(\theta_b\), and \(T'\): + +$$ +\theta_b = \operatorname{eq}(r_b,b) +$$ + +$$ +T' += +\frac{c_{\mathrm{SF}}}{\operatorname{eq}(\beta,r_b)} +$$ + +`folded_claim_sumcheck` proves that the folded target is the row-domain sum of +the folded residue expression: + +$$ +T' += +\sum_{x \in \{0,1\}^{d}} +\operatorname{eq}(r_{\mathrm{ic}},x) +\cdot +\Phi_{\mathrm{folded}}(x) +$$ + +This sumcheck reduces the folded claim to a terminal point \(r_\star\). + +`terminal_evals` contains the claimed evaluations needed to reconstruct +\(\Phi_{\mathrm{folded}}(r_\star)\). It should include source identifiers, +shift identifiers, scalarized values, and any coefficient-level values needed by +the shape-specific expression. For SHA-256 this corresponds to the current +endpoint evaluations of folded word and integer sources. + +```rust +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct SourceId(pub u16); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ShiftId(pub u16); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TerminalEvals { + pub polynomial_sources: Vec>, + pub scalar_sources: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TerminalPolynomialEval { + pub source: SourceId, + pub shift: ShiftId, + pub scalarized: F, + pub coeffs: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TerminalScalarEval { + pub source: SourceId, + pub shift: ShiftId, + pub value: F, +} +``` + +The verifier uses `terminal_evals` to check: + +$$ +\mathrm{terminal} += +\operatorname{eq}(r_{\mathrm{ic}},r_\star) +\cdot +\Phi_{\mathrm{folded}}(r_\star) +$$ + +`multipoint` reduces all terminal evaluation claims at \(r_\star\) and shifted +points into one batched opening claim at a verifier-derived point \(r_0\): + +$$ +\{p_i(s_i(r_\star)) = v_i\}_i +\quad\Longrightarrow\quad +P(r_0)=v_0 +$$ + +`opening_evals` are the claimed folded committed-source evaluations at \(r_0\). +`pcs_opening_bytes` is the serialized PCS proof that those evaluations match +the folded commitments. + +```rust +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OpeningEvals { + pub polynomial_sources: Vec>, + pub scalar_sources: Vec, +} +``` + +The proof chain is: + +```text +ideal_family_polys + -> derive C0 and prove ideal membership +nifs + -> fold C0 into T' +folded_claim_sumcheck + -> reduce T' to terminal point r_star +terminal_evals + -> reconstruct the terminal folded expression +multipoint + -> reduce many endpoint claims to one opening point r_0 +opening_evals + pcs_opening_bytes + -> prove consistency with folded commitments +``` + +## SHA-256 Domain Objects + +For one SHA-256 compression, the semantic relation is: + +```rust +H_{i+1} = compress(H_i, M_i) +``` + +where: + +```rust +H_i: [u32; 8] +M_i: [u32; 16] +H_{i+1}: [u32; 8] +``` + +For a chain of `N` compressions: + +```rust +pub struct Sha256ChainPublicInput { + pub initial_state: [u32; 8], + pub message_blocks: [[u32; 16]; N], + pub final_state: [u32; 8], +} +``` + +For standard SHA-256 hashing from the fixed IV, use a wrapper: + +```rust +pub struct Sha256HashPublicInput { + pub message_blocks: [[u32; 16]; N], + pub digest: [u32; 8], +} +``` + +This wrapper expands into `Sha256ChainPublicInput` by setting +`initial_state = SHA256_IV`. + +The current e2e SHA UAIR packs multiple compressions into a single trace. It +does not pass intermediate states as separate public inputs. Witness generation +computes: + +```rust +H_1 = compress(H_0, M_0) +H_2 = compress(H_1, M_1) +... +H_N = compress(H_{N-1}, M_{N-1}) +``` + +and writes the relevant public values into public UAIR columns. + +## SHA-256 to UAIR Mapping + +The SHA domain input is not the same thing as the UAIR public trace. + +The SHA public input: + +```rust +Sha256ChainPublicInput { + initial_state, + message_blocks, + final_state, +} +``` + +is used to build the UAIR public trace. For the current SHA UAIR this includes +columns such as: + +- `PA_M`: message block words +- `PA_A` / `PA_E`: chaining states and final output prefix +- `PA_K`: SHA-256 round constants +- selector columns +- implementation-specific public helper columns + +The prover also builds the full `UairWitness` trace containing the public +columns plus private/witness columns such as: + +- message schedule `W` +- round state columns +- sigma/Sigma columns +- Ch/Maj auxiliary columns +- carry columns +- compensator columns + +The verifier should build or receive only the public trace, then verify the +proof against commitments to the private columns. + +## Recommended Production Flow + +Witness generation is outside the prover. It consumes semantic public input and +produces a full pre-projection UAIR witness: + +```rust +pub fn build_uair_witness( + shape: &UairShape, + public: &Input, +) -> Result, UairWitnessError> +where + U: Uair, + PolyCoeff: Clone, + Int: Clone; +``` + +The SHA-256 instantiation can be written more concretely as: + +```rust +pub fn build_sha256_witness( + shape: &UairShape>, + public: &Sha256ChainPublicInput, +) -> Result, ShaWitnessError> +where + Zt: ZincTypes; +``` + +The prover receives witnesses and commits to the witness columns internally. It +returns fresh verifier-visible instances, the folded accumulator pair, and the +proof: + +```rust +pub struct LinearIdealFoldProveOutput { + pub fresh_instances: Vec, + pub folded_instance: FoldedInstance, + pub folded_witness: FoldedWitness, + pub proof: Proof, +} + +pub fn prove_linear_ideal_fold( + pp: &LinearIdealFoldProverParams, + shape: &UairShape, + witnesses: &[UairWitness<'_, Zt::Int, Zt::Int, D>], + transcript: &mut impl Transcript, +) -> Result< + LinearIdealFoldProveOutput< + UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, + FoldedLinearIdealInstance, FoldedPublicEvals>, + FoldedLinearIdealWitness>, + LinearIdealFoldProof, + >, + LinearIdealFoldError, +> +where + U: Uair, + Zt: ZincTypes, + F: PrimeField; +``` + +The verifier receives the fresh instances and proof. It derives the same folded +instance, but it never receives the folded witness: + +```rust +pub fn verify_linear_ideal_fold( + vp: &LinearIdealFoldVerifierParams, + shape: &UairShape, + instances: &[UairInstance<'_, Zt::Int, Zt::Int, PCSCommitments, D>], + proof: &LinearIdealFoldProof, + transcript: &mut impl Transcript, +) -> Result< + FoldedLinearIdealInstance, FoldedPublicEvals>, + LinearIdealFoldError, +> +where + U: Uair, + Zt: ZincTypes, + F: PrimeField; +``` + +```rust +Sha256ChainPublicInput + -> build public UairTrace + +Sha256ChainPublicInput + witness generation + -> full UairWitness + +UairWitness + UairShape + -> commit witness columns + -> UairInstance { public_trace, commitments } + -> prove + +UairInstance + proof + -> verify +``` + +Projection to the proof field and evaluation at verifier challenges are internal +protocol phases. They should not be part of the initial public instance type. diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index 75b0193a..9588471c 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -28,12 +28,14 @@ pub use projection_sha::{ FoldedCommitments, FoldedShaAccumulator, FoldedShaWitness, FreshShaIdealCache, NUM_NONZERO_SHA_FAMILIES, NUM_SHA_RESIDUAL_FAMILIES, ProjectedShaPublic, ProjectedShaTrace, SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, ShaBitSliceColumns, ShaBooleanitySource, ShaIntCol, - ShaIntColumns, ShaProductionIdeal, ShaProjectionError, ShaPublicCol, ShaPublicColumns, - ShaResidualFamily, ShaScalarizedRows, ShaSumFoldOutput, ShaWordCol, VirtualChMajValues, + ShaIntColumns, ShaLinearResidualCoeffCache, ShaProductionIdeal, ShaProjectionError, + ShaPublicCol, ShaPublicColumns, ShaPublicWordCol, ShaPublicWordColumns, ShaResidualFamily, + ShaScalarizedRows, ShaSumFoldOutput, ShaWordCol, VirtualChMajValues, build_dense_sha_sumfold_group, build_expression_folded_row_sumcheck_group, build_folded_row_sumcheck_group, build_fresh_sha_ideal_cache, build_production_sha_sumfold_group, build_production_sha_sumfold_group_owned, - build_sha_ideal_values_at_point, check_fresh_sha_ideal_cache, check_sha_ideal_values, + build_production_sha_sumfold_group_with_linear_cache, build_sha_ideal_values_at_point, + build_sha_linear_residual_coeff_cache, check_fresh_sha_ideal_cache, check_sha_ideal_values, evaluate_fresh_sha_targets, expression_folded_row_sum, finalize_sha_sumfold, fold_projected_sha_traces, folded_row_integrand_sum, folded_row_integrand_values, production_sha_booleanity_sources, production_sha_nonzero_families, diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 4ae0f882..47a309fc 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -247,6 +247,45 @@ impl ShaPublicCol { } } +#[repr(usize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ShaPublicWordCol { + PAIn = 0, + PEIn = 1, + PAOut = 2, + PEOut = 3, + Message = 4, +} + +impl ShaPublicWordCol { + pub const ALL: [Self; 5] = [ + Self::PAIn, + Self::PEIn, + Self::PAOut, + Self::PEOut, + Self::Message, + ]; + + pub const COUNT: usize = 5; + + pub fn index(self) -> usize { + self as usize + } +} + +impl ShaPublicCol { + fn public_word_col(self) -> Option { + match self { + Self::PAIn => Some(ShaPublicWordCol::PAIn), + Self::PEIn => Some(ShaPublicWordCol::PEIn), + Self::PAOut => Some(ShaPublicWordCol::PAOut), + Self::PEOut => Some(ShaPublicWordCol::PEOut), + Self::Message => Some(ShaPublicWordCol::Message), + _ => None, + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ShaBitSliceColumns { /// Indexed as `[word_col][row][bit]`. @@ -271,6 +310,12 @@ pub struct ShaPublicColumns { pub columns: Vec>, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShaPublicWordColumns { + /// Indexed as `[public_word_col][row][bit]`. + pub columns: Vec>>, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ProjectedShaTrace { pub rows: usize, @@ -283,6 +328,7 @@ pub struct ProjectedShaTrace { #[derive(Clone, Debug, PartialEq, Eq)] pub struct ProjectedShaPublic { pub columns: ShaPublicColumns, + pub word_columns: Option>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -293,6 +339,88 @@ pub struct FreshShaIdealCache { pub fresh_targets: Vec, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShaLinearResidualCoeffCache { + /// Logical layout is `[family][instance][degree_slot]`. + coeffs_by_instance: Vec<[DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES]>, +} + +impl ShaLinearResidualCoeffCache +where + F: PrimeField, +{ + pub fn instance_count(&self) -> usize { + self.coeffs_by_instance.len() + } + + pub fn coeffs_for_instance( + &self, + instance: usize, + ) -> Option<&[DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES]> { + self.coeffs_by_instance.get(instance) + } + + pub fn beta_aggregate_nonzero_ideal_polys( + &self, + beta: &[F], + field_cfg: &F::Config, + ) -> Result<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], ShaProjectionError> { + let weights = build_eq_x_r_vec(beta, field_cfg)?; + if weights.len() != self.coeffs_by_instance.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: weights.len(), + expected: self.coeffs_by_instance.len(), + }); + } + + let mut aggregate: [DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES] = + std::array::from_fn(|_| DynamicPolynomialF::ZERO); + for (weight, instance) in weights.iter().zip(&self.coeffs_by_instance) { + for (slot, family) in NONZERO_SHA_FAMILIES.iter().enumerate() { + let weighted = scale_poly(&instance[family.index()], weight); + aggregate[slot] += &weighted; + } + } + aggregate.iter_mut().for_each(DynamicPolynomialF::trim); + Ok(aggregate) + } +} + +impl ShaLinearResidualCoeffCache +where + F: DelayedFieldProductSum, +{ + pub fn linear_values_at_a_lambda( + &self, + a: &F, + lambda: &F, + field_cfg: &F::Config, + ) -> Result, ShaProjectionError> { + let lambda_powers = powers( + lambda.clone(), + F::one_with_cfg(field_cfg), + NUM_SHA_RESIDUAL_FAMILIES, + ); + let a_powers = powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_RESIDUAL_EVAL_POWER_COUNT, + ); + + self.coeffs_by_instance + .iter() + .map(|instance| { + let mut target = F::zero_with_cfg(field_cfg); + for (family_idx, residual) in instance.iter().enumerate() { + target += lambda_powers[family_idx].clone() + * evaluate_poly_at_powers_dmr(residual, &a_powers, field_cfg)?; + } + Ok(target) + }) + .collect() + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ShaSumFoldOutput { r_b: Vec, @@ -409,6 +537,8 @@ pub enum ShaProjectionError { InstanceCountNotPowerOfTwo { got: usize }, #[error("instance count mismatch: got {got}, expected {expected}")] InstanceCountMismatch { got: usize, expected: usize }, + #[error("public word column presence mismatch across folded instances")] + PublicWordColumnPresenceMismatch, #[error("folding weight count mismatch: got {got}, expected {expected}")] FoldingWeightCount { got: usize, expected: usize }, #[error("SumFold denominator eq(beta, r_b) is zero")] @@ -575,6 +705,44 @@ where }) } +pub fn build_sha_linear_residual_coeff_cache( + traces: &[ProjectedShaTrace], + publics: &[ProjectedShaPublic], + r_ic: &[F; SHA_ROW_VARS], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + if traces.len() != publics.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: publics.len(), + expected: traces.len(), + }); + } + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + let coeffs_by_instance = traces + .iter() + .zip(publics) + .map(|(trace, public)| { + validate_trace(trace)?; + validate_public(public)?; + let mut coeffs: [DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES] = + std::array::from_fn(|_| DynamicPolynomialF::ZERO); + for (row, row_weight) in row_weights.iter().enumerate().take(SHA_ROW_COUNT) { + let residuals = residual_polys_at_row(trace, public, row, field_cfg)?; + for (family_idx, residual) in residuals.iter().enumerate() { + let weighted = scale_poly(residual, row_weight); + coeffs[family_idx] += &weighted; + } + } + coeffs.iter_mut().for_each(DynamicPolynomialF::trim); + Ok::<[DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES], ShaProjectionError>(coeffs) + }) + .collect::, _>>()?; + Ok(ShaLinearResidualCoeffCache { coeffs_by_instance }) +} + pub fn check_fresh_sha_ideal_cache( cache: &FreshShaIdealCache, field_cfg: &F::Config, @@ -737,6 +905,12 @@ where field_cfg, )?, }, + word_columns: fold_optional_3d( + publics.iter().map(|public| public.word_columns.as_ref()), + &sumfold.theta, + field_cfg, + )? + .map(|columns| ShaPublicWordColumns { columns }), }; Ok(( @@ -1390,6 +1564,38 @@ where booleanity_sources: &[ShaBooleanitySource], prefix_vars: usize, field_cfg: &F::Config, + ) -> Result { + let coeff_cache = build_sha_linear_residual_coeff_cache(&traces, publics, r_ic, field_cfg)?; + Self::new_owned_with_linear_cache( + traces, + publics, + beta, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + prefix_vars, + &coeff_cache, + field_cfg, + ) + } + + #[allow(clippy::too_many_arguments)] + fn new_owned_with_linear_cache( + traces: Box<[ProjectedShaTrace]>, + publics: &[ProjectedShaPublic], + beta: &[F], + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + coeff_cache: &ShaLinearResidualCoeffCache, + field_cfg: &F::Config, ) -> Result { let ell = validate_sha_sumfold_inputs(&traces, publics, beta)?; if prefix_vars == 0 || prefix_vars > ell { @@ -1399,40 +1605,23 @@ where } .into()); } + if coeff_cache.instance_count() != traces.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: coeff_cache.instance_count(), + expected: traces.len(), + }); + } let tail_vars = ell - prefix_vars; let tail_len = binary_len(tail_vars); let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; - let lambda_powers = powers( - lambda.clone(), - F::one_with_cfg(field_cfg), - NUM_SHA_RESIDUAL_FAMILIES, - ); - let a_powers = powers( - a.clone(), - F::one_with_cfg(field_cfg), - SHA_RESIDUAL_EVAL_POWER_COUNT, - ); let rho_powers = powers( rho.clone(), F::one_with_cfg(field_cfg), booleanity_sources.len(), ); - let linear_values = traces - .iter() - .zip(publics.iter()) - .map(|(trace, public)| { - sha_linear_residual_sum_with_weights( - trace, - public, - &row_weights, - &a_powers, - &lambda_powers, - field_cfg, - ) - }) - .collect::, _>>()?; + let linear_values = coeff_cache.linear_values_at_a_lambda(a, lambda, field_cfg)?; let linear = BinaryPrefixTailTable::new(linear_values, prefix_vars, tail_len); let booleanity = TernaryPrefixTailTable::new( build_sha_booleanity_prefix_tail_table( @@ -1809,6 +1998,156 @@ where )) } +#[allow(clippy::too_many_arguments)] +pub fn build_production_sha_sumfold_group_with_linear_cache( + traces: &[ProjectedShaTrace], + publics: &[ProjectedShaPublic], + linear_cache: &ShaLinearResidualCoeffCache, + beta: &[F], + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + let ell = validate_sha_sumfold_inputs(traces, publics, beta)?; + if linear_cache.instance_count() != traces.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: linear_cache.instance_count(), + expected: traces.len(), + }); + } + if prefix_vars > ell { + return Err(SumFoldError::Ell0TooLarge { + ell0: prefix_vars, + ell, + } + .into()); + } + if prefix_vars == 0 { + return build_dense_sha_sumfold_group_with_linear_cache( + traces, + linear_cache, + beta, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + ); + } + + let fast_path = ShaSumFoldPrefixFastPath::new_owned_with_linear_cache( + traces.to_vec().into_boxed_slice(), + publics, + beta, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + prefix_vars, + linear_cache, + field_cfg, + ); + + Ok(MultiDegreeSumcheckGroup::with_prefix_fast( + 3, + Vec::new(), + sha_sumfold_comb_fn( + build_eq_x_r_vec(r_ic, field_cfg)?, + powers( + rho.clone(), + F::one_with_cfg(field_cfg), + booleanity_sources.len(), + ), + xi.clone(), + field_cfg, + ), + Box::new(fast_path?), + )) +} + +#[allow(clippy::too_many_arguments)] +fn build_dense_sha_sumfold_group_with_linear_cache( + traces: &[ProjectedShaTrace], + linear_cache: &ShaLinearResidualCoeffCache, + beta: &[F], + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + let ell = beta.len(); + let zero = F::zero_with_cfg(field_cfg); + let zero_inner = zero.inner().clone(); + let mut mles = Vec::with_capacity(2 + booleanity_sources.len() * SHA_ROW_COUNT); + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + let eq_beta = build_eq_x_r_vec(beta, field_cfg)?; + + mles.push(DenseMultilinearExtension::from_evaluations_vec( + ell, + eq_beta.iter().map(|value| value.inner().clone()).collect(), + zero_inner.clone(), + )); + + let linear_values = linear_cache.linear_values_at_a_lambda(a, lambda, field_cfg)?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + ell, + linear_values + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner.clone(), + )); + + for source in booleanity_sources { + for row in 0..SHA_ROW_COUNT { + let values = traces + .iter() + .map(|trace| booleanity_source_value_at_row(trace, row, source, field_cfg)) + .collect::, _>>()?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + ell, + values.iter().map(|value| value.inner().clone()).collect(), + zero_inner.clone(), + )); + } + } + + Ok(MultiDegreeSumcheckGroup::new( + 3, + mles, + sha_sumfold_comb_fn( + row_weights, + powers( + rho.clone(), + F::one_with_cfg(field_cfg), + booleanity_sources.len(), + ), + xi.clone(), + field_cfg, + ), + )) +} + #[allow(clippy::too_many_arguments)] pub fn build_production_sha_sumfold_group_owned( traces: Box<[ProjectedShaTrace]>, @@ -2539,7 +2878,7 @@ where - &word_poly_shifted(trace, ShaWordCol::Uef, row, 3, field_cfg)? - &word_poly_shifted(trace, ShaWordCol::UNegEg, row, 3, field_cfg)? - &public_const_poly(public, ShaPublicCol::K, row + 3, field_cfg)? - - &word_poly_shifted(trace, ShaWordCol::W, row, 3, field_cfg)? + - &w - &word_poly_shifted(trace, ShaWordCol::Sigma0, row, 3, field_cfg)? - &word_poly_shifted(trace, ShaWordCol::Maj, row, 3, field_cfg)? + &mu_a @@ -2552,7 +2891,7 @@ where - &word_poly_shifted(trace, ShaWordCol::Uef, row, 3, field_cfg)? - &word_poly_shifted(trace, ShaWordCol::UNegEg, row, 3, field_cfg)? - &public_const_poly(public, ShaPublicCol::K, row + 3, field_cfg)? - - &word_poly_shifted(trace, ShaWordCol::W, row, 3, field_cfg)? + - &w + &mu_e + &int_const_poly(trace, ShaIntCol::CompUpdateE, row, field_cfg)?; @@ -2564,17 +2903,17 @@ where let s_out = public_scalar(public, ShaPublicCol::SOut, row, field_cfg)?; let r7 = scale_poly( - &(a.clone() - &public_const_poly(public, ShaPublicCol::PAIn, row, field_cfg)?), + &(a.clone() - &public_word_or_const_poly(public, ShaPublicCol::PAIn, row, field_cfg)?), &s_init, ) + &scale_poly( - &(a.clone() - &public_const_poly(public, ShaPublicCol::PAOut, row, field_cfg)?), + &(a.clone() - &public_word_or_const_poly(public, ShaPublicCol::PAOut, row, field_cfg)?), &s_out, ); let r8 = scale_poly( - &(e.clone() - &public_const_poly(public, ShaPublicCol::PEIn, row, field_cfg)?), + &(e.clone() - &public_word_or_const_poly(public, ShaPublicCol::PEIn, row, field_cfg)?), &s_init, ) + &scale_poly( - &(e.clone() - &public_const_poly(public, ShaPublicCol::PEOut, row, field_cfg)?), + &(e.clone() - &public_word_or_const_poly(public, ShaPublicCol::PEOut, row, field_cfg)?), &s_out, ); @@ -2589,7 +2928,7 @@ where + &mu_ff_e + &int_const_poly(trace, ShaIntCol::CompFeedForwardE, row, field_cfg)?; let r11 = scale_poly( - &(w - &public_const_poly(public, ShaPublicCol::Message, row, field_cfg)?), + &(w - &public_word_or_const_poly(public, ShaPublicCol::Message, row, field_cfg)?), &s_msg, ); @@ -2656,7 +2995,43 @@ fn validate_trace(trace: &ProjectedShaTrace) -> Result<(), ShaProjectionEr } fn validate_public(public: &ProjectedShaPublic) -> Result<(), ShaProjectionError> { - validate_matrix("public.columns", &public.columns.columns, SHA_ROW_COUNT) + validate_matrix("public.columns", &public.columns.columns, SHA_ROW_COUNT)?; + if let Some(word_columns) = &public.word_columns { + validate_public_word_columns(word_columns)?; + } + Ok(()) +} + +fn validate_public_word_columns( + word_columns: &ShaPublicWordColumns, +) -> Result<(), ShaProjectionError> { + if word_columns.columns.len() != ShaPublicWordCol::COUNT { + return Err(ShaProjectionError::MissingColumn { + kind: "public.word_columns", + col: ShaPublicWordCol::COUNT, + }); + } + for (col, rows) in word_columns.columns.iter().enumerate() { + if rows.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "public.word_columns", + col, + got: rows.len(), + expected: SHA_ROW_COUNT, + }); + } + for (row, bits) in rows.iter().enumerate() { + if bits.len() != SHA_WORD_BITS { + return Err(ShaProjectionError::BitCount { + col, + row, + got: bits.len(), + expected: SHA_WORD_BITS, + }); + } + } + } + Ok(()) } fn validate_matrix( @@ -2836,6 +3211,49 @@ where )) } +fn public_word_or_const_poly( + public: &ProjectedShaPublic, + col: ShaPublicCol, + row: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + let Some(word_col) = col.public_word_col() else { + return public_const_poly(public, col, row, field_cfg); + }; + let Some(word_columns) = &public.word_columns else { + return public_const_poly(public, col, row, field_cfg); + }; + if row >= SHA_ROW_COUNT { + return Ok(DynamicPolynomialF::ZERO); + } + let col_idx = word_col.index(); + let rows = word_columns + .columns + .get(col_idx) + .ok_or(ShaProjectionError::MissingColumn { + kind: "public.word_columns", + col: col_idx, + })?; + let bits = rows.get(row).ok_or(ShaProjectionError::ColumnRowCount { + kind: "public.word_columns", + col: col_idx, + got: rows.len(), + expected: SHA_ROW_COUNT, + })?; + if bits.len() != SHA_WORD_BITS { + return Err(ShaProjectionError::BitCount { + col: col_idx, + row, + got: bits.len(), + expected: SHA_WORD_BITS, + }); + } + Ok(DynamicPolynomialF::new_trimmed(bits.clone())) +} + fn int_scalar( trace: &ProjectedShaTrace, col: ShaIntCol, @@ -3246,6 +3664,29 @@ where Ok(out) } +fn fold_optional_3d<'a, F, I>( + tensors: I, + theta: &[F], + field_cfg: &F::Config, +) -> Result>>>, ShaProjectionError> +where + F: PrimeField + 'a, + I: IntoIterator>>, +{ + let tensors = tensors.into_iter().collect::>(); + if tensors.iter().all(Option::is_none) { + return Ok(None); + } + let mut present = Vec::with_capacity(tensors.len()); + for tensor in tensors { + let Some(tensor) = tensor else { + return Err(ShaProjectionError::PublicWordColumnPresenceMismatch); + }; + present.push(&tensor.columns); + } + fold_3d(present, theta, field_cfg).map(Some) +} + #[cfg(test)] mod tests { use super::*; @@ -3286,6 +3727,7 @@ mod tests { columns: ShaPublicColumns { columns: vec![vec![F::zero_with_cfg(&cfg); SHA_ROW_COUNT]; ShaPublicCol::COUNT], }, + word_columns: None, } } diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 69c646db..1a24f9aa 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -4,7 +4,7 @@ //! `Proof`: production ProjectionFold has a different transcript order and //! derives folded commitments only after SumFold fixes the instance-axis point. -use std::io::Cursor; +use std::{borrow::Cow, io::Cursor, marker::PhantomData}; use crate::{ ZincTypes, @@ -27,13 +27,15 @@ use zinc_piop::{ neutron_nova::{ NUM_NONZERO_SHA_FAMILIES, NUM_SHA_RESIDUAL_FAMILIES, ProjectedShaPublic, ProjectedShaTrace, SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, ShaBooleanitySource, ShaIntCol, - ShaProjectionError, ShaPublicCol, ShaResidualFamily, ShaSumFoldOutput, ShaWordCol, - build_dense_sha_sumfold_group, build_expression_folded_row_sumcheck_group, - build_folded_row_sumcheck_group, build_fresh_sha_ideal_cache, evaluate_fresh_sha_targets, - finalize_sha_sumfold, fold_projected_sha_traces, folded_row_integrand_sum, - production_sha_booleanity_sources, production_sha_nonzero_families, sha_int_at_point, - sha_public_at_point, sha_scalarized_word_at_point, sha_word_bits_at_point, - verify_folded_row_sumcheck_claim, verify_fresh_sha_ideal_polys, + ShaLinearResidualCoeffCache, ShaProjectionError, ShaPublicCol, ShaPublicWordColumns, + ShaResidualFamily, ShaSumFoldOutput, ShaWordCol, build_dense_sha_sumfold_group, + build_expression_folded_row_sumcheck_group, build_folded_row_sumcheck_group, + build_production_sha_sumfold_group_with_linear_cache, + build_sha_linear_residual_coeff_cache, finalize_sha_sumfold, fold_projected_sha_traces, + folded_row_integrand_sum, production_sha_booleanity_sources, + production_sha_nonzero_families, sha_int_at_point, sha_public_at_point, + sha_scalarized_word_at_point, sha_word_bits_at_point, verify_folded_row_sumcheck_claim, + verify_fresh_sha_ideal_polys, }, sumcheck::{ SumCheckError, @@ -52,7 +54,7 @@ use zinc_poly::{ }; use zinc_transcript::Blake3Transcript; use zinc_transcript::traits::{ConstTranscribable, Transcribable, Transcript}; -use zinc_uair::ShiftSpec; +use zinc_uair::{ShiftSpec, Uair, UairSignature, UairTrace, UairWitness}; use zinc_utils::{ UNCHECKED, delayed_reduction::DelayedFieldProductSum, inner_product::InnerProduct, inner_transparent_field::InnerTransparentField, @@ -102,6 +104,28 @@ where pub witness_polys: ProductionShaWitnessPolys, } +pub trait ProductionShaProjectionAdapter: Uair +where + Zt: ZincTypes, + F: PrimeField, +{ + fn project_production_sha_witness( + shape: &UairShape, + public_trace: &UairTrace<'_, Zt::Int, Zt::Int, D>, + witness_trace: &UairTrace<'_, Zt::Int, Zt::Int, D>, + field_cfg: &F::Config, + ) -> Result< + ( + ProjectedShaTrace, + ProjectedShaPublic, + ProductionShaWitnessPolys, + ), + ProductionShaError, + > + where + Self: Sized; +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ShaEndpointEvals { pub sources: Vec>, @@ -179,6 +203,153 @@ const SHA_IDEAL_EVAL_POWER_COUNT: usize = 62; const PRODUCTION_SHA_FRESH_BATCH_DOMAIN: &[u8] = b"PF_CONCISE_SHA256_FRESH_BATCH_V1"; +#[derive(Clone, Debug)] +pub struct UairShape { + pub num_vars: usize, + pub signature: UairSignature, + _marker: PhantomData, +} + +impl UairShape { + pub fn new(num_vars: usize) -> Self { + Self { + num_vars, + signature: U::signature(), + _marker: PhantomData, + } + } +} + +#[derive(Clone, Debug)] +pub struct UairInstance<'a, PolyCoeff: Clone, Int: Clone, Commitments, const D: usize> { + pub public_trace: UairTrace<'a, PolyCoeff, Int, D>, + pub commitments: Commitments, +} + +#[derive(Clone, Debug)] +pub struct LinearIdealFoldProverParams +where + U: Uair, + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub pcs_params: PCSParams, + pub field_cfg: F::Config, + pub prefix_vars: usize, + _marker: PhantomData, +} + +impl LinearIdealFoldProverParams +where + U: Uair, + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub fn new( + pcs_params: PCSParams, + field_cfg: F::Config, + prefix_vars: usize, + ) -> Self { + Self { + pcs_params, + field_cfg, + prefix_vars, + _marker: PhantomData, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinearIdealFoldProveOutput { + pub fresh_instances: Vec, + pub folded_instance: FoldedInstance, + pub folded_witness: FoldedWitness, + pub proof: Proof, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FoldedLinearIdealInstance { + pub target: F, + pub commitments: Commitments, + pub public: Public, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FoldedLinearIdealWitness { + pub witness: Witness, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FoldedPublicEvals { + pub values: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FoldedUairTrace { + pub values: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct IdealFamilyId(pub u16); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct IdealPolySlot(pub u16); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IdealFamilyPolys { + pub family: IdealFamilyId, + pub polys: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IdealFamilyPoly { + pub slot: IdealPolySlot, + pub poly: DynamicPolynomialF, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct SourceId(pub u16); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ShiftId(pub u16); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TerminalEvals { + pub polynomial_sources: Vec>, + pub scalar_sources: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TerminalPolynomialEval { + pub source: SourceId, + pub shift: ShiftId, + pub scalarized: F, + pub coeffs: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TerminalScalarEval { + pub source: SourceId, + pub shift: ShiftId, + pub value: F, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OpeningEvals { + pub polynomial_sources: Vec>, + pub scalar_sources: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinearIdealFoldProof { + pub ideal_family_polys: Vec>, + pub nifs: MultiDegreeSumcheckProof, +} + +pub type LinearIdealFoldError = ProductionShaError; + #[derive(Clone, Debug, PartialEq, Eq)] pub struct FoldedRowSumcheckOutput { pub r_star: Vec, @@ -301,10 +472,115 @@ pub fn absorb_projected_sha_publics( transcript.absorb_slice(&(col.len() as u64).to_le_bytes()); transcript.absorb_random_field_slice(col, &mut buf); } + match &public.word_columns { + Some(word_columns) => { + transcript.absorb_slice(&[1]); + transcript.absorb_slice(&(word_columns.columns.len() as u64).to_le_bytes()); + for (col_idx, rows) in word_columns.columns.iter().enumerate() { + transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); + transcript.absorb_slice(&(rows.len() as u64).to_le_bytes()); + for (row_idx, bits) in rows.iter().enumerate() { + transcript.absorb_slice(&(row_idx as u64).to_le_bytes()); + transcript.absorb_slice(&(bits.len() as u64).to_le_bytes()); + transcript.absorb_random_field_slice(bits, &mut buf); + } + } + } + None => transcript.absorb_slice(&[0]), + } } transcript.absorb_slice(b"production_sha_publics_end"); } +fn absorb_uair_shape_metadata(transcript: &mut impl Transcript, shape: &UairShape) { + let sig = &shape.signature; + + transcript.absorb_slice(b"uair_shape_metadata_begin"); + transcript.absorb_slice(&(shape.num_vars as u64).to_le_bytes()); + absorb_uair_column_counts( + transcript, + sig.total_cols().num_binary_poly_cols(), + sig.total_cols().num_arbitrary_poly_cols(), + sig.total_cols().num_int_cols(), + ); + absorb_uair_column_counts( + transcript, + sig.public_cols().num_binary_poly_cols(), + sig.public_cols().num_arbitrary_poly_cols(), + sig.public_cols().num_int_cols(), + ); + absorb_uair_column_counts( + transcript, + sig.witness_cols().num_binary_poly_cols(), + sig.witness_cols().num_arbitrary_poly_cols(), + sig.witness_cols().num_int_cols(), + ); + transcript.absorb_slice(&(sig.shifts().len() as u64).to_le_bytes()); + for shift in sig.shifts() { + transcript.absorb_slice(&(shift.source_col() as u64).to_le_bytes()); + transcript.absorb_slice(&(shift.shift_amount() as u64).to_le_bytes()); + } + transcript.absorb_slice(b"uair_shape_metadata_end"); +} + +fn absorb_uair_column_counts( + transcript: &mut impl Transcript, + binary: usize, + arbitrary: usize, + int: usize, +) { + transcript.absorb_slice(&(binary as u64).to_le_bytes()); + transcript.absorb_slice(&(arbitrary as u64).to_le_bytes()); + transcript.absorb_slice(&(int as u64).to_le_bytes()); +} + +fn absorb_transcribable(transcript: &mut impl Transcript, value: &T) { + let mut buf = vec![0u8; value.get_num_bytes() + T::LENGTH_NUM_BYTES]; + value.write_transcription_bytes_subset(&mut buf); + transcript.absorb_slice(&buf); +} + +fn absorb_public_uair_trace( + transcript: &mut impl Transcript, + instance_idx: usize, + trace: &UairTrace<'_, Zt::Int, Zt::Int, D>, +) where + Zt: ZincTypes, +{ + transcript.absorb_slice(b"uair_public_trace_begin"); + transcript.absorb_slice(&(instance_idx as u64).to_le_bytes()); + transcript.absorb_slice(&(trace.binary_poly.len() as u64).to_le_bytes()); + for (col_idx, col) in trace.binary_poly.iter().enumerate() { + transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); + transcript.absorb_slice(&(col.num_vars as u64).to_le_bytes()); + transcript.absorb_slice(&(col.evaluations.len() as u64).to_le_bytes()); + for value in &col.evaluations { + absorb_transcribable(transcript, value); + } + } + transcript.absorb_slice(&(trace.arbitrary_poly.len() as u64).to_le_bytes()); + for (col_idx, col) in trace.arbitrary_poly.iter().enumerate() { + transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); + transcript.absorb_slice(&(col.num_vars as u64).to_le_bytes()); + transcript.absorb_slice(&(col.evaluations.len() as u64).to_le_bytes()); + for poly in &col.evaluations { + for coeff in poly.iter() { + absorb_transcribable(transcript, coeff); + } + } + } + transcript.absorb_slice(&(trace.int.len() as u64).to_le_bytes()); + for (col_idx, col) in trace.int.iter().enumerate() { + transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); + transcript.absorb_slice(&(col.num_vars as u64).to_le_bytes()); + transcript.absorb_slice(&(col.evaluations.len() as u64).to_le_bytes()); + for value in &col.evaluations { + absorb_transcribable(transcript, value); + } + } + transcript.absorb_slice(b"uair_public_trace_end"); +} + fn absorb_production_sha_statement_metadata(transcript: &mut impl Transcript) { transcript.absorb_slice(PRODUCTION_SHA_FRESH_BATCH_DOMAIN); transcript.absorb_slice(b"production_sha_statement_metadata_begin"); @@ -399,6 +675,25 @@ pub fn absorb_fresh_sha_ideal_polys( transcript.absorb_slice(b"production_sha_fresh_ideals_end"); } +pub fn absorb_aggregate_sha_ideal_polys( + transcript: &mut impl Transcript, + ideal_polys: &[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], +) where + F: PrimeField, + F::Inner: ConstTranscribable + Transcribable, + F::Modulus: Transcribable, +{ + let mut buf = vec![0; F::Inner::NUM_BYTES]; + transcript.absorb_slice(b"production_sha_aggregate_ideals_begin"); + transcript.absorb_slice(&(ideal_polys.len() as u64).to_le_bytes()); + for (family_idx, poly) in ideal_polys.iter().enumerate() { + transcript.absorb_slice(&(family_idx as u64).to_le_bytes()); + transcript.absorb_slice(&(poly.coeffs.len() as u64).to_le_bytes()); + transcript.absorb_random_field_slice(&poly.coeffs, &mut buf); + } + transcript.absorb_slice(b"production_sha_aggregate_ideals_end"); +} + pub fn absorb_sha_endpoint_evals( transcript: &mut impl Transcript, endpoint_evals: &ShaEndpointEvals, @@ -454,11 +749,11 @@ where std::array::from_fn(|_| transcript.get_field_challenge(field_cfg)) } -pub fn sample_post_ideal_challenges( +pub fn sample_instance_batch_challenge( transcript: &mut impl Transcript, instance_count: usize, field_cfg: &F::Config, -) -> Result<(F, F, F, F, Vec), ProductionShaError> +) -> Result, ProductionShaError> where F: PrimeField, F::Inner: ConstTranscribable, @@ -469,12 +764,41 @@ where )); } let ell = usize::try_from(instance_count.trailing_zeros()).expect("ell fits usize"); - Ok(( + Ok(transcript.get_field_challenges(ell, field_cfg)) +} + +pub fn sample_post_aggregate_ideal_challenges( + transcript: &mut impl Transcript, + field_cfg: &F::Config, +) -> (F, F, F, F) +where + F: PrimeField, + F::Inner: ConstTranscribable, +{ + ( transcript.get_field_challenge(field_cfg), transcript.get_field_challenge(field_cfg), transcript.get_field_challenge(field_cfg), transcript.get_field_challenge(field_cfg), - transcript.get_field_challenges(ell, field_cfg), + ) +} + +pub fn sample_post_ideal_challenges( + transcript: &mut impl Transcript, + instance_count: usize, + field_cfg: &F::Config, +) -> Result<(F, F, F, F, Vec), ProductionShaError> +where + F: PrimeField, + F::Inner: ConstTranscribable, +{ + let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); + Ok(( + a, + lambda, + rho, + xi, + sample_instance_batch_challenge(transcript, instance_count, field_cfg)?, )) } @@ -489,6 +813,17 @@ where Ok(()) } +pub fn check_aggregate_sha_ideal_membership( + ideal_polys: &[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], + field_cfg: &F::Config, +) -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + verify_fresh_sha_ideal_polys(std::slice::from_ref(ideal_polys), field_cfg)?; + Ok(()) +} + impl ProductionShaOpeningPCS for AllHyraxPCSTypes where Zt: ZincTypes, @@ -740,13 +1075,22 @@ where } #[allow(clippy::too_many_arguments)] -pub fn prove_production_sha_core( +pub fn prove_linear_ideal_fold( + pp: &LinearIdealFoldProverParams, + shape: &UairShape, + witnesses: &[UairWitness<'_, Zt::Int, Zt::Int, D>], transcript: &mut impl Transcript, - pcs_params: &PCSParams, - instances: &[ProductionShaProverInstance], - field_cfg: &F::Config, -) -> Result>, ProductionShaError> +) -> Result< + LinearIdealFoldProveOutput< + UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, + FoldedLinearIdealInstance, FoldedPublicEvals>, + FoldedLinearIdealWitness>, + LinearIdealFoldProof, + >, + LinearIdealFoldError, +> where + U: Uair + ProductionShaProjectionAdapter, Zt: ZincTypes, F: InnerTransparentField + DelayedFieldProductSum @@ -756,40 +1100,48 @@ where + 'static, F::Inner: ConstTranscribable + Transcribable + Zero + Default + Send + Sync, F::Modulus: ConstTranscribable + Transcribable, - P: ProductionShaOpeningPCS, + P: ProductionShaPCS, P::BinaryPCS: FoldablePCS, D>, P::ArbitraryPCS: FoldablePCS, D>, P::IntPCS: FoldablePCS, { + let field_cfg = &pp.field_cfg; ensure_production_sha_word_degree::()?; - if instances.len() < 2 { - return Err(ProductionShaError::InstanceCountTooSmall(instances.len())); + if witnesses.len() < 2 { + return Err(ProductionShaError::InstanceCountTooSmall(witnesses.len())); } - if !instances.len().is_power_of_two() { + if !witnesses.len().is_power_of_two() { return Err(ProductionShaError::InstanceCountNotPowerOfTwo( - instances.len(), + witnesses.len(), )); } + let booleanity_sources = production_sha_booleanity_sources(); absorb_production_sha_statement_metadata(transcript); - - let mut prover_data = Vec::with_capacity(instances.len()); - let mut instance_commitments = Vec::with_capacity(instances.len()); - for instance in instances { - let (data, commitment) = - commit_production_sha_instance::(pcs_params, &instance.witness_polys)?; - prover_data.push(data); + absorb_uair_shape_metadata(transcript, shape); + + let mut fresh_instances = Vec::with_capacity(witnesses.len()); + let mut instance_commitments = Vec::with_capacity(witnesses.len()); + let mut traces = Vec::with_capacity(witnesses.len()); + let mut publics = Vec::with_capacity(witnesses.len()); + + for witness in witnesses { + let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; + let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; + absorb_public_uair_trace::(transcript, fresh_instances.len(), &public_trace); + let (trace, public, witness_polys) = + U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg)?; + let (_data, commitment) = + commit_production_sha_instance::(&pp.pcs_params, &witness_polys)?; + + fresh_instances.push(UairInstance { + public_trace: own_uair_trace(&public_trace), + commitments: commitment.clone(), + }); instance_commitments.push(commitment); + traces.push(trace); + publics.push(public); } - - let traces = instances - .iter() - .map(|instance| instance.trace.clone()) - .collect::>(); - let publics = instances - .iter() - .map(|instance| instance.public.clone()) - .collect::>(); validate_production_sha_publics(&publics, field_cfg)?; absorb_production_sha_commitments::( @@ -800,16 +1152,16 @@ where absorb_projected_sha_publics(transcript, &publics); let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); - let mut ideal_cache = build_fresh_sha_ideal_cache(&traces, &publics, r_ic.clone(), field_cfg)?; - absorb_fresh_sha_ideal_polys(transcript, &ideal_cache.ideal_polys); - check_fresh_sha_ideal_membership(&ideal_cache.ideal_polys, field_cfg)?; - - let (a, lambda, rho, xi, beta) = - sample_post_ideal_challenges(transcript, instances.len(), field_cfg)?; - evaluate_fresh_sha_targets(&mut ideal_cache, &a, &lambda, field_cfg)?; - let initial_claim = eq_weighted_sum(&beta, &ideal_cache.fresh_targets, field_cfg)?; - - let (sumfold_proof, sumfold_output) = prove_full_sha_sumfold( + let coeff_cache = build_sha_linear_residual_coeff_cache(&traces, &publics, &r_ic, field_cfg)?; + let beta = sample_instance_batch_challenge(transcript, witnesses.len(), field_cfg)?; + let aggregate_ideal_polys = coeff_cache.beta_aggregate_nonzero_ideal_polys(&beta, field_cfg)?; + absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys); + check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, field_cfg)?; + + let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); + let initial_claim = + evaluate_aggregate_sha_ideal_claim(&aggregate_ideal_polys, &a, &lambda, field_cfg)?; + let (sumfold_proof, sumfold_output) = prove_optimized_sha_sumfold( transcript, &traces, &publics, @@ -821,89 +1173,230 @@ where &rho, &xi, &booleanity_sources, + &coeff_cache, + pp.prefix_vars, field_cfg, )?; - let (folded, folded_public) = - fold_projected_sha_traces(&traces, &publics, &sumfold_output, field_cfg)?; let folded_commitments = fold_pcs_commitments::( &instance_commitments, sumfold_output.theta(), field_cfg, )?; - let folded_prover_data = - fold_pcs_prover_data::(&prover_data, sumfold_output.theta(), field_cfg)?; - absorb_production_sha_commitments::( - transcript, - b"production_sha_derived_folded_commitments", - std::slice::from_ref(&folded_commitments), - ); + let (folded, folded_public) = + fold_projected_sha_traces(&traces, &publics, &sumfold_output, field_cfg)?; - let (folded_row_sumcheck, row_output) = prove_expression_folded_row_sumcheck_with_output( - transcript, - &folded.trace, - &folded_public, - &r_ic, - &a, - &lambda, - &rho, - &xi, - &booleanity_sources, - sumfold_output.t_prime(), - field_cfg, - )?; - let endpoint_evals = - build_sha_endpoint_evals_from_trace(&folded.trace, &row_output.r_star, field_cfg)?; - absorb_sha_endpoint_evals(transcript, &endpoint_evals); - let terminal = reconstruct_folded_row_terminal_from_endpoints( - &endpoint_evals, - &folded_public, - &r_ic, - &row_output.r_star, - &a, - &lambda, - &rho, - &xi, - &booleanity_sources, - field_cfg, - )?; - verify_folded_row_terminal_value(&row_output, &terminal)?; + Ok(LinearIdealFoldProveOutput { + fresh_instances, + folded_instance: FoldedLinearIdealInstance { + target: sumfold_output.t_prime().clone(), + commitments: folded_commitments, + public: flatten_projected_sha_public(&folded_public), + }, + folded_witness: FoldedLinearIdealWitness { + witness: flatten_projected_sha_trace(&folded.trace), + }, + proof: LinearIdealFoldProof { + ideal_family_polys: sha_ideal_family_polys(&aggregate_ideal_polys), + nifs: sumfold_proof, + }, + }) +} - let (multipoint_eval, r_0) = prove_sha_endpoint_multipoint( - transcript, - &folded.trace, - &folded_public, - &endpoint_evals, - &row_output.r_star, - field_cfg, - )?; - let folded_lifted_evals = build_folded_sha_pcs_lifted_evals(&folded.trace, &r_0, field_cfg)?; - absorb_folded_lifted_evals(transcript, &folded_lifted_evals); - let pcs_opening_bytes = P::prove_folded_sha_opening( - pcs_params, - &folded_prover_data, - &folded_commitments, - &folded.trace, - &r_0, - &folded_lifted_evals, - field_cfg, - )?; - transcript.absorb_slice(b"production_sha_pcs_opening_bytes"); - transcript.absorb_slice(&(pcs_opening_bytes.len() as u64).to_le_bytes()); - transcript.absorb_slice(&pcs_opening_bytes); - - Ok(ProductionShaProof { - instance_commitments, - fresh_ideal_polys: ideal_cache.ideal_polys, - sumfold_proof, - folded_row_sumcheck, - endpoint_evals, - multipoint_eval, - folded_lifted_evals, - pcs_opening_bytes, +fn public_uair_trace_view<'a, PolyCoeff, Int, F, const D: usize>( + trace: &'a UairTrace<'_, PolyCoeff, Int, D>, + sig: &UairSignature, +) -> Result, ProductionShaError> +where + PolyCoeff: Clone, + Int: Clone, + F: PrimeField, +{ + let public = sig.public_cols(); + validate_uair_trace_shape(trace, sig)?; + Ok(UairTrace { + binary_poly: Cow::Borrowed( + trace + .binary_poly + .get(..public.num_binary_poly_cols()) + .ok_or(ProductionShaError::LengthMismatch { + label: "UAIR public binary columns", + got: trace.binary_poly.len(), + expected: public.num_binary_poly_cols(), + })?, + ), + arbitrary_poly: Cow::Borrowed( + trace + .arbitrary_poly + .get(..public.num_arbitrary_poly_cols()) + .ok_or(ProductionShaError::LengthMismatch { + label: "UAIR public arbitrary columns", + got: trace.arbitrary_poly.len(), + expected: public.num_arbitrary_poly_cols(), + })?, + ), + int: Cow::Borrowed(trace.int.get(..public.num_int_cols()).ok_or( + ProductionShaError::LengthMismatch { + label: "UAIR public int columns", + got: trace.int.len(), + expected: public.num_int_cols(), + }, + )?), }) } +fn witness_uair_trace_view<'a, PolyCoeff, Int, F, const D: usize>( + trace: &'a UairTrace<'_, PolyCoeff, Int, D>, + sig: &UairSignature, +) -> Result, ProductionShaError> +where + PolyCoeff: Clone, + Int: Clone, + F: PrimeField, +{ + let public = sig.public_cols(); + let total = sig.total_cols(); + validate_uair_trace_shape(trace, sig)?; + Ok(UairTrace { + binary_poly: Cow::Borrowed( + trace + .binary_poly + .get(public.num_binary_poly_cols()..total.num_binary_poly_cols()) + .ok_or(ProductionShaError::LengthMismatch { + label: "UAIR witness binary columns", + got: trace.binary_poly.len(), + expected: total.num_binary_poly_cols(), + })?, + ), + arbitrary_poly: Cow::Borrowed( + trace + .arbitrary_poly + .get(public.num_arbitrary_poly_cols()..total.num_arbitrary_poly_cols()) + .ok_or(ProductionShaError::LengthMismatch { + label: "UAIR witness arbitrary columns", + got: trace.arbitrary_poly.len(), + expected: total.num_arbitrary_poly_cols(), + })?, + ), + int: Cow::Borrowed( + trace + .int + .get(public.num_int_cols()..total.num_int_cols()) + .ok_or(ProductionShaError::LengthMismatch { + label: "UAIR witness int columns", + got: trace.int.len(), + expected: total.num_int_cols(), + })?, + ), + }) +} + +fn validate_uair_trace_shape( + trace: &UairTrace<'_, PolyCoeff, Int, D>, + sig: &UairSignature, +) -> Result<(), ProductionShaError> +where + PolyCoeff: Clone, + Int: Clone, + F: PrimeField, +{ + let total = sig.total_cols(); + if trace.binary_poly.len() != total.num_binary_poly_cols() { + return Err(ProductionShaError::LengthMismatch { + label: "UAIR binary columns", + got: trace.binary_poly.len(), + expected: total.num_binary_poly_cols(), + }); + } + if trace.arbitrary_poly.len() != total.num_arbitrary_poly_cols() { + return Err(ProductionShaError::LengthMismatch { + label: "UAIR arbitrary columns", + got: trace.arbitrary_poly.len(), + expected: total.num_arbitrary_poly_cols(), + }); + } + if trace.int.len() != total.num_int_cols() { + return Err(ProductionShaError::LengthMismatch { + label: "UAIR int columns", + got: trace.int.len(), + expected: total.num_int_cols(), + }); + } + Ok(()) +} + +fn own_uair_trace( + trace: &UairTrace<'_, PolyCoeff, Int, D>, +) -> UairTrace<'static, PolyCoeff, Int, D> +where + PolyCoeff: Clone, + Int: Clone, +{ + UairTrace { + binary_poly: Cow::Owned(trace.binary_poly.iter().cloned().collect()), + arbitrary_poly: Cow::Owned(trace.arbitrary_poly.iter().cloned().collect()), + int: Cow::Owned(trace.int.iter().cloned().collect()), + } +} + +fn flatten_projected_sha_public(public: &ProjectedShaPublic) -> FoldedPublicEvals +where + F: PrimeField, +{ + let mut values = public + .columns + .columns + .iter() + .flat_map(|column| column.iter().cloned()) + .collect::>(); + if let Some(word_columns) = &public.word_columns { + values.extend( + word_columns + .columns + .iter() + .flat_map(|column| column.iter().flat_map(|bits| bits.iter().cloned())), + ); + } + FoldedPublicEvals { values } +} + +fn flatten_projected_sha_trace(trace: &ProjectedShaTrace) -> FoldedUairTrace +where + F: PrimeField, +{ + let mut values = Vec::new(); + values.extend( + trace + .bit_slices + .columns + .iter() + .flat_map(|column| column.iter()) + .flat_map(|row| row.iter().cloned()), + ); + values.extend( + trace + .scalarized_words + .words + .iter() + .flat_map(|column| column.iter().cloned()), + ); + values.extend( + trace + .int_columns + .columns + .iter() + .flat_map(|column| column.iter().cloned()), + ); + values.extend( + trace + .public_columns + .columns + .iter() + .flat_map(|column| column.iter().cloned()), + ); + FoldedUairTrace { values } +} + pub fn verify_production_sha_core( transcript: &mut impl Transcript, pcs_params: &PCSVerifierParams, @@ -1043,31 +1536,6 @@ where Ok(()) } -#[allow(clippy::too_many_arguments)] -pub fn prove_production_sha( - transcript: &mut impl Transcript, - pcs_params: &PCSParams, - instances: &[ProductionShaProverInstance], - field_cfg: &F::Config, -) -> Result>, ProductionShaError> -where - Zt: ZincTypes, - F: InnerTransparentField - + DelayedFieldProductSum - + FromPrimitiveWithConfig - + Send - + Sync - + 'static, - F::Inner: ConstTranscribable + Transcribable + Zero + Default + Send + Sync, - F::Modulus: ConstTranscribable + Transcribable, - P: ProductionShaOpeningPCS, - P::BinaryPCS: FoldablePCS, D>, - P::ArbitraryPCS: FoldablePCS, D>, - P::IntPCS: FoldablePCS, -{ - prove_production_sha_core::(transcript, pcs_params, instances, field_cfg) -} - pub fn verify_production_sha( transcript: &mut impl Transcript, pcs_params: &PCSVerifierParams, @@ -1129,6 +1597,93 @@ where .collect() } +fn beta_aggregate_sha_ideal_polys( + ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], + beta: &[F], + field_cfg: &F::Config, +) -> Result<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], ProductionShaError> +where + F: PrimeField, +{ + let weights = build_eq_x_r_vec(beta, field_cfg)?; + if weights.len() != ideal_polys.len() { + return Err(ProductionShaError::LengthMismatch { + label: "beta weights/fresh ideal polys", + got: weights.len(), + expected: ideal_polys.len(), + }); + } + + let mut aggregate: [DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES] = + std::array::from_fn(|_| DynamicPolynomialF::ZERO); + for (weight, instance) in weights.iter().zip(ideal_polys) { + for (slot, poly) in instance.iter().enumerate() { + let weighted = scale_production_sha_poly(poly, weight); + aggregate[slot] += &weighted; + } + } + aggregate.iter_mut().for_each(DynamicPolynomialF::trim); + Ok(aggregate) +} + +fn scale_production_sha_poly(poly: &DynamicPolynomialF, scalar: &F) -> DynamicPolynomialF +where + F: PrimeField, +{ + DynamicPolynomialF::new_trimmed( + poly.coeffs + .iter() + .map(|coeff| coeff.clone() * scalar) + .collect::>(), + ) +} + +fn evaluate_aggregate_sha_ideal_claim( + ideal_polys: &[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], + a: &F, + lambda: &F, + field_cfg: &F::Config, +) -> Result> +where + F: DelayedFieldProductSum, +{ + let lambda_powers = zinc_utils::powers( + lambda.clone(), + F::one_with_cfg(field_cfg), + NUM_SHA_RESIDUAL_FAMILIES, + ); + let a_powers = zinc_utils::powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_IDEAL_EVAL_POWER_COUNT, + ); + let mut target = F::zero_with_cfg(field_cfg); + for (slot, family) in production_sha_nonzero_families().iter().enumerate() { + target += lambda_powers[family.index()].clone() + * evaluate_production_sha_poly_at_powers(&ideal_polys[slot], &a_powers, field_cfg)?; + } + Ok(target) +} + +fn sha_ideal_family_polys( + ideal_polys: &[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], +) -> Vec> +where + F: PrimeField, +{ + production_sha_nonzero_families() + .iter() + .enumerate() + .map(|(slot, family)| IdealFamilyPolys { + family: IdealFamilyId(family.index() as u16), + polys: vec![IdealFamilyPoly { + slot: IdealPolySlot(0), + poly: ideal_polys[slot].clone(), + }], + }) + .collect() +} + fn evaluate_production_sha_poly_at_powers( poly: &DynamicPolynomialF, powers: &[F], @@ -1220,6 +1775,9 @@ where .map(|col| col.len()) .unwrap_or(0); let mut columns = vec![vec![F::zero_with_cfg(field_cfg); row_count]; col_count]; + let mut word_columns = first.word_columns.as_ref().map(|words| { + vec![vec![vec![F::zero_with_cfg(field_cfg); SHA_WORD_BITS]; row_count]; words.columns.len()] + }); for (public, weight) in publics.iter().zip(theta.iter()) { if public.columns.columns.len() != col_count { return Err(ProductionShaError::LengthMismatch { @@ -1231,18 +1789,59 @@ where for (col_idx, col) in public.columns.columns.iter().enumerate() { if col.len() != row_count { return Err(ProductionShaError::LengthMismatch { - label: "public row count", - got: col.len(), - expected: row_count, + label: "public row count", + got: col.len(), + expected: row_count, + }); + } + for (out, value) in columns[col_idx].iter_mut().zip(col.iter()) { + *out += weight.clone() * value; + } + } + match (&mut word_columns, &public.word_columns) { + (Some(out_columns), Some(public_words)) => { + if public_words.columns.len() != out_columns.len() { + return Err(ProductionShaError::LengthMismatch { + label: "public word column count", + got: public_words.columns.len(), + expected: out_columns.len(), + }); + } + for (col_idx, rows) in public_words.columns.iter().enumerate() { + if rows.len() != row_count { + return Err(ProductionShaError::LengthMismatch { + label: "public word row count", + got: rows.len(), + expected: row_count, + }); + } + for (row_idx, bits) in rows.iter().enumerate() { + if bits.len() != SHA_WORD_BITS { + return Err(ProductionShaError::LengthMismatch { + label: "public word bit count", + got: bits.len(), + expected: SHA_WORD_BITS, + }); + } + for (out, value) in out_columns[col_idx][row_idx].iter_mut().zip(bits) { + *out += weight.clone() * value; + } + } + } + } + (None, None) => {} + (Some(_), None) | (None, Some(_)) => { + return Err(ProductionShaError::LengthMismatch { + label: "public word column presence", + got: usize::from(public.word_columns.is_some()), + expected: usize::from(word_columns.is_some()), }); } - for (out, value) in columns[col_idx].iter_mut().zip(col.iter()) { - *out += weight.clone() * value; - } } } Ok(ProjectedShaPublic { columns: zinc_piop::neutron_nova::ShaPublicColumns { columns }, + word_columns: word_columns.map(|columns| ShaPublicWordColumns { columns }), }) } @@ -1340,6 +1939,7 @@ where } } +#[allow(dead_code)] fn build_folded_sha_pcs_lifted_evals( folded_trace: &ProjectedShaTrace, r_0: &[F], @@ -1662,6 +2262,96 @@ where )) } +#[allow(clippy::too_many_arguments)] +pub fn prove_optimized_sha_sumfold( + transcript: &mut impl Transcript, + traces: &[ProjectedShaTrace], + publics: &[ProjectedShaPublic], + initial_claim: &F, + beta: &[F], + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + linear_cache: &ShaLinearResidualCoeffCache, + prefix_vars: usize, + field_cfg: &F::Config, +) -> Result<(MultiDegreeSumcheckProof, ShaSumFoldOutput), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: ConstTranscribable + Zero, + F::Modulus: ConstTranscribable, +{ + let group = build_production_sha_sumfold_group_with_linear_cache( + traces, + publics, + linear_cache, + beta, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + prefix_vars, + field_cfg, + )?; + let ell = beta.len(); + let (proof, states) = + MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], ell, field_cfg); + require_single_sumcheck_group(&proof, "SHA SumFold")?; + if proof.claimed_sums()[0] != *initial_claim { + return Err(ProductionShaError::SumFoldTerminalMismatch); + } + + let r_b = states + .first() + .ok_or(ProductionShaError::LengthMismatch { + label: "sumfold states", + got: 0, + expected: 1, + })? + .randomness + .clone(); + let provisional = finalize_sha_sumfold( + beta, + r_b.clone(), + F::one_with_cfg(field_cfg), + traces.len(), + field_cfg, + )?; + let (folded, folded_public) = zinc_piop::neutron_nova::fold_projected_sha_traces( + traces, + publics, + &provisional, + field_cfg, + )?; + let t_prime = zinc_piop::neutron_nova::expression_folded_row_sum( + &folded.trace, + &folded_public, + r_ic, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + )?; + let d = eq_eval(beta, &r_b, F::one_with_cfg(field_cfg))?; + let c_sf = d * t_prime; + Ok(( + proof, + finalize_sha_sumfold(beta, r_b, c_sf, traces.len(), field_cfg)?, + )) +} + pub fn verify_full_sha_sumfold( transcript: &mut impl Transcript, proof: &MultiDegreeSumcheckProof, @@ -2670,7 +3360,7 @@ where - &endpoint_word_poly(endpoint_evals, ShaWordCol::Uef, 3, field_cfg)? - &endpoint_word_poly(endpoint_evals, ShaWordCol::UNegEg, 3, field_cfg)? - &endpoint_public_const_poly(folded_public, ShaPublicCol::K, 3, r_star, field_cfg)? - - &endpoint_word_poly(endpoint_evals, ShaWordCol::W, 3, field_cfg)? + - &w - &endpoint_word_poly(endpoint_evals, ShaWordCol::Sigma0, 3, field_cfg)? - &endpoint_word_poly(endpoint_evals, ShaWordCol::Maj, 3, field_cfg)? + &mu_a @@ -2683,7 +3373,7 @@ where - &endpoint_word_poly(endpoint_evals, ShaWordCol::Uef, 3, field_cfg)? - &endpoint_word_poly(endpoint_evals, ShaWordCol::UNegEg, 3, field_cfg)? - &endpoint_public_const_poly(folded_public, ShaPublicCol::K, 3, r_star, field_cfg)? - - &endpoint_word_poly(endpoint_evals, ShaWordCol::W, 3, field_cfg)? + - &w + &mu_e + &endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompUpdateE, field_cfg)?; @@ -2978,17 +3668,37 @@ mod tests { use super::*; use crate::fixed_prime; + use core::fmt::Debug; use crypto_primitives::{ - FromWithConfig, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, + FromWithConfig, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, + crypto_bigint_uint::Uint, }; use zinc_piop::neutron_nova::{ SHA_ROW_COUNT, SHA_WORD_BITS, ShaBitSliceColumns, ShaIntColumns, ShaPublicColumns, expression_folded_row_sum, fold_projected_sha_traces, scalarize_trace_words, }; use zinc_poly::mle::MultilinearExtensionWithConfig; + use zinc_poly::univariate::{binary::BinaryPolyInnerProduct, dense::DensePolyInnerProduct}; + use zinc_primality::MillerRabin; + use zinc_test_uair::{ + EC_FP_INT_LIMBS, Sha256CompressionSliceUair, sha256::cols as sha256_cols, + synthesize_sha256_chain_witnesses, + }; use zinc_transcript::{Blake3Transcript, traits::Transcript}; + use zinc_utils::inner_product::{MBSInnerProduct, ScalarProduct}; + use zip_plus::{ + code::iprs::{IprsCode, PnttConfigF65537}, + pcs::structs::ZipTypes, + pcs_transcript::{PcsProverTranscript, PcsVerifierTranscript}, + }; type F = MontyField<4>; + type ShaInt = Int; + const TEST_DEGREE_PLUS_ONE: usize = 32; + const TEST_REP: usize = 4; + const TEST_CHECKED: bool = false; + const TEST_FIELD_LIMBS: usize = 4; + const TEST_NUM_COLUMN_OPENINGS: usize = 150; fn cfg() -> ::Config { fixed_prime::secp256k1_field_cfg::>() @@ -2998,6 +3708,485 @@ mod tests { F::from_with_cfg(value, &cfg()) } + #[derive(Debug, Clone)] + struct TestShaBinaryZipTypes; + + impl ZipTypes for TestShaBinaryZipTypes { + const NUM_COLUMN_OPENINGS: usize = TEST_NUM_COLUMN_OPENINGS; + type Eval = BinaryPoly; + type Cw = DensePolynomial; + type Fmod = Uint; + type PrimeTest = MillerRabin; + type Chal = i128; + type Pt = i128; + type CombR = Int<{ EC_FP_INT_LIMBS * 4 }>; + type Comb = DensePolynomial; + type EvalDotChal = BinaryPolyInnerProduct; + type CombDotChal = DensePolyInnerProduct< + Self::CombR, + Self::Chal, + Self::CombR, + MBSInnerProduct, + TEST_DEGREE_PLUS_ONE, + >; + type ArrCombRDotChal = MBSInnerProduct; + } + + #[derive(Debug, Clone)] + struct TestShaArbitraryZipTypes; + + impl ZipTypes for TestShaArbitraryZipTypes { + const NUM_COLUMN_OPENINGS: usize = TEST_NUM_COLUMN_OPENINGS; + type Eval = DensePolynomial; + type Cw = DensePolynomial, TEST_DEGREE_PLUS_ONE>; + type Fmod = Uint; + type PrimeTest = MillerRabin; + type Chal = i128; + type Pt = i128; + type CombR = Int<{ EC_FP_INT_LIMBS * 4 }>; + type Comb = DensePolynomial; + type EvalDotChal = DensePolyInnerProduct< + ShaInt, + Self::Chal, + Self::CombR, + MBSInnerProduct, + TEST_DEGREE_PLUS_ONE, + >; + type CombDotChal = DensePolyInnerProduct< + Self::CombR, + Self::Chal, + Self::CombR, + MBSInnerProduct, + TEST_DEGREE_PLUS_ONE, + >; + type ArrCombRDotChal = MBSInnerProduct; + } + + #[derive(Debug, Clone)] + struct TestShaIntZipTypes; + + impl ZipTypes for TestShaIntZipTypes { + const NUM_COLUMN_OPENINGS: usize = TEST_NUM_COLUMN_OPENINGS; + type Eval = ShaInt; + type Cw = Int<6>; + type Fmod = Uint; + type PrimeTest = MillerRabin; + type Chal = i128; + type Pt = i128; + type CombR = Int<{ EC_FP_INT_LIMBS * 4 }>; + type Comb = Self::CombR; + type EvalDotChal = ScalarProduct; + type CombDotChal = ScalarProduct; + type ArrCombRDotChal = MBSInnerProduct; + } + + #[derive(Clone, Debug)] + struct TestShaZincTypes; + + impl ZincTypes for TestShaZincTypes { + type Int = ShaInt; + type Chal = i128; + type Pt = i128; + type Fmod = Uint; + type PrimeTest = MillerRabin; + + type BinaryZt = TestShaBinaryZipTypes; + type ArbitraryZt = TestShaArbitraryZipTypes; + type IntZt = TestShaIntZipTypes; + + type BinaryLc = IprsCode; + type ArbitraryLc = IprsCode; + type IntLc = IprsCode; + } + + #[derive(Clone, Debug, PartialEq, Eq)] + struct NoopCommitment { + batch_size: usize, + } + + #[derive(Clone, Debug)] + struct NoopPCS; + + impl PCS for NoopPCS + where + Fp: PrimeField, + Eval: Clone + Debug + Send + Sync, + { + type CommitmentKey = (); + type VerifierKey = (); + type Commitment = NoopCommitment; + type ProverData = (); + + fn commit( + _ck: &Self::CommitmentKey, + polys: &[DenseMultilinearExtension], + ) -> Result<(Self::ProverData, Self::Commitment), ZipError> { + Ok(( + (), + NoopCommitment { + batch_size: polys.len(), + }, + )) + } + + fn absorb_commitment(transcript: &mut T, commitment: &Self::Commitment) { + transcript.absorb_slice(&(commitment.batch_size as u64).to_le_bytes()); + } + + fn commitment_num_bytes(_commitment: &Self::Commitment) -> usize { + core::mem::size_of::() + } + + fn write_commitment_bytes(commitment: &Self::Commitment, buf: &mut Vec) { + buf.extend_from_slice(&(commitment.batch_size as u64).to_le_bytes()); + } + + fn batch_size(commitment: &Self::Commitment) -> usize { + commitment.batch_size + } + + fn prove_open( + _transcript: &mut PcsProverTranscript, + _ck: &Self::CommitmentKey, + _polys: &[DenseMultilinearExtension], + _point: &[Fp], + _prover_data: &Self::ProverData, + _field_cfg: &Fp::Config, + ) -> Result<(), ZipError> + where + Fp::Inner: Transcribable, + Fp::Modulus: Transcribable, + { + Ok(()) + } + + fn verify_open( + _transcript: &mut PcsVerifierTranscript, + _vk: &Self::VerifierKey, + _commitment: &Self::Commitment, + _point: &[Fp], + _lifted_evals: &[DynamicPolynomialF], + _field_cfg: &Fp::Config, + ) -> Result<(), ZipError> + where + Fp::Inner: Transcribable, + Fp::Modulus: Transcribable, + { + Ok(()) + } + } + + impl FoldablePCS for NoopPCS + where + Fp: PrimeField, + Eval: Clone + Debug + Send + Sync, + { + fn fold_commitments( + commitments: &[Self::Commitment], + _theta: &[Fp], + _field_cfg: &Fp::Config, + ) -> Result { + Ok(NoopCommitment { + batch_size: commitments + .first() + .map(|commitment| commitment.batch_size) + .unwrap_or_default(), + }) + } + + fn fold_prover_data( + _prover_data: &[Self::ProverData], + _theta: &[Fp], + _field_cfg: &Fp::Config, + ) -> Result { + Ok(()) + } + } + + #[derive(Clone, Debug)] + struct NoopPCSTypes; + + impl ZincPCSTypes for NoopPCSTypes + where + Zt: ZincTypes, + Fp: PrimeField, + { + type BinaryPCS = NoopPCS; + type ArbitraryPCS = NoopPCS; + type IntPCS = NoopPCS; + } + + impl ProductionShaPCS for NoopPCSTypes + where + Zt: ZincTypes, + Fp: PrimeField, + { + } + + fn sha_binary_col<'a>( + public_trace: &'a UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, + witness_trace: &'a UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, + flat_col: usize, + ) -> Result< + &'a DenseMultilinearExtension>, + ProductionShaError, + > { + if flat_col < sha256_cols::NUM_BIN_PUB { + public_trace + .binary_poly + .get(flat_col) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA binary public source columns", + got: public_trace.binary_poly.len(), + expected: flat_col + 1, + }) + } else { + let witness_col = flat_col - sha256_cols::NUM_BIN_PUB; + witness_trace + .binary_poly + .get(witness_col) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA binary witness source columns", + got: witness_trace.binary_poly.len(), + expected: witness_col + 1, + }) + } + } + + fn sha_int_col<'a>( + public_trace: &'a UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, + witness_trace: &'a UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, + flat_col: usize, + ) -> Result<&'a DenseMultilinearExtension, ProductionShaError> { + if flat_col < sha256_cols::NUM_INT_PUB { + public_trace + .int + .get(flat_col) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA int public source columns", + got: public_trace.int.len(), + expected: flat_col + 1, + }) + } else { + let witness_col = flat_col - sha256_cols::NUM_INT_PUB; + witness_trace + .int + .get(witness_col) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA int witness source columns", + got: witness_trace.int.len(), + expected: witness_col + 1, + }) + } + } + + fn project_binary_source( + col: &DenseMultilinearExtension>, + field_cfg: &::Config, + ) -> Result>, ProductionShaError> { + if col.evaluations.len() < SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "SHA binary source rows", + got: col.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + Ok(col + .evaluations + .iter() + .take(SHA_ROW_COUNT) + .map(|poly| { + poly.iter() + .take(SHA_WORD_BITS) + .map(|bit| { + if bit.into_inner() { + F::one_with_cfg(field_cfg) + } else { + F::zero_with_cfg(field_cfg) + } + }) + .collect() + }) + .collect()) + } + + fn project_int_source( + col: &DenseMultilinearExtension, + field_cfg: &::Config, + ) -> Result, ProductionShaError> { + if col.evaluations.len() < SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "SHA int source rows", + got: col.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + Ok(col + .evaluations + .iter() + .take(SHA_ROW_COUNT) + .map(|value| F::from_with_cfg(value, field_cfg)) + .collect()) + } + + fn word_scalar_at_two(bits: &[F], field_cfg: &::Config) -> F { + let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); + let mut power = F::one_with_cfg(field_cfg); + let mut value = F::zero_with_cfg(field_cfg); + for bit in bits { + value += bit.clone() * &power; + power *= &two; + } + value + } + + fn projected_public_from_sources( + pa_a: &[Vec], + pa_e: &[Vec], + message: &[Vec], + field_cfg: &::Config, + ) -> ShaPublicColumns { + let mut columns = + vec![vec![F::zero_with_cfg(field_cfg); SHA_ROW_COUNT]; ShaPublicCol::COUNT]; + for row in 0..SHA_ROW_COUNT { + columns[ShaPublicCol::K.index()][row] = production_sha_k_expected(row, field_cfg); + columns[ShaPublicCol::PAIn.index()][row] = word_scalar_at_two(&pa_a[row], field_cfg); + columns[ShaPublicCol::PEIn.index()][row] = word_scalar_at_two(&pa_e[row], field_cfg); + columns[ShaPublicCol::PAOut.index()][row] = word_scalar_at_two(&pa_a[row], field_cfg); + columns[ShaPublicCol::PEOut.index()][row] = word_scalar_at_two(&pa_e[row], field_cfg); + columns[ShaPublicCol::Message.index()][row] = + word_scalar_at_two(&message[row], field_cfg); + } + for selector in [ + ShaPublicCol::SInit, + ShaPublicCol::SMsg, + ShaPublicCol::SSched, + ShaPublicCol::SUpd, + ShaPublicCol::SFf, + ShaPublicCol::SOut, + ] { + for row in 0..SHA_ROW_COUNT { + columns[selector.index()][row] = + production_sha_selector_expected(selector, row, field_cfg); + } + } + ShaPublicColumns { columns } + } + + impl ProductionShaProjectionAdapter + for Sha256CompressionSliceUair + { + fn project_production_sha_witness( + _shape: &UairShape, + public_trace: &UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, + witness_trace: &UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, + field_cfg: &::Config, + ) -> Result< + ( + ProjectedShaTrace, + ProjectedShaPublic, + ProductionShaWitnessPolys, + ), + ProductionShaError, + > { + let word_sources = [ + sha256_cols::W_A, + sha256_cols::W_E, + sha256_cols::W_SIG0, + sha256_cols::W_SIG1, + sha256_cols::W_W, + sha256_cols::W_LSIG0, + sha256_cols::W_LSIG1, + sha256_cols::W_U_EF, + sha256_cols::W_U_NEG_E_G, + sha256_cols::W_MAJ, + sha256_cols::W_MU_PACKED, + sha256_cols::PA_OV_SIG0, + sha256_cols::PA_OV_SIG1, + sha256_cols::PA_OV_LSIG0, + sha256_cols::PA_OV_LSIG1, + sha256_cols::PA_R_CH2_COMP, + sha256_cols::PA_R_MAJ_COMP, + ]; + let int_sources = [ + sha256_cols::PA_C_C7, + sha256_cols::PA_C_C8, + sha256_cols::PA_C_C9, + sha256_cols::PA_C_FF_A, + sha256_cols::PA_C_FF_E, + ]; + + let bit_columns = word_sources + .iter() + .map(|&col| { + project_binary_source( + sha_binary_col(public_trace, witness_trace, col)?, + field_cfg, + ) + }) + .collect::, _>>()?; + let scalarized_words = scalarize_trace_words( + &ShaBitSliceColumns { + columns: bit_columns.clone(), + }, + &f(2), + field_cfg, + )?; + let pa_a = project_binary_source( + sha_binary_col(public_trace, witness_trace, sha256_cols::PA_A)?, + field_cfg, + )?; + let pa_e = project_binary_source( + sha_binary_col(public_trace, witness_trace, sha256_cols::PA_E)?, + field_cfg, + )?; + let message = project_binary_source( + sha_binary_col(public_trace, witness_trace, sha256_cols::PA_M)?, + field_cfg, + )?; + let public_columns = projected_public_from_sources(&pa_a, &pa_e, &message, field_cfg); + let int_columns = int_sources + .iter() + .map(|&col| { + project_int_source(sha_int_col(public_trace, witness_trace, col)?, field_cfg) + }) + .collect::, _>>()?; + + let trace = ProjectedShaTrace { + rows: SHA_ROW_COUNT, + bit_slices: ShaBitSliceColumns { + columns: bit_columns, + }, + scalarized_words, + int_columns: ShaIntColumns { + columns: int_columns.clone(), + }, + public_columns: public_columns.clone(), + }; + let public = ProjectedShaPublic { + columns: public_columns, + word_columns: Some(ShaPublicWordColumns { + columns: vec![pa_a.clone(), pa_e.clone(), pa_a, pa_e, message], + }), + }; + Ok(( + trace, + public, + ProductionShaWitnessPolys { + binary: word_sources + .iter() + .map(|&col| sha_binary_col(public_trace, witness_trace, col).cloned()) + .collect::, _>>()?, + arbitrary: Vec::new(), + int: int_sources + .iter() + .map(|&col| sha_int_col(public_trace, witness_trace, col).cloned()) + .collect::, _>>()?, + }, + )) + } + } + fn zero_trace_with_scalar_challenge(a: &F) -> ProjectedShaTrace { let field_cfg = cfg(); let zero = F::zero_with_cfg(&field_cfg); @@ -3030,6 +4219,7 @@ mod tests { ShaPublicCol::COUNT ], }, + word_columns: None, } } @@ -3109,6 +4299,56 @@ mod tests { } } + #[test] + fn prove_linear_ideal_fold_accepts_sha256_uair_witnesses() { + type U = Sha256CompressionSliceUair; + + let field_cfg = cfg(); + let initial_state = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, + 0x5be0cd19, + ]; + let message_blocks = [[0u32; 16], [1u32; 16]]; + let (witnesses, _final_state) = + synthesize_sha256_chain_witnesses::(initial_state, message_blocks) + .expect("SHA-256 UAIR witnesses synthesize"); + let num_vars = witnesses[0].trace.binary_poly[0].num_vars; + let shape = UairShape::::new(num_vars); + let pp = LinearIdealFoldProverParams::< + NoopPCSTypes, + U, + TestShaZincTypes, + F, + TEST_DEGREE_PLUS_ONE, + > { + pcs_params: PCSParams { + binary: (), + arbitrary: (), + int: (), + }, + prefix_vars: 1, + field_cfg, + _marker: PhantomData, + }; + let mut transcript = Blake3Transcript::new(); + + let result = prove_linear_ideal_fold::< + NoopPCSTypes, + U, + TestShaZincTypes, + F, + TEST_DEGREE_PLUS_ONE, + >(&pp, &shape, &witnesses, &mut transcript); + + let output = result.expect("new prove API should complete through SumFold"); + assert_eq!(output.fresh_instances.len(), witnesses.len()); + assert_eq!( + output.proof.ideal_family_polys.len(), + NUM_NONZERO_SHA_FAMILIES + ); + assert_eq!(output.proof.nifs.claimed_sums().len(), 1); + } + #[test] fn fresh_ideal_coefficients_are_bound_before_a() { let field_cfg = cfg(); @@ -3158,6 +4398,92 @@ mod tests { assert_ne!(sample_a(&packed), sample_a(&split)); } + #[test] + fn aggregate_ideal_claim_matches_old_per_instance_targets() { + let field_cfg = cfg(); + let mut ideal_polys = Vec::new(); + for instance_idx in 0..4 { + ideal_polys.push(std::array::from_fn(|slot| { + let family = production_sha_nonzero_families()[slot]; + match family { + ShaResidualFamily::R0BigSigmaA | ShaResidualFamily::R1BigSigmaE => { + let c = f((instance_idx * 10 + slot + 1) as u64); + let mut coeffs = vec![F::zero_with_cfg(&field_cfg); 33]; + coeffs[0] = -c.clone(); + coeffs[32] = c; + DynamicPolynomialF::new_trimmed(coeffs) + } + _ => { + let c = f((instance_idx * 10 + slot + 1) as u64); + DynamicPolynomialF::new_trimmed(vec![-f(2) * &c, c]) + } + } + })); + } + let beta = vec![f(3), f(5)]; + let a = f(7); + let lambda = f(11); + + let aggregate = beta_aggregate_sha_ideal_polys(&ideal_polys, &beta, &field_cfg).unwrap(); + let aggregate_claim = + evaluate_aggregate_sha_ideal_claim(&aggregate, &a, &lambda, &field_cfg).unwrap(); + let fresh_targets = + evaluate_fresh_targets_from_ideal_polys(&ideal_polys, &a, &lambda, &field_cfg).unwrap(); + let old_claim = eq_weighted_sum(&beta, &fresh_targets, &field_cfg).unwrap(); + + assert_eq!(aggregate_claim, old_claim); + } + + #[test] + fn aggregate_ideal_membership_rejects_wrong_family_polynomial() { + let field_cfg = cfg(); + let mut aggregate = std::array::from_fn(|slot| { + let family = production_sha_nonzero_families()[slot]; + match family { + ShaResidualFamily::R0BigSigmaA | ShaResidualFamily::R1BigSigmaE => { + let mut coeffs = vec![F::zero_with_cfg(&field_cfg); 33]; + coeffs[0] = -f(3); + coeffs[32] = f(3); + DynamicPolynomialF::new_trimmed(coeffs) + } + _ => DynamicPolynomialF::new_trimmed(vec![-f(10), f(5)]), + } + }); + check_aggregate_sha_ideal_membership(&aggregate, &field_cfg).unwrap(); + + aggregate[2] = DynamicPolynomialF::new_trimmed(vec![f(1)]); + assert!(matches!( + check_aggregate_sha_ideal_membership(&aggregate, &field_cfg), + Err(ProductionShaError::ShaProjection( + ShaProjectionError::IdealMembership + )) + )); + } + + #[test] + fn aggregate_ideal_absorption_precedes_scalarization_challenges() { + let field_cfg = cfg(); + let aggregate = std::array::from_fn(|slot| { + DynamicPolynomialF::new_trimmed(vec![f(slot as u64 + 1), f(slot as u64 + 2)]) + }); + let mut tampered = aggregate.clone(); + tampered[0].coeffs[0] += f(1); + + let sample_a = |values: &[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]| { + let mut transcript = Blake3Transcript::new(); + transcript.absorb_slice(b"fresh-commitments-and-public-inputs"); + let _r_ic = sample_pre_ideal_challenge::(&mut transcript, &field_cfg); + let _beta = + sample_instance_batch_challenge::(&mut transcript, 4, &field_cfg).unwrap(); + absorb_aggregate_sha_ideal_polys(&mut transcript, values); + let (a, _, _, _) = + sample_post_aggregate_ideal_challenges::(&mut transcript, &field_cfg); + a + }; + + assert_ne!(sample_a(&aggregate), sample_a(&tampered)); + } + #[test] fn production_public_validation_requires_fixed_selectors_and_k() { let field_cfg = cfg(); From 87acf77874462e1a4ef3ec0bb3d667ec4215d30d Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 13:34:24 -0700 Subject: [PATCH 22/49] Implement ProjectionFold verifier harness --- .../sha-uair-doc/uair-object-model.md | 508 +++++--- piop/src/neutron_nova/mod.rs | 2 +- piop/src/neutron_nova/projection_sha.rs | 49 +- protocol/src/pcs.rs | 61 +- protocol/src/production_sha.rs | 1090 +++++++++-------- protocol/src/verifier.rs | 3 + zip-plus/src/pcs/generic.rs | 37 +- zip-plus/src/pcs/hyrax.rs | 37 +- 8 files changed, 1025 insertions(+), 762 deletions(-) diff --git a/documentation/sha-uair-doc/uair-object-model.md b/documentation/sha-uair-doc/uair-object-model.md index 8fcf3d25..32ec4120 100644 --- a/documentation/sha-uair-doc/uair-object-model.md +++ b/documentation/sha-uair-doc/uair-object-model.md @@ -105,20 +105,41 @@ pub struct UairInstance<'a, PolyCoeff: Clone, Int: Clone, Commitments, const D: The folded objects should mirror the fresh split: ```rust -pub struct FoldedUairWitness { - // prover-private folded evaluations, residuals, and randomness +pub struct FoldedUairWitness { + pub trace: FoldedUairTrace, + pub opening_witness: OpeningWitness, } -pub struct FoldedUairInstance { +pub struct FoldedUairTrace { + pub binary_poly: Vec>>, + pub arbitrary_poly: Vec>>, + pub int: Vec>, +} + +pub struct FoldedUairInstance { pub commitments: Commitments, - pub public_evals: Vec, + pub public: Public, pub u: F, } ``` -The exact fields of the folded objects should be chosen by the folding protocol, -but the boundary remains the same: witness is private, instance is -verifier-visible. +`FoldedUairTrace` intentionally keeps the same top-level column families as +`UairTrace`, but its cell types are proof-field objects after projection and +instance folding. Polynomial-valued sources become MLEs whose row values are +univariate proof-field polynomials. Scalar or integer sources become scalar MLEs +over the proof field. + +`opening_witness` is prover-only data needed by the PCS to open the folded +commitments, such as commitment randomness or backend-specific prover state. It +is not part of the proof object. Residual and ideal-polynomial caches should +remain prover working state unless a later phase genuinely needs to carry them. + +The exact field types can be specialized by the folding protocol. For example, +the SHA-256 production path may use a SHA-specific projected trace with +`bit_slices`, `scalarized_words`, `int_columns`, and `public_columns`, while the +generic UAIR object should preserve the `binary_poly`, `arbitrary_poly`, and +`int` families. The boundary remains the same: witness is prover-private, +instance is verifier-visible. ## LinearIdealFold Proof Objects @@ -128,34 +149,190 @@ verifier messages and claimed evaluations. It should not contain prover-side caches such as PCS prover data, and it should not contain NeutronNova-specific objects such as `comm_E` for a committed power-vector witness. -The family association for ideal polynomials is part of the proof contract. A -bare `Vec>` is not enough unless `UairShape` defines the -exact canonical order. Prefer an explicit family-tagged form: +`ProjectionFold Concise` is the source of truth for the production protocol: +the verifier algorithm, Fiat-Shamir ordering, concrete SHA-256 ideal families, +degree bounds, and equations. This file records the UAIR object boundaries and +Rust-facing shape of the proof. If a protocol equation is duplicated here, it is +included only to make the object model unambiguous. + +The implementation should reuse the generic proof objects exercised by +`protocol/benches/e2e.rs`. The baseline proof shape in `protocol/src/lib.rs` is: ```rust -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct IdealFamilyId(pub u16); +pub struct Proof { + pub commitments: Commitments, + pub zip: Vec, + pub ideal_check: IdealCheckProof, + pub resolver: CombinedPolyResolverProof, + pub combined_sumcheck: MultiDegreeSumcheckProof, + pub multipoint_eval: MultipointEvalProof, + pub witness_lifted_evals: Vec>, + pub lookup_proof: Option>, +} +``` -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct IdealPolySlot(pub u16); +Here `IdealCheckProof`, `CombinedPolyResolverProof`, +`MultiDegreeSumcheckProof`, `MultipointEvalProof`, `DynamicPolynomialF`, and +`BatchedLookupProof` are existing protocol/PIOP types exposed by the baseline +e2e proof shape. The production object model should reuse the active proof +components directly. In particular, do not add a separate family-tag proof layer +such as `IdealFamilyId`, `IdealPolySlot`, `IdealFamilyPolys`, or +`IdealFamilyPoly` just to carry the batched ideal polynomials. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct IdealFamilyPolys { - pub family: IdealFamilyId, - pub polys: Vec>, +`lookup_proof` is currently a forward-compatible stub in the e2e proof shape. +The prover sets it to `None`, serialization skips it, and the verifier only +carries it through. Production SHA currently has empty `lookup_specs`, so the +production SHA wrapper omits this field. + +The current e2e Rust type calls the serialized PCS opening transcript `zip` +because the first backend was Zip+. Semantically this field is the PCS opening +proof. The production object model should not expose this as Zip-specific state. + +Production folding may need a thin wrapper around this shape because it also has +an instance-axis SumFold/NIFS proof and multiple fresh commitments. Those extra +fields should reuse existing types: + +```rust +pub struct ProductionLinearIdealFoldProof +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub instance_commitments: Vec>, + pub ideal_check: IdealCheckProof, + pub sumfold_proof: MultiDegreeSumcheckProof, + pub resolver: CombinedPolyResolverProof, + pub combined_sumcheck: MultiDegreeSumcheckProof, + pub multipoint_eval: MultipointEvalProof, + pub witness_lifted_evals: Vec>, + pub opening_proof: PCSOpeningProof, } +``` -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct IdealFamilyPoly { - pub slot: IdealPolySlot, - pub poly: DynamicPolynomialF, +This is intentionally a field-level reuse of the e2e proof object, not a new +PIOP object model. The production wrapper belongs in `protocol`, if needed; it +should not introduce new generic proof structs in `piop/src/neutron_nova`. + +The PCS backend remains generic through `ZincPCSTypes` and the component `PCS` +implementations in `zip-plus/src/pcs/generic.rs`. Do not introduce a separate +SHA-specific PCS trait for the proof object. The proof object only needs the +associated commitment and opening-proof types. Production code that actually +folds commitments should put any homomorphic-folding requirements directly on +the component PCS types at the prover/verifier function boundary. + +```rust +pub struct PCSOpeningProof +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub binary: <

>::BinaryPCS + as PCS, D>>::OpeningProof, + pub arbitrary: <

>::ArbitraryPCS + as PCS, D>>::OpeningProof, + pub int: <

>::IntPCS + as PCS>::OpeningProof, +} +``` + +That requires the PCS trait to expose the opening proof as an associated type: + +```rust +pub trait PCS: Clone + Debug + Send + Sync +where + F: PrimeField, + Eval: Clone + Debug + Send + Sync, +{ + type CommitmentKey: Clone + Debug + Send + Sync; + type VerifierKey: Clone + Debug + Send + Sync; + type Commitment: Clone + Debug + Send + Sync; + type ProverData: Clone + Debug + Send + Sync; + type OpeningProof: Clone + Debug + Send + Sync + Default; + + fn prove_open( + transcript: &mut PcsProverTranscript, + ck: &Self::CommitmentKey, + polys: &[DenseMultilinearExtension], + point: &[F], + prover_data: &Self::ProverData, + field_cfg: &F::Config, + ) -> Result; + + fn verify_open( + transcript: &mut PcsVerifierTranscript, + vk: &Self::VerifierKey, + commitment: &Self::Commitment, + point: &[F], + lifted_evals: &[DynamicPolynomialF], + opening_proof: &Self::OpeningProof, + field_cfg: &F::Config, + ) -> Result<(), ZipError>; +} +``` + +The current trait writes and reads opening data through PCS transcripts and +returns `Result<(), ZipError>`. That should be treated as the current adapter +shape, not the production proof-object shape. Zip+ can set +`type OpeningProof = Vec` while Hyrax or any future PCS can use its native +typed proof. + +The aggregate ideal component from `ProjectionFold Concise` should be carried by +the existing ideal-check proof: + +```rust +pub struct IdealCheckProof { + pub combined_mle_values: Vec>, } ``` -The verifier must check that these families and slots are in the canonical -shape-defined order, with no missing or duplicate entries. For SHA-256, this -corresponds to the current `production_sha_nonzero_families()` order, but the -generic API should make that association shape-level data. +The family/order information is setup data, not a new proof object. For +production SHA-256, `verify_setup` fixes the canonical mapping from entries of +`combined_mle_values` to the nonzero ideal families: + + ℱ_≠0 = {R₀, R₁, R₄, R₅, R₆, R₉, R₁₀} + +The compact production interpretation is: + + ideal_check.combined_mle_values[f] = Ē_f^β(X) + for f ∈ ℱ_≠0 in setup-defined order + +If the generic e2e verifier path is used unchanged, the vector length/order must +match `U::verify_as_subprotocol` and `count_constraints::()`. If the +production verifier uses the seven-family compact form, that compact mapping is +part of `verify_setup`; the carrier is still `IdealCheckProof`. + +The honest aggregate polynomial is: + + Ē_f^β(X) + = ∑_{b ∈ {0,1}^ℓ} eq(β,b) + ∑_{z ∈ H_row} eq(r_ic,z) · C_f(z,X;w_b,y_b) + +In the production transcript, r_ic and β are sampled after binding VS and the +fresh instances, and before E_agg is read. `ProjectionFold Concise` owns the +full Fiat-Shamir sequence. + +Thus the submitted ideal component is already batched over both verifier-visible +axes: + + instance axis b via eq(β,b) + row axis z via eq(r_ic,z) + +The verifier still does not trust these aggregate polynomials blindly. It checks +the shape-level degree bound and ideal membership for each family: + + deg_X Ē_f^β(X) < δ_f + Ē_f^β(X) ∈ I_f + +After accepting and absorbing the aggregate polynomials, the verifier samples +the scalarization and family-batching challenges and computes the initial +NIFS/SumFold claim: + + C₀ = ∑_{f ∈ ℱ_≠0} λ^f · Ē_f^β(a) + +The later NIFS, row sumcheck, terminal reconstruction, multipoint reduction, and +PCS opening bind this same scalar to the folded commitments and public trace. The folded verifier-visible instance is the result of folding fresh instances. It contains the folded target claim, folded commitments, and any folded public @@ -172,7 +349,8 @@ pub struct FoldedLinearIdealInstance { The folded witness is prover-private. Its concrete representation can be an owned folded trace, folded source MLEs, or another protocol-specific witness -bundle: +bundle. In the generic UAIR model, this is normally a +`FoldedUairWitness`: ```rust #[derive(Clone, Debug, PartialEq, Eq)] @@ -181,176 +359,77 @@ pub struct FoldedLinearIdealWitness { } ``` -The proof object is: +Here `OpeningWitness` is the PCS/backend-specific prover-only state needed to +open the folded commitments. -```rust -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct LinearIdealFoldProof { - pub ideal_family_polys: Vec>, - pub nifs: MultiDegreeSumcheckProof, - pub folded_claim_sumcheck: MultiDegreeSumcheckProof, - pub terminal_evals: TerminalEvals, - pub multipoint: MultipointEvalProof, - pub opening_evals: OpeningEvals, - pub pcs_opening_bytes: Vec, -} -``` +The ideal-check proof is the production `E_agg` component from +`ProjectionFold Concise`, represented with `IdealCheckProof`. It contains the +seven SHA-256 aggregate ideal polynomials for the nonzero families, or the +shape-defined analogue for another UAIR. These polynomials are verifier-visible +and must be absorbed before sampling a, λ, ρ, ξ. -`ideal_family_polys` contains the nonzero ideal-witness polynomials, grouped by -ideal family. These prove ideal membership and define the scalar targets used by -the instance-axis folding claim. - -For fresh instances \(b \in \{0,\dots,B-1\}\), ideal families -\(f \in \mathcal F\), and family slots \(k\): - -$$ -E_{b,f,k}(X) \in I_f -$$ - -The beta-aggregated family polynomial is: - -$$ -\bar E^{\beta}_{f,k}(X) -= -\sum_{b=0}^{B-1} \operatorname{eq}(\beta,b)\,E_{b,f,k}(X) -$$ - -Because the ideals are linear: - -$$ -E_{b,f,k}(X) \in I_f -\quad\Longrightarrow\quad -\bar E^{\beta}_{f,k}(X) \in I_f -$$ - -The fresh scalar target for instance \(b\) is: - -$$ -T_b -= -\sum_{f \in \mathcal F} -\lambda_f -\sum_k E_{b,f,k}(a) -$$ - -The initial SumFold claim is: - -$$ -C_0 -= -\sum_{b=0}^{B-1} -\operatorname{eq}(\beta,b)\,T_b -$$ - -`nifs` is the instance-axis `MultiDegreeSumcheckProof`. It proves the SumFold -transition from \(C_0\) to the folded target. The verifier derives \(r_b\), -folding weights \(\theta_b\), and \(T'\): - -$$ -\theta_b = \operatorname{eq}(r_b,b) -$$ - -$$ -T' -= -\frac{c_{\mathrm{SF}}}{\operatorname{eq}(\beta,r_b)} -$$ - -`folded_claim_sumcheck` proves that the folded target is the row-domain sum of -the folded residue expression: - -$$ -T' -= -\sum_{x \in \{0,1\}^{d}} -\operatorname{eq}(r_{\mathrm{ic}},x) -\cdot -\Phi_{\mathrm{folded}}(x) -$$ - -This sumcheck reduces the folded claim to a terminal point \(r_\star\). - -`terminal_evals` contains the claimed evaluations needed to reconstruct -\(\Phi_{\mathrm{folded}}(r_\star)\). It should include source identifiers, -shift identifiers, scalarized values, and any coefficient-level values needed by -the shape-specific expression. For SHA-256 this corresponds to the current -endpoint evaluations of folded word and integer sources. - -```rust -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct SourceId(pub u16); +`sumfold_proof` is the instance-axis `MultiDegreeSumcheckProof`. It proves the +SumFold transition from the verifier-computed C₀ to the folded target. The +verifier derives r_b, folding weights θ_b, and T′: -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct ShiftId(pub u16); + θ_b = eq(r_b,b) -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TerminalEvals { - pub polynomial_sources: Vec>, - pub scalar_sources: Vec>, -} + T′ = c_SF / eq(β,r_b) -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TerminalPolynomialEval { - pub source: SourceId, - pub shift: ShiftId, - pub scalarized: F, - pub coeffs: Vec, -} +`resolver` and `combined_sumcheck` are the same terminal-reconstruction objects +used by e2e step 4. `combined_sumcheck` reduces the folded row claim to r⋆, and +`resolver` carries the terminal evaluations needed to close the combined +polynomial resolver: -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TerminalScalarEval { - pub source: SourceId, - pub shift: ShiftId, - pub value: F, +```rust +pub struct CombinedPolyResolverProof { + pub up_evals: Vec, + pub down_evals: Vec, + pub bit_slice_evals: Vec, + pub bit_op_down_evals: Vec, + pub shifted_bit_slice_evals: Vec, } ``` -The verifier uses `terminal_evals` to check: +Together they prove that the folded target is the row-domain sum of the folded +residue expression: -$$ -\mathrm{terminal} -= -\operatorname{eq}(r_{\mathrm{ic}},r_\star) -\cdot -\Phi_{\mathrm{folded}}(r_\star) -$$ + T′ = ∑_{x ∈ {0,1}^d} eq(r_ic,x) · Φ_folded(x) -`multipoint` reduces all terminal evaluation claims at \(r_\star\) and shifted -points into one batched opening claim at a verifier-derived point \(r_0\): +This sumcheck reduces the folded claim to a terminal point r⋆. -$$ -\{p_i(s_i(r_\star)) = v_i\}_i -\quad\Longrightarrow\quad -P(r_0)=v_0 -$$ +The verifier uses `resolver` to check: -`opening_evals` are the claimed folded committed-source evaluations at \(r_0\). -`pcs_opening_bytes` is the serialized PCS proof that those evaluations match -the folded commitments. + terminal = eq(r_ic,r⋆) · Φ_folded(r⋆) -```rust -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct OpeningEvals { - pub polynomial_sources: Vec>, - pub scalar_sources: Vec, -} -``` +`multipoint_eval` is the existing e2e multipoint proof. It reduces all terminal +evaluation claims at r⋆ and shifted points into one batched opening claim at a +verifier-derived point r₀: + + { p_i(s_i(r⋆)) = v_i }_i ⇒ P(r₀) = v₀ + +`witness_lifted_evals` are the existing e2e opening-evaluation carrier. They +are witness-only lifted MLE evaluations at r₀ in F_q[X], ordered as +`[wit_bin..., wit_arb..., wit_int...]`. The verifier recomputes public lifted +evals from the public trace, interleaves public and witness lifted evals, +derives scalar `open_evals` by ψ_a, derives bit-op virtual opens locally, and +checks the `multipoint_eval` subclaim. The serialized PCS opening proof is +`opening_proof`. The proof chain is: ```text -ideal_family_polys - -> derive C0 and prove ideal membership -nifs - -> fold C0 into T' -folded_claim_sumcheck - -> reduce T' to terminal point r_star -terminal_evals - -> reconstruct the terminal folded expression -multipoint - -> reduce many endpoint claims to one opening point r_0 -opening_evals + pcs_opening_bytes - -> prove consistency with folded commitments +ideal_check + → check ideal membership and derive C₀ +sumfold_proof + → fold C₀ into T′ +resolver + combined_sumcheck + → reduce T′ to terminal point r⋆ + → reconstruct the terminal folded expression +multipoint_eval + → reduce many endpoint claims to one opening point r₀ +witness_lifted_evals + opening_proof + → prove consistency with folded commitments ``` ## SHA-256 Domain Objects @@ -487,38 +566,85 @@ pub fn prove_linear_ideal_fold( ) -> Result< LinearIdealFoldProveOutput< UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, - FoldedLinearIdealInstance, FoldedPublicEvals>, - FoldedLinearIdealWitness>, - LinearIdealFoldProof, + FoldedLinearIdealInstance, ProjectedShaPublic>, + FoldedLinearIdealWitness>, + ProductionLinearIdealFoldProof, >, LinearIdealFoldError, > where U: Uair, Zt: ZincTypes, - F: PrimeField; + F: PrimeField, + P: ZincPCSTypes; +``` + +The SHA production folded witness keeps the structured folded SHA trace and the +folded PCS opening witness: + +```rust +pub struct ProductionShaFoldedWitness +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub trace: ProjectedShaTrace, + pub opening_witness: PCSProverData, +} +``` + +The folded verifier-visible public value is `ProjectedShaPublic`, not a flat +field vector, because the verifier needs structured SHA public columns for +terminal reconstruction and multipoint checks. + +The production verifier interface in `ProjectionFold Concise` is the acceptance +predicate: + + verify(VS, {Inst_b}_{b ∈ {0,1}^ℓ}, π) → {true, false} + +When VS is fixed by context, the shorthand is: + + verify({Inst_b}_{b ∈ {0,1}^ℓ}, π) → {true, false} + +The Rust-facing API uses the same two-step shape. Setup verification checks and +stores static material: + +```rust +pub fn setup_verify_linear_ideal_fold( + params: LinearIdealFoldVerifierParams, + shape: UairShape, +) -> Result, LinearIdealFoldError> +where + U: Uair + ProductionShaProjectionAdapter, + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes; ``` -The verifier receives the fresh instances and proof. It derives the same folded -instance, but it never receives the folded witness: +The verifier then receives `VS`, fresh instances, and the proof. It derives the +same folded instance, but it never receives the folded witness: ```rust pub fn verify_linear_ideal_fold( - vp: &LinearIdealFoldVerifierParams, - shape: &UairShape, + vs: &VerifiedLinearIdealFoldSetup, instances: &[UairInstance<'_, Zt::Int, Zt::Int, PCSCommitments, D>], - proof: &LinearIdealFoldProof, + proof: &ProductionLinearIdealFoldProof, transcript: &mut impl Transcript, ) -> Result< - FoldedLinearIdealInstance, FoldedPublicEvals>, + FoldedLinearIdealInstance, ProjectedShaPublic>, LinearIdealFoldError, > where - U: Uair, + U: Uair + ProductionShaProjectionAdapter, Zt: ZincTypes, - F: PrimeField; + F: PrimeField, + P: ZincPCSTypes; ``` +Returning `Ok(folded_instance)` means the production verifier accepts. Returning +`Err(_)` means rejection. + ```rust Sha256ChainPublicInput -> build public UairTrace diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index 9588471c..6f9bca78 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -36,7 +36,7 @@ pub use projection_sha::{ build_production_sha_sumfold_group, build_production_sha_sumfold_group_owned, build_production_sha_sumfold_group_with_linear_cache, build_sha_ideal_values_at_point, build_sha_linear_residual_coeff_cache, check_fresh_sha_ideal_cache, check_sha_ideal_values, - evaluate_fresh_sha_targets, expression_folded_row_sum, finalize_sha_sumfold, + derive_sha_instance_fold_claim, evaluate_fresh_sha_targets, expression_folded_row_sum, fold_projected_sha_traces, folded_row_integrand_sum, folded_row_integrand_values, production_sha_booleanity_sources, production_sha_nonzero_families, production_sha_nonzero_ideals, reconstruct_virtual_ch_maj_at_row, scalarize_trace_words, diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 47a309fc..deb521f0 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -426,7 +426,7 @@ pub struct ShaSumFoldOutput { r_b: Vec, c_sf: F, t_prime: F, - theta: Vec, + eq_instance_weights: Vec, } impl ShaSumFoldOutput { @@ -442,8 +442,8 @@ impl ShaSumFoldOutput { &self.t_prime } - pub fn theta(&self) -> &[F] { - &self.theta + pub fn eq_instance_weights(&self) -> &[F] { + &self.eq_instance_weights } } @@ -792,7 +792,7 @@ where Ok(()) } -pub fn finalize_sha_sumfold( +pub fn derive_sha_instance_fold_claim( beta: &[F], r_b: Vec, c_sf: F, @@ -827,14 +827,14 @@ where return Err(ShaProjectionError::ZeroSumFoldDenominator); } - let theta = build_eq_x_r_vec(&r_b, field_cfg)?; - debug_assert_eq!(theta.len(), instance_count); + let eq_instance_weights = build_eq_x_r_vec(&r_b, field_cfg)?; + debug_assert_eq!(eq_instance_weights.len(), instance_count); let t_prime = c_sf.clone() / d; Ok(ShaSumFoldOutput { r_b, c_sf, t_prime, - theta, + eq_instance_weights, }) } @@ -853,9 +853,9 @@ where expected: traces.len(), }); } - if sumfold.theta.len() != traces.len() { + if sumfold.eq_instance_weights.len() != traces.len() { return Err(ShaProjectionError::FoldingWeightCount { - got: sumfold.theta.len(), + got: sumfold.eq_instance_weights.len(), expected: traces.len(), }); } @@ -871,28 +871,28 @@ where bit_slices: ShaBitSliceColumns { columns: fold_3d( traces.iter().map(|trace| &trace.bit_slices.columns), - &sumfold.theta, + &sumfold.eq_instance_weights, field_cfg, )?, }, scalarized_words: ShaScalarizedRows { words: fold_2d( traces.iter().map(|trace| &trace.scalarized_words.words), - &sumfold.theta, + &sumfold.eq_instance_weights, field_cfg, )?, }, int_columns: ShaIntColumns { columns: fold_2d( traces.iter().map(|trace| &trace.int_columns.columns), - &sumfold.theta, + &sumfold.eq_instance_weights, field_cfg, )?, }, public_columns: ShaPublicColumns { columns: fold_2d( traces.iter().map(|trace| &trace.public_columns.columns), - &sumfold.theta, + &sumfold.eq_instance_weights, field_cfg, )?, }, @@ -901,13 +901,13 @@ where columns: ShaPublicColumns { columns: fold_2d( publics.iter().map(|public| &public.columns.columns), - &sumfold.theta, + &sumfold.eq_instance_weights, field_cfg, )?, }, word_columns: fold_optional_3d( publics.iter().map(|public| public.word_columns.as_ref()), - &sumfold.theta, + &sumfold.eq_instance_weights, field_cfg, )? .map(|columns| ShaPublicWordColumns { columns }), @@ -4014,19 +4014,23 @@ mod tests { let beta = vec![f(2), f(3)]; let r_b = vec![f(5), f(7)]; let c_sf = f(11); - let out = finalize_sha_sumfold(&beta, r_b.clone(), c_sf.clone(), 4, &cfg).unwrap(); + let out = + derive_sha_instance_fold_claim(&beta, r_b.clone(), c_sf.clone(), 4, &cfg).unwrap(); let d = eq_eval(&beta, &r_b, F::one_with_cfg(&cfg)).unwrap(); assert_eq!(out.t_prime(), &(c_sf / d)); - assert_eq!(out.theta(), build_eq_x_r_vec(&r_b, &cfg).unwrap()); + assert_eq!( + out.eq_instance_weights(), + build_eq_x_r_vec(&r_b, &cfg).unwrap() + ); } #[test] - fn folding_uses_sumfold_theta() { + fn folding_uses_eq_instance_weights() { let cfg = test_config(); let beta = vec![f(2)]; let r_b = vec![f(3)]; - let out = finalize_sha_sumfold(&beta, r_b, f(9), 2, &cfg).unwrap(); + let out = derive_sha_instance_fold_claim(&beta, r_b, f(9), 2, &cfg).unwrap(); let mut left = zero_trace(); let mut right = zero_trace(); @@ -4042,8 +4046,8 @@ mod tests { &cfg, ) .unwrap(); - let expected = out.theta()[0].clone() * &left.bit_slices.columns[0][0][0] - + out.theta()[1].clone() * &right.bit_slices.columns[0][0][0]; + let expected = out.eq_instance_weights()[0].clone() * &left.bit_slices.columns[0][0][0] + + out.eq_instance_weights()[1].clone() * &right.bit_slices.columns[0][0][0]; assert_eq!(folded.trace.bit_slices.columns[0][0][0], expected); } @@ -4182,7 +4186,8 @@ mod tests { .unwrap(); let (_proof, r_b, expected) = prove_and_verify_sumfold(sumfold_group, ell); let sumfold = - finalize_sha_sumfold(&beta, r_b, expected[0].clone(), traces.len(), &cfg).unwrap(); + derive_sha_instance_fold_claim(&beta, r_b, expected[0].clone(), traces.len(), &cfg) + .unwrap(); let (folded_witness, folded_public) = fold_projected_sha_traces(&traces, &publics, &sumfold, &cfg).unwrap(); diff --git a/protocol/src/pcs.rs b/protocol/src/pcs.rs index 4f0f87b9..0aa53794 100644 --- a/protocol/src/pcs.rs +++ b/protocol/src/pcs.rs @@ -4,7 +4,7 @@ use ark_ec::AffineRepr; use crypto_primitives::PrimeField; use zinc_poly::univariate::{binary::BinaryPoly, dense::DensePolynomial}; use zip_plus::pcs::{ - generic::{FoldablePCS, PCS, ZipPlusPCS}, + generic::{PCS, ZipPlusPCS}, hyrax::{BinaryLanes, DensePolyScalarLanes, HyraxPCS, IntScalarLane}, structs::ZipPlusCommitment, }; @@ -23,21 +23,6 @@ where type IntPCS: PCS; } -/// PCS bundle allowed by production SHA ProjectionFold. -/// -/// Production ProjectionFold folds instance commitments after SumFold, so all -/// committed witness domains must be homomorphic. `AllHyraxPCSTypes` satisfies -/// this today; Zip+/Merkle-based bundles intentionally do not. -pub trait ProductionShaPCS: ZincPCSTypes -where - Zt: ZincTypes, - F: PrimeField, - Self::BinaryPCS: FoldablePCS, D>, - Self::ArbitraryPCS: FoldablePCS, D>, - Self::IntPCS: FoldablePCS, -{ -} - #[derive(Clone, Debug)] pub struct AllZipPCSTypes; @@ -92,18 +77,6 @@ where type IntPCS = HyraxPCS; } -impl ProductionShaPCS for AllHyraxPCSTypes -where - Zt: ZincTypes, - F: PrimeField, - C: AffineRepr, - HyraxPCS: PCS, D> + FoldablePCS, D>, - HyraxPCS: - PCS, D> + FoldablePCS, D>, - HyraxPCS: PCS + FoldablePCS, -{ -} - #[derive(Clone, Debug)] pub struct PCSParams where @@ -168,3 +141,35 @@ where >>::ProverData, pub int: <

>::IntPCS as PCS>::ProverData, } + +#[derive(Clone, Debug)] +pub struct PCSOpeningProof +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub binary: + <

>::BinaryPCS as PCS, D>>::OpeningProof, + pub arbitrary: <

>::ArbitraryPCS as PCS< + F, + DensePolynomial, + D, + >>::OpeningProof, + pub int: <

>::IntPCS as PCS>::OpeningProof, +} + +impl Default for PCSOpeningProof +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + fn default() -> Self { + Self { + binary: Default::default(), + arbitrary: Default::default(), + int: Default::default(), + } + } +} diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 1a24f9aa..138a12ef 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -10,8 +10,7 @@ use crate::{ ZincTypes, multipoint_reduction::{prove_multipoint_reduction, verify_multipoint_reduction}, pcs::{ - AllHyraxPCSTypes, PCSCommitments, PCSParams, PCSProverData, PCSVerifierParams, - ProductionShaPCS, ZincPCSTypes, + PCSCommitments, PCSOpeningProof, PCSParams, PCSProverData, PCSVerifierParams, ZincPCSTypes, }, }; use ark_ec::AffineRepr; @@ -19,6 +18,8 @@ use crypto_primitives::{FromPrimitiveWithConfig, PrimeField}; use num_traits::{ConstZero, Zero}; use thiserror::Error; use zinc_piop::{ + combined_poly_resolver::Proof as CombinedPolyResolverProof, + ideal_check::Proof as IdealCheckProof, multipoint_eval::{ MultipointEval, MultipointEvalError, Proof as MultipointEvalProof, Subclaim as MultipointSubclaim, @@ -31,8 +32,8 @@ use zinc_piop::{ ShaResidualFamily, ShaSumFoldOutput, ShaWordCol, build_dense_sha_sumfold_group, build_expression_folded_row_sumcheck_group, build_folded_row_sumcheck_group, build_production_sha_sumfold_group_with_linear_cache, - build_sha_linear_residual_coeff_cache, finalize_sha_sumfold, fold_projected_sha_traces, - folded_row_integrand_sum, production_sha_booleanity_sources, + build_sha_linear_residual_coeff_cache, derive_sha_instance_fold_claim, + fold_projected_sha_traces, folded_row_integrand_sum, production_sha_booleanity_sources, production_sha_nonzero_families, sha_int_at_point, sha_public_at_point, sha_scalarized_word_at_point, sha_word_bits_at_point, verify_folded_row_sumcheck_claim, verify_fresh_sha_ideal_polys, @@ -63,24 +64,26 @@ use zip_plus::{ ZipError, pcs::{ generic::{FoldablePCS, PCS}, - hyrax::{ - BinaryLanes, DensePolyScalarLanes, HyraxCommitment, HyraxCommitmentKey, - HyraxFieldBridge, HyraxPCS, HyraxProverData, HyraxVerifierKey, IntScalarLane, - }, + hyrax::HyraxFieldBridge, }, - pcs_transcript::{PcsProverTranscript, PcsVerifierTranscript}, + pcs_transcript::PcsVerifierTranscript, }; -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ProductionShaProof { - pub instance_commitments: Vec, - pub fresh_ideal_polys: Vec<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]>, +#[derive(Clone, Debug)] +pub struct ProductionLinearIdealFoldProof +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub instance_commitments: Vec>, + pub ideal_check: IdealCheckProof, pub sumfold_proof: MultiDegreeSumcheckProof, - pub folded_row_sumcheck: MultiDegreeSumcheckProof, - pub endpoint_evals: ShaEndpointEvals, + pub resolver: CombinedPolyResolverProof, + pub combined_sumcheck: MultiDegreeSumcheckProof, pub multipoint_eval: MultipointEvalProof, - pub folded_lifted_evals: Vec>, - pub pcs_opening_bytes: Vec, + pub witness_lifted_evals: Vec>, + pub opening_proof: PCSOpeningProof, } #[derive(Clone, Debug)] @@ -109,6 +112,14 @@ where Zt: ZincTypes, F: PrimeField, { + fn project_production_sha_public( + shape: &UairShape, + public_trace: &UairTrace<'_, Zt::Int, Zt::Int, D>, + field_cfg: &F::Config, + ) -> Result, ProductionShaError> + where + Self: Sized; + fn project_production_sha_witness( shape: &UairShape, public_trace: &UairTrace<'_, Zt::Int, Zt::Int, D>, @@ -261,7 +272,49 @@ where } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] +pub struct LinearIdealFoldVerifierParams +where + U: Uair, + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub pcs_params: PCSVerifierParams, + pub field_cfg: F::Config, + _marker: PhantomData, +} + +impl LinearIdealFoldVerifierParams +where + U: Uair, + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub fn new(pcs_params: PCSVerifierParams, field_cfg: F::Config) -> Self { + Self { + pcs_params, + field_cfg, + _marker: PhantomData, + } + } +} + +#[derive(Clone, Debug)] +pub struct VerifiedLinearIdealFoldSetup +where + U: Uair, + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub pcs_params: PCSVerifierParams, + pub shape: UairShape, + pub field_cfg: F::Config, +} + +#[derive(Clone, Debug)] pub struct LinearIdealFoldProveOutput { pub fresh_instances: Vec, pub folded_instance: FoldedInstance, @@ -276,76 +329,26 @@ pub struct FoldedLinearIdealInstance { pub public: Public, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub struct FoldedLinearIdealWitness { pub witness: Witness, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct FoldedPublicEvals { - pub values: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct FoldedUairTrace { - pub values: Vec, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct IdealFamilyId(pub u16); - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct IdealPolySlot(pub u16); - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct IdealFamilyPolys { - pub family: IdealFamilyId, - pub polys: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct IdealFamilyPoly { - pub slot: IdealPolySlot, - pub poly: DynamicPolynomialF, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct SourceId(pub u16); - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct ShiftId(pub u16); - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TerminalEvals { - pub polynomial_sources: Vec>, - pub scalar_sources: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TerminalPolynomialEval { - pub source: SourceId, - pub shift: ShiftId, - pub scalarized: F, - pub coeffs: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TerminalScalarEval { - pub source: SourceId, - pub shift: ShiftId, - pub value: F, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct OpeningEvals { - pub polynomial_sources: Vec>, - pub scalar_sources: Vec, +#[derive(Clone, Debug)] +pub struct ProductionShaFoldedWitness +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + pub trace: ProjectedShaTrace, + pub opening_witness: PCSProverData, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct LinearIdealFoldProof { - pub ideal_family_polys: Vec>, - pub nifs: MultiDegreeSumcheckProof, +pub struct VerifiedShaSumFold { + pub r_b: Vec, + pub c_sf: F, } pub type LinearIdealFoldError = ProductionShaError; @@ -384,6 +387,8 @@ pub enum ProductionShaError { UnsupportedProductionShaWordDegree { got: usize, expected: usize }, #[error("unsupported production SHA PCS shape: {0}")] UnsupportedProductionShaPcsShape(&'static str), + #[error("production SHA prover not implemented: {0}")] + ProverNotImplemented(&'static str), #[error("PCS opening transcript has trailing bytes")] TrailingPcsOpeningBytes, #[error("{label} expected exactly one sumcheck group, got {got}")] @@ -418,41 +423,6 @@ pub enum ProductionShaError { PolyEval(#[from] EvaluationError), } -pub trait ProductionShaOpeningPCS: - ProductionShaPCS + Sized -where - Zt: ZincTypes, - F: PrimeField, - Self::BinaryPCS: FoldablePCS, D>, - Self::ArbitraryPCS: FoldablePCS, D>, - Self::IntPCS: FoldablePCS, -{ - fn prove_folded_sha_opening( - pcs_params: &PCSParams, - folded_prover_data: &PCSProverData, - folded_commitments: &PCSCommitments, - folded_trace: &ProjectedShaTrace, - r_0: &[F], - folded_lifted_evals: &[DynamicPolynomialF], - field_cfg: &F::Config, - ) -> Result, ProductionShaError> - where - F::Inner: ConstTranscribable + Transcribable, - F::Modulus: ConstTranscribable + Transcribable; - - fn verify_folded_sha_opening( - pcs_params: &PCSVerifierParams, - folded_commitments: &PCSCommitments, - r_0: &[F], - folded_lifted_evals: &[DynamicPolynomialF], - pcs_opening_bytes: &[u8], - field_cfg: &F::Config, - ) -> Result<(), ProductionShaError> - where - F::Inner: ConstTranscribable + Transcribable, - F::Modulus: ConstTranscribable + Transcribable; -} - pub fn absorb_projected_sha_publics( transcript: &mut impl Transcript, publics: &[zinc_piop::neutron_nova::ProjectedShaPublic], @@ -824,180 +794,6 @@ where Ok(()) } -impl ProductionShaOpeningPCS for AllHyraxPCSTypes -where - Zt: ZincTypes, - F: HyraxFieldBridge, - C: AffineRepr, - HyraxPCS: PCS< - F, - BinaryPoly, - D, - CommitmentKey = HyraxCommitmentKey, - VerifierKey = HyraxVerifierKey, - Commitment = HyraxCommitment, - ProverData = HyraxProverData, - > + FoldablePCS, D>, - HyraxPCS: PCS< - F, - DensePolynomial, - D, - CommitmentKey = HyraxCommitmentKey, - VerifierKey = HyraxVerifierKey, - Commitment = HyraxCommitment, - ProverData = HyraxProverData, - > + FoldablePCS, D>, - HyraxPCS: PCS< - F, - Zt::Int, - D, - CommitmentKey = HyraxCommitmentKey, - VerifierKey = HyraxVerifierKey, - Commitment = HyraxCommitment, - ProverData = HyraxProverData, - > + FoldablePCS, -{ - fn prove_folded_sha_opening( - pcs_params: &PCSParams, - folded_prover_data: &PCSProverData, - folded_commitments: &PCSCommitments, - folded_trace: &ProjectedShaTrace, - r_0: &[F], - folded_lifted_evals: &[DynamicPolynomialF], - field_cfg: &F::Config, - ) -> Result, ProductionShaError> - where - F::Inner: ConstTranscribable + Transcribable, - F::Modulus: ConstTranscribable + Transcribable, - { - ensure_production_sha_word_degree::()?; - validate_production_sha_batch_sizes::( - , D>>::batch_size(&folded_commitments.binary), - , D>>::batch_size( - &folded_commitments.arbitrary, - ), - >::batch_size(&folded_commitments.int), - )?; - let (binary_lifted, int_lifted) = split_folded_sha_pcs_lifted_evals(folded_lifted_evals)?; - - let mut transcript = PcsProverTranscript { - fs_transcript: Blake3Transcript::default(), - stream: Cursor::default(), - }; - let mut transcription_buf = vec![0u8; F::Inner::NUM_BYTES]; - - as PCS, D>>::absorb_commitment( - &mut transcript.fs_transcript, - &folded_commitments.binary, - ); - absorb_pcs_lifted_evals( - &mut transcript.fs_transcript, - binary_lifted, - &mut transcription_buf, - ); - let binary_scalar_lanes = folded_sha_binary_scalar_lanes::(folded_trace); - HyraxPCS::::prove_open_scalar_lanes::( - &mut transcript, - &pcs_params.binary, - &binary_scalar_lanes, - r_0, - &folded_prover_data.binary, - field_cfg, - )?; - - as PCS>::absorb_commitment( - &mut transcript.fs_transcript, - &folded_commitments.int, - ); - absorb_pcs_lifted_evals( - &mut transcript.fs_transcript, - int_lifted, - &mut transcription_buf, - ); - let int_scalar_lanes = folded_sha_int_scalar_lanes::(folded_trace); - HyraxPCS::::prove_open_scalar_lanes::( - &mut transcript, - &pcs_params.int, - &int_scalar_lanes, - r_0, - &folded_prover_data.int, - field_cfg, - )?; - - Ok(transcript.stream.into_inner()) - } - - fn verify_folded_sha_opening( - pcs_params: &PCSVerifierParams, - folded_commitments: &PCSCommitments, - r_0: &[F], - folded_lifted_evals: &[DynamicPolynomialF], - pcs_opening_bytes: &[u8], - field_cfg: &F::Config, - ) -> Result<(), ProductionShaError> - where - F::Inner: ConstTranscribable + Transcribable, - F::Modulus: ConstTranscribable + Transcribable, - { - ensure_production_sha_word_degree::()?; - validate_production_sha_batch_sizes::( - , D>>::batch_size(&folded_commitments.binary), - , D>>::batch_size( - &folded_commitments.arbitrary, - ), - >::batch_size(&folded_commitments.int), - )?; - let (binary_lifted, int_lifted) = split_folded_sha_pcs_lifted_evals(folded_lifted_evals)?; - - let mut transcript = PcsVerifierTranscript { - fs_transcript: Blake3Transcript::default(), - stream: Cursor::new(pcs_opening_bytes.to_vec()), - }; - let mut transcription_buf = vec![0u8; F::Inner::NUM_BYTES]; - - as PCS, D>>::absorb_commitment( - &mut transcript.fs_transcript, - &folded_commitments.binary, - ); - absorb_pcs_lifted_evals( - &mut transcript.fs_transcript, - binary_lifted, - &mut transcription_buf, - ); - as PCS, D>>::verify_open::( - &mut transcript, - &pcs_params.binary, - &folded_commitments.binary, - r_0, - binary_lifted, - field_cfg, - )?; - - as PCS>::absorb_commitment( - &mut transcript.fs_transcript, - &folded_commitments.int, - ); - absorb_pcs_lifted_evals( - &mut transcript.fs_transcript, - int_lifted, - &mut transcription_buf, - ); - as PCS>::verify_open::( - &mut transcript, - &pcs_params.int, - &folded_commitments.int, - r_0, - int_lifted, - field_cfg, - )?; - - if transcript.stream.position() != pcs_opening_bytes.len() as u64 { - return Err(ProductionShaError::TrailingPcsOpeningBytes); - } - Ok(()) - } -} - fn ensure_production_sha_word_degree() -> Result<(), ProductionShaError> where F: PrimeField, @@ -1044,7 +840,7 @@ pub fn commit_production_sha_instance( where Zt: ZincTypes, F: PrimeField, - P: ProductionShaPCS, + P: ZincPCSTypes, P::BinaryPCS: FoldablePCS, D>, P::ArbitraryPCS: FoldablePCS, D>, P::IntPCS: FoldablePCS, @@ -1083,9 +879,9 @@ pub fn prove_linear_ideal_fold( ) -> Result< LinearIdealFoldProveOutput< UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, - FoldedLinearIdealInstance, FoldedPublicEvals>, - FoldedLinearIdealWitness>, - LinearIdealFoldProof, + FoldedLinearIdealInstance, ProjectedShaPublic>, + FoldedLinearIdealWitness>, + ProductionLinearIdealFoldProof, >, LinearIdealFoldError, > @@ -1100,7 +896,7 @@ where + 'static, F::Inner: ConstTranscribable + Transcribable + Zero + Default + Send + Sync, F::Modulus: ConstTranscribable + Transcribable, - P: ProductionShaPCS, + P: ZincPCSTypes, P::BinaryPCS: FoldablePCS, D>, P::ArbitraryPCS: FoldablePCS, D>, P::IntPCS: FoldablePCS, @@ -1120,28 +916,41 @@ where absorb_production_sha_statement_metadata(transcript); absorb_uair_shape_metadata(transcript, shape); - let mut fresh_instances = Vec::with_capacity(witnesses.len()); - let mut instance_commitments = Vec::with_capacity(witnesses.len()); - let mut traces = Vec::with_capacity(witnesses.len()); - let mut publics = Vec::with_capacity(witnesses.len()); - - for witness in witnesses { - let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; - let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; - absorb_public_uair_trace::(transcript, fresh_instances.len(), &public_trace); - let (trace, public, witness_polys) = - U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg)?; - let (_data, commitment) = - commit_production_sha_instance::(&pp.pcs_params, &witness_polys)?; - - fresh_instances.push(UairInstance { - public_trace: own_uair_trace(&public_trace), - commitments: commitment.clone(), - }); - instance_commitments.push(commitment); - traces.push(trace); - publics.push(public); - } + let instance_artifacts = witnesses + .iter() + .enumerate() + .map(|(instance_idx, witness)| { + let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; + let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; + absorb_public_uair_trace::(transcript, instance_idx, &public_trace); + let (trace, public, witness_polys) = + U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg)?; + let (data, commitment) = + commit_production_sha_instance::(&pp.pcs_params, &witness_polys)?; + let fresh_instance = UairInstance { + public_trace: own_uair_trace(&public_trace), + commitments: commitment.clone(), + }; + + Ok::<_, ProductionShaError>((fresh_instance, commitment, data, trace, public)) + }) + .collect::, _>>()?; + + let (fresh_instances, instance_artifacts): (Vec<_>, Vec<_>) = instance_artifacts + .into_iter() + .map(|(fresh_instance, commitment, data, trace, public)| { + (fresh_instance, (commitment, data, trace, public)) + }) + .unzip(); + let (instance_commitments, instance_artifacts): (Vec<_>, Vec<_>) = instance_artifacts + .into_iter() + .map(|(commitment, data, trace, public)| (commitment, (data, trace, public))) + .unzip(); + let (instance_prover_data, instance_artifacts): (Vec<_>, Vec<_>) = instance_artifacts + .into_iter() + .map(|(data, trace, public)| (data, (trace, public))) + .unzip(); + let (traces, publics): (Vec<_>, Vec<_>) = instance_artifacts.into_iter().unzip(); validate_production_sha_publics(&publics, field_cfg)?; absorb_production_sha_commitments::( @@ -1155,8 +964,8 @@ where let coeff_cache = build_sha_linear_residual_coeff_cache(&traces, &publics, &r_ic, field_cfg)?; let beta = sample_instance_batch_challenge(transcript, witnesses.len(), field_cfg)?; let aggregate_ideal_polys = coeff_cache.beta_aggregate_nonzero_ideal_polys(&beta, field_cfg)?; - absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys); check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, field_cfg)?; + absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys); let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); let initial_claim = @@ -1180,27 +989,35 @@ where let folded_commitments = fold_pcs_commitments::( &instance_commitments, - sumfold_output.theta(), + sumfold_output.eq_instance_weights(), + field_cfg, + )?; + let folded_prover_data = fold_pcs_prover_data::( + &instance_prover_data, + sumfold_output.eq_instance_weights(), field_cfg, )?; let (folded, folded_public) = fold_projected_sha_traces(&traces, &publics, &sumfold_output, field_cfg)?; - - Ok(LinearIdealFoldProveOutput { + let _partial_output = ( fresh_instances, - folded_instance: FoldedLinearIdealInstance { + FoldedLinearIdealInstance { target: sumfold_output.t_prime().clone(), commitments: folded_commitments, - public: flatten_projected_sha_public(&folded_public), + public: folded_public, }, - folded_witness: FoldedLinearIdealWitness { - witness: flatten_projected_sha_trace(&folded.trace), - }, - proof: LinearIdealFoldProof { - ideal_family_polys: sha_ideal_family_polys(&aggregate_ideal_polys), - nifs: sumfold_proof, + FoldedLinearIdealWitness { + witness: ProductionShaFoldedWitness { + trace: folded.trace, + opening_witness: folded_prover_data, + }, }, - }) + sumfold_proof, + ); + + Err(ProductionShaError::ProverNotImplemented( + "folded row/multipoint/PCS proof assembly", + )) } fn public_uair_trace_view<'a, PolyCoeff, Int, F, const D: usize>( @@ -1339,72 +1156,53 @@ where } } -fn flatten_projected_sha_public(public: &ProjectedShaPublic) -> FoldedPublicEvals +pub fn setup_verify_linear_ideal_fold( + params: LinearIdealFoldVerifierParams, + shape: UairShape, +) -> Result, LinearIdealFoldError> where + U: Uair + ProductionShaProjectionAdapter, + Zt: ZincTypes, F: PrimeField, + P: ZincPCSTypes, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, { - let mut values = public - .columns - .columns - .iter() - .flat_map(|column| column.iter().cloned()) - .collect::>(); - if let Some(word_columns) = &public.word_columns { - values.extend( - word_columns - .columns - .iter() - .flat_map(|column| column.iter().flat_map(|bits| bits.iter().cloned())), - ); + ensure_production_sha_word_degree::()?; + if shape.num_vars != SHA_ROW_VARS { + return Err(ProductionShaError::LengthMismatch { + label: "production SHA row variables", + got: shape.num_vars, + expected: SHA_ROW_VARS, + }); } - FoldedPublicEvals { values } -} -fn flatten_projected_sha_trace(trace: &ProjectedShaTrace) -> FoldedUairTrace -where - F: PrimeField, -{ - let mut values = Vec::new(); - values.extend( - trace - .bit_slices - .columns - .iter() - .flat_map(|column| column.iter()) - .flat_map(|row| row.iter().cloned()), - ); - values.extend( - trace - .scalarized_words - .words - .iter() - .flat_map(|column| column.iter().cloned()), - ); - values.extend( - trace - .int_columns - .columns - .iter() - .flat_map(|column| column.iter().cloned()), - ); - values.extend( - trace - .public_columns - .columns - .iter() - .flat_map(|column| column.iter().cloned()), - ); - FoldedUairTrace { values } + let witness = shape.signature.witness_cols(); + validate_production_sha_batch_sizes::( + witness.num_binary_poly_cols(), + witness.num_arbitrary_poly_cols(), + witness.num_int_cols(), + )?; + + Ok(VerifiedLinearIdealFoldSetup { + pcs_params: params.pcs_params, + shape, + field_cfg: params.field_cfg, + }) } -pub fn verify_production_sha_core( +pub fn verify_linear_ideal_fold( + vs: &VerifiedLinearIdealFoldSetup, + instances: &[UairInstance<'_, Zt::Int, Zt::Int, PCSCommitments, D>], + proof: &ProductionLinearIdealFoldProof, transcript: &mut impl Transcript, - pcs_params: &PCSVerifierParams, - proof: &ProductionShaProof>, - publics: &[ProjectedShaPublic], - field_cfg: &F::Config, -) -> Result<(), ProductionShaError> +) -> Result< + FoldedLinearIdealInstance, ProjectedShaPublic>, + LinearIdealFoldError, +> where + U: Uair + ProductionShaProjectionAdapter, Zt: ZincTypes, F: InnerTransparentField + DelayedFieldProductSum @@ -1414,66 +1212,91 @@ where + 'static, F::Inner: ConstTranscribable + Transcribable + Zero + Default + Send + Sync, F::Modulus: ConstTranscribable + Transcribable, - P: ProductionShaOpeningPCS, + P: ZincPCSTypes, P::BinaryPCS: FoldablePCS, D>, P::ArbitraryPCS: FoldablePCS, D>, P::IntPCS: FoldablePCS, { + let field_cfg = &vs.field_cfg; ensure_production_sha_word_degree::()?; - if publics.len() < 2 { - return Err(ProductionShaError::InstanceCountTooSmall(publics.len())); + if instances.len() < 2 { + return Err(ProductionShaError::InstanceCountTooSmall(instances.len())); } - if !publics.len().is_power_of_two() { + if !instances.len().is_power_of_two() { return Err(ProductionShaError::InstanceCountNotPowerOfTwo( - publics.len(), + instances.len(), )); } - if proof.instance_commitments.len() != publics.len() { + if proof.instance_commitments.len() != instances.len() { return Err(ProductionShaError::LengthMismatch { - label: "commitments/publics", + label: "proof commitments/instances", got: proof.instance_commitments.len(), - expected: publics.len(), + expected: instances.len(), }); } - if proof.fresh_ideal_polys.len() != publics.len() { - return Err(ProductionShaError::LengthMismatch { - label: "fresh ideals/publics", - got: proof.fresh_ideal_polys.len(), - expected: publics.len(), - }); + + absorb_production_sha_statement_metadata(transcript); + absorb_uair_shape_metadata(transcript, &vs.shape); + + let mut publics = Vec::with_capacity(instances.len()); + for (instance_idx, instance) in instances.iter().enumerate() { + validate_public_uair_trace_shape::( + &instance.public_trace, + &vs.shape.signature, + )?; + if !pcs_commitments_match::( + &instance.commitments, + &proof.instance_commitments[instance_idx], + ) { + return Err(ProductionShaError::NonCanonicalProofObject( + "instance commitments do not match proof commitments", + )); + } + absorb_public_uair_trace::(transcript, instance_idx, &instance.public_trace); + publics.push(U::project_production_sha_public( + &vs.shape, + &instance.public_trace, + field_cfg, + )?); } - validate_production_sha_publics(publics, field_cfg)?; + + validate_production_sha_publics(&publics, field_cfg)?; let booleanity_sources = production_sha_booleanity_sources(); - absorb_production_sha_statement_metadata(transcript); absorb_production_sha_commitments::( transcript, b"production_sha_fresh_commitments", &proof.instance_commitments, ); - absorb_projected_sha_publics(transcript, publics); + absorb_projected_sha_publics(transcript, &publics); let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); - absorb_fresh_sha_ideal_polys(transcript, &proof.fresh_ideal_polys); - check_fresh_sha_ideal_membership(&proof.fresh_ideal_polys, field_cfg)?; + let beta = sample_instance_batch_challenge(transcript, instances.len(), field_cfg)?; + let aggregate_ideal_polys = aggregate_sha_ideal_polys_from_proof(&proof.ideal_check)?; + check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, field_cfg)?; + absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys); - let (a, lambda, rho, xi, beta) = - sample_post_ideal_challenges(transcript, publics.len(), field_cfg)?; - let fresh_targets = - evaluate_fresh_targets_from_ideal_polys(&proof.fresh_ideal_polys, &a, &lambda, field_cfg)?; - let initial_claim = eq_weighted_sum(&beta, &fresh_targets, field_cfg)?; + let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); + let initial_claim = + evaluate_aggregate_sha_ideal_claim(&aggregate_ideal_polys, &a, &lambda, field_cfg)?; - let sumfold_output = verify_full_sha_sumfold( + let verified_sumfold = verify_full_sha_sumfold( transcript, &proof.sumfold_proof, &initial_claim, + beta.len(), + field_cfg, + )?; + let sumfold_output = derive_sha_instance_fold_claim( &beta, - publics.len(), + verified_sumfold.r_b, + verified_sumfold.c_sf, + instances.len(), field_cfg, )?; let folded_commitments = fold_pcs_commitments::( &proof.instance_commitments, - sumfold_output.theta(), + sumfold_output.eq_instance_weights(), field_cfg, )?; absorb_production_sha_commitments::( @@ -1484,14 +1307,16 @@ where let row_output = verify_folded_row_sumcheck( transcript, - &proof.folded_row_sumcheck, + &proof.combined_sumcheck, sumfold_output.t_prime(), field_cfg, )?; - absorb_sha_endpoint_evals(transcript, &proof.endpoint_evals); - let folded_public = fold_projected_sha_publics(publics, sumfold_output.theta(), field_cfg)?; + let folded_public = + fold_projected_sha_publics(&publics, sumfold_output.eq_instance_weights(), field_cfg)?; + absorb_transcribable(transcript, &proof.resolver); + let endpoint_evals = sha_endpoint_evals_from_resolver(&proof.resolver, &a, field_cfg)?; let terminal = reconstruct_folded_row_terminal_from_endpoints( - &proof.endpoint_evals, + &endpoint_evals, &folded_public, &r_ic, &row_output.r_star, @@ -1507,60 +1332,294 @@ where let (subclaim, shift_specs) = verify_sha_endpoint_multipoint( transcript, &proof.multipoint_eval, - &proof.endpoint_evals, + &endpoint_evals, &folded_public, &row_output.r_star, field_cfg, )?; let open_evals = multipoint_open_evals_from_pcs_lifted( - &proof.folded_lifted_evals, + &proof.witness_lifted_evals, &production_sha_multipoint_layout(), &folded_public, &subclaim.sumcheck_subclaim.point, field_cfg, )?; verify_sha_endpoint_multipoint_open_evals(&subclaim, &open_evals, &shift_specs, field_cfg)?; - absorb_folded_lifted_evals(transcript, &proof.folded_lifted_evals); - P::verify_folded_sha_opening( - pcs_params, + absorb_folded_lifted_evals(transcript, &proof.witness_lifted_evals); + verify_production_sha_pcs_opening::( + &vs.pcs_params, &folded_commitments, &subclaim.sumcheck_subclaim.point, - &proof.folded_lifted_evals, - &proof.pcs_opening_bytes, + &proof.witness_lifted_evals, + &proof.opening_proof, field_cfg, )?; - transcript.absorb_slice(b"production_sha_pcs_opening_bytes"); - transcript.absorb_slice(&(proof.pcs_opening_bytes.len() as u64).to_le_bytes()); - transcript.absorb_slice(&proof.pcs_opening_bytes); + Ok(FoldedLinearIdealInstance { + target: sumfold_output.t_prime().clone(), + commitments: folded_commitments, + public: folded_public, + }) +} + +fn validate_public_uair_trace_shape( + trace: &UairTrace<'_, PolyCoeff, Int, D>, + sig: &UairSignature, +) -> Result<(), ProductionShaError> +where + PolyCoeff: Clone, + Int: Clone, + F: PrimeField, +{ + let public = sig.public_cols(); + if trace.binary_poly.len() != public.num_binary_poly_cols() { + return Err(ProductionShaError::LengthMismatch { + label: "UAIR public binary columns", + got: trace.binary_poly.len(), + expected: public.num_binary_poly_cols(), + }); + } + if trace.arbitrary_poly.len() != public.num_arbitrary_poly_cols() { + return Err(ProductionShaError::LengthMismatch { + label: "UAIR public arbitrary columns", + got: trace.arbitrary_poly.len(), + expected: public.num_arbitrary_poly_cols(), + }); + } + if trace.int.len() != public.num_int_cols() { + return Err(ProductionShaError::LengthMismatch { + label: "UAIR public int columns", + got: trace.int.len(), + expected: public.num_int_cols(), + }); + } Ok(()) } -pub fn verify_production_sha( - transcript: &mut impl Transcript, +fn pcs_commitments_match( + lhs: &PCSCommitments, + rhs: &PCSCommitments, +) -> bool +where + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, +{ + fn commitment_bytes(commitment: &C, write: W) -> Vec + where + W: FnOnce(&C, &mut Vec), + { + let mut bytes = Vec::new(); + write(commitment, &mut bytes); + bytes + } + + commitment_bytes(&lhs.binary, P::BinaryPCS::write_commitment_bytes) + == commitment_bytes(&rhs.binary, P::BinaryPCS::write_commitment_bytes) + && commitment_bytes(&lhs.arbitrary, P::ArbitraryPCS::write_commitment_bytes) + == commitment_bytes(&rhs.arbitrary, P::ArbitraryPCS::write_commitment_bytes) + && commitment_bytes(&lhs.int, P::IntPCS::write_commitment_bytes) + == commitment_bytes(&rhs.int, P::IntPCS::write_commitment_bytes) +} + +fn aggregate_sha_ideal_polys_from_proof( + proof: &IdealCheckProof, +) -> Result<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], ProductionShaError> +where + F: PrimeField, +{ + let got = proof.combined_mle_values.len(); + proof + .combined_mle_values + .clone() + .try_into() + .map_err(|_| ProductionShaError::LengthMismatch { + label: "aggregate SHA ideal polynomial count", + got, + expected: NUM_NONZERO_SHA_FAMILIES, + }) +} + +fn sha_endpoint_evals_from_resolver( + resolver: &CombinedPolyResolverProof, + a: &F, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + if !resolver.down_evals.is_empty() { + return Err(ProductionShaError::LengthMismatch { + label: "production SHA resolver down evals", + got: resolver.down_evals.len(), + expected: 0, + }); + } + if !resolver.bit_op_down_evals.is_empty() { + return Err(ProductionShaError::LengthMismatch { + label: "production SHA resolver bit-op down evals", + got: resolver.bit_op_down_evals.len(), + expected: 0, + }); + } + + let word_sources = production_sha_endpoint_word_sources(); + let unshifted_words = word_sources.iter().filter(|(_, shift)| *shift == 0).count(); + let shifted_words = word_sources.len() - unshifted_words; + let expected_unshifted_bits = unshifted_words * SHA_WORD_BITS; + let expected_shifted_bits = shifted_words * SHA_WORD_BITS; + if resolver.bit_slice_evals.len() != expected_unshifted_bits { + return Err(ProductionShaError::LengthMismatch { + label: "production SHA resolver unshifted bit slices", + got: resolver.bit_slice_evals.len(), + expected: expected_unshifted_bits, + }); + } + if resolver.shifted_bit_slice_evals.len() != expected_shifted_bits { + return Err(ProductionShaError::LengthMismatch { + label: "production SHA resolver shifted bit slices", + got: resolver.shifted_bit_slice_evals.len(), + expected: expected_shifted_bits, + }); + } + + let powers = zinc_utils::powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); + let mut unshifted_idx = 0usize; + let mut shifted_idx = 0usize; + let mut sources = Vec::with_capacity(word_sources.len()); + for (col, shift) in word_sources { + let bit_slice = if shift == 0 { + let start = unshifted_idx * SHA_WORD_BITS; + unshifted_idx += 1; + &resolver.bit_slice_evals[start..start + SHA_WORD_BITS] + } else { + let start = shifted_idx * SHA_WORD_BITS; + shifted_idx += 1; + &resolver.shifted_bit_slice_evals[start..start + SHA_WORD_BITS] + }; + let bits: [F; SHA_WORD_BITS] = std::array::from_fn(|idx| bit_slice[idx].clone()); + let scalarized = bits + .iter() + .zip(powers.iter()) + .fold(F::zero_with_cfg(field_cfg), |acc, (bit, power)| { + acc + bit.clone() * power + }); + sources.push(ShaSourceEndpointEval { + col, + shift, + scalarized, + bits, + }); + } + + let int_sources = production_sha_endpoint_int_sources(); + if resolver.up_evals.len() != int_sources.len() { + return Err(ProductionShaError::LengthMismatch { + label: "production SHA resolver int evals", + got: resolver.up_evals.len(), + expected: int_sources.len(), + }); + } + let int_sources = int_sources + .into_iter() + .zip(resolver.up_evals.iter()) + .map(|(col, scalar)| ShaIntEndpointEval { + col, + scalar: scalar.clone(), + }) + .collect(); + + Ok(ShaEndpointEvals { + sources, + int_sources, + }) +} + +fn verify_production_sha_pcs_opening( pcs_params: &PCSVerifierParams, - proof: &ProductionShaProof>, - publics: &[ProjectedShaPublic], + folded_commitments: &PCSCommitments, + r_0: &[F], + folded_lifted_evals: &[DynamicPolynomialF], + opening_proof: &PCSOpeningProof, field_cfg: &F::Config, ) -> Result<(), ProductionShaError> where Zt: ZincTypes, - F: InnerTransparentField - + DelayedFieldProductSum - + FromPrimitiveWithConfig - + Send - + Sync - + 'static, - F::Inner: ConstTranscribable + Transcribable + Zero + Default + Send + Sync, + F: PrimeField, + F::Inner: ConstTranscribable + Transcribable, F::Modulus: ConstTranscribable + Transcribable, - P: ProductionShaOpeningPCS, - P::BinaryPCS: FoldablePCS, D>, - P::ArbitraryPCS: FoldablePCS, D>, - P::IntPCS: FoldablePCS, + P: ZincPCSTypes, { - verify_production_sha_core::(transcript, pcs_params, proof, publics, field_cfg) + ensure_production_sha_word_degree::()?; + validate_production_sha_batch_sizes::( + P::BinaryPCS::batch_size(&folded_commitments.binary), + P::ArbitraryPCS::batch_size(&folded_commitments.arbitrary), + P::IntPCS::batch_size(&folded_commitments.int), + )?; + let (binary_lifted, int_lifted) = split_folded_sha_pcs_lifted_evals(folded_lifted_evals)?; + let arbitrary_lifted: &[DynamicPolynomialF] = &[]; + + let mut transcript = PcsVerifierTranscript { + fs_transcript: Blake3Transcript::default(), + stream: Cursor::default(), + }; + let mut transcription_buf = vec![0u8; F::Inner::NUM_BYTES]; + + P::BinaryPCS::absorb_commitment(&mut transcript.fs_transcript, &folded_commitments.binary); + absorb_pcs_lifted_evals( + &mut transcript.fs_transcript, + binary_lifted, + &mut transcription_buf, + ); + P::BinaryPCS::verify_open::( + &mut transcript, + &pcs_params.binary, + &folded_commitments.binary, + r_0, + binary_lifted, + &opening_proof.binary, + field_cfg, + )?; + + P::ArbitraryPCS::absorb_commitment( + &mut transcript.fs_transcript, + &folded_commitments.arbitrary, + ); + absorb_pcs_lifted_evals( + &mut transcript.fs_transcript, + arbitrary_lifted, + &mut transcription_buf, + ); + P::ArbitraryPCS::verify_open::( + &mut transcript, + &pcs_params.arbitrary, + &folded_commitments.arbitrary, + r_0, + arbitrary_lifted, + &opening_proof.arbitrary, + field_cfg, + )?; + + P::IntPCS::absorb_commitment(&mut transcript.fs_transcript, &folded_commitments.int); + absorb_pcs_lifted_evals( + &mut transcript.fs_transcript, + int_lifted, + &mut transcription_buf, + ); + P::IntPCS::verify_open::( + &mut transcript, + &pcs_params.int, + &folded_commitments.int, + r_0, + int_lifted, + &opening_proof.int, + field_cfg, + )?; + + Ok(()) } +#[allow(dead_code)] fn evaluate_fresh_targets_from_ideal_polys( ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], a: &F, @@ -1597,6 +1656,7 @@ where .collect() } +#[allow(dead_code)] fn beta_aggregate_sha_ideal_polys( ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], beta: &[F], @@ -1626,6 +1686,7 @@ where Ok(aggregate) } +#[allow(dead_code)] fn scale_production_sha_poly(poly: &DynamicPolynomialF, scalar: &F) -> DynamicPolynomialF where F: PrimeField, @@ -1665,25 +1726,6 @@ where Ok(target) } -fn sha_ideal_family_polys( - ideal_polys: &[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], -) -> Vec> -where - F: PrimeField, -{ - production_sha_nonzero_families() - .iter() - .enumerate() - .map(|(slot, family)| IdealFamilyPolys { - family: IdealFamilyId(family.index() as u16), - polys: vec![IdealFamilyPoly { - slot: IdealPolySlot(0), - poly: ideal_polys[slot].clone(), - }], - }) - .collect() -} - fn evaluate_production_sha_poly_at_powers( poly: &DynamicPolynomialF, powers: &[F], @@ -1710,6 +1752,7 @@ where }) } +#[allow(dead_code)] fn eq_weighted_sum( point: &[F], values: &[F], @@ -2008,6 +2051,7 @@ where Ok(()) } +#[allow(dead_code)] fn folded_sha_binary_scalar_lanes( folded_trace: &ProjectedShaTrace, ) -> Vec>> @@ -2033,6 +2077,7 @@ where .collect() } +#[allow(dead_code)] fn folded_sha_int_scalar_lanes( folded_trace: &ProjectedShaTrace, ) -> Vec>> @@ -2132,7 +2177,7 @@ where .randomness .clone(); let c_sf = sumfold_expected_eval(beta, fresh_targets, &r_b, field_cfg)?; - let output = finalize_sha_sumfold(beta, r_b, c_sf, fresh_targets.len(), field_cfg)?; + let output = derive_sha_instance_fold_claim(beta, r_b, c_sf, fresh_targets.len(), field_cfg)?; Ok((proof, output)) } @@ -2167,7 +2212,7 @@ where if c_sf != sumfold_expected_eval(beta, fresh_targets, &r_b, field_cfg)? { return Err(ProductionShaError::SumFoldTerminalMismatch); } - Ok(finalize_sha_sumfold( + Ok(derive_sha_instance_fold_claim( beta, r_b, c_sf, @@ -2230,7 +2275,7 @@ where })? .randomness .clone(); - let provisional = finalize_sha_sumfold( + let provisional = derive_sha_instance_fold_claim( beta, r_b.clone(), F::one_with_cfg(field_cfg), @@ -2258,7 +2303,7 @@ where let c_sf = d * t_prime; Ok(( proof, - finalize_sha_sumfold(beta, r_b, c_sf, traces.len(), field_cfg)?, + derive_sha_instance_fold_claim(beta, r_b, c_sf, traces.len(), field_cfg)?, )) } @@ -2320,7 +2365,7 @@ where })? .randomness .clone(); - let provisional = finalize_sha_sumfold( + let provisional = derive_sha_instance_fold_claim( beta, r_b.clone(), F::one_with_cfg(field_cfg), @@ -2348,7 +2393,7 @@ where let c_sf = d * t_prime; Ok(( proof, - finalize_sha_sumfold(beta, r_b, c_sf, traces.len(), field_cfg)?, + derive_sha_instance_fold_claim(beta, r_b, c_sf, traces.len(), field_cfg)?, )) } @@ -2356,10 +2401,9 @@ pub fn verify_full_sha_sumfold( transcript: &mut impl Transcript, proof: &MultiDegreeSumcheckProof, initial_claim: &F, - beta: &[F], - instance_count: usize, + instance_vars: usize, field_cfg: &F::Config, -) -> Result, ProductionShaError> +) -> Result, ProductionShaError> where F: InnerTransparentField + DelayedFieldProductSum @@ -2388,16 +2432,10 @@ where } let subclaims = - MultiDegreeSumcheck::verify_as_subprotocol(transcript, beta.len(), proof, field_cfg)?; + MultiDegreeSumcheck::verify_as_subprotocol(transcript, instance_vars, proof, field_cfg)?; let r_b = subclaims.point().to_vec(); let c_sf = subclaims.expected_evaluations()[0].clone(); - Ok(finalize_sha_sumfold( - beta, - r_b, - c_sf, - instance_count, - field_cfg, - )?) + Ok(VerifiedShaSumFold { r_b, c_sf }) } pub fn fold_pcs_commitments( @@ -3816,6 +3854,7 @@ mod tests { type VerifierKey = (); type Commitment = NoopCommitment; type ProverData = (); + type OpeningProof = Vec; fn commit( _ck: &Self::CommitmentKey, @@ -3852,12 +3891,12 @@ mod tests { _point: &[Fp], _prover_data: &Self::ProverData, _field_cfg: &Fp::Config, - ) -> Result<(), ZipError> + ) -> Result where Fp::Inner: Transcribable, Fp::Modulus: Transcribable, { - Ok(()) + Ok(Vec::new()) } fn verify_open( @@ -3866,6 +3905,7 @@ mod tests { _commitment: &Self::Commitment, _point: &[Fp], _lifted_evals: &[DynamicPolynomialF], + _opening_proof: &Self::OpeningProof, _field_cfg: &Fp::Config, ) -> Result<(), ZipError> where @@ -3916,13 +3956,6 @@ mod tests { type IntPCS = NoopPCS; } - impl ProductionShaPCS for NoopPCSTypes - where - Zt: ZincTypes, - Fp: PrimeField, - { - } - fn sha_binary_col<'a>( public_trace: &'a UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, witness_trace: &'a UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, @@ -4076,6 +4109,37 @@ mod tests { impl ProductionShaProjectionAdapter for Sha256CompressionSliceUair { + fn project_production_sha_public( + _shape: &UairShape, + public_trace: &UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, + field_cfg: &::Config, + ) -> Result, ProductionShaError> { + let empty_witness = UairTrace { + binary_poly: Cow::Borrowed(&[]), + arbitrary_poly: Cow::Borrowed(&[]), + int: Cow::Borrowed(&[]), + }; + let pa_a = project_binary_source( + sha_binary_col(public_trace, &empty_witness, sha256_cols::PA_A)?, + field_cfg, + )?; + let pa_e = project_binary_source( + sha_binary_col(public_trace, &empty_witness, sha256_cols::PA_E)?, + field_cfg, + )?; + let message = project_binary_source( + sha_binary_col(public_trace, &empty_witness, sha256_cols::PA_M)?, + field_cfg, + )?; + let public_columns = projected_public_from_sources(&pa_a, &pa_e, &message, field_cfg); + Ok(ProjectedShaPublic { + columns: public_columns, + word_columns: Some(ShaPublicWordColumns { + columns: vec![pa_a.clone(), pa_e.clone(), pa_a, pa_e, message], + }), + }) + } + fn project_production_sha_witness( _shape: &UairShape, public_trace: &UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, @@ -4300,7 +4364,7 @@ mod tests { } #[test] - fn prove_linear_ideal_fold_accepts_sha256_uair_witnesses() { + fn prove_linear_ideal_fold_stops_at_unimplemented_proof_tail() { type U = Sha256CompressionSliceUair; let field_cfg = cfg(); @@ -4340,13 +4404,12 @@ mod tests { TEST_DEGREE_PLUS_ONE, >(&pp, &shape, &witnesses, &mut transcript); - let output = result.expect("new prove API should complete through SumFold"); - assert_eq!(output.fresh_instances.len(), witnesses.len()); - assert_eq!( - output.proof.ideal_family_polys.len(), - NUM_NONZERO_SHA_FAMILIES - ); - assert_eq!(output.proof.nifs.claimed_sums().len(), 1); + assert!(matches!( + result, + Err(ProductionShaError::ProverNotImplemented( + "folded row/multipoint/PCS proof assembly" + )) + )); } #[test] @@ -4519,7 +4582,7 @@ mod tests { } #[test] - fn sumfold_outputs_instance_fold_point_before_theta() { + fn sumfold_outputs_instance_fold_point_before_weights() { let field_cfg = cfg(); let fresh_targets = vec![f(2), f(5), f(7), f(11)]; let beta = vec![f(13), f(17)]; @@ -4543,10 +4606,13 @@ mod tests { assert_eq!(verifier_output, prover_output); assert_eq!( - prover_output.theta(), + prover_output.eq_instance_weights(), build_eq_x_r_vec(prover_output.r_b(), &field_cfg).unwrap() ); - assert_eq!(prover_output.theta().len(), fresh_targets.len()); + assert_eq!( + prover_output.eq_instance_weights().len(), + fresh_targets.len() + ); let d = eq_eval(&beta, prover_output.r_b(), F::one_with_cfg(&field_cfg)).unwrap(); assert_eq!(prover_output.c_sf(), &(d * prover_output.t_prime())); @@ -4650,11 +4716,18 @@ mod tests { let mut verifier_transcript = Blake3Transcript::new(); verifier_transcript.absorb_slice(b"full-sha-sumfold-context"); - let verifier_output = verify_full_sha_sumfold( + let verified_sumfold = verify_full_sha_sumfold( &mut verifier_transcript, &proof, &initial_claim, + beta.len(), + &field_cfg, + ) + .unwrap(); + let verifier_output = derive_sha_instance_fold_claim( &beta, + verified_sumfold.r_b, + verified_sumfold.c_sf, traces.len(), &field_cfg, ) @@ -4662,10 +4735,10 @@ mod tests { assert_eq!(verifier_output, prover_output); assert_eq!( - prover_output.theta(), + prover_output.eq_instance_weights(), build_eq_x_r_vec(prover_output.r_b(), &field_cfg).unwrap() ); - assert_eq!(prover_output.theta().len(), traces.len()); + assert_eq!(prover_output.eq_instance_weights().len(), traces.len()); let (folded, folded_public) = fold_projected_sha_traces(&traces, &publics, &prover_output, &field_cfg).unwrap(); @@ -4686,15 +4759,8 @@ mod tests { let mut bad_transcript = Blake3Transcript::new(); bad_transcript.absorb_slice(b"full-sha-sumfold-context"); assert!( - verify_full_sha_sumfold( - &mut bad_transcript, - &proof, - &f(1), - &beta, - traces.len(), - &field_cfg - ) - .is_err() + verify_full_sha_sumfold(&mut bad_transcript, &proof, &f(1), beta.len(), &field_cfg) + .is_err() ); } diff --git a/protocol/src/verifier.rs b/protocol/src/verifier.rs index fd86a249..0d17b3b8 100644 --- a/protocol/src/verifier.rs +++ b/protocol/src/verifier.rs @@ -1043,6 +1043,7 @@ where &commitments.binary, r_0, &all_lifted_evals[num_pub_bin..num_total_bin], + &Default::default(), field_cfg, ) .map_err(|e| ProtocolError::PcsVerification(0, e))?; @@ -1052,6 +1053,7 @@ where &commitments.arbitrary, r_0, &all_lifted_evals[add!(num_total_bin, num_pub_arb)..add!(num_total_bin, num_total_arb)], + &Default::default(), field_cfg, ) .map_err(|e| ProtocolError::PcsVerification(1, e))?; @@ -1061,6 +1063,7 @@ where &commitments.int, r_0, &all_lifted_evals[add!(add!(num_total_bin, num_total_arb), num_pub_int)..], + &Default::default(), field_cfg, ) .map_err(|e| ProtocolError::PcsVerification(2, e))?; diff --git a/zip-plus/src/pcs/generic.rs b/zip-plus/src/pcs/generic.rs index 6bb7782e..aa07db04 100644 --- a/zip-plus/src/pcs/generic.rs +++ b/zip-plus/src/pcs/generic.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, marker::PhantomData}; +use std::{fmt::Debug, io::Cursor, marker::PhantomData}; use crypto_primitives::{FromPrimitiveWithConfig, FromWithConfig, PrimeField}; use zinc_poly::{ @@ -28,6 +28,7 @@ where type VerifierKey: Clone + Debug + Send + Sync; type Commitment: Clone + Debug + Send + Sync; type ProverData: Clone + Debug + Send + Sync; + type OpeningProof: Clone + Debug + Send + Sync + Default; fn precompute_ck(_ck: &Self::CommitmentKey) {} @@ -51,7 +52,7 @@ where point: &[F], prover_data: &Self::ProverData, field_cfg: &F::Config, - ) -> Result<(), ZipError> + ) -> Result where F::Inner: Transcribable, F::Modulus: Transcribable; @@ -62,6 +63,7 @@ where commitment: &Self::Commitment, point: &[F], lifted_evals: &[DynamicPolynomialF], + opening_proof: &Self::OpeningProof, field_cfg: &F::Config, ) -> Result<(), ZipError> where @@ -119,6 +121,7 @@ where type VerifierKey = ZipPlusParams; type Commitment = ZipPlusCommitment; type ProverData = Option>; + type OpeningProof = Vec; fn commit( ck: &Self::CommitmentKey, @@ -156,17 +159,19 @@ where point: &[F], prover_data: &Self::ProverData, field_cfg: &F::Config, - ) -> Result<(), ZipError> + ) -> Result where F::Inner: Transcribable, F::Modulus: Transcribable, { + let start = transcript.stream.position() as usize; if let Some(hint) = prover_data { let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( transcript, ck, polys, point, hint, field_cfg, )?; } - Ok(()) + let end = transcript.stream.position() as usize; + Ok(transcript.stream.get_ref()[start..end].to_vec()) } fn verify_open( @@ -175,12 +180,36 @@ where commitment: &Self::Commitment, point: &[F], lifted_evals: &[DynamicPolynomialF], + opening_proof: &Self::OpeningProof, field_cfg: &F::Config, ) -> Result<(), ZipError> where F::Inner: Transcribable, F::Modulus: Transcribable, { + if !opening_proof.is_empty() { + let original_stream = + std::mem::replace(&mut transcript.stream, Cursor::new(opening_proof.clone())); + let result = >::verify_open::( + transcript, + vk, + commitment, + point, + lifted_evals, + &Vec::new(), + field_cfg, + ); + let consumed = transcript.stream.position() == opening_proof.len() as u64; + transcript.stream = original_stream; + result?; + if !consumed { + return Err(ZipError::InvalidPcsOpen( + "PCS opening proof has trailing bytes".to_string(), + )); + } + return Ok(()); + } + if commitment.batch_size == 0 { return Ok(()); } diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index 4566a7d8..6da0e5f3 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -2,7 +2,7 @@ use std::{ fmt::Debug, - io::{Read, Write}, + io::{Cursor, Read, Write}, marker::PhantomData, }; @@ -437,6 +437,7 @@ where type VerifierKey = HyraxVerifierKey; type Commitment = HyraxCommitment; type ProverData = HyraxProverData; + type OpeningProof = Vec; fn precompute_ck(ck: &Self::CommitmentKey) { Lanes::Strategy::precompute_ck(&ck.msm_ck); @@ -554,14 +555,15 @@ where point: &[F], prover_data: &Self::ProverData, field_cfg: &F::Config, - ) -> Result<(), ZipError> + ) -> Result where F::Inner: Transcribable, F::Modulus: Transcribable, { let _ = CHECK_FOR_OVERFLOW; + let start = transcript.stream.position() as usize; if polys.is_empty() { - return Ok(()); + return Ok(Vec::new()); } validate_polys(polys)?; validate_hyrax_shape::( @@ -653,7 +655,8 @@ where )); } - Ok(()) + let end = transcript.stream.position() as usize; + Ok(transcript.stream.get_ref()[start..end].to_vec()) } fn verify_open( @@ -662,6 +665,7 @@ where commitment: &Self::Commitment, point: &[F], lifted_evals: &[DynamicPolynomialF], + opening_proof: &Self::OpeningProof, field_cfg: &F::Config, ) -> Result<(), ZipError> where @@ -669,6 +673,29 @@ where F::Modulus: Transcribable, { let _ = CHECK_FOR_OVERFLOW; + if !opening_proof.is_empty() { + let original_stream = + std::mem::replace(&mut transcript.stream, Cursor::new(opening_proof.clone())); + let result = >::verify_open::( + transcript, + vk, + commitment, + point, + lifted_evals, + &Vec::new(), + field_cfg, + ); + let consumed = transcript.stream.position() == opening_proof.len() as u64; + transcript.stream = original_stream; + result?; + if !consumed { + return Err(ZipError::InvalidPcsOpen( + "PCS opening proof has trailing bytes".to_string(), + )); + } + return Ok(()); + } + if commitment.batch_size == 0 { return Ok(()); } @@ -1407,6 +1434,7 @@ mod tests { &commitment, &point, &lifted_evals, + &Vec::new(), &cfg, ) } @@ -1603,6 +1631,7 @@ mod tests { &folded_commitment, &point, &folded_lifted_evals, + &Vec::new(), &cfg, ) .unwrap(); From e215dc7c0d8c0fa775cada6c9b7570ea08f684cc Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 17:16:59 -0700 Subject: [PATCH 23/49] Implement production SHA SumFold integration --- .../sha-uair-doc/uair-object-model.md | 8 +- piop/benches/neutron_nova_sumfold.rs | 68 +- piop/src/multipoint_eval.rs | 16 +- piop/src/neutron_nova/mod.rs | 44 +- piop/src/neutron_nova/projection_sha.rs | 1564 ++++++---- piop/src/sumcheck.rs | 14 +- piop/src/sumcheck/multi_degree.rs | 16 +- piop/src/sumcheck/prover.rs | 14 +- piop/src/sumcheck/verifier.rs | 6 +- poly/src/mle/dense.rs | 4 +- protocol/src/lib.rs | 19 +- protocol/src/multipoint_reduction.rs | 10 +- protocol/src/production_sha.rs | 2778 +++++++++++++---- test-uair/src/lib.rs | 6 +- test-uair/src/sha256.rs | 188 +- test-uair/src/sha_ecdsa.rs | 6 +- transcript/src/traits.rs | 56 + utils/src/field/boxed_monty.rs | 22 +- zip-plus/src/pcs/hyrax.rs | 45 +- 19 files changed, 3521 insertions(+), 1363 deletions(-) diff --git a/documentation/sha-uair-doc/uair-object-model.md b/documentation/sha-uair-doc/uair-object-model.md index 32ec4120..9d3c5d0d 100644 --- a/documentation/sha-uair-doc/uair-object-model.md +++ b/documentation/sha-uair-doc/uair-object-model.md @@ -566,7 +566,7 @@ pub fn prove_linear_ideal_fold( ) -> Result< LinearIdealFoldProveOutput< UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, - FoldedLinearIdealInstance, ProjectedShaPublic>, + FoldedLinearIdealInstance, ProjectedPublic>, FoldedLinearIdealWitness>, ProductionLinearIdealFoldProof, >, @@ -589,12 +589,12 @@ where F: PrimeField, P: ZincPCSTypes, { - pub trace: ProjectedShaTrace, + pub trace: ProjectedTrace, pub opening_witness: PCSProverData, } ``` -The folded verifier-visible public value is `ProjectedShaPublic`, not a flat +The folded verifier-visible public value is `ProjectedPublic`, not a flat field vector, because the verifier needs structured SHA public columns for terminal reconstruction and multipoint checks. @@ -632,7 +632,7 @@ pub fn verify_linear_ideal_fold( proof: &ProductionLinearIdealFoldProof, transcript: &mut impl Transcript, ) -> Result< - FoldedLinearIdealInstance, ProjectedShaPublic>, + FoldedLinearIdealInstance, ProjectedPublic>, LinearIdealFoldError, > where diff --git a/piop/benches/neutron_nova_sumfold.rs b/piop/benches/neutron_nova_sumfold.rs index 9df0303d..52c1954e 100644 --- a/piop/benches/neutron_nova_sumfold.rs +++ b/piop/benches/neutron_nova_sumfold.rs @@ -6,13 +6,14 @@ use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_ma use crypto_primitives::{Field, FromWithConfig, PrimeField, crypto_bigint_monty::MontyField}; use zinc_piop::{ neutron_nova::{ - ProjectedShaPublic, ProjectedShaTrace, SHA_ROW_COUNT, SHA_WORD_BITS, ShaBitSliceColumns, - ShaBooleanitySource, ShaIntCol, ShaIntColumns, ShaPublicCol, ShaPublicColumns, ShaWordCol, + MleTable, ProjectedPublic, ProjectedTrace, SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, + ShaBooleanitySource, ShaIntCol, ShaPublicCol, ShaWordCol, bit_slice_index, build_dense_sha_sumfold_group, build_production_sha_sumfold_group_owned, - scalarize_trace_words, + scalarize_bit_slices, }, sumcheck::multi_degree::MultiDegreeSumcheck, }; +use zinc_poly::mle::DenseMultilinearExtension; use zinc_primality::{MillerRabin, PrimalityTest}; use zinc_transcript::{Blake3Transcript, traits::Transcript}; @@ -30,11 +31,35 @@ fn f(value: u64, cfg: &::Config) -> F { F::from_with_cfg(value, cfg) } +fn mle_table_from_columns(columns: Vec>) -> MleTable { + columns + .into_iter() + .map(|evaluations| DenseMultilinearExtension { + evaluations, + num_vars: SHA_ROW_VARS, + }) + .collect() +} + +fn flatten_bits(bits: Vec>>) -> MleTable { + let mut flattened = (0..bits.len() * SHA_WORD_BITS) + .map(|_| Vec::new()) + .collect::>(); + for (col_idx, rows) in bits.into_iter().enumerate() { + for row in rows { + for (bit_idx, value) in row.into_iter().enumerate() { + flattened[bit_slice_index(col_idx, bit_idx, SHA_WORD_BITS)].push(value); + } + } + } + mle_table_from_columns(flattened) +} + fn synthetic_boolean_trace( instance_idx: u64, a: &F, cfg: &::Config, -) -> ProjectedShaTrace { +) -> ProjectedTrace { let zero = F::zero_with_cfg(cfg); let mut bits = vec![vec![vec![zero.clone(); SHA_WORD_BITS]; SHA_ROW_COUNT]; ShaWordCol::COUNT]; for (col_idx, col) in bits.iter_mut().enumerate() { @@ -49,26 +74,29 @@ fn synthetic_boolean_trace( } } } - let bit_slices = ShaBitSliceColumns { columns: bits }; - let scalarized_words = scalarize_trace_words(&bit_slices, a, cfg).unwrap(); - ProjectedShaTrace { - rows: SHA_ROW_COUNT, + let bit_slices = flatten_bits(bits); + let scalarized = scalarize_bit_slices(&bit_slices, a, cfg).unwrap(); + ProjectedTrace { bit_slices, - scalarized_words, - int_columns: ShaIntColumns { - columns: vec![vec![zero.clone(); SHA_ROW_COUNT]; ShaIntCol::COUNT], - }, - public_columns: ShaPublicColumns { - columns: vec![vec![zero; SHA_ROW_COUNT]; ShaPublicCol::COUNT], - }, + scalarized, + int_columns: mle_table_from_columns(vec![ + vec![zero.clone(); SHA_ROW_COUNT]; + ShaIntCol::COUNT + ]), + public_columns: mle_table_from_columns(vec![ + vec![zero; SHA_ROW_COUNT]; + ShaPublicCol::COUNT + ]), } } -fn zero_public(cfg: &::Config) -> ProjectedShaPublic { - ProjectedShaPublic { - columns: ShaPublicColumns { - columns: vec![vec![F::zero_with_cfg(cfg); SHA_ROW_COUNT]; ShaPublicCol::COUNT], - }, +fn zero_public(cfg: &::Config) -> ProjectedPublic { + ProjectedPublic { + columns: mle_table_from_columns(vec![ + vec![F::zero_with_cfg(cfg); SHA_ROW_COUNT]; + ShaPublicCol::COUNT + ]), + bit_slices: None, } } diff --git a/piop/src/multipoint_eval.rs b/piop/src/multipoint_eval.rs index 4afe22fb..ee1320b4 100644 --- a/piop/src/multipoint_eval.rs +++ b/piop/src/multipoint_eval.rs @@ -44,7 +44,7 @@ use zinc_poly::{ }; use zinc_transcript::{ delegate_transcribable, - traits::{ConstTranscribable, Transcript}, + traits::{ConstTranscribable, Transcribable, Transcript}, }; use zinc_uair::ShiftSpec; use zinc_utils::{ @@ -115,8 +115,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero + Default + Send + Sync, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, { /// Multi-point evaluation protocol prover. /// @@ -143,8 +143,9 @@ where // Step 1: Sample multi-point batching coefficient \alpha and column // batching coefficients \gamma_1,...,\gamma_J. - let alphas: Vec = transcript.get_field_challenges(num_down_cols, field_cfg); - let gammas: Vec = transcript.get_field_challenges(num_cols, field_cfg); + let alphas: Vec = + transcript.get_transcribable_field_challenges(num_down_cols, field_cfg); + let gammas: Vec = transcript.get_transcribable_field_challenges(num_cols, field_cfg); // Step 2: Build the two selector MLEs: // eq_r(b) = eq(b, r') @@ -259,8 +260,9 @@ where let one = F::one_with_cfg(field_cfg); // Step 1: Sample \alpha_k and \gamma_j (must match prover). - let alphas: Vec = transcript.get_field_challenges(num_down_cols, field_cfg); - let gammas: Vec = transcript.get_field_challenges(num_cols, field_cfg); + let alphas: Vec = + transcript.get_transcribable_field_challenges(num_down_cols, field_cfg); + let gammas: Vec = transcript.get_transcribable_field_challenges(num_cols, field_cfg); // Step 2: Compute expected sum let expected_sum: F = diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index 6f9bca78..b4d25cf2 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -25,25 +25,31 @@ pub use linear_cpr::{ build_linear_cpr_prefix_table, build_sumfold_eq_weights, }; pub use projection_sha::{ - FoldedCommitments, FoldedShaAccumulator, FoldedShaWitness, FreshShaIdealCache, - NUM_NONZERO_SHA_FAMILIES, NUM_SHA_RESIDUAL_FAMILIES, ProjectedShaPublic, ProjectedShaTrace, - SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, ShaBitSliceColumns, ShaBooleanitySource, ShaIntCol, - ShaIntColumns, ShaLinearResidualCoeffCache, ShaProductionIdeal, ShaProjectionError, - ShaPublicCol, ShaPublicColumns, ShaPublicWordCol, ShaPublicWordColumns, ShaResidualFamily, - ShaScalarizedRows, ShaSumFoldOutput, ShaWordCol, VirtualChMajValues, - build_dense_sha_sumfold_group, build_expression_folded_row_sumcheck_group, - build_folded_row_sumcheck_group, build_fresh_sha_ideal_cache, - build_production_sha_sumfold_group, build_production_sha_sumfold_group_owned, - build_production_sha_sumfold_group_with_linear_cache, build_sha_ideal_values_at_point, - build_sha_linear_residual_coeff_cache, check_fresh_sha_ideal_cache, check_sha_ideal_values, - derive_sha_instance_fold_claim, evaluate_fresh_sha_targets, expression_folded_row_sum, - fold_projected_sha_traces, folded_row_integrand_sum, folded_row_integrand_values, + FoldedCommitments, FreshIdealEvaluationCache, InstanceFoldClaim, LinearResidualCoeffTable, + MleColumn, MleTable, NUM_NONZERO_SHA_FAMILIES, NUM_SHA_RESIDUAL_FAMILIES, ProjectedPublic, + ProjectedTrace, ProjectionFoldAccumulator, ProjectionFoldWitness, SHA_ROW_COUNT, SHA_ROW_VARS, + SHA_WORD_BITS, ShaBooleanitySource, ShaIntCol, ShaProductionIdeal, ShaProjectionError, + ShaPublicCol, ShaPublicWordCol, ShaResidualFamily, ShaWordCol, VirtualChMajValues, + beta_aggregate_nonzero_ideal_polys, beta_aggregate_nonzero_ideal_polys_with_weights, + bit_slice_index, build_dense_sha_sumfold_group, build_dense_sha_sumfold_group_with_weights, + build_expression_folded_row_sumcheck_group, + build_expression_folded_row_sumcheck_group_with_row_weights, build_folded_row_sumcheck_group, + build_fresh_sha_ideal_cache, build_linear_residual_coeff_tables, + build_linear_residual_coeff_tables_with_row_weights, build_production_sha_sumfold_group, + build_production_sha_sumfold_group_owned, build_production_sha_sumfold_group_with_linear_cache, + build_production_sha_sumfold_group_with_linear_cache_and_weights, + build_sha_ideal_values_at_point, check_fresh_sha_ideal_cache, check_sha_ideal_values, + derive_instance_fold_claim, evaluate_fresh_sha_targets, expression_folded_row_sum, + expression_folded_row_sum_with_row_weights, fold_projected_traces, folded_row_integrand_sum, + folded_row_integrand_values, folded_row_integrand_values_with_row_weights, production_sha_booleanity_sources, production_sha_nonzero_families, - production_sha_nonzero_ideals, reconstruct_virtual_ch_maj_at_row, scalarize_trace_words, - sha_int_at_point, sha_linear_residual_row_value, sha_linear_residual_sum, sha_public_at_point, - sha_scalarized_word_at_point, sha_word_bits_at_point, validate_fresh_sha_ideal_polys_canonical, - verify_folded_row_sumcheck_claim, verify_folded_scalarization_links, - verify_folded_scalarization_links_at_point, verify_folded_shifted_scalarization_link_at_point, - verify_fresh_sha_ideal_polys, + production_sha_nonzero_ideals, reconstruct_virtual_ch_maj_at_row, scalarize_bit_slices, + sha_int_at_point, sha_int_at_point_with_weights, sha_linear_residual_row_value, + sha_linear_residual_sum, sha_public_at_point, sha_public_at_point_with_weights, + sha_scalarized_word_at_point, sha_scalarized_word_at_point_with_weights, + sha_word_bits_at_point, sha_word_bits_at_point_with_weights, + validate_fresh_sha_ideal_polys_canonical, verify_folded_row_sumcheck_claim, + verify_folded_scalarization_links, verify_folded_scalarization_links_at_point, + verify_folded_shifted_scalarization_link_at_point, verify_fresh_sha_ideal_polys, }; pub use sumfold::{LinearInstanceClaims, LinearPrefixTable, SumFoldError}; diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index deb521f0..f3e55f43 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -40,6 +40,9 @@ pub const NUM_SHA_RESIDUAL_FAMILIES: usize = 18; pub const NUM_NONZERO_SHA_FAMILIES: usize = 7; const SHA_RESIDUAL_EVAL_POWER_COUNT: usize = 62; +pub type MleColumn = DenseMultilinearExtension; +pub type MleTable = Vec>; + const NONZERO_SHA_FAMILIES: [ShaResidualFamily; NUM_NONZERO_SHA_FAMILIES] = [ ShaResidualFamily::R0BigSigmaA, ShaResidualFamily::R1BigSigmaE, @@ -287,52 +290,27 @@ impl ShaPublicCol { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ShaBitSliceColumns { - /// Indexed as `[word_col][row][bit]`. - pub columns: Vec>>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ShaScalarizedRows { +pub struct ProjectedTrace { + /// Flattened as `[word_col * SHA_WORD_BITS + bit][row]`. + pub bit_slices: MleTable, /// Indexed as `[word_col][row]`. - pub words: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ShaIntColumns { + pub scalarized: MleTable, /// Indexed as `[int_col][row]`. - pub columns: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ShaPublicColumns { + pub int_columns: MleTable, /// Indexed as `[public_col][row]`. - pub columns: Vec>, + pub public_columns: MleTable, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ShaPublicWordColumns { - /// Indexed as `[public_word_col][row][bit]`. - pub columns: Vec>>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ProjectedShaTrace { - pub rows: usize, - pub bit_slices: ShaBitSliceColumns, - pub scalarized_words: ShaScalarizedRows, - pub int_columns: ShaIntColumns, - pub public_columns: ShaPublicColumns, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ProjectedShaPublic { - pub columns: ShaPublicColumns, - pub word_columns: Option>, +pub struct ProjectedPublic { + /// Indexed as `[public_col][row]`. + pub columns: MleTable, + /// Flattened as `[public_word_col * SHA_WORD_BITS + bit][row]`. + pub bit_slices: Option>, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct FreshShaIdealCache { +pub struct FreshIdealEvaluationCache { pub r_ic: [F; SHA_ROW_VARS], pub ideal_polys: Vec<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]>, pub taus_at_a: Vec<[F; NUM_NONZERO_SHA_FAMILIES]>, @@ -340,96 +318,114 @@ pub struct FreshShaIdealCache { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ShaLinearResidualCoeffCache { - /// Logical layout is `[family][instance][degree_slot]`. - coeffs_by_instance: Vec<[DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES]>, +pub struct LinearResidualCoeffTable { + /// Indexed by residual family. + pub coeffs: Vec>, } -impl ShaLinearResidualCoeffCache +impl LinearResidualCoeffTable where F: PrimeField, { - pub fn instance_count(&self) -> usize { - self.coeffs_by_instance.len() + pub fn coeffs_for_family(&self, family: ShaResidualFamily) -> Option<&DynamicPolynomialF> { + self.coeffs.get(family.index()) } +} - pub fn coeffs_for_instance( - &self, - instance: usize, - ) -> Option<&[DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES]> { - self.coeffs_by_instance.get(instance) - } +pub fn beta_aggregate_nonzero_ideal_polys( + tables: &[LinearResidualCoeffTable], + beta: &[F], + field_cfg: &F::Config, +) -> Result<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], ShaProjectionError> +where + F: PrimeField, +{ + let weights = build_eq_x_r_vec(beta, field_cfg)?; + beta_aggregate_nonzero_ideal_polys_with_weights(tables, &weights) +} - pub fn beta_aggregate_nonzero_ideal_polys( - &self, - beta: &[F], - field_cfg: &F::Config, - ) -> Result<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], ShaProjectionError> { - let weights = build_eq_x_r_vec(beta, field_cfg)?; - if weights.len() != self.coeffs_by_instance.len() { - return Err(ShaProjectionError::InstanceCountMismatch { - got: weights.len(), - expected: self.coeffs_by_instance.len(), - }); - } +pub fn beta_aggregate_nonzero_ideal_polys_with_weights( + tables: &[LinearResidualCoeffTable], + beta_eq_weights: &[F], +) -> Result<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], ShaProjectionError> +where + F: PrimeField, +{ + if beta_eq_weights.len() != tables.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: beta_eq_weights.len(), + expected: tables.len(), + }); + } - let mut aggregate: [DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES] = - std::array::from_fn(|_| DynamicPolynomialF::ZERO); - for (weight, instance) in weights.iter().zip(&self.coeffs_by_instance) { - for (slot, family) in NONZERO_SHA_FAMILIES.iter().enumerate() { - let weighted = scale_poly(&instance[family.index()], weight); - aggregate[slot] += &weighted; - } + let mut aggregate: [DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES] = + std::array::from_fn(|_| DynamicPolynomialF::ZERO); + for (weight, table) in beta_eq_weights.iter().zip(tables) { + for (slot, family) in NONZERO_SHA_FAMILIES.iter().enumerate() { + let residual = + table + .coeffs + .get(family.index()) + .ok_or(ShaProjectionError::MissingColumn { + kind: "linear_residual_coeffs", + col: family.index(), + })?; + let weighted = scale_poly(residual, weight); + aggregate[slot] += &weighted; } - aggregate.iter_mut().for_each(DynamicPolynomialF::trim); - Ok(aggregate) } + aggregate.iter_mut().for_each(DynamicPolynomialF::trim); + Ok(aggregate) } -impl ShaLinearResidualCoeffCache +fn linear_values_at_a_lambda( + tables: &[LinearResidualCoeffTable], + a: &F, + lambda: &F, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> where F: DelayedFieldProductSum, { - pub fn linear_values_at_a_lambda( - &self, - a: &F, - lambda: &F, - field_cfg: &F::Config, - ) -> Result, ShaProjectionError> { - let lambda_powers = powers( - lambda.clone(), - F::one_with_cfg(field_cfg), - NUM_SHA_RESIDUAL_FAMILIES, - ); - let a_powers = powers( - a.clone(), - F::one_with_cfg(field_cfg), - SHA_RESIDUAL_EVAL_POWER_COUNT, - ); + let lambda_powers = powers( + lambda.clone(), + F::one_with_cfg(field_cfg), + NUM_SHA_RESIDUAL_FAMILIES, + ); + let a_powers = powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_RESIDUAL_EVAL_POWER_COUNT, + ); - self.coeffs_by_instance - .iter() - .map(|instance| { - let mut target = F::zero_with_cfg(field_cfg); - for (family_idx, residual) in instance.iter().enumerate() { - target += lambda_powers[family_idx].clone() - * evaluate_poly_at_powers_dmr(residual, &a_powers, field_cfg)?; - } - Ok(target) - }) - .collect() - } + tables + .iter() + .map(|table| { + if table.coeffs.len() != NUM_SHA_RESIDUAL_FAMILIES { + return Err(ShaProjectionError::MissingColumn { + kind: "linear_residual_coeffs", + col: table.coeffs.len(), + }); + } + let mut target = F::zero_with_cfg(field_cfg); + for (family_idx, residual) in table.coeffs.iter().enumerate() { + target += lambda_powers[family_idx].clone() + * evaluate_poly_at_powers_dmr(residual, &a_powers, field_cfg)?; + } + Ok(target) + }) + .collect() } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ShaSumFoldOutput { - r_b: Vec, - c_sf: F, - t_prime: F, - eq_instance_weights: Vec, +pub struct InstanceFoldClaim { + pub r_b: Vec, + pub c_sf: F, + pub final_round_sumcheck_claim: F, + pub eq_instance_weights: Vec, } -impl ShaSumFoldOutput { +impl InstanceFoldClaim { pub fn r_b(&self) -> &[F] { &self.r_b } @@ -438,8 +434,8 @@ impl ShaSumFoldOutput { &self.c_sf } - pub fn t_prime(&self) -> &F { - &self.t_prime + pub fn final_round_sumcheck_claim(&self) -> &F { + &self.final_round_sumcheck_claim } pub fn eq_instance_weights(&self) -> &[F] { @@ -453,16 +449,15 @@ pub struct FoldedCommitments { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct FoldedShaAccumulator { - pub t_prime: F, - pub folded_commitments: FoldedCommitments, - pub folded_public: ProjectedShaPublic, - pub r_b: Vec, +pub struct ProjectionFoldAccumulator { + pub instance_fold_claim: InstanceFoldClaim, + pub commitments: FoldedCommitments, + pub public: ProjectedPublic, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct FoldedShaWitness { - pub trace: ProjectedShaTrace, +pub struct ProjectionFoldWitness { + pub trace: ProjectedTrace, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -639,8 +634,8 @@ where } pub fn build_sha_ideal_values_at_point( - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, r_ic: &[F; SHA_ROW_VARS], field_cfg: &F::Config, ) -> Result<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], ShaProjectionError> @@ -676,12 +671,63 @@ where batched_ideal_check(&ideals, values).map_err(|_err| ShaProjectionError::IdealMembership) } +#[allow(clippy::arithmetic_side_effects)] +pub fn bit_slice_index(col: usize, bit: usize, bits_per_col: usize) -> usize { + col * bits_per_col + bit +} + +fn mle_table_from_columns(columns: Vec>, num_vars: usize) -> MleTable { + columns + .into_iter() + .map(|evaluations| DenseMultilinearExtension { + evaluations, + num_vars, + }) + .collect() +} + +#[cfg(test)] +fn flatten_bit_columns( + columns: Vec>>, + bits_per_col: usize, + num_vars: usize, + kind: &'static str, +) -> Result, ShaProjectionError> { + let mut flattened = (0..columns.len() * bits_per_col) + .map(|_| Vec::new()) + .collect::>>(); + for (col_idx, rows) in columns.into_iter().enumerate() { + if rows.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind, + col: col_idx, + got: rows.len(), + expected: SHA_ROW_COUNT, + }); + } + for (row, bits) in rows.into_iter().enumerate() { + if bits.len() != bits_per_col { + return Err(ShaProjectionError::BitCount { + col: col_idx, + row, + got: bits.len(), + expected: bits_per_col, + }); + } + for (bit, value) in bits.into_iter().enumerate() { + flattened[bit_slice_index(col_idx, bit, bits_per_col)].push(value); + } + } + } + Ok(mle_table_from_columns(flattened, num_vars)) +} + pub fn build_fresh_sha_ideal_cache( - traces: &[ProjectedShaTrace], - publics: &[ProjectedShaPublic], + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], r_ic: [F; SHA_ROW_VARS], field_cfg: &F::Config, -) -> Result, ShaProjectionError> +) -> Result, ShaProjectionError> where F: PrimeField, { @@ -697,7 +743,7 @@ where .map(|(trace, public)| build_sha_ideal_values_at_point(trace, public, &r_ic, field_cfg)) .collect::, _>>()?; - Ok(FreshShaIdealCache { + Ok(FreshIdealEvaluationCache { r_ic, ideal_polys, taus_at_a: Vec::new(), @@ -705,12 +751,25 @@ where }) } -pub fn build_sha_linear_residual_coeff_cache( - traces: &[ProjectedShaTrace], - publics: &[ProjectedShaPublic], +pub fn build_linear_residual_coeff_tables( + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], r_ic: &[F; SHA_ROW_VARS], field_cfg: &F::Config, -) -> Result, ShaProjectionError> +) -> Result>, ShaProjectionError> +where + F: PrimeField, +{ + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + build_linear_residual_coeff_tables_with_row_weights(traces, publics, &row_weights, field_cfg) +} + +pub fn build_linear_residual_coeff_tables_with_row_weights( + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], + row_weights: &[F], + field_cfg: &F::Config, +) -> Result>, ShaProjectionError> where F: PrimeField, { @@ -720,15 +779,21 @@ where expected: traces.len(), }); } - let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; - let coeffs_by_instance = traces + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } + traces .iter() .zip(publics) .map(|(trace, public)| { validate_trace(trace)?; validate_public(public)?; - let mut coeffs: [DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES] = - std::array::from_fn(|_| DynamicPolynomialF::ZERO); + let mut coeffs = vec![DynamicPolynomialF::ZERO; NUM_SHA_RESIDUAL_FAMILIES]; for (row, row_weight) in row_weights.iter().enumerate().take(SHA_ROW_COUNT) { let residuals = residual_polys_at_row(trace, public, row, field_cfg)?; for (family_idx, residual) in residuals.iter().enumerate() { @@ -737,14 +802,13 @@ where } } coeffs.iter_mut().for_each(DynamicPolynomialF::trim); - Ok::<[DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES], ShaProjectionError>(coeffs) + Ok(LinearResidualCoeffTable { coeffs }) }) - .collect::, _>>()?; - Ok(ShaLinearResidualCoeffCache { coeffs_by_instance }) + .collect::, _>>() } pub fn check_fresh_sha_ideal_cache( - cache: &FreshShaIdealCache, + cache: &FreshIdealEvaluationCache, field_cfg: &F::Config, ) -> Result<(), ShaProjectionError> where @@ -754,7 +818,7 @@ where } pub fn evaluate_fresh_sha_targets( - cache: &mut FreshShaIdealCache, + cache: &mut FreshIdealEvaluationCache, a: &F, lambda: &F, field_cfg: &F::Config, @@ -792,13 +856,13 @@ where Ok(()) } -pub fn derive_sha_instance_fold_claim( +pub fn derive_instance_fold_claim( beta: &[F], r_b: Vec, c_sf: F, instance_count: usize, field_cfg: &F::Config, -) -> Result, ShaProjectionError> +) -> Result, ShaProjectionError> where F: PrimeField, { @@ -829,21 +893,21 @@ where let eq_instance_weights = build_eq_x_r_vec(&r_b, field_cfg)?; debug_assert_eq!(eq_instance_weights.len(), instance_count); - let t_prime = c_sf.clone() / d; - Ok(ShaSumFoldOutput { + let final_round_sumcheck_claim = c_sf.clone() / d; + Ok(InstanceFoldClaim { r_b, c_sf, - t_prime, + final_round_sumcheck_claim, eq_instance_weights, }) } -pub fn fold_projected_sha_traces( - traces: &[ProjectedShaTrace], - publics: &[ProjectedShaPublic], - sumfold: &ShaSumFoldOutput, +pub fn fold_projected_traces( + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], + sumfold: &InstanceFoldClaim, field_cfg: &F::Config, -) -> Result<(FoldedShaWitness, ProjectedShaPublic), ShaProjectionError> +) -> Result<(ProjectionFoldWitness, ProjectedPublic), ShaProjectionError> where F: PrimeField, { @@ -866,101 +930,96 @@ where validate_public(public)?; } - let folded_trace = ProjectedShaTrace { - rows: SHA_ROW_COUNT, - bit_slices: ShaBitSliceColumns { - columns: fold_3d( - traces.iter().map(|trace| &trace.bit_slices.columns), - &sumfold.eq_instance_weights, - field_cfg, - )?, - }, - scalarized_words: ShaScalarizedRows { - words: fold_2d( - traces.iter().map(|trace| &trace.scalarized_words.words), - &sumfold.eq_instance_weights, - field_cfg, - )?, - }, - int_columns: ShaIntColumns { - columns: fold_2d( - traces.iter().map(|trace| &trace.int_columns.columns), - &sumfold.eq_instance_weights, - field_cfg, - )?, - }, - public_columns: ShaPublicColumns { - columns: fold_2d( - traces.iter().map(|trace| &trace.public_columns.columns), - &sumfold.eq_instance_weights, - field_cfg, - )?, - }, + let folded_trace = ProjectedTrace { + bit_slices: fold_mle_tables( + "bit_slices", + traces.iter().map(|trace| &trace.bit_slices), + &sumfold.eq_instance_weights, + field_cfg, + )?, + scalarized: fold_mle_tables( + "scalarized", + traces.iter().map(|trace| &trace.scalarized), + &sumfold.eq_instance_weights, + field_cfg, + )?, + int_columns: fold_mle_tables( + "int_columns", + traces.iter().map(|trace| &trace.int_columns), + &sumfold.eq_instance_weights, + field_cfg, + )?, + public_columns: fold_mle_tables( + "public_columns", + traces.iter().map(|trace| &trace.public_columns), + &sumfold.eq_instance_weights, + field_cfg, + )?, }; - let folded_public = ProjectedShaPublic { - columns: ShaPublicColumns { - columns: fold_2d( - publics.iter().map(|public| &public.columns.columns), - &sumfold.eq_instance_weights, - field_cfg, - )?, - }, - word_columns: fold_optional_3d( - publics.iter().map(|public| public.word_columns.as_ref()), + let folded_public = ProjectedPublic { + columns: fold_mle_tables( + "public.columns", + publics.iter().map(|public| &public.columns), &sumfold.eq_instance_weights, field_cfg, - )? - .map(|columns| ShaPublicWordColumns { columns }), + )?, + bit_slices: fold_optional_mle_tables( + "public.bit_slices", + publics.iter().map(|public| public.bit_slices.as_ref()), + &sumfold.eq_instance_weights, + field_cfg, + )?, }; Ok(( - FoldedShaWitness { + ProjectionFoldWitness { trace: folded_trace, }, folded_public, )) } -pub fn scalarize_trace_words( - bit_slices: &ShaBitSliceColumns, +pub fn scalarize_bit_slices( + bit_slices: &MleTable, a: &F, field_cfg: &F::Config, -) -> Result, ShaProjectionError> +) -> Result, ShaProjectionError> where - F: MontgomeryLimbs + DelayedFieldProductSum + Send + Sync, + F: PrimeField + MontgomeryLimbs + DelayedFieldProductSum + Send + Sync, { let powers = powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); - let mut words = Vec::with_capacity(bit_slices.columns.len()); - for (col_idx, col) in bit_slices.columns.iter().enumerate() { - if col.len() != SHA_ROW_COUNT { - return Err(ShaProjectionError::ColumnRowCount { - kind: "bit_slices", - col: col_idx, - got: col.len(), - expected: SHA_ROW_COUNT, - }); - } + if bit_slices.len() % SHA_WORD_BITS != 0 { + return Err(ShaProjectionError::MissingColumn { + kind: "bit_slices", + col: bit_slices.len(), + }); + } + let word_col_count = bit_slices.len() / SHA_WORD_BITS; + let mut words = Vec::with_capacity(word_col_count); + for col_idx in 0..word_col_count { let mut out_col = Vec::with_capacity(SHA_ROW_COUNT); - for (row, bits) in col.iter().enumerate() { - if bits.len() != SHA_WORD_BITS { - return Err(ShaProjectionError::BitCount { - col: col_idx, + for row in 0..SHA_ROW_COUNT { + let mut bits = Vec::with_capacity(SHA_WORD_BITS); + for bit in 0..SHA_WORD_BITS { + bits.push(scalar_from_table( + "bit_slices", + bit_slices, + bit_slice_index(col_idx, bit, SHA_WORD_BITS), row, - got: bits.len(), - expected: SHA_WORD_BITS, - }); + field_cfg, + )?); } out_col.push(project_binary_bits_conditional_add_dmr( - bits, &powers, field_cfg, + &bits, &powers, field_cfg, )?); } words.push(out_col); } - Ok(ShaScalarizedRows { words }) + Ok(mle_table_from_columns(words, SHA_ROW_VARS)) } pub fn verify_folded_scalarization_links( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, a: &F, word_cols: &[ShaWordCol], field_cfg: &F::Config, @@ -972,27 +1031,21 @@ where let powers = powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); for col in word_cols { let col_idx = col.index(); - let bit_col = - trace - .bit_slices - .columns - .get(col_idx) - .ok_or(ShaProjectionError::MissingColumn { - kind: "bit_slices", - col: col_idx, - })?; - let scalar_col = - trace - .scalarized_words - .words - .get(col_idx) - .ok_or(ShaProjectionError::MissingColumn { - kind: "scalarized_words", - col: col_idx, - })?; for row in 0..SHA_ROW_COUNT { - let recombined = project_bits_dmr(&bit_col[row], &powers, field_cfg)?; - if recombined != scalar_col[row] { + let mut bits = Vec::with_capacity(SHA_WORD_BITS); + for bit in 0..SHA_WORD_BITS { + bits.push(scalar_from_table( + "bit_slices", + &trace.bit_slices, + bit_slice_index(col_idx, bit, SHA_WORD_BITS), + row, + field_cfg, + )?); + } + let recombined = project_bits_dmr(&bits, &powers, field_cfg)?; + let scalar = + scalar_from_table("scalarized", &trace.scalarized, col_idx, row, field_cfg)?; + if recombined != scalar { return Err(ShaProjectionError::ScalarizationMismatch { col: col_idx }); } } @@ -1001,7 +1054,7 @@ where } pub fn verify_folded_scalarization_links_at_point( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, a: &F, r_star: &[F; SHA_ROW_VARS], word_cols: &[ShaWordCol], @@ -1017,7 +1070,7 @@ where } pub fn verify_folded_shifted_scalarization_link_at_point( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, a: &F, r_star: &[F; SHA_ROW_VARS], col: ShaWordCol, @@ -1057,7 +1110,7 @@ where } pub fn reconstruct_virtual_ch_maj_at_row( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, row: usize, field_cfg: &F::Config, ) -> Result, ShaProjectionError> @@ -1069,7 +1122,7 @@ where } fn reconstruct_virtual_ch_maj_at_row_unchecked( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, row: usize, field_cfg: &F::Config, ) -> Result, ShaProjectionError> @@ -1118,8 +1171,8 @@ where } pub fn folded_row_integrand_values( - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, r_ic: &[F; SHA_ROW_VARS], a: &F, lambda: &F, @@ -1135,6 +1188,45 @@ where validate_trace(trace)?; validate_public(public)?; let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + folded_row_integrand_values_with_row_weights( + trace, + public, + &row_weights, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn folded_row_integrand_values_with_row_weights( + trace: &ProjectedTrace, + public: &ProjectedPublic, + row_weights: &[F], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum, + F::Inner: Zero, +{ + validate_trace(trace)?; + validate_public(public)?; + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let lambda_powers = powers( lambda.clone(), F::one_with_cfg(field_cfg), @@ -1262,8 +1354,8 @@ pub fn production_sha_booleanity_sources() -> Vec { /// Evaluate the linear SHA residual scalarization at one row. pub fn sha_linear_residual_row_value( - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, row: usize, a: &F, lambda: &F, @@ -1295,8 +1387,8 @@ where /// Evaluate the row-weighted linear SHA residual scalarization for one /// instance. pub fn sha_linear_residual_sum( - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, r_ic: &[F; SHA_ROW_VARS], a: &F, lambda: &F, @@ -1329,8 +1421,8 @@ where } fn sha_linear_residual_sum_with_weights( - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, row_weights: &[F], a_powers: &[F], lambda_powers: &[F], @@ -1355,8 +1447,8 @@ where } fn sha_linear_residual_row_value_with_powers( - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, row: usize, a_powers: &[F], lambda_powers: &[F], @@ -1496,8 +1588,8 @@ where } } -struct ShaSumFoldPrefixFastPath { - traces: Box<[ProjectedShaTrace]>, +struct RelationSumFoldPrefixFastPath { + traces: Box<[ProjectedTrace]>, beta: Vec, xi: F, booleanity_sources: Vec, @@ -1517,15 +1609,15 @@ struct TernaryCoeffPlan { vertices: Vec<(usize, bool)>, } -impl ShaSumFoldPrefixFastPath +impl RelationSumFoldPrefixFastPath where F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: Zero, { #[allow(clippy::too_many_arguments)] fn new( - traces: &[ProjectedShaTrace], - publics: &[ProjectedShaPublic], + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], beta: &[F], r_ic: &[F; SHA_ROW_VARS], a: &F, @@ -1553,8 +1645,8 @@ where #[allow(clippy::too_many_arguments)] fn new_owned( - traces: Box<[ProjectedShaTrace]>, - publics: &[ProjectedShaPublic], + traces: Box<[ProjectedTrace]>, + publics: &[ProjectedPublic], beta: &[F], r_ic: &[F; SHA_ROW_VARS], a: &F, @@ -1565,36 +1657,39 @@ where prefix_vars: usize, field_cfg: &F::Config, ) -> Result { - let coeff_cache = build_sha_linear_residual_coeff_cache(&traces, publics, r_ic, field_cfg)?; + let coeff_tables = build_linear_residual_coeff_tables(&traces, publics, r_ic, field_cfg)?; + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; Self::new_owned_with_linear_cache( traces, publics, beta, r_ic, + &row_weights, a, lambda, rho, xi, booleanity_sources, prefix_vars, - &coeff_cache, + &coeff_tables, field_cfg, ) } #[allow(clippy::too_many_arguments)] fn new_owned_with_linear_cache( - traces: Box<[ProjectedShaTrace]>, - publics: &[ProjectedShaPublic], + traces: Box<[ProjectedTrace]>, + publics: &[ProjectedPublic], beta: &[F], - r_ic: &[F; SHA_ROW_VARS], + _r_ic: &[F; SHA_ROW_VARS], + row_weights: &[F], a: &F, lambda: &F, rho: &F, xi: &F, booleanity_sources: &[ShaBooleanitySource], prefix_vars: usize, - coeff_cache: &ShaLinearResidualCoeffCache, + coeff_tables: &[LinearResidualCoeffTable], field_cfg: &F::Config, ) -> Result { let ell = validate_sha_sumfold_inputs(&traces, publics, beta)?; @@ -1605,23 +1700,30 @@ where } .into()); } - if coeff_cache.instance_count() != traces.len() { + if coeff_tables.len() != traces.len() { return Err(ShaProjectionError::InstanceCountMismatch { - got: coeff_cache.instance_count(), + got: coeff_tables.len(), expected: traces.len(), }); } + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let tail_vars = ell - prefix_vars; let tail_len = binary_len(tail_vars); - let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; let rho_powers = powers( rho.clone(), F::one_with_cfg(field_cfg), booleanity_sources.len(), ); - let linear_values = coeff_cache.linear_values_at_a_lambda(a, lambda, field_cfg)?; + let linear_values = linear_values_at_a_lambda(coeff_tables, a, lambda, field_cfg)?; let linear = BinaryPrefixTailTable::new(linear_values, prefix_vars, tail_len); let booleanity = TernaryPrefixTailTable::new( build_sha_booleanity_prefix_tail_table( @@ -1762,7 +1864,7 @@ where } } -impl PrefixFastPath for ShaSumFoldPrefixFastPath +impl PrefixFastPath for RelationSumFoldPrefixFastPath where F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: Zero, @@ -1834,8 +1936,8 @@ where /// is the folded booleanity expression. #[allow(clippy::too_many_arguments)] pub fn build_dense_sha_sumfold_group( - traces: &[ProjectedShaTrace], - publics: &[ProjectedShaPublic], + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], beta: &[F], r_ic: &[F; SHA_ROW_VARS], a: &F, @@ -1849,12 +1951,59 @@ where F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: Zero, { - let ell = validate_sha_sumfold_inputs(traces, publics, beta)?; + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + let beta_eq_weights = build_eq_x_r_vec(beta, field_cfg)?; + build_dense_sha_sumfold_group_with_weights( + traces, + publics, + beta, + &beta_eq_weights, + &row_weights, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + ) +} +#[allow(clippy::too_many_arguments)] +pub fn build_dense_sha_sumfold_group_with_weights( + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], + beta: &[F], + beta_eq_weights: &[F], + row_weights: &[F], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + let ell = validate_sha_sumfold_inputs(traces, publics, beta)?; + if beta_eq_weights.len() != traces.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: beta_eq_weights.len(), + expected: traces.len(), + }); + } + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let zero = F::zero_with_cfg(field_cfg); let zero_inner = zero.inner().clone(); let mut mles = Vec::with_capacity(2 + booleanity_sources.len() * SHA_ROW_COUNT); - let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; let lambda_powers = powers( lambda.clone(), F::one_with_cfg(field_cfg), @@ -1866,10 +2015,12 @@ where SHA_RESIDUAL_EVAL_POWER_COUNT, ); - let eq_beta = build_eq_x_r_vec(beta, field_cfg)?; mles.push(DenseMultilinearExtension::from_evaluations_vec( ell, - eq_beta.iter().map(|value| value.inner().clone()).collect(), + beta_eq_weights + .iter() + .map(|value| value.inner().clone()) + .collect(), zero_inner.clone(), )); @@ -1914,7 +2065,7 @@ where 3, mles, sha_sumfold_comb_fn( - row_weights, + row_weights.to_vec(), powers( rho.clone(), F::one_with_cfg(field_cfg), @@ -1928,8 +2079,8 @@ where #[allow(clippy::too_many_arguments)] pub fn build_production_sha_sumfold_group( - traces: &[ProjectedShaTrace], - publics: &[ProjectedShaPublic], + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], beta: &[F], r_ic: &[F; SHA_ROW_VARS], a: &F, @@ -1967,7 +2118,7 @@ where ); } - let fast_path = ShaSumFoldPrefixFastPath::new( + let fast_path = RelationSumFoldPrefixFastPath::new( traces, publics, beta, @@ -2000,9 +2151,9 @@ where #[allow(clippy::too_many_arguments)] pub fn build_production_sha_sumfold_group_with_linear_cache( - traces: &[ProjectedShaTrace], - publics: &[ProjectedShaPublic], - linear_cache: &ShaLinearResidualCoeffCache, + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], + linear_cache: &[LinearResidualCoeffTable], beta: &[F], r_ic: &[F; SHA_ROW_VARS], a: &F, @@ -2013,17 +2164,72 @@ pub fn build_production_sha_sumfold_group_with_linear_cache( prefix_vars: usize, field_cfg: &F::Config, ) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + let beta_eq_weights = build_eq_x_r_vec(beta, field_cfg)?; + build_production_sha_sumfold_group_with_linear_cache_and_weights( + traces, + publics, + linear_cache, + beta, + &beta_eq_weights, + r_ic, + &row_weights, + a, + lambda, + rho, + xi, + booleanity_sources, + prefix_vars, + field_cfg, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn build_production_sha_sumfold_group_with_linear_cache_and_weights( + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], + linear_cache: &[LinearResidualCoeffTable], + beta: &[F], + beta_eq_weights: &[F], + r_ic: &[F; SHA_ROW_VARS], + row_weights: &[F], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> where F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: Zero, { let ell = validate_sha_sumfold_inputs(traces, publics, beta)?; - if linear_cache.instance_count() != traces.len() { + if linear_cache.len() != traces.len() { return Err(ShaProjectionError::InstanceCountMismatch { - got: linear_cache.instance_count(), + got: linear_cache.len(), expected: traces.len(), }); } + if beta_eq_weights.len() != traces.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: beta_eq_weights.len(), + expected: traces.len(), + }); + } + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } if prefix_vars > ell { return Err(SumFoldError::Ell0TooLarge { ell0: prefix_vars, @@ -2032,11 +2238,12 @@ where .into()); } if prefix_vars == 0 { - return build_dense_sha_sumfold_group_with_linear_cache( + return build_dense_sha_sumfold_group_with_linear_cache_and_weights( traces, linear_cache, beta, - r_ic, + beta_eq_weights, + row_weights, a, lambda, rho, @@ -2046,11 +2253,12 @@ where ); } - let fast_path = ShaSumFoldPrefixFastPath::new_owned_with_linear_cache( + let fast_path = RelationSumFoldPrefixFastPath::new_owned_with_linear_cache( traces.to_vec().into_boxed_slice(), publics, beta, r_ic, + row_weights, a, lambda, rho, @@ -2065,7 +2273,7 @@ where 3, Vec::new(), sha_sumfold_comb_fn( - build_eq_x_r_vec(r_ic, field_cfg)?, + row_weights.to_vec(), powers( rho.clone(), F::one_with_cfg(field_cfg), @@ -2079,11 +2287,12 @@ where } #[allow(clippy::too_many_arguments)] -fn build_dense_sha_sumfold_group_with_linear_cache( - traces: &[ProjectedShaTrace], - linear_cache: &ShaLinearResidualCoeffCache, +fn build_dense_sha_sumfold_group_with_linear_cache_and_weights( + traces: &[ProjectedTrace], + linear_cache: &[LinearResidualCoeffTable], beta: &[F], - r_ic: &[F; SHA_ROW_VARS], + beta_eq_weights: &[F], + row_weights: &[F], a: &F, lambda: &F, rho: &F, @@ -2096,19 +2305,34 @@ where F::Inner: Zero, { let ell = beta.len(); + if beta_eq_weights.len() != traces.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: beta_eq_weights.len(), + expected: traces.len(), + }); + } + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let zero = F::zero_with_cfg(field_cfg); let zero_inner = zero.inner().clone(); let mut mles = Vec::with_capacity(2 + booleanity_sources.len() * SHA_ROW_COUNT); - let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; - let eq_beta = build_eq_x_r_vec(beta, field_cfg)?; mles.push(DenseMultilinearExtension::from_evaluations_vec( ell, - eq_beta.iter().map(|value| value.inner().clone()).collect(), + beta_eq_weights + .iter() + .map(|value| value.inner().clone()) + .collect(), zero_inner.clone(), )); - let linear_values = linear_cache.linear_values_at_a_lambda(a, lambda, field_cfg)?; + let linear_values = linear_values_at_a_lambda(linear_cache, a, lambda, field_cfg)?; mles.push(DenseMultilinearExtension::from_evaluations_vec( ell, linear_values @@ -2136,7 +2360,7 @@ where 3, mles, sha_sumfold_comb_fn( - row_weights, + row_weights.to_vec(), powers( rho.clone(), F::one_with_cfg(field_cfg), @@ -2150,8 +2374,8 @@ where #[allow(clippy::too_many_arguments)] pub fn build_production_sha_sumfold_group_owned( - traces: Box<[ProjectedShaTrace]>, - publics: &[ProjectedShaPublic], + traces: Box<[ProjectedTrace]>, + publics: &[ProjectedPublic], beta: &[F], r_ic: &[F; SHA_ROW_VARS], a: &F, @@ -2189,7 +2413,7 @@ where ); } - let fast_path = ShaSumFoldPrefixFastPath::new_owned( + let fast_path = RelationSumFoldPrefixFastPath::new_owned( traces, publics, beta, @@ -2249,8 +2473,8 @@ where } fn validate_sha_sumfold_inputs( - traces: &[ProjectedShaTrace], - publics: &[ProjectedShaPublic], + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], beta: &[F], ) -> Result { if traces.is_empty() { @@ -2318,7 +2542,7 @@ where #[allow(clippy::arithmetic_side_effects)] fn build_sha_booleanity_prefix_tail_table( - traces: &[ProjectedShaTrace], + traces: &[ProjectedTrace], booleanity_sources: &[ShaBooleanitySource], prefix_vars: usize, tail_len: usize, @@ -2373,7 +2597,7 @@ where #[allow(clippy::arithmetic_side_effects)] fn bind_sha_booleanity_sources_to_prefix( - traces: &[ProjectedShaTrace], + traces: &[ProjectedTrace], booleanity_sources: &[ShaBooleanitySource], prefix_vars: usize, tail_len: usize, @@ -2416,7 +2640,7 @@ where #[allow(clippy::arithmetic_side_effects)] fn fill_booleanity_source_prefix_values( - traces: &[ProjectedShaTrace], + traces: &[ProjectedTrace], booleanity_sources: &[ShaBooleanitySource], prefix_vars: usize, tail: usize, @@ -2585,8 +2809,8 @@ fn binary_bits_to_ternary_index(mut bits: usize, vars: usize) -> usize { /// precomputed row-integrand values. #[allow(clippy::too_many_arguments)] pub fn build_expression_folded_row_sumcheck_group( - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, r_ic: &[F; SHA_ROW_VARS], a: &F, lambda: &F, @@ -2595,18 +2819,55 @@ pub fn build_expression_folded_row_sumcheck_group( booleanity_sources: &[ShaBooleanitySource], field_cfg: &F::Config, ) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + build_expression_folded_row_sumcheck_group_with_row_weights( + trace, + public, + &row_weights, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn build_expression_folded_row_sumcheck_group_with_row_weights( + trace: &ProjectedTrace, + public: &ProjectedPublic, + row_weights: &[F], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> where F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: Zero, { validate_trace(trace)?; validate_public(public)?; + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let zero = F::zero_with_cfg(field_cfg); let zero_inner = zero.inner().clone(); let mut mles = Vec::with_capacity(2 + booleanity_sources.len()); - let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; mles.push(DenseMultilinearExtension::from_evaluations_vec( SHA_ROW_VARS, row_weights @@ -2685,8 +2946,8 @@ where /// Claimed sum for the expression-backed folded row check. #[allow(clippy::too_many_arguments)] pub fn expression_folded_row_sum( - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, r_ic: &[F; SHA_ROW_VARS], a: &F, lambda: &F, @@ -2713,8 +2974,40 @@ where folded_row_integrand_sum(&values, field_cfg) } +/// Claimed sum for the expression-backed folded row check with precomputed +/// `eq(r_ic, row)` weights. +#[allow(clippy::too_many_arguments)] +pub fn expression_folded_row_sum_with_row_weights( + trace: &ProjectedTrace, + public: &ProjectedPublic, + row_weights: &[F], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result +where + F: InnerTransparentField + DelayedFieldProductSum, + F::Inner: Zero, +{ + let values = folded_row_integrand_values_with_row_weights( + trace, + public, + row_weights, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + )?; + folded_row_integrand_sum(&values, field_cfg) +} + pub fn sha_word_bits_at_point( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, col: ShaWordCol, shift: usize, point: &[F], @@ -2728,6 +3021,28 @@ where } validate_trace(trace)?; let row_weights = build_eq_x_r_vec(point, field_cfg)?; + sha_word_bits_at_point_with_weights(trace, col, shift, &row_weights, field_cfg) +} + +pub fn sha_word_bits_at_point_with_weights( + trace: &ProjectedTrace, + col: ShaWordCol, + shift: usize, + row_weights: &[F], + field_cfg: &F::Config, +) -> Result<[F; SHA_WORD_BITS], ShaProjectionError> +where + F: PrimeField, +{ + validate_trace(trace)?; + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let mut bits: [F; SHA_WORD_BITS] = std::array::from_fn(|_| F::zero_with_cfg(field_cfg)); for (row, row_weight) in row_weights.iter().enumerate() { for (bit, out) in bits.iter_mut().enumerate() { @@ -2739,7 +3054,7 @@ where } pub fn sha_scalarized_word_at_point( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, col: ShaWordCol, shift: usize, point: &[F], @@ -2753,6 +3068,28 @@ where } validate_trace(trace)?; let row_weights = build_eq_x_r_vec(point, field_cfg)?; + sha_scalarized_word_at_point_with_weights(trace, col, shift, &row_weights, field_cfg) +} + +pub fn sha_scalarized_word_at_point_with_weights( + trace: &ProjectedTrace, + col: ShaWordCol, + shift: usize, + row_weights: &[F], + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + validate_trace(trace)?; + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let mut value = F::zero_with_cfg(field_cfg); for (row, row_weight) in row_weights.iter().enumerate() { value += row_weight.clone() @@ -2762,7 +3099,7 @@ where } pub fn sha_int_at_point( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, col: ShaIntCol, point: &[F], field_cfg: &F::Config, @@ -2775,6 +3112,27 @@ where } validate_trace(trace)?; let row_weights = build_eq_x_r_vec(point, field_cfg)?; + sha_int_at_point_with_weights(trace, col, &row_weights, field_cfg) +} + +pub fn sha_int_at_point_with_weights( + trace: &ProjectedTrace, + col: ShaIntCol, + row_weights: &[F], + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + validate_trace(trace)?; + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let mut value = F::zero_with_cfg(field_cfg); for (row, row_weight) in row_weights.iter().enumerate() { value += row_weight.clone() * int_scalar(trace, col, row, field_cfg)?; @@ -2783,7 +3141,7 @@ where } pub fn sha_public_at_point( - public: &ProjectedShaPublic, + public: &ProjectedPublic, col: ShaPublicCol, shift: usize, point: &[F], @@ -2797,6 +3155,28 @@ where } validate_public(public)?; let row_weights = build_eq_x_r_vec(point, field_cfg)?; + sha_public_at_point_with_weights(public, col, shift, &row_weights, field_cfg) +} + +pub fn sha_public_at_point_with_weights( + public: &ProjectedPublic, + col: ShaPublicCol, + shift: usize, + row_weights: &[F], + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ + validate_public(public)?; + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let mut value = F::zero_with_cfg(field_cfg); for (row, row_weight) in row_weights.iter().enumerate() { let shifted = row.checked_add(shift).unwrap_or(SHA_ROW_COUNT); @@ -2807,20 +3187,20 @@ where pub fn verify_folded_row_sumcheck_claim( claimed_sum: &F, - t_prime: &F, + final_round_sumcheck_claim: &F, ) -> Result<(), ShaProjectionError> where F: PrimeField, { - if claimed_sum != t_prime { + if claimed_sum != final_round_sumcheck_claim { return Err(ShaProjectionError::FoldedRowClaimMismatch); } Ok(()) } pub fn residual_polys_at_row( - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, row: usize, field_cfg: &F::Config, ) -> Result<[DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES], ShaProjectionError> @@ -2955,8 +3335,8 @@ where } fn residual_values_at_row_with_powers( - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, row: usize, a_powers: &[F], field_cfg: &F::Config, @@ -2973,111 +3353,55 @@ where Ok(out) } -fn validate_trace(trace: &ProjectedShaTrace) -> Result<(), ShaProjectionError> { - if trace.rows != SHA_ROW_COUNT { - return Err(ShaProjectionError::RowCount { - expected: SHA_ROW_COUNT, - got: trace.rows, - }); - } - validate_bit_columns(&trace.bit_slices)?; - validate_matrix( - "scalarized_words", - &trace.scalarized_words.words, - SHA_ROW_COUNT, +fn validate_trace(trace: &ProjectedTrace) -> Result<(), ShaProjectionError> { + validate_table( + "bit_slices", + &trace.bit_slices, + ShaWordCol::COUNT * SHA_WORD_BITS, )?; - validate_matrix("int_columns", &trace.int_columns.columns, SHA_ROW_COUNT)?; - validate_matrix( - "public_columns", - &trace.public_columns.columns, - SHA_ROW_COUNT, - ) + validate_table("scalarized", &trace.scalarized, ShaWordCol::COUNT)?; + validate_table("int_columns", &trace.int_columns, ShaIntCol::COUNT)?; + validate_table("public_columns", &trace.public_columns, ShaPublicCol::COUNT) } -fn validate_public(public: &ProjectedShaPublic) -> Result<(), ShaProjectionError> { - validate_matrix("public.columns", &public.columns.columns, SHA_ROW_COUNT)?; - if let Some(word_columns) = &public.word_columns { - validate_public_word_columns(word_columns)?; +fn validate_public(public: &ProjectedPublic) -> Result<(), ShaProjectionError> { + validate_table("public.columns", &public.columns, ShaPublicCol::COUNT)?; + if let Some(bit_slices) = &public.bit_slices { + validate_table( + "public.bit_slices", + bit_slices, + ShaPublicWordCol::COUNT * SHA_WORD_BITS, + )?; } Ok(()) } -fn validate_public_word_columns( - word_columns: &ShaPublicWordColumns, +fn validate_table( + kind: &'static str, + columns: &MleTable, + expected_cols: usize, ) -> Result<(), ShaProjectionError> { - if word_columns.columns.len() != ShaPublicWordCol::COUNT { + if columns.len() != expected_cols { return Err(ShaProjectionError::MissingColumn { - kind: "public.word_columns", - col: ShaPublicWordCol::COUNT, + kind, + col: columns.len(), }); } - for (col, rows) in word_columns.columns.iter().enumerate() { - if rows.len() != SHA_ROW_COUNT { - return Err(ShaProjectionError::ColumnRowCount { - kind: "public.word_columns", - col, - got: rows.len(), - expected: SHA_ROW_COUNT, - }); - } - for (row, bits) in rows.iter().enumerate() { - if bits.len() != SHA_WORD_BITS { - return Err(ShaProjectionError::BitCount { - col, - row, - got: bits.len(), - expected: SHA_WORD_BITS, - }); - } - } - } - Ok(()) -} - -fn validate_matrix( - kind: &'static str, - columns: &[Vec], - rows: usize, -) -> Result<(), ShaProjectionError> { for (col, values) in columns.iter().enumerate() { - if values.len() != rows { + if values.num_vars != SHA_ROW_VARS || values.evaluations.len() != SHA_ROW_COUNT { return Err(ShaProjectionError::ColumnRowCount { kind, col, - got: values.len(), - expected: rows, - }); - } - } - Ok(()) -} - -fn validate_bit_columns(bit_slices: &ShaBitSliceColumns) -> Result<(), ShaProjectionError> { - for (col, rows) in bit_slices.columns.iter().enumerate() { - if rows.len() != SHA_ROW_COUNT { - return Err(ShaProjectionError::ColumnRowCount { - kind: "bit_slices", - col, - got: rows.len(), + got: values.evaluations.len(), expected: SHA_ROW_COUNT, }); } - for (row, bits) in rows.iter().enumerate() { - if bits.len() != SHA_WORD_BITS { - return Err(ShaProjectionError::BitCount { - col, - row, - got: bits.len(), - expected: SHA_WORD_BITS, - }); - } - } } Ok(()) } fn word_poly( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, col: ShaWordCol, row: usize, field_cfg: &F::Config, @@ -3089,35 +3413,22 @@ where return Ok(DynamicPolynomialF::ZERO); } let col_idx = col.index(); - let rows = trace - .bit_slices - .columns - .get(col_idx) - .ok_or(ShaProjectionError::MissingColumn { - kind: "bit_slices", - col: col_idx, - })?; - let bits = rows.get(row).ok_or(ShaProjectionError::ColumnRowCount { - kind: "bit_slices", - col: col_idx, - got: rows.len(), - expected: SHA_ROW_COUNT, - })?; - if bits.len() != SHA_WORD_BITS { - return Err(ShaProjectionError::BitCount { - col: col_idx, + let mut coeffs = Vec::with_capacity(SHA_WORD_BITS); + for bit in 0..SHA_WORD_BITS { + coeffs.push(scalar_from_table( + "bit_slices", + &trace.bit_slices, + bit_slice_index(col_idx, bit, SHA_WORD_BITS), row, - got: bits.len(), - expected: SHA_WORD_BITS, - }); + field_cfg, + )?); } - let mut coeffs = bits.clone(); coeffs.resize(SHA_WORD_BITS, F::zero_with_cfg(field_cfg)); Ok(DynamicPolynomialF::new_trimmed(coeffs)) } fn word_poly_shifted( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, col: ShaWordCol, row: usize, shift: usize, @@ -3133,7 +3444,7 @@ where } fn bit_at_shifted_or_zero( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, col: ShaWordCol, row: usize, shift: usize, @@ -3153,36 +3464,17 @@ where return Ok(F::zero_with_cfg(field_cfg)); } let col_idx = col.index(); - let rows = trace - .bit_slices - .columns - .get(col_idx) - .ok_or(ShaProjectionError::MissingColumn { - kind: "bit_slices", - col: col_idx, - })?; - if rows.len() != SHA_ROW_COUNT { - return Err(ShaProjectionError::ColumnRowCount { - kind: "bit_slices", - col: col_idx, - got: rows.len(), - expected: SHA_ROW_COUNT, - }); - } - let bits = &rows[shifted]; - if bits.len() != SHA_WORD_BITS { - return Err(ShaProjectionError::BitCount { - col: col_idx, - row: shifted, - got: bits.len(), - expected: SHA_WORD_BITS, - }); - } - Ok(bits[bit].clone()) + scalar_from_table( + "bit_slices", + &trace.bit_slices, + bit_slice_index(col_idx, bit, SHA_WORD_BITS), + shifted, + field_cfg, + ) } fn int_const_poly( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, col: ShaIntCol, row: usize, field_cfg: &F::Config, @@ -3197,7 +3489,7 @@ where } fn public_const_poly( - public: &ProjectedShaPublic, + public: &ProjectedPublic, col: ShaPublicCol, row: usize, field_cfg: &F::Config, @@ -3212,7 +3504,7 @@ where } fn public_word_or_const_poly( - public: &ProjectedShaPublic, + public: &ProjectedPublic, col: ShaPublicCol, row: usize, field_cfg: &F::Config, @@ -3223,39 +3515,28 @@ where let Some(word_col) = col.public_word_col() else { return public_const_poly(public, col, row, field_cfg); }; - let Some(word_columns) = &public.word_columns else { + let Some(bit_slices) = &public.bit_slices else { return public_const_poly(public, col, row, field_cfg); }; if row >= SHA_ROW_COUNT { return Ok(DynamicPolynomialF::ZERO); } let col_idx = word_col.index(); - let rows = word_columns - .columns - .get(col_idx) - .ok_or(ShaProjectionError::MissingColumn { - kind: "public.word_columns", - col: col_idx, - })?; - let bits = rows.get(row).ok_or(ShaProjectionError::ColumnRowCount { - kind: "public.word_columns", - col: col_idx, - got: rows.len(), - expected: SHA_ROW_COUNT, - })?; - if bits.len() != SHA_WORD_BITS { - return Err(ShaProjectionError::BitCount { - col: col_idx, + let mut bits = Vec::with_capacity(SHA_WORD_BITS); + for bit in 0..SHA_WORD_BITS { + bits.push(scalar_from_table( + "public.bit_slices", + bit_slices, + bit_slice_index(col_idx, bit, SHA_WORD_BITS), row, - got: bits.len(), - expected: SHA_WORD_BITS, - }); + field_cfg, + )?); } - Ok(DynamicPolynomialF::new_trimmed(bits.clone())) + Ok(DynamicPolynomialF::new_trimmed(bits)) } fn int_scalar( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, col: ShaIntCol, row: usize, field_cfg: &F::Config, @@ -3266,9 +3547,9 @@ where if row >= SHA_ROW_COUNT { return Ok(F::zero_with_cfg(field_cfg)); } - scalar_from_matrix( + scalar_from_table( "int_columns", - &trace.int_columns.columns, + &trace.int_columns, col.index(), row, field_cfg, @@ -3276,7 +3557,7 @@ where } fn public_scalar( - public: &ProjectedShaPublic, + public: &ProjectedPublic, col: ShaPublicCol, row: usize, field_cfg: &F::Config, @@ -3287,18 +3568,18 @@ where if row >= SHA_ROW_COUNT { return Ok(F::zero_with_cfg(field_cfg)); } - scalar_from_matrix( + scalar_from_table( "public.columns", - &public.columns.columns, + &public.columns, col.index(), row, field_cfg, ) } -fn scalar_from_matrix( +fn scalar_from_table( kind: &'static str, - columns: &[Vec], + columns: &MleTable, col: usize, row: usize, field_cfg: &F::Config, @@ -3309,7 +3590,16 @@ where let values = columns .get(col) .ok_or(ShaProjectionError::MissingColumn { kind, col })?; + if values.num_vars != SHA_ROW_VARS || values.evaluations.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind, + col, + got: values.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } Ok(values + .evaluations .get(row) .cloned() .unwrap_or_else(|| F::zero_with_cfg(field_cfg))) @@ -3349,7 +3639,7 @@ fn pow_two(exp: usize, field_cfg: &F::Config) -> F { } fn mu_contribution( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, row: usize, low: usize, high: usize, @@ -3472,7 +3762,7 @@ where } fn scalarized_word_at_shifted_or_zero( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, col: ShaWordCol, row: usize, shift: usize, @@ -3488,28 +3778,11 @@ where return Ok(F::zero_with_cfg(field_cfg)); } let col_idx = col.index(); - let rows = - trace - .scalarized_words - .words - .get(col_idx) - .ok_or(ShaProjectionError::MissingColumn { - kind: "scalarized_words", - col: col_idx, - })?; - if rows.len() != SHA_ROW_COUNT { - return Err(ShaProjectionError::ColumnRowCount { - kind: "scalarized_words", - col: col_idx, - got: rows.len(), - expected: SHA_ROW_COUNT, - }); - } - Ok(rows[shifted].clone()) + scalar_from_table("scalarized", &trace.scalarized, col_idx, shifted, field_cfg) } fn booleanity_source_value_at_row( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, row: usize, source: &ShaBooleanitySource, field_cfg: &F::Config, @@ -3531,7 +3804,7 @@ where } fn booleanity_source_value_at_row_with_virtuals( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, row: usize, source: &ShaBooleanitySource, virtuals: Option<&VirtualChMajValues>, @@ -3565,126 +3838,81 @@ fn virtual_bit_at( .ok_or(ShaProjectionError::BitIndexOutOfRange { bit }) } -fn fold_2d<'a, F, I>( - matrices: I, - theta: &[F], - field_cfg: &F::Config, -) -> Result>, ShaProjectionError> -where - F: PrimeField + 'a, - I: IntoIterator>>, -{ - let matrices = matrices.into_iter().collect::>(); - if matrices.len() != theta.len() { - return Err(ShaProjectionError::FoldingWeightCount { - got: theta.len(), - expected: matrices.len(), - }); - } - let Some(first) = matrices.first() else { - return Ok(Vec::new()); - }; - let mut out = vec![vec![F::zero_with_cfg(field_cfg); SHA_ROW_COUNT]; first.len()]; - for (matrix, weight) in matrices.iter().zip(theta) { - if matrix.len() != first.len() { - return Err(ShaProjectionError::InstanceCountMismatch { - got: matrix.len(), - expected: first.len(), - }); - } - for (col_idx, col) in matrix.iter().enumerate() { - if col.len() != SHA_ROW_COUNT { - return Err(ShaProjectionError::ColumnRowCount { - kind: "fold_2d", - col: col_idx, - got: col.len(), - expected: SHA_ROW_COUNT, - }); - } - for row in 0..SHA_ROW_COUNT { - out[col_idx][row] += weight.clone() * &col[row]; - } - } - } - Ok(out) -} - -fn fold_3d<'a, F, I>( - tensors: I, +fn fold_mle_tables<'a, F, I>( + kind: &'static str, + tables: I, theta: &[F], field_cfg: &F::Config, -) -> Result>>, ShaProjectionError> +) -> Result, ShaProjectionError> where F: PrimeField + 'a, - I: IntoIterator>>>, + I: IntoIterator>, { - let tensors = tensors.into_iter().collect::>(); - if tensors.len() != theta.len() { + let tables = tables.into_iter().collect::>(); + if tables.len() != theta.len() { return Err(ShaProjectionError::FoldingWeightCount { got: theta.len(), - expected: tensors.len(), + expected: tables.len(), }); } - let Some(first) = tensors.first() else { + let Some(first) = tables.first() else { return Ok(Vec::new()); }; - let mut out = - vec![vec![vec![F::zero_with_cfg(field_cfg); SHA_WORD_BITS]; SHA_ROW_COUNT]; first.len()]; - for (tensor, weight) in tensors.iter().zip(theta) { - if tensor.len() != first.len() { + let mut out = first + .iter() + .map(|column| DenseMultilinearExtension { + evaluations: vec![F::zero_with_cfg(field_cfg); column.evaluations.len()], + num_vars: column.num_vars, + }) + .collect::>(); + for (table, weight) in tables.iter().zip(theta) { + if table.len() != first.len() { return Err(ShaProjectionError::InstanceCountMismatch { - got: tensor.len(), + got: table.len(), expected: first.len(), }); } - for (col_idx, col) in tensor.iter().enumerate() { - if col.len() != SHA_ROW_COUNT { + for (col_idx, col) in table.iter().enumerate() { + if col.num_vars != out[col_idx].num_vars + || col.evaluations.len() != out[col_idx].evaluations.len() + { return Err(ShaProjectionError::ColumnRowCount { - kind: "fold_3d", + kind, col: col_idx, - got: col.len(), - expected: SHA_ROW_COUNT, + got: col.evaluations.len(), + expected: out[col_idx].evaluations.len(), }); } - for row in 0..SHA_ROW_COUNT { - if col[row].len() != SHA_WORD_BITS { - return Err(ShaProjectionError::BitCount { - col: col_idx, - row, - got: col[row].len(), - expected: SHA_WORD_BITS, - }); - } - for bit in 0..SHA_WORD_BITS { - out[col_idx][row][bit] += weight.clone() * &col[row][bit]; - } + for (out, value) in out[col_idx].evaluations.iter_mut().zip(&col.evaluations) { + *out += weight.clone() * value; } } } Ok(out) } -fn fold_optional_3d<'a, F, I>( - tensors: I, +fn fold_optional_mle_tables<'a, F, I>( + kind: &'static str, + tables: I, theta: &[F], field_cfg: &F::Config, -) -> Result>>>, ShaProjectionError> +) -> Result>, ShaProjectionError> where F: PrimeField + 'a, - I: IntoIterator>>, + I: IntoIterator>>, { - let tensors = tensors.into_iter().collect::>(); - if tensors.iter().all(Option::is_none) { + let tables = tables.into_iter().collect::>(); + if tables.iter().all(Option::is_none) { return Ok(None); } - let mut present = Vec::with_capacity(tensors.len()); - for tensor in tensors { - let Some(tensor) = tensor else { + let mut present = Vec::with_capacity(tables.len()); + for table in tables { + let Some(table) = table else { return Err(ShaProjectionError::PublicWordColumnPresenceMismatch); }; - present.push(&tensor.columns); + present.push(table); } - fold_3d(present, theta, field_cfg).map(Some) + fold_mle_tables(kind, present, theta, field_cfg).map(Some) } #[cfg(test)] @@ -3702,36 +3930,52 @@ mod tests { F::from_with_cfg(value, &test_config()) } - fn zero_trace() -> ProjectedShaTrace { + fn zero_table(cols: usize) -> MleTable { + let cfg = test_config(); + mle_table_from_columns( + vec![vec![F::zero_with_cfg(&cfg); SHA_ROW_COUNT]; cols], + SHA_ROW_VARS, + ) + } + + fn set_word_bit( + trace: &mut ProjectedTrace, + col: ShaWordCol, + row: usize, + bit: usize, + value: F, + ) { + let idx = bit_slice_index(col.index(), bit, SHA_WORD_BITS); + trace.bit_slices[idx].evaluations[row] = value; + } + + fn word_bit(trace: &ProjectedTrace, col: ShaWordCol, row: usize, bit: usize) -> &F { + &trace.bit_slices[bit_slice_index(col.index(), bit, SHA_WORD_BITS)].evaluations[row] + } + + fn zero_trace() -> ProjectedTrace { let cfg = test_config(); let zero = F::zero_with_cfg(&cfg); let bits = vec![vec![vec![zero.clone(); SHA_WORD_BITS]; SHA_ROW_COUNT]; ShaWordCol::COUNT]; - let bit_slices = ShaBitSliceColumns { columns: bits }; - let scalarized_words = scalarize_trace_words(&bit_slices, &f(5), &cfg).unwrap(); - ProjectedShaTrace { - rows: SHA_ROW_COUNT, + let bit_slices = + flatten_bit_columns(bits, SHA_WORD_BITS, SHA_ROW_VARS, "bit_slices").unwrap(); + let scalarized = scalarize_bit_slices(&bit_slices, &f(5), &cfg).unwrap(); + ProjectedTrace { bit_slices, - scalarized_words, - int_columns: ShaIntColumns { - columns: vec![vec![zero.clone(); SHA_ROW_COUNT]; ShaIntCol::COUNT], - }, - public_columns: ShaPublicColumns { - columns: vec![vec![zero; SHA_ROW_COUNT]; ShaPublicCol::COUNT], - }, + scalarized, + int_columns: zero_table(ShaIntCol::COUNT), + public_columns: zero_table(ShaPublicCol::COUNT), } } - fn zero_public() -> ProjectedShaPublic { - let cfg = test_config(); - ProjectedShaPublic { - columns: ShaPublicColumns { - columns: vec![vec![F::zero_with_cfg(&cfg); SHA_ROW_COUNT]; ShaPublicCol::COUNT], - }, - word_columns: None, + fn zero_public() -> ProjectedPublic { + ProjectedPublic { + columns: zero_table(ShaPublicCol::COUNT), + bit_slices: None, } } - fn synthetic_boolean_trace(instance_idx: u64, a: &F) -> ProjectedShaTrace { + fn synthetic_boolean_trace(instance_idx: u64, a: &F) -> ProjectedTrace { let cfg = test_config(); let zero = F::zero_with_cfg(&cfg); let mut bits = @@ -3748,18 +3992,14 @@ mod tests { } } } - let bit_slices = ShaBitSliceColumns { columns: bits }; - let scalarized_words = scalarize_trace_words(&bit_slices, a, &cfg).unwrap(); - ProjectedShaTrace { - rows: SHA_ROW_COUNT, + let bit_slices = + flatten_bit_columns(bits, SHA_WORD_BITS, SHA_ROW_VARS, "bit_slices").unwrap(); + let scalarized = scalarize_bit_slices(&bit_slices, a, &cfg).unwrap(); + ProjectedTrace { bit_slices, - scalarized_words, - int_columns: ShaIntColumns { - columns: vec![vec![zero.clone(); SHA_ROW_COUNT]; ShaIntCol::COUNT], - }, - public_columns: ShaPublicColumns { - columns: vec![vec![zero; SHA_ROW_COUNT]; ShaPublicCol::COUNT], - }, + scalarized, + int_columns: zero_table(ShaIntCol::COUNT), + public_columns: zero_table(ShaPublicCol::COUNT), } } @@ -3861,7 +4101,7 @@ mod tests { let cfg = test_config(); let a = f(13); let lambda = f(17); - let mut cache = FreshShaIdealCache { + let mut cache = FreshIdealEvaluationCache { r_ic: std::array::from_fn(|_| F::zero_with_cfg(&cfg)), ideal_polys: vec![std::array::from_fn(|slot| { DynamicPolynomialF::new_trimmed([ @@ -3910,6 +4150,30 @@ mod tests { assert_eq!(cache.fresh_targets[0], F::zero_with_cfg(&cfg)); } + #[test] + fn beta_aggregate_with_weights_matches_wrapper() { + let cfg = test_config(); + let table = |offset: u64| LinearResidualCoeffTable { + coeffs: (0..NUM_SHA_RESIDUAL_FAMILIES) + .map(|idx| { + DynamicPolynomialF::new_trimmed([ + f(offset + idx as u64 + 1), + f(offset + idx as u64 + 101), + ]) + }) + .collect(), + }; + let tables = vec![table(0), table(1_000)]; + let beta = [f(17)]; + let beta_eq_weights = zinc_poly::utils::build_eq_x_r_vec(&beta, &cfg).unwrap(); + + let wrapped = beta_aggregate_nonzero_ideal_polys(&tables, &beta, &cfg).unwrap(); + let cached = + beta_aggregate_nonzero_ideal_polys_with_weights(&tables, &beta_eq_weights).unwrap(); + + assert_eq!(cached, wrapped); + } + #[test] fn tampered_ideal_cache_fails_membership() { let cfg = test_config(); @@ -3959,14 +4223,14 @@ mod tests { fn scalarization_links_check_folded_words() { let cfg = test_config(); let mut trace = zero_trace(); - trace.bit_slices.columns[ShaWordCol::A.index()][0][0] = f(1); - trace.bit_slices.columns[ShaWordCol::A.index()][0][3] = f(1); - trace.scalarized_words = scalarize_trace_words(&trace.bit_slices, &f(5), &cfg).unwrap(); + set_word_bit(&mut trace, ShaWordCol::A, 0, 0, f(1)); + set_word_bit(&mut trace, ShaWordCol::A, 0, 3, f(1)); + trace.scalarized = scalarize_bit_slices(&trace.bit_slices, &f(5), &cfg).unwrap(); verify_folded_scalarization_links(&trace, &f(5), &[ShaWordCol::A], &cfg) .expect("scalarization should pass"); - trace.scalarized_words.words[ShaWordCol::A.index()][0] += f(1); + trace.scalarized[ShaWordCol::A.index()].evaluations[0] += f(1); assert!(matches!( verify_folded_scalarization_links(&trace, &f(5), &[ShaWordCol::A], &cfg), Err(ShaProjectionError::ScalarizationMismatch { .. }) @@ -3977,10 +4241,10 @@ mod tests { fn scalarization_links_check_endpoint_and_shifted_sources() { let cfg = test_config(); let mut trace = zero_trace(); - trace.bit_slices.columns[ShaWordCol::A.index()][0][1] = f(1); - trace.bit_slices.columns[ShaWordCol::A.index()][1][0] = f(1); - trace.bit_slices.columns[ShaWordCol::A.index()][1][2] = f(1); - trace.scalarized_words = scalarize_trace_words(&trace.bit_slices, &f(3), &cfg).unwrap(); + set_word_bit(&mut trace, ShaWordCol::A, 0, 1, f(1)); + set_word_bit(&mut trace, ShaWordCol::A, 1, 0, f(1)); + set_word_bit(&mut trace, ShaWordCol::A, 1, 2, f(1)); + trace.scalarized = scalarize_bit_slices(&trace.bit_slices, &f(3), &cfg).unwrap(); let r_star = std::array::from_fn(|_| F::zero_with_cfg(&cfg)); verify_folded_scalarization_links_at_point(&trace, &f(3), &r_star, &[ShaWordCol::A], &cfg) @@ -3995,7 +4259,7 @@ mod tests { ) .expect("shifted endpoint scalarization should pass"); - trace.scalarized_words.words[ShaWordCol::A.index()][0] += f(1); + trace.scalarized[ShaWordCol::A.index()].evaluations[0] += f(1); assert!(matches!( verify_folded_scalarization_links_at_point( &trace, @@ -4009,16 +4273,15 @@ mod tests { } #[test] - fn sumfold_output_derives_theta_after_endpoint() { + fn instance_fold_claim_derives_weights_after_endpoint() { let cfg = test_config(); let beta = vec![f(2), f(3)]; let r_b = vec![f(5), f(7)]; let c_sf = f(11); - let out = - derive_sha_instance_fold_claim(&beta, r_b.clone(), c_sf.clone(), 4, &cfg).unwrap(); + let out = derive_instance_fold_claim(&beta, r_b.clone(), c_sf.clone(), 4, &cfg).unwrap(); let d = eq_eval(&beta, &r_b, F::one_with_cfg(&cfg)).unwrap(); - assert_eq!(out.t_prime(), &(c_sf / d)); + assert_eq!(out.final_round_sumcheck_claim(), &(c_sf / d)); assert_eq!( out.eq_instance_weights(), build_eq_x_r_vec(&r_b, &cfg).unwrap() @@ -4030,38 +4293,38 @@ mod tests { let cfg = test_config(); let beta = vec![f(2)]; let r_b = vec![f(3)]; - let out = derive_sha_instance_fold_claim(&beta, r_b, f(9), 2, &cfg).unwrap(); + let out = derive_instance_fold_claim(&beta, r_b, f(9), 2, &cfg).unwrap(); let mut left = zero_trace(); let mut right = zero_trace(); - left.bit_slices.columns[ShaWordCol::A.index()][0][0] = f(1); - right.bit_slices.columns[ShaWordCol::A.index()][0][0] = f(2); - left.scalarized_words = scalarize_trace_words(&left.bit_slices, &f(5), &cfg).unwrap(); - right.scalarized_words = scalarize_trace_words(&right.bit_slices, &f(5), &cfg).unwrap(); + set_word_bit(&mut left, ShaWordCol::A, 0, 0, f(1)); + set_word_bit(&mut right, ShaWordCol::A, 0, 0, f(2)); + left.scalarized = scalarize_bit_slices(&left.bit_slices, &f(5), &cfg).unwrap(); + right.scalarized = scalarize_bit_slices(&right.bit_slices, &f(5), &cfg).unwrap(); - let (folded, _public) = fold_projected_sha_traces( + let (folded, _public) = fold_projected_traces( &[left.clone(), right.clone()], &[zero_public(), zero_public()], &out, &cfg, ) .unwrap(); - let expected = out.eq_instance_weights()[0].clone() * &left.bit_slices.columns[0][0][0] - + out.eq_instance_weights()[1].clone() * &right.bit_slices.columns[0][0][0]; - assert_eq!(folded.trace.bit_slices.columns[0][0][0], expected); + let expected = out.eq_instance_weights()[0].clone() * word_bit(&left, ShaWordCol::A, 0, 0) + + out.eq_instance_weights()[1].clone() * word_bit(&right, ShaWordCol::A, 0, 0); + assert_eq!(*word_bit(&folded.trace, ShaWordCol::A, 0, 0), expected); } #[test] fn virtual_ch_maj_reconstructs_from_source_bits() { let cfg = test_config(); let mut trace = zero_trace(); - trace.bit_slices.columns[ShaWordCol::E.index()][2][0] = f(1); - trace.bit_slices.columns[ShaWordCol::E.index()][1][0] = f(1); - trace.bit_slices.columns[ShaWordCol::Uef.index()][2][0] = f(1); - trace.bit_slices.columns[ShaWordCol::A.index()][0][1] = f(1); - trace.bit_slices.columns[ShaWordCol::A.index()][1][1] = f(1); - trace.bit_slices.columns[ShaWordCol::A.index()][2][1] = f(1); - trace.bit_slices.columns[ShaWordCol::Maj.index()][2][1] = f(1); + set_word_bit(&mut trace, ShaWordCol::E, 2, 0, f(1)); + set_word_bit(&mut trace, ShaWordCol::E, 1, 0, f(1)); + set_word_bit(&mut trace, ShaWordCol::Uef, 2, 0, f(1)); + set_word_bit(&mut trace, ShaWordCol::A, 0, 1, f(1)); + set_word_bit(&mut trace, ShaWordCol::A, 1, 1, f(1)); + set_word_bit(&mut trace, ShaWordCol::A, 2, 1, f(1)); + set_word_bit(&mut trace, ShaWordCol::Maj, 2, 1, f(1)); let virtuals = reconstruct_virtual_ch_maj_at_row(&trace, 0, &cfg).unwrap(); @@ -4186,10 +4449,10 @@ mod tests { .unwrap(); let (_proof, r_b, expected) = prove_and_verify_sumfold(sumfold_group, ell); let sumfold = - derive_sha_instance_fold_claim(&beta, r_b, expected[0].clone(), traces.len(), &cfg) + derive_instance_fold_claim(&beta, r_b, expected[0].clone(), traces.len(), &cfg) .unwrap(); let (folded_witness, folded_public) = - fold_projected_sha_traces(&traces, &publics, &sumfold, &cfg).unwrap(); + fold_projected_traces(&traces, &publics, &sumfold, &cfg).unwrap(); let folded_claim = expression_folded_row_sum( &folded_witness.trace, @@ -4203,7 +4466,7 @@ mod tests { &cfg, ) .unwrap(); - assert_eq!(&folded_claim, sumfold.t_prime()); + assert_eq!(&folded_claim, sumfold.final_round_sumcheck_claim()); let row_group = build_expression_folded_row_sumcheck_group( &folded_witness.trace, @@ -4232,8 +4495,11 @@ mod tests { &cfg, ) .expect("folded row sumcheck proof should verify"); - verify_folded_row_sumcheck_claim(&row_proof.claimed_sums()[0], sumfold.t_prime()) - .expect("folded row claim matches T'"); + verify_folded_row_sumcheck_claim( + &row_proof.claimed_sums()[0], + sumfold.final_round_sumcheck_claim(), + ) + .expect("folded row claim matches T'"); } #[test] diff --git a/piop/src/sumcheck.rs b/piop/src/sumcheck.rs index c16759ae..f339c7b8 100644 --- a/piop/src/sumcheck.rs +++ b/piop/src/sumcheck.rs @@ -170,14 +170,14 @@ impl MLSumcheck { ) -> (SumcheckProof, ProverState) where F: InnerTransparentField, - F::Inner: ConstTranscribable + Zero, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, { if nvars == 0 { panic!("Attempt to prove a constant") } - let mut buf = vec![0; F::Inner::NUM_BYTES]; + let mut buf = vec![0; F::zero_with_cfg(config).inner().get_num_bytes()]; let nvars_field = F::from_with_cfg(nvars as u64, config); let degree_field = F::from_with_cfg(degree as u64, config); @@ -192,7 +192,7 @@ impl MLSumcheck { let prover_msg = prover_state.prove_round(&verifier_msg, &comb_fn, config); transcript.absorb_random_field_slice(&prover_msg.0.tail_evaluations, &mut buf); prover_msgs.push(prover_msg); - let next_verifier_msg = transcript.get_field_challenge(config); + let next_verifier_msg = transcript.get_transcribable_field_challenge(config); transcript.absorb_random_field(&next_verifier_msg, &mut buf); verifier_msg = Some(next_verifier_msg); @@ -273,14 +273,14 @@ impl MLSumcheck { config: &F::Config, ) -> Result, SumCheckError> where - F::Inner: ConstTranscribable, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable, + F::Modulus: Transcribable, { if num_vars == 0 { panic!("Attempt to verify a sumcheck claim for 0 variables") } - let mut buf = vec![0; F::Inner::NUM_BYTES]; + let mut buf = vec![0; F::zero_with_cfg(config).inner().get_num_bytes()]; let (nvars_field, degree_field): (F, F) = { ( diff --git a/piop/src/sumcheck/multi_degree.rs b/piop/src/sumcheck/multi_degree.rs index 1172617b..3beff594 100644 --- a/piop/src/sumcheck/multi_degree.rs +++ b/piop/src/sumcheck/multi_degree.rs @@ -408,8 +408,8 @@ impl MultiDegreeSumcheck { ) -> (MultiDegreeSumcheckProof, Vec>) where F: InnerTransparentField + Send + Sync, - F::Inner: ConstTranscribable + Zero, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, { assert!( num_vars > 0, @@ -418,7 +418,7 @@ impl MultiDegreeSumcheck { assert!(!groups.is_empty(), "need at least one degree group"); let num_groups = groups.len(); - let mut buf = vec![0; F::Inner::NUM_BYTES]; + let mut buf = vec![0; F::zero_with_cfg(config).inner().get_num_bytes()]; let nvars_field = F::from_with_cfg(num_vars as u64, config); let ngroups_field = F::from_with_cfg(num_groups as u64, config); transcript.absorb_random_field(&nvars_field, &mut buf); @@ -492,7 +492,7 @@ impl MultiDegreeSumcheck { group_messages[j].push(msg); } - let challenge: F = transcript.get_field_challenge(config); + let challenge: F = transcript.get_transcribable_field_challenge(config); transcript.absorb_random_field(&challenge, &mut buf); for group_idx in 0..num_groups { @@ -588,8 +588,8 @@ impl MultiDegreeSumcheck { ) -> Result, SumCheckError> where F: InnerTransparentField, - F::Inner: ConstTranscribable, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable, + F::Modulus: Transcribable, { assert!( num_vars > 0, @@ -598,7 +598,7 @@ impl MultiDegreeSumcheck { let num_groups = proof.degrees.len(); assert!(num_groups != 0, "need at least one degree group"); - let mut buf = vec![0; F::Inner::NUM_BYTES]; + let mut buf = vec![0; F::zero_with_cfg(config).inner().get_num_bytes()]; let nvars_field = F::from_with_cfg(num_vars as u64, config); let ngroups_field = F::from_with_cfg(num_groups as u64, config); transcript.absorb_random_field(&nvars_field, &mut buf); @@ -636,7 +636,7 @@ impl MultiDegreeSumcheck { transcript.absorb_random_field_slice(&msg[i].0.tail_evaluations, &mut buf) }); - let shared_challenge: F = transcript.get_field_challenge(config); + let shared_challenge: F = transcript.get_transcribable_field_challenge(config); transcript.absorb_random_field(&shared_challenge, &mut buf); verifier_states diff --git a/piop/src/sumcheck/prover.rs b/piop/src/sumcheck/prover.rs index 79f3fa96..d0d812cc 100644 --- a/piop/src/sumcheck/prover.rs +++ b/piop/src/sumcheck/prover.rs @@ -182,17 +182,15 @@ where // My bet is that it won't affect running time, but better safe than // sorry. - s.vals0 - .iter_mut() - .zip(polys.iter()) - .for_each(|(v0, poly)| *v0.inner_mut() = poly[index].clone()); + s.vals0.iter_mut().zip(polys.iter()).for_each(|(v0, poly)| { + *v0 = F::new_unchecked_with_cfg(poly[index].clone(), config); + }); s.levals[0] = comb_fn(&s.vals0); if degree > 0 { - s.vals1 - .iter_mut() - .zip(polys.iter()) - .for_each(|(v1, poly)| *v1.inner_mut() = poly[index + 1].clone()); + s.vals1.iter_mut().zip(polys.iter()).for_each(|(v1, poly)| { + *v1 = F::new_unchecked_with_cfg(poly[index + 1].clone(), config); + }); s.levals[1] = comb_fn(&s.vals1); for (i, (v1, v0)) in s.vals1.iter().zip(s.vals0.iter()).enumerate() { diff --git a/piop/src/sumcheck/verifier.rs b/piop/src/sumcheck/verifier.rs index f9c857c4..e580e8d8 100644 --- a/piop/src/sumcheck/verifier.rs +++ b/piop/src/sumcheck/verifier.rs @@ -2,7 +2,7 @@ use crypto_primitives::{FromPrimitiveWithConfig, PrimeField}; use zinc_poly::{EvaluatablePolynomial, univariate::nat_evaluation::NatEvaluatedPoly}; -use zinc_transcript::traits::{ConstTranscribable, Transcript}; +use zinc_transcript::traits::{Transcribable, Transcript}; use zinc_utils::add; use crate::sumcheck::prover::{NatEvaluatedPolyWithoutConstant, ProverMsg}; @@ -65,9 +65,9 @@ impl VerifierState { /// [`Self::verify_round_with_challenge`]. Returns the sampled challenge. pub fn verify_round(&mut self, prover_msg: &ProverMsg, transcript: &mut impl Transcript) -> F where - F::Inner: ConstTranscribable, + F::Inner: Transcribable, { - let challenge: F = transcript.get_field_challenge(&self.config); + let challenge: F = transcript.get_transcribable_field_challenge(&self.config); self.verify_round_with_challenge(prover_msg, challenge.clone()); challenge } diff --git a/poly/src/mle/dense.rs b/poly/src/mle/dense.rs index 84735905..389aabff 100644 --- a/poly/src/mle/dense.rs +++ b/poly/src/mle/dense.rs @@ -257,15 +257,15 @@ where let nv = self.num_vars; let dim = partial_point.len(); - let mut r = partial_point[0].clone(); for i in 1..dim + 1 { + let r_base = &partial_point[i - 1]; for b in 0..1 << (nv - i) { - *r.inner_mut() = partial_point[i - 1].inner().clone(); if self[2 * b + 1] != self[2 * b] { // a = f(1) - f(0) let a = F::sub_inner(&self[2 * b + 1], &self[2 * b], config); // self[b] = f(0) + r * a + let mut r = r_base.clone(); r.mul_assign_by_inner(&a); self[b] = F::add_inner(&self[2 * b], r.inner(), config); } else { diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 6d9bfc30..f3467489 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -825,10 +825,10 @@ mod tests { use zinc_primality::MillerRabin; use zinc_test_uair::{ BigLinearUair, BigLinearUairWithPublicInput, BinaryDecompositionUair, BitOpRotUair, - EC_FP_INT_LIMBS, GenerateRandomTrace, Sha256CompressionSliceUair, Sha256Ideal, - Sha256MessageBlock, Sha256State, ShaEcdsaUair, TestUairMixedDegrees, TestUairMixedShifts, - TestUairNoMultiplication, TestUairSimpleMultiplication, sha256_compress_native, - synthesize_sha256_chain_witnesses, + EC_FP_INT_LIMBS, GenerateRandomTrace, SHA256_INITIAL_STATE, Sha256CompressionSliceUair, + Sha256Ideal, Sha256MessageBlock, Sha256State, ShaEcdsaUair, TestUairMixedDegrees, + TestUairMixedShifts, TestUairNoMultiplication, TestUairSimpleMultiplication, + sha256_compress_native, sha256_padded_message_blocks, synthesize_sha256_chain_witnesses, }; use zinc_uair::{ ideal::{DegreeOneIdeal, rotation::RotationIdeal}, @@ -2043,13 +2043,10 @@ mod tests { type U = Sha256CompressionSliceUair; const NUM_VARS: usize = 9; - let initial_state: Sha256State = [ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, - 0x5be0cd19, - ]; - let message_blocks: [Sha256MessageBlock; 1] = [std::array::from_fn(|idx| { - 0x0102_0304u32.wrapping_mul(idx as u32 + 1) - })]; + let initial_state: Sha256State = SHA256_INITIAL_STATE; + let message_blocks: [Sha256MessageBlock; 1] = + sha256_padded_message_blocks(b"zinc-plus synthesized SHA witness") + .expect("test message should canonically pad to one SHA-256 block"); let (witnesses, final_state) = synthesize_sha256_chain_witnesses::(initial_state, message_blocks) diff --git a/protocol/src/multipoint_reduction.rs b/protocol/src/multipoint_reduction.rs index e4afb911..89b9fb26 100644 --- a/protocol/src/multipoint_reduction.rs +++ b/protocol/src/multipoint_reduction.rs @@ -5,7 +5,7 @@ use zinc_piop::multipoint_eval::{ Subclaim as MultipointSubclaim, }; use zinc_poly::mle::DenseMultilinearExtension; -use zinc_transcript::traits::{ConstTranscribable, Transcript}; +use zinc_transcript::traits::{Transcribable, Transcript}; use zinc_uair::ShiftSpec; use zinc_utils::{ delayed_reduction::DelayedFieldProductSum, inner_transparent_field::InnerTransparentField, @@ -27,8 +27,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero + Default + Send + Sync, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, { let (proof, state) = MultipointEval::prove_as_subprotocol( transcript, trace_mles, eval_point, up_evals, down_evals, shifts, field_cfg, @@ -53,8 +53,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero + Default + Send + Sync, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, { MultipointEval::verify_as_subprotocol( transcript, proof, eval_point, up_evals, down_evals, shifts, num_vars, field_cfg, diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 138a12ef..fb47efea 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -10,7 +10,8 @@ use crate::{ ZincTypes, multipoint_reduction::{prove_multipoint_reduction, verify_multipoint_reduction}, pcs::{ - PCSCommitments, PCSOpeningProof, PCSParams, PCSProverData, PCSVerifierParams, ZincPCSTypes, + AllHyraxPCSTypes, PCSCommitments, PCSOpeningProof, PCSParams, PCSProverData, + PCSVerifierParams, ZincPCSTypes, }, }; use ark_ec::AffineRepr; @@ -26,21 +27,23 @@ use zinc_piop::{ }, neutron_nova::SumFoldError, neutron_nova::{ - NUM_NONZERO_SHA_FAMILIES, NUM_SHA_RESIDUAL_FAMILIES, ProjectedShaPublic, ProjectedShaTrace, - SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, ShaBooleanitySource, ShaIntCol, - ShaLinearResidualCoeffCache, ShaProjectionError, ShaPublicCol, ShaPublicWordColumns, - ShaResidualFamily, ShaSumFoldOutput, ShaWordCol, build_dense_sha_sumfold_group, - build_expression_folded_row_sumcheck_group, build_folded_row_sumcheck_group, - build_production_sha_sumfold_group_with_linear_cache, - build_sha_linear_residual_coeff_cache, derive_sha_instance_fold_claim, - fold_projected_sha_traces, folded_row_integrand_sum, production_sha_booleanity_sources, - production_sha_nonzero_families, sha_int_at_point, sha_public_at_point, - sha_scalarized_word_at_point, sha_word_bits_at_point, verify_folded_row_sumcheck_claim, - verify_fresh_sha_ideal_polys, + InstanceFoldClaim, LinearResidualCoeffTable, MleTable, NUM_NONZERO_SHA_FAMILIES, + NUM_SHA_RESIDUAL_FAMILIES, ProjectedPublic, ProjectedTrace, SHA_ROW_COUNT, SHA_ROW_VARS, + SHA_WORD_BITS, ShaBooleanitySource, ShaIntCol, ShaProjectionError, ShaPublicCol, + ShaPublicWordCol, ShaResidualFamily, ShaWordCol, + beta_aggregate_nonzero_ideal_polys_with_weights, bit_slice_index, + build_dense_sha_sumfold_group, build_folded_row_sumcheck_group, + build_linear_residual_coeff_tables_with_row_weights, + build_production_sha_sumfold_group_with_linear_cache_and_weights, + derive_instance_fold_claim, expression_folded_row_sum_with_row_weights, + fold_projected_traces, folded_row_integrand_sum, production_sha_booleanity_sources, + production_sha_nonzero_families, sha_int_at_point_with_weights, sha_public_at_point, + sha_public_at_point_with_weights, sha_word_bits_at_point_with_weights, + verify_folded_row_sumcheck_claim, verify_fresh_sha_ideal_polys, }, sumcheck::{ SumCheckError, - multi_degree::{MultiDegreeSumcheck, MultiDegreeSumcheckProof}, + multi_degree::{MultiDegreeSumcheck, MultiDegreeSumcheckGroup, MultiDegreeSumcheckProof}, }, }; use zinc_poly::{ @@ -54,7 +57,7 @@ use zinc_poly::{ utils::{ArithErrors, build_eq_x_r_vec, eq_eval}, }; use zinc_transcript::Blake3Transcript; -use zinc_transcript::traits::{ConstTranscribable, Transcribable, Transcript}; +use zinc_transcript::traits::{Transcribable, Transcript}; use zinc_uair::{ShiftSpec, Uair, UairSignature, UairTrace, UairWitness}; use zinc_utils::{ UNCHECKED, delayed_reduction::DelayedFieldProductSum, inner_product::InnerProduct, @@ -64,9 +67,9 @@ use zip_plus::{ ZipError, pcs::{ generic::{FoldablePCS, PCS}, - hyrax::HyraxFieldBridge, + hyrax::{BinaryLanes, DensePolyScalarLanes, HyraxFieldBridge, HyraxPCS, IntScalarLane}, }, - pcs_transcript::PcsVerifierTranscript, + pcs_transcript::{PcsProverTranscript, PcsVerifierTranscript}, }; #[derive(Clone, Debug)] @@ -91,9 +94,9 @@ pub struct ProductionShaWitnessPolys where Zt: ZincTypes, { - pub binary: Vec>>, - pub arbitrary: Vec>>, - pub int: Vec>, + pub binary: MleTable>, + pub arbitrary: MleTable>, + pub int: MleTable, } #[derive(Clone, Debug)] @@ -102,8 +105,8 @@ where Zt: ZincTypes, F: PrimeField, { - pub trace: ProjectedShaTrace, - pub public: ProjectedShaPublic, + pub trace: ProjectedTrace, + pub public: ProjectedPublic, pub witness_polys: ProductionShaWitnessPolys, } @@ -112,11 +115,15 @@ where Zt: ZincTypes, F: PrimeField, { + fn production_sha_pcs_batch_sizes() -> (usize, usize, usize) { + (ShaWordCol::COUNT, 0, ShaIntCol::COUNT) + } + fn project_production_sha_public( shape: &UairShape, public_trace: &UairTrace<'_, Zt::Int, Zt::Int, D>, field_cfg: &F::Config, - ) -> Result, ProductionShaError> + ) -> Result, ProductionShaError> where Self: Sized; @@ -127,8 +134,8 @@ where field_cfg: &F::Config, ) -> Result< ( - ProjectedShaTrace, - ProjectedShaPublic, + ProjectedTrace, + ProjectedPublic, ProductionShaWitnessPolys, ), ProductionShaError, @@ -341,10 +348,82 @@ where F: PrimeField, P: ZincPCSTypes, { - pub trace: ProjectedShaTrace, + pub trace: ProjectedTrace, pub opening_witness: PCSProverData, } +pub trait ProductionShaFoldedPcsOpen: ZincPCSTypes +where + Zt: ZincTypes, + F: PrimeField, +{ + #[allow(clippy::too_many_arguments)] + fn prove_folded_pcs_opening( + pcs_params: &PCSParams, + folded_commitments: &PCSCommitments, + folded_trace: &ProjectedTrace, + folded_prover_data: &PCSProverData, + r_0: &[F], + folded_lifted_evals: &[DynamicPolynomialF], + field_cfg: &F::Config, + ) -> Result, ProductionShaError> + where + Self: Sized; +} + +impl ProductionShaFoldedPcsOpen for AllHyraxPCSTypes +where + Zt: ZincTypes, + F: HyraxFieldBridge, + F::Inner: Transcribable, + F::Modulus: Transcribable, + C: AffineRepr, + HyraxPCS: PCS< + F, + BinaryPoly, + D, + CommitmentKey = zip_plus::pcs::hyrax::HyraxCommitmentKey, + ProverData = zip_plus::pcs::hyrax::HyraxProverData, + OpeningProof = Vec, + >, + HyraxPCS: PCS< + F, + DensePolynomial, + D, + CommitmentKey = zip_plus::pcs::hyrax::HyraxCommitmentKey, + ProverData = zip_plus::pcs::hyrax::HyraxProverData, + OpeningProof = Vec, + >, + HyraxPCS: PCS< + F, + Zt::Int, + D, + CommitmentKey = zip_plus::pcs::hyrax::HyraxCommitmentKey, + ProverData = zip_plus::pcs::hyrax::HyraxProverData, + OpeningProof = Vec, + >, +{ + fn prove_folded_pcs_opening( + pcs_params: &PCSParams, + folded_commitments: &PCSCommitments, + folded_trace: &ProjectedTrace, + folded_prover_data: &PCSProverData, + r_0: &[F], + folded_lifted_evals: &[DynamicPolynomialF], + field_cfg: &F::Config, + ) -> Result, ProductionShaError> { + prove_production_sha_hyrax_pcs_opening::( + pcs_params, + folded_commitments, + folded_trace, + folded_prover_data, + r_0, + folded_lifted_evals, + field_cfg, + ) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct VerifiedShaSumFold { pub r_b: Vec, @@ -356,6 +435,7 @@ pub type LinearIdealFoldError = ProductionShaError; #[derive(Clone, Debug, PartialEq, Eq)] pub struct FoldedRowSumcheckOutput { pub r_star: Vec, + pub r_star_eq_weights: Vec, pub terminal_value: F, } @@ -425,35 +505,32 @@ pub enum ProductionShaError { pub fn absorb_projected_sha_publics( transcript: &mut impl Transcript, - publics: &[zinc_piop::neutron_nova::ProjectedShaPublic], + publics: &[zinc_piop::neutron_nova::ProjectedPublic], + field_cfg: &F::Config, ) where F: PrimeField, - F::Inner: ConstTranscribable + Transcribable, + F::Inner: Transcribable, F::Modulus: Transcribable, { - let mut buf = vec![0; F::Inner::NUM_BYTES]; + let mut field_buf = runtime_field_transcript_buf::(field_cfg); transcript.absorb_slice(b"production_sha_publics_begin"); transcript.absorb_slice(&(publics.len() as u64).to_le_bytes()); for (instance_idx, public) in publics.iter().enumerate() { transcript.absorb_slice(&(instance_idx as u64).to_le_bytes()); - transcript.absorb_slice(&(public.columns.columns.len() as u64).to_le_bytes()); - for (col_idx, col) in public.columns.columns.iter().enumerate() { + transcript.absorb_slice(&(public.columns.len() as u64).to_le_bytes()); + for (col_idx, col) in public.columns.iter().enumerate() { transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); - transcript.absorb_slice(&(col.len() as u64).to_le_bytes()); - transcript.absorb_random_field_slice(col, &mut buf); + transcript.absorb_slice(&(col.evaluations.len() as u64).to_le_bytes()); + transcript.absorb_random_field_slice(&col.evaluations, &mut field_buf); } - match &public.word_columns { - Some(word_columns) => { + match &public.bit_slices { + Some(bit_slices) => { transcript.absorb_slice(&[1]); - transcript.absorb_slice(&(word_columns.columns.len() as u64).to_le_bytes()); - for (col_idx, rows) in word_columns.columns.iter().enumerate() { + transcript.absorb_slice(&(bit_slices.len() as u64).to_le_bytes()); + for (col_idx, col) in bit_slices.iter().enumerate() { transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); - transcript.absorb_slice(&(rows.len() as u64).to_le_bytes()); - for (row_idx, bits) in rows.iter().enumerate() { - transcript.absorb_slice(&(row_idx as u64).to_le_bytes()); - transcript.absorb_slice(&(bits.len() as u64).to_le_bytes()); - transcript.absorb_random_field_slice(bits, &mut buf); - } + transcript.absorb_slice(&(col.evaluations.len() as u64).to_le_bytes()); + transcript.absorb_random_field_slice(&col.evaluations, &mut field_buf); } } None => transcript.absorb_slice(&[0]), @@ -510,6 +587,63 @@ fn absorb_transcribable(transcript: &mut impl Transcript, valu transcript.absorb_slice(&buf); } +fn runtime_field_transcript_buf(field_cfg: &F::Config) -> Vec +where + F: PrimeField, + F::Inner: Transcribable, +{ + vec![0u8; F::zero_with_cfg(field_cfg).inner().get_num_bytes()] +} + +fn absorb_sha_resolver_proof( + transcript: &mut impl Transcript, + proof: &CombinedPolyResolverProof, + field_cfg: &F::Config, +) where + F: PrimeField, + F::Inner: Transcribable, + F::Modulus: Transcribable, +{ + let mut field_buf = runtime_field_transcript_buf::(field_cfg); + fn absorb_vec( + transcript: &mut impl Transcript, + label: &'static [u8], + values: &[F], + field_buf: &mut [u8], + ) where + F: PrimeField, + F::Inner: Transcribable, + F::Modulus: Transcribable, + { + transcript.absorb_slice(label); + transcript.absorb_slice(&(values.len() as u64).to_le_bytes()); + transcript.absorb_random_field_slice(values, field_buf); + } + + transcript.absorb_slice(b"production_sha_resolver_begin"); + absorb_vec(transcript, b"up", &proof.up_evals, &mut field_buf); + absorb_vec(transcript, b"down", &proof.down_evals, &mut field_buf); + absorb_vec( + transcript, + b"bit_slice", + &proof.bit_slice_evals, + &mut field_buf, + ); + absorb_vec( + transcript, + b"bit_op_down", + &proof.bit_op_down_evals, + &mut field_buf, + ); + absorb_vec( + transcript, + b"shifted_bit_slice", + &proof.shifted_bit_slice_evals, + &mut field_buf, + ); + transcript.absorb_slice(b"production_sha_resolver_end"); +} + fn absorb_public_uair_trace( transcript: &mut impl Transcript, instance_idx: usize, @@ -625,12 +759,13 @@ pub fn absorb_production_sha_commitments( pub fn absorb_fresh_sha_ideal_polys( transcript: &mut impl Transcript, ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], + field_cfg: &F::Config, ) where F: PrimeField, - F::Inner: ConstTranscribable + Transcribable, + F::Inner: Transcribable, F::Modulus: Transcribable, { - let mut buf = vec![0; F::Inner::NUM_BYTES]; + let mut field_buf = runtime_field_transcript_buf::(field_cfg); transcript.absorb_slice(b"production_sha_fresh_ideals_begin"); transcript.absorb_slice(&(ideal_polys.len() as u64).to_le_bytes()); for (instance_idx, instance) in ideal_polys.iter().enumerate() { @@ -639,7 +774,7 @@ pub fn absorb_fresh_sha_ideal_polys( for (family_idx, poly) in instance.iter().enumerate() { transcript.absorb_slice(&(family_idx as u64).to_le_bytes()); transcript.absorb_slice(&(poly.coeffs.len() as u64).to_le_bytes()); - transcript.absorb_random_field_slice(&poly.coeffs, &mut buf); + transcript.absorb_random_field_slice(&poly.coeffs, &mut field_buf); } } transcript.absorb_slice(b"production_sha_fresh_ideals_end"); @@ -648,18 +783,19 @@ pub fn absorb_fresh_sha_ideal_polys( pub fn absorb_aggregate_sha_ideal_polys( transcript: &mut impl Transcript, ideal_polys: &[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], + field_cfg: &F::Config, ) where F: PrimeField, - F::Inner: ConstTranscribable + Transcribable, + F::Inner: Transcribable, F::Modulus: Transcribable, { - let mut buf = vec![0; F::Inner::NUM_BYTES]; + let mut field_buf = runtime_field_transcript_buf::(field_cfg); transcript.absorb_slice(b"production_sha_aggregate_ideals_begin"); transcript.absorb_slice(&(ideal_polys.len() as u64).to_le_bytes()); for (family_idx, poly) in ideal_polys.iter().enumerate() { transcript.absorb_slice(&(family_idx as u64).to_le_bytes()); transcript.absorb_slice(&(poly.coeffs.len() as u64).to_le_bytes()); - transcript.absorb_random_field_slice(&poly.coeffs, &mut buf); + transcript.absorb_random_field_slice(&poly.coeffs, &mut field_buf); } transcript.absorb_slice(b"production_sha_aggregate_ideals_end"); } @@ -667,24 +803,25 @@ pub fn absorb_aggregate_sha_ideal_polys( pub fn absorb_sha_endpoint_evals( transcript: &mut impl Transcript, endpoint_evals: &ShaEndpointEvals, + field_cfg: &F::Config, ) where F: PrimeField, - F::Inner: ConstTranscribable + Transcribable, + F::Inner: Transcribable, F::Modulus: Transcribable, { - let mut buf = vec![0; F::Inner::NUM_BYTES]; + let mut field_buf = runtime_field_transcript_buf::(field_cfg); transcript.absorb_slice(b"production_sha_endpoint_evals_begin"); transcript.absorb_slice(&(endpoint_evals.sources.len() as u64).to_le_bytes()); for source in &endpoint_evals.sources { transcript.absorb_slice(&(source.col.index() as u64).to_le_bytes()); transcript.absorb_slice(&(source.shift as u64).to_le_bytes()); - transcript.absorb_random_field(&source.scalarized, &mut buf); - transcript.absorb_random_field_slice(&source.bits, &mut buf); + transcript.absorb_random_field(&source.scalarized, &mut field_buf); + transcript.absorb_random_field_slice(&source.bits, &mut field_buf); } transcript.absorb_slice(&(endpoint_evals.int_sources.len() as u64).to_le_bytes()); for source in &endpoint_evals.int_sources { transcript.absorb_slice(&(source.col.index() as u64).to_le_bytes()); - transcript.absorb_random_field(&source.scalar, &mut buf); + transcript.absorb_random_field(&source.scalar, &mut field_buf); } transcript.absorb_slice(b"production_sha_endpoint_evals_end"); } @@ -692,18 +829,19 @@ pub fn absorb_sha_endpoint_evals( pub fn absorb_folded_lifted_evals( transcript: &mut impl Transcript, lifted_evals: &[DynamicPolynomialF], + field_cfg: &F::Config, ) where F: PrimeField, - F::Inner: ConstTranscribable + Transcribable, + F::Inner: Transcribable, F::Modulus: Transcribable, { - let mut buf = vec![0; F::Inner::NUM_BYTES]; + let mut field_buf = runtime_field_transcript_buf::(field_cfg); transcript.absorb_slice(b"production_sha_folded_lifted_evals_begin"); transcript.absorb_slice(&(lifted_evals.len() as u64).to_le_bytes()); for (idx, lifted_eval) in lifted_evals.iter().enumerate() { transcript.absorb_slice(&(idx as u64).to_le_bytes()); transcript.absorb_slice(&(lifted_eval.coeffs.len() as u64).to_le_bytes()); - transcript.absorb_random_field_slice(&lifted_eval.coeffs, &mut buf); + transcript.absorb_random_field_slice(&lifted_eval.coeffs, &mut field_buf); } transcript.absorb_slice(b"production_sha_folded_lifted_evals_end"); } @@ -714,9 +852,9 @@ pub fn sample_pre_ideal_challenge( ) -> [F; SHA_ROW_VARS] where F: DelayedFieldProductSum, - F::Inner: ConstTranscribable, + F::Inner: Transcribable, { - std::array::from_fn(|_| transcript.get_field_challenge(field_cfg)) + std::array::from_fn(|_| transcript.get_transcribable_field_challenge(field_cfg)) } pub fn sample_instance_batch_challenge( @@ -726,7 +864,7 @@ pub fn sample_instance_batch_challenge( ) -> Result, ProductionShaError> where F: PrimeField, - F::Inner: ConstTranscribable, + F::Inner: Transcribable, { if !instance_count.is_power_of_two() { return Err(ProductionShaError::InstanceCountNotPowerOfTwo( @@ -734,7 +872,7 @@ where )); } let ell = usize::try_from(instance_count.trailing_zeros()).expect("ell fits usize"); - Ok(transcript.get_field_challenges(ell, field_cfg)) + Ok(transcript.get_transcribable_field_challenges(ell, field_cfg)) } pub fn sample_post_aggregate_ideal_challenges( @@ -743,13 +881,13 @@ pub fn sample_post_aggregate_ideal_challenges( ) -> (F, F, F, F) where F: PrimeField, - F::Inner: ConstTranscribable, + F::Inner: Transcribable, { ( - transcript.get_field_challenge(field_cfg), - transcript.get_field_challenge(field_cfg), - transcript.get_field_challenge(field_cfg), - transcript.get_field_challenge(field_cfg), + transcript.get_transcribable_field_challenge(field_cfg), + transcript.get_transcribable_field_challenge(field_cfg), + transcript.get_transcribable_field_challenge(field_cfg), + transcript.get_transcribable_field_challenge(field_cfg), ) } @@ -760,7 +898,7 @@ pub fn sample_post_ideal_challenges( ) -> Result<(F, F, F, F, Vec), ProductionShaError> where F: PrimeField, - F::Inner: ConstTranscribable, + F::Inner: Transcribable, { let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); Ok(( @@ -879,7 +1017,7 @@ pub fn prove_linear_ideal_fold( ) -> Result< LinearIdealFoldProveOutput< UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, - FoldedLinearIdealInstance, ProjectedShaPublic>, + FoldedLinearIdealInstance, ProjectedPublic>, FoldedLinearIdealWitness>, ProductionLinearIdealFoldProof, >, @@ -894,9 +1032,10 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Transcribable + Zero + Default + Send + Sync, - F::Modulus: ConstTranscribable + Transcribable, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, P: ZincPCSTypes, + P: ProductionShaFoldedPcsOpen, P::BinaryPCS: FoldablePCS, D>, P::ArbitraryPCS: FoldablePCS, D>, P::IntPCS: FoldablePCS, @@ -958,31 +1097,45 @@ where b"production_sha_fresh_commitments", &instance_commitments, ); - absorb_projected_sha_publics(transcript, &publics); + absorb_projected_sha_publics(transcript, &publics, field_cfg); let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); - let coeff_cache = build_sha_linear_residual_coeff_cache(&traces, &publics, &r_ic, field_cfg)?; + let r_ic_eq_weights = build_eq_x_r_vec(&r_ic, field_cfg)?; + let coeff_tables = build_linear_residual_coeff_tables_with_row_weights( + &traces, + &publics, + &r_ic_eq_weights, + field_cfg, + )?; let beta = sample_instance_batch_challenge(transcript, witnesses.len(), field_cfg)?; - let aggregate_ideal_polys = coeff_cache.beta_aggregate_nonzero_ideal_polys(&beta, field_cfg)?; + let beta_eq_weights = build_eq_x_r_vec(&beta, field_cfg)?; + let aggregate_ideal_polys = + beta_aggregate_nonzero_ideal_polys_with_weights(&coeff_tables, &beta_eq_weights)?; + let ideal_check = IdealCheckProof { + combined_mle_values: aggregate_ideal_polys.iter().cloned().collect(), + }; + let aggregate_ideal_polys = aggregate_sha_ideal_polys_from_proof(&ideal_check)?; check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, field_cfg)?; - absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys); + absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys, field_cfg); let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); let initial_claim = evaluate_aggregate_sha_ideal_claim(&aggregate_ideal_polys, &a, &lambda, field_cfg)?; - let (sumfold_proof, sumfold_output) = prove_optimized_sha_sumfold( + let (sumfold_proof, sumfold_output) = prove_optimized_sha_sumfold_with_weights( transcript, &traces, &publics, &initial_claim, &beta, + &beta_eq_weights, &r_ic, + &r_ic_eq_weights, &a, &lambda, &rho, &xi, &booleanity_sources, - &coeff_cache, + &coeff_tables, pp.prefix_vars, field_cfg, )?; @@ -998,26 +1151,102 @@ where field_cfg, )?; let (folded, folded_public) = - fold_projected_sha_traces(&traces, &publics, &sumfold_output, field_cfg)?; - let _partial_output = ( + fold_projected_traces(&traces, &publics, &sumfold_output, field_cfg)?; + absorb_production_sha_commitments::( + transcript, + b"production_sha_derived_folded_commitments", + std::slice::from_ref(&folded_commitments), + ); + + let (combined_sumcheck, row_output) = + prove_expression_folded_row_sumcheck_with_output_and_weights( + transcript, + &folded.trace, + &folded_public, + &r_ic, + &r_ic_eq_weights, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + sumfold_output.final_round_sumcheck_claim(), + field_cfg, + )?; + let endpoint_evals = build_sha_endpoint_evals_from_trace_with_row_weights( + &folded.trace, + &row_output.r_star_eq_weights, + &a, + field_cfg, + )?; + let resolver = sha_resolver_from_endpoint_evals(&endpoint_evals)?; + absorb_sha_resolver_proof(transcript, &resolver, field_cfg); + let resolver_endpoint_evals = sha_endpoint_evals_from_resolver(&resolver, &a, field_cfg)?; + let terminal = reconstruct_folded_row_terminal_from_endpoints_with_row_weights( + &resolver_endpoint_evals, + &folded_public, + &r_ic, + &row_output.r_star, + &row_output.r_star_eq_weights, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + field_cfg, + )?; + verify_folded_row_terminal_value(&row_output, &terminal)?; + + let (multipoint_eval, r_0) = prove_sha_endpoint_multipoint_with_row_weights( + transcript, + &folded.trace, + &folded_public, + &resolver_endpoint_evals, + &row_output.r_star, + &row_output.r_star_eq_weights, + field_cfg, + )?; + let r_0_eq_weights = build_eq_x_r_vec(&r_0, field_cfg)?; + let witness_lifted_evals = build_folded_sha_pcs_lifted_evals_with_row_weights( + &folded.trace, + &r_0_eq_weights, + field_cfg, + )?; + absorb_folded_lifted_evals(transcript, &witness_lifted_evals, field_cfg); + let opening_proof = P::prove_folded_pcs_opening( + &pp.pcs_params, + &folded_commitments, + &folded.trace, + &folded_prover_data, + &r_0, + &witness_lifted_evals, + field_cfg, + )?; + + Ok(LinearIdealFoldProveOutput { fresh_instances, - FoldedLinearIdealInstance { - target: sumfold_output.t_prime().clone(), + folded_instance: FoldedLinearIdealInstance { + target: sumfold_output.final_round_sumcheck_claim().clone(), commitments: folded_commitments, public: folded_public, }, - FoldedLinearIdealWitness { + folded_witness: FoldedLinearIdealWitness { witness: ProductionShaFoldedWitness { trace: folded.trace, opening_witness: folded_prover_data, }, }, - sumfold_proof, - ); - - Err(ProductionShaError::ProverNotImplemented( - "folded row/multipoint/PCS proof assembly", - )) + proof: ProductionLinearIdealFoldProof { + instance_commitments, + ideal_check, + sumfold_proof, + resolver, + combined_sumcheck, + multipoint_eval, + witness_lifted_evals, + opening_proof, + }, + }) } fn public_uair_trace_view<'a, PolyCoeff, Int, F, const D: usize>( @@ -1178,12 +1407,8 @@ where }); } - let witness = shape.signature.witness_cols(); - validate_production_sha_batch_sizes::( - witness.num_binary_poly_cols(), - witness.num_arbitrary_poly_cols(), - witness.num_int_cols(), - )?; + let (binary, arbitrary, int) = U::production_sha_pcs_batch_sizes(); + validate_production_sha_batch_sizes::(binary, arbitrary, int)?; Ok(VerifiedLinearIdealFoldSetup { pcs_params: params.pcs_params, @@ -1198,7 +1423,7 @@ pub fn verify_linear_ideal_fold( proof: &ProductionLinearIdealFoldProof, transcript: &mut impl Transcript, ) -> Result< - FoldedLinearIdealInstance, ProjectedShaPublic>, + FoldedLinearIdealInstance, ProjectedPublic>, LinearIdealFoldError, > where @@ -1210,8 +1435,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Transcribable + Zero + Default + Send + Sync, - F::Modulus: ConstTranscribable + Transcribable, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, P: ZincPCSTypes, P::BinaryPCS: FoldablePCS, D>, P::ArbitraryPCS: FoldablePCS, D>, @@ -1268,13 +1493,13 @@ where b"production_sha_fresh_commitments", &proof.instance_commitments, ); - absorb_projected_sha_publics(transcript, &publics); + absorb_projected_sha_publics(transcript, &publics, field_cfg); let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); let beta = sample_instance_batch_challenge(transcript, instances.len(), field_cfg)?; let aggregate_ideal_polys = aggregate_sha_ideal_polys_from_proof(&proof.ideal_check)?; check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, field_cfg)?; - absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys); + absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys, field_cfg); let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); let initial_claim = @@ -1287,7 +1512,7 @@ where beta.len(), field_cfg, )?; - let sumfold_output = derive_sha_instance_fold_claim( + let sumfold_output = derive_instance_fold_claim( &beta, verified_sumfold.r_b, verified_sumfold.c_sf, @@ -1308,12 +1533,12 @@ where let row_output = verify_folded_row_sumcheck( transcript, &proof.combined_sumcheck, - sumfold_output.t_prime(), + sumfold_output.final_round_sumcheck_claim(), field_cfg, )?; let folded_public = - fold_projected_sha_publics(&publics, sumfold_output.eq_instance_weights(), field_cfg)?; - absorb_transcribable(transcript, &proof.resolver); + fold_projected_publics(&publics, sumfold_output.eq_instance_weights(), field_cfg)?; + absorb_sha_resolver_proof(transcript, &proof.resolver, field_cfg); let endpoint_evals = sha_endpoint_evals_from_resolver(&proof.resolver, &a, field_cfg)?; let terminal = reconstruct_folded_row_terminal_from_endpoints( &endpoint_evals, @@ -1345,7 +1570,7 @@ where field_cfg, )?; verify_sha_endpoint_multipoint_open_evals(&subclaim, &open_evals, &shift_specs, field_cfg)?; - absorb_folded_lifted_evals(transcript, &proof.witness_lifted_evals); + absorb_folded_lifted_evals(transcript, &proof.witness_lifted_evals, field_cfg); verify_production_sha_pcs_opening::( &vs.pcs_params, &folded_commitments, @@ -1356,7 +1581,7 @@ where )?; Ok(FoldedLinearIdealInstance { - target: sumfold_output.t_prime().clone(), + target: sumfold_output.final_round_sumcheck_claim().clone(), commitments: folded_commitments, public: folded_public, }) @@ -1440,6 +1665,18 @@ where }) } +fn scalarize_sha_endpoint_bits(bits: &[F; SHA_WORD_BITS], a: &F, field_cfg: &F::Config) -> F +where + F: PrimeField, +{ + let powers = zinc_utils::powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); + bits.iter() + .zip(powers.iter()) + .fold(F::zero_with_cfg(field_cfg), |acc, (bit, power)| { + acc + bit.clone() * power + }) +} + fn sha_endpoint_evals_from_resolver( resolver: &CombinedPolyResolverProof, a: &F, @@ -1483,7 +1720,6 @@ where }); } - let powers = zinc_utils::powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); let mut unshifted_idx = 0usize; let mut shifted_idx = 0usize; let mut sources = Vec::with_capacity(word_sources.len()); @@ -1498,12 +1734,7 @@ where &resolver.shifted_bit_slice_evals[start..start + SHA_WORD_BITS] }; let bits: [F; SHA_WORD_BITS] = std::array::from_fn(|idx| bit_slice[idx].clone()); - let scalarized = bits - .iter() - .zip(powers.iter()) - .fold(F::zero_with_cfg(field_cfg), |acc, (bit, power)| { - acc + bit.clone() * power - }); + let scalarized = scalarize_sha_endpoint_bits(&bits, a, field_cfg); sources.push(ShaSourceEndpointEval { col, shift, @@ -1535,6 +1766,37 @@ where }) } +fn sha_resolver_from_endpoint_evals( + endpoint_evals: &ShaEndpointEvals, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + validate_sha_endpoint_layout(endpoint_evals)?; + + let mut bit_slice_evals = Vec::new(); + let mut shifted_bit_slice_evals = Vec::new(); + for source in &endpoint_evals.sources { + if source.shift == 0 { + bit_slice_evals.extend(source.bits.iter().cloned()); + } else { + shifted_bit_slice_evals.extend(source.bits.iter().cloned()); + } + } + + Ok(CombinedPolyResolverProof { + up_evals: endpoint_evals + .int_sources + .iter() + .map(|source| source.scalar.clone()) + .collect(), + down_evals: Vec::new(), + bit_slice_evals, + bit_op_down_evals: Vec::new(), + shifted_bit_slice_evals, + }) +} + fn verify_production_sha_pcs_opening( pcs_params: &PCSVerifierParams, folded_commitments: &PCSCommitments, @@ -1546,8 +1808,8 @@ fn verify_production_sha_pcs_opening( where Zt: ZincTypes, F: PrimeField, - F::Inner: ConstTranscribable + Transcribable, - F::Modulus: ConstTranscribable + Transcribable, + F::Inner: Transcribable, + F::Modulus: Transcribable, P: ZincPCSTypes, { ensure_production_sha_word_degree::()?; @@ -1563,7 +1825,7 @@ where fs_transcript: Blake3Transcript::default(), stream: Cursor::default(), }; - let mut transcription_buf = vec![0u8; F::Inner::NUM_BYTES]; + let mut transcription_buf = vec![0u8; F::zero_with_cfg(field_cfg).inner().get_num_bytes()]; P::BinaryPCS::absorb_commitment(&mut transcript.fs_transcript, &folded_commitments.binary); absorb_pcs_lifted_evals( @@ -1619,7 +1881,137 @@ where Ok(()) } -#[allow(dead_code)] +#[allow(clippy::too_many_arguments)] +fn prove_production_sha_hyrax_pcs_opening( + pcs_params: &PCSParams, Zt, F, D>, + folded_commitments: &PCSCommitments, Zt, F, D>, + folded_trace: &ProjectedTrace, + folded_prover_data: &PCSProverData, Zt, F, D>, + r_0: &[F], + folded_lifted_evals: &[DynamicPolynomialF], + field_cfg: &F::Config, +) -> Result, Zt, F, D>, ProductionShaError> +where + Zt: ZincTypes, + F: HyraxFieldBridge, + F::Inner: Transcribable, + F::Modulus: Transcribable, + C: AffineRepr, + HyraxPCS: PCS< + F, + BinaryPoly, + D, + CommitmentKey = zip_plus::pcs::hyrax::HyraxCommitmentKey, + ProverData = zip_plus::pcs::hyrax::HyraxProverData, + OpeningProof = Vec, + >, + HyraxPCS: PCS< + F, + DensePolynomial, + D, + CommitmentKey = zip_plus::pcs::hyrax::HyraxCommitmentKey, + ProverData = zip_plus::pcs::hyrax::HyraxProverData, + OpeningProof = Vec, + >, + HyraxPCS: PCS< + F, + Zt::Int, + D, + CommitmentKey = zip_plus::pcs::hyrax::HyraxCommitmentKey, + ProverData = zip_plus::pcs::hyrax::HyraxProverData, + OpeningProof = Vec, + >, +{ + ensure_production_sha_word_degree::()?; + validate_production_sha_batch_sizes::( + HyraxPCS::::batch_size(&folded_commitments.binary), + HyraxPCS::::batch_size(&folded_commitments.arbitrary), + HyraxPCS::::batch_size(&folded_commitments.int), + )?; + let (binary_lifted, int_lifted) = split_folded_sha_pcs_lifted_evals(folded_lifted_evals)?; + let arbitrary_lifted: &[DynamicPolynomialF] = &[]; + + let binary_scalar_lanes = folded_sha_binary_scalar_lanes::(folded_trace); + let arbitrary_scalar_lanes: Vec>> = Vec::new(); + let int_scalar_lanes = folded_sha_int_scalar_lanes::(folded_trace); + + let mut transcript = PcsProverTranscript { + fs_transcript: Blake3Transcript::default(), + stream: Cursor::default(), + }; + let mut transcription_buf = vec![0u8; F::zero_with_cfg(field_cfg).inner().get_num_bytes()]; + + HyraxPCS::::absorb_commitment( + &mut transcript.fs_transcript, + &folded_commitments.binary, + ); + absorb_pcs_lifted_evals( + &mut transcript.fs_transcript, + binary_lifted, + &mut transcription_buf, + ); + let binary_start = transcript.stream.position() as usize; + HyraxPCS::::prove_open_scalar_lanes::( + &mut transcript, + &pcs_params.binary, + &binary_scalar_lanes, + r_0, + &folded_prover_data.binary, + field_cfg, + )?; + let binary_end = transcript.stream.position() as usize; + let binary = transcript.stream.get_ref()[binary_start..binary_end].to_vec(); + + HyraxPCS::::absorb_commitment( + &mut transcript.fs_transcript, + &folded_commitments.arbitrary, + ); + absorb_pcs_lifted_evals( + &mut transcript.fs_transcript, + arbitrary_lifted, + &mut transcription_buf, + ); + let arbitrary_start = transcript.stream.position() as usize; + HyraxPCS::::prove_open_scalar_lanes::( + &mut transcript, + &pcs_params.arbitrary, + &arbitrary_scalar_lanes, + r_0, + &folded_prover_data.arbitrary, + field_cfg, + )?; + let arbitrary_end = transcript.stream.position() as usize; + let arbitrary = transcript.stream.get_ref()[arbitrary_start..arbitrary_end].to_vec(); + + HyraxPCS::::absorb_commitment( + &mut transcript.fs_transcript, + &folded_commitments.int, + ); + absorb_pcs_lifted_evals( + &mut transcript.fs_transcript, + int_lifted, + &mut transcription_buf, + ); + let int_start = transcript.stream.position() as usize; + HyraxPCS::::prove_open_scalar_lanes::( + &mut transcript, + &pcs_params.int, + &int_scalar_lanes, + r_0, + &folded_prover_data.int, + field_cfg, + )?; + let int_end = transcript.stream.position() as usize; + let int = transcript.stream.get_ref()[int_start..int_end].to_vec(); + + Ok(PCSOpeningProof { + binary, + arbitrary, + int, + }) +} + +#[cfg(test)] fn evaluate_fresh_targets_from_ideal_polys( ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], a: &F, @@ -1790,124 +2182,184 @@ where })) } -fn fold_projected_sha_publics( - publics: &[ProjectedShaPublic], +fn fold_mle_tables( + kind: &'static str, + tables: &[&MleTable], theta: &[F], field_cfg: &F::Config, -) -> Result, ProductionShaError> +) -> Result, ProductionShaError> where F: PrimeField, { - if publics.len() != theta.len() { + if tables.len() != theta.len() { return Err(ProductionShaError::LengthMismatch { - label: "publics/theta", - got: publics.len(), + label: kind, + got: tables.len(), expected: theta.len(), }); } - let first = publics.first().ok_or(ProductionShaError::LengthMismatch { - label: "publics", + let first = tables.first().ok_or(ProductionShaError::LengthMismatch { + label: kind, got: 0, expected: 1, })?; - let col_count = first.columns.columns.len(); - let row_count = first - .columns - .columns - .first() - .map(|col| col.len()) - .unwrap_or(0); - let mut columns = vec![vec![F::zero_with_cfg(field_cfg); row_count]; col_count]; - let mut word_columns = first.word_columns.as_ref().map(|words| { - vec![vec![vec![F::zero_with_cfg(field_cfg); SHA_WORD_BITS]; row_count]; words.columns.len()] - }); - for (public, weight) in publics.iter().zip(theta.iter()) { - if public.columns.columns.len() != col_count { + let col_count = first.len(); + let first_col = first.first().ok_or(ProductionShaError::LengthMismatch { + label: kind, + got: 0, + expected: 1, + })?; + let row_count = first_col.evaluations.len(); + let num_vars = first_col.num_vars; + let mut folded = vec![vec![F::zero_with_cfg(field_cfg); row_count]; col_count]; + for (table, weight) in tables.iter().zip(theta.iter()) { + if table.len() != col_count { return Err(ProductionShaError::LengthMismatch { - label: "public column count", - got: public.columns.columns.len(), + label: kind, + got: table.len(), expected: col_count, }); } - for (col_idx, col) in public.columns.columns.iter().enumerate() { - if col.len() != row_count { + for (col_idx, column) in table.iter().enumerate() { + if column.num_vars != num_vars { return Err(ProductionShaError::LengthMismatch { - label: "public row count", - got: col.len(), - expected: row_count, + label: kind, + got: column.num_vars, + expected: num_vars, }); } - for (out, value) in columns[col_idx].iter_mut().zip(col.iter()) { - *out += weight.clone() * value; - } - } - match (&mut word_columns, &public.word_columns) { - (Some(out_columns), Some(public_words)) => { - if public_words.columns.len() != out_columns.len() { - return Err(ProductionShaError::LengthMismatch { - label: "public word column count", - got: public_words.columns.len(), - expected: out_columns.len(), - }); - } - for (col_idx, rows) in public_words.columns.iter().enumerate() { - if rows.len() != row_count { - return Err(ProductionShaError::LengthMismatch { - label: "public word row count", - got: rows.len(), - expected: row_count, - }); - } - for (row_idx, bits) in rows.iter().enumerate() { - if bits.len() != SHA_WORD_BITS { - return Err(ProductionShaError::LengthMismatch { - label: "public word bit count", - got: bits.len(), - expected: SHA_WORD_BITS, - }); - } - for (out, value) in out_columns[col_idx][row_idx].iter_mut().zip(bits) { - *out += weight.clone() * value; - } - } - } - } - (None, None) => {} - (Some(_), None) | (None, Some(_)) => { + if column.evaluations.len() != row_count { return Err(ProductionShaError::LengthMismatch { - label: "public word column presence", - got: usize::from(public.word_columns.is_some()), - expected: usize::from(word_columns.is_some()), + label: kind, + got: column.evaluations.len(), + expected: row_count, }); } + for (out, value) in folded[col_idx].iter_mut().zip(column.evaluations.iter()) { + *out += weight.clone() * value; + } } } - Ok(ProjectedShaPublic { - columns: zinc_piop::neutron_nova::ShaPublicColumns { columns }, - word_columns: word_columns.map(|columns| ShaPublicWordColumns { columns }), - }) + Ok(folded + .into_iter() + .map(|evaluations| DenseMultilinearExtension { + evaluations, + num_vars, + }) + .collect()) } -fn validate_production_sha_publics( - publics: &[ProjectedShaPublic], +fn fold_optional_mle_tables( + kind: &'static str, + tables: &[Option<&MleTable>], + theta: &[F], field_cfg: &F::Config, -) -> Result<(), ProductionShaError> +) -> Result>, ProductionShaError> +where + F: PrimeField, +{ + let has_table = tables + .first() + .ok_or(ProductionShaError::LengthMismatch { + label: kind, + got: 0, + expected: 1, + })? + .is_some(); + if !has_table { + if tables.iter().any(Option::is_some) { + return Err(ProductionShaError::LengthMismatch { + label: kind, + got: 1, + expected: 0, + }); + } + return Ok(None); + } + let present = tables + .iter() + .map(|table| { + table.ok_or(ProductionShaError::LengthMismatch { + label: kind, + got: 0, + expected: 1, + }) + }) + .collect::, _>>()?; + fold_mle_tables(kind, &present, theta, field_cfg).map(Some) +} + +fn fold_projected_publics( + publics: &[ProjectedPublic], + theta: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + if publics.len() != theta.len() { + return Err(ProductionShaError::LengthMismatch { + label: "publics/theta", + got: publics.len(), + expected: theta.len(), + }); + } + let first = publics.first().ok_or(ProductionShaError::LengthMismatch { + label: "publics", + got: 0, + expected: 1, + })?; + let columns = fold_mle_tables( + "public columns", + &publics + .iter() + .map(|public| &public.columns) + .collect::>(), + theta, + field_cfg, + )?; + let bit_slices = fold_optional_mle_tables( + "public bit slices", + &publics + .iter() + .map(|public| public.bit_slices.as_ref()) + .collect::>(), + theta, + field_cfg, + )?; + if first.bit_slices.is_none() != bit_slices.is_none() { + return Err(ProductionShaError::LengthMismatch { + label: "public bit slice presence", + got: usize::from(bit_slices.is_some()), + expected: usize::from(first.bit_slices.is_some()), + }); + } + Ok(ProjectedPublic { + columns, + bit_slices, + }) +} + +fn validate_production_sha_publics( + publics: &[ProjectedPublic], + field_cfg: &F::Config, +) -> Result<(), ProductionShaError> where F: PrimeField + FromPrimitiveWithConfig, { for public in publics { - if public.columns.columns.len() != ShaPublicCol::COUNT { + if public.columns.len() != ShaPublicCol::COUNT { return Err(ProductionShaError::LengthMismatch { label: "SHA public column count", - got: public.columns.columns.len(), + got: public.columns.len(), expected: ShaPublicCol::COUNT, }); } - for col in &public.columns.columns { - if col.len() != SHA_ROW_COUNT { + for col in &public.columns { + if col.num_vars != SHA_ROW_VARS || col.evaluations.len() != SHA_ROW_COUNT { return Err(ProductionShaError::LengthMismatch { label: "SHA public row count", - got: col.len(), + got: col.evaluations.len(), expected: SHA_ROW_COUNT, }); } @@ -1920,8 +2372,8 @@ where ShaPublicCol::SFf, ShaPublicCol::SOut, ] { - let col = &public.columns.columns[selector.index()]; - for (row, value) in col.iter().enumerate() { + let col = &public.columns[selector.index()]; + for (row, value) in col.evaluations.iter().enumerate() { let expected = production_sha_selector_expected(selector, row, field_cfg); if value != &expected { if *value != F::zero_with_cfg(field_cfg) && *value != F::one_with_cfg(field_cfg) @@ -1936,17 +2388,113 @@ where } } - let k_col = &public.columns.columns[ShaPublicCol::K.index()]; - for (row, value) in k_col.iter().enumerate() { + let k_col = &public.columns[ShaPublicCol::K.index()]; + for (row, value) in k_col.evaluations.iter().enumerate() { let expected = production_sha_k_expected(row, field_cfg); if value != &expected { return Err(ProductionShaError::InvalidRoundConstant { row }); } } + + validate_production_sha_public_word_columns(public, field_cfg)?; + } + Ok(()) +} + +fn validate_production_sha_public_word_columns( + public: &ProjectedPublic, + field_cfg: &F::Config, +) -> Result<(), ProductionShaError> +where + F: PrimeField, +{ + let bit_slices = + public + .bit_slices + .as_ref() + .ok_or(ProductionShaError::NonCanonicalProofObject( + "production SHA public word columns are required", + ))?; + if bit_slices.len() != ShaPublicWordCol::COUNT * SHA_WORD_BITS { + return Err(ProductionShaError::LengthMismatch { + label: "SHA public word column count", + got: bit_slices.len(), + expected: ShaPublicWordCol::COUNT * SHA_WORD_BITS, + }); + } + + for (word_idx, public_col) in production_sha_public_word_column_map().iter().enumerate() { + let scalar_col = &public.columns[public_col.index()]; + for row in 0..SHA_ROW_COUNT { + let mut bits = Vec::with_capacity(SHA_WORD_BITS); + for bit in 0..SHA_WORD_BITS { + let table_idx = bit_slice_index(word_idx, bit, SHA_WORD_BITS); + let bit_col = + bit_slices + .get(table_idx) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA public word bit column", + got: table_idx, + expected: bit_slices.len(), + })?; + if bit_col.num_vars != SHA_ROW_VARS || bit_col.evaluations.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "SHA public word row count", + got: bit_col.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + let bit = bit_col.evaluations[row].clone(); + if bit != F::zero_with_cfg(field_cfg) && bit != F::one_with_cfg(field_cfg) { + return Err(ProductionShaError::NonCanonicalProofObject( + "production SHA public word bit is not boolean", + )); + } + bits.push(bit); + } + if bits.len() != SHA_WORD_BITS { + return Err(ProductionShaError::LengthMismatch { + label: "SHA public word bit count", + got: bits.len(), + expected: SHA_WORD_BITS, + }); + } + let scalarized = scalarize_sha_public_word_bits_at_two(&bits, field_cfg); + if scalarized != scalar_col.evaluations[row] { + return Err(ProductionShaError::NonCanonicalProofObject( + "production SHA public word bits do not match scalar public column", + )); + } + } + debug_assert_eq!(Some(word_idx), public_word_col_index(*public_col)); } Ok(()) } +fn production_sha_public_word_column_map() -> [ShaPublicCol; ShaPublicWordCol::COUNT] { + [ + ShaPublicCol::PAIn, + ShaPublicCol::PEIn, + ShaPublicCol::PAOut, + ShaPublicCol::PEOut, + ShaPublicCol::Message, + ] +} + +fn scalarize_sha_public_word_bits_at_two(bits: &[F], field_cfg: &F::Config) -> F +where + F: PrimeField, +{ + let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); + let mut power = F::one_with_cfg(field_cfg); + let mut out = F::zero_with_cfg(field_cfg); + for bit in bits { + out += bit.clone() * &power; + power *= &two; + } + out +} + fn production_sha_selector_expected( selector: ShaPublicCol, row: usize, @@ -1984,25 +2532,43 @@ where #[allow(dead_code)] fn build_folded_sha_pcs_lifted_evals( - folded_trace: &ProjectedShaTrace, + folded_trace: &ProjectedTrace, r_0: &[F], field_cfg: &F::Config, ) -> Result>, ProductionShaError> where F: PrimeField + DelayedFieldProductSum, { + let row_weights = build_eq_x_r_vec(r_0, field_cfg)?; + build_folded_sha_pcs_lifted_evals_with_row_weights(folded_trace, &row_weights, field_cfg) +} + +fn build_folded_sha_pcs_lifted_evals_with_row_weights( + folded_trace: &ProjectedTrace, + row_weights: &[F], + field_cfg: &F::Config, +) -> Result>, ProductionShaError> +where + F: PrimeField + DelayedFieldProductSum, +{ + if row_weights.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "row weights", + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let mut lifted = Vec::with_capacity(ShaWordCol::COUNT + ShaIntCol::COUNT); for col in ShaWordCol::ALL { - let coeffs = sha_word_bits_at_point(folded_trace, col, 0, r_0, field_cfg)?.to_vec(); + let coeffs = + sha_word_bits_at_point_with_weights(folded_trace, col, 0, row_weights, field_cfg)? + .to_vec(); lifted.push(DynamicPolynomialF::new_trimmed(coeffs)); } for col in ShaIntCol::ALL { - lifted.push(DynamicPolynomialF::new_trimmed([sha_int_at_point( - folded_trace, - col, - r_0, - field_cfg, - )?])); + lifted.push(DynamicPolynomialF::new_trimmed([ + sha_int_at_point_with_weights(folded_trace, col, row_weights, field_cfg)?, + ])); } Ok(lifted) } @@ -2053,7 +2619,7 @@ where #[allow(dead_code)] fn folded_sha_binary_scalar_lanes( - folded_trace: &ProjectedShaTrace, + folded_trace: &ProjectedTrace, ) -> Vec>> where C: AffineRepr, @@ -2064,12 +2630,10 @@ where .map(|col| { (0..32) .map(|bit| { + let column = + &folded_trace.bit_slices[bit_slice_index(col.index(), bit, SHA_WORD_BITS)]; (0..SHA_ROW_COUNT) - .map(|row| { - F::field_to_scalar( - &folded_trace.bit_slices.columns[col.index()][row][bit], - ) - }) + .map(|row| F::field_to_scalar(&column.evaluations[row])) .collect::>() }) .collect::>() @@ -2079,7 +2643,7 @@ where #[allow(dead_code)] fn folded_sha_int_scalar_lanes( - folded_trace: &ProjectedShaTrace, + folded_trace: &ProjectedTrace, ) -> Vec>> where C: AffineRepr, @@ -2088,11 +2652,10 @@ where ShaIntCol::ALL .iter() .map(|col| { + let column = &folded_trace.int_columns[col.index()]; vec![ (0..SHA_ROW_COUNT) - .map(|row| { - F::field_to_scalar(&folded_trace.int_columns.columns[col.index()][row]) - }) + .map(|row| F::field_to_scalar(&column.evaluations[row])) .collect::>(), ] }) @@ -2105,7 +2668,7 @@ fn absorb_pcs_lifted_evals( transcription_buf: &mut Vec, ) where F: PrimeField, - F::Inner: ConstTranscribable + Transcribable, + F::Inner: Transcribable, F::Modulus: Transcribable, { for lifted_eval in lifted_evals { @@ -2116,7 +2679,7 @@ fn absorb_pcs_lifted_evals( fn multipoint_open_evals_from_pcs_lifted( lifted_evals: &[DynamicPolynomialF], layout: &ShaMultipointLayout, - folded_public: &ProjectedShaPublic, + folded_public: &ProjectedPublic, r_0: &[F], field_cfg: &F::Config, ) -> Result, ProductionShaError> @@ -2152,7 +2715,7 @@ pub fn prove_sha_sumfold_targets( beta: &[F], prefix_vars: usize, field_cfg: &F::Config, -) -> Result<(MultiDegreeSumcheckProof, ShaSumFoldOutput), ProductionShaError> +) -> Result<(MultiDegreeSumcheckProof, InstanceFoldClaim), ProductionShaError> where F: InnerTransparentField + DelayedFieldProductSum @@ -2160,8 +2723,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, { let claims = zinc_piop::neutron_nova::LinearInstanceClaims::new(fresh_targets.to_vec())?; let group = claims.build_hybrid_sumcheck_group(beta, prefix_vars, field_cfg)?; @@ -2177,7 +2740,7 @@ where .randomness .clone(); let c_sf = sumfold_expected_eval(beta, fresh_targets, &r_b, field_cfg)?; - let output = derive_sha_instance_fold_claim(beta, r_b, c_sf, fresh_targets.len(), field_cfg)?; + let output = derive_instance_fold_claim(beta, r_b, c_sf, fresh_targets.len(), field_cfg)?; Ok((proof, output)) } @@ -2187,7 +2750,7 @@ pub fn verify_sha_sumfold_targets( fresh_targets: &[F], beta: &[F], field_cfg: &F::Config, -) -> Result, ProductionShaError> +) -> Result, ProductionShaError> where F: InnerTransparentField + DelayedFieldProductSum @@ -2195,8 +2758,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, { require_single_sumcheck_group(proof, "SHA SumFold")?; for °ree in proof.degrees() { @@ -2212,7 +2775,7 @@ where if c_sf != sumfold_expected_eval(beta, fresh_targets, &r_b, field_cfg)? { return Err(ProductionShaError::SumFoldTerminalMismatch); } - Ok(derive_sha_instance_fold_claim( + Ok(derive_instance_fold_claim( beta, r_b, c_sf, @@ -2224,8 +2787,8 @@ where #[allow(clippy::too_many_arguments)] pub fn prove_full_sha_sumfold( transcript: &mut impl Transcript, - traces: &[ProjectedShaTrace], - publics: &[ProjectedShaPublic], + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], initial_claim: &F, beta: &[F], r_ic: &[F; SHA_ROW_VARS], @@ -2235,7 +2798,7 @@ pub fn prove_full_sha_sumfold( xi: &F, booleanity_sources: &[ShaBooleanitySource], field_cfg: &F::Config, -) -> Result<(MultiDegreeSumcheckProof, ShaSumFoldOutput), ProductionShaError> +) -> Result<(MultiDegreeSumcheckProof, InstanceFoldClaim), ProductionShaError> where F: InnerTransparentField + DelayedFieldProductSum @@ -2243,8 +2806,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, { let group = build_dense_sha_sumfold_group( traces, @@ -2275,19 +2838,15 @@ where })? .randomness .clone(); - let provisional = derive_sha_instance_fold_claim( + let provisional = derive_instance_fold_claim( beta, r_b.clone(), F::one_with_cfg(field_cfg), traces.len(), field_cfg, )?; - let (folded, folded_public) = zinc_piop::neutron_nova::fold_projected_sha_traces( - traces, - publics, - &provisional, - field_cfg, - )?; + let (folded, folded_public) = + zinc_piop::neutron_nova::fold_projected_traces(traces, publics, &provisional, field_cfg)?; let t_prime = zinc_piop::neutron_nova::expression_folded_row_sum( &folded.trace, &folded_public, @@ -2303,15 +2862,15 @@ where let c_sf = d * t_prime; Ok(( proof, - derive_sha_instance_fold_claim(beta, r_b, c_sf, traces.len(), field_cfg)?, + derive_instance_fold_claim(beta, r_b, c_sf, traces.len(), field_cfg)?, )) } #[allow(clippy::too_many_arguments)] pub fn prove_optimized_sha_sumfold( transcript: &mut impl Transcript, - traces: &[ProjectedShaTrace], - publics: &[ProjectedShaPublic], + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], initial_claim: &F, beta: &[F], r_ic: &[F; SHA_ROW_VARS], @@ -2320,10 +2879,10 @@ pub fn prove_optimized_sha_sumfold( rho: &F, xi: &F, booleanity_sources: &[ShaBooleanitySource], - linear_cache: &ShaLinearResidualCoeffCache, + coeff_tables: &[LinearResidualCoeffTable], prefix_vars: usize, field_cfg: &F::Config, -) -> Result<(MultiDegreeSumcheckProof, ShaSumFoldOutput), ProductionShaError> +) -> Result<(MultiDegreeSumcheckProof, InstanceFoldClaim), ProductionShaError> where F: InnerTransparentField + DelayedFieldProductSum @@ -2331,15 +2890,68 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, { - let group = build_production_sha_sumfold_group_with_linear_cache( + let beta_eq_weights = build_eq_x_r_vec(beta, field_cfg)?; + let r_ic_eq_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + prove_optimized_sha_sumfold_with_weights( + transcript, traces, publics, - linear_cache, + initial_claim, beta, + &beta_eq_weights, r_ic, + &r_ic_eq_weights, + a, + lambda, + rho, + xi, + booleanity_sources, + coeff_tables, + prefix_vars, + field_cfg, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn prove_optimized_sha_sumfold_with_weights( + transcript: &mut impl Transcript, + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], + initial_claim: &F, + beta: &[F], + beta_eq_weights: &[F], + r_ic: &[F; SHA_ROW_VARS], + r_ic_eq_weights: &[F], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + coeff_tables: &[LinearResidualCoeffTable], + prefix_vars: usize, + field_cfg: &F::Config, +) -> Result<(MultiDegreeSumcheckProof, InstanceFoldClaim), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, +{ + let group = build_production_sha_sumfold_group_with_linear_cache_and_weights( + traces, + publics, + coeff_tables, + beta, + beta_eq_weights, + r_ic, + r_ic_eq_weights, a, lambda, rho, @@ -2365,23 +2977,19 @@ where })? .randomness .clone(); - let provisional = derive_sha_instance_fold_claim( + let provisional = derive_instance_fold_claim( beta, r_b.clone(), F::one_with_cfg(field_cfg), traces.len(), field_cfg, )?; - let (folded, folded_public) = zinc_piop::neutron_nova::fold_projected_sha_traces( - traces, - publics, - &provisional, - field_cfg, - )?; - let t_prime = zinc_piop::neutron_nova::expression_folded_row_sum( + let (folded, folded_public) = + zinc_piop::neutron_nova::fold_projected_traces(traces, publics, &provisional, field_cfg)?; + let t_prime = expression_folded_row_sum_with_row_weights( &folded.trace, &folded_public, - r_ic, + r_ic_eq_weights, a, lambda, rho, @@ -2393,7 +3001,7 @@ where let c_sf = d * t_prime; Ok(( proof, - derive_sha_instance_fold_claim(beta, r_b, c_sf, traces.len(), field_cfg)?, + derive_instance_fold_claim(beta, r_b, c_sf, traces.len(), field_cfg)?, )) } @@ -2411,8 +3019,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, { require_single_sumcheck_group(proof, "SHA SumFold")?; for °ree in proof.degrees() { @@ -2529,8 +3137,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, { let claimed = folded_row_integrand_sum(row_integrand_values, field_cfg)?; verify_folded_row_sumcheck_claim(&claimed, t_prime)?; @@ -2540,11 +3148,586 @@ where Ok(proof) } +#[derive(Clone)] +struct RowExpressionLayout { + word_sources: Vec<(ShaWordCol, usize)>, + int_sources: Vec, + public_scalar_sources: Vec<(ShaPublicCol, usize)>, + public_word_sources: Vec, + word_offset: usize, + int_offset: usize, + public_scalar_offset: usize, + public_word_offset: usize, +} + +impl RowExpressionLayout { + fn new() -> Self { + let word_sources = production_sha_endpoint_word_sources(); + let int_sources = production_sha_endpoint_int_sources(); + let mut public_scalar_sources = ShaPublicCol::ALL + .iter() + .copied() + .map(|col| (col, 0)) + .collect::>(); + public_scalar_sources.push((ShaPublicCol::K, 3)); + let public_word_sources = vec![ + ShaPublicCol::PAIn, + ShaPublicCol::PEIn, + ShaPublicCol::PAOut, + ShaPublicCol::PEOut, + ShaPublicCol::Message, + ]; + let word_offset = 1; + let int_offset = word_offset + word_sources.len() * SHA_WORD_BITS; + let public_scalar_offset = int_offset + int_sources.len(); + let public_word_offset = public_scalar_offset + public_scalar_sources.len(); + Self { + word_sources, + int_sources, + public_scalar_sources, + public_word_sources, + word_offset, + int_offset, + public_scalar_offset, + public_word_offset, + } + } + + fn public_word_index(&self, col: ShaPublicCol) -> Option { + self.public_word_sources + .iter() + .position(|candidate| *candidate == col) + } +} + +fn trace_word_bit_at_row( + trace: &ProjectedTrace, + col: ShaWordCol, + row: usize, + shift: usize, + bit: usize, + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + if bit >= SHA_WORD_BITS { + return Err(ProductionShaError::LengthMismatch { + label: "SHA word bit index", + got: bit, + expected: SHA_WORD_BITS, + }); + } + let Some(shifted) = row.checked_add(shift) else { + return Ok(F::zero_with_cfg(field_cfg)); + }; + if shifted >= SHA_ROW_COUNT { + return Ok(F::zero_with_cfg(field_cfg)); + } + trace + .bit_slices + .get(bit_slice_index(col.index(), bit, SHA_WORD_BITS)) + .and_then(|column| column.evaluations.get(shifted)) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA trace word bit", + got: shifted, + expected: SHA_ROW_COUNT, + }) +} + +fn trace_int_at_row( + trace: &ProjectedTrace, + col: ShaIntCol, + row: usize, + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + if row >= SHA_ROW_COUNT { + return Ok(F::zero_with_cfg(field_cfg)); + } + trace + .int_columns + .get(col.index()) + .and_then(|column| column.evaluations.get(row)) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA trace int row", + got: row, + expected: SHA_ROW_COUNT, + }) +} + +fn public_scalar_at_row( + public: &ProjectedPublic, + col: ShaPublicCol, + row: usize, + shift: usize, + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + let Some(shifted) = row.checked_add(shift) else { + return Ok(F::zero_with_cfg(field_cfg)); + }; + if shifted >= SHA_ROW_COUNT { + return Ok(F::zero_with_cfg(field_cfg)); + } + public + .columns + .get(col.index()) + .and_then(|column| column.evaluations.get(shifted)) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA public scalar row", + got: shifted, + expected: SHA_ROW_COUNT, + }) +} + +fn public_word_col_index(col: ShaPublicCol) -> Option { + match col { + ShaPublicCol::PAIn => Some(0), + ShaPublicCol::PEIn => Some(1), + ShaPublicCol::PAOut => Some(2), + ShaPublicCol::PEOut => Some(3), + ShaPublicCol::Message => Some(4), + _ => None, + } +} + +fn public_word_bit_at_row( + public: &ProjectedPublic, + col: ShaPublicCol, + row: usize, + bit: usize, + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + if bit >= SHA_WORD_BITS { + return Err(ProductionShaError::LengthMismatch { + label: "SHA public word bit index", + got: bit, + expected: SHA_WORD_BITS, + }); + } + if row >= SHA_ROW_COUNT { + return Ok(F::zero_with_cfg(field_cfg)); + } + let col_idx = public_word_col_index(col).ok_or(ProductionShaError::NonCanonicalProofObject( + "SHA public column is not a public word", + ))?; + let bit_slices = + public + .bit_slices + .as_ref() + .ok_or(ProductionShaError::NonCanonicalProofObject( + "production SHA public word columns are required", + ))?; + let table_idx = bit_slice_index(col_idx, bit, SHA_WORD_BITS); + bit_slices + .get(table_idx) + .and_then(|column| column.evaluations.get(row)) + .cloned() + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA public word bit row", + got: row, + expected: SHA_ROW_COUNT, + }) +} + +fn production_sha_pow_two(exp: usize, field_cfg: &F::Config) -> F +where + F: PrimeField, +{ + let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); + let mut out = F::one_with_cfg(field_cfg); + for _ in 0..exp { + out *= &two; + } + out +} + +#[allow(clippy::too_many_arguments)] +fn build_production_sha_row_expression_sumcheck_group( + trace: &ProjectedTrace, + public: &ProjectedPublic, + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + build_production_sha_row_expression_sumcheck_group_with_row_weights( + trace, + public, + &row_weights, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + ) +} + +#[allow(clippy::too_many_arguments)] +fn build_production_sha_row_expression_sumcheck_group_with_row_weights( + trace: &ProjectedTrace, + public: &ProjectedPublic, + row_weights: &[F], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + if row_weights.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "row weights", + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } + let zero = F::zero_with_cfg(field_cfg); + let zero_inner = zero.inner().clone(); + let layout = RowExpressionLayout::new(); + let mut mles = Vec::with_capacity( + 1 + layout.word_sources.len() * SHA_WORD_BITS + + layout.int_sources.len() + + layout.public_scalar_sources.len() + + layout.public_word_sources.len() * SHA_WORD_BITS, + ); + + mles.push(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + row_weights + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner.clone(), + )); + + for &(col, shift) in &layout.word_sources { + for bit in 0..SHA_WORD_BITS { + let values = (0..SHA_ROW_COUNT) + .map(|row| trace_word_bit_at_row(trace, col, row, shift, bit, field_cfg)) + .collect::, _>>()?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + values.iter().map(|value| value.inner().clone()).collect(), + zero_inner.clone(), + )); + } + } + + for &col in &layout.int_sources { + let values = (0..SHA_ROW_COUNT) + .map(|row| trace_int_at_row(trace, col, row, field_cfg)) + .collect::, _>>()?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + values.iter().map(|value| value.inner().clone()).collect(), + zero_inner.clone(), + )); + } + + for &(col, shift) in &layout.public_scalar_sources { + let values = (0..SHA_ROW_COUNT) + .map(|row| public_scalar_at_row(public, col, row, shift, field_cfg)) + .collect::, _>>()?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + values.iter().map(|value| value.inner().clone()).collect(), + zero_inner.clone(), + )); + } + + for &col in &layout.public_word_sources { + for bit in 0..SHA_WORD_BITS { + let values = (0..SHA_ROW_COUNT) + .map(|row| public_word_bit_at_row(public, col, row, bit, field_cfg)) + .collect::, _>>()?; + mles.push(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + values.iter().map(|value| value.inner().clone()).collect(), + zero_inner.clone(), + )); + } + } + + let a_powers = zinc_utils::powers( + a.clone(), + F::one_with_cfg(field_cfg), + SHA_IDEAL_EVAL_POWER_COUNT, + ); + let lambda_powers = zinc_utils::powers( + lambda.clone(), + F::one_with_cfg(field_cfg), + NUM_SHA_RESIDUAL_FAMILIES, + ); + let rho_powers = zinc_utils::powers( + rho.clone(), + F::one_with_cfg(field_cfg), + booleanity_sources.len(), + ); + let xi = xi.clone(); + let booleanity_sources = booleanity_sources.to_vec(); + let one = F::one_with_cfg(field_cfg); + let two = one.clone() + &one; + let rho_sig0 = sparse_endpoint_poly::(&[10, 19, 30], field_cfg); + let rho_sig1 = sparse_endpoint_poly::(&[7, 21, 26], field_cfg); + let low_mu_coeff = production_sha_pow_two(32, field_cfg); + let high_mu_w_coeff = production_sha_pow_two(34, field_cfg); + let high_mu_3_bit_coeff = production_sha_pow_two(35, field_cfg); + let high_mu_1_bit_coeff = production_sha_pow_two(33, field_cfg); + + Ok(MultiDegreeSumcheckGroup::new( + 3, + mles, + Box::new(move |values: &[F]| { + let zero = zero.clone(); + let const_poly = |value: F| { + if F::is_zero(&value) { + DynamicPolynomialF::ZERO + } else { + DynamicPolynomialF::new_trimmed([value]) + } + }; + let eval_poly = |poly: &DynamicPolynomialF| { + if poly.coeffs.is_empty() { + return zero.clone(); + } + DynamicPolyFInnerProduct::inner_product::( + &poly.coeffs, + &a_powers[..poly.coeffs.len()], + zero.clone(), + ) + .expect("row expression polynomial degree is bounded") + }; + + let word_bits_by_source = (0..layout.word_sources.len()) + .map(|source_idx| { + std::array::from_fn(|bit| { + values[layout.word_offset + source_idx * SHA_WORD_BITS + bit].clone() + }) + }) + .collect::>(); + let public_word_bits_by_source = (0..layout.public_word_sources.len()) + .map(|source_idx| { + std::array::from_fn(|bit| { + values[layout.public_word_offset + source_idx * SHA_WORD_BITS + bit].clone() + }) + }) + .collect::>(); + + let word_source_idx = |col: ShaWordCol, shift: usize| { + layout + .word_sources + .iter() + .position(|source| *source == (col, shift)) + .expect("row expression word source is present") + }; + let word_bits = |col: ShaWordCol, shift: usize| -> &[F; SHA_WORD_BITS] { + &word_bits_by_source[word_source_idx(col, shift)] + }; + let word_poly = |col: ShaWordCol, shift: usize| { + DynamicPolynomialF::new_trimmed(word_bits(col, shift).to_vec()) + }; + let int_value = |col: ShaIntCol| { + let idx = layout + .int_sources + .iter() + .position(|candidate| *candidate == col) + .expect("row expression int source is present"); + values[layout.int_offset + idx].clone() + }; + let int_poly = |col: ShaIntCol| const_poly(int_value(col)); + let public_scalar = |col: ShaPublicCol, shift: usize| { + let idx = layout + .public_scalar_sources + .iter() + .position(|source| *source == (col, shift)) + .expect("row expression public scalar source is present"); + values[layout.public_scalar_offset + idx].clone() + }; + let public_word_or_const_poly = |col: ShaPublicCol| { + if let Some(idx) = layout.public_word_index(col) { + DynamicPolynomialF::new_trimmed(public_word_bits_by_source[idx].to_vec()) + } else { + const_poly(public_scalar(col, 0)) + } + }; + + let a_word = word_poly(ShaWordCol::A, 0); + let e_word = word_poly(ShaWordCol::E, 0); + let sigma0 = word_poly(ShaWordCol::Sigma0, 0); + let sigma1 = word_poly(ShaWordCol::Sigma1, 0); + let w = word_poly(ShaWordCol::W, 0); + let small_sigma0 = word_poly(ShaWordCol::SmallSigma0, 0); + let small_sigma1 = word_poly(ShaWordCol::SmallSigma1, 0); + let ov_sigma0 = word_poly(ShaWordCol::OvSigma0, 0); + let ov_sigma1 = word_poly(ShaWordCol::OvSigma1, 0); + let ov_small_sigma0 = word_poly(ShaWordCol::OvSmallSigma0, 0); + let ov_small_sigma1 = word_poly(ShaWordCol::OvSmallSigma1, 0); + + let mu = |low: u32, high: u32, high_coeff: &F| { + let packed = word_poly(ShaWordCol::MuPacked, 0).shift_r_c(low); + let tail = word_poly(ShaWordCol::MuPacked, 0).shift_r_c(high); + scale_endpoint_poly(&packed, &low_mu_coeff) + - &scale_endpoint_poly(&tail, high_coeff) + }; + let mu_w = mu(0, 2, &high_mu_w_coeff); + let mu_a = mu(2, 5, &high_mu_3_bit_coeff); + let mu_e = mu(5, 8, &high_mu_3_bit_coeff); + let mu_ff_a = mu(8, 9, &high_mu_1_bit_coeff); + let mu_ff_e = mu(9, 10, &high_mu_1_bit_coeff); + + let r0 = (&a_word * &rho_sig0) - &sigma0 - &scale_endpoint_poly(&ov_sigma0, &two); + let r1 = (&e_word * &rho_sig1) - &sigma1 - &scale_endpoint_poly(&ov_sigma1, &two); + let r2 = word_poly(ShaWordCol::W, 0).rot_c(25) + + &word_poly(ShaWordCol::W, 0).rot_c(14) + + &word_poly(ShaWordCol::W, 0).shift_r_c(3) + - &small_sigma0 + - &scale_endpoint_poly(&ov_small_sigma0, &two); + let r3 = word_poly(ShaWordCol::W, 0).rot_c(15) + + &word_poly(ShaWordCol::W, 0).rot_c(13) + + &word_poly(ShaWordCol::W, 0).shift_r_c(10) + - &small_sigma1 + - &scale_endpoint_poly(&ov_small_sigma1, &two); + let r4 = word_poly(ShaWordCol::W, 16) + - &w + - &word_poly(ShaWordCol::SmallSigma0, 1) + - &word_poly(ShaWordCol::W, 9) + - &word_poly(ShaWordCol::SmallSigma1, 14) + + &mu_w + + &int_poly(ShaIntCol::CompSchedule); + let r5 = word_poly(ShaWordCol::A, 4) + - &e_word + - &word_poly(ShaWordCol::Sigma1, 3) + - &word_poly(ShaWordCol::Uef, 3) + - &word_poly(ShaWordCol::UNegEg, 3) + - &const_poly(public_scalar(ShaPublicCol::K, 3)) + - &w + - &word_poly(ShaWordCol::Sigma0, 3) + - &word_poly(ShaWordCol::Maj, 3) + + &mu_a + + &int_poly(ShaIntCol::CompUpdateA); + let r6 = word_poly(ShaWordCol::E, 4) + - &a_word + - &e_word + - &word_poly(ShaWordCol::Sigma1, 3) + - &word_poly(ShaWordCol::Uef, 3) + - &word_poly(ShaWordCol::UNegEg, 3) + - &const_poly(public_scalar(ShaPublicCol::K, 3)) + - &w + + &mu_e + + &int_poly(ShaIntCol::CompUpdateE); + + let s_init = public_scalar(ShaPublicCol::SInit, 0); + let s_msg = public_scalar(ShaPublicCol::SMsg, 0); + let s_sched = public_scalar(ShaPublicCol::SSched, 0); + let s_upd = public_scalar(ShaPublicCol::SUpd, 0); + let s_ff = public_scalar(ShaPublicCol::SFf, 0); + let s_out = public_scalar(ShaPublicCol::SOut, 0); + + let r7 = scale_endpoint_poly( + &(a_word.clone() - &public_word_or_const_poly(ShaPublicCol::PAIn)), + &s_init, + ) + &scale_endpoint_poly( + &(a_word.clone() - &public_word_or_const_poly(ShaPublicCol::PAOut)), + &s_out, + ); + let r8 = scale_endpoint_poly( + &(e_word.clone() - &public_word_or_const_poly(ShaPublicCol::PEIn)), + &s_init, + ) + &scale_endpoint_poly( + &(e_word.clone() - &public_word_or_const_poly(ShaPublicCol::PEOut)), + &s_out, + ); + let r9 = word_poly(ShaWordCol::A, 4) + - &a_word + - &const_poly(public_scalar(ShaPublicCol::PAIn, 0)) + + &mu_ff_a + + &int_poly(ShaIntCol::CompFeedForwardA); + let r10 = word_poly(ShaWordCol::E, 4) + - &e_word + - &const_poly(public_scalar(ShaPublicCol::PEIn, 0)) + + &mu_ff_e + + &int_poly(ShaIntCol::CompFeedForwardE); + let r11 = scale_endpoint_poly( + &(w - &public_word_or_const_poly(ShaPublicCol::Message)), + &s_msg, + ); + let r12 = scale_endpoint_poly(&int_poly(ShaIntCol::CompSchedule), &s_sched); + let r13 = scale_endpoint_poly(&int_poly(ShaIntCol::CompUpdateA), &s_upd); + let r14 = scale_endpoint_poly(&int_poly(ShaIntCol::CompUpdateE), &s_upd); + let r15 = scale_endpoint_poly(&int_poly(ShaIntCol::CompFeedForwardA), &s_ff); + let r16 = scale_endpoint_poly(&int_poly(ShaIntCol::CompFeedForwardE), &s_ff); + let r17 = word_poly(ShaWordCol::MuPacked, 0).shift_r_c(10); + let residuals = [ + r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, + ]; + let mut linear = zero.clone(); + for (residual, weight) in residuals.iter().zip(lambda_powers.iter()) { + linear += weight.clone() * eval_poly(residual); + } + + let source_bit = + |col: ShaWordCol, shift: usize, bit: usize| word_bits(col, shift)[bit].clone(); + let mut bool_sum = zero.clone(); + for (source, rho_power) in booleanity_sources.iter().zip(rho_powers.iter()) { + let d = match *source { + ShaBooleanitySource::WordBit { col, bit } => source_bit(col, 0, bit), + ShaBooleanitySource::VirtualCh1 { bit: bit_idx } => { + source_bit(ShaWordCol::E, 2, bit_idx) + + &source_bit(ShaWordCol::E, 1, bit_idx) + - two.clone() * source_bit(ShaWordCol::Uef, 2, bit_idx) + } + ShaBooleanitySource::VirtualCh2 { bit: bit_idx } => { + source_bit(ShaWordCol::E, 2, bit_idx) + - &source_bit(ShaWordCol::E, 0, bit_idx) + + two.clone() * source_bit(ShaWordCol::UNegEg, 2, bit_idx) + + two.clone() * source_bit(ShaWordCol::Ch2Comp, 0, bit_idx) + } + ShaBooleanitySource::VirtualMaj { bit: bit_idx } => { + source_bit(ShaWordCol::A, 0, bit_idx) + + &source_bit(ShaWordCol::A, 1, bit_idx) + + &source_bit(ShaWordCol::A, 2, bit_idx) + - two.clone() * source_bit(ShaWordCol::Maj, 2, bit_idx) + - two.clone() * source_bit(ShaWordCol::MajComp, 0, bit_idx) + } + }; + bool_sum += rho_power.clone() * d.clone() * (d - one.clone()); + } + + values[0].clone() * (linear + xi.clone() * bool_sum) + }), + )) +} + #[allow(clippy::too_many_arguments)] pub fn prove_expression_folded_row_sumcheck( transcript: &mut impl Transcript, - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, r_ic: &[F; SHA_ROW_VARS], a: &F, lambda: &F, @@ -2561,8 +3744,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, { let claimed = zinc_piop::neutron_nova::expression_folded_row_sum( trace, @@ -2576,7 +3759,7 @@ where field_cfg, )?; verify_folded_row_sumcheck_claim(&claimed, t_prime)?; - let group = build_expression_folded_row_sumcheck_group( + let group = build_production_sha_row_expression_sumcheck_group( trace, public, r_ic, @@ -2595,8 +3778,8 @@ where #[allow(clippy::too_many_arguments)] pub fn prove_expression_folded_row_sumcheck_with_output( transcript: &mut impl Transcript, - trace: &ProjectedShaTrace, - public: &ProjectedShaPublic, + trace: &ProjectedTrace, + public: &ProjectedPublic, r_ic: &[F; SHA_ROW_VARS], a: &F, lambda: &F, @@ -2613,13 +3796,55 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, { - let claimed = zinc_piop::neutron_nova::expression_folded_row_sum( + let r_ic_eq_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + prove_expression_folded_row_sumcheck_with_output_and_weights( + transcript, trace, public, r_ic, + &r_ic_eq_weights, + a, + lambda, + rho, + xi, + booleanity_sources, + t_prime, + field_cfg, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn prove_expression_folded_row_sumcheck_with_output_and_weights( + transcript: &mut impl Transcript, + trace: &ProjectedTrace, + public: &ProjectedPublic, + r_ic: &[F; SHA_ROW_VARS], + r_ic_eq_weights: &[F], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + t_prime: &F, + field_cfg: &F::Config, +) -> Result<(MultiDegreeSumcheckProof, FoldedRowSumcheckOutput), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, +{ + let claimed = expression_folded_row_sum_with_row_weights( + trace, + public, + r_ic_eq_weights, a, lambda, rho, @@ -2628,10 +3853,10 @@ where field_cfg, )?; verify_folded_row_sumcheck_claim(&claimed, t_prime)?; - let group = build_expression_folded_row_sumcheck_group( + let group = build_production_sha_row_expression_sumcheck_group_with_row_weights( trace, public, - r_ic, + r_ic_eq_weights, a, lambda, rho, @@ -2650,12 +3875,19 @@ where })? .randomness .clone(); - let endpoint_evals = build_sha_endpoint_evals_from_trace(trace, &r_star, field_cfg)?; - let terminal_value = reconstruct_folded_row_terminal_from_endpoints( + let r_star_eq_weights = build_eq_x_r_vec(&r_star, field_cfg)?; + let endpoint_evals = build_sha_endpoint_evals_from_trace_with_row_weights( + trace, + &r_star_eq_weights, + a, + field_cfg, + )?; + let terminal_value = reconstruct_folded_row_terminal_from_endpoints_with_row_weights( &endpoint_evals, public, r_ic, &r_star, + &r_star_eq_weights, a, lambda, rho, @@ -2667,6 +3899,7 @@ where proof, FoldedRowSumcheckOutput { r_star, + r_star_eq_weights, terminal_value, }, )) @@ -2685,8 +3918,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, { require_single_sumcheck_group(proof, "folded row sumcheck")?; for °ree in proof.degrees() { @@ -2704,8 +3937,11 @@ where verify_folded_row_sumcheck_claim(claimed_sum, t_prime)?; let subclaims = MultiDegreeSumcheck::verify_as_subprotocol(transcript, SHA_ROW_VARS, proof, field_cfg)?; + let r_star = subclaims.point().to_vec(); + let r_star_eq_weights = build_eq_x_r_vec(&r_star, field_cfg)?; Ok(FoldedRowSumcheckOutput { - r_star: subclaims.point().to_vec(), + r_star, + r_star_eq_weights, terminal_value: subclaims.expected_evaluations()[0].clone(), }) } @@ -2800,27 +4036,49 @@ pub fn production_sha_multipoint_layout() -> ShaMultipointLayout { } pub fn build_sha_endpoint_evals_from_trace( - trace: &ProjectedShaTrace, + trace: &ProjectedTrace, r_star: &[F], + a: &F, field_cfg: &F::Config, ) -> Result, ProductionShaError> where - F: PrimeField + DelayedFieldProductSum, + F: PrimeField, { + let row_weights = build_eq_x_r_vec(r_star, field_cfg)?; + build_sha_endpoint_evals_from_trace_with_row_weights(trace, &row_weights, a, field_cfg) +} + +pub fn build_sha_endpoint_evals_from_trace_with_row_weights( + trace: &ProjectedTrace, + row_weights: &[F], + a: &F, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + if row_weights.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "row weights", + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let mut sources = Vec::new(); for (col, shift) in production_sha_endpoint_word_sources() { + let bits = sha_word_bits_at_point_with_weights(trace, col, shift, row_weights, field_cfg)?; sources.push(ShaSourceEndpointEval { col, shift, - scalarized: sha_scalarized_word_at_point(trace, col, shift, r_star, field_cfg)?, - bits: sha_word_bits_at_point(trace, col, shift, r_star, field_cfg)?, + scalarized: scalarize_sha_endpoint_bits(&bits, a, field_cfg), + bits, }); } let mut int_sources = Vec::new(); for col in production_sha_endpoint_int_sources() { int_sources.push(ShaIntEndpointEval { col, - scalar: sha_int_at_point(trace, col, r_star, field_cfg)?, + scalar: sha_int_at_point_with_weights(trace, col, row_weights, field_cfg)?, }); } Ok(ShaEndpointEvals { @@ -2871,8 +4129,8 @@ where pub fn prove_sha_endpoint_multipoint( transcript: &mut impl Transcript, - folded_trace: &ProjectedShaTrace, - folded_public: &ProjectedShaPublic, + folded_trace: &ProjectedTrace, + folded_public: &ProjectedPublic, endpoint_evals: &ShaEndpointEvals, r_star: &[F], field_cfg: &F::Config, @@ -2884,18 +4142,61 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero + Default + Send + Sync, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, { + let row_weights = build_eq_x_r_vec(r_star, field_cfg)?; + prove_sha_endpoint_multipoint_with_row_weights( + transcript, + folded_trace, + folded_public, + endpoint_evals, + r_star, + &row_weights, + field_cfg, + ) +} + +pub fn prove_sha_endpoint_multipoint_with_row_weights( + transcript: &mut impl Transcript, + folded_trace: &ProjectedTrace, + folded_public: &ProjectedPublic, + endpoint_evals: &ShaEndpointEvals, + r_star: &[F], + row_weights: &[F], + field_cfg: &F::Config, +) -> Result<(MultipointEvalProof, Vec), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, +{ + if row_weights.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "row weights", + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } validate_sha_endpoint_layout(endpoint_evals)?; let layout = production_sha_multipoint_layout(); let trace_mles = sha_multipoint_trace_mles(folded_trace, folded_public, &layout, field_cfg)?; - let up_evals = - sha_multipoint_up_evals(endpoint_evals, folded_public, r_star, &layout, field_cfg)?; - let (shift_specs, down_evals) = sha_multipoint_shift_specs_and_down_evals( + let up_evals = sha_multipoint_up_evals_with_row_weights( endpoint_evals, folded_public, - r_star, + row_weights, + &layout, + field_cfg, + )?; + let (shift_specs, down_evals) = sha_multipoint_shift_specs_and_down_evals_with_row_weights( + endpoint_evals, + folded_public, + row_weights, &layout, field_cfg, )?; @@ -2915,7 +4216,7 @@ pub fn verify_sha_endpoint_multipoint( transcript: &mut impl Transcript, proof: &MultipointEvalProof, endpoint_evals: &ShaEndpointEvals, - folded_public: &ProjectedShaPublic, + folded_public: &ProjectedPublic, r_star: &[F], field_cfg: &F::Config, ) -> Result<(MultipointSubclaim, Vec), ProductionShaError> @@ -2926,8 +4227,8 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero + Default + Send + Sync, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, { validate_sha_endpoint_layout(endpoint_evals)?; let layout = production_sha_multipoint_layout(); @@ -2966,23 +4267,63 @@ where + Send + Sync + 'static, - F::Inner: ConstTranscribable + Zero + Default + Send + Sync, - F::Modulus: ConstTranscribable, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, { Ok(MultipointEval::verify_subclaim( subclaim, open_evals, shift_specs, field_cfg, - )?) + )?) +} + +#[allow(clippy::too_many_arguments)] +pub fn reconstruct_folded_row_terminal_from_endpoints( + endpoint_evals: &ShaEndpointEvals, + folded_public: &ProjectedPublic, + r_ic: &[F; SHA_ROW_VARS], + r_star: &[F], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result> +where + F: DelayedFieldProductSum, +{ + if r_star.len() != SHA_ROW_VARS { + return Err(ProductionShaError::LengthMismatch { + label: "r_star", + got: r_star.len(), + expected: SHA_ROW_VARS, + }); + } + let row_weights = build_eq_x_r_vec(r_star, field_cfg)?; + reconstruct_folded_row_terminal_from_endpoints_with_row_weights( + endpoint_evals, + folded_public, + r_ic, + r_star, + &row_weights, + a, + lambda, + rho, + xi, + booleanity_sources, + field_cfg, + ) } #[allow(clippy::too_many_arguments)] -pub fn reconstruct_folded_row_terminal_from_endpoints( +pub fn reconstruct_folded_row_terminal_from_endpoints_with_row_weights( endpoint_evals: &ShaEndpointEvals, - folded_public: &ProjectedShaPublic, + folded_public: &ProjectedPublic, r_ic: &[F; SHA_ROW_VARS], r_star: &[F], + row_weights: &[F], a: &F, lambda: &F, rho: &F, @@ -3000,12 +4341,22 @@ where expected: SHA_ROW_VARS, }); } - + if row_weights.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "row weights", + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } validate_sha_endpoint_layout(endpoint_evals)?; verify_endpoint_scalarization(endpoint_evals, a, field_cfg)?; - let residuals = - residual_polys_from_endpoints(endpoint_evals, folded_public, r_star, field_cfg)?; + let residuals = residual_polys_from_endpoints_with_row_weights( + endpoint_evals, + folded_public, + row_weights, + field_cfg, + )?; let lambda_powers = zinc_utils::powers(lambda.clone(), F::one_with_cfg(field_cfg), residuals.len()); let a_powers = zinc_utils::powers( @@ -3153,8 +4504,8 @@ where } fn sha_multipoint_trace_mles( - folded_trace: &ProjectedShaTrace, - folded_public: &ProjectedShaPublic, + folded_trace: &ProjectedTrace, + folded_public: &ProjectedPublic, layout: &ShaMultipointLayout, field_cfg: &F::Config, ) -> Result>, ProductionShaError> @@ -3182,11 +4533,31 @@ where fn sha_multipoint_up_evals( endpoint_evals: &ShaEndpointEvals, - folded_public: &ProjectedShaPublic, + folded_public: &ProjectedPublic, r_star: &[F], layout: &ShaMultipointLayout, field_cfg: &F::Config, ) -> Result, ProductionShaError> +where + F: PrimeField, +{ + let row_weights = build_eq_x_r_vec(r_star, field_cfg)?; + sha_multipoint_up_evals_with_row_weights( + endpoint_evals, + folded_public, + &row_weights, + layout, + field_cfg, + ) +} + +fn sha_multipoint_up_evals_with_row_weights( + endpoint_evals: &ShaEndpointEvals, + folded_public: &ProjectedPublic, + row_weights: &[F], + layout: &ShaMultipointLayout, + field_cfg: &F::Config, +) -> Result, ProductionShaError> where F: PrimeField, { @@ -3194,18 +4565,44 @@ where .sources .iter() .map(|source| { - sha_mp_source_endpoint_value(endpoint_evals, folded_public, r_star, *source, field_cfg) + sha_mp_source_endpoint_value_with_row_weights( + endpoint_evals, + folded_public, + row_weights, + *source, + field_cfg, + ) }) .collect() } fn sha_multipoint_shift_specs_and_down_evals( endpoint_evals: &ShaEndpointEvals, - folded_public: &ProjectedShaPublic, + folded_public: &ProjectedPublic, r_star: &[F], layout: &ShaMultipointLayout, field_cfg: &F::Config, ) -> Result<(Vec, Vec), ProductionShaError> +where + F: PrimeField, +{ + let row_weights = build_eq_x_r_vec(r_star, field_cfg)?; + sha_multipoint_shift_specs_and_down_evals_with_row_weights( + endpoint_evals, + folded_public, + &row_weights, + layout, + field_cfg, + ) +} + +fn sha_multipoint_shift_specs_and_down_evals_with_row_weights( + endpoint_evals: &ShaEndpointEvals, + folded_public: &ProjectedPublic, + row_weights: &[F], + layout: &ShaMultipointLayout, + field_cfg: &F::Config, +) -> Result<(Vec, Vec), ProductionShaError> where F: PrimeField, { @@ -3228,7 +4625,13 @@ where ShaMpShiftSource::Public { col, shift } => ( ShaMpSource::Public { col }, shift, - sha_public_at_point(folded_public, col, shift, r_star, field_cfg)?, + sha_public_at_point_with_weights( + folded_public, + col, + shift, + row_weights, + field_cfg, + )?, ), }; let source_idx = layout @@ -3247,8 +4650,8 @@ where } fn sha_mp_source_row_value( - folded_trace: &ProjectedShaTrace, - folded_public: &ProjectedShaPublic, + folded_trace: &ProjectedTrace, + folded_public: &ProjectedPublic, source: ShaMpSource, row: usize, field_cfg: &F::Config, @@ -3259,10 +4662,8 @@ where match source { ShaMpSource::WordBit { col, bit } => folded_trace .bit_slices - .columns - .get(col.index()) - .and_then(|rows| rows.get(row)) - .and_then(|bits| bits.get(bit)) + .get(bit_slice_index(col.index(), bit, SHA_WORD_BITS)) + .and_then(|column| column.evaluations.get(row)) .cloned() .ok_or(ProductionShaError::LengthMismatch { label: "multipoint word bit source", @@ -3271,9 +4672,8 @@ where }), ShaMpSource::Int { col } => folded_trace .int_columns - .columns .get(col.index()) - .and_then(|rows| rows.get(row)) + .and_then(|column| column.evaluations.get(row)) .cloned() .ok_or(ProductionShaError::LengthMismatch { label: "multipoint int source", @@ -3281,10 +4681,9 @@ where expected: SHA_ROW_COUNT, }), ShaMpSource::Public { col } => folded_public - .columns .columns .get(col.index()) - .and_then(|rows| rows.get(row)) + .and_then(|column| column.evaluations.get(row)) .cloned() .ok_or(ProductionShaError::LengthMismatch { label: "multipoint public source", @@ -3298,10 +4697,10 @@ where }) } -fn sha_mp_source_endpoint_value( +fn sha_mp_source_endpoint_value_with_row_weights( endpoint_evals: &ShaEndpointEvals, - folded_public: &ProjectedShaPublic, - r_star: &[F], + folded_public: &ProjectedPublic, + row_weights: &[F], source: ShaMpSource, field_cfg: &F::Config, ) -> Result> @@ -3327,25 +4726,32 @@ where got: endpoint_evals.int_sources.len(), expected: ShaIntCol::COUNT, }), - ShaMpSource::Public { col } => Ok(sha_public_at_point( + ShaMpSource::Public { col } => Ok(sha_public_at_point_with_weights( folded_public, col, 0, - r_star, + row_weights, field_cfg, )?), } } -fn residual_polys_from_endpoints( +fn residual_polys_from_endpoints_with_row_weights( endpoint_evals: &ShaEndpointEvals, - folded_public: &ProjectedShaPublic, - r_star: &[F], + folded_public: &ProjectedPublic, + row_weights: &[F], field_cfg: &F::Config, ) -> Result>, ProductionShaError> where F: PrimeField, { + if row_weights.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "row weights", + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } let one = F::one_with_cfg(field_cfg); let two = one.clone() + &one; let rho_sig0 = sparse_endpoint_poly::(&[10, 19, 30], field_cfg); @@ -3397,7 +4803,13 @@ where - &endpoint_word_poly(endpoint_evals, ShaWordCol::Sigma1, 3, field_cfg)? - &endpoint_word_poly(endpoint_evals, ShaWordCol::Uef, 3, field_cfg)? - &endpoint_word_poly(endpoint_evals, ShaWordCol::UNegEg, 3, field_cfg)? - - &endpoint_public_const_poly(folded_public, ShaPublicCol::K, 3, r_star, field_cfg)? + - &endpoint_public_const_poly_with_row_weights( + folded_public, + ShaPublicCol::K, + 3, + row_weights, + field_cfg, + )? - &w - &endpoint_word_poly(endpoint_evals, ShaWordCol::Sigma0, 3, field_cfg)? - &endpoint_word_poly(endpoint_evals, ShaWordCol::Maj, 3, field_cfg)? @@ -3410,56 +4822,94 @@ where - &endpoint_word_poly(endpoint_evals, ShaWordCol::Sigma1, 3, field_cfg)? - &endpoint_word_poly(endpoint_evals, ShaWordCol::Uef, 3, field_cfg)? - &endpoint_word_poly(endpoint_evals, ShaWordCol::UNegEg, 3, field_cfg)? - - &endpoint_public_const_poly(folded_public, ShaPublicCol::K, 3, r_star, field_cfg)? + - &endpoint_public_const_poly_with_row_weights( + folded_public, + ShaPublicCol::K, + 3, + row_weights, + field_cfg, + )? - &w + &mu_e + &endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompUpdateE, field_cfg)?; - let s_init = sha_public_at_point(folded_public, ShaPublicCol::SInit, 0, r_star, field_cfg)?; - let s_msg = sha_public_at_point(folded_public, ShaPublicCol::SMsg, 0, r_star, field_cfg)?; - let s_sched = sha_public_at_point(folded_public, ShaPublicCol::SSched, 0, r_star, field_cfg)?; - let s_upd = sha_public_at_point(folded_public, ShaPublicCol::SUpd, 0, r_star, field_cfg)?; - let s_ff = sha_public_at_point(folded_public, ShaPublicCol::SFf, 0, r_star, field_cfg)?; - let s_out = sha_public_at_point(folded_public, ShaPublicCol::SOut, 0, r_star, field_cfg)?; + let s_init = sha_public_at_point_with_weights( + folded_public, + ShaPublicCol::SInit, + 0, + row_weights, + field_cfg, + )?; + let s_msg = sha_public_at_point_with_weights( + folded_public, + ShaPublicCol::SMsg, + 0, + row_weights, + field_cfg, + )?; + let s_sched = sha_public_at_point_with_weights( + folded_public, + ShaPublicCol::SSched, + 0, + row_weights, + field_cfg, + )?; + let s_upd = sha_public_at_point_with_weights( + folded_public, + ShaPublicCol::SUpd, + 0, + row_weights, + field_cfg, + )?; + let s_ff = sha_public_at_point_with_weights( + folded_public, + ShaPublicCol::SFf, + 0, + row_weights, + field_cfg, + )?; + let s_out = sha_public_at_point_with_weights( + folded_public, + ShaPublicCol::SOut, + 0, + row_weights, + field_cfg, + )?; let r7 = scale_endpoint_poly( &(a.clone() - - &endpoint_public_const_poly( + - &endpoint_public_word_or_const_poly_with_row_weights( folded_public, ShaPublicCol::PAIn, - 0, - r_star, + row_weights, field_cfg, )?), &s_init, ) + &scale_endpoint_poly( &(a.clone() - - &endpoint_public_const_poly( + - &endpoint_public_word_or_const_poly_with_row_weights( folded_public, ShaPublicCol::PAOut, - 0, - r_star, + row_weights, field_cfg, )?), &s_out, ); let r8 = scale_endpoint_poly( &(e.clone() - - &endpoint_public_const_poly( + - &endpoint_public_word_or_const_poly_with_row_weights( folded_public, ShaPublicCol::PEIn, - 0, - r_star, + row_weights, field_cfg, )?), &s_init, ) + &scale_endpoint_poly( &(e.clone() - - &endpoint_public_const_poly( + - &endpoint_public_word_or_const_poly_with_row_weights( folded_public, ShaPublicCol::PEOut, - 0, - r_star, + row_weights, field_cfg, )?), &s_out, @@ -3467,20 +4917,31 @@ where let r9 = endpoint_word_poly(endpoint_evals, ShaWordCol::A, 4, field_cfg)? - &a - - &endpoint_public_const_poly(folded_public, ShaPublicCol::PAIn, 0, r_star, field_cfg)? + - &endpoint_public_const_poly_with_row_weights( + folded_public, + ShaPublicCol::PAIn, + 0, + row_weights, + field_cfg, + )? + &mu_ff_a + &endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompFeedForwardA, field_cfg)?; let r10 = endpoint_word_poly(endpoint_evals, ShaWordCol::E, 4, field_cfg)? - &e - - &endpoint_public_const_poly(folded_public, ShaPublicCol::PEIn, 0, r_star, field_cfg)? + - &endpoint_public_const_poly_with_row_weights( + folded_public, + ShaPublicCol::PEIn, + 0, + row_weights, + field_cfg, + )? + &mu_ff_e + &endpoint_int_const_poly(endpoint_evals, ShaIntCol::CompFeedForwardE, field_cfg)?; let r11 = scale_endpoint_poly( - &(w - &endpoint_public_const_poly( + &(w - &endpoint_public_word_or_const_poly_with_row_weights( folded_public, ShaPublicCol::Message, - 0, - r_star, + row_weights, field_cfg, )?), &s_msg, @@ -3544,22 +5005,73 @@ where Ok(endpoint_const_poly(value, field_cfg)) } -fn endpoint_public_const_poly( - folded_public: &ProjectedShaPublic, +fn endpoint_public_const_poly_with_row_weights( + folded_public: &ProjectedPublic, col: ShaPublicCol, shift: usize, - r_star: &[F], + row_weights: &[F], field_cfg: &F::Config, ) -> Result, ProductionShaError> where F: PrimeField, { Ok(endpoint_const_poly( - sha_public_at_point(folded_public, col, shift, r_star, field_cfg)?, + sha_public_at_point_with_weights(folded_public, col, shift, row_weights, field_cfg)?, field_cfg, )) } +fn endpoint_public_word_or_const_poly_with_row_weights( + folded_public: &ProjectedPublic, + col: ShaPublicCol, + row_weights: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + let Some(col_idx) = public_word_col_index(col) else { + return endpoint_public_const_poly_with_row_weights( + folded_public, + col, + 0, + row_weights, + field_cfg, + ); + }; + let bit_slices = + folded_public + .bit_slices + .as_ref() + .ok_or(ProductionShaError::NonCanonicalProofObject( + "production SHA public word columns are required", + ))?; + let mut coeffs = vec![F::zero_with_cfg(field_cfg); SHA_WORD_BITS]; + for (row, row_weight) in row_weights.iter().enumerate() { + for (bit, coeff) in coeffs.iter_mut().enumerate() { + let table_idx = bit_slice_index(col_idx, bit, SHA_WORD_BITS); + let bit_column = + bit_slices + .get(table_idx) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA public word bit column", + got: table_idx, + expected: bit_slices.len(), + })?; + if bit_column.num_vars != SHA_ROW_VARS || bit_column.evaluations.len() != SHA_ROW_COUNT + { + return Err(ProductionShaError::LengthMismatch { + label: "SHA public word row count", + got: bit_column.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + *coeff += row_weight.clone() * &bit_column.evaluations[row]; + } + } + Ok(DynamicPolynomialF::new_trimmed(coeffs)) +} + fn endpoint_mu_contribution( endpoint_evals: &ShaEndpointEvals, low: usize, @@ -3705,32 +5217,35 @@ fn family_weight_index(family: ShaResidualFamily) -> usize { mod tests { use super::*; - use crate::fixed_prime; + use crate::{fixed_prime, pcs::AllHyraxPCSTypes}; + use ark_ec::{CurveGroup, PrimeGroup}; use core::fmt::Debug; use crypto_primitives::{ - FromWithConfig, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, + FromWithConfig, crypto_bigint_boxed_monty::BoxedMontyField, crypto_bigint_int::Int, crypto_bigint_uint::Uint, }; use zinc_piop::neutron_nova::{ - SHA_ROW_COUNT, SHA_WORD_BITS, ShaBitSliceColumns, ShaIntColumns, ShaPublicColumns, - expression_folded_row_sum, fold_projected_sha_traces, scalarize_trace_words, + SHA_ROW_COUNT, SHA_WORD_BITS, expression_folded_row_sum, fold_projected_traces, }; use zinc_poly::mle::MultilinearExtensionWithConfig; use zinc_poly::univariate::{binary::BinaryPolyInnerProduct, dense::DensePolyInnerProduct}; use zinc_primality::MillerRabin; use zinc_test_uair::{ - EC_FP_INT_LIMBS, Sha256CompressionSliceUair, sha256::cols as sha256_cols, + EC_FP_INT_LIMBS, SHA256_INITIAL_STATE, Sha256CompressionSliceUair, + sha256::cols as sha256_cols, sha256_padded_message_blocks, synthesize_sha256_chain_witnesses, }; use zinc_transcript::{Blake3Transcript, traits::Transcript}; use zinc_utils::inner_product::{MBSInnerProduct, ScalarProduct}; use zip_plus::{ code::iprs::{IprsCode, PnttConfigF65537}, - pcs::structs::ZipTypes, - pcs_transcript::{PcsProverTranscript, PcsVerifierTranscript}, + pcs::{ + hyrax::{HyraxBlindingMode, HyraxPCS}, + structs::ZipTypes, + }, }; - type F = MontyField<4>; + type F = BoxedMontyField; type ShaInt = Int; const TEST_DEGREE_PLUS_ONE: usize = 32; const TEST_REP: usize = 4; @@ -3739,7 +5254,7 @@ mod tests { const TEST_NUM_COLUMN_OPENINGS: usize = 150; fn cfg() -> ::Config { - fixed_prime::secp256k1_field_cfg::>() + fixed_prime::secp256k1_field_cfg::>() } fn f(value: u64) -> F { @@ -3837,125 +5352,6 @@ mod tests { type IntLc = IprsCode; } - #[derive(Clone, Debug, PartialEq, Eq)] - struct NoopCommitment { - batch_size: usize, - } - - #[derive(Clone, Debug)] - struct NoopPCS; - - impl PCS for NoopPCS - where - Fp: PrimeField, - Eval: Clone + Debug + Send + Sync, - { - type CommitmentKey = (); - type VerifierKey = (); - type Commitment = NoopCommitment; - type ProverData = (); - type OpeningProof = Vec; - - fn commit( - _ck: &Self::CommitmentKey, - polys: &[DenseMultilinearExtension], - ) -> Result<(Self::ProverData, Self::Commitment), ZipError> { - Ok(( - (), - NoopCommitment { - batch_size: polys.len(), - }, - )) - } - - fn absorb_commitment(transcript: &mut T, commitment: &Self::Commitment) { - transcript.absorb_slice(&(commitment.batch_size as u64).to_le_bytes()); - } - - fn commitment_num_bytes(_commitment: &Self::Commitment) -> usize { - core::mem::size_of::() - } - - fn write_commitment_bytes(commitment: &Self::Commitment, buf: &mut Vec) { - buf.extend_from_slice(&(commitment.batch_size as u64).to_le_bytes()); - } - - fn batch_size(commitment: &Self::Commitment) -> usize { - commitment.batch_size - } - - fn prove_open( - _transcript: &mut PcsProverTranscript, - _ck: &Self::CommitmentKey, - _polys: &[DenseMultilinearExtension], - _point: &[Fp], - _prover_data: &Self::ProverData, - _field_cfg: &Fp::Config, - ) -> Result - where - Fp::Inner: Transcribable, - Fp::Modulus: Transcribable, - { - Ok(Vec::new()) - } - - fn verify_open( - _transcript: &mut PcsVerifierTranscript, - _vk: &Self::VerifierKey, - _commitment: &Self::Commitment, - _point: &[Fp], - _lifted_evals: &[DynamicPolynomialF], - _opening_proof: &Self::OpeningProof, - _field_cfg: &Fp::Config, - ) -> Result<(), ZipError> - where - Fp::Inner: Transcribable, - Fp::Modulus: Transcribable, - { - Ok(()) - } - } - - impl FoldablePCS for NoopPCS - where - Fp: PrimeField, - Eval: Clone + Debug + Send + Sync, - { - fn fold_commitments( - commitments: &[Self::Commitment], - _theta: &[Fp], - _field_cfg: &Fp::Config, - ) -> Result { - Ok(NoopCommitment { - batch_size: commitments - .first() - .map(|commitment| commitment.batch_size) - .unwrap_or_default(), - }) - } - - fn fold_prover_data( - _prover_data: &[Self::ProverData], - _theta: &[Fp], - _field_cfg: &Fp::Config, - ) -> Result { - Ok(()) - } - } - - #[derive(Clone, Debug)] - struct NoopPCSTypes; - - impl ZincPCSTypes for NoopPCSTypes - where - Zt: ZincTypes, - Fp: PrimeField, - { - type BinaryPCS = NoopPCS; - type ArbitraryPCS = NoopPCS; - type IntPCS = NoopPCS; - } - fn sha_binary_col<'a>( public_trace: &'a UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, witness_trace: &'a UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, @@ -4062,6 +5458,23 @@ mod tests { .collect()) } + fn truncate_sha_row_domain( + col: &DenseMultilinearExtension, + label: &'static str, + ) -> Result, ProductionShaError> { + if col.evaluations.len() < SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label, + got: col.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + Ok(DenseMultilinearExtension { + evaluations: col.evaluations[..SHA_ROW_COUNT].to_vec(), + num_vars: SHA_ROW_VARS, + }) + } + fn word_scalar_at_two(bits: &[F], field_cfg: &::Config) -> F { let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); let mut power = F::one_with_cfg(field_cfg); @@ -4073,12 +5486,68 @@ mod tests { value } + fn mle_table_from_columns(columns: Vec>) -> MleTable { + columns + .into_iter() + .map(|evaluations| DenseMultilinearExtension { + evaluations, + num_vars: SHA_ROW_VARS, + }) + .collect() + } + + fn flatten_bit_columns(columns: Vec>>) -> MleTable { + let mut flattened = (0..columns.len() * SHA_WORD_BITS) + .map(|_| Vec::with_capacity(SHA_ROW_COUNT)) + .collect::>(); + for (col_idx, rows) in columns.into_iter().enumerate() { + for bits in rows { + for (bit, value) in bits.into_iter().enumerate() { + flattened[bit_slice_index(col_idx, bit, SHA_WORD_BITS)].push(value); + } + } + } + mle_table_from_columns(flattened) + } + + fn scalarize_bit_slices_plain( + bit_slices: &MleTable, + a: &F, + field_cfg: &::Config, + ) -> Result, ProductionShaError> { + let powers = zinc_utils::powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); + let word_count = bit_slices.len() / SHA_WORD_BITS; + let mut words = Vec::with_capacity(word_count); + for col_idx in 0..word_count { + let mut out_col = Vec::with_capacity(SHA_ROW_COUNT); + for row in 0..SHA_ROW_COUNT { + let mut value = F::zero_with_cfg(field_cfg); + for (bit, power) in powers.iter().enumerate() { + let bit_col = &bit_slices[bit_slice_index(col_idx, bit, SHA_WORD_BITS)]; + if bit_col.num_vars != SHA_ROW_VARS + || bit_col.evaluations.len() != SHA_ROW_COUNT + { + return Err(ProductionShaError::LengthMismatch { + label: "SHA scalarized bit-slice rows", + got: bit_col.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + value += bit_col.evaluations[row].clone() * power; + } + out_col.push(value); + } + words.push(out_col); + } + Ok(mle_table_from_columns(words)) + } + fn projected_public_from_sources( pa_a: &[Vec], pa_e: &[Vec], message: &[Vec], field_cfg: &::Config, - ) -> ShaPublicColumns { + ) -> MleTable { let mut columns = vec![vec![F::zero_with_cfg(field_cfg); SHA_ROW_COUNT]; ShaPublicCol::COUNT]; for row in 0..SHA_ROW_COUNT { @@ -4103,7 +5572,7 @@ mod tests { production_sha_selector_expected(selector, row, field_cfg); } } - ShaPublicColumns { columns } + mle_table_from_columns(columns) } impl ProductionShaProjectionAdapter @@ -4113,7 +5582,7 @@ mod tests { _shape: &UairShape, public_trace: &UairTrace<'_, ShaInt, ShaInt, TEST_DEGREE_PLUS_ONE>, field_cfg: &::Config, - ) -> Result, ProductionShaError> { + ) -> Result, ProductionShaError> { let empty_witness = UairTrace { binary_poly: Cow::Borrowed(&[]), arbitrary_poly: Cow::Borrowed(&[]), @@ -4132,11 +5601,15 @@ mod tests { field_cfg, )?; let public_columns = projected_public_from_sources(&pa_a, &pa_e, &message, field_cfg); - Ok(ProjectedShaPublic { + Ok(ProjectedPublic { columns: public_columns, - word_columns: Some(ShaPublicWordColumns { - columns: vec![pa_a.clone(), pa_e.clone(), pa_a, pa_e, message], - }), + bit_slices: Some(flatten_bit_columns(vec![ + pa_a.clone(), + pa_e.clone(), + pa_a, + pa_e, + message, + ])), }) } @@ -4147,8 +5620,8 @@ mod tests { field_cfg: &::Config, ) -> Result< ( - ProjectedShaTrace, - ProjectedShaPublic, + ProjectedTrace, + ProjectedPublic, ProductionShaWitnessPolys, ), ProductionShaError, @@ -4189,11 +5662,10 @@ mod tests { ) }) .collect::, _>>()?; - let scalarized_words = scalarize_trace_words( - &ShaBitSliceColumns { - columns: bit_columns.clone(), - }, - &f(2), + let bit_slices = flatten_bit_columns(bit_columns.clone()); + let scalarized = scalarize_bit_slices_plain( + &bit_slices, + &F::from_with_cfg(2u64, field_cfg), field_cfg, )?; let pa_a = project_binary_source( @@ -4216,22 +5688,21 @@ mod tests { }) .collect::, _>>()?; - let trace = ProjectedShaTrace { - rows: SHA_ROW_COUNT, - bit_slices: ShaBitSliceColumns { - columns: bit_columns, - }, - scalarized_words, - int_columns: ShaIntColumns { - columns: int_columns.clone(), - }, + let trace = ProjectedTrace { + bit_slices, + scalarized, + int_columns: mle_table_from_columns(int_columns.clone()), public_columns: public_columns.clone(), }; - let public = ProjectedShaPublic { + let public = ProjectedPublic { columns: public_columns, - word_columns: Some(ShaPublicWordColumns { - columns: vec![pa_a.clone(), pa_e.clone(), pa_a, pa_e, message], - }), + bit_slices: Some(flatten_bit_columns(vec![ + pa_a.clone(), + pa_e.clone(), + pa_a, + pa_e, + message, + ])), }; Ok(( trace, @@ -4239,55 +5710,74 @@ mod tests { ProductionShaWitnessPolys { binary: word_sources .iter() - .map(|&col| sha_binary_col(public_trace, witness_trace, col).cloned()) + .map(|&col| { + truncate_sha_row_domain( + sha_binary_col(public_trace, witness_trace, col)?, + "SHA binary witness row-domain projection", + ) + }) .collect::, _>>()?, arbitrary: Vec::new(), int: int_sources .iter() - .map(|&col| sha_int_col(public_trace, witness_trace, col).cloned()) + .map(|&col| { + truncate_sha_row_domain( + sha_int_col(public_trace, witness_trace, col)?, + "SHA int witness row-domain projection", + ) + }) .collect::, _>>()?, }, )) } } - fn zero_trace_with_scalar_challenge(a: &F) -> ProjectedShaTrace { + fn zero_trace_with_scalar_challenge(a: &F) -> ProjectedTrace { let field_cfg = cfg(); let zero = F::zero_with_cfg(&field_cfg); - let bit_slices = ShaBitSliceColumns { - columns: vec![ + let bit_slices = + flatten_bit_columns(vec![ vec![vec![zero.clone(); SHA_WORD_BITS]; SHA_ROW_COUNT]; ShaWordCol::COUNT - ], - }; - let scalarized_words = scalarize_trace_words(&bit_slices, a, &field_cfg).unwrap(); - ProjectedShaTrace { - rows: SHA_ROW_COUNT, + ]); + let scalarized = scalarize_bit_slices_plain(&bit_slices, a, &field_cfg).unwrap(); + ProjectedTrace { bit_slices, - scalarized_words, - int_columns: ShaIntColumns { - columns: vec![vec![zero.clone(); SHA_ROW_COUNT]; ShaIntCol::COUNT], - }, - public_columns: ShaPublicColumns { - columns: vec![vec![zero; SHA_ROW_COUNT]; ShaPublicCol::COUNT], - }, + scalarized, + int_columns: mle_table_from_columns(vec![ + vec![zero.clone(); SHA_ROW_COUNT]; + ShaIntCol::COUNT + ]), + public_columns: mle_table_from_columns(vec![ + vec![zero; SHA_ROW_COUNT]; + ShaPublicCol::COUNT + ]), } } - fn zero_public() -> ProjectedShaPublic { + fn zero_public() -> ProjectedPublic { let field_cfg = cfg(); - ProjectedShaPublic { - columns: ShaPublicColumns { - columns: vec![ - vec![F::zero_with_cfg(&field_cfg); SHA_ROW_COUNT]; - ShaPublicCol::COUNT - ], - }, - word_columns: None, + ProjectedPublic { + columns: mle_table_from_columns(vec![ + vec![F::zero_with_cfg(&field_cfg); SHA_ROW_COUNT]; + ShaPublicCol::COUNT + ]), + bit_slices: Some(flatten_bit_columns(vec![ + vec![ + vec![ + F::zero_with_cfg( + &field_cfg + ); + SHA_WORD_BITS + ]; + SHA_ROW_COUNT + ]; + ShaPublicWordCol::COUNT + ])), } } - fn fixed_layout_public() -> ProjectedShaPublic { + fn fixed_layout_public() -> ProjectedPublic { let field_cfg = cfg(); let mut public = zero_public(); for selector in [ @@ -4299,12 +5789,12 @@ mod tests { ShaPublicCol::SOut, ] { for row in 0..SHA_ROW_COUNT { - public.columns.columns[selector.index()][row] = + public.columns[selector.index()].evaluations[row] = production_sha_selector_expected(selector, row, &field_cfg); } } for row in 0..SHA_ROW_COUNT { - public.columns.columns[ShaPublicCol::K.index()][row] = + public.columns[ShaPublicCol::K.index()].evaluations[row] = production_sha_k_expected(row, &field_cfg); } public @@ -4363,52 +5853,128 @@ mod tests { } } + fn hyrax_key_pair( + width: usize, + offset: u64, + ) -> ( + zip_plus::pcs::hyrax::HyraxCommitmentKey, + zip_plus::pcs::hyrax::HyraxVerifierKey, + ) + where + C: AffineRepr, + Lanes: Clone + Debug + Send + Sync, + { + let generator = C::Group::generator(); + let bases = (0..width) + .map(|idx| { + let scalar = C::ScalarField::from( + offset + u64::try_from(idx).expect("Hyrax basis index fits u64") + 1, + ); + (generator * scalar).into_affine() + }) + .collect::>(); + let h = generator + * C::ScalarField::from( + offset + u64::try_from(width).expect("Hyrax width fits u64") + 1, + ); + HyraxPCS::::setup_from_bases_with_blinding( + width, + bases, + h, + HyraxBlindingMode::Unblinded, + ) + .expect("Hyrax test setup must be valid") + } + + fn all_hyrax_test_pcs_params() -> ( + PCSParams, TestShaZincTypes, F, TEST_DEGREE_PLUS_ONE>, + PCSVerifierParams, TestShaZincTypes, F, TEST_DEGREE_PLUS_ONE>, + ) + where + C: AffineRepr, + AllHyraxPCSTypes: ZincPCSTypes< + TestShaZincTypes, + F, + TEST_DEGREE_PLUS_ONE, + BinaryPCS = HyraxPCS, + ArbitraryPCS = HyraxPCS, + IntPCS = HyraxPCS, + >, + { + let width = SHA_ROW_COUNT; + let (binary_ck, binary_vk) = hyrax_key_pair::(width, 0); + let (arbitrary_ck, arbitrary_vk) = hyrax_key_pair::(width, 1_000); + let (int_ck, int_vk) = hyrax_key_pair::(width, 2_000); + + ( + PCSParams::, TestShaZincTypes, F, TEST_DEGREE_PLUS_ONE> { + binary: binary_ck, + arbitrary: arbitrary_ck, + int: int_ck, + }, + PCSVerifierParams::, TestShaZincTypes, F, TEST_DEGREE_PLUS_ONE> { + binary: binary_vk, + arbitrary: arbitrary_vk, + int: int_vk, + }, + ) + } + #[test] - fn prove_linear_ideal_fold_stops_at_unimplemented_proof_tail() { + fn linear_ideal_fold_proves_and_verifies_eight_sha_instances_with_hyrax() { + type C = ark_bn254::G1Affine; + type P = AllHyraxPCSTypes; type U = Sha256CompressionSliceUair; - let field_cfg = cfg(); - let initial_state = [ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, - 0x5be0cd19, - ]; - let message_blocks = [[0u32; 16], [1u32; 16]]; + let field_cfg = fixed_prime::field_cfg_from_curve_scalar::, C>(); + let initial_state = SHA256_INITIAL_STATE; + let message = vec!["hello world"; 40].join(" "); + let message_blocks = sha256_padded_message_blocks::<8>(message.as_bytes()) + .expect("test message should canonically pad to 8 SHA-256 blocks"); let (witnesses, _final_state) = - synthesize_sha256_chain_witnesses::(initial_state, message_blocks) + synthesize_sha256_chain_witnesses::(initial_state, message_blocks) .expect("SHA-256 UAIR witnesses synthesize"); - let num_vars = witnesses[0].trace.binary_poly[0].num_vars; - let shape = UairShape::::new(num_vars); - let pp = LinearIdealFoldProverParams::< - NoopPCSTypes, - U, - TestShaZincTypes, - F, - TEST_DEGREE_PLUS_ONE, - > { - pcs_params: PCSParams { - binary: (), - arbitrary: (), - int: (), - }, - prefix_vars: 1, - field_cfg, - _marker: PhantomData, - }; - let mut transcript = Blake3Transcript::new(); + let shape = UairShape::::new(SHA_ROW_VARS); + let (pcs_params, pcs_verifier_params) = all_hyrax_test_pcs_params::(); + let pp = + LinearIdealFoldProverParams::::new( + pcs_params, + field_cfg.clone(), + 3, + ); + let vs = setup_verify_linear_ideal_fold::( + LinearIdealFoldVerifierParams::new(pcs_verifier_params, field_cfg), + shape.clone(), + ) + .expect("production SHA verifier setup succeeds"); + + let mut prover_transcript = Blake3Transcript::new(); + let output = prove_linear_ideal_fold::( + &pp, + &shape, + &witnesses, + &mut prover_transcript, + ) + .expect("production SHA ProjectionFold proof succeeds"); + + let mut verifier_transcript = Blake3Transcript::new(); + let verified = verify_linear_ideal_fold::( + &vs, + &output.fresh_instances, + &output.proof, + &mut verifier_transcript, + ) + .expect("production SHA ProjectionFold proof verifies"); - let result = prove_linear_ideal_fold::< - NoopPCSTypes, - U, + assert_eq!(verified.target, output.folded_instance.target); + assert_eq!(verified.public, output.folded_instance.public); + assert!(pcs_commitments_match::< + P, TestShaZincTypes, F, TEST_DEGREE_PLUS_ONE, - >(&pp, &shape, &witnesses, &mut transcript); - - assert!(matches!( - result, - Err(ProductionShaError::ProverNotImplemented( - "folded row/multipoint/PCS proof assembly" - )) + >( + &verified.commitments, &output.folded_instance.commitments )); } @@ -4425,7 +5991,7 @@ mod tests { let mut transcript = Blake3Transcript::new(); transcript.absorb_slice(b"fresh-commitments-and-public-inputs"); let _r_ic = sample_pre_ideal_challenge::(&mut transcript, &field_cfg); - absorb_fresh_sha_ideal_polys(&mut transcript, values); + absorb_fresh_sha_ideal_polys(&mut transcript, values, &field_cfg); let (a, _, _, _, _) = sample_post_ideal_challenges::(&mut transcript, 1, &field_cfg).unwrap(); a @@ -4452,7 +6018,7 @@ mod tests { let mut transcript = Blake3Transcript::new(); transcript.absorb_slice(b"fresh-commitments-and-public-inputs"); let _r_ic = sample_pre_ideal_challenge::(&mut transcript, &field_cfg); - absorb_fresh_sha_ideal_polys(&mut transcript, values); + absorb_fresh_sha_ideal_polys(&mut transcript, values, &field_cfg); let (a, _, _, _, _) = sample_post_ideal_challenges::(&mut transcript, 1, &field_cfg).unwrap(); a @@ -4538,7 +6104,7 @@ mod tests { let _r_ic = sample_pre_ideal_challenge::(&mut transcript, &field_cfg); let _beta = sample_instance_batch_challenge::(&mut transcript, 4, &field_cfg).unwrap(); - absorb_aggregate_sha_ideal_polys(&mut transcript, values); + absorb_aggregate_sha_ideal_polys(&mut transcript, values, &field_cfg); let (a, _, _, _) = sample_post_aggregate_ideal_challenges::(&mut transcript, &field_cfg); a @@ -4554,7 +6120,7 @@ mod tests { validate_production_sha_publics(std::slice::from_ref(&valid), &field_cfg).unwrap(); let mut bad_selector = valid.clone(); - bad_selector.columns.columns[ShaPublicCol::SOut.index()][0] = f(1); + bad_selector.columns[ShaPublicCol::SOut.index()].evaluations[0] = f(1); assert!(matches!( validate_production_sha_publics(&[bad_selector], &field_cfg), Err(ProductionShaError::InvalidPublicSelector { @@ -4564,7 +6130,7 @@ mod tests { )); let mut non_boolean_selector = valid.clone(); - non_boolean_selector.columns.columns[ShaPublicCol::SInit.index()][0] = f(2); + non_boolean_selector.columns[ShaPublicCol::SInit.index()].evaluations[0] = f(2); assert!(matches!( validate_production_sha_publics(&[non_boolean_selector], &field_cfg), Err(ProductionShaError::NonBooleanPublicSelector { @@ -4574,7 +6140,7 @@ mod tests { )); let mut bad_k = valid; - bad_k.columns.columns[ShaPublicCol::K.index()][3] += f(1); + bad_k.columns[ShaPublicCol::K.index()].evaluations[3] += f(1); assert!(matches!( validate_production_sha_publics(&[bad_k], &field_cfg), Err(ProductionShaError::InvalidRoundConstant { row: 3 }) @@ -4615,7 +6181,10 @@ mod tests { ); let d = eq_eval(&beta, prover_output.r_b(), F::one_with_cfg(&field_cfg)).unwrap(); - assert_eq!(prover_output.c_sf(), &(d * prover_output.t_prime())); + assert_eq!( + prover_output.c_sf(), + &(d * prover_output.final_round_sumcheck_claim()) + ); let mut bad_targets = fresh_targets; bad_targets[0] += f(1); @@ -4724,7 +6293,7 @@ mod tests { &field_cfg, ) .unwrap(); - let verifier_output = derive_sha_instance_fold_claim( + let verifier_output = derive_instance_fold_claim( &beta, verified_sumfold.r_b, verified_sumfold.c_sf, @@ -4741,7 +6310,7 @@ mod tests { assert_eq!(prover_output.eq_instance_weights().len(), traces.len()); let (folded, folded_public) = - fold_projected_sha_traces(&traces, &publics, &prover_output, &field_cfg).unwrap(); + fold_projected_traces(&traces, &publics, &prover_output, &field_cfg).unwrap(); let folded_sum = expression_folded_row_sum( &folded.trace, &folded_public, @@ -4754,7 +6323,7 @@ mod tests { &field_cfg, ) .unwrap(); - assert_eq!(prover_output.t_prime(), &folded_sum); + assert_eq!(prover_output.final_round_sumcheck_claim(), &folded_sum); let mut bad_transcript = Blake3Transcript::new(); bad_transcript.absorb_slice(b"full-sha-sumfold-context"); @@ -4765,29 +6334,34 @@ mod tests { } #[test] - fn folded_row_sumcheck_claim_is_t_prime() { + fn folded_row_sumcheck_claim_matches_folded_integrand_sum() { let field_cfg = cfg(); let row_integrand_values = (0..(1usize << SHA_ROW_VARS)) .map(|idx| f((idx as u64).wrapping_mul(3) + 1)) .collect::>(); - let t_prime = folded_row_integrand_sum(&row_integrand_values, &field_cfg).unwrap(); + let final_round_sumcheck_claim = + folded_row_integrand_sum(&row_integrand_values, &field_cfg).unwrap(); let mut prover_transcript = Blake3Transcript::new(); prover_transcript.absorb_slice(b"folded-row-context"); let proof = prove_folded_row_sumcheck( &mut prover_transcript, &row_integrand_values, - &t_prime, + &final_round_sumcheck_claim, &field_cfg, ) .unwrap(); - assert_eq!(proof.claimed_sums(), &[t_prime.clone()]); + assert_eq!(proof.claimed_sums(), &[final_round_sumcheck_claim.clone()]); let mut verifier_transcript = Blake3Transcript::new(); verifier_transcript.absorb_slice(b"folded-row-context"); - let output = - verify_folded_row_sumcheck(&mut verifier_transcript, &proof, &t_prime, &field_cfg) - .unwrap(); + let output = verify_folded_row_sumcheck( + &mut verifier_transcript, + &proof, + &final_round_sumcheck_claim, + &field_cfg, + ) + .unwrap(); assert_eq!(output.r_star.len(), SHA_ROW_VARS); let row_weights = build_eq_x_r_vec(&output.r_star, &field_cfg).unwrap(); @@ -4803,12 +6377,13 @@ mod tests { bad_terminal += f(1); assert!(verify_folded_row_terminal_value(&output, &bad_terminal).is_err()); - let mut bad_t = t_prime; - bad_t += f(1); + let mut bad_claim = final_round_sumcheck_claim; + bad_claim += f(1); let mut bad_transcript = Blake3Transcript::new(); bad_transcript.absorb_slice(b"folded-row-context"); assert!( - verify_folded_row_sumcheck(&mut bad_transcript, &proof, &bad_t, &field_cfg).is_err() + verify_folded_row_sumcheck(&mut bad_transcript, &proof, &bad_claim, &field_cfg) + .is_err() ); } @@ -4895,7 +6470,7 @@ mod tests { verify_folded_row_sumcheck(&mut verifier_transcript, &proof, &t_prime, &field_cfg) .unwrap(); let endpoint_evals = - build_sha_endpoint_evals_from_trace(&trace, &output.r_star, &field_cfg).unwrap(); + build_sha_endpoint_evals_from_trace(&trace, &output.r_star, &a, &field_cfg).unwrap(); let terminal = reconstruct_folded_row_terminal_from_endpoints( &endpoint_evals, &public, @@ -4942,7 +6517,7 @@ mod tests { let public = zero_public(); let r_star = vec![f(2), f(3), f(5), f(7), f(11), f(13), f(17)]; let endpoint_evals = - build_sha_endpoint_evals_from_trace(&trace, &r_star, &field_cfg).unwrap(); + build_sha_endpoint_evals_from_trace(&trace, &r_star, &a, &field_cfg).unwrap(); let mut prover_transcript = Blake3Transcript::new(); prover_transcript.absorb_slice(b"endpoint-multipoint-context"); @@ -5037,10 +6612,11 @@ mod tests { #[test] fn endpoint_layout_must_be_exact_and_canonical() { let field_cfg = cfg(); - let trace = zero_trace_with_scalar_challenge(&f(5)); + let a = f(5); + let trace = zero_trace_with_scalar_challenge(&a); let r_star = vec![f(2), f(3), f(5), f(7), f(11), f(13), f(17)]; let endpoint_evals = - build_sha_endpoint_evals_from_trace(&trace, &r_star, &field_cfg).unwrap(); + build_sha_endpoint_evals_from_trace(&trace, &r_star, &a, &field_cfg).unwrap(); validate_sha_endpoint_layout(&endpoint_evals).unwrap(); let mut missing = endpoint_evals.clone(); @@ -5059,7 +6635,7 @@ mod tests { fn pcs_lifted_evals_drive_multipoint_sources_and_recompute_publics() { let field_cfg = cfg(); let mut public = zero_public(); - public.columns.columns[ShaPublicCol::K.index()][0] = f(99); + public.columns[ShaPublicCol::K.index()].evaluations[0] = f(99); let r_0 = vec![F::zero_with_cfg(&field_cfg); SHA_ROW_VARS]; let layout = production_sha_multipoint_layout(); diff --git a/test-uair/src/lib.rs b/test-uair/src/lib.rs index 776023a7..68980caa 100644 --- a/test-uair/src/lib.rs +++ b/test-uair/src/lib.rs @@ -14,9 +14,9 @@ pub use ecdsa_doubling::{EC_FP_INT_LIMBS, EcdsaFpRing, JacobianDoublingUair}; pub use generate_trace::*; pub use sha_ecdsa::ShaEcdsaUair; pub use sha256::{ - Sha256CompressionSliceUair, Sha256Ideal, Sha256MessageBlock, Sha256State, Sha256WitnessError, - sha256_compress_native, synthesize_one_sha256_compression_trace, - synthesize_sha256_chain_witnesses, + SHA256_INITIAL_STATE, Sha256CompressionSliceUair, Sha256Ideal, Sha256MessageBlock, Sha256State, + Sha256WitnessError, sha256_compress_native, sha256_padded_message_blocks, + synthesize_one_sha256_compression_trace, synthesize_sha256_chain_witnesses, }; use crypto_primitives::{ConstSemiring, FixedSemiring, Semiring, boolean::Boolean}; diff --git a/test-uair/src/sha256.rs b/test-uair/src/sha256.rs index 72f496af..ee2055e0 100644 --- a/test-uair/src/sha256.rs +++ b/test-uair/src/sha256.rs @@ -252,12 +252,23 @@ pub struct Sha256CompressionSliceUair(PhantomData); /// Canonical SHA-256 compression state `[a, b, c, d, e, f, g, h]`. pub type Sha256State = [u32; 8]; +/// Canonical SHA-256 initial hash state H_0 (FIPS 180-4 section 5.3.3). +pub const SHA256_INITIAL_STATE: Sha256State = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, +]; + /// One 512-bit SHA-256 message block as sixteen big-endian words. pub type Sha256MessageBlock = [u32; 16]; /// Errors surfaced by deterministic SHA-256 witness synthesis. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Sha256WitnessError { + /// A byte message is too long to encode its bit length in SHA-256's u64 + /// length suffix. + MessageBitLengthOverflow { bytes: usize }, + /// The canonical SHA-256 padding of a byte message did not produce the + /// caller-requested fixed number of blocks. + PaddedBlockCountMismatch { expected: usize, got: usize }, /// The requested packed trace would not fit in the requested MLE size. TraceTooSmall { num_compressions: usize, @@ -277,6 +288,16 @@ pub enum Sha256WitnessError { impl fmt::Display for Sha256WitnessError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::MessageBitLengthOverflow { bytes } => { + write!( + f, + "message of {bytes} byte(s) is too long for SHA-256 padding" + ) + } + Self::PaddedBlockCountMismatch { expected, got } => write!( + f, + "expected canonical SHA-256 padding to produce {expected} block(s), got {got}" + ), Self::TraceTooSmall { num_compressions, active_rows, @@ -1348,6 +1369,70 @@ pub fn sha256_compress_native( ] } +/// Canonically pad `message` as SHA-256 input and return exactly `N` 512-bit +/// blocks as big-endian words. +/// +/// This intentionally rejects messages whose canonical padding would require a +/// different number of blocks; callers that ask for `[Sha256MessageBlock; 8]` +/// should pass a message whose SHA-256 padding really spans eight blocks. +pub fn sha256_padded_message_blocks( + message: &[u8], +) -> Result<[Sha256MessageBlock; N], Sha256WitnessError> { + const BLOCK_BYTES: usize = 64; + const LENGTH_BYTES: usize = 8; + const WORD_BYTES: usize = 4; + + let message_len_u64 = + u64::try_from(message.len()).map_err(|_| Sha256WitnessError::MessageBitLengthOverflow { + bytes: message.len(), + })?; + let bit_len = + message_len_u64 + .checked_mul(8) + .ok_or(Sha256WitnessError::MessageBitLengthOverflow { + bytes: message.len(), + })?; + let padded_len = message + .len() + .checked_add(1) + .and_then(|len| len.checked_add(LENGTH_BYTES)) + .ok_or(Sha256WitnessError::MessageBitLengthOverflow { + bytes: message.len(), + })?; + let required_blocks = padded_len.div_ceil(BLOCK_BYTES); + if required_blocks != N { + return Err(Sha256WitnessError::PaddedBlockCountMismatch { + expected: N, + got: required_blocks, + }); + } + + let requested_len = + N.checked_mul(BLOCK_BYTES) + .ok_or(Sha256WitnessError::MessageBitLengthOverflow { + bytes: message.len(), + })?; + let mut padded = vec![0u8; requested_len]; + padded[..message.len()].copy_from_slice(message); + padded[message.len()] = 0x80; + padded[requested_len - LENGTH_BYTES..].copy_from_slice(&bit_len.to_be_bytes()); + + let mut blocks = [[0u32; 16]; N]; + for (block_idx, block) in blocks.iter_mut().enumerate() { + let block_start = block_idx * BLOCK_BYTES; + for (word_idx, word) in block.iter_mut().enumerate() { + let word_start = block_start + word_idx * WORD_BYTES; + *word = u32::from_be_bytes([ + padded[word_start], + padded[word_start + 1], + padded[word_start + 2], + padded[word_start + 3], + ]); + } + } + Ok(blocks) +} + /// Synthesize one fresh SHA-256 compression UAIR trace. pub fn synthesize_one_sha256_compression_trace( initial_state: Sha256State, @@ -2004,17 +2089,14 @@ where #[cfg(test)] mod tests { use super::*; - use crypto_primitives::crypto_bigint_int::Int; + use crypto_primitives::{crypto_bigint_int::Int, semiring::boolean::Boolean}; use zinc_uair::{ Uair, degree_counter::{count_effective_max_degree, count_max_degree}, }; fn test_initial_state() -> Sha256State { - [ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, - 0x5be0cd19, - ] + SHA256_INITIAL_STATE } fn test_message_blocks() -> [Sha256MessageBlock; N] { @@ -2034,6 +2116,72 @@ mod tests { }) } + #[test] + fn padded_message_blocks_encode_short_message() { + let blocks = sha256_padded_message_blocks::<1>(b"abc") + .expect("abc should canonically pad to one SHA-256 block"); + + assert_eq!(blocks[0][0], 0x6162_6380); + assert_eq!(blocks[0][14], 0); + assert_eq!(blocks[0][15], 24); + } + + #[test] + fn padded_message_blocks_reject_wrong_fixed_count() { + let err = sha256_padded_message_blocks::<8>(b"abc") + .expect_err("abc should not canonically pad to eight SHA-256 blocks"); + + assert!(matches!( + err, + Sha256WitnessError::PaddedBlockCountMismatch { + expected: 8, + got: 1 + } + )); + } + + #[test] + fn padded_message_blocks_can_make_eight_block_fixture_from_string() { + let message = vec!["hello world"; 40].join(" "); + let blocks = sha256_padded_message_blocks::<8>(message.as_bytes()) + .expect("test message should canonically pad to eight SHA-256 blocks"); + + assert_eq!(blocks[0][0], u32::from_be_bytes(*b"hell")); + assert_eq!(blocks[7][14], 0); + assert_eq!(blocks[7][15], u32::try_from(message.len() * 8).unwrap()); + } + + fn binary_word_to_u32(word: &BinaryPoly<32>) -> u32 { + let dense: DensePolynomial = word.clone().into(); + dense + .coeffs + .iter() + .enumerate() + .fold(0u32, |acc, (bit, coeff)| { + if coeff.inner() { + let shift = u32::try_from(bit).expect("SHA-256 bit index fits u32"); + acc | 1u32 + .checked_shl(shift) + .expect("SHA-256 bit index is below word width") + } else { + acc + } + }) + } + + fn public_sha_state_at( + trace: &UairTrace<'_, P, I, 32>, + row_start: usize, + ) -> Sha256State { + let h_a = std::array::from_fn(|j| { + binary_word_to_u32(&trace.binary_poly[cols::PA_A].evaluations[row_start + j]) + }); + let h_e = std::array::from_fn(|j| { + binary_word_to_u32(&trace.binary_poly[cols::PA_E].evaluations[row_start + j]) + }); + trace_halves_to_state(h_a, h_e) + } + fn assert_sha_trace_splits_cleanly(trace: &UairTrace<'static, Int<5>, Int<5>, 32>) { let sig = > as Uair>::signature(); let public = trace.public(&sig); @@ -2097,6 +2245,30 @@ mod tests { } } + #[test] + fn synthesize_sha256_chain_witnesses_links_adjacent_states() { + let initial_state = test_initial_state(); + let message_blocks = test_message_blocks::<8>(); + + let (witnesses, final_state) = + synthesize_sha256_chain_witnesses::, 8>(initial_state, message_blocks) + .expect("N=8 SHA witness synthesis should succeed"); + + assert_eq!(public_sha_state_at(&witnesses[0].trace, 0), initial_state); + for (index, pair) in witnesses.windows(2).enumerate() { + let left_output = public_sha_state_at(&pair[0].trace, cols::ROWS_PER_COMP); + let right_input = public_sha_state_at(&pair[1].trace, 0); + assert_eq!( + left_output, right_input, + "SHA witness {index} output must feed the next witness input" + ); + } + assert_eq!( + public_sha_state_at(&witnesses[witnesses.len() - 1].trace, cols::ROWS_PER_COMP), + final_state + ); + } + /// Cross-check the K_CANONICAL table against the canonical SHA-256 /// initial hash values H_0 — running one full compression of the /// empty-padding block (with H_0 as input) must produce the @@ -2106,11 +2278,7 @@ mod tests { /// logic itself). #[test] fn k_canonical_matches_sha256_empty_string_digest() { - // SHA-256 H_0 (FIPS 180-4 §5.3.3). - let h_in: [u32; 8] = [ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, - 0x5be0cd19, - ]; + let h_in = SHA256_INITIAL_STATE; // Empty-string padded block: single 0x80 byte then 63 zero bytes. let mut m = [0u32; 16]; m[0] = 0x80000000; diff --git a/test-uair/src/sha_ecdsa.rs b/test-uair/src/sha_ecdsa.rs index 5aea66b8..2eb3e11e 100644 --- a/test-uair/src/sha_ecdsa.rs +++ b/test-uair/src/sha_ecdsa.rs @@ -481,7 +481,7 @@ where let _down_w_e_sh2 = &down.binary_poly[5]; let down_w_e_sh4 = &down.binary_poly[6]; let down_w_sig1_sh3 = &down.binary_poly[7]; - let down_w_w_sh3 = &down.binary_poly[8]; + let _down_w_w_sh3 = &down.binary_poly[8]; let down_w_w_sh9 = &down.binary_poly[9]; let down_w_w_sh16 = &down.binary_poly[10]; let down_w_lsig0_sh1 = &down.binary_poly[11]; @@ -602,7 +602,7 @@ where - down_w_u_ef_sh3 - down_w_u_neg_e_g_sh3 - down_pa_k_sh3 - - down_w_w_sh3 + - w_big_w - down_w_sig0_sh3 - down_w_maj_sh3 + &mu_a_contrib; @@ -616,7 +616,7 @@ where - down_w_u_ef_sh3 - down_w_u_neg_e_g_sh3 - down_pa_k_sh3 - - down_w_w_sh3 + - w_big_w + &mu_e_contrib; b.assert_in_ideal(e_update_inner + pa_c_c9, &ideal_rot_x2); diff --git a/transcript/src/traits.rs b/transcript/src/traits.rs index 8c29dc1f..cc3f1460 100644 --- a/transcript/src/traits.rs +++ b/transcript/src/traits.rs @@ -340,6 +340,30 @@ pub trait Transcript { F::new_with_cfg(random_inner, cfg) } + fn get_variable_field_challenge( + &mut self, + cfg: &F::Config, + num_bytes: usize, + ) -> F + where + F::Inner: GenTranscribable, + { + let mut buf = vec![0u8; num_bytes]; + for chunk in buf.chunks_mut(u64::NUM_BYTES) { + let word = self.get_challenge::(); + chunk.copy_from_slice(&word.to_le_bytes()[..chunk.len()]); + } + F::new_with_cfg(F::Inner::read_transcription_bytes_exact(&buf), cfg) + } + + fn get_transcribable_field_challenge(&mut self, cfg: &F::Config) -> F + where + F::Inner: Transcribable, + { + let zero = F::zero_with_cfg(cfg); + self.get_variable_field_challenge(cfg, zero.inner().get_num_bytes()) + } + /// Generates a pseudorandom transcribable values as challenges based on the /// current transcript state, updating it. // TODO(Alex): `get_field_challenge` is not efficient @@ -353,6 +377,19 @@ pub trait Transcript { (0..n).map(|_| self.get_field_challenge(cfg)).collect() } + fn get_transcribable_field_challenges( + &mut self, + n: usize, + cfg: &F::Config, + ) -> Vec + where + F::Inner: Transcribable, + { + (0..n) + .map(|_| self.get_transcribable_field_challenge(cfg)) + .collect() + } + /// Generates a pseudorandom transcribable values as challenges based on the /// current transcript state, updating it. fn get_challenges(&mut self, n: usize) -> Vec { @@ -412,6 +449,16 @@ pub trait Transcript { self.absorb_inner(&[0x3]) } + fn absorb_random_field_owned(&mut self, v: &F) + where + F: PrimeField, + F::Inner: Transcribable, + F::Modulus: Transcribable, + { + let mut buf = vec![0u8; v.inner().get_num_bytes()]; + self.absorb_random_field(v, &mut buf); + } + /// Absorbs a slice of field element into the transcript. /// Delegates to the field element's implementation of /// absorb_into_transcript. @@ -423,6 +470,15 @@ pub trait Transcript { { v.iter().for_each(|x| self.absorb_random_field(x, buf)); } + + fn absorb_random_field_slice_owned(&mut self, v: &[F]) + where + F: PrimeField, + F::Inner: Transcribable, + F::Modulus: Transcribable, + { + v.iter().for_each(|x| self.absorb_random_field_owned(x)); + } } // diff --git a/utils/src/field/boxed_monty.rs b/utils/src/field/boxed_monty.rs index 3032120d..eb63dd5b 100644 --- a/utils/src/field/boxed_monty.rs +++ b/utils/src/field/boxed_monty.rs @@ -5,7 +5,8 @@ use crypto_primitives::{ }; use crate::{ - delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, mul_by_scalar::MulByScalar, + delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, + inner_transparent_field::InnerTransparentField, mul_by_scalar::MulByScalar, projectable_to_field::ProjectableToField, }; @@ -69,6 +70,25 @@ where } } +impl InnerTransparentField for BoxedMontyField { + fn add_inner(lhs: &Self::Inner, rhs: &Self::Inner, config: &Self::Config) -> Self::Inner { + let lhs = BoxedMontyForm::from_montgomery(lhs.clone(), config.clone()); + let rhs = BoxedMontyForm::from_montgomery(rhs.clone(), config.clone()); + (lhs + rhs).to_montgomery() + } + + fn sub_inner(lhs: &Self::Inner, rhs: &Self::Inner, config: &Self::Config) -> Self::Inner { + let lhs = BoxedMontyForm::from_montgomery(lhs.clone(), config.clone()); + let rhs = BoxedMontyForm::from_montgomery(rhs.clone(), config.clone()); + (lhs - rhs).to_montgomery() + } + + fn mul_assign_by_inner(&mut self, rhs: &Self::Inner) { + let rhs = Self::new_unchecked_with_cfg(rhs.clone(), self.cfg()); + *self *= rhs; + } +} + #[cfg(test)] #[allow( clippy::arithmetic_side_effects, diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index 6da0e5f3..bd2ee854 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -9,9 +9,10 @@ use std::{ use ark_ec::{AffineRepr, CurveGroup}; use ark_ff::{BigInteger, PrimeField as ArkPrimeField, Zero}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress}; +use crypto_bigint::{BoxedUint, modular::BoxedMontyForm}; use crypto_primitives::{ - FromWithConfig, IntRing, PrimeField, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, - crypto_bigint_uint::Uint, + FromWithConfig, IntRing, PrimeField, crypto_bigint_boxed_monty::BoxedMontyField, + crypto_bigint_int::Int, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, }; use num_integer::Integer; use zinc_poly::{ @@ -225,6 +226,31 @@ where } } +impl HyraxFieldBridge for BoxedMontyField +where + C: AffineRepr, +{ + fn field_to_scalar(value: &Self) -> C::ScalarField { + assert_curve_scalar_modulus_boxed::(&value.modulus()); + + let canonical = BoxedMontyForm::from(value.clone()).retrieve(); + C::ScalarField::from_le_bytes_mod_order(&canonical.to_le_bytes()) + } + + fn scalar_to_field(value: &C::ScalarField, cfg: &Self::Config) -> Self { + let actual_modulus = cfg.modulus().clone().get(); + assert_curve_scalar_modulus_boxed::(&actual_modulus); + + let scalar_bigint: ::BigInt = value.clone().into(); + let scalar_uint = BoxedUint::from_le_slice( + &scalar_bigint.to_bytes_le(), + actual_modulus.bits_precision(), + ) + .expect("curve scalar must fit protocol field precision"); + BoxedMontyField::from_with_cfg(&scalar_uint, cfg) + } +} + fn validate_fold_inputs(values: &[T], theta_len: usize, label: &str) -> Result<(), ZipError> { if values.is_empty() { return Err(ZipError::InvalidPcsParam(format!( @@ -1100,6 +1126,21 @@ where ); } +fn assert_curve_scalar_modulus_boxed(actual: &BoxedUint) +where + C: AffineRepr, +{ + let expected = BoxedUint::from_le_slice( + &::MODULUS.to_bytes_le(), + actual.bits_precision(), + ) + .expect("curve scalar modulus must fit protocol field precision"); + assert_eq!( + actual, &expected, + "Hyrax field mismatch: protocol field modulus must equal curve scalar modulus", + ); +} + fn uint_from_le_bytes(bytes: &[u8]) -> Uint { let num_bytes = as ConstTranscribable>::NUM_BYTES; assert!( From 6b924c5943038517233c5ac43b68ef5ae8256145 Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 18:15:55 -0700 Subject: [PATCH 24/49] Refactor production SHA SumFold accumulators --- piop/src/neutron_nova/mod.rs | 16 +- piop/src/neutron_nova/projection_sha.rs | 629 +++++++++++++++----- protocol/src/production_sha.rs | 745 ++++++++++++++++++------ 3 files changed, 1070 insertions(+), 320 deletions(-) diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index b4d25cf2..15cea98e 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -31,17 +31,21 @@ pub use projection_sha::{ SHA_WORD_BITS, ShaBooleanitySource, ShaIntCol, ShaProductionIdeal, ShaProjectionError, ShaPublicCol, ShaPublicWordCol, ShaResidualFamily, ShaWordCol, VirtualChMajValues, beta_aggregate_nonzero_ideal_polys, beta_aggregate_nonzero_ideal_polys_with_weights, - bit_slice_index, build_dense_sha_sumfold_group, build_dense_sha_sumfold_group_with_weights, - build_expression_folded_row_sumcheck_group, + bit_slice_index, build_booleanity_weights, build_dense_sha_sumfold_group, + build_dense_sha_sumfold_group_with_weights, build_expression_folded_row_sumcheck_group, build_expression_folded_row_sumcheck_group_with_row_weights, build_folded_row_sumcheck_group, build_fresh_sha_ideal_cache, build_linear_residual_coeff_tables, build_linear_residual_coeff_tables_with_row_weights, build_production_sha_sumfold_group, + build_production_sha_sumfold_group_from_prefix_accumulators, build_production_sha_sumfold_group_owned, build_production_sha_sumfold_group_with_linear_cache, build_production_sha_sumfold_group_with_linear_cache_and_weights, - build_sha_ideal_values_at_point, check_fresh_sha_ideal_cache, check_sha_ideal_values, - derive_instance_fold_claim, evaluate_fresh_sha_targets, expression_folded_row_sum, - expression_folded_row_sum_with_row_weights, fold_projected_traces, folded_row_integrand_sum, - folded_row_integrand_values, folded_row_integrand_values_with_row_weights, + build_sha_ideal_values_at_point, build_sha_lambda_powers, build_sha_residual_eval_powers, + build_sha_sumfold_linear_accumulator, build_sha_sumfold_quadratic_prefix_accumulator, + check_fresh_sha_ideal_cache, check_sha_ideal_values, derive_instance_fold_claim, + evaluate_fresh_sha_targets, expression_folded_row_sum, + expression_folded_row_sum_with_row_weights, expression_folded_row_sum_with_vectors, + fold_projected_traces, folded_row_integrand_sum, folded_row_integrand_values, + folded_row_integrand_values_with_row_weights, folded_row_integrand_values_with_vectors, production_sha_booleanity_sources, production_sha_nonzero_families, production_sha_nonzero_ideals, reconstruct_virtual_ch_maj_at_row, scalarize_bit_slices, sha_int_at_point, sha_int_at_point_with_weights, sha_linear_residual_row_value, diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index f3e55f43..455f37a3 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -378,26 +378,64 @@ where Ok(aggregate) } -fn linear_values_at_a_lambda( - tables: &[LinearResidualCoeffTable], - a: &F, - lambda: &F, - field_cfg: &F::Config, -) -> Result, ShaProjectionError> +pub fn build_sha_residual_eval_powers(a: &F, field_cfg: &F::Config) -> Vec where - F: DelayedFieldProductSum, + F: PrimeField, { - let lambda_powers = powers( - lambda.clone(), - F::one_with_cfg(field_cfg), - NUM_SHA_RESIDUAL_FAMILIES, - ); - let a_powers = powers( + powers( a.clone(), F::one_with_cfg(field_cfg), SHA_RESIDUAL_EVAL_POWER_COUNT, - ); + ) +} +pub fn build_sha_lambda_powers(lambda: &F, field_cfg: &F::Config) -> Vec +where + F: PrimeField, +{ + powers( + lambda.clone(), + F::one_with_cfg(field_cfg), + NUM_SHA_RESIDUAL_FAMILIES, + ) +} + +pub fn build_booleanity_weights( + rho: &F, + xi: &F, + source_count: usize, + field_cfg: &F::Config, +) -> Vec +where + F: PrimeField, +{ + powers(rho.clone(), F::one_with_cfg(field_cfg), source_count) + .into_iter() + .map(|rho_power| xi.clone() * rho_power) + .collect() +} + +pub fn build_sha_sumfold_linear_accumulator( + tables: &[LinearResidualCoeffTable], + a_powers: &[F], + lambda_powers: &[F], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: DelayedFieldProductSum, +{ + if lambda_powers.len() != NUM_SHA_RESIDUAL_FAMILIES { + return Err(ShaProjectionError::MissingColumn { + kind: "lambda_powers", + col: lambda_powers.len(), + }); + } + if a_powers.len() < SHA_RESIDUAL_EVAL_POWER_COUNT { + return Err(ShaProjectionError::MissingColumn { + kind: "a_powers", + col: a_powers.len(), + }); + } tables .iter() .map(|table| { @@ -1213,6 +1251,36 @@ pub fn folded_row_integrand_values_with_row_weights( booleanity_sources: &[ShaBooleanitySource], field_cfg: &F::Config, ) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum, + F::Inner: Zero, +{ + let lambda_powers = build_sha_lambda_powers(lambda, field_cfg); + let a_powers = build_sha_residual_eval_powers(a, field_cfg); + let booleanity_weights = build_booleanity_weights(rho, xi, booleanity_sources.len(), field_cfg); + folded_row_integrand_values_with_vectors( + trace, + public, + row_weights, + &a_powers, + &lambda_powers, + &booleanity_weights, + booleanity_sources, + field_cfg, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn folded_row_integrand_values_with_vectors( + trace: &ProjectedTrace, + public: &ProjectedPublic, + row_weights: &[F], + a_powers: &[F], + lambda_powers: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> where F: InnerTransparentField + DelayedFieldProductSum, F::Inner: Zero, @@ -1227,21 +1295,26 @@ where expected: SHA_ROW_COUNT, }); } - let lambda_powers = powers( - lambda.clone(), - F::one_with_cfg(field_cfg), - NUM_SHA_RESIDUAL_FAMILIES, - ); - let a_powers = powers( - a.clone(), - F::one_with_cfg(field_cfg), - SHA_RESIDUAL_EVAL_POWER_COUNT, - ); - let rho_powers = powers( - rho.clone(), - F::one_with_cfg(field_cfg), - booleanity_sources.len(), - ); + if lambda_powers.len() != NUM_SHA_RESIDUAL_FAMILIES { + return Err(ShaProjectionError::MissingColumn { + kind: "lambda_powers", + col: lambda_powers.len(), + }); + } + if a_powers.len() < SHA_RESIDUAL_EVAL_POWER_COUNT { + return Err(ShaProjectionError::MissingColumn { + kind: "a_powers", + col: a_powers.len(), + }); + } + if booleanity_weights.len() != booleanity_sources.len() { + return Err(ShaProjectionError::ColumnRowCount { + kind: "booleanity_weights", + col: 0, + got: booleanity_weights.len(), + expected: booleanity_sources.len(), + }); + } let needs_virtuals = sources_need_virtuals(booleanity_sources); let mut out = Vec::with_capacity(SHA_ROW_COUNT); @@ -1272,9 +1345,9 @@ where field_cfg, )?; let term = d.clone() * (d - F::one_with_cfg(field_cfg)); - bool_sum += rho_powers[idx].clone() * term; + bool_sum += booleanity_weights[idx].clone() * term; } - out.push(row_weights[row].clone() * (linear + xi.clone() * bool_sum)); + out.push(row_weights[row].clone() * (linear + bool_sum)); } Ok(out) } @@ -1591,7 +1664,6 @@ where struct RelationSumFoldPrefixFastPath { traces: Box<[ProjectedTrace]>, beta: Vec, - xi: F, booleanity_sources: Vec, linear: BinaryPrefixTailTable, booleanity: TernaryPrefixTailTable, @@ -1717,27 +1789,74 @@ where let tail_vars = ell - prefix_vars; let tail_len = binary_len(tail_vars); - let rho_powers = powers( - rho.clone(), - F::one_with_cfg(field_cfg), - booleanity_sources.len(), - ); - - let linear_values = linear_values_at_a_lambda(coeff_tables, a, lambda, field_cfg)?; - let linear = BinaryPrefixTailTable::new(linear_values, prefix_vars, tail_len); - let booleanity = TernaryPrefixTailTable::new( - build_sha_booleanity_prefix_tail_table( - &traces, - booleanity_sources, - prefix_vars, - tail_len, - &row_weights, - &rho_powers, - field_cfg, - )?, + let a_powers = build_sha_residual_eval_powers(a, field_cfg); + let lambda_powers = build_sha_lambda_powers(lambda, field_cfg); + let booleanity_weights = + build_booleanity_weights(rho, xi, booleanity_sources.len(), field_cfg); + let linear_values = build_sha_sumfold_linear_accumulator( + coeff_tables, + &a_powers, + &lambda_powers, + field_cfg, + )?; + let quadratic_values = build_sha_booleanity_prefix_tail_table( + &traces, + booleanity_sources, prefix_vars, tail_len, - )?; + row_weights, + &booleanity_weights, + field_cfg, + ); + Self::new_owned_with_accumulators( + traces, + beta, + &linear_values, + &quadratic_values?, + booleanity_sources, + prefix_vars, + field_cfg, + ) + } + + fn new_owned_with_accumulators( + traces: Box<[ProjectedTrace]>, + beta: &[F], + linear_values: &[F], + quadratic_values: &[F], + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + field_cfg: &F::Config, + ) -> Result { + let ell = validate_sha_sumfold_traces(&traces, beta)?; + if prefix_vars == 0 || prefix_vars > ell { + return Err(SumFoldError::Ell0TooLarge { + ell0: prefix_vars, + ell, + } + .into()); + } + + let tail_vars = ell - prefix_vars; + let tail_len = binary_len(tail_vars); + let linear_len = binary_len(prefix_vars) * tail_len; + if linear_values.len() != linear_len { + return Err(ShaProjectionError::InstanceCountMismatch { + got: linear_values.len(), + expected: linear_len, + }); + } + let quadratic_len = checked_ternary_len(prefix_vars)? * tail_len; + if quadratic_values.len() != quadratic_len { + return Err(ShaProjectionError::InstanceCountMismatch { + got: quadratic_values.len(), + expected: quadratic_len, + }); + } + + let linear = BinaryPrefixTailTable::new(linear_values.to_vec(), prefix_vars, tail_len); + let booleanity = + TernaryPrefixTailTable::new(quadratic_values.to_vec(), prefix_vars, tail_len)?; let tail_eq_weights = eq_weights_or_one(&beta[prefix_vars..], field_cfg)?; let mut prefix_suffix_eq_weights = Vec::with_capacity(prefix_vars); @@ -1749,7 +1868,6 @@ where Ok(Self { traces, beta: beta.to_vec(), - xi: xi.clone(), booleanity_sources: booleanity_sources.to_vec(), linear, booleanity, @@ -1786,9 +1904,7 @@ where let booleanity = self.booleanity .value_with_first_axis(ternary_rest, tail, x, field_cfg)?; - acc += self.tail_eq_weights[tail].clone() - * suffix_weight - * (linear + self.xi.clone() * booleanity); + acc += self.tail_eq_weights[tail].clone() * suffix_weight * (linear + booleanity); } } @@ -1986,7 +2102,7 @@ where F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: Zero, { - let ell = validate_sha_sumfold_inputs(traces, publics, beta)?; + let _ell = validate_sha_sumfold_inputs(traces, publics, beta)?; if beta_eq_weights.len() != traces.len() { return Err(ShaProjectionError::InstanceCountMismatch { got: beta_eq_weights.len(), @@ -2001,29 +2117,9 @@ where expected: SHA_ROW_COUNT, }); } - let zero = F::zero_with_cfg(field_cfg); - let zero_inner = zero.inner().clone(); - let mut mles = Vec::with_capacity(2 + booleanity_sources.len() * SHA_ROW_COUNT); - let lambda_powers = powers( - lambda.clone(), - F::one_with_cfg(field_cfg), - NUM_SHA_RESIDUAL_FAMILIES, - ); - let a_powers = powers( - a.clone(), - F::one_with_cfg(field_cfg), - SHA_RESIDUAL_EVAL_POWER_COUNT, - ); - - mles.push(DenseMultilinearExtension::from_evaluations_vec( - ell, - beta_eq_weights - .iter() - .map(|value| value.inner().clone()) - .collect(), - zero_inner.clone(), - )); - + let lambda_powers = build_sha_lambda_powers(lambda, field_cfg); + let a_powers = build_sha_residual_eval_powers(a, field_cfg); + let booleanity_weights = build_booleanity_weights(rho, xi, booleanity_sources.len(), field_cfg); let linear_values = traces .iter() .zip(publics.iter()) @@ -2038,9 +2134,77 @@ where ) }) .collect::, _>>()?; + build_dense_sha_sumfold_group_from_accumulators( + traces, + beta, + beta_eq_weights, + row_weights, + &linear_values, + &booleanity_weights, + booleanity_sources, + field_cfg, + ) +} + +#[allow(clippy::too_many_arguments)] +fn build_dense_sha_sumfold_group_from_accumulators( + traces: &[ProjectedTrace], + beta: &[F], + beta_eq_weights: &[F], + row_weights: &[F], + linear_accumulator: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + let ell = validate_sha_sumfold_traces(traces, beta)?; + if beta_eq_weights.len() != traces.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: beta_eq_weights.len(), + expected: traces.len(), + }); + } + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } + if linear_accumulator.len() != traces.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: linear_accumulator.len(), + expected: traces.len(), + }); + } + if booleanity_weights.len() != booleanity_sources.len() { + return Err(ShaProjectionError::ColumnRowCount { + kind: "booleanity_weights", + col: 0, + got: booleanity_weights.len(), + expected: booleanity_sources.len(), + }); + } + + let zero_inner = F::zero_with_cfg(field_cfg).inner().clone(); + let mut mles = Vec::with_capacity(2 + booleanity_sources.len() * SHA_ROW_COUNT); + mles.push(DenseMultilinearExtension::from_evaluations_vec( ell, - linear_values + beta_eq_weights + .iter() + .map(|value| value.inner().clone()) + .collect(), + zero_inner.clone(), + )); + mles.push(DenseMultilinearExtension::from_evaluations_vec( + ell, + linear_accumulator .iter() .map(|value| value.inner().clone()) .collect(), @@ -2064,16 +2228,97 @@ where Ok(MultiDegreeSumcheckGroup::new( 3, mles, - sha_sumfold_comb_fn( - row_weights.to_vec(), - powers( - rho.clone(), - F::one_with_cfg(field_cfg), - booleanity_sources.len(), - ), - xi.clone(), + sha_weighted_sumfold_comb_fn(row_weights.to_vec(), booleanity_weights.to_vec(), field_cfg), + )) +} + +#[allow(clippy::too_many_arguments)] +pub fn build_production_sha_sumfold_group_from_prefix_accumulators( + traces: &[ProjectedTrace], + beta: &[F], + beta_eq_weights: &[F], + row_weights: &[F], + linear_accumulator: &[F], + quadratic_prefix_accumulator: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + let ell = validate_sha_sumfold_traces(traces, beta)?; + if beta_eq_weights.len() != traces.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: beta_eq_weights.len(), + expected: traces.len(), + }); + } + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } + if linear_accumulator.len() != traces.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: linear_accumulator.len(), + expected: traces.len(), + }); + } + if booleanity_weights.len() != booleanity_sources.len() { + return Err(ShaProjectionError::ColumnRowCount { + kind: "booleanity_weights", + col: 0, + got: booleanity_weights.len(), + expected: booleanity_sources.len(), + }); + } + if prefix_vars > ell { + return Err(SumFoldError::Ell0TooLarge { + ell0: prefix_vars, + ell, + } + .into()); + } + if prefix_vars == 0 { + if !quadratic_prefix_accumulator.is_empty() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: quadratic_prefix_accumulator.len(), + expected: 0, + }); + } + return build_dense_sha_sumfold_group_from_accumulators( + traces, + beta, + beta_eq_weights, + row_weights, + linear_accumulator, + booleanity_weights, + booleanity_sources, field_cfg, - ), + ); + } + + let fast_path = RelationSumFoldPrefixFastPath::new_owned_with_accumulators( + traces.to_vec().into_boxed_slice(), + beta, + linear_accumulator, + quadratic_prefix_accumulator, + booleanity_sources, + prefix_vars, + field_cfg, + ); + + Ok(MultiDegreeSumcheckGroup::with_prefix_fast( + 3, + Vec::new(), + sha_weighted_sumfold_comb_fn(row_weights.to_vec(), booleanity_weights.to_vec(), field_cfg), + Box::new(fast_path?), )) } @@ -2304,7 +2549,6 @@ where F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: Zero, { - let ell = beta.len(); if beta_eq_weights.len() != traces.len() { return Err(ShaProjectionError::InstanceCountMismatch { got: beta_eq_weights.len(), @@ -2319,57 +2563,21 @@ where expected: SHA_ROW_COUNT, }); } - let zero = F::zero_with_cfg(field_cfg); - let zero_inner = zero.inner().clone(); - let mut mles = Vec::with_capacity(2 + booleanity_sources.len() * SHA_ROW_COUNT); - - mles.push(DenseMultilinearExtension::from_evaluations_vec( - ell, - beta_eq_weights - .iter() - .map(|value| value.inner().clone()) - .collect(), - zero_inner.clone(), - )); - - let linear_values = linear_values_at_a_lambda(linear_cache, a, lambda, field_cfg)?; - mles.push(DenseMultilinearExtension::from_evaluations_vec( - ell, - linear_values - .iter() - .map(|value| value.inner().clone()) - .collect(), - zero_inner.clone(), - )); - - for source in booleanity_sources { - for row in 0..SHA_ROW_COUNT { - let values = traces - .iter() - .map(|trace| booleanity_source_value_at_row(trace, row, source, field_cfg)) - .collect::, _>>()?; - mles.push(DenseMultilinearExtension::from_evaluations_vec( - ell, - values.iter().map(|value| value.inner().clone()).collect(), - zero_inner.clone(), - )); - } - } - - Ok(MultiDegreeSumcheckGroup::new( - 3, - mles, - sha_sumfold_comb_fn( - row_weights.to_vec(), - powers( - rho.clone(), - F::one_with_cfg(field_cfg), - booleanity_sources.len(), - ), - xi.clone(), - field_cfg, - ), - )) + let a_powers = build_sha_residual_eval_powers(a, field_cfg); + let lambda_powers = build_sha_lambda_powers(lambda, field_cfg); + let booleanity_weights = build_booleanity_weights(rho, xi, booleanity_sources.len(), field_cfg); + let linear_values = + build_sha_sumfold_linear_accumulator(linear_cache, &a_powers, &lambda_powers, field_cfg)?; + build_dense_sha_sumfold_group_from_accumulators( + traces, + beta, + beta_eq_weights, + row_weights, + &linear_values, + &booleanity_weights, + booleanity_sources, + field_cfg, + ) } #[allow(clippy::too_many_arguments)] @@ -2450,6 +2658,21 @@ fn sha_sumfold_comb_fn( xi: F, field_cfg: &F::Config, ) -> CombFn +where + F: PrimeField + Send + Sync + 'static, +{ + let booleanity_weights = rho_powers + .into_iter() + .map(|rho_power| xi.clone() * rho_power) + .collect(); + sha_weighted_sumfold_comb_fn(row_weights, booleanity_weights, field_cfg) +} + +fn sha_weighted_sumfold_comb_fn( + row_weights: Vec, + booleanity_weights: Vec, + field_cfg: &F::Config, +) -> CombFn where F: PrimeField + Send + Sync + 'static, { @@ -2460,15 +2683,15 @@ where let linear = values[1].clone(); let mut bool_sum = zero_for_comb.clone(); let mut cursor = 2usize; - for rho_power in &rho_powers { + for booleanity_weight in &booleanity_weights { for row_weight in &row_weights { let d = values[cursor].clone(); cursor += 1; let term = d.clone() * (d - one_for_comb.clone()); - bool_sum += row_weight.clone() * rho_power * term; + bool_sum += row_weight.clone() * booleanity_weight * term; } } - eq_beta * (linear + xi.clone() * bool_sum) + eq_beta * (linear + bool_sum) }) } @@ -2540,6 +2763,90 @@ where x.clone() * beta + (one.clone() - x) * (one - beta) } +fn validate_sha_sumfold_traces( + traces: &[ProjectedTrace], + beta: &[F], +) -> Result { + if traces.is_empty() { + return Err(ShaProjectionError::InstanceCountNotPowerOfTwo { got: 0 }); + } + if !traces.len().is_power_of_two() { + return Err(ShaProjectionError::InstanceCountNotPowerOfTwo { got: traces.len() }); + } + let ell = usize::try_from(traces.len().trailing_zeros()).expect("trailing_zeros fits usize"); + if beta.len() != ell { + return Err(ShaProjectionError::InstanceCountMismatch { + got: beta.len(), + expected: ell, + }); + } + for trace in traces { + validate_trace(trace)?; + } + Ok(ell) +} + +#[allow(clippy::too_many_arguments)] +pub fn build_sha_sumfold_quadratic_prefix_accumulator( + traces: &[ProjectedTrace], + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + row_weights: &[F], + booleanity_weights: &[F], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + if traces.is_empty() { + return Err(ShaProjectionError::InstanceCountNotPowerOfTwo { got: 0 }); + } + if !traces.len().is_power_of_two() { + return Err(ShaProjectionError::InstanceCountNotPowerOfTwo { got: traces.len() }); + } + let ell = usize::try_from(traces.len().trailing_zeros()).expect("trailing_zeros fits usize"); + if prefix_vars > ell { + return Err(SumFoldError::Ell0TooLarge { + ell0: prefix_vars, + ell, + } + .into()); + } + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } + if booleanity_weights.len() != booleanity_sources.len() { + return Err(ShaProjectionError::ColumnRowCount { + kind: "booleanity_weights", + col: 0, + got: booleanity_weights.len(), + expected: booleanity_sources.len(), + }); + } + for trace in traces { + validate_trace(trace)?; + } + if prefix_vars == 0 { + return Ok(Vec::new()); + } + + let tail_len = binary_len(ell - prefix_vars); + build_sha_booleanity_prefix_tail_table( + traces, + booleanity_sources, + prefix_vars, + tail_len, + row_weights, + booleanity_weights, + field_cfg, + ) +} + #[allow(clippy::arithmetic_side_effects)] fn build_sha_booleanity_prefix_tail_table( traces: &[ProjectedTrace], @@ -2547,7 +2854,7 @@ fn build_sha_booleanity_prefix_tail_table( prefix_vars: usize, tail_len: usize, row_weights: &[F], - rho_powers: &[F], + booleanity_weights: &[F], field_cfg: &F::Config, ) -> Result, ShaProjectionError> where @@ -2577,8 +2884,8 @@ where field_cfg, )?; - for (source_idx, rho_power) in rho_powers.iter().enumerate() { - let source_weight = row_weight.clone() * rho_power; + for (source_idx, booleanity_weight) in booleanity_weights.iter().enumerate() { + let source_weight = row_weight.clone() * booleanity_weight; for (ternary_idx, plan) in coeff_plans.iter().enumerate() { let coeff = booleanity_degree_two_coeff( &source_values, @@ -3006,6 +3313,34 @@ where folded_row_integrand_sum(&values, field_cfg) } +#[allow(clippy::too_many_arguments)] +pub fn expression_folded_row_sum_with_vectors( + trace: &ProjectedTrace, + public: &ProjectedPublic, + row_weights: &[F], + a_powers: &[F], + lambda_powers: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result +where + F: InnerTransparentField + DelayedFieldProductSum, + F::Inner: Zero, +{ + let values = folded_row_integrand_values_with_vectors( + trace, + public, + row_weights, + a_powers, + lambda_powers, + booleanity_weights, + booleanity_sources, + field_cfg, + )?; + folded_row_integrand_sum(&values, field_cfg) +} + pub fn sha_word_bits_at_point( trace: &ProjectedTrace, col: ShaWordCol, diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index fb47efea..af0ffce4 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -31,11 +31,13 @@ use zinc_piop::{ NUM_SHA_RESIDUAL_FAMILIES, ProjectedPublic, ProjectedTrace, SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, ShaBooleanitySource, ShaIntCol, ShaProjectionError, ShaPublicCol, ShaPublicWordCol, ShaResidualFamily, ShaWordCol, - beta_aggregate_nonzero_ideal_polys_with_weights, bit_slice_index, + beta_aggregate_nonzero_ideal_polys_with_weights, bit_slice_index, build_booleanity_weights, build_dense_sha_sumfold_group, build_folded_row_sumcheck_group, build_linear_residual_coeff_tables_with_row_weights, - build_production_sha_sumfold_group_with_linear_cache_and_weights, - derive_instance_fold_claim, expression_folded_row_sum_with_row_weights, + build_production_sha_sumfold_group_from_prefix_accumulators, build_sha_lambda_powers, + build_sha_residual_eval_powers, build_sha_sumfold_linear_accumulator, + build_sha_sumfold_quadratic_prefix_accumulator, derive_instance_fold_claim, + expression_folded_row_sum_with_row_weights, expression_folded_row_sum_with_vectors, fold_projected_traces, folded_row_integrand_sum, production_sha_booleanity_sources, production_sha_nonzero_families, sha_int_at_point_with_weights, sha_public_at_point, sha_public_at_point_with_weights, sha_word_bits_at_point_with_weights, @@ -72,6 +74,11 @@ use zip_plus::{ pcs_transcript::{PcsProverTranscript, PcsVerifierTranscript}, }; +/// Serialized production ProjectionFold proof object. +/// +/// This object carries verifier messages and claimed evaluations only. Folding +/// weights, batching powers, folded accumulator values, and prover working +/// caches are derived from the transcript/setup or kept as prover-local state. #[derive(Clone, Debug)] pub struct ProductionLinearIdealFoldProof where @@ -1119,26 +1126,67 @@ where absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys, field_cfg); let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); + let a_powers = build_sha_residual_eval_powers(&a, field_cfg); + let lambda_powers = build_sha_lambda_powers(&lambda, field_cfg); + let booleanity_weights = + build_booleanity_weights(&rho, &xi, booleanity_sources.len(), field_cfg); let initial_claim = evaluate_aggregate_sha_ideal_claim(&aggregate_ideal_polys, &a, &lambda, field_cfg)?; - let (sumfold_proof, sumfold_output) = prove_optimized_sha_sumfold_with_weights( - transcript, + let linear_accumulator = + build_sha_sumfold_linear_accumulator(&coeff_tables, &a_powers, &lambda_powers, field_cfg)?; + let quadratic_prefix_accumulator = build_sha_sumfold_quadratic_prefix_accumulator( + &traces, + &booleanity_sources, + pp.prefix_vars, + &r_ic_eq_weights, + &booleanity_weights, + field_cfg, + )?; + let sumfold_group = build_production_sha_sumfold_group_from_prefix_accumulators( &traces, - &publics, - &initial_claim, &beta, &beta_eq_weights, - &r_ic, &r_ic_eq_weights, - &a, - &lambda, - &rho, - &xi, + &linear_accumulator, + &quadratic_prefix_accumulator, + &booleanity_weights, &booleanity_sources, - &coeff_tables, pp.prefix_vars, field_cfg, )?; + let (sumfold_proof, sumfold_r_b) = prove_optimized_sha_sumfold_with_weights( + transcript, + sumfold_group, + &initial_claim, + beta.len(), + field_cfg, + )?; + let provisional_sumfold_output = derive_instance_fold_claim( + &beta, + sumfold_r_b.clone(), + F::one_with_cfg(field_cfg), + witnesses.len(), + field_cfg, + )?; + let (folded, folded_public) = + fold_projected_traces(&traces, &publics, &provisional_sumfold_output, field_cfg)?; + let row_claim = expression_folded_row_sum_with_vectors( + &folded.trace, + &folded_public, + &r_ic_eq_weights, + &a_powers, + &lambda_powers, + &booleanity_weights, + &booleanity_sources, + field_cfg, + )?; + let sumfold_output = derive_instance_fold_claim_from_row_claim( + &beta, + sumfold_r_b, + &row_claim, + witnesses.len(), + field_cfg, + )?; let folded_commitments = fold_pcs_commitments::( &instance_commitments, @@ -1150,29 +1198,27 @@ where sumfold_output.eq_instance_weights(), field_cfg, )?; - let (folded, folded_public) = - fold_projected_traces(&traces, &publics, &sumfold_output, field_cfg)?; absorb_production_sha_commitments::( transcript, b"production_sha_derived_folded_commitments", std::slice::from_ref(&folded_commitments), ); + verify_folded_row_sumcheck_claim(&row_claim, sumfold_output.final_round_sumcheck_claim())?; let (combined_sumcheck, row_output) = - prove_expression_folded_row_sumcheck_with_output_and_weights( + prove_expression_folded_row_sumcheck_with_output_and_vectors( transcript, &folded.trace, &folded_public, &r_ic, &r_ic_eq_weights, - &a, - &lambda, - &rho, - &xi, + &a_powers, + &lambda_powers, + &booleanity_weights, &booleanity_sources, - sumfold_output.final_round_sumcheck_claim(), field_cfg, )?; + verify_folded_row_sumcheck_claim(&combined_sumcheck.claimed_sums()[0], &row_claim)?; let endpoint_evals = build_sha_endpoint_evals_from_trace_with_row_weights( &folded.trace, &row_output.r_star_eq_weights, @@ -1182,16 +1228,15 @@ where let resolver = sha_resolver_from_endpoint_evals(&endpoint_evals)?; absorb_sha_resolver_proof(transcript, &resolver, field_cfg); let resolver_endpoint_evals = sha_endpoint_evals_from_resolver(&resolver, &a, field_cfg)?; - let terminal = reconstruct_folded_row_terminal_from_endpoints_with_row_weights( + let terminal = reconstruct_folded_row_terminal_from_endpoints_with_vectors( &resolver_endpoint_evals, &folded_public, &r_ic, &row_output.r_star, &row_output.r_star_eq_weights, - &a, - &lambda, - &rho, - &xi, + &a_powers, + &lambda_powers, + &booleanity_weights, &booleanity_sources, field_cfg, )?; @@ -2847,7 +2892,7 @@ where )?; let (folded, folded_public) = zinc_piop::neutron_nova::fold_projected_traces(traces, publics, &provisional, field_cfg)?; - let t_prime = zinc_piop::neutron_nova::expression_folded_row_sum( + let post_sumfold_claim = zinc_piop::neutron_nova::expression_folded_row_sum( &folded.trace, &folded_public, r_ic, @@ -2859,7 +2904,7 @@ where field_cfg, )?; let d = eq_eval(beta, &r_b, F::one_with_cfg(field_cfg))?; - let c_sf = d * t_prime; + let c_sf = d * post_sumfold_claim; Ok(( proof, derive_instance_fold_claim(beta, r_b, c_sf, traces.len(), field_cfg)?, @@ -2895,45 +2940,70 @@ where { let beta_eq_weights = build_eq_x_r_vec(beta, field_cfg)?; let r_ic_eq_weights = build_eq_x_r_vec(r_ic, field_cfg)?; - prove_optimized_sha_sumfold_with_weights( - transcript, + let a_powers = build_sha_residual_eval_powers(a, field_cfg); + let lambda_powers = build_sha_lambda_powers(lambda, field_cfg); + let booleanity_weights = build_booleanity_weights(rho, xi, booleanity_sources.len(), field_cfg); + let linear_accumulator = + build_sha_sumfold_linear_accumulator(coeff_tables, &a_powers, &lambda_powers, field_cfg)?; + let quadratic_prefix_accumulator = build_sha_sumfold_quadratic_prefix_accumulator( + traces, + booleanity_sources, + prefix_vars, + &r_ic_eq_weights, + &booleanity_weights, + field_cfg, + )?; + let group = build_production_sha_sumfold_group_from_prefix_accumulators( traces, - publics, - initial_claim, beta, &beta_eq_weights, - r_ic, &r_ic_eq_weights, - a, - lambda, - rho, - xi, + &linear_accumulator, + &quadratic_prefix_accumulator, + &booleanity_weights, booleanity_sources, - coeff_tables, prefix_vars, field_cfg, - ) + )?; + let (proof, r_b) = prove_optimized_sha_sumfold_with_weights( + transcript, + group, + initial_claim, + beta.len(), + field_cfg, + )?; + let provisional = derive_instance_fold_claim( + beta, + r_b.clone(), + F::one_with_cfg(field_cfg), + traces.len(), + field_cfg, + )?; + let (folded, folded_public) = + zinc_piop::neutron_nova::fold_projected_traces(traces, publics, &provisional, field_cfg)?; + let row_claim = expression_folded_row_sum_with_vectors( + &folded.trace, + &folded_public, + &r_ic_eq_weights, + &a_powers, + &lambda_powers, + &booleanity_weights, + booleanity_sources, + field_cfg, + )?; + Ok(( + proof, + derive_instance_fold_claim_from_row_claim(beta, r_b, &row_claim, traces.len(), field_cfg)?, + )) } -#[allow(clippy::too_many_arguments)] pub fn prove_optimized_sha_sumfold_with_weights( transcript: &mut impl Transcript, - traces: &[ProjectedTrace], - publics: &[ProjectedPublic], + group: MultiDegreeSumcheckGroup, initial_claim: &F, - beta: &[F], - beta_eq_weights: &[F], - r_ic: &[F; SHA_ROW_VARS], - r_ic_eq_weights: &[F], - a: &F, - lambda: &F, - rho: &F, - xi: &F, - booleanity_sources: &[ShaBooleanitySource], - coeff_tables: &[LinearResidualCoeffTable], - prefix_vars: usize, + instance_vars: usize, field_cfg: &F::Config, -) -> Result<(MultiDegreeSumcheckProof, InstanceFoldClaim), ProductionShaError> +) -> Result<(MultiDegreeSumcheckProof, Vec), ProductionShaError> where F: InnerTransparentField + DelayedFieldProductSum @@ -2944,67 +3014,47 @@ where F::Inner: Transcribable + Zero, F::Modulus: Transcribable, { - let group = build_production_sha_sumfold_group_with_linear_cache_and_weights( - traces, - publics, - coeff_tables, - beta, - beta_eq_weights, - r_ic, - r_ic_eq_weights, - a, - lambda, - rho, - xi, - booleanity_sources, - prefix_vars, + let (proof, states) = MultiDegreeSumcheck::prove_as_subprotocol( + transcript, + vec![group], + instance_vars, field_cfg, - )?; - let ell = beta.len(); - let (proof, states) = - MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], ell, field_cfg); + ); require_single_sumcheck_group(&proof, "SHA SumFold")?; if proof.claimed_sums()[0] != *initial_claim { return Err(ProductionShaError::SumFoldTerminalMismatch); } - let r_b = states - .first() - .ok_or(ProductionShaError::LengthMismatch { - label: "sumfold states", - got: 0, - expected: 1, - })? - .randomness - .clone(); - let provisional = derive_instance_fold_claim( - beta, - r_b.clone(), - F::one_with_cfg(field_cfg), - traces.len(), - field_cfg, - )?; - let (folded, folded_public) = - zinc_piop::neutron_nova::fold_projected_traces(traces, publics, &provisional, field_cfg)?; - let t_prime = expression_folded_row_sum_with_row_weights( - &folded.trace, - &folded_public, - r_ic_eq_weights, - a, - lambda, - rho, - xi, - booleanity_sources, - field_cfg, - )?; - let d = eq_eval(beta, &r_b, F::one_with_cfg(field_cfg))?; - let c_sf = d * t_prime; Ok(( proof, - derive_instance_fold_claim(beta, r_b, c_sf, traces.len(), field_cfg)?, + states + .first() + .ok_or(ProductionShaError::LengthMismatch { + label: "sumfold states", + got: 0, + expected: 1, + })? + .randomness + .clone(), )) } +pub fn derive_instance_fold_claim_from_row_claim( + beta: &[F], + r_b: Vec, + row_claim: &F, + instance_count: usize, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + let d = eq_eval(beta, &r_b, F::one_with_cfg(field_cfg))?; + let c_sf = d * row_claim; + derive_instance_fold_claim(beta, r_b, c_sf, instance_count, field_cfg) + .map_err(ProductionShaError::from) +} + pub fn verify_full_sha_sumfold( transcript: &mut impl Transcript, proof: &MultiDegreeSumcheckProof, @@ -3127,7 +3177,7 @@ where pub fn prove_folded_row_sumcheck( transcript: &mut impl Transcript, row_integrand_values: &[F], - t_prime: &F, + post_sumfold_claim: &F, field_cfg: &F::Config, ) -> Result, ProductionShaError> where @@ -3141,7 +3191,7 @@ where F::Modulus: Transcribable, { let claimed = folded_row_integrand_sum(row_integrand_values, field_cfg)?; - verify_folded_row_sumcheck_claim(&claimed, t_prime)?; + verify_folded_row_sumcheck_claim(&claimed, post_sumfold_claim)?; let group = build_folded_row_sumcheck_group(row_integrand_values, field_cfg)?; let (proof, _) = MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], SHA_ROW_VARS, field_cfg); @@ -3395,6 +3445,36 @@ fn build_production_sha_row_expression_sumcheck_group_with_row_weights( booleanity_sources: &[ShaBooleanitySource], field_cfg: &F::Config, ) -> Result, ProductionShaError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + let a_powers = build_sha_residual_eval_powers(a, field_cfg); + let lambda_powers = build_sha_lambda_powers(lambda, field_cfg); + let booleanity_weights = build_booleanity_weights(rho, xi, booleanity_sources.len(), field_cfg); + build_production_sha_row_expression_sumcheck_group_with_vectors( + trace, + public, + row_weights, + &a_powers, + &lambda_powers, + &booleanity_weights, + booleanity_sources, + field_cfg, + ) +} + +#[allow(clippy::too_many_arguments)] +fn build_production_sha_row_expression_sumcheck_group_with_vectors( + trace: &ProjectedTrace, + public: &ProjectedPublic, + row_weights: &[F], + a_powers: &[F], + lambda_powers: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ProductionShaError> where F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: Zero, @@ -3406,6 +3486,27 @@ where expected: SHA_ROW_COUNT, }); } + if a_powers.len() < SHA_IDEAL_EVAL_POWER_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "a powers", + got: a_powers.len(), + expected: SHA_IDEAL_EVAL_POWER_COUNT, + }); + } + if lambda_powers.len() != NUM_SHA_RESIDUAL_FAMILIES { + return Err(ProductionShaError::LengthMismatch { + label: "lambda powers", + got: lambda_powers.len(), + expected: NUM_SHA_RESIDUAL_FAMILIES, + }); + } + if booleanity_weights.len() != booleanity_sources.len() { + return Err(ProductionShaError::LengthMismatch { + label: "booleanity weights", + got: booleanity_weights.len(), + expected: booleanity_sources.len(), + }); + } let zero = F::zero_with_cfg(field_cfg); let zero_inner = zero.inner().clone(); let layout = RowExpressionLayout::new(); @@ -3473,22 +3574,9 @@ where } } - let a_powers = zinc_utils::powers( - a.clone(), - F::one_with_cfg(field_cfg), - SHA_IDEAL_EVAL_POWER_COUNT, - ); - let lambda_powers = zinc_utils::powers( - lambda.clone(), - F::one_with_cfg(field_cfg), - NUM_SHA_RESIDUAL_FAMILIES, - ); - let rho_powers = zinc_utils::powers( - rho.clone(), - F::one_with_cfg(field_cfg), - booleanity_sources.len(), - ); - let xi = xi.clone(); + let a_powers = a_powers.to_vec(); + let lambda_powers = lambda_powers.to_vec(); + let booleanity_weights = booleanity_weights.to_vec(); let booleanity_sources = booleanity_sources.to_vec(); let one = F::one_with_cfg(field_cfg); let two = one.clone() + &one; @@ -3693,7 +3781,9 @@ where let source_bit = |col: ShaWordCol, shift: usize, bit: usize| word_bits(col, shift)[bit].clone(); let mut bool_sum = zero.clone(); - for (source, rho_power) in booleanity_sources.iter().zip(rho_powers.iter()) { + for (source, booleanity_weight) in + booleanity_sources.iter().zip(booleanity_weights.iter()) + { let d = match *source { ShaBooleanitySource::WordBit { col, bit } => source_bit(col, 0, bit), ShaBooleanitySource::VirtualCh1 { bit: bit_idx } => { @@ -3715,10 +3805,10 @@ where - two.clone() * source_bit(ShaWordCol::MajComp, 0, bit_idx) } }; - bool_sum += rho_power.clone() * d.clone() * (d - one.clone()); + bool_sum += booleanity_weight.clone() * d.clone() * (d - one.clone()); } - values[0].clone() * (linear + xi.clone() * bool_sum) + values[0].clone() * (linear + bool_sum) }), )) } @@ -3734,7 +3824,7 @@ pub fn prove_expression_folded_row_sumcheck( rho: &F, xi: &F, booleanity_sources: &[ShaBooleanitySource], - t_prime: &F, + post_sumfold_claim: &F, field_cfg: &F::Config, ) -> Result, ProductionShaError> where @@ -3758,7 +3848,7 @@ where booleanity_sources, field_cfg, )?; - verify_folded_row_sumcheck_claim(&claimed, t_prime)?; + verify_folded_row_sumcheck_claim(&claimed, post_sumfold_claim)?; let group = build_production_sha_row_expression_sumcheck_group( trace, public, @@ -3786,7 +3876,7 @@ pub fn prove_expression_folded_row_sumcheck_with_output( rho: &F, xi: &F, booleanity_sources: &[ShaBooleanitySource], - t_prime: &F, + post_sumfold_claim: &F, field_cfg: &F::Config, ) -> Result<(MultiDegreeSumcheckProof, FoldedRowSumcheckOutput), ProductionShaError> where @@ -3811,7 +3901,7 @@ where rho, xi, booleanity_sources, - t_prime, + post_sumfold_claim, field_cfg, ) } @@ -3828,7 +3918,7 @@ pub fn prove_expression_folded_row_sumcheck_with_output_and_weights( rho: &F, xi: &F, booleanity_sources: &[ShaBooleanitySource], - t_prime: &F, + post_sumfold_claim: &F, field_cfg: &F::Config, ) -> Result<(MultiDegreeSumcheckProof, FoldedRowSumcheckOutput), ProductionShaError> where @@ -3841,6 +3931,9 @@ where F::Inner: Transcribable + Zero, F::Modulus: Transcribable, { + let a_powers = build_sha_residual_eval_powers(a, field_cfg); + let lambda_powers = build_sha_lambda_powers(lambda, field_cfg); + let booleanity_weights = build_booleanity_weights(rho, xi, booleanity_sources.len(), field_cfg); let claimed = expression_folded_row_sum_with_row_weights( trace, public, @@ -3852,15 +3945,51 @@ where booleanity_sources, field_cfg, )?; - verify_folded_row_sumcheck_claim(&claimed, t_prime)?; - let group = build_production_sha_row_expression_sumcheck_group_with_row_weights( + verify_folded_row_sumcheck_claim(&claimed, post_sumfold_claim)?; + prove_expression_folded_row_sumcheck_with_output_and_vectors( + transcript, trace, public, + r_ic, r_ic_eq_weights, - a, - lambda, - rho, - xi, + &a_powers, + &lambda_powers, + &booleanity_weights, + booleanity_sources, + field_cfg, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn prove_expression_folded_row_sumcheck_with_output_and_vectors( + transcript: &mut impl Transcript, + trace: &ProjectedTrace, + public: &ProjectedPublic, + r_ic: &[F; SHA_ROW_VARS], + r_ic_eq_weights: &[F], + a_powers: &[F], + lambda_powers: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result<(MultiDegreeSumcheckProof, FoldedRowSumcheckOutput), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, +{ + let group = build_production_sha_row_expression_sumcheck_group_with_vectors( + trace, + public, + r_ic_eq_weights, + a_powers, + lambda_powers, + booleanity_weights, booleanity_sources, field_cfg, )?; @@ -3876,22 +4005,26 @@ where .randomness .clone(); let r_star_eq_weights = build_eq_x_r_vec(&r_star, field_cfg)?; + let a = a_powers.get(1).ok_or(ProductionShaError::LengthMismatch { + label: "a powers", + got: a_powers.len(), + expected: 2, + })?; let endpoint_evals = build_sha_endpoint_evals_from_trace_with_row_weights( trace, &r_star_eq_weights, a, field_cfg, )?; - let terminal_value = reconstruct_folded_row_terminal_from_endpoints_with_row_weights( + let terminal_value = reconstruct_folded_row_terminal_from_endpoints_with_vectors( &endpoint_evals, public, r_ic, &r_star, &r_star_eq_weights, - a, - lambda, - rho, - xi, + a_powers, + lambda_powers, + booleanity_weights, booleanity_sources, field_cfg, )?; @@ -3908,7 +4041,7 @@ where pub fn verify_folded_row_sumcheck( transcript: &mut impl Transcript, proof: &MultiDegreeSumcheckProof, - t_prime: &F, + post_sumfold_claim: &F, field_cfg: &F::Config, ) -> Result, ProductionShaError> where @@ -3934,7 +4067,7 @@ where expected: 1, }); }; - verify_folded_row_sumcheck_claim(claimed_sum, t_prime)?; + verify_folded_row_sumcheck_claim(claimed_sum, post_sumfold_claim)?; let subclaims = MultiDegreeSumcheck::verify_as_subprotocol(transcript, SHA_ROW_VARS, proof, field_cfg)?; let r_star = subclaims.point().to_vec(); @@ -4348,8 +4481,76 @@ where expected: SHA_ROW_COUNT, }); } + let a_powers = build_sha_residual_eval_powers(a, field_cfg); + let lambda_powers = build_sha_lambda_powers(lambda, field_cfg); + let booleanity_weights = build_booleanity_weights(rho, xi, booleanity_sources.len(), field_cfg); + reconstruct_folded_row_terminal_from_endpoints_with_vectors( + endpoint_evals, + folded_public, + r_ic, + r_star, + row_weights, + &a_powers, + &lambda_powers, + &booleanity_weights, + booleanity_sources, + field_cfg, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn reconstruct_folded_row_terminal_from_endpoints_with_vectors( + endpoint_evals: &ShaEndpointEvals, + folded_public: &ProjectedPublic, + r_ic: &[F; SHA_ROW_VARS], + r_star: &[F], + row_weights: &[F], + a_powers: &[F], + lambda_powers: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result> +where + F: DelayedFieldProductSum, +{ + if r_star.len() != SHA_ROW_VARS { + return Err(ProductionShaError::LengthMismatch { + label: "r_star", + got: r_star.len(), + expected: SHA_ROW_VARS, + }); + } + if row_weights.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "row weights", + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } + if a_powers.len() < SHA_IDEAL_EVAL_POWER_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "a powers", + got: a_powers.len(), + expected: SHA_IDEAL_EVAL_POWER_COUNT, + }); + } + if lambda_powers.len() != NUM_SHA_RESIDUAL_FAMILIES { + return Err(ProductionShaError::LengthMismatch { + label: "lambda powers", + got: lambda_powers.len(), + expected: NUM_SHA_RESIDUAL_FAMILIES, + }); + } + if booleanity_weights.len() != booleanity_sources.len() { + return Err(ProductionShaError::LengthMismatch { + label: "booleanity weights", + got: booleanity_weights.len(), + expected: booleanity_sources.len(), + }); + } validate_sha_endpoint_layout(endpoint_evals)?; - verify_endpoint_scalarization(endpoint_evals, a, field_cfg)?; + verify_endpoint_scalarization_with_powers(endpoint_evals, a_powers, field_cfg)?; let residuals = residual_polys_from_endpoints_with_row_weights( endpoint_evals, @@ -4357,32 +4558,20 @@ where row_weights, field_cfg, )?; - let lambda_powers = - zinc_utils::powers(lambda.clone(), F::one_with_cfg(field_cfg), residuals.len()); - let a_powers = zinc_utils::powers( - a.clone(), - F::one_with_cfg(field_cfg), - SHA_IDEAL_EVAL_POWER_COUNT, - ); let mut linear = F::zero_with_cfg(field_cfg); for (residual, weight) in residuals.iter().zip(lambda_powers.iter()) { linear += weight.clone() * evaluate_production_sha_poly_at_powers(residual, &a_powers, field_cfg)?; } - let rho_powers = zinc_utils::powers( - rho.clone(), - F::one_with_cfg(field_cfg), - booleanity_sources.len(), - ); let mut bool_sum = F::zero_with_cfg(field_cfg); - for (source, rho_power) in booleanity_sources.iter().zip(rho_powers.iter()) { + for (source, booleanity_weight) in booleanity_sources.iter().zip(booleanity_weights.iter()) { let d = booleanity_endpoint_value(endpoint_evals, source, field_cfg)?; - bool_sum += rho_power.clone() * d.clone() * (d - F::one_with_cfg(field_cfg)); + bool_sum += booleanity_weight.clone() * d.clone() * (d - F::one_with_cfg(field_cfg)); } let row_weight = eq_eval(r_ic, r_star, F::one_with_cfg(field_cfg))?; - Ok(row_weight * (linear + xi.clone() * bool_sum)) + Ok(row_weight * (linear + bool_sum)) } pub fn verify_endpoint_scalarization( @@ -4394,10 +4583,32 @@ where F: DelayedFieldProductSum, { let powers = zinc_utils::powers(a.clone(), F::one_with_cfg(field_cfg), 32); + verify_endpoint_scalarization_with_powers(endpoint_evals, &powers, field_cfg) +} + +pub fn verify_endpoint_scalarization_with_powers( + endpoint_evals: &ShaEndpointEvals, + a_powers: &[F], + field_cfg: &F::Config, +) -> Result<(), ProductionShaError> +where + F: DelayedFieldProductSum, +{ + if a_powers.len() < SHA_WORD_BITS { + return Err(ProductionShaError::LengthMismatch { + label: "endpoint scalarization powers", + got: a_powers.len(), + expected: SHA_WORD_BITS, + }); + } for source in &endpoint_evals.sources { let recombined = zinc_utils::inner_product::FieldFieldInnerProduct::inner_product::< UNCHECKED, - >(&source.bits, &powers, F::zero_with_cfg(field_cfg)) + >( + &source.bits, + &a_powers[..SHA_WORD_BITS], + F::zero_with_cfg(field_cfg), + ) .map_err(|_| { ProductionShaError::NonCanonicalProofObject("endpoint scalarization dot product failed") })?; @@ -5978,6 +6189,196 @@ mod tests { )); } + #[test] + fn optimized_sumfold_claim_feeds_folded_row_sumcheck_with_tail_for_eight_sha_instances() { + type U = Sha256CompressionSliceUair; + + let field_cfg = cfg(); + let initial_state = SHA256_INITIAL_STATE; + let message = vec!["hello world"; 40].join(" "); + let message_blocks = sha256_padded_message_blocks::<8>(message.as_bytes()) + .expect("test message should canonically pad to eight SHA-256 blocks"); + let (witnesses, _final_state) = + synthesize_sha256_chain_witnesses::(initial_state, message_blocks) + .expect("SHA-256 UAIR witnesses synthesize"); + let shape = UairShape::::new(SHA_ROW_VARS); + + let (traces, publics): (Vec<_>, Vec<_>) = witnesses + .iter() + .map(|witness| { + let public_trace = + public_uair_trace_view::( + &witness.trace, + &shape.signature, + ) + .unwrap(); + let witness_trace = + witness_uair_trace_view::( + &witness.trace, + &shape.signature, + ) + .unwrap(); + let (trace, public, _witness_polys) = U::project_production_sha_witness( + &shape, + &public_trace, + &witness_trace, + &field_cfg, + ) + .unwrap(); + (trace, public) + }) + .unzip(); + validate_production_sha_publics(&publics, &field_cfg).unwrap(); + + let r_ic = sparse_r_ic(); + let r_ic_eq_weights = build_eq_x_r_vec(&r_ic, &field_cfg).unwrap(); + let coeff_tables = build_linear_residual_coeff_tables_with_row_weights( + &traces, + &publics, + &r_ic_eq_weights, + &field_cfg, + ) + .unwrap(); + let beta = vec![f(13), f(17), f(19)]; + let beta_eq_weights = build_eq_x_r_vec(&beta, &field_cfg).unwrap(); + let aggregate_ideal_polys = + beta_aggregate_nonzero_ideal_polys_with_weights(&coeff_tables, &beta_eq_weights) + .unwrap(); + let ideal_check = IdealCheckProof { + combined_mle_values: aggregate_ideal_polys.iter().cloned().collect(), + }; + let aggregate_ideal_polys = aggregate_sha_ideal_polys_from_proof(&ideal_check).unwrap(); + check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, &field_cfg).unwrap(); + + let a = f(5); + let lambda = f(7); + let rho = f(11); + let xi = f(13); + let booleanity_sources = production_sha_booleanity_sources(); + let a_powers = build_sha_residual_eval_powers(&a, &field_cfg); + let lambda_powers = build_sha_lambda_powers(&lambda, &field_cfg); + let booleanity_weights = + build_booleanity_weights(&rho, &xi, booleanity_sources.len(), &field_cfg); + let initial_claim = + evaluate_aggregate_sha_ideal_claim(&aggregate_ideal_polys, &a, &lambda, &field_cfg) + .unwrap(); + let linear_accumulator = build_sha_sumfold_linear_accumulator( + &coeff_tables, + &a_powers, + &lambda_powers, + &field_cfg, + ) + .unwrap(); + let prefix_vars = 2; + let quadratic_prefix_accumulator = build_sha_sumfold_quadratic_prefix_accumulator( + &traces, + &booleanity_sources, + prefix_vars, + &r_ic_eq_weights, + &booleanity_weights, + &field_cfg, + ) + .unwrap(); + assert_eq!(linear_accumulator.len(), traces.len()); + assert_eq!(quadratic_prefix_accumulator.len(), 18); + + let group = build_production_sha_sumfold_group_from_prefix_accumulators( + &traces, + &beta, + &beta_eq_weights, + &r_ic_eq_weights, + &linear_accumulator, + &quadratic_prefix_accumulator, + &booleanity_weights, + &booleanity_sources, + prefix_vars, + &field_cfg, + ) + .unwrap(); + let mut sumfold_prover_transcript = Blake3Transcript::new(); + sumfold_prover_transcript.absorb_slice(b"sha-sumfold-row-bridge"); + let (sumfold_proof, r_b) = prove_optimized_sha_sumfold_with_weights( + &mut sumfold_prover_transcript, + group, + &initial_claim, + beta.len(), + &field_cfg, + ) + .unwrap(); + + let mut sumfold_verifier_transcript = Blake3Transcript::new(); + sumfold_verifier_transcript.absorb_slice(b"sha-sumfold-row-bridge"); + let verified_sumfold = verify_full_sha_sumfold( + &mut sumfold_verifier_transcript, + &sumfold_proof, + &initial_claim, + beta.len(), + &field_cfg, + ) + .unwrap(); + assert_eq!(verified_sumfold.r_b, r_b); + + let provisional = derive_instance_fold_claim( + &beta, + r_b.clone(), + F::one_with_cfg(&field_cfg), + traces.len(), + &field_cfg, + ) + .unwrap(); + let (folded, folded_public) = + fold_projected_traces(&traces, &publics, &provisional, &field_cfg).unwrap(); + let row_claim = expression_folded_row_sum_with_vectors( + &folded.trace, + &folded_public, + &r_ic_eq_weights, + &a_powers, + &lambda_powers, + &booleanity_weights, + &booleanity_sources, + &field_cfg, + ) + .unwrap(); + let sumfold_output = derive_instance_fold_claim_from_row_claim( + &beta, + r_b, + &row_claim, + traces.len(), + &field_cfg, + ) + .unwrap(); + assert_eq!(verified_sumfold.c_sf, *sumfold_output.c_sf()); + assert_eq!(sumfold_output.final_round_sumcheck_claim(), &row_claim); + + let mut row_prover_transcript = Blake3Transcript::new(); + row_prover_transcript.absorb_slice(b"sha-folded-row-bridge"); + let (row_proof, row_output) = prove_expression_folded_row_sumcheck_with_output_and_vectors( + &mut row_prover_transcript, + &folded.trace, + &folded_public, + &r_ic, + &r_ic_eq_weights, + &a_powers, + &lambda_powers, + &booleanity_weights, + &booleanity_sources, + &field_cfg, + ) + .unwrap(); + assert_eq!(row_proof.claimed_sums(), &[row_claim.clone()]); + + let mut row_verifier_transcript = Blake3Transcript::new(); + row_verifier_transcript.absorb_slice(b"sha-folded-row-bridge"); + let verified_row = verify_folded_row_sumcheck( + &mut row_verifier_transcript, + &row_proof, + &row_claim, + &field_cfg, + ) + .unwrap(); + verify_folded_row_terminal_value(&verified_row, &row_output.terminal_value).unwrap(); + } + #[test] fn fresh_ideal_coefficients_are_bound_before_a() { let field_cfg = cfg(); @@ -6393,7 +6794,8 @@ mod tests { let row_integrand_values = (0..(1usize << SHA_ROW_VARS)) .map(|idx| f((idx as u64).wrapping_mul(5) + 9)) .collect::>(); - let t_prime = folded_row_integrand_sum(&row_integrand_values, &field_cfg).unwrap(); + let post_sumfold_claim = + folded_row_integrand_sum(&row_integrand_values, &field_cfg).unwrap(); let group_0 = build_folded_row_sumcheck_group(&row_integrand_values, &field_cfg).unwrap(); let group_1 = build_folded_row_sumcheck_group(&row_integrand_values, &field_cfg).unwrap(); @@ -6409,7 +6811,12 @@ mod tests { let mut verifier_transcript = Blake3Transcript::new(); verifier_transcript.absorb_slice(b"folded-row-context"); assert!(matches!( - verify_folded_row_sumcheck(&mut verifier_transcript, &proof, &t_prime, &field_cfg), + verify_folded_row_sumcheck( + &mut verifier_transcript, + &proof, + &post_sumfold_claim, + &field_cfg + ), Err(ProductionShaError::UnexpectedSumcheckGroupCount { label: "folded row sumcheck", got: 2 @@ -6434,7 +6841,7 @@ mod tests { }, ShaBooleanitySource::VirtualCh1 { bit: 0 }, ]; - let t_prime = expression_folded_row_sum( + let post_sumfold_claim = expression_folded_row_sum( &trace, &public, &r_ic, @@ -6459,16 +6866,20 @@ mod tests { &rho, &xi, &booleanity_sources, - &t_prime, + &post_sumfold_claim, &field_cfg, ) .unwrap(); let mut verifier_transcript = Blake3Transcript::new(); verifier_transcript.absorb_slice(b"expression-row-context"); - let output = - verify_folded_row_sumcheck(&mut verifier_transcript, &proof, &t_prime, &field_cfg) - .unwrap(); + let output = verify_folded_row_sumcheck( + &mut verifier_transcript, + &proof, + &post_sumfold_claim, + &field_cfg, + ) + .unwrap(); let endpoint_evals = build_sha_endpoint_evals_from_trace(&trace, &output.r_star, &a, &field_cfg).unwrap(); let terminal = reconstruct_folded_row_terminal_from_endpoints( From 98c4d5c532e768ef56a561a5361f7f13584c9e8e Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 18:39:55 -0700 Subject: [PATCH 25/49] Add production SHA timing APIs --- protocol/src/production_sha.rs | 537 +++++++++++++++++++++++++++++++-- 1 file changed, 516 insertions(+), 21 deletions(-) diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index af0ffce4..618a4431 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -4,7 +4,7 @@ //! `Proof`: production ProjectionFold has a different transcript order and //! derives folded commitments only after SumFold fixes the instance-axis point. -use std::{borrow::Cow, io::Cursor, marker::PhantomData}; +use std::{borrow::Cow, io::Cursor, marker::PhantomData, time::Duration}; use crate::{ ZincTypes, @@ -214,6 +214,158 @@ pub struct ProductionShaChallenges { pub beta: Vec, } +/// Heavy-region wall-time breakdown for a production SHA prover run. +/// +/// These regions intentionally omit cheap challenge/equality-vector setup and +/// final struct assembly, so [`Self::total`] is the sum of measured heavy +/// regions rather than exact end-to-end wall time. +#[derive(Default, Debug, Clone, Copy)] +pub struct ProductionShaProveTimings { + pub witness_generation: Duration, + pub commitment: Duration, + pub residual_coeff_tables: Duration, + pub aggregate_ideal: Duration, + pub sumfold_linear_accumulator: Duration, + pub sumfold_quadratic_prefix_accumulator: Duration, + pub sumfold_group_build: Duration, + pub sumfold_prove: Duration, + pub fold_projected_traces: Duration, + pub row_claim_derivation: Duration, + pub fold_pcs_commitments: Duration, + pub fold_pcs_prover_data: Duration, + pub row_sumcheck: Duration, + pub endpoint_resolver: Duration, + pub multipoint_eval: Duration, + pub lifted_evals: Duration, + pub pcs_opening: Duration, +} + +impl ProductionShaProveTimings { + pub fn add_assign(&mut self, other: &Self) { + self.witness_generation += other.witness_generation; + self.commitment += other.commitment; + self.residual_coeff_tables += other.residual_coeff_tables; + self.aggregate_ideal += other.aggregate_ideal; + self.sumfold_linear_accumulator += other.sumfold_linear_accumulator; + self.sumfold_quadratic_prefix_accumulator += other.sumfold_quadratic_prefix_accumulator; + self.sumfold_group_build += other.sumfold_group_build; + self.sumfold_prove += other.sumfold_prove; + self.fold_projected_traces += other.fold_projected_traces; + self.row_claim_derivation += other.row_claim_derivation; + self.fold_pcs_commitments += other.fold_pcs_commitments; + self.fold_pcs_prover_data += other.fold_pcs_prover_data; + self.row_sumcheck += other.row_sumcheck; + self.endpoint_resolver += other.endpoint_resolver; + self.multipoint_eval += other.multipoint_eval; + self.lifted_evals += other.lifted_evals; + self.pcs_opening += other.pcs_opening; + } + + pub fn divide_by(&mut self, n: u32) { + self.witness_generation /= n; + self.commitment /= n; + self.residual_coeff_tables /= n; + self.aggregate_ideal /= n; + self.sumfold_linear_accumulator /= n; + self.sumfold_quadratic_prefix_accumulator /= n; + self.sumfold_group_build /= n; + self.sumfold_prove /= n; + self.fold_projected_traces /= n; + self.row_claim_derivation /= n; + self.fold_pcs_commitments /= n; + self.fold_pcs_prover_data /= n; + self.row_sumcheck /= n; + self.endpoint_resolver /= n; + self.multipoint_eval /= n; + self.lifted_evals /= n; + self.pcs_opening /= n; + } + + pub fn total(&self) -> Duration { + self.witness_generation + + self.commitment + + self.residual_coeff_tables + + self.aggregate_ideal + + self.sumfold_linear_accumulator + + self.sumfold_quadratic_prefix_accumulator + + self.sumfold_group_build + + self.sumfold_prove + + self.fold_projected_traces + + self.row_claim_derivation + + self.fold_pcs_commitments + + self.fold_pcs_prover_data + + self.row_sumcheck + + self.endpoint_resolver + + self.multipoint_eval + + self.lifted_evals + + self.pcs_opening + } +} + +/// Heavy-region wall-time breakdown for a production SHA verifier run. +/// +/// These regions intentionally omit cheap challenge/equality-vector setup, so +/// [`Self::total`] is the sum of measured heavy regions rather than exact +/// end-to-end wall time. +#[derive(Default, Debug, Clone, Copy)] +pub struct ProductionShaVerifyTimings { + pub public_projection: Duration, + pub aggregate_ideal_verify: Duration, + pub sumfold_verify: Duration, + pub fold_pcs_commitments: Duration, + pub row_sumcheck_verify: Duration, + pub fold_projected_publics: Duration, + pub resolver_terminal_verify: Duration, + pub multipoint_verify: Duration, + pub multipoint_open_eval_checks: Duration, + pub lifted_eval_absorb: Duration, + pub pcs_verify: Duration, +} + +impl ProductionShaVerifyTimings { + pub fn add_assign(&mut self, other: &Self) { + self.public_projection += other.public_projection; + self.aggregate_ideal_verify += other.aggregate_ideal_verify; + self.sumfold_verify += other.sumfold_verify; + self.fold_pcs_commitments += other.fold_pcs_commitments; + self.row_sumcheck_verify += other.row_sumcheck_verify; + self.fold_projected_publics += other.fold_projected_publics; + self.resolver_terminal_verify += other.resolver_terminal_verify; + self.multipoint_verify += other.multipoint_verify; + self.multipoint_open_eval_checks += other.multipoint_open_eval_checks; + self.lifted_eval_absorb += other.lifted_eval_absorb; + self.pcs_verify += other.pcs_verify; + } + + pub fn divide_by(&mut self, n: u32) { + self.public_projection /= n; + self.aggregate_ideal_verify /= n; + self.sumfold_verify /= n; + self.fold_pcs_commitments /= n; + self.row_sumcheck_verify /= n; + self.fold_projected_publics /= n; + self.resolver_terminal_verify /= n; + self.multipoint_verify /= n; + self.multipoint_open_eval_checks /= n; + self.lifted_eval_absorb /= n; + self.pcs_verify /= n; + } + + pub fn total(&self) -> Duration { + self.public_projection + + self.aggregate_ideal_verify + + self.sumfold_verify + + self.fold_pcs_commitments + + self.row_sumcheck_verify + + self.fold_projected_publics + + self.resolver_terminal_verify + + self.multipoint_verify + + self.multipoint_open_eval_checks + + self.lifted_eval_absorb + + self.pcs_verify + } +} + const SHA256_ROUND_CONSTANTS: [u32; 64] = [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, @@ -1030,6 +1182,83 @@ pub fn prove_linear_ideal_fold( >, LinearIdealFoldError, > +where + U: Uair + ProductionShaProjectionAdapter, + Zt: ZincTypes, + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, + P: ZincPCSTypes, + P: ProductionShaFoldedPcsOpen, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + prove_linear_ideal_fold_inner(pp, shape, witnesses, transcript, None) +} + +#[allow(clippy::too_many_arguments)] +pub fn prove_linear_ideal_fold_with_timings( + pp: &LinearIdealFoldProverParams, + shape: &UairShape, + witnesses: &[UairWitness<'_, Zt::Int, Zt::Int, D>], + transcript: &mut impl Transcript, +) -> Result< + ( + LinearIdealFoldProveOutput< + UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, + FoldedLinearIdealInstance, ProjectedPublic>, + FoldedLinearIdealWitness>, + ProductionLinearIdealFoldProof, + >, + ProductionShaProveTimings, + ), + LinearIdealFoldError, +> +where + U: Uair + ProductionShaProjectionAdapter, + Zt: ZincTypes, + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, + P: ZincPCSTypes, + P: ProductionShaFoldedPcsOpen, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + let mut timings = ProductionShaProveTimings::default(); + let output = + prove_linear_ideal_fold_inner(pp, shape, witnesses, transcript, Some(&mut timings))?; + Ok((output, timings)) +} + +#[allow(clippy::too_many_arguments)] +fn prove_linear_ideal_fold_inner( + pp: &LinearIdealFoldProverParams, + shape: &UairShape, + witnesses: &[UairWitness<'_, Zt::Int, Zt::Int, D>], + transcript: &mut impl Transcript, + mut timings: Option<&mut ProductionShaProveTimings>, +) -> Result< + LinearIdealFoldProveOutput< + UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, + FoldedLinearIdealInstance, ProjectedPublic>, + FoldedLinearIdealWitness>, + ProductionLinearIdealFoldProof, + >, + LinearIdealFoldError, +> where U: Uair + ProductionShaProjectionAdapter, Zt: ZincTypes, @@ -1062,25 +1291,38 @@ where absorb_production_sha_statement_metadata(transcript); absorb_uair_shape_metadata(transcript, shape); - let instance_artifacts = witnesses - .iter() - .enumerate() - .map(|(instance_idx, witness)| { - let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; - let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; - absorb_public_uair_trace::(transcript, instance_idx, &public_trace); - let (trace, public, witness_polys) = - U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg)?; - let (data, commitment) = - commit_production_sha_instance::(&pp.pcs_params, &witness_polys)?; - let fresh_instance = UairInstance { - public_trace: own_uair_trace(&public_trace), - commitments: commitment.clone(), - }; + let mut instance_artifacts = Vec::with_capacity(witnesses.len()); + for (instance_idx, witness) in witnesses.iter().enumerate() { + let witness_start = std::time::Instant::now(); + let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; + let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; + if let Some(t) = timings.as_mut() { + t.witness_generation += witness_start.elapsed(); + } - Ok::<_, ProductionShaError>((fresh_instance, commitment, data, trace, public)) - }) - .collect::, _>>()?; + absorb_public_uair_trace::(transcript, instance_idx, &public_trace); + + let witness_start = std::time::Instant::now(); + let (trace, public, witness_polys) = + U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg)?; + if let Some(t) = timings.as_mut() { + t.witness_generation += witness_start.elapsed(); + } + + let commitment_start = std::time::Instant::now(); + let (data, commitment) = + commit_production_sha_instance::(&pp.pcs_params, &witness_polys)?; + if let Some(t) = timings.as_mut() { + t.commitment += commitment_start.elapsed(); + } + + let fresh_instance = UairInstance { + public_trace: own_uair_trace(&public_trace), + commitments: commitment.clone(), + }; + + instance_artifacts.push((fresh_instance, commitment, data, trace, public)); + } let (fresh_instances, instance_artifacts): (Vec<_>, Vec<_>) = instance_artifacts .into_iter() @@ -1108,14 +1350,20 @@ where let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); let r_ic_eq_weights = build_eq_x_r_vec(&r_ic, field_cfg)?; + let residual_start = std::time::Instant::now(); let coeff_tables = build_linear_residual_coeff_tables_with_row_weights( &traces, &publics, &r_ic_eq_weights, field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.residual_coeff_tables = residual_start.elapsed(); + } + let beta = sample_instance_batch_challenge(transcript, witnesses.len(), field_cfg)?; let beta_eq_weights = build_eq_x_r_vec(&beta, field_cfg)?; + let aggregate_start = std::time::Instant::now(); let aggregate_ideal_polys = beta_aggregate_nonzero_ideal_polys_with_weights(&coeff_tables, &beta_eq_weights)?; let ideal_check = IdealCheckProof { @@ -1124,6 +1372,9 @@ where let aggregate_ideal_polys = aggregate_sha_ideal_polys_from_proof(&ideal_check)?; check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, field_cfg)?; absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys, field_cfg); + if let Some(t) = timings.as_mut() { + t.aggregate_ideal = aggregate_start.elapsed(); + } let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); let a_powers = build_sha_residual_eval_powers(&a, field_cfg); @@ -1132,8 +1383,15 @@ where build_booleanity_weights(&rho, &xi, booleanity_sources.len(), field_cfg); let initial_claim = evaluate_aggregate_sha_ideal_claim(&aggregate_ideal_polys, &a, &lambda, field_cfg)?; + + let linear_accumulator_start = std::time::Instant::now(); let linear_accumulator = build_sha_sumfold_linear_accumulator(&coeff_tables, &a_powers, &lambda_powers, field_cfg)?; + if let Some(t) = timings.as_mut() { + t.sumfold_linear_accumulator = linear_accumulator_start.elapsed(); + } + + let quadratic_accumulator_start = std::time::Instant::now(); let quadratic_prefix_accumulator = build_sha_sumfold_quadratic_prefix_accumulator( &traces, &booleanity_sources, @@ -1142,6 +1400,11 @@ where &booleanity_weights, field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.sumfold_quadratic_prefix_accumulator = quadratic_accumulator_start.elapsed(); + } + + let sumfold_group_start = std::time::Instant::now(); let sumfold_group = build_production_sha_sumfold_group_from_prefix_accumulators( &traces, &beta, @@ -1154,6 +1417,11 @@ where pp.prefix_vars, field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.sumfold_group_build = sumfold_group_start.elapsed(); + } + + let sumfold_prove_start = std::time::Instant::now(); let (sumfold_proof, sumfold_r_b) = prove_optimized_sha_sumfold_with_weights( transcript, sumfold_group, @@ -1161,6 +1429,10 @@ where beta.len(), field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.sumfold_prove = sumfold_prove_start.elapsed(); + } + let provisional_sumfold_output = derive_instance_fold_claim( &beta, sumfold_r_b.clone(), @@ -1168,8 +1440,15 @@ where witnesses.len(), field_cfg, )?; + + let fold_traces_start = std::time::Instant::now(); let (folded, folded_public) = fold_projected_traces(&traces, &publics, &provisional_sumfold_output, field_cfg)?; + if let Some(t) = timings.as_mut() { + t.fold_projected_traces = fold_traces_start.elapsed(); + } + + let row_claim_start = std::time::Instant::now(); let row_claim = expression_folded_row_sum_with_vectors( &folded.trace, &folded_public, @@ -1187,17 +1466,29 @@ where witnesses.len(), field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.row_claim_derivation = row_claim_start.elapsed(); + } + let fold_commitments_start = std::time::Instant::now(); let folded_commitments = fold_pcs_commitments::( &instance_commitments, sumfold_output.eq_instance_weights(), field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.fold_pcs_commitments = fold_commitments_start.elapsed(); + } + + let fold_prover_data_start = std::time::Instant::now(); let folded_prover_data = fold_pcs_prover_data::( &instance_prover_data, sumfold_output.eq_instance_weights(), field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.fold_pcs_prover_data = fold_prover_data_start.elapsed(); + } absorb_production_sha_commitments::( transcript, b"production_sha_derived_folded_commitments", @@ -1205,6 +1496,7 @@ where ); verify_folded_row_sumcheck_claim(&row_claim, sumfold_output.final_round_sumcheck_claim())?; + let row_sumcheck_start = std::time::Instant::now(); let (combined_sumcheck, row_output) = prove_expression_folded_row_sumcheck_with_output_and_vectors( transcript, @@ -1219,6 +1511,11 @@ where field_cfg, )?; verify_folded_row_sumcheck_claim(&combined_sumcheck.claimed_sums()[0], &row_claim)?; + if let Some(t) = timings.as_mut() { + t.row_sumcheck = row_sumcheck_start.elapsed(); + } + + let endpoint_resolver_start = std::time::Instant::now(); let endpoint_evals = build_sha_endpoint_evals_from_trace_with_row_weights( &folded.trace, &row_output.r_star_eq_weights, @@ -1241,7 +1538,11 @@ where field_cfg, )?; verify_folded_row_terminal_value(&row_output, &terminal)?; + if let Some(t) = timings.as_mut() { + t.endpoint_resolver = endpoint_resolver_start.elapsed(); + } + let multipoint_start = std::time::Instant::now(); let (multipoint_eval, r_0) = prove_sha_endpoint_multipoint_with_row_weights( transcript, &folded.trace, @@ -1251,13 +1552,23 @@ where &row_output.r_star_eq_weights, field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.multipoint_eval = multipoint_start.elapsed(); + } + let r_0_eq_weights = build_eq_x_r_vec(&r_0, field_cfg)?; + let lifted_evals_start = std::time::Instant::now(); let witness_lifted_evals = build_folded_sha_pcs_lifted_evals_with_row_weights( &folded.trace, &r_0_eq_weights, field_cfg, )?; absorb_folded_lifted_evals(transcript, &witness_lifted_evals, field_cfg); + if let Some(t) = timings.as_mut() { + t.lifted_evals = lifted_evals_start.elapsed(); + } + + let pcs_opening_start = std::time::Instant::now(); let opening_proof = P::prove_folded_pcs_opening( &pp.pcs_params, &folded_commitments, @@ -1267,6 +1578,9 @@ where &witness_lifted_evals, field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.pcs_opening = pcs_opening_start.elapsed(); + } Ok(LinearIdealFoldProveOutput { fresh_instances, @@ -1471,6 +1785,69 @@ pub fn verify_linear_ideal_fold( FoldedLinearIdealInstance, ProjectedPublic>, LinearIdealFoldError, > +where + U: Uair + ProductionShaProjectionAdapter, + Zt: ZincTypes, + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, + P: ZincPCSTypes, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + verify_linear_ideal_fold_inner(vs, instances, proof, transcript, None) +} + +pub fn verify_linear_ideal_fold_with_timings( + vs: &VerifiedLinearIdealFoldSetup, + instances: &[UairInstance<'_, Zt::Int, Zt::Int, PCSCommitments, D>], + proof: &ProductionLinearIdealFoldProof, + transcript: &mut impl Transcript, +) -> Result< + ( + FoldedLinearIdealInstance, ProjectedPublic>, + ProductionShaVerifyTimings, + ), + LinearIdealFoldError, +> +where + U: Uair + ProductionShaProjectionAdapter, + Zt: ZincTypes, + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, + P: ZincPCSTypes, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + let mut timings = ProductionShaVerifyTimings::default(); + let folded_instance = + verify_linear_ideal_fold_inner(vs, instances, proof, transcript, Some(&mut timings))?; + Ok((folded_instance, timings)) +} + +fn verify_linear_ideal_fold_inner( + vs: &VerifiedLinearIdealFoldSetup, + instances: &[UairInstance<'_, Zt::Int, Zt::Int, PCSCommitments, D>], + proof: &ProductionLinearIdealFoldProof, + transcript: &mut impl Transcript, + mut timings: Option<&mut ProductionShaVerifyTimings>, +) -> Result< + FoldedLinearIdealInstance, ProjectedPublic>, + LinearIdealFoldError, +> where U: Uair + ProductionShaProjectionAdapter, Zt: ZincTypes, @@ -1508,6 +1885,7 @@ where absorb_production_sha_statement_metadata(transcript); absorb_uair_shape_metadata(transcript, &vs.shape); + let public_projection_start = std::time::Instant::now(); let mut publics = Vec::with_capacity(instances.len()); for (instance_idx, instance) in instances.iter().enumerate() { validate_public_uair_trace_shape::( @@ -1531,17 +1909,22 @@ where } validate_production_sha_publics(&publics, field_cfg)?; - let booleanity_sources = production_sha_booleanity_sources(); - absorb_production_sha_commitments::( transcript, b"production_sha_fresh_commitments", &proof.instance_commitments, ); absorb_projected_sha_publics(transcript, &publics, field_cfg); + if let Some(t) = timings.as_mut() { + t.public_projection = public_projection_start.elapsed(); + } + + let booleanity_sources = production_sha_booleanity_sources(); let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); let beta = sample_instance_batch_challenge(transcript, instances.len(), field_cfg)?; + + let aggregate_ideal_start = std::time::Instant::now(); let aggregate_ideal_polys = aggregate_sha_ideal_polys_from_proof(&proof.ideal_check)?; check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, field_cfg)?; absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys, field_cfg); @@ -1549,7 +1932,11 @@ where let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); let initial_claim = evaluate_aggregate_sha_ideal_claim(&aggregate_ideal_polys, &a, &lambda, field_cfg)?; + if let Some(t) = timings.as_mut() { + t.aggregate_ideal_verify = aggregate_ideal_start.elapsed(); + } + let sumfold_verify_start = std::time::Instant::now(); let verified_sumfold = verify_full_sha_sumfold( transcript, &proof.sumfold_proof, @@ -1564,25 +1951,44 @@ where instances.len(), field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.sumfold_verify = sumfold_verify_start.elapsed(); + } + + let fold_commitments_start = std::time::Instant::now(); let folded_commitments = fold_pcs_commitments::( &proof.instance_commitments, sumfold_output.eq_instance_weights(), field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.fold_pcs_commitments = fold_commitments_start.elapsed(); + } absorb_production_sha_commitments::( transcript, b"production_sha_derived_folded_commitments", std::slice::from_ref(&folded_commitments), ); + let row_sumcheck_start = std::time::Instant::now(); let row_output = verify_folded_row_sumcheck( transcript, &proof.combined_sumcheck, sumfold_output.final_round_sumcheck_claim(), field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.row_sumcheck_verify = row_sumcheck_start.elapsed(); + } + + let fold_publics_start = std::time::Instant::now(); let folded_public = fold_projected_publics(&publics, sumfold_output.eq_instance_weights(), field_cfg)?; + if let Some(t) = timings.as_mut() { + t.fold_projected_publics = fold_publics_start.elapsed(); + } + + let resolver_terminal_start = std::time::Instant::now(); absorb_sha_resolver_proof(transcript, &proof.resolver, field_cfg); let endpoint_evals = sha_endpoint_evals_from_resolver(&proof.resolver, &a, field_cfg)?; let terminal = reconstruct_folded_row_terminal_from_endpoints( @@ -1598,7 +2004,11 @@ where field_cfg, )?; verify_folded_row_terminal_value(&row_output, &terminal)?; + if let Some(t) = timings.as_mut() { + t.resolver_terminal_verify = resolver_terminal_start.elapsed(); + } + let multipoint_start = std::time::Instant::now(); let (subclaim, shift_specs) = verify_sha_endpoint_multipoint( transcript, &proof.multipoint_eval, @@ -1607,6 +2017,11 @@ where &row_output.r_star, field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.multipoint_verify = multipoint_start.elapsed(); + } + + let open_eval_start = std::time::Instant::now(); let open_evals = multipoint_open_evals_from_pcs_lifted( &proof.witness_lifted_evals, &production_sha_multipoint_layout(), @@ -1615,7 +2030,17 @@ where field_cfg, )?; verify_sha_endpoint_multipoint_open_evals(&subclaim, &open_evals, &shift_specs, field_cfg)?; + if let Some(t) = timings.as_mut() { + t.multipoint_open_eval_checks = open_eval_start.elapsed(); + } + + let lifted_absorb_start = std::time::Instant::now(); absorb_folded_lifted_evals(transcript, &proof.witness_lifted_evals, field_cfg); + if let Some(t) = timings.as_mut() { + t.lifted_eval_absorb = lifted_absorb_start.elapsed(); + } + + let pcs_verify_start = std::time::Instant::now(); verify_production_sha_pcs_opening::( &vs.pcs_params, &folded_commitments, @@ -1624,6 +2049,9 @@ where &proof.opening_proof, field_cfg, )?; + if let Some(t) = timings.as_mut() { + t.pcs_verify = pcs_verify_start.elapsed(); + } Ok(FoldedLinearIdealInstance { target: sumfold_output.final_round_sumcheck_claim().clone(), @@ -6189,6 +6617,73 @@ mod tests { )); } + #[test] + fn linear_ideal_fold_timing_apis_smoke_test_with_hyrax() { + type C = ark_bn254::G1Affine; + type P = AllHyraxPCSTypes; + type U = Sha256CompressionSliceUair; + + let field_cfg = fixed_prime::field_cfg_from_curve_scalar::, C>(); + let initial_state = SHA256_INITIAL_STATE; + let message = vec!["hello world"; 40].join(" "); + let message_blocks = sha256_padded_message_blocks::<8>(message.as_bytes()) + .expect("test message should canonically pad to 8 SHA-256 blocks"); + let (witnesses, _final_state) = + synthesize_sha256_chain_witnesses::(initial_state, message_blocks) + .expect("SHA-256 UAIR witnesses synthesize"); + let shape = UairShape::::new(SHA_ROW_VARS); + let (pcs_params, pcs_verifier_params) = all_hyrax_test_pcs_params::(); + let pp = + LinearIdealFoldProverParams::::new( + pcs_params, + field_cfg.clone(), + 3, + ); + let vs = setup_verify_linear_ideal_fold::( + LinearIdealFoldVerifierParams::new(pcs_verifier_params, field_cfg), + shape.clone(), + ) + .expect("production SHA verifier setup succeeds"); + + let mut prover_transcript = Blake3Transcript::new(); + let (output, prove_timings) = prove_linear_ideal_fold_with_timings::< + P, + U, + TestShaZincTypes, + F, + TEST_DEGREE_PLUS_ONE, + >(&pp, &shape, &witnesses, &mut prover_transcript) + .expect("production SHA timed ProjectionFold proof succeeds"); + + let mut verifier_transcript = Blake3Transcript::new(); + let (verified, verify_timings) = verify_linear_ideal_fold_with_timings::< + P, + U, + TestShaZincTypes, + F, + TEST_DEGREE_PLUS_ONE, + >( + &vs, + &output.fresh_instances, + &output.proof, + &mut verifier_transcript, + ) + .expect("production SHA timed ProjectionFold proof verifies"); + + assert!(prove_timings.total() > Duration::ZERO); + assert!(verify_timings.total() > Duration::ZERO); + assert_eq!(verified.target, output.folded_instance.target); + assert_eq!(verified.public, output.folded_instance.public); + assert!(pcs_commitments_match::< + P, + TestShaZincTypes, + F, + TEST_DEGREE_PLUS_ONE, + >( + &verified.commitments, &output.folded_instance.commitments + )); + } + #[test] fn optimized_sumfold_claim_feeds_folded_row_sumcheck_with_tail_for_eight_sha_instances() { type U = Sha256CompressionSliceUair; From dcfe5515abd6bf400a871f9059ffe67157f8303c Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 20:47:16 -0700 Subject: [PATCH 26/49] Use delayed products for SHA aggregations --- piop/src/neutron_nova/projection_sha.rs | 101 ++++++--- protocol/src/production_sha.rs | 287 ++++++++++++++++++------ 2 files changed, 286 insertions(+), 102 deletions(-) diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 455f37a3..91a1d301 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -325,7 +325,7 @@ pub struct LinearResidualCoeffTable { impl LinearResidualCoeffTable where - F: PrimeField, + F: DelayedFieldProductSum, { pub fn coeffs_for_family(&self, family: ShaResidualFamily) -> Option<&DynamicPolynomialF> { self.coeffs.get(family.index()) @@ -338,7 +338,7 @@ pub fn beta_aggregate_nonzero_ideal_polys( field_cfg: &F::Config, ) -> Result<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], ShaProjectionError> where - F: PrimeField, + F: DelayedFieldProductSum, { let weights = build_eq_x_r_vec(beta, field_cfg)?; beta_aggregate_nonzero_ideal_polys_with_weights(tables, &weights) @@ -415,6 +415,23 @@ where .collect() } +fn selected_nonzero_sha_lambda_powers( + lambda_powers: &[F], +) -> Result<[F; NUM_NONZERO_SHA_FAMILIES], ShaProjectionError> +where + F: PrimeField, +{ + if lambda_powers.len() != NUM_SHA_RESIDUAL_FAMILIES { + return Err(ShaProjectionError::MissingColumn { + kind: "lambda_powers", + col: lambda_powers.len(), + }); + } + Ok(std::array::from_fn(|slot| { + lambda_powers[NONZERO_SHA_FAMILIES[slot].index()].clone() + })) +} + pub fn build_sha_sumfold_linear_accumulator( tables: &[LinearResidualCoeffTable], a_powers: &[F], @@ -445,12 +462,17 @@ where col: table.coeffs.len(), }); } - let mut target = F::zero_with_cfg(field_cfg); + let mut values: [F; NUM_SHA_RESIDUAL_FAMILIES] = + std::array::from_fn(|_| F::zero_with_cfg(field_cfg)); for (family_idx, residual) in table.coeffs.iter().enumerate() { - target += lambda_powers[family_idx].clone() - * evaluate_poly_at_powers_dmr(residual, &a_powers, field_cfg)?; + values[family_idx] = evaluate_poly_at_powers_dmr(residual, &a_powers, field_cfg)?; } - Ok(target) + FieldFieldInnerProduct::inner_product::( + &values, + lambda_powers, + F::zero_with_cfg(field_cfg), + ) + .map_err(ShaProjectionError::from) }) .collect() } @@ -872,6 +894,7 @@ where F::one_with_cfg(field_cfg), SHA_RESIDUAL_EVAL_POWER_COUNT, ); + let nonzero_lambda_powers = selected_nonzero_sha_lambda_powers(&lambda_powers)?; cache.taus_at_a.clear(); cache.fresh_targets.clear(); @@ -884,10 +907,12 @@ where let taus: [F; NUM_NONZERO_SHA_FAMILIES] = tau_values .try_into() .unwrap_or_else(|_| unreachable!("exactly seven SHA ideal values were evaluated")); - let mut target = zero.clone(); - for (slot, family) in NONZERO_SHA_FAMILIES.iter().enumerate() { - target += lambda_powers[family.index()].clone() * &taus[slot]; - } + let target = FieldFieldInnerProduct::inner_product::( + &nonzero_lambda_powers, + &taus, + zero.clone(), + ) + .map_err(ShaProjectionError::from)?; cache.taus_at_a.push(taus); cache.fresh_targets.push(target); } @@ -1328,7 +1353,7 @@ where field_cfg, )?; - let mut bool_sum = F::zero_with_cfg(field_cfg); + let mut bool_terms = Vec::with_capacity(booleanity_sources.len()); let virtuals = if needs_virtuals { Some(reconstruct_virtual_ch_maj_at_row_unchecked( trace, row, field_cfg, @@ -1336,7 +1361,7 @@ where } else { None }; - for (idx, source) in booleanity_sources.iter().enumerate() { + for source in booleanity_sources { let d = booleanity_source_value_at_row_with_virtuals( trace, row, @@ -1345,8 +1370,14 @@ where field_cfg, )?; let term = d.clone() * (d - F::one_with_cfg(field_cfg)); - bool_sum += booleanity_weights[idx].clone() * term; + bool_terms.push(term); } + let bool_sum = FieldFieldInnerProduct::inner_product::( + booleanity_weights, + &bool_terms, + F::zero_with_cfg(field_cfg), + ) + .map_err(ShaProjectionError::from)?; out.push(row_weights[row].clone() * (linear + bool_sum)); } Ok(out) @@ -1504,19 +1535,23 @@ fn sha_linear_residual_sum_with_weights( where F: DelayedFieldProductSum, { - let mut sum = F::zero_with_cfg(field_cfg); - for (row, row_weight) in row_weights.iter().enumerate() { - sum += row_weight.clone() - * sha_linear_residual_row_value_with_powers( - trace, - public, - row, - a_powers, - lambda_powers, - field_cfg, - )?; + let mut values = Vec::with_capacity(row_weights.len()); + for row in 0..row_weights.len() { + values.push(sha_linear_residual_row_value_with_powers( + trace, + public, + row, + a_powers, + lambda_powers, + field_cfg, + )?); } - Ok(sum) + FieldFieldInnerProduct::inner_product::( + row_weights, + &values, + F::zero_with_cfg(field_cfg), + ) + .map_err(ShaProjectionError::from) } fn sha_linear_residual_row_value_with_powers( @@ -2912,13 +2947,14 @@ fn bind_sha_booleanity_sources_to_prefix( field_cfg: &F::Config, ) -> Result>, ShaProjectionError> where - F: PrimeField, + F: DelayedFieldProductSum, { let prefix_len = binary_len(prefix_vars); let source_count = booleanity_sources.len(); let needs_virtuals = sources_need_virtuals(booleanity_sources); let mut source_values = vec![F::zero_with_cfg(field_cfg); prefix_len * source_count]; let mut out = vec![vec![F::zero_with_cfg(field_cfg); tail_len]; source_count * SHA_ROW_COUNT]; + let mut source_column_values = Vec::with_capacity(prefix_len); for tail in 0..tail_len { for row in 0..SHA_ROW_COUNT { @@ -2933,10 +2969,17 @@ where field_cfg, )?; for source_idx in 0..source_count { - let mut acc = F::zero_with_cfg(field_cfg); - for (prefix, weight) in prefix_weights.iter().enumerate().take(prefix_len) { - acc += weight.clone() * &source_values[prefix * source_count + source_idx]; + source_column_values.clear(); + for prefix in 0..prefix_len { + source_column_values + .push(source_values[prefix * source_count + source_idx].clone()); } + let acc = FieldFieldInnerProduct::inner_product::( + prefix_weights, + &source_column_values, + F::zero_with_cfg(field_cfg), + ) + .map_err(ShaProjectionError::from)?; out[source_idx * SHA_ROW_COUNT + row][tail] = acc; } } diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 618a4431..89229c94 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -62,8 +62,8 @@ use zinc_transcript::Blake3Transcript; use zinc_transcript::traits::{Transcribable, Transcript}; use zinc_uair::{ShiftSpec, Uair, UairSignature, UairTrace, UairWitness}; use zinc_utils::{ - UNCHECKED, delayed_reduction::DelayedFieldProductSum, inner_product::InnerProduct, - inner_transparent_field::InnerTransparentField, + UNCHECKED, delayed_reduction::DelayedFieldProductSum, inner_product::FieldFieldInnerProduct, + inner_product::InnerProduct, inner_transparent_field::InnerTransparentField, }; use zip_plus::{ ZipError, @@ -1074,7 +1074,7 @@ pub fn check_fresh_sha_ideal_membership( field_cfg: &F::Config, ) -> Result<(), ProductionShaError> where - F: PrimeField, + F: DelayedFieldProductSum, { verify_fresh_sha_ideal_polys(ideal_polys, field_cfg)?; Ok(()) @@ -1381,8 +1381,12 @@ where let lambda_powers = build_sha_lambda_powers(&lambda, field_cfg); let booleanity_weights = build_booleanity_weights(&rho, &xi, booleanity_sources.len(), field_cfg); - let initial_claim = - evaluate_aggregate_sha_ideal_claim(&aggregate_ideal_polys, &a, &lambda, field_cfg)?; + let initial_claim = evaluate_aggregate_sha_ideal_claim_with_powers( + &aggregate_ideal_polys, + &a_powers, + &lambda_powers, + field_cfg, + )?; let linear_accumulator_start = std::time::Instant::now(); let linear_accumulator = @@ -2504,19 +2508,25 @@ where F::one_with_cfg(field_cfg), SHA_IDEAL_EVAL_POWER_COUNT, ); + let nonzero_lambda_powers = selected_nonzero_sha_lambda_powers(&lambda_powers)?; ideal_polys .iter() .map(|instance| { - let mut target = F::zero_with_cfg(field_cfg); - for (slot, family) in production_sha_nonzero_families().iter().enumerate() { - target += lambda_powers[family.index()].clone() - * evaluate_production_sha_poly_at_powers( - &instance[slot], - &a_powers, - field_cfg, - )?; + let mut values: [F; NUM_NONZERO_SHA_FAMILIES] = + std::array::from_fn(|_| F::zero_with_cfg(field_cfg)); + for (slot, poly) in instance.iter().enumerate() { + values[slot] = evaluate_production_sha_poly_at_powers(poly, &a_powers, field_cfg)?; } - Ok(target) + FieldFieldInnerProduct::inner_product::( + &values, + &nonzero_lambda_powers, + F::zero_with_cfg(field_cfg), + ) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject( + "production SHA nonzero-family dot product failed", + ) + }) }) .collect() } @@ -2583,12 +2593,115 @@ where F::one_with_cfg(field_cfg), SHA_IDEAL_EVAL_POWER_COUNT, ); - let mut target = F::zero_with_cfg(field_cfg); - for (slot, family) in production_sha_nonzero_families().iter().enumerate() { - target += lambda_powers[family.index()].clone() - * evaluate_production_sha_poly_at_powers(&ideal_polys[slot], &a_powers, field_cfg)?; + evaluate_aggregate_sha_ideal_claim_with_powers( + ideal_polys, + &a_powers, + &lambda_powers, + field_cfg, + ) +} + +fn evaluate_aggregate_sha_ideal_claim_with_powers( + ideal_polys: &[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], + a_powers: &[F], + lambda_powers: &[F], + field_cfg: &F::Config, +) -> Result> +where + F: DelayedFieldProductSum, +{ + if a_powers.len() < SHA_IDEAL_EVAL_POWER_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "aggregate SHA ideal a powers", + got: a_powers.len(), + expected: SHA_IDEAL_EVAL_POWER_COUNT, + }); + } + let mut values: [F; NUM_NONZERO_SHA_FAMILIES] = + std::array::from_fn(|_| F::zero_with_cfg(field_cfg)); + for (slot, poly) in ideal_polys.iter().enumerate() { + values[slot] = evaluate_production_sha_poly_at_powers(poly, a_powers, field_cfg)?; + } + lambda_weighted_nonzero_sha_values(&values, lambda_powers, field_cfg) +} + +fn selected_nonzero_sha_lambda_powers( + lambda_powers: &[F], +) -> Result<[F; NUM_NONZERO_SHA_FAMILIES], ProductionShaError> +where + F: PrimeField, +{ + if lambda_powers.len() != NUM_SHA_RESIDUAL_FAMILIES { + return Err(ProductionShaError::LengthMismatch { + label: "lambda powers", + got: lambda_powers.len(), + expected: NUM_SHA_RESIDUAL_FAMILIES, + }); } - Ok(target) + Ok(std::array::from_fn(|slot| { + lambda_powers[production_sha_nonzero_families()[slot].index()].clone() + })) +} + +fn lambda_weighted_nonzero_sha_values( + values: &[F; NUM_NONZERO_SHA_FAMILIES], + lambda_powers: &[F], + field_cfg: &F::Config, +) -> Result> +where + F: DelayedFieldProductSum, +{ + let weights = selected_nonzero_sha_lambda_powers(lambda_powers)?; + FieldFieldInnerProduct::inner_product::( + values, + &weights, + F::zero_with_cfg(field_cfg), + ) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject( + "production SHA nonzero-family dot product failed", + ) + }) +} + +fn lambda_weighted_sha_residual_polys_at_powers( + residuals: &[DynamicPolynomialF], + a_powers: &[F], + lambda_powers: &[F], + field_cfg: &F::Config, +) -> Result> +where + F: DelayedFieldProductSum, +{ + if residuals.len() != NUM_SHA_RESIDUAL_FAMILIES { + return Err(ProductionShaError::LengthMismatch { + label: "SHA residual families", + got: residuals.len(), + expected: NUM_SHA_RESIDUAL_FAMILIES, + }); + } + if lambda_powers.len() != NUM_SHA_RESIDUAL_FAMILIES { + return Err(ProductionShaError::LengthMismatch { + label: "lambda powers", + got: lambda_powers.len(), + expected: NUM_SHA_RESIDUAL_FAMILIES, + }); + } + let mut values: [F; NUM_SHA_RESIDUAL_FAMILIES] = + std::array::from_fn(|_| F::zero_with_cfg(field_cfg)); + for (idx, residual) in residuals.iter().enumerate() { + values[idx] = evaluate_production_sha_poly_at_powers(residual, a_powers, field_cfg)?; + } + FieldFieldInnerProduct::inner_product::( + &values, + lambda_powers, + F::zero_with_cfg(field_cfg), + ) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject( + "production SHA residual-family dot product failed", + ) + }) } fn evaluate_production_sha_poly_at_powers( @@ -2624,7 +2737,7 @@ fn eq_weighted_sum( field_cfg: &F::Config, ) -> Result> where - F: PrimeField, + F: DelayedFieldProductSum, { let expected = 1usize .checked_shl(u32::try_from(point.len()).map_err(|_| { @@ -2647,12 +2760,14 @@ where }); } let weights = build_eq_x_r_vec(point, field_cfg)?; - Ok(weights - .iter() - .zip(values.iter()) - .fold(F::zero_with_cfg(field_cfg), |acc, (weight, value)| { - acc + weight.clone() * value - })) + FieldFieldInnerProduct::inner_product::( + &weights, + values, + F::zero_with_cfg(field_cfg), + ) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject("eq-weighted value dot product failed") + }) } fn fold_mle_tables( @@ -4201,17 +4316,19 @@ where let residuals = [ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, ]; - let mut linear = zero.clone(); - for (residual, weight) in residuals.iter().zip(lambda_powers.iter()) { - linear += weight.clone() * eval_poly(residual); - } + let residual_evals: [F; NUM_SHA_RESIDUAL_FAMILIES] = + std::array::from_fn(|idx| eval_poly(&residuals[idx])); + let linear = FieldFieldInnerProduct::inner_product::( + &residual_evals, + &lambda_powers, + zero.clone(), + ) + .expect("row expression residual dot product lengths match"); let source_bit = |col: ShaWordCol, shift: usize, bit: usize| word_bits(col, shift)[bit].clone(); - let mut bool_sum = zero.clone(); - for (source, booleanity_weight) in - booleanity_sources.iter().zip(booleanity_weights.iter()) - { + let mut bool_terms = Vec::with_capacity(booleanity_sources.len()); + for source in &booleanity_sources { let d = match *source { ShaBooleanitySource::WordBit { col, bit } => source_bit(col, 0, bit), ShaBooleanitySource::VirtualCh1 { bit: bit_idx } => { @@ -4233,8 +4350,14 @@ where - two.clone() * source_bit(ShaWordCol::MajComp, 0, bit_idx) } }; - bool_sum += booleanity_weight.clone() * d.clone() * (d - one.clone()); + bool_terms.push(d.clone() * (d - one.clone())); } + let bool_sum = FieldFieldInnerProduct::inner_product::( + &booleanity_weights, + &bool_terms, + zero.clone(), + ) + .expect("row expression booleanity dot product lengths match"); values[0].clone() * (linear + bool_sum) }), @@ -4986,17 +5109,26 @@ where row_weights, field_cfg, )?; - let mut linear = F::zero_with_cfg(field_cfg); - for (residual, weight) in residuals.iter().zip(lambda_powers.iter()) { - linear += weight.clone() - * evaluate_production_sha_poly_at_powers(residual, &a_powers, field_cfg)?; - } + let linear = lambda_weighted_sha_residual_polys_at_powers( + &residuals, + a_powers, + lambda_powers, + field_cfg, + )?; - let mut bool_sum = F::zero_with_cfg(field_cfg); - for (source, booleanity_weight) in booleanity_sources.iter().zip(booleanity_weights.iter()) { + let mut bool_terms = Vec::with_capacity(booleanity_sources.len()); + for source in booleanity_sources { let d = booleanity_endpoint_value(endpoint_evals, source, field_cfg)?; - bool_sum += booleanity_weight.clone() * d.clone() * (d - F::one_with_cfg(field_cfg)); + bool_terms.push(d.clone() * (d - F::one_with_cfg(field_cfg))); } + let bool_sum = FieldFieldInnerProduct::inner_product::( + booleanity_weights, + &bool_terms, + F::zero_with_cfg(field_cfg), + ) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject("endpoint booleanity dot product failed") + })?; let row_weight = eq_eval(r_ic, r_star, F::one_with_cfg(field_cfg))?; Ok(row_weight * (linear + bool_sum)) @@ -5382,7 +5514,7 @@ fn residual_polys_from_endpoints_with_row_weights( field_cfg: &F::Config, ) -> Result>, ProductionShaError> where - F: PrimeField, + F: DelayedFieldProductSum, { if row_weights.len() != SHA_ROW_COUNT { return Err(ProductionShaError::LengthMismatch { @@ -5667,7 +5799,7 @@ fn endpoint_public_word_or_const_poly_with_row_weights( field_cfg: &F::Config, ) -> Result, ProductionShaError> where - F: PrimeField, + F: DelayedFieldProductSum, { let Some(col_idx) = public_word_col_index(col) else { return endpoint_public_const_poly_with_row_weights( @@ -5685,28 +5817,35 @@ where .ok_or(ProductionShaError::NonCanonicalProofObject( "production SHA public word columns are required", ))?; - let mut coeffs = vec![F::zero_with_cfg(field_cfg); SHA_WORD_BITS]; - for (row, row_weight) in row_weights.iter().enumerate() { - for (bit, coeff) in coeffs.iter_mut().enumerate() { - let table_idx = bit_slice_index(col_idx, bit, SHA_WORD_BITS); - let bit_column = - bit_slices - .get(table_idx) - .ok_or(ProductionShaError::LengthMismatch { - label: "SHA public word bit column", - got: table_idx, - expected: bit_slices.len(), - })?; - if bit_column.num_vars != SHA_ROW_VARS || bit_column.evaluations.len() != SHA_ROW_COUNT - { - return Err(ProductionShaError::LengthMismatch { - label: "SHA public word row count", - got: bit_column.evaluations.len(), - expected: SHA_ROW_COUNT, - }); - } - *coeff += row_weight.clone() * &bit_column.evaluations[row]; + let mut coeffs = Vec::with_capacity(SHA_WORD_BITS); + for bit in 0..SHA_WORD_BITS { + let table_idx = bit_slice_index(col_idx, bit, SHA_WORD_BITS); + let bit_column = bit_slices + .get(table_idx) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA public word bit column", + got: table_idx, + expected: bit_slices.len(), + })?; + if bit_column.num_vars != SHA_ROW_VARS || bit_column.evaluations.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "SHA public word row count", + got: bit_column.evaluations.len(), + expected: SHA_ROW_COUNT, + }); } + coeffs.push( + FieldFieldInnerProduct::inner_product::( + row_weights, + &bit_column.evaluations, + F::zero_with_cfg(field_cfg), + ) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject( + "SHA public word row-weight dot product failed", + ) + })?, + ); } Ok(DynamicPolynomialF::new_trimmed(coeffs)) } @@ -5802,7 +5941,7 @@ fn sumfold_expected_eval( field_cfg: &F::Config, ) -> Result> where - F: PrimeField, + F: DelayedFieldProductSum, { let d = eq_eval(beta, r_b, F::one_with_cfg(field_cfg))?; let weights = build_eq_x_r_vec(r_b, field_cfg)?; @@ -5813,12 +5952,14 @@ where expected: fresh_targets.len(), }); } - let claim_at_r = weights - .iter() - .zip(fresh_targets) - .fold(F::zero_with_cfg(field_cfg), |acc, (weight, target)| { - acc + weight.clone() * target - }); + let claim_at_r = FieldFieldInnerProduct::inner_product::( + &weights, + fresh_targets, + F::zero_with_cfg(field_cfg), + ) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject("SumFold expected-value dot product failed") + })?; Ok(d * claim_at_r) } From d17bdd9430d16b76d84344307cfbd693c1d578bd Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 20:49:58 -0700 Subject: [PATCH 27/49] Add SHA-chain proving comparison bench --- protocol/benches/e2e.rs | 775 ++++++++++++++++++++++++++++++++++++++-- test-uair/src/lib.rs | 3 +- test-uair/src/sha256.rs | 74 ++++ 3 files changed, 814 insertions(+), 38 deletions(-) diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index 351f191e..7ae28c89 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -7,11 +7,16 @@ use criterion::{ }; use crypto_bigint::U64; use crypto_primitives::{ - ConstIntRing, ConstIntSemiring, Field, FixedSemiring, FromWithConfig, PrimeField, - crypto_bigint_int::Int, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, + ConstIntRing, ConstIntSemiring, ConstSemiring, Field, FixedSemiring, FromWithConfig, + PrimeField, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, }; use rand::rng; -use std::{fmt::Debug, hint::black_box, marker::PhantomData, ops::Neg}; +use std::{borrow::Cow, fmt::Debug, hint::black_box, marker::PhantomData, ops::Neg}; +use zinc_piop::neutron_nova::{ + MleTable, ProjectedPublic, ProjectedTrace, SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, + ShaPublicCol, bit_slice_index, +}; +use zinc_poly::mle::DenseMultilinearExtension; use zinc_poly::univariate::dynamic::over_field::DynamicPolyVecF; use zinc_poly::{ ConstCoeffBitWidth, Polynomial, @@ -26,18 +31,28 @@ use zinc_protocol::{ FoldedZincTypes, IntFoldedZincTypes4x, Proof, ZincPlusPiop, ZincTypes, fixed_prime::field_cfg_from_curve_scalar, pcs::{ - AllZipPCSTypes, BinaryHyraxZipRest, PCSCommitments, PCSParams, PCSVerifierParams, - ZincPCSTypes, + AllHyraxPCSTypes, AllZipPCSTypes, BinaryHyraxZipRest, PCSCommitments, PCSParams, + PCSVerifierParams, ZincPCSTypes, + }, + production_sha::{ + LinearIdealFoldProverParams, LinearIdealFoldVerifierParams, ProductionShaError, + ProductionShaProjectionAdapter, ProductionShaWitnessPolys, UairShape, + prove_linear_ideal_fold, setup_verify_linear_ideal_fold, verify_linear_ideal_fold, }, }; use zinc_test_uair::{ BigLinearUair, BigLinearUairWithPublicInput, BinaryDecompositionUair, EC_FP_INT_LIMBS, - EcdsaUair, GenerateRandomTrace, Sha256CompressionSliceUair, Sha256Ideal, ShaEcdsaUair, - ShaProxy, TestUairNoMultiplication, + EcdsaUair, GenerateRandomTrace, SHA256_INITIAL_STATE, Sha256CompressionSliceUair, Sha256Ideal, + Sha256MessageBlock, ShaEcdsaUair, ShaProxy, TestUairNoMultiplication, + sha256::{K_CANONICAL, cols as sha256_cols}, + sha256_padded_message_blocks, synthesize_sha256_chain_trace, synthesize_sha256_chain_witnesses, +}; +use zinc_transcript::{ + Blake3Transcript, + traits::{ConstTranscribable, Transcribable}, }; -use zinc_transcript::traits::{ConstTranscribable, Transcribable}; use zinc_uair::{ - Uair, UairTrace, + ConstraintBuilder, PublicStructureError, TraceRow, Uair, UairSignature, UairTrace, degree_counter::count_effective_max_degree, ideal::{DegreeOneIdeal, Ideal, IdealCheck, ImpossibleIdeal, rotation::RotationIdeal}, ideal_collector::IdealOrZero, @@ -55,7 +70,7 @@ use zip_plus::{ iprs::{IprsCode, PnttConfigF65537}, }, pcs::generic::{PCS, ZipPlusPCS}, - pcs::hyrax::{BinaryLanes, HyraxBlindingMode, HyraxPCS}, + pcs::hyrax::{BinaryLanes, DensePolyScalarLanes, HyraxBlindingMode, HyraxPCS, IntScalarLane}, pcs::structs::{ZipPlus, ZipPlusCommitment, ZipPlusParams, ZipTypes}, utils::{eprint_bytes_size, eprint_bytes_size_breakdown, eprint_proof_size}, }; @@ -428,6 +443,565 @@ fn sha256_real_project_ideal( } } +const REAL_SHA256_CHAIN_BLOCKS: usize = 8; +const REAL_SHA256_CHAIN_NUM_VARS: usize = 10; + +fn real_sha256_chain_message() -> String { + vec!["hello world"; 40].join(" ") +} + +#[allow(clippy::unwrap_used)] +fn real_sha256_chain_blocks() -> [Sha256MessageBlock; REAL_SHA256_CHAIN_BLOCKS] { + let message = real_sha256_chain_message(); + sha256_padded_message_blocks::(message.as_bytes()) + .expect("real SHA-256 benchmark fixture should canonically pad to eight blocks") +} + +#[allow(clippy::unwrap_used)] +fn real_sha256_chain_trace( + num_vars: usize, +) -> UairTrace<'static, RealEcdsaInt, RealEcdsaInt, DEGREE_PLUS_ONE> { + let (trace, _final_state) = synthesize_sha256_chain_trace::< + RealEcdsaInt, + REAL_SHA256_CHAIN_BLOCKS, + >(num_vars, SHA256_INITIAL_STATE, real_sha256_chain_blocks()) + .expect("real SHA-256 monolithic chain trace synthesis should succeed"); + trace +} + +#[derive(Clone, Debug)] +struct ProjectionShaBenchUair(PhantomData); + +impl Uair for ProjectionShaBenchUair +where + R: ConstSemiring + 'static, +{ + type Ideal = Sha256Ideal; + type Scalar = DensePolynomial; + + fn signature() -> UairSignature { + Sha256CompressionSliceUair::::signature() + } + + fn constrain_general( + b: &mut B, + up: TraceRow, + down: TraceRow, + from_ref: FromR, + mbs: MulByScalarFn, + ideal_from_ref: IFromR, + ) where + B: ConstraintBuilder, + FromR: Fn(&Self::Scalar) -> B::Expr, + MulByScalarFn: Fn(&B::Expr, &Self::Scalar) -> Option, + IFromR: Fn(&Self::Ideal) -> B::Ideal, + { + Sha256CompressionSliceUair::::constrain_general( + b, + up, + down, + from_ref, + mbs, + ideal_from_ref, + ); + } + + fn verify_public_structure( + public_trace: &UairTrace<'_, RT, IntT, D>, + num_vars: usize, + ) -> Result<(), PublicStructureError> + where + RT: Clone, + IntT: Clone + num_traits::Zero, + { + Sha256CompressionSliceUair::::verify_public_structure(public_trace, num_vars) + } +} + +fn projection_sha_binary_col<'a>( + public_trace: &'a UairTrace<'_, RealEcdsaInt, RealEcdsaInt, DEGREE_PLUS_ONE>, + witness_trace: &'a UairTrace<'_, RealEcdsaInt, RealEcdsaInt, DEGREE_PLUS_ONE>, + flat_col: usize, +) -> Result<&'a DenseMultilinearExtension>, ProductionShaError> { + if flat_col < sha256_cols::NUM_BIN_PUB { + public_trace + .binary_poly + .get(flat_col) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA binary public source columns", + got: public_trace.binary_poly.len(), + expected: flat_col + 1, + }) + } else { + let witness_col = flat_col - sha256_cols::NUM_BIN_PUB; + witness_trace + .binary_poly + .get(witness_col) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA binary witness source columns", + got: witness_trace.binary_poly.len(), + expected: witness_col + 1, + }) + } +} + +fn projection_sha_int_col<'a>( + public_trace: &'a UairTrace<'_, RealEcdsaInt, RealEcdsaInt, DEGREE_PLUS_ONE>, + witness_trace: &'a UairTrace<'_, RealEcdsaInt, RealEcdsaInt, DEGREE_PLUS_ONE>, + flat_col: usize, +) -> Result<&'a DenseMultilinearExtension, ProductionShaError> { + if flat_col < sha256_cols::NUM_INT_PUB { + public_trace + .int + .get(flat_col) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA int public source columns", + got: public_trace.int.len(), + expected: flat_col + 1, + }) + } else { + let witness_col = flat_col - sha256_cols::NUM_INT_PUB; + witness_trace + .int + .get(witness_col) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA int witness source columns", + got: witness_trace.int.len(), + expected: witness_col + 1, + }) + } +} + +fn projection_sha_project_binary_source( + col: &DenseMultilinearExtension>, + field_cfg: &::Config, +) -> Result>, ProductionShaError> { + if col.evaluations.len() < SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "SHA binary source rows", + got: col.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + Ok(col + .evaluations + .iter() + .take(SHA_ROW_COUNT) + .map(|poly| { + poly.iter() + .take(SHA_WORD_BITS) + .map(|bit| { + if bit.into_inner() { + F::one_with_cfg(field_cfg) + } else { + F::zero_with_cfg(field_cfg) + } + }) + .collect() + }) + .collect()) +} + +fn projection_sha_project_int_source( + col: &DenseMultilinearExtension, + field_cfg: &::Config, +) -> Result, ProductionShaError> { + if col.evaluations.len() < SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "SHA int source rows", + got: col.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + Ok(col + .evaluations + .iter() + .take(SHA_ROW_COUNT) + .map(|value| F::from_with_cfg(value, field_cfg)) + .collect()) +} + +fn projection_sha_truncate_row_domain( + col: &DenseMultilinearExtension, + label: &'static str, +) -> Result, ProductionShaError> { + if col.evaluations.len() < SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label, + got: col.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + Ok(DenseMultilinearExtension { + evaluations: col.evaluations[..SHA_ROW_COUNT].to_vec(), + num_vars: SHA_ROW_VARS, + }) +} + +fn projection_sha_word_scalar_at_two(bits: &[F], field_cfg: &::Config) -> F { + let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); + let mut power = F::one_with_cfg(field_cfg); + let mut value = F::zero_with_cfg(field_cfg); + for bit in bits { + value += bit.clone() * &power; + power *= &two; + } + value +} + +fn projection_sha_mle_table_from_columns(columns: Vec>) -> MleTable { + columns + .into_iter() + .map(|evaluations| DenseMultilinearExtension { + evaluations, + num_vars: SHA_ROW_VARS, + }) + .collect() +} + +fn projection_sha_flatten_bit_columns(columns: Vec>>) -> MleTable { + let mut flattened = (0..columns.len() * SHA_WORD_BITS) + .map(|_| Vec::with_capacity(SHA_ROW_COUNT)) + .collect::>(); + for (col_idx, rows) in columns.into_iter().enumerate() { + for bits in rows { + for (bit, value) in bits.into_iter().enumerate() { + flattened[bit_slice_index(col_idx, bit, SHA_WORD_BITS)].push(value); + } + } + } + projection_sha_mle_table_from_columns(flattened) +} + +fn projection_sha_scalarize_bit_slices( + bit_slices: &MleTable, + a: &F, + field_cfg: &::Config, +) -> Result, ProductionShaError> { + let powers = zinc_utils::powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); + let word_count = bit_slices.len() / SHA_WORD_BITS; + let mut words = Vec::with_capacity(word_count); + for col_idx in 0..word_count { + let mut out_col = Vec::with_capacity(SHA_ROW_COUNT); + for row in 0..SHA_ROW_COUNT { + let mut value = F::zero_with_cfg(field_cfg); + for (bit, power) in powers.iter().enumerate() { + let bit_col = &bit_slices[bit_slice_index(col_idx, bit, SHA_WORD_BITS)]; + if bit_col.num_vars != SHA_ROW_VARS || bit_col.evaluations.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "SHA scalarized bit-slice rows", + got: bit_col.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + value += bit_col.evaluations[row].clone() * power; + } + out_col.push(value); + } + words.push(out_col); + } + Ok(projection_sha_mle_table_from_columns(words)) +} + +fn projection_sha_selector_expected( + selector: ShaPublicCol, + row: usize, + field_cfg: &::Config, +) -> F { + let active = match selector { + ShaPublicCol::SInit => row < 4, + ShaPublicCol::SMsg => row < 16, + ShaPublicCol::SSched => row < 48, + ShaPublicCol::SUpd => row < 64, + ShaPublicCol::SFf => (64..68).contains(&row), + ShaPublicCol::SOut => (68..72).contains(&row), + _ => false, + }; + if active { + F::one_with_cfg(field_cfg) + } else { + F::zero_with_cfg(field_cfg) + } +} + +fn projection_sha_k_expected(row: usize, field_cfg: &::Config) -> F { + if (3..67).contains(&row) { + F::from_with_cfg(K_CANONICAL[row - 3] as u64, field_cfg) + } else { + F::zero_with_cfg(field_cfg) + } +} + +fn projection_sha_projected_public_from_sources( + pa_a: &[Vec], + pa_e: &[Vec], + message: &[Vec], + field_cfg: &::Config, +) -> MleTable { + let mut columns = vec![vec![F::zero_with_cfg(field_cfg); SHA_ROW_COUNT]; ShaPublicCol::COUNT]; + for row in 0..SHA_ROW_COUNT { + columns[ShaPublicCol::K.index()][row] = projection_sha_k_expected(row, field_cfg); + columns[ShaPublicCol::PAIn.index()][row] = + projection_sha_word_scalar_at_two(&pa_a[row], field_cfg); + columns[ShaPublicCol::PEIn.index()][row] = + projection_sha_word_scalar_at_two(&pa_e[row], field_cfg); + columns[ShaPublicCol::PAOut.index()][row] = + projection_sha_word_scalar_at_two(&pa_a[row], field_cfg); + columns[ShaPublicCol::PEOut.index()][row] = + projection_sha_word_scalar_at_two(&pa_e[row], field_cfg); + columns[ShaPublicCol::Message.index()][row] = + projection_sha_word_scalar_at_two(&message[row], field_cfg); + } + for selector in [ + ShaPublicCol::SInit, + ShaPublicCol::SMsg, + ShaPublicCol::SSched, + ShaPublicCol::SUpd, + ShaPublicCol::SFf, + ShaPublicCol::SOut, + ] { + for row in 0..SHA_ROW_COUNT { + columns[selector.index()][row] = + projection_sha_selector_expected(selector, row, field_cfg); + } + } + projection_sha_mle_table_from_columns(columns) +} + +impl ProductionShaProjectionAdapter + for ProjectionShaBenchUair +{ + fn project_production_sha_public( + _shape: &UairShape, + public_trace: &UairTrace<'_, RealEcdsaInt, RealEcdsaInt, DEGREE_PLUS_ONE>, + field_cfg: &::Config, + ) -> Result, ProductionShaError> { + let empty_witness = UairTrace { + binary_poly: Cow::Borrowed(&[]), + arbitrary_poly: Cow::Borrowed(&[]), + int: Cow::Borrowed(&[]), + }; + let pa_a = projection_sha_project_binary_source( + projection_sha_binary_col(public_trace, &empty_witness, sha256_cols::PA_A)?, + field_cfg, + )?; + let pa_e = projection_sha_project_binary_source( + projection_sha_binary_col(public_trace, &empty_witness, sha256_cols::PA_E)?, + field_cfg, + )?; + let message = projection_sha_project_binary_source( + projection_sha_binary_col(public_trace, &empty_witness, sha256_cols::PA_M)?, + field_cfg, + )?; + let public_columns = + projection_sha_projected_public_from_sources(&pa_a, &pa_e, &message, field_cfg); + Ok(ProjectedPublic { + columns: public_columns, + bit_slices: Some(projection_sha_flatten_bit_columns(vec![ + pa_a.clone(), + pa_e.clone(), + pa_a, + pa_e, + message, + ])), + }) + } + + fn project_production_sha_witness( + _shape: &UairShape, + public_trace: &UairTrace<'_, RealEcdsaInt, RealEcdsaInt, DEGREE_PLUS_ONE>, + witness_trace: &UairTrace<'_, RealEcdsaInt, RealEcdsaInt, DEGREE_PLUS_ONE>, + field_cfg: &::Config, + ) -> Result< + ( + ProjectedTrace, + ProjectedPublic, + ProductionShaWitnessPolys, + ), + ProductionShaError, + > { + let word_sources = [ + sha256_cols::W_A, + sha256_cols::W_E, + sha256_cols::W_SIG0, + sha256_cols::W_SIG1, + sha256_cols::W_W, + sha256_cols::W_LSIG0, + sha256_cols::W_LSIG1, + sha256_cols::W_U_EF, + sha256_cols::W_U_NEG_E_G, + sha256_cols::W_MAJ, + sha256_cols::W_MU_PACKED, + sha256_cols::PA_OV_SIG0, + sha256_cols::PA_OV_SIG1, + sha256_cols::PA_OV_LSIG0, + sha256_cols::PA_OV_LSIG1, + sha256_cols::PA_R_CH2_COMP, + sha256_cols::PA_R_MAJ_COMP, + ]; + let int_sources = [ + sha256_cols::PA_C_C7, + sha256_cols::PA_C_C8, + sha256_cols::PA_C_C9, + sha256_cols::PA_C_FF_A, + sha256_cols::PA_C_FF_E, + ]; + + let bit_columns = word_sources + .iter() + .map(|&col| { + projection_sha_project_binary_source( + projection_sha_binary_col(public_trace, witness_trace, col)?, + field_cfg, + ) + }) + .collect::, _>>()?; + let bit_slices = projection_sha_flatten_bit_columns(bit_columns); + let scalarized = projection_sha_scalarize_bit_slices( + &bit_slices, + &F::from_with_cfg(2u64, field_cfg), + field_cfg, + )?; + let pa_a = projection_sha_project_binary_source( + projection_sha_binary_col(public_trace, witness_trace, sha256_cols::PA_A)?, + field_cfg, + )?; + let pa_e = projection_sha_project_binary_source( + projection_sha_binary_col(public_trace, witness_trace, sha256_cols::PA_E)?, + field_cfg, + )?; + let message = projection_sha_project_binary_source( + projection_sha_binary_col(public_trace, witness_trace, sha256_cols::PA_M)?, + field_cfg, + )?; + let public_columns = + projection_sha_projected_public_from_sources(&pa_a, &pa_e, &message, field_cfg); + let int_columns = int_sources + .iter() + .map(|&col| { + projection_sha_project_int_source( + projection_sha_int_col(public_trace, witness_trace, col)?, + field_cfg, + ) + }) + .collect::, _>>()?; + + let trace = ProjectedTrace { + bit_slices, + scalarized, + int_columns: projection_sha_mle_table_from_columns(int_columns), + public_columns: public_columns.clone(), + }; + let public = ProjectedPublic { + columns: public_columns, + bit_slices: Some(projection_sha_flatten_bit_columns(vec![ + pa_a.clone(), + pa_e.clone(), + pa_a, + pa_e, + message, + ])), + }; + Ok(( + trace, + public, + ProductionShaWitnessPolys { + binary: word_sources + .iter() + .map(|&col| { + projection_sha_truncate_row_domain( + projection_sha_binary_col(public_trace, witness_trace, col)?, + "SHA binary witness row-domain projection", + ) + }) + .collect::, _>>()?, + arbitrary: Vec::new(), + int: int_sources + .iter() + .map(|&col| { + projection_sha_truncate_row_domain( + projection_sha_int_col(public_trace, witness_trace, col)?, + "SHA int witness row-domain projection", + ) + }) + .collect::, _>>()?, + }, + )) + } +} + +fn projection_sha_hyrax_key_pair( + width: usize, + offset: u64, +) -> ( + zip_plus::pcs::hyrax::HyraxCommitmentKey, + zip_plus::pcs::hyrax::HyraxVerifierKey, +) +where + C: AffineRepr, + Lanes: Clone + Debug + Send + Sync, +{ + let generator = C::Group::generator(); + let bases = (0..width) + .map(|idx| { + let scalar = C::ScalarField::from( + offset + u64::try_from(idx).expect("Hyrax basis index fits u64") + 1, + ); + (generator * scalar).into_affine() + }) + .collect::>(); + let h = generator + * C::ScalarField::from(offset + u64::try_from(width).expect("Hyrax width fits u64") + 1); + HyraxPCS::::setup_from_bases_with_blinding( + width, + bases, + h, + HyraxBlindingMode::Unblinded, + ) + .expect("Hyrax benchmark setup must be valid") +} + +fn projection_sha_hyrax_pcs_params_bn254() -> ( + PCSParams, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>, + PCSVerifierParams< + AllHyraxPCSTypes, + RealEcdsaBenchZincTypes, + F, + DEGREE_PLUS_ONE, + >, +) { + let width = SHA_ROW_COUNT; + let (binary_ck, binary_vk) = + projection_sha_hyrax_key_pair::(width, 0); + let (arbitrary_ck, arbitrary_vk) = + projection_sha_hyrax_key_pair::(width, 1_000); + let (int_ck, int_vk) = + projection_sha_hyrax_key_pair::(width, 2_000); + + ( + PCSParams::< + AllHyraxPCSTypes, + RealEcdsaBenchZincTypes, + F, + DEGREE_PLUS_ONE, + > { + binary: binary_ck, + arbitrary: arbitrary_ck, + int: int_ck, + }, + PCSVerifierParams::< + AllHyraxPCSTypes, + RealEcdsaBenchZincTypes, + F, + DEGREE_PLUS_ONE, + > { + binary: binary_vk, + arbitrary: arbitrary_vk, + int: int_vk, + }, + ) +} + // // End-to-end benchmarks (total prove/verify time) // @@ -1163,13 +1737,12 @@ fn bench_real_ecdsa_steps(group: &mut BenchmarkGroup, num_vars: usize) fn bench_real_sha256_e2e(group: &mut BenchmarkGroup, num_vars: usize) { type U = Sha256CompressionSliceUair; - let mut rng = rng(); - let trace = U::generate_random_trace(num_vars, &mut rng); + let trace = real_sha256_chain_trace(num_vars); let pp = setup_pp_real_ecdsa(num_vars); do_bench_e2e::( group, - "RealSha256", + "RealSha256Chain8", num_vars, &pp, &trace, @@ -1181,13 +1754,12 @@ fn bench_real_sha256_e2e(group: &mut BenchmarkGroup, num_vars: usize) fn bench_real_sha256_steps(group: &mut BenchmarkGroup, num_vars: usize) { type U = Sha256CompressionSliceUair; - let mut rng = rng(); - let trace = U::generate_random_trace(num_vars, &mut rng); + let trace = real_sha256_chain_trace(num_vars); let pp = setup_pp_real_ecdsa(num_vars); do_bench_steps::( group, - "RealSha256", + "RealSha256Chain8", num_vars, &pp, &trace, @@ -1298,8 +1870,7 @@ fn bench_real_sha256_pcs_curve_e2e( { type U = Sha256CompressionSliceUair; - let mut rng = rng(); - let trace = U::generate_random_trace(num_vars, &mut rng); + let trace = real_sha256_chain_trace(num_vars); let field_cfg = field_cfg_from_curve_scalar::< F, >::Fmod, @@ -1356,8 +1927,7 @@ fn bench_real_sha256_pcs_curve_steps( { type U = Sha256CompressionSliceUair; - let mut rng = rng(); - let trace = U::generate_random_trace(num_vars, &mut rng); + let trace = real_sha256_chain_trace(num_vars); let field_cfg = field_cfg_from_curve_scalar::< F, >::Fmod, @@ -1395,14 +1965,14 @@ fn bench_real_sha256_pcs_e2e(group: &mut BenchmarkGroup, num_vars: usi bench_real_sha256_pcs_curve_e2e::( group, num_vars, - "RealSha256PCS/ZipBn254Fr", - "RealSha256PCS/HyraxBn254Unblinded", + "RealSha256Chain8PCS/ZipBn254Fr", + "RealSha256Chain8PCS/HyraxBn254Unblinded", ); bench_real_sha256_pcs_curve_e2e::( group, num_vars, - "RealSha256PCS/ZipSecp256k1Fr", - "RealSha256PCS/HyraxSecp256k1Unblinded", + "RealSha256Chain8PCS/ZipSecp256k1Fr", + "RealSha256Chain8PCS/HyraxSecp256k1Unblinded", ); } @@ -1410,17 +1980,135 @@ fn bench_real_sha256_pcs_steps(group: &mut BenchmarkGroup, num_vars: u bench_real_sha256_pcs_curve_steps::( group, num_vars, - "RealSha256PCS/ZipBn254Fr", - "RealSha256PCS/HyraxBn254Unblinded", + "RealSha256Chain8PCS/ZipBn254Fr", + "RealSha256Chain8PCS/HyraxBn254Unblinded", ); bench_real_sha256_pcs_curve_steps::( group, num_vars, - "RealSha256PCS/ZipSecp256k1Fr", - "RealSha256PCS/HyraxSecp256k1Unblinded", + "RealSha256Chain8PCS/ZipSecp256k1Fr", + "RealSha256Chain8PCS/HyraxSecp256k1Unblinded", ); } +fn bench_og_sha256_zip_compare(group: &mut BenchmarkGroup, num_vars: usize) { + type U = Sha256CompressionSliceUair; + + let trace = real_sha256_chain_trace(num_vars); + let field_cfg = field_cfg_from_curve_scalar::< + F, + >::Fmod, + ark_bn254::G1Affine, + >(); + let (zip_pp, zip_vp) = zip_pcs_params(num_vars); + + do_bench_pcs_e2e::( + group, + "OG-ZincPlus-ZipBn254/SHA256Chain8", + num_vars, + &zip_pp, + &zip_vp, + &trace, + field_cfg, + zinc_protocol::project_scalar_fn, + sha256_real_project_ideal, + ); +} + +fn bench_projectionfold_sha256_concise_hyrax_bn254(group: &mut BenchmarkGroup) { + type C = ark_bn254::G1Affine; + type P = AllHyraxPCSTypes; + type U = ProjectionShaBenchUair; + + let message_blocks = real_sha256_chain_blocks(); + let (_mono_trace, mono_final_state) = + synthesize_sha256_chain_trace::( + REAL_SHA256_CHAIN_NUM_VARS, + SHA256_INITIAL_STATE, + message_blocks, + ) + .expect("monolithic N=8 SHA trace synthesis should succeed"); + let (witnesses, projection_final_state) = synthesize_sha256_chain_witnesses::< + RealEcdsaInt, + REAL_SHA256_CHAIN_BLOCKS, + >(SHA256_INITIAL_STATE, message_blocks) + .expect("ProjectionFold SHA witness synthesis should succeed"); + assert_eq!(mono_final_state, projection_final_state); + + let shape = UairShape::::new(SHA_ROW_VARS); + let field_cfg = field_cfg_from_curve_scalar::< + F, + >::Fmod, + C, + >(); + let (pcs_params, pcs_verifier_params) = projection_sha_hyrax_pcs_params_bn254(); + let pp = LinearIdealFoldProverParams::::new( + pcs_params, + field_cfg.clone(), + 3, + ); + let vs = setup_verify_linear_ideal_fold::( + LinearIdealFoldVerifierParams::new(pcs_verifier_params, field_cfg), + shape.clone(), + ) + .expect("ProjectionFold SHA verifier setup succeeds"); + + let params = format!("ProjectionFoldConcise-HyraxBn254/SHA256Chain8/row-vars={SHA_ROW_VARS}"); + + group.bench_function(BenchmarkId::new("Prove", ¶ms), |bench| { + bench.iter(|| { + let mut transcript = Blake3Transcript::new(); + black_box(prove_linear_ideal_fold::< + P, + U, + RealEcdsaBenchZincTypes, + F, + DEGREE_PLUS_ONE, + >(&pp, &shape, &witnesses, &mut transcript)) + .expect("ProjectionFold Concise prover failed"); + }); + }); + + let mut prover_transcript = Blake3Transcript::new(); + let output = prove_linear_ideal_fold::( + &pp, + &shape, + &witnesses, + &mut prover_transcript, + ) + .expect("proof generation for ProjectionFold verifier bench"); + + let mut verifier_transcript = Blake3Transcript::new(); + let verified = verify_linear_ideal_fold::( + &vs, + &output.fresh_instances, + &output.proof, + &mut verifier_transcript, + ) + .expect("ProjectionFold verifier preflight failed"); + assert_eq!(verified.target, output.folded_instance.target); + assert_eq!(verified.public, output.folded_instance.public); + + group.bench_function(BenchmarkId::new("Verify", ¶ms), |bench| { + bench.iter(|| { + let mut transcript = Blake3Transcript::new(); + black_box(verify_linear_ideal_fold::< + P, + U, + RealEcdsaBenchZincTypes, + F, + DEGREE_PLUS_ONE, + >( + &vs, + &output.fresh_instances, + &output.proof, + &mut transcript, + )) + .expect("ProjectionFold Concise verifier failed"); + }); + }); +} + fn bench_real_sha_ecdsa_e2e(group: &mut BenchmarkGroup, num_vars: usize) { type U = ShaEcdsaUair; @@ -1519,8 +2207,8 @@ fn e2e_benches(c: &mut Criterion) { // Real UAIRs ported from main-gamma. Trace size for ECDSA needs >= 256 // rows (Shamir loop), so num_vars=9 is the smallest meaningful size. // bench_real_ecdsa_e2e(&mut group, 9); - bench_real_sha256_e2e(&mut group, 9); - bench_real_sha256_pcs_e2e(&mut group, 9); + bench_real_sha256_e2e(&mut group, REAL_SHA256_CHAIN_NUM_VARS); + bench_real_sha256_pcs_e2e(&mut group, REAL_SHA256_CHAIN_NUM_VARS); bench_real_sha_ecdsa_e2e(&mut group, 9); group.finish(); @@ -1552,13 +2240,22 @@ fn e2e_steps_benches(c: &mut Criterion) { // Real UAIRs ported from main-gamma. See `e2e_benches` for the // num_vars=9 lower-bound rationale. bench_real_ecdsa_steps(&mut group, 9); - bench_real_sha256_steps(&mut group, 9); - bench_real_sha256_pcs_steps(&mut group, 9); + bench_real_sha256_steps(&mut group, REAL_SHA256_CHAIN_NUM_VARS); + bench_real_sha256_pcs_steps(&mut group, REAL_SHA256_CHAIN_NUM_VARS); bench_real_sha_ecdsa_steps(&mut group, 9); group.finish(); } +fn sha256_proving_system_compare_benches(c: &mut Criterion) { + let mut group = c.benchmark_group("SHA-256 Proving System Comparison"); + + bench_og_sha256_zip_compare(&mut group, REAL_SHA256_CHAIN_NUM_VARS); + bench_projectionfold_sha256_concise_hyrax_bn254(&mut group); + + group.finish(); +} + // // Folded Zip+ (1× fold) — total prove/verify benchmark. // @@ -2588,13 +3285,12 @@ fn bench_real_ecdsa_e2e_folded(group: &mut BenchmarkGroup, num_vars: u fn bench_real_sha256_e2e_folded(group: &mut BenchmarkGroup, num_vars: usize) { type U = Sha256CompressionSliceUair; - let mut rng = rng(); - let trace = U::generate_random_trace(num_vars, &mut rng); + let trace = real_sha256_chain_trace(num_vars); let pp = setup_folded_pp_real_ecdsa(num_vars); do_bench_e2e_folded::( group, - "RealSha256", + "RealSha256Chain8", num_vars, &pp, &trace, @@ -2660,7 +3356,7 @@ fn e2e_folded_benches(c: &mut Criterion) { // bench_sha_proxy_e2e_folded(&mut group, 12); bench_real_ecdsa_e2e_folded(&mut group, 9); - bench_real_sha256_e2e_folded(&mut group, 9); + bench_real_sha256_e2e_folded(&mut group, REAL_SHA256_CHAIN_NUM_VARS); bench_real_sha_ecdsa_e2e_folded(&mut group, 9); group.finish(); @@ -2712,6 +3408,11 @@ criterion_group! { config = Criterion::default().sample_size(100); targets = e2e_steps_benches } +criterion_group! { + name = sha256_compare; + config = Criterion::default().sample_size(20); + targets = sha256_proving_system_compare_benches +} criterion_group! { name = e2e_folded; config = Criterion::default().sample_size(500); @@ -2722,4 +3423,4 @@ criterion_group! { config = Criterion::default().sample_size(500); targets = e2e_folded_4x_benches } -criterion_main!(e2e, e2e_steps, e2e_folded, e2e_folded_4x); +criterion_main!(e2e, e2e_steps, sha256_compare, e2e_folded, e2e_folded_4x); diff --git a/test-uair/src/lib.rs b/test-uair/src/lib.rs index 68980caa..47de863c 100644 --- a/test-uair/src/lib.rs +++ b/test-uair/src/lib.rs @@ -16,7 +16,8 @@ pub use sha_ecdsa::ShaEcdsaUair; pub use sha256::{ SHA256_INITIAL_STATE, Sha256CompressionSliceUair, Sha256Ideal, Sha256MessageBlock, Sha256State, Sha256WitnessError, sha256_compress_native, sha256_padded_message_blocks, - synthesize_one_sha256_compression_trace, synthesize_sha256_chain_witnesses, + synthesize_one_sha256_compression_trace, synthesize_sha256_chain_trace, + synthesize_sha256_chain_witnesses, }; use crypto_primitives::{ConstSemiring, FixedSemiring, Semiring, boolean::Boolean}; diff --git a/test-uair/src/sha256.rs b/test-uair/src/sha256.rs index ee2055e0..aa37580a 100644 --- a/test-uair/src/sha256.rs +++ b/test-uair/src/sha256.rs @@ -1448,6 +1448,24 @@ where ) } +/// Synthesize one monolithic SHA-256 compression-chain trace. +/// +/// This packs all `N` chained compressions into a single UAIR trace, using +/// `num_vars` to choose the MLE row domain. It is the deterministic, +/// message-driven counterpart to [`GenerateRandomTrace`] for benchmark cases +/// that need to compare a monolithic proof against a batched/folded proof over +/// the same SHA-256 chain. +pub fn synthesize_sha256_chain_trace( + num_vars: usize, + initial_state: Sha256State, + message_blocks: [Sha256MessageBlock; N], +) -> Result<(UairTrace<'static, R, R, 32>, Sha256State), Sha256WitnessError> +where + R: ConstSemiring + From + Clone + 'static, +{ + synthesize_sha256_compression_chain_trace::(num_vars, initial_state, &message_blocks) +} + /// Synthesize `N` ordered fresh SHA-256 compression witnesses for a chain. /// /// State computation is sequential (`H_{i+1} = compress(H_i, M_i)`); once all @@ -2245,6 +2263,62 @@ mod tests { } } + #[test] + fn synthesize_sha256_chain_trace_n8_matches_native() { + let initial_state = test_initial_state(); + let message_blocks = test_message_blocks::<8>(); + let expected = native_chain(initial_state, &message_blocks); + + let (trace, final_state) = + synthesize_sha256_chain_trace::, 8>(10, initial_state, message_blocks) + .expect("monolithic N=8 SHA trace synthesis should succeed"); + let sig = > as Uair>::signature(); + let public = trace.public(&sig); + + assert_eq!(final_state, expected); + assert_eq!( + public_sha_state_at(&trace, 8 * cols::ROWS_PER_COMP), + final_state + ); + > as Uair>::verify_public_structure(&public, 10) + .expect("generated monolithic SHA public trace should satisfy public structure checks"); + } + + #[test] + fn sha256_chain_trace_and_projection_witnesses_expose_same_h8() { + let initial_state = SHA256_INITIAL_STATE; + let message = vec!["hello world"; 40].join(" "); + let message_blocks = sha256_padded_message_blocks::<8>(message.as_bytes()) + .expect("fixture should canonically pad to eight SHA-256 blocks"); + let expected = native_chain(initial_state, &message_blocks); + + let (mono_trace, mono_final_state) = + synthesize_sha256_chain_trace::, 8>(10, initial_state, message_blocks) + .expect("monolithic N=8 SHA trace synthesis should succeed"); + let (pf_witnesses, pf_final_state) = + synthesize_sha256_chain_witnesses::, 8>(initial_state, message_blocks) + .expect("N=8 SHA witness synthesis should succeed"); + + let mono_public_final = public_sha_state_at(&mono_trace, 8 * cols::ROWS_PER_COMP); + let pf_public_final = public_sha_state_at( + &pf_witnesses[pf_witnesses.len() - 1].trace, + cols::ROWS_PER_COMP, + ); + + assert_eq!(mono_final_state, expected); + assert_eq!(pf_final_state, expected); + assert_eq!(mono_final_state, pf_final_state); + assert_eq!(mono_public_final, pf_public_final); + assert_eq!(mono_public_final, mono_final_state); + assert_eq!(pf_public_final, pf_final_state); + + let digest_hex = mono_final_state + .iter() + .map(|word| format!("{word:08x}")) + .collect::(); + println!("final chained SHA-256 H_8 = {digest_hex}"); + } + #[test] fn synthesize_sha256_chain_witnesses_links_adjacent_states() { let initial_state = test_initial_state(); From cf19388f656d98bf105f274d6997ce0479c0f6cd Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 21:05:09 -0700 Subject: [PATCH 28/49] Trace production SHA heavy phases --- Cargo.toml | 2 + protocol/Cargo.toml | 2 + protocol/benches/e2e.rs | 30 + protocol/src/production_sha.rs | 1542 +++++++++++++++++--------------- 4 files changed, 877 insertions(+), 699 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa232980..60f149bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ rand = "0.9" rand_core = "0.9" rayon = { version = "1.10" } thiserror = "2.0" +tracing = "0.1" +tracing-subscriber = "0.3" zinc-primality = { path = "primality/" } zinc-test-uair = { path = "test-uair/" } zinc-transcript = { path = "transcript/" } diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index f3429c01..0944f3fc 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -12,6 +12,7 @@ rayon = { workspace = true, optional = true } itertools = { workspace = true } num-traits = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } zip-plus = { workspace = true } zinc-piop = { workspace = true } zinc-poly = { workspace = true } @@ -31,6 +32,7 @@ ark-secp256k1 = { version = "0.5.0", default-features = false } criterion = { workspace = true } crypto-bigint = { workspace = true } rand = { workspace = true } +tracing-subscriber = { workspace = true } zinc-test-uair = { workspace = true } zstd = "0.13" libc = "0.2" diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index 7ae28c89..d8082bb6 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -2089,6 +2089,36 @@ fn bench_projectionfold_sha256_concise_hyrax_bn254(group: &mut BenchmarkGroup(&pp, &shape, &witnesses, &mut prover_transcript) + .expect("ProjectionFold traced prover failed"); + + let mut verifier_transcript = Blake3Transcript::new(); + let traced_verified = + verify_linear_ideal_fold::( + &vs, + &traced_output.fresh_instances, + &traced_output.proof, + &mut verifier_transcript, + ) + .expect("ProjectionFold traced verifier failed"); + assert_eq!(traced_verified.target, traced_output.folded_instance.target); + assert_eq!(traced_verified.public, traced_output.folded_instance.public); + }); + group.bench_function(BenchmarkId::new("Verify", ¶ms), |bench| { bench.iter(|| { let mut transcript = Blake3Transcript::new(); diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 89229c94..bdf71e28 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -4,7 +4,7 @@ //! `Proof`: production ProjectionFold has a different transcript order and //! derives folded commitments only after SumFold fixes the instance-axis point. -use std::{borrow::Cow, io::Cursor, marker::PhantomData, time::Duration}; +use std::{borrow::Cow, io::Cursor, marker::PhantomData}; use crate::{ ZincTypes, @@ -28,9 +28,9 @@ use zinc_piop::{ neutron_nova::SumFoldError, neutron_nova::{ InstanceFoldClaim, LinearResidualCoeffTable, MleTable, NUM_NONZERO_SHA_FAMILIES, - NUM_SHA_RESIDUAL_FAMILIES, ProjectedPublic, ProjectedTrace, SHA_ROW_COUNT, SHA_ROW_VARS, - SHA_WORD_BITS, ShaBooleanitySource, ShaIntCol, ShaProjectionError, ShaPublicCol, - ShaPublicWordCol, ShaResidualFamily, ShaWordCol, + NUM_SHA_RESIDUAL_FAMILIES, ProjectedPublic, ProjectedTrace, ProjectionFoldWitness, + SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, ShaBooleanitySource, ShaIntCol, + ShaProjectionError, ShaPublicCol, ShaPublicWordCol, ShaResidualFamily, ShaWordCol, beta_aggregate_nonzero_ideal_polys_with_weights, bit_slice_index, build_booleanity_weights, build_dense_sha_sumfold_group, build_folded_row_sumcheck_group, build_linear_residual_coeff_tables_with_row_weights, @@ -214,158 +214,6 @@ pub struct ProductionShaChallenges { pub beta: Vec, } -/// Heavy-region wall-time breakdown for a production SHA prover run. -/// -/// These regions intentionally omit cheap challenge/equality-vector setup and -/// final struct assembly, so [`Self::total`] is the sum of measured heavy -/// regions rather than exact end-to-end wall time. -#[derive(Default, Debug, Clone, Copy)] -pub struct ProductionShaProveTimings { - pub witness_generation: Duration, - pub commitment: Duration, - pub residual_coeff_tables: Duration, - pub aggregate_ideal: Duration, - pub sumfold_linear_accumulator: Duration, - pub sumfold_quadratic_prefix_accumulator: Duration, - pub sumfold_group_build: Duration, - pub sumfold_prove: Duration, - pub fold_projected_traces: Duration, - pub row_claim_derivation: Duration, - pub fold_pcs_commitments: Duration, - pub fold_pcs_prover_data: Duration, - pub row_sumcheck: Duration, - pub endpoint_resolver: Duration, - pub multipoint_eval: Duration, - pub lifted_evals: Duration, - pub pcs_opening: Duration, -} - -impl ProductionShaProveTimings { - pub fn add_assign(&mut self, other: &Self) { - self.witness_generation += other.witness_generation; - self.commitment += other.commitment; - self.residual_coeff_tables += other.residual_coeff_tables; - self.aggregate_ideal += other.aggregate_ideal; - self.sumfold_linear_accumulator += other.sumfold_linear_accumulator; - self.sumfold_quadratic_prefix_accumulator += other.sumfold_quadratic_prefix_accumulator; - self.sumfold_group_build += other.sumfold_group_build; - self.sumfold_prove += other.sumfold_prove; - self.fold_projected_traces += other.fold_projected_traces; - self.row_claim_derivation += other.row_claim_derivation; - self.fold_pcs_commitments += other.fold_pcs_commitments; - self.fold_pcs_prover_data += other.fold_pcs_prover_data; - self.row_sumcheck += other.row_sumcheck; - self.endpoint_resolver += other.endpoint_resolver; - self.multipoint_eval += other.multipoint_eval; - self.lifted_evals += other.lifted_evals; - self.pcs_opening += other.pcs_opening; - } - - pub fn divide_by(&mut self, n: u32) { - self.witness_generation /= n; - self.commitment /= n; - self.residual_coeff_tables /= n; - self.aggregate_ideal /= n; - self.sumfold_linear_accumulator /= n; - self.sumfold_quadratic_prefix_accumulator /= n; - self.sumfold_group_build /= n; - self.sumfold_prove /= n; - self.fold_projected_traces /= n; - self.row_claim_derivation /= n; - self.fold_pcs_commitments /= n; - self.fold_pcs_prover_data /= n; - self.row_sumcheck /= n; - self.endpoint_resolver /= n; - self.multipoint_eval /= n; - self.lifted_evals /= n; - self.pcs_opening /= n; - } - - pub fn total(&self) -> Duration { - self.witness_generation - + self.commitment - + self.residual_coeff_tables - + self.aggregate_ideal - + self.sumfold_linear_accumulator - + self.sumfold_quadratic_prefix_accumulator - + self.sumfold_group_build - + self.sumfold_prove - + self.fold_projected_traces - + self.row_claim_derivation - + self.fold_pcs_commitments - + self.fold_pcs_prover_data - + self.row_sumcheck - + self.endpoint_resolver - + self.multipoint_eval - + self.lifted_evals - + self.pcs_opening - } -} - -/// Heavy-region wall-time breakdown for a production SHA verifier run. -/// -/// These regions intentionally omit cheap challenge/equality-vector setup, so -/// [`Self::total`] is the sum of measured heavy regions rather than exact -/// end-to-end wall time. -#[derive(Default, Debug, Clone, Copy)] -pub struct ProductionShaVerifyTimings { - pub public_projection: Duration, - pub aggregate_ideal_verify: Duration, - pub sumfold_verify: Duration, - pub fold_pcs_commitments: Duration, - pub row_sumcheck_verify: Duration, - pub fold_projected_publics: Duration, - pub resolver_terminal_verify: Duration, - pub multipoint_verify: Duration, - pub multipoint_open_eval_checks: Duration, - pub lifted_eval_absorb: Duration, - pub pcs_verify: Duration, -} - -impl ProductionShaVerifyTimings { - pub fn add_assign(&mut self, other: &Self) { - self.public_projection += other.public_projection; - self.aggregate_ideal_verify += other.aggregate_ideal_verify; - self.sumfold_verify += other.sumfold_verify; - self.fold_pcs_commitments += other.fold_pcs_commitments; - self.row_sumcheck_verify += other.row_sumcheck_verify; - self.fold_projected_publics += other.fold_projected_publics; - self.resolver_terminal_verify += other.resolver_terminal_verify; - self.multipoint_verify += other.multipoint_verify; - self.multipoint_open_eval_checks += other.multipoint_open_eval_checks; - self.lifted_eval_absorb += other.lifted_eval_absorb; - self.pcs_verify += other.pcs_verify; - } - - pub fn divide_by(&mut self, n: u32) { - self.public_projection /= n; - self.aggregate_ideal_verify /= n; - self.sumfold_verify /= n; - self.fold_pcs_commitments /= n; - self.row_sumcheck_verify /= n; - self.fold_projected_publics /= n; - self.resolver_terminal_verify /= n; - self.multipoint_verify /= n; - self.multipoint_open_eval_checks /= n; - self.lifted_eval_absorb /= n; - self.pcs_verify /= n; - } - - pub fn total(&self) -> Duration { - self.public_projection - + self.aggregate_ideal_verify - + self.sumfold_verify - + self.fold_pcs_commitments - + self.row_sumcheck_verify - + self.fold_projected_publics - + self.resolver_terminal_verify - + self.multipoint_verify - + self.multipoint_open_eval_checks - + self.lifted_eval_absorb - + self.pcs_verify - } -} - const SHA256_ROUND_CONSTANTS: [u32; 64] = [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, @@ -500,6 +348,52 @@ pub struct FoldedLinearIdealWitness { pub witness: Witness, } +type ProductionShaFreshArtifacts = ( + Vec< + UairInstance< + 'static, + >::Int, + >::Int, + PCSCommitments, + D, + >, + >, + Vec>, + Vec>, + Vec>, + Vec>, +); + +type ProductionShaSumfoldAccumulators = (Vec, Vec, MultiDegreeSumcheckGroup); + +type ProductionShaFoldAfterSumfold = ( + ProjectionFoldWitness, + ProjectedPublic, + F, + InstanceFoldClaim, + PCSCommitments, + PCSProverData, +); + +type ProductionShaEndpointMultipoint = ( + CombinedPolyResolverProof, + ShaEndpointEvals, + MultipointEvalProof, + Vec, +); + +type ProductionShaPcsOpening = + (Vec>, PCSOpeningProof); + +type ProductionShaVerifierAggregate = ( + [DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], + F, + F, + F, + F, + F, +); + #[derive(Clone, Debug)] pub struct ProductionShaFoldedWitness where @@ -1086,6 +980,8 @@ pub fn check_aggregate_sha_ideal_membership( ) -> Result<(), ProductionShaError> where F: PrimeField, + F::Inner: Transcribable, + F::Modulus: Transcribable, { verify_fresh_sha_ideal_polys(std::slice::from_ref(ideal_polys), field_cfg)?; Ok(()) @@ -1182,83 +1078,6 @@ pub fn prove_linear_ideal_fold( >, LinearIdealFoldError, > -where - U: Uair + ProductionShaProjectionAdapter, - Zt: ZincTypes, - F: InnerTransparentField - + DelayedFieldProductSum - + FromPrimitiveWithConfig - + Send - + Sync - + 'static, - F::Inner: Transcribable + Zero + Default + Send + Sync, - F::Modulus: Transcribable, - P: ZincPCSTypes, - P: ProductionShaFoldedPcsOpen, - P::BinaryPCS: FoldablePCS, D>, - P::ArbitraryPCS: FoldablePCS, D>, - P::IntPCS: FoldablePCS, -{ - prove_linear_ideal_fold_inner(pp, shape, witnesses, transcript, None) -} - -#[allow(clippy::too_many_arguments)] -pub fn prove_linear_ideal_fold_with_timings( - pp: &LinearIdealFoldProverParams, - shape: &UairShape, - witnesses: &[UairWitness<'_, Zt::Int, Zt::Int, D>], - transcript: &mut impl Transcript, -) -> Result< - ( - LinearIdealFoldProveOutput< - UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, - FoldedLinearIdealInstance, ProjectedPublic>, - FoldedLinearIdealWitness>, - ProductionLinearIdealFoldProof, - >, - ProductionShaProveTimings, - ), - LinearIdealFoldError, -> -where - U: Uair + ProductionShaProjectionAdapter, - Zt: ZincTypes, - F: InnerTransparentField - + DelayedFieldProductSum - + FromPrimitiveWithConfig - + Send - + Sync - + 'static, - F::Inner: Transcribable + Zero + Default + Send + Sync, - F::Modulus: Transcribable, - P: ZincPCSTypes, - P: ProductionShaFoldedPcsOpen, - P::BinaryPCS: FoldablePCS, D>, - P::ArbitraryPCS: FoldablePCS, D>, - P::IntPCS: FoldablePCS, -{ - let mut timings = ProductionShaProveTimings::default(); - let output = - prove_linear_ideal_fold_inner(pp, shape, witnesses, transcript, Some(&mut timings))?; - Ok((output, timings)) -} - -#[allow(clippy::too_many_arguments)] -fn prove_linear_ideal_fold_inner( - pp: &LinearIdealFoldProverParams, - shape: &UairShape, - witnesses: &[UairWitness<'_, Zt::Int, Zt::Int, D>], - transcript: &mut impl Transcript, - mut timings: Option<&mut ProductionShaProveTimings>, -) -> Result< - LinearIdealFoldProveOutput< - UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, - FoldedLinearIdealInstance, ProjectedPublic>, - FoldedLinearIdealWitness>, - ProductionLinearIdealFoldProof, - >, - LinearIdealFoldError, -> where U: Uair + ProductionShaProjectionAdapter, Zt: ZincTypes, @@ -1291,54 +1110,14 @@ where absorb_production_sha_statement_metadata(transcript); absorb_uair_shape_metadata(transcript, shape); - let mut instance_artifacts = Vec::with_capacity(witnesses.len()); - for (instance_idx, witness) in witnesses.iter().enumerate() { - let witness_start = std::time::Instant::now(); - let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; - let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; - if let Some(t) = timings.as_mut() { - t.witness_generation += witness_start.elapsed(); - } - - absorb_public_uair_trace::(transcript, instance_idx, &public_trace); - - let witness_start = std::time::Instant::now(); - let (trace, public, witness_polys) = - U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg)?; - if let Some(t) = timings.as_mut() { - t.witness_generation += witness_start.elapsed(); - } - - let commitment_start = std::time::Instant::now(); - let (data, commitment) = - commit_production_sha_instance::(&pp.pcs_params, &witness_polys)?; - if let Some(t) = timings.as_mut() { - t.commitment += commitment_start.elapsed(); - } - - let fresh_instance = UairInstance { - public_trace: own_uair_trace(&public_trace), - commitments: commitment.clone(), - }; - - instance_artifacts.push((fresh_instance, commitment, data, trace, public)); - } - - let (fresh_instances, instance_artifacts): (Vec<_>, Vec<_>) = instance_artifacts - .into_iter() - .map(|(fresh_instance, commitment, data, trace, public)| { - (fresh_instance, (commitment, data, trace, public)) - }) - .unzip(); - let (instance_commitments, instance_artifacts): (Vec<_>, Vec<_>) = instance_artifacts - .into_iter() - .map(|(commitment, data, trace, public)| (commitment, (data, trace, public))) - .unzip(); - let (instance_prover_data, instance_artifacts): (Vec<_>, Vec<_>) = instance_artifacts - .into_iter() - .map(|(data, trace, public)| (data, (trace, public))) - .unzip(); - let (traces, publics): (Vec<_>, Vec<_>) = instance_artifacts.into_iter().unzip(); + let (fresh_instances, instance_commitments, instance_prover_data, traces, publics) = + prove_fresh_instances_phase::( + &pp.pcs_params, + shape, + witnesses, + transcript, + field_cfg, + )?; validate_production_sha_publics(&publics, field_cfg)?; absorb_production_sha_commitments::( @@ -1350,31 +1129,13 @@ where let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); let r_ic_eq_weights = build_eq_x_r_vec(&r_ic, field_cfg)?; - let residual_start = std::time::Instant::now(); - let coeff_tables = build_linear_residual_coeff_tables_with_row_weights( - &traces, - &publics, - &r_ic_eq_weights, - field_cfg, - )?; - if let Some(t) = timings.as_mut() { - t.residual_coeff_tables = residual_start.elapsed(); - } + let coeff_tables = + build_residual_coeff_tables_phase(&traces, &publics, &r_ic_eq_weights, field_cfg)?; let beta = sample_instance_batch_challenge(transcript, witnesses.len(), field_cfg)?; let beta_eq_weights = build_eq_x_r_vec(&beta, field_cfg)?; - let aggregate_start = std::time::Instant::now(); - let aggregate_ideal_polys = - beta_aggregate_nonzero_ideal_polys_with_weights(&coeff_tables, &beta_eq_weights)?; - let ideal_check = IdealCheckProof { - combined_mle_values: aggregate_ideal_polys.iter().cloned().collect(), - }; - let aggregate_ideal_polys = aggregate_sha_ideal_polys_from_proof(&ideal_check)?; - check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, field_cfg)?; - absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys, field_cfg); - if let Some(t) = timings.as_mut() { - t.aggregate_ideal = aggregate_start.elapsed(); - } + let (ideal_check, aggregate_ideal_polys) = + prove_aggregate_ideal_phase(&coeff_tables, &beta_eq_weights, transcript, field_cfg)?; let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); let a_powers = build_sha_residual_eval_powers(&a, field_cfg); @@ -1388,54 +1149,28 @@ where field_cfg, )?; - let linear_accumulator_start = std::time::Instant::now(); - let linear_accumulator = - build_sha_sumfold_linear_accumulator(&coeff_tables, &a_powers, &lambda_powers, field_cfg)?; - if let Some(t) = timings.as_mut() { - t.sumfold_linear_accumulator = linear_accumulator_start.elapsed(); - } - - let quadratic_accumulator_start = std::time::Instant::now(); - let quadratic_prefix_accumulator = build_sha_sumfold_quadratic_prefix_accumulator( - &traces, - &booleanity_sources, - pp.prefix_vars, - &r_ic_eq_weights, - &booleanity_weights, - field_cfg, - )?; - if let Some(t) = timings.as_mut() { - t.sumfold_quadratic_prefix_accumulator = quadratic_accumulator_start.elapsed(); - } - - let sumfold_group_start = std::time::Instant::now(); - let sumfold_group = build_production_sha_sumfold_group_from_prefix_accumulators( - &traces, - &beta, - &beta_eq_weights, - &r_ic_eq_weights, - &linear_accumulator, - &quadratic_prefix_accumulator, - &booleanity_weights, - &booleanity_sources, - pp.prefix_vars, - field_cfg, - )?; - if let Some(t) = timings.as_mut() { - t.sumfold_group_build = sumfold_group_start.elapsed(); - } + let (_linear_accumulator, _quadratic_prefix_accumulator, sumfold_group) = + build_sumfold_accumulators_phase( + &traces, + &beta, + &beta_eq_weights, + &r_ic_eq_weights, + &coeff_tables, + &a_powers, + &lambda_powers, + &booleanity_weights, + &booleanity_sources, + pp.prefix_vars, + field_cfg, + )?; - let sumfold_prove_start = std::time::Instant::now(); - let (sumfold_proof, sumfold_r_b) = prove_optimized_sha_sumfold_with_weights( + let (sumfold_proof, sumfold_r_b) = prove_sumfold_phase( transcript, sumfold_group, &initial_claim, beta.len(), field_cfg, )?; - if let Some(t) = timings.as_mut() { - t.sumfold_prove = sumfold_prove_start.elapsed(); - } let provisional_sumfold_output = derive_instance_fold_claim( &beta, @@ -1445,146 +1180,69 @@ where field_cfg, )?; - let fold_traces_start = std::time::Instant::now(); - let (folded, folded_public) = - fold_projected_traces(&traces, &publics, &provisional_sumfold_output, field_cfg)?; - if let Some(t) = timings.as_mut() { - t.fold_projected_traces = fold_traces_start.elapsed(); - } + let (folded, folded_public, row_claim, sumfold_output, folded_commitments, folded_prover_data) = + prove_fold_after_sumfold_phase::( + &traces, + &publics, + &provisional_sumfold_output, + &beta, + sumfold_r_b, + &r_ic_eq_weights, + &a_powers, + &lambda_powers, + &booleanity_weights, + &booleanity_sources, + &instance_commitments, + &instance_prover_data, + field_cfg, + )?; + absorb_production_sha_commitments::( + transcript, + b"production_sha_derived_folded_commitments", + std::slice::from_ref(&folded_commitments), + ); - let row_claim_start = std::time::Instant::now(); - let row_claim = expression_folded_row_sum_with_vectors( + verify_folded_row_sumcheck_claim(&row_claim, sumfold_output.final_round_sumcheck_claim())?; + let (combined_sumcheck, row_output) = prove_row_sumcheck_phase( + transcript, &folded.trace, &folded_public, + &r_ic, &r_ic_eq_weights, &a_powers, &lambda_powers, &booleanity_weights, &booleanity_sources, - field_cfg, - )?; - let sumfold_output = derive_instance_fold_claim_from_row_claim( - &beta, - sumfold_r_b, &row_claim, - witnesses.len(), - field_cfg, - )?; - if let Some(t) = timings.as_mut() { - t.row_claim_derivation = row_claim_start.elapsed(); - } - - let fold_commitments_start = std::time::Instant::now(); - let folded_commitments = fold_pcs_commitments::( - &instance_commitments, - sumfold_output.eq_instance_weights(), - field_cfg, - )?; - if let Some(t) = timings.as_mut() { - t.fold_pcs_commitments = fold_commitments_start.elapsed(); - } - - let fold_prover_data_start = std::time::Instant::now(); - let folded_prover_data = fold_pcs_prover_data::( - &instance_prover_data, - sumfold_output.eq_instance_weights(), field_cfg, )?; - if let Some(t) = timings.as_mut() { - t.fold_pcs_prover_data = fold_prover_data_start.elapsed(); - } - absorb_production_sha_commitments::( - transcript, - b"production_sha_derived_folded_commitments", - std::slice::from_ref(&folded_commitments), - ); - verify_folded_row_sumcheck_claim(&row_claim, sumfold_output.final_round_sumcheck_claim())?; - let row_sumcheck_start = std::time::Instant::now(); - let (combined_sumcheck, row_output) = - prove_expression_folded_row_sumcheck_with_output_and_vectors( + let (resolver, _resolver_endpoint_evals, multipoint_eval, r_0) = + prove_endpoint_multipoint_phase( transcript, &folded.trace, &folded_public, + &row_output, &r_ic, - &r_ic_eq_weights, + &a, &a_powers, &lambda_powers, &booleanity_weights, &booleanity_sources, field_cfg, )?; - verify_folded_row_sumcheck_claim(&combined_sumcheck.claimed_sums()[0], &row_claim)?; - if let Some(t) = timings.as_mut() { - t.row_sumcheck = row_sumcheck_start.elapsed(); - } - - let endpoint_resolver_start = std::time::Instant::now(); - let endpoint_evals = build_sha_endpoint_evals_from_trace_with_row_weights( - &folded.trace, - &row_output.r_star_eq_weights, - &a, - field_cfg, - )?; - let resolver = sha_resolver_from_endpoint_evals(&endpoint_evals)?; - absorb_sha_resolver_proof(transcript, &resolver, field_cfg); - let resolver_endpoint_evals = sha_endpoint_evals_from_resolver(&resolver, &a, field_cfg)?; - let terminal = reconstruct_folded_row_terminal_from_endpoints_with_vectors( - &resolver_endpoint_evals, - &folded_public, - &r_ic, - &row_output.r_star, - &row_output.r_star_eq_weights, - &a_powers, - &lambda_powers, - &booleanity_weights, - &booleanity_sources, - field_cfg, - )?; - verify_folded_row_terminal_value(&row_output, &terminal)?; - if let Some(t) = timings.as_mut() { - t.endpoint_resolver = endpoint_resolver_start.elapsed(); - } - - let multipoint_start = std::time::Instant::now(); - let (multipoint_eval, r_0) = prove_sha_endpoint_multipoint_with_row_weights( - transcript, - &folded.trace, - &folded_public, - &resolver_endpoint_evals, - &row_output.r_star, - &row_output.r_star_eq_weights, - field_cfg, - )?; - if let Some(t) = timings.as_mut() { - t.multipoint_eval = multipoint_start.elapsed(); - } let r_0_eq_weights = build_eq_x_r_vec(&r_0, field_cfg)?; - let lifted_evals_start = std::time::Instant::now(); - let witness_lifted_evals = build_folded_sha_pcs_lifted_evals_with_row_weights( + let (witness_lifted_evals, opening_proof) = prove_pcs_opening_phase::( + transcript, &folded.trace, - &r_0_eq_weights, - field_cfg, - )?; - absorb_folded_lifted_evals(transcript, &witness_lifted_evals, field_cfg); - if let Some(t) = timings.as_mut() { - t.lifted_evals = lifted_evals_start.elapsed(); - } - - let pcs_opening_start = std::time::Instant::now(); - let opening_proof = P::prove_folded_pcs_opening( - &pp.pcs_params, &folded_commitments, - &folded.trace, &folded_prover_data, &r_0, - &witness_lifted_evals, + &r_0_eq_weights, + &pp.pcs_params, field_cfg, )?; - if let Some(t) = timings.as_mut() { - t.pcs_opening = pcs_opening_start.elapsed(); - } Ok(LinearIdealFoldProveOutput { fresh_instances, @@ -1612,15 +1270,447 @@ where }) } -fn public_uair_trace_view<'a, PolyCoeff, Int, F, const D: usize>( - trace: &'a UairTrace<'_, PolyCoeff, Int, D>, - sig: &UairSignature, -) -> Result, ProductionShaError> -where - PolyCoeff: Clone, - Int: Clone, - F: PrimeField, -{ +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "prove", phase = "fresh_instances", instances = witnesses.len()) +)] +#[allow(clippy::too_many_arguments)] +fn prove_fresh_instances_phase( + pcs_params: &PCSParams, + shape: &UairShape, + witnesses: &[UairWitness<'_, Zt::Int, Zt::Int, D>], + transcript: &mut impl Transcript, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + U: Uair + ProductionShaProjectionAdapter, + Zt: ZincTypes, + F: PrimeField, + P: ZincPCSTypes, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + let mut fresh_instances = Vec::with_capacity(witnesses.len()); + let mut instance_commitments = Vec::with_capacity(witnesses.len()); + let mut instance_prover_data = Vec::with_capacity(witnesses.len()); + let mut traces = Vec::with_capacity(witnesses.len()); + let mut publics = Vec::with_capacity(witnesses.len()); + + for (instance_idx, witness) in witnesses.iter().enumerate() { + let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; + let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; + absorb_public_uair_trace::(transcript, instance_idx, &public_trace); + + let (trace, public, witness_polys) = + U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg)?; + let (data, commitment) = + commit_production_sha_instance::(pcs_params, &witness_polys)?; + + fresh_instances.push(UairInstance { + public_trace: own_uair_trace(&public_trace), + commitments: commitment.clone(), + }); + instance_commitments.push(commitment); + instance_prover_data.push(data); + traces.push(trace); + publics.push(public); + } + + Ok(( + fresh_instances, + instance_commitments, + instance_prover_data, + traces, + publics, + )) +} + +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "prove", phase = "residual_coeff_tables", instances = traces.len()) +)] +fn build_residual_coeff_tables_phase( + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], + r_ic_eq_weights: &[F], + field_cfg: &F::Config, +) -> Result>, ProductionShaError> +where + F: PrimeField, + F::Inner: Transcribable, + F::Modulus: Transcribable, +{ + build_linear_residual_coeff_tables_with_row_weights(traces, publics, r_ic_eq_weights, field_cfg) + .map_err(ProductionShaError::from) +} + +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "prove", phase = "aggregate_ideal", instances = coeff_tables.len()) +)] +fn prove_aggregate_ideal_phase( + coeff_tables: &[LinearResidualCoeffTable], + beta_eq_weights: &[F], + transcript: &mut impl Transcript, + field_cfg: &F::Config, +) -> Result< + ( + IdealCheckProof, + [DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], + ), + ProductionShaError, +> +where + F: PrimeField, + F::Inner: Transcribable, + F::Modulus: Transcribable, +{ + let aggregate_ideal_polys = + beta_aggregate_nonzero_ideal_polys_with_weights(coeff_tables, beta_eq_weights)?; + let ideal_check = IdealCheckProof { + combined_mle_values: aggregate_ideal_polys.iter().cloned().collect(), + }; + let aggregate_ideal_polys = aggregate_sha_ideal_polys_from_proof(&ideal_check)?; + check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, field_cfg)?; + absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys, field_cfg); + Ok((ideal_check, aggregate_ideal_polys)) +} + +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields( + side = "prove", + phase = "sumfold_accumulators", + instances = traces.len(), + prefix_vars, + ) +)] +#[allow(clippy::too_many_arguments)] +fn build_sumfold_accumulators_phase( + traces: &[ProjectedTrace], + beta: &[F], + beta_eq_weights: &[F], + r_ic_eq_weights: &[F], + coeff_tables: &[LinearResidualCoeffTable], + a_powers: &[F], + lambda_powers: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, +{ + let linear_accumulator = + build_sha_sumfold_linear_accumulator(coeff_tables, a_powers, lambda_powers, field_cfg)?; + let quadratic_prefix_accumulator = build_sha_sumfold_quadratic_prefix_accumulator( + traces, + booleanity_sources, + prefix_vars, + r_ic_eq_weights, + booleanity_weights, + field_cfg, + )?; + let sumfold_group = build_production_sha_sumfold_group_from_prefix_accumulators( + traces, + beta, + beta_eq_weights, + r_ic_eq_weights, + &linear_accumulator, + &quadratic_prefix_accumulator, + booleanity_weights, + booleanity_sources, + prefix_vars, + field_cfg, + )?; + Ok(( + linear_accumulator, + quadratic_prefix_accumulator, + sumfold_group, + )) +} + +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "prove", phase = "sumfold_prove", instance_vars) +)] +fn prove_sumfold_phase( + transcript: &mut impl Transcript, + sumfold_group: MultiDegreeSumcheckGroup, + initial_claim: &F, + instance_vars: usize, + field_cfg: &F::Config, +) -> Result<(MultiDegreeSumcheckProof, Vec), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Send + Sync, + F::Modulus: Transcribable, +{ + prove_optimized_sha_sumfold_with_weights( + transcript, + sumfold_group, + initial_claim, + instance_vars, + field_cfg, + ) +} + +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "prove", phase = "fold_after_sumfold", instances = traces.len()) +)] +#[allow(clippy::too_many_arguments)] +fn prove_fold_after_sumfold_phase( + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], + provisional_sumfold_output: &InstanceFoldClaim, + beta: &[F], + sumfold_r_b: Vec, + r_ic_eq_weights: &[F], + a_powers: &[F], + lambda_powers: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + instance_commitments: &[PCSCommitments], + instance_prover_data: &[PCSProverData], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + Zt: ZincTypes, + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, + P: ZincPCSTypes, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + let (folded, folded_public) = + fold_projected_traces(traces, publics, provisional_sumfold_output, field_cfg)?; + let row_claim = expression_folded_row_sum_with_vectors( + &folded.trace, + &folded_public, + r_ic_eq_weights, + a_powers, + lambda_powers, + booleanity_weights, + booleanity_sources, + field_cfg, + )?; + let sumfold_output = derive_instance_fold_claim_from_row_claim( + beta, + sumfold_r_b, + &row_claim, + traces.len(), + field_cfg, + )?; + let folded_commitments = fold_pcs_commitments::( + instance_commitments, + sumfold_output.eq_instance_weights(), + field_cfg, + )?; + let folded_prover_data = fold_pcs_prover_data::( + instance_prover_data, + sumfold_output.eq_instance_weights(), + field_cfg, + )?; + + Ok(( + folded, + folded_public, + row_claim, + sumfold_output, + folded_commitments, + folded_prover_data, + )) +} + +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "prove", phase = "row_sumcheck") +)] +#[allow(clippy::too_many_arguments)] +fn prove_row_sumcheck_phase( + transcript: &mut impl Transcript, + trace: &ProjectedTrace, + public: &ProjectedPublic, + r_ic: &[F; SHA_ROW_VARS], + r_ic_eq_weights: &[F], + a_powers: &[F], + lambda_powers: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + row_claim: &F, + field_cfg: &F::Config, +) -> Result<(MultiDegreeSumcheckProof, FoldedRowSumcheckOutput), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, +{ + let (combined_sumcheck, row_output) = + prove_expression_folded_row_sumcheck_with_output_and_vectors( + transcript, + trace, + public, + r_ic, + r_ic_eq_weights, + a_powers, + lambda_powers, + booleanity_weights, + booleanity_sources, + field_cfg, + )?; + verify_folded_row_sumcheck_claim(&combined_sumcheck.claimed_sums()[0], row_claim)?; + Ok((combined_sumcheck, row_output)) +} + +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "prove", phase = "endpoint_multipoint") +)] +#[allow(clippy::too_many_arguments)] +fn prove_endpoint_multipoint_phase( + transcript: &mut impl Transcript, + trace: &ProjectedTrace, + folded_public: &ProjectedPublic, + row_output: &FoldedRowSumcheckOutput, + r_ic: &[F; SHA_ROW_VARS], + a: &F, + a_powers: &[F], + lambda_powers: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, +{ + let endpoint_evals = build_sha_endpoint_evals_from_trace_with_row_weights( + trace, + &row_output.r_star_eq_weights, + a, + field_cfg, + )?; + let resolver = sha_resolver_from_endpoint_evals(&endpoint_evals)?; + absorb_sha_resolver_proof(transcript, &resolver, field_cfg); + let resolver_endpoint_evals = sha_endpoint_evals_from_resolver(&resolver, a, field_cfg)?; + let terminal = reconstruct_folded_row_terminal_from_endpoints_with_vectors( + &resolver_endpoint_evals, + folded_public, + r_ic, + &row_output.r_star, + &row_output.r_star_eq_weights, + a_powers, + lambda_powers, + booleanity_weights, + booleanity_sources, + field_cfg, + )?; + verify_folded_row_terminal_value(row_output, &terminal)?; + + let (multipoint_eval, r_0) = prove_sha_endpoint_multipoint_with_row_weights( + transcript, + trace, + folded_public, + &resolver_endpoint_evals, + &row_output.r_star, + &row_output.r_star_eq_weights, + field_cfg, + )?; + Ok((resolver, resolver_endpoint_evals, multipoint_eval, r_0)) +} + +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "prove", phase = "pcs_opening") +)] +#[allow(clippy::too_many_arguments)] +fn prove_pcs_opening_phase( + transcript: &mut impl Transcript, + folded_trace: &ProjectedTrace, + folded_commitments: &PCSCommitments, + folded_prover_data: &PCSProverData, + r_0: &[F], + r_0_eq_weights: &[F], + pcs_params: &PCSParams, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + Zt: ZincTypes, + F: PrimeField + DelayedFieldProductSum, + F::Inner: Transcribable, + F::Modulus: Transcribable, + P: ZincPCSTypes, + P: ProductionShaFoldedPcsOpen, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + let witness_lifted_evals = build_folded_sha_pcs_lifted_evals_with_row_weights( + folded_trace, + r_0_eq_weights, + field_cfg, + )?; + absorb_folded_lifted_evals(transcript, &witness_lifted_evals, field_cfg); + let opening_proof = P::prove_folded_pcs_opening( + pcs_params, + folded_commitments, + folded_trace, + folded_prover_data, + r_0, + &witness_lifted_evals, + field_cfg, + )?; + Ok((witness_lifted_evals, opening_proof)) +} + +fn public_uair_trace_view<'a, PolyCoeff, Int, F, const D: usize>( + trace: &'a UairTrace<'_, PolyCoeff, Int, D>, + sig: &UairSignature, +) -> Result, ProductionShaError> +where + PolyCoeff: Clone, + Int: Clone, + F: PrimeField, +{ let public = sig.public_cols(); validate_uair_trace_shape(trace, sig)?; Ok(UairTrace { @@ -1755,7 +1845,9 @@ pub fn setup_verify_linear_ideal_fold( where U: Uair + ProductionShaProjectionAdapter, Zt: ZincTypes, - F: PrimeField, + F: PrimeField + FromPrimitiveWithConfig, + F::Inner: Transcribable, + F::Modulus: Transcribable, P: ZincPCSTypes, P::BinaryPCS: FoldablePCS, D>, P::ArbitraryPCS: FoldablePCS, D>, @@ -1789,69 +1881,6 @@ pub fn verify_linear_ideal_fold( FoldedLinearIdealInstance, ProjectedPublic>, LinearIdealFoldError, > -where - U: Uair + ProductionShaProjectionAdapter, - Zt: ZincTypes, - F: InnerTransparentField - + DelayedFieldProductSum - + FromPrimitiveWithConfig - + Send - + Sync - + 'static, - F::Inner: Transcribable + Zero + Default + Send + Sync, - F::Modulus: Transcribable, - P: ZincPCSTypes, - P::BinaryPCS: FoldablePCS, D>, - P::ArbitraryPCS: FoldablePCS, D>, - P::IntPCS: FoldablePCS, -{ - verify_linear_ideal_fold_inner(vs, instances, proof, transcript, None) -} - -pub fn verify_linear_ideal_fold_with_timings( - vs: &VerifiedLinearIdealFoldSetup, - instances: &[UairInstance<'_, Zt::Int, Zt::Int, PCSCommitments, D>], - proof: &ProductionLinearIdealFoldProof, - transcript: &mut impl Transcript, -) -> Result< - ( - FoldedLinearIdealInstance, ProjectedPublic>, - ProductionShaVerifyTimings, - ), - LinearIdealFoldError, -> -where - U: Uair + ProductionShaProjectionAdapter, - Zt: ZincTypes, - F: InnerTransparentField - + DelayedFieldProductSum - + FromPrimitiveWithConfig - + Send - + Sync - + 'static, - F::Inner: Transcribable + Zero + Default + Send + Sync, - F::Modulus: Transcribable, - P: ZincPCSTypes, - P::BinaryPCS: FoldablePCS, D>, - P::ArbitraryPCS: FoldablePCS, D>, - P::IntPCS: FoldablePCS, -{ - let mut timings = ProductionShaVerifyTimings::default(); - let folded_instance = - verify_linear_ideal_fold_inner(vs, instances, proof, transcript, Some(&mut timings))?; - Ok((folded_instance, timings)) -} - -fn verify_linear_ideal_fold_inner( - vs: &VerifiedLinearIdealFoldSetup, - instances: &[UairInstance<'_, Zt::Int, Zt::Int, PCSCommitments, D>], - proof: &ProductionLinearIdealFoldProof, - transcript: &mut impl Transcript, - mut timings: Option<&mut ProductionShaVerifyTimings>, -) -> Result< - FoldedLinearIdealInstance, ProjectedPublic>, - LinearIdealFoldError, -> where U: Uair + ProductionShaProjectionAdapter, Zt: ZincTypes, @@ -1889,7 +1918,100 @@ where absorb_production_sha_statement_metadata(transcript); absorb_uair_shape_metadata(transcript, &vs.shape); - let public_projection_start = std::time::Instant::now(); + let publics = + verify_public_projection_phase::(vs, instances, proof, transcript)?; + + let booleanity_sources = production_sha_booleanity_sources(); + + let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); + let beta = sample_instance_batch_challenge(transcript, instances.len(), field_cfg)?; + + let (_aggregate_ideal_polys, a, lambda, rho, xi, initial_claim) = + verify_aggregate_ideal_phase(&proof.ideal_check, transcript, field_cfg)?; + + let sumfold_output = verify_sumfold_phase( + transcript, + &proof.sumfold_proof, + &initial_claim, + &beta, + beta.len(), + instances.len(), + field_cfg, + )?; + + let folded_commitments = verify_fold_commitments_phase::( + &proof.instance_commitments, + sumfold_output.eq_instance_weights(), + field_cfg, + )?; + absorb_production_sha_commitments::( + transcript, + b"production_sha_derived_folded_commitments", + std::slice::from_ref(&folded_commitments), + ); + + let row_output = verify_row_sumcheck_phase( + transcript, + &proof.combined_sumcheck, + sumfold_output.final_round_sumcheck_claim(), + field_cfg, + )?; + + let folded_public = + verify_fold_publics_phase(&publics, sumfold_output.eq_instance_weights(), field_cfg)?; + + let subclaim = verify_endpoint_multipoint_phase( + transcript, + proof, + &folded_public, + &row_output, + &r_ic, + &a, + &lambda, + &rho, + &xi, + &booleanity_sources, + field_cfg, + )?; + + verify_pcs_phase::( + transcript, + &vs.pcs_params, + &folded_commitments, + &subclaim.sumcheck_subclaim.point, + &proof.witness_lifted_evals, + &proof.opening_proof, + field_cfg, + )?; + + Ok(FoldedLinearIdealInstance { + target: sumfold_output.final_round_sumcheck_claim().clone(), + commitments: folded_commitments, + public: folded_public, + }) +} + +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "verify", phase = "public_projection", instances = instances.len()) +)] +fn verify_public_projection_phase( + vs: &VerifiedLinearIdealFoldSetup, + instances: &[UairInstance<'_, Zt::Int, Zt::Int, PCSCommitments, D>], + proof: &ProductionLinearIdealFoldProof, + transcript: &mut impl Transcript, +) -> Result>, ProductionShaError> +where + U: Uair + ProductionShaProjectionAdapter, + Zt: ZincTypes, + F: PrimeField + FromPrimitiveWithConfig, + F::Inner: Transcribable, + F::Modulus: Transcribable, + P: ZincPCSTypes, +{ + let field_cfg = &vs.field_cfg; let mut publics = Vec::with_capacity(instances.len()); for (instance_idx, instance) in instances.iter().enumerate() { validate_public_uair_trace_shape::( @@ -1919,149 +2041,238 @@ where &proof.instance_commitments, ); absorb_projected_sha_publics(transcript, &publics, field_cfg); - if let Some(t) = timings.as_mut() { - t.public_projection = public_projection_start.elapsed(); - } - - let booleanity_sources = production_sha_booleanity_sources(); - - let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); - let beta = sample_instance_batch_challenge(transcript, instances.len(), field_cfg)?; + Ok(publics) +} - let aggregate_ideal_start = std::time::Instant::now(); - let aggregate_ideal_polys = aggregate_sha_ideal_polys_from_proof(&proof.ideal_check)?; +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "verify", phase = "aggregate_ideal_verify") +)] +fn verify_aggregate_ideal_phase( + proof: &IdealCheckProof, + transcript: &mut impl Transcript, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField + DelayedFieldProductSum, + F::Inner: Transcribable, + F::Modulus: Transcribable, +{ + let aggregate_ideal_polys = aggregate_sha_ideal_polys_from_proof(proof)?; check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, field_cfg)?; absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys, field_cfg); let (a, lambda, rho, xi) = sample_post_aggregate_ideal_challenges(transcript, field_cfg); let initial_claim = evaluate_aggregate_sha_ideal_claim(&aggregate_ideal_polys, &a, &lambda, field_cfg)?; - if let Some(t) = timings.as_mut() { - t.aggregate_ideal_verify = aggregate_ideal_start.elapsed(); - } + Ok((aggregate_ideal_polys, a, lambda, rho, xi, initial_claim)) +} - let sumfold_verify_start = std::time::Instant::now(); - let verified_sumfold = verify_full_sha_sumfold( - transcript, - &proof.sumfold_proof, - &initial_claim, - beta.len(), - field_cfg, - )?; - let sumfold_output = derive_instance_fold_claim( - &beta, +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "verify", phase = "sumfold_verify", instance_vars, instances) +)] +fn verify_sumfold_phase( + transcript: &mut impl Transcript, + proof: &MultiDegreeSumcheckProof, + initial_claim: &F, + beta: &[F], + instance_vars: usize, + instances: usize, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, +{ + let verified_sumfold = + verify_full_sha_sumfold(transcript, proof, initial_claim, instance_vars, field_cfg)?; + Ok(derive_instance_fold_claim( + beta, verified_sumfold.r_b, verified_sumfold.c_sf, - instances.len(), + instances, field_cfg, - )?; - if let Some(t) = timings.as_mut() { - t.sumfold_verify = sumfold_verify_start.elapsed(); - } + )?) +} - let fold_commitments_start = std::time::Instant::now(); - let folded_commitments = fold_pcs_commitments::( - &proof.instance_commitments, - sumfold_output.eq_instance_weights(), - field_cfg, - )?; - if let Some(t) = timings.as_mut() { - t.fold_pcs_commitments = fold_commitments_start.elapsed(); - } - absorb_production_sha_commitments::( - transcript, - b"production_sha_derived_folded_commitments", - std::slice::from_ref(&folded_commitments), - ); +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "verify", phase = "fold_after_sumfold", instances = commitments.len()) +)] +fn verify_fold_commitments_phase( + commitments: &[PCSCommitments], + weights: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + Zt: ZincTypes, + F: PrimeField, + F::Inner: Transcribable, + F::Modulus: Transcribable, + P: ZincPCSTypes, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + fold_pcs_commitments::(commitments, weights, field_cfg) +} - let row_sumcheck_start = std::time::Instant::now(); - let row_output = verify_folded_row_sumcheck( - transcript, - &proof.combined_sumcheck, - sumfold_output.final_round_sumcheck_claim(), - field_cfg, - )?; - if let Some(t) = timings.as_mut() { - t.row_sumcheck_verify = row_sumcheck_start.elapsed(); - } +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "verify", phase = "row_sumcheck_verify") +)] +fn verify_row_sumcheck_phase( + transcript: &mut impl Transcript, + proof: &MultiDegreeSumcheckProof, + final_round_sumcheck_claim: &F, + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, +{ + verify_folded_row_sumcheck(transcript, proof, final_round_sumcheck_claim, field_cfg) +} - let fold_publics_start = std::time::Instant::now(); - let folded_public = - fold_projected_publics(&publics, sumfold_output.eq_instance_weights(), field_cfg)?; - if let Some(t) = timings.as_mut() { - t.fold_projected_publics = fold_publics_start.elapsed(); - } +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "verify", phase = "fold_after_sumfold", instances = publics.len()) +)] +fn verify_fold_publics_phase( + publics: &[ProjectedPublic], + weights: &[F], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: PrimeField, +{ + fold_projected_publics(publics, weights, field_cfg) +} - let resolver_terminal_start = std::time::Instant::now(); +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "verify", phase = "endpoint_multipoint_verify") +)] +#[allow(clippy::too_many_arguments)] +fn verify_endpoint_multipoint_phase( + transcript: &mut impl Transcript, + proof: &ProductionLinearIdealFoldProof, + folded_public: &ProjectedPublic, + row_output: &FoldedRowSumcheckOutput, + r_ic: &[F; SHA_ROW_VARS], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + Zt: ZincTypes, + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, + P: ZincPCSTypes, +{ absorb_sha_resolver_proof(transcript, &proof.resolver, field_cfg); - let endpoint_evals = sha_endpoint_evals_from_resolver(&proof.resolver, &a, field_cfg)?; + let endpoint_evals = sha_endpoint_evals_from_resolver(&proof.resolver, a, field_cfg)?; let terminal = reconstruct_folded_row_terminal_from_endpoints( &endpoint_evals, - &folded_public, - &r_ic, + folded_public, + r_ic, &row_output.r_star, - &a, - &lambda, - &rho, - &xi, - &booleanity_sources, + a, + lambda, + rho, + xi, + booleanity_sources, field_cfg, )?; - verify_folded_row_terminal_value(&row_output, &terminal)?; - if let Some(t) = timings.as_mut() { - t.resolver_terminal_verify = resolver_terminal_start.elapsed(); - } + verify_folded_row_terminal_value(row_output, &terminal)?; - let multipoint_start = std::time::Instant::now(); let (subclaim, shift_specs) = verify_sha_endpoint_multipoint( transcript, &proof.multipoint_eval, &endpoint_evals, - &folded_public, + folded_public, &row_output.r_star, field_cfg, )?; - if let Some(t) = timings.as_mut() { - t.multipoint_verify = multipoint_start.elapsed(); - } - - let open_eval_start = std::time::Instant::now(); let open_evals = multipoint_open_evals_from_pcs_lifted( &proof.witness_lifted_evals, &production_sha_multipoint_layout(), - &folded_public, + folded_public, &subclaim.sumcheck_subclaim.point, field_cfg, )?; verify_sha_endpoint_multipoint_open_evals(&subclaim, &open_evals, &shift_specs, field_cfg)?; - if let Some(t) = timings.as_mut() { - t.multipoint_open_eval_checks = open_eval_start.elapsed(); - } - - let lifted_absorb_start = std::time::Instant::now(); - absorb_folded_lifted_evals(transcript, &proof.witness_lifted_evals, field_cfg); - if let Some(t) = timings.as_mut() { - t.lifted_eval_absorb = lifted_absorb_start.elapsed(); - } + Ok(subclaim) +} - let pcs_verify_start = std::time::Instant::now(); +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "verify", phase = "pcs_verify") +)] +fn verify_pcs_phase( + transcript: &mut impl Transcript, + pcs_params: &PCSVerifierParams, + folded_commitments: &PCSCommitments, + point: &[F], + witness_lifted_evals: &[DynamicPolynomialF], + opening_proof: &PCSOpeningProof, + field_cfg: &F::Config, +) -> Result<(), ProductionShaError> +where + Zt: ZincTypes, + F: PrimeField, + F::Inner: Transcribable, + F::Modulus: Transcribable, + P: ZincPCSTypes, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + absorb_folded_lifted_evals(transcript, witness_lifted_evals, field_cfg); verify_production_sha_pcs_opening::( - &vs.pcs_params, - &folded_commitments, - &subclaim.sumcheck_subclaim.point, - &proof.witness_lifted_evals, - &proof.opening_proof, + pcs_params, + folded_commitments, + point, + witness_lifted_evals, + opening_proof, field_cfg, - )?; - if let Some(t) = timings.as_mut() { - t.pcs_verify = pcs_verify_start.elapsed(); - } - - Ok(FoldedLinearIdealInstance { - target: sumfold_output.final_round_sumcheck_claim().clone(), - commitments: folded_commitments, - public: folded_public, - }) + ) } fn validate_public_uair_trace_shape( @@ -6758,73 +6969,6 @@ mod tests { )); } - #[test] - fn linear_ideal_fold_timing_apis_smoke_test_with_hyrax() { - type C = ark_bn254::G1Affine; - type P = AllHyraxPCSTypes; - type U = Sha256CompressionSliceUair; - - let field_cfg = fixed_prime::field_cfg_from_curve_scalar::, C>(); - let initial_state = SHA256_INITIAL_STATE; - let message = vec!["hello world"; 40].join(" "); - let message_blocks = sha256_padded_message_blocks::<8>(message.as_bytes()) - .expect("test message should canonically pad to 8 SHA-256 blocks"); - let (witnesses, _final_state) = - synthesize_sha256_chain_witnesses::(initial_state, message_blocks) - .expect("SHA-256 UAIR witnesses synthesize"); - let shape = UairShape::::new(SHA_ROW_VARS); - let (pcs_params, pcs_verifier_params) = all_hyrax_test_pcs_params::(); - let pp = - LinearIdealFoldProverParams::::new( - pcs_params, - field_cfg.clone(), - 3, - ); - let vs = setup_verify_linear_ideal_fold::( - LinearIdealFoldVerifierParams::new(pcs_verifier_params, field_cfg), - shape.clone(), - ) - .expect("production SHA verifier setup succeeds"); - - let mut prover_transcript = Blake3Transcript::new(); - let (output, prove_timings) = prove_linear_ideal_fold_with_timings::< - P, - U, - TestShaZincTypes, - F, - TEST_DEGREE_PLUS_ONE, - >(&pp, &shape, &witnesses, &mut prover_transcript) - .expect("production SHA timed ProjectionFold proof succeeds"); - - let mut verifier_transcript = Blake3Transcript::new(); - let (verified, verify_timings) = verify_linear_ideal_fold_with_timings::< - P, - U, - TestShaZincTypes, - F, - TEST_DEGREE_PLUS_ONE, - >( - &vs, - &output.fresh_instances, - &output.proof, - &mut verifier_transcript, - ) - .expect("production SHA timed ProjectionFold proof verifies"); - - assert!(prove_timings.total() > Duration::ZERO); - assert!(verify_timings.total() > Duration::ZERO); - assert_eq!(verified.target, output.folded_instance.target); - assert_eq!(verified.public, output.folded_instance.public); - assert!(pcs_commitments_match::< - P, - TestShaZincTypes, - F, - TEST_DEGREE_PLUS_ONE, - >( - &verified.commitments, &output.folded_instance.commitments - )); - } - #[test] fn optimized_sumfold_claim_feeds_folded_row_sumcheck_with_tail_for_eight_sha_instances() { type U = Sha256CompressionSliceUair; From 53a2dcf74a643f2f0687ea10db577aaac938561e Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 21:25:02 -0700 Subject: [PATCH 29/49] Trace folded 4x prover phases --- protocol/benches/e2e.rs | 23 + protocol/src/prover.rs | 997 +++++++++++++++++++++++----------------- 2 files changed, 586 insertions(+), 434 deletions(-) diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index d8082bb6..98921a72 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -2547,6 +2547,29 @@ fn do_bench_e2e_folded_4x( let sig = U::signature(); let public_trace = trace.public(&sig); + eprintln!(" Folded 4× tracing ({params}):"); + let subscriber = tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_target(true) + .with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE) + .finish(); + tracing::subscriber::with_default(subscriber, || { + let (_traced_proof, _traced_timings) = + zinc_protocol::prover::prove_folded_4x_with_timings::< + ZtF, + U, + F, + DEGREE_PLUS_ONE, + HALF_DEGREE_PLUS_ONE, + QUARTER_DEGREE_PLUS_ONE, + EC_FP_INT_LIMBS, + INT_QUARTER_LIMBS_BENCH, + false, + PERFORM_CHECKS, + >(pp, trace, num_vars, project_scalar) + .expect("Folded 4× traced prover failed"); + }); + group.bench_function(BenchmarkId::new("Verify (folded 4×)", ¶ms), |bench| { bench.iter_batched( || proof.clone(), diff --git a/protocol/src/prover.rs b/protocol/src/prover.rs index 294d2a65..a2be0524 100644 --- a/protocol/src/prover.rs +++ b/protocol/src/prover.rs @@ -1847,6 +1847,29 @@ impl FoldedProveTimings { } } +#[tracing::instrument( + target = "zinc_protocol::prover", + level = "info", + skip_all, + fields( + side = "prove", + prover = "folded_4x", + phase = _phase, + num_vars = _num_vars, + mle_first = _mle_first, + check_for_overflow = _check_for_overflow + ) +)] +fn trace_folded_4x_prover_phase( + _phase: &'static str, + _num_vars: usize, + _mle_first: bool, + _check_for_overflow: bool, + run: impl FnOnce() -> T, +) -> T { + run() +} + pub fn prove_folded_4x< ZtF, U, @@ -1984,7 +2007,10 @@ where None, )?; - let (_compressed, dt) = zip_plus::utils::serialize_and_compress(&proof); + let (_compressed, dt) = + trace_folded_4x_prover_phase("compress", num_vars, MLE_FIRST, CHECK_FOR_OVERFLOW, || { + zip_plus::utils::serialize_and_compress(&proof) + }); timings.step8_compress = dt; Ok((proof, timings)) @@ -2126,27 +2152,9 @@ where // ── Step 0: Commit (twice-split binary, arb, quartered int) ───────── let _t_step0 = std::time::Instant::now(); - let split1: Vec>> = - zip_plus::pcs::folding::split_columns::(&witness_trace.binary_poly); - let split_binary_witness: Vec>> = - zip_plus::pcs::folding::split_columns::(&split1); - drop(split1); - let split_int_witness: Vec>> = - zip_plus::pcs::folding::split_int_columns_4x::( - &witness_trace.int, - ); - - // Shared-Merkle dispatch (same criterion as the 1× int-fold path): - // ≥2 non-empty batches AND arb is empty / has matching codeword. - let arb_compatible = witness_trace.arbitrary_poly.is_empty() - || pp_arb.linear_code.codeword_len() == pp_bin_split2.linear_code.codeword_len(); - let bin_nonempty = !split_binary_witness.is_empty(); - let int_nonempty = !split_int_witness.is_empty(); - let arb_nonempty = !witness_trace.arbitrary_poly.is_empty(); - let nonempty_count = (bin_nonempty as u8) + (arb_nonempty as u8) + (int_nonempty as u8); - let use_multi = nonempty_count >= 2 && arb_compatible; - let ( + split_binary_witness, + split_int_witness, hint_bin_split, hint_arb, hint_int_split, @@ -2154,53 +2162,106 @@ where commitment_bin, commitment_arb, commitment_int, - ) = if use_multi { - let (multi, comm_bin, comm_arb, comm_int) = MultiZip3::< - ZtF::BinaryZt, - ZtF::ArbitraryZt, - ZtF::IntZt, - ZtF::BinaryLc, - ZtF::ArbitraryLc, - ZtF::IntLc, - >::commit( - pp_bin_split2, - pp_arb, - pp_int_split4, - &split_binary_witness, - &witness_trace.arbitrary_poly, - &split_int_witness, - )?; - (None, None, None, Some(multi), comm_bin, comm_arb, comm_int) - } else { - let (res_bin, (res_arb, res_int)) = cfg_join!( - commit_optionally(pp_bin_split2, &split_binary_witness), - commit_optionally(pp_arb, &witness_trace.arbitrary_poly), - commit_optionally(pp_int_split4, &split_int_witness), + mut pcs_transcript, + ) = trace_folded_4x_prover_phase("commit", num_vars, MLE_FIRST, CHECK_FOR_OVERFLOW, || { + let split1: Vec>> = + zip_plus::pcs::folding::split_columns::(&witness_trace.binary_poly); + let split_binary_witness: Vec>> = + zip_plus::pcs::folding::split_columns::(&split1); + drop(split1); + let split_int_witness: Vec>> = + zip_plus::pcs::folding::split_int_columns_4x::( + &witness_trace.int, + ); + + // Shared-Merkle dispatch (same criterion as the 1× int-fold path): + // ≥2 non-empty batches AND arb is empty / has matching codeword. + let arb_compatible = witness_trace.arbitrary_poly.is_empty() + || pp_arb.linear_code.codeword_len() == pp_bin_split2.linear_code.codeword_len(); + let bin_nonempty = !split_binary_witness.is_empty(); + let int_nonempty = !split_int_witness.is_empty(); + let arb_nonempty = !witness_trace.arbitrary_poly.is_empty(); + let nonempty_count = (bin_nonempty as u8) + (arb_nonempty as u8) + (int_nonempty as u8); + let use_multi = nonempty_count >= 2 && arb_compatible; + + let ( + hint_bin_split, + hint_arb, + hint_int_split, + multi_hint, + commitment_bin, + commitment_arb, + commitment_int, + ) = if use_multi { + let (multi, comm_bin, comm_arb, comm_int) = MultiZip3::< + ZtF::BinaryZt, + ZtF::ArbitraryZt, + ZtF::IntZt, + ZtF::BinaryLc, + ZtF::ArbitraryLc, + ZtF::IntLc, + >::commit( + pp_bin_split2, + pp_arb, + pp_int_split4, + &split_binary_witness, + &witness_trace.arbitrary_poly, + &split_int_witness, + )?; + (None, None, None, Some(multi), comm_bin, comm_arb, comm_int) + } else { + let (res_bin, (res_arb, res_int)) = cfg_join!( + commit_optionally(pp_bin_split2, &split_binary_witness), + commit_optionally(pp_arb, &witness_trace.arbitrary_poly), + commit_optionally(pp_int_split4, &split_int_witness), + ); + let (hb, cb) = res_bin?; + let (ha, ca) = res_arb?; + let (hi, ci) = res_int?; + (hb, ha, hi, None, cb, ca, ci) + }; + + let mut pcs_transcript = PcsProverTranscript::new_from_commitments( + [&commitment_bin, &commitment_arb, &commitment_int].into_iter(), ); - let (hb, cb) = res_bin?; - let (ha, ca) = res_arb?; - let (hi, ci) = res_int?; - (hb, ha, hi, None, cb, ca, ci) - }; - let mut pcs_transcript = PcsProverTranscript::new_from_commitments( - [&commitment_bin, &commitment_arb, &commitment_int].into_iter(), - ); + absorb_public_columns(&mut pcs_transcript.fs_transcript, &public_trace.binary_poly); + absorb_public_columns( + &mut pcs_transcript.fs_transcript, + &public_trace.arbitrary_poly, + ); + absorb_public_columns(&mut pcs_transcript.fs_transcript, &public_trace.int); - absorb_public_columns(&mut pcs_transcript.fs_transcript, &public_trace.binary_poly); - absorb_public_columns( - &mut pcs_transcript.fs_transcript, - &public_trace.arbitrary_poly, - ); - absorb_public_columns(&mut pcs_transcript.fs_transcript, &public_trace.int); + Ok::<_, ProtocolError>(( + split_binary_witness, + split_int_witness, + hint_bin_split, + hint_arb, + hint_int_split, + multi_hint, + commitment_bin, + commitment_arb, + commitment_int, + pcs_transcript, + )) + })?; if let Some(t) = timings.as_mut() { t.step0_commit = _t_step0.elapsed(); } // ── Step 1: Prime projection ──────────────────────────────────────── let _t_step1 = std::time::Instant::now(); - let field_cfg = crate::fixed_prime::secp256k1_field_cfg::(); - let projected_scalars_fx = project_scalars::(|s| project_scalar(s, &field_cfg)); + let (field_cfg, projected_scalars_fx) = trace_folded_4x_prover_phase( + "prime_projection", + num_vars, + MLE_FIRST, + CHECK_FOR_OVERFLOW, + || { + let field_cfg = crate::fixed_prime::secp256k1_field_cfg::(); + let projected_scalars_fx = project_scalars::(|s| project_scalar(s, &field_cfg)); + (field_cfg, projected_scalars_fx) + }, + ); if let Some(t) = timings.as_mut() { t.step1_prime_projection = _t_step1.elapsed(); } @@ -2208,50 +2269,72 @@ where // ── Step 2: Ideal check ───────────────────────────────────────────── let _t_step2 = std::time::Instant::now(); let num_constraints = count_constraints::(); - let (ic_proof, ic_prover_state, projected_trace) = if MLE_FIRST { - let mask = zinc_uair::degree_counter::linear_constraint_mask::(); - let ideals = zinc_uair::ideal_collector::collect_ideals::(num_constraints).ideals; - let (mut any_linear, mut any_nonlinear) = (false, false); - for (m, i) in mask.iter().zip(ideals.iter()) { - if i.is_zero_ideal() { - continue; - } - if *m { - any_linear = true + let (ic_proof, ic_prover_state, projected_trace) = trace_folded_4x_prover_phase( + "ideal_check", + num_vars, + MLE_FIRST, + CHECK_FOR_OVERFLOW, + || { + let out = if MLE_FIRST { + let mask = zinc_uair::degree_counter::linear_constraint_mask::(); + let ideals = + zinc_uair::ideal_collector::collect_ideals::(num_constraints).ideals; + let (mut any_linear, mut any_nonlinear) = (false, false); + for (m, i) in mask.iter().zip(ideals.iter()) { + if i.is_zero_ideal() { + continue; + } + if *m { + any_linear = true + } else { + any_nonlinear = true + } + } + match (any_linear, any_nonlinear) { + (true, false) => { + let projected_trace_cm = + project_trace_coeffs_column_major(trace, &field_cfg); + let (p, s) = U::prove_linear( + &mut pcs_transcript.fs_transcript, + &projected_trace_cm, + &projected_scalars_fx, + num_constraints, + num_vars, + &field_cfg, + )?; + (p, s, ProjectedTrace::ColumnMajor(projected_trace_cm)) + } + (true, true) => { + let (rm, cm) = cfg_join!( + project_trace_coeffs_row_major::(trace, &field_cfg), + project_trace_coeffs_column_major(trace, &field_cfg), + ); + let (p, s) = U::prove_hybrid( + &mut pcs_transcript.fs_transcript, + &rm, + &cm, + &projected_scalars_fx, + num_constraints, + num_vars, + &field_cfg, + )?; + (p, s, ProjectedTrace::RowMajor(rm)) + } + (false, _) => { + let projected_trace_rm = + project_trace_coeffs_row_major::(trace, &field_cfg); + let (p, s) = U::prove_combined( + &mut pcs_transcript.fs_transcript, + &projected_trace_rm, + &projected_scalars_fx, + num_constraints, + num_vars, + &field_cfg, + )?; + (p, s, ProjectedTrace::RowMajor(projected_trace_rm)) + } + } } else { - any_nonlinear = true - } - } - match (any_linear, any_nonlinear) { - (true, false) => { - let projected_trace_cm = project_trace_coeffs_column_major(trace, &field_cfg); - let (p, s) = U::prove_linear( - &mut pcs_transcript.fs_transcript, - &projected_trace_cm, - &projected_scalars_fx, - num_constraints, - num_vars, - &field_cfg, - )?; - (p, s, ProjectedTrace::ColumnMajor(projected_trace_cm)) - } - (true, true) => { - let (rm, cm) = cfg_join!( - project_trace_coeffs_row_major::(trace, &field_cfg), - project_trace_coeffs_column_major(trace, &field_cfg), - ); - let (p, s) = U::prove_hybrid( - &mut pcs_transcript.fs_transcript, - &rm, - &cm, - &projected_scalars_fx, - num_constraints, - num_vars, - &field_cfg, - )?; - (p, s, ProjectedTrace::RowMajor(rm)) - } - (false, _) => { let projected_trace_rm = project_trace_coeffs_row_major::(trace, &field_cfg); let (p, s) = U::prove_combined( @@ -2263,20 +2346,10 @@ where &field_cfg, )?; (p, s, ProjectedTrace::RowMajor(projected_trace_rm)) - } - } - } else { - let projected_trace_rm = project_trace_coeffs_row_major::(trace, &field_cfg); - let (p, s) = U::prove_combined( - &mut pcs_transcript.fs_transcript, - &projected_trace_rm, - &projected_scalars_fx, - num_constraints, - num_vars, - &field_cfg, - )?; - (p, s, ProjectedTrace::RowMajor(projected_trace_rm)) - }; + }; + Ok::<_, ProtocolError>(out) + }, + )?; let ic_eval_point = ic_prover_state.evaluation_point; if let Some(t) = timings.as_mut() { t.step2_ideal_check = _t_step2.elapsed(); @@ -2284,173 +2357,210 @@ where // ── Step 3: Eval projection (ψ_a) ─────────────────────────────────── let _t_step3 = std::time::Instant::now(); - let projecting_element: ZtF::Chal = pcs_transcript.fs_transcript.get_challenge(); - let projecting_element_f: F = F::from_with_cfg(&projecting_element, &field_cfg); - - let projected_trace_f = - evaluate_trace_to_column_mles_fast(trace, &projecting_element_f, &field_cfg); - let projected_scalars_f = project_scalars_to_field(projected_scalars_fx, &projecting_element_f) - .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?; + let (projecting_element_f, projected_trace_f, projected_scalars_f) = + trace_folded_4x_prover_phase( + "eval_projection", + num_vars, + MLE_FIRST, + CHECK_FOR_OVERFLOW, + || { + let projecting_element: ZtF::Chal = pcs_transcript.fs_transcript.get_challenge(); + let projecting_element_f: F = F::from_with_cfg(&projecting_element, &field_cfg); + + let projected_trace_f = + evaluate_trace_to_column_mles_fast(trace, &projecting_element_f, &field_cfg); + let projected_scalars_f = + project_scalars_to_field(projected_scalars_fx, &projecting_element_f) + .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?; + + Ok::<_, ProtocolError>(( + projecting_element_f, + projected_trace_f, + projected_scalars_f, + )) + }, + )?; if let Some(t) = timings.as_mut() { t.step3_eval_projection = _t_step3.elapsed(); } // ── Step 4: CPR + booleanity multi-degree sumcheck ────────────────── let _t_step4 = std::time::Instant::now(); - let max_degree = count_max_degree::(); - let (cpr_group, cpr_ancillary) = CombinedPolyResolver::prepare_sumcheck_group::( - &mut pcs_transcript.fs_transcript, - projected_trace_f.clone(), - &ic_eval_point, - &projected_scalars_f, - num_constraints, - num_vars, - max_degree, - &field_cfg, - &trace.binary_poly, - &projecting_element_f, - )?; + let (cpr_proof, cpr_eval_point, combined_sumcheck, lookup_proof) = + trace_folded_4x_prover_phase("sumcheck", num_vars, MLE_FIRST, CHECK_FOR_OVERFLOW, || { + let max_degree = count_max_degree::(); + let (cpr_group, cpr_ancillary) = CombinedPolyResolver::prepare_sumcheck_group::( + &mut pcs_transcript.fs_transcript, + projected_trace_f.clone(), + &ic_eval_point, + &projected_scalars_f, + num_constraints, + num_vars, + max_degree, + &field_cfg, + &trace.binary_poly, + &projecting_element_f, + )?; - let num_pub_bin = uair_signature.public_cols().num_binary_poly_cols(); - let num_pub_int = uair_signature.public_cols().num_int_cols(); - let num_wit_int = uair_signature.witness_cols().num_int_cols(); - let int_offset = trace.binary_poly.len() + trace.arbitrary_poly.len(); - let int_bit_cols: Vec<_> = uair_signature - .int_witness_bit_cols() - .iter() - .map(|&idx| projected_trace_f[int_offset + idx].clone()) - .collect(); - let virtual_specs = uair_signature.virtual_booleanity_cols(); - let shifted_bit_slice_mles = if virtual_specs.is_empty() { - Vec::new() - } else { - build_shifted_bit_slice_mles::( - &trace.binary_poly[num_pub_bin..], - uair_signature.shifted_bit_slice_specs(), - &field_cfg, - ) - }; - let virtual_mles = if virtual_specs.is_empty() { - Vec::new() - } else { - let self_bit_slices = - compute_bit_slices_flat::(&trace.binary_poly[num_pub_bin..], &field_cfg); - let public_bit_slices = - compute_bit_slices_flat::(&trace.binary_poly[..num_pub_bin], &field_cfg); - let int_witness_cols: Vec<_> = (0..num_wit_int) - .map(|i| projected_trace_f[int_offset + num_pub_int + i].clone()) - .collect(); - build_virtual_booleanity_mles::( - &self_bit_slices, - &shifted_bit_slice_mles, - &public_bit_slices, - &int_witness_cols, - virtual_specs, - &field_cfg, - ) - }; - let mut extra_bit_cols = int_bit_cols; - extra_bit_cols.extend(virtual_mles); - let virtual_bp_specs = uair_signature.virtual_binary_poly_cols(); - let virtual_binary_mles = build_virtual_binary_poly_mles::( - &trace.binary_poly[num_pub_bin..], - &trace.binary_poly[..num_pub_bin], - uair_signature.shifted_bit_slice_specs(), - virtual_bp_specs, - ); - let kept_witness_binary = filter_booleanity_witness( - &trace.binary_poly[num_pub_bin..], - uair_signature.booleanity_skip_indices(), - ); - let booleanity_binary_cols: Vec<_> = kept_witness_binary - .iter() - .chain(virtual_binary_mles.iter()) - .cloned() - .collect(); - let bool_prep = prepare_booleanity_group::( - &mut pcs_transcript.fs_transcript, - &booleanity_binary_cols, - &extra_bit_cols, - &ic_eval_point, - &field_cfg, - ) - .map_err(ProtocolError::Booleanity)?; + let num_pub_bin = uair_signature.public_cols().num_binary_poly_cols(); + let num_pub_int = uair_signature.public_cols().num_int_cols(); + let num_wit_int = uair_signature.witness_cols().num_int_cols(); + let int_offset = trace.binary_poly.len() + trace.arbitrary_poly.len(); + let int_bit_cols: Vec<_> = uair_signature + .int_witness_bit_cols() + .iter() + .map(|&idx| projected_trace_f[int_offset + idx].clone()) + .collect(); + let virtual_specs = uair_signature.virtual_booleanity_cols(); + let shifted_bit_slice_mles = if virtual_specs.is_empty() { + Vec::new() + } else { + build_shifted_bit_slice_mles::( + &trace.binary_poly[num_pub_bin..], + uair_signature.shifted_bit_slice_specs(), + &field_cfg, + ) + }; + let virtual_mles = if virtual_specs.is_empty() { + Vec::new() + } else { + let self_bit_slices = + compute_bit_slices_flat::(&trace.binary_poly[num_pub_bin..], &field_cfg); + let public_bit_slices = + compute_bit_slices_flat::(&trace.binary_poly[..num_pub_bin], &field_cfg); + let int_witness_cols: Vec<_> = (0..num_wit_int) + .map(|i| projected_trace_f[int_offset + num_pub_int + i].clone()) + .collect(); + build_virtual_booleanity_mles::( + &self_bit_slices, + &shifted_bit_slice_mles, + &public_bit_slices, + &int_witness_cols, + virtual_specs, + &field_cfg, + ) + }; + let mut extra_bit_cols = int_bit_cols; + extra_bit_cols.extend(virtual_mles); + let virtual_bp_specs = uair_signature.virtual_binary_poly_cols(); + let virtual_binary_mles = build_virtual_binary_poly_mles::( + &trace.binary_poly[num_pub_bin..], + &trace.binary_poly[..num_pub_bin], + uair_signature.shifted_bit_slice_specs(), + virtual_bp_specs, + ); + let kept_witness_binary = filter_booleanity_witness( + &trace.binary_poly[num_pub_bin..], + uair_signature.booleanity_skip_indices(), + ); + let booleanity_binary_cols: Vec<_> = kept_witness_binary + .iter() + .chain(virtual_binary_mles.iter()) + .cloned() + .collect(); + let bool_prep = prepare_booleanity_group::( + &mut pcs_transcript.fs_transcript, + &booleanity_binary_cols, + &extra_bit_cols, + &ic_eval_point, + &field_cfg, + ) + .map_err(ProtocolError::Booleanity)?; - let mut groups = vec![cpr_group]; - let mut bool_ancillary_opt = None; - if let Some((bg, ba)) = bool_prep { - groups.push(bg); - bool_ancillary_opt = Some(ba); - } + let mut groups = vec![cpr_group]; + let mut bool_ancillary_opt = None; + if let Some((bg, ba)) = bool_prep { + groups.push(bg); + bool_ancillary_opt = Some(ba); + } - let (combined_sumcheck, mut md_states) = MultiDegreeSumcheck::prove_as_subprotocol( - &mut pcs_transcript.fs_transcript, - groups, - num_vars, - &field_cfg, - ); - let cpr_state = md_states.remove(0); - let (mut cpr_proof, cpr_prover_state) = CombinedPolyResolver::finalize_prover( - &mut pcs_transcript.fs_transcript, - cpr_state, - cpr_ancillary, - &field_cfg, - )?; - let shifted_bit_slice_evals: Vec = if shifted_bit_slice_mles.is_empty() { - compute_shifted_bit_slice_evals_streaming::( - &trace.binary_poly[num_pub_bin..], - uair_signature.shifted_bit_slice_specs(), - &cpr_prover_state.evaluation_point, - &field_cfg, - ) - .map_err(|e| ProtocolError::Booleanity(e.into()))? - } else { - shifted_bit_slice_mles - .into_iter() - .map(|mle| mle.evaluate_with_config(&cpr_prover_state.evaluation_point, &field_cfg)) - .collect::, _>>() - .map_err(ProtocolError::ShiftedBitSliceEval)? - }; - cpr_proof.shifted_bit_slice_evals = shifted_bit_slice_evals; - if let Some(ba) = bool_ancillary_opt { - let bool_state = md_states.remove(0); - let bit_slice_evals = finalize_booleanity_prover( - &mut pcs_transcript.fs_transcript, - bool_state, - ba, - &field_cfg, - ) - .map_err(ProtocolError::Booleanity)?; - cpr_proof.bit_slice_evals = bit_slice_evals; - } - let lookup_proof: Option> = None; + let (combined_sumcheck, mut md_states) = MultiDegreeSumcheck::prove_as_subprotocol( + &mut pcs_transcript.fs_transcript, + groups, + num_vars, + &field_cfg, + ); + let cpr_state = md_states.remove(0); + let (mut cpr_proof, cpr_prover_state) = CombinedPolyResolver::finalize_prover( + &mut pcs_transcript.fs_transcript, + cpr_state, + cpr_ancillary, + &field_cfg, + )?; + let shifted_bit_slice_evals: Vec = if shifted_bit_slice_mles.is_empty() { + compute_shifted_bit_slice_evals_streaming::( + &trace.binary_poly[num_pub_bin..], + uair_signature.shifted_bit_slice_specs(), + &cpr_prover_state.evaluation_point, + &field_cfg, + ) + .map_err(|e| ProtocolError::Booleanity(e.into()))? + } else { + shifted_bit_slice_mles + .into_iter() + .map(|mle| { + mle.evaluate_with_config(&cpr_prover_state.evaluation_point, &field_cfg) + }) + .collect::, _>>() + .map_err(ProtocolError::ShiftedBitSliceEval)? + }; + cpr_proof.shifted_bit_slice_evals = shifted_bit_slice_evals; + if let Some(ba) = bool_ancillary_opt { + let bool_state = md_states.remove(0); + let bit_slice_evals = finalize_booleanity_prover( + &mut pcs_transcript.fs_transcript, + bool_state, + ba, + &field_cfg, + ) + .map_err(ProtocolError::Booleanity)?; + cpr_proof.bit_slice_evals = bit_slice_evals; + } + let lookup_proof: Option> = None; + + Ok::<_, ProtocolError>(( + cpr_proof, + cpr_prover_state.evaluation_point, + combined_sumcheck, + lookup_proof, + )) + })?; if let Some(t) = timings.as_mut() { t.step4_sumcheck = _t_step4.elapsed(); } // ── Step 5: Multi-point evaluation sumcheck ───────────────────────── let _t_step5 = std::time::Instant::now(); - let cpr_eval_point = cpr_prover_state.evaluation_point.clone(); - let bit_op_mles = zinc_piop::combined_poly_resolver::build_bit_op_mles::( - &trace.binary_poly, - uair_signature.bit_op_specs(), - uair_signature.total_cols().num_binary_poly_cols(), - &projecting_element_f, + let (mp_proof, r_0) = trace_folded_4x_prover_phase( + "multipoint_eval", num_vars, - &field_cfg, - ); - let mut sources = projected_trace_f.clone(); - sources.extend(bit_op_mles); - let mut up_evals_with_bit_op = cpr_proof.up_evals.clone(); - up_evals_with_bit_op.extend(cpr_proof.bit_op_down_evals.iter().cloned()); - let (mp_proof, r_0) = prove_multipoint_reduction( - &mut pcs_transcript.fs_transcript, - &sources, - &cpr_eval_point, - &up_evals_with_bit_op, - &cpr_proof.down_evals, - uair_signature.shifts(), - &field_cfg, + MLE_FIRST, + CHECK_FOR_OVERFLOW, + || { + let bit_op_mles = zinc_piop::combined_poly_resolver::build_bit_op_mles::( + &trace.binary_poly, + uair_signature.bit_op_specs(), + uair_signature.total_cols().num_binary_poly_cols(), + &projecting_element_f, + num_vars, + &field_cfg, + ); + let mut sources = projected_trace_f.clone(); + sources.extend(bit_op_mles); + let mut up_evals_with_bit_op = cpr_proof.up_evals.clone(); + up_evals_with_bit_op.extend(cpr_proof.bit_op_down_evals.iter().cloned()); + let (mp_proof, r_0) = prove_multipoint_reduction( + &mut pcs_transcript.fs_transcript, + &sources, + &cpr_eval_point, + &up_evals_with_bit_op, + &cpr_proof.down_evals, + uair_signature.shifts(), + &field_cfg, + )?; + + Ok::<_, ProtocolError>((mp_proof, r_0)) + }, )?; if let Some(t) = timings.as_mut() { t.step5_multipoint_eval = _t_step5.elapsed(); @@ -2461,207 +2571,226 @@ where // the int section is replaced below with 4-coeff bar_us, so the // standard 1-coeff int compute would be wasted work. let _t_step6 = std::time::Instant::now(); - let total_cols = uair_signature.total_cols(); - let num_total_bin = total_cols.num_binary_poly_cols(); - let num_total_arb = total_cols.num_arbitrary_poly_cols(); - let mut lifted_evals = crate::compute_lifted_evals_capped::( - &r_0, - &trace.binary_poly, - &projected_trace, - &field_cfg, - Some(num_total_arb), + let (num_total_bin, num_total_arb, lifted_evals, gamma1, gamma2) = trace_folded_4x_prover_phase( + "lift_and_project", + num_vars, + MLE_FIRST, + CHECK_FOR_OVERFLOW, + || { + let total_cols = uair_signature.total_cols(); + let num_total_bin = total_cols.num_binary_poly_cols(); + let num_total_arb = total_cols.num_arbitrary_poly_cols(); + let mut lifted_evals = crate::compute_lifted_evals_capped::( + &r_0, + &trace.binary_poly, + &projected_trace, + &field_cfg, + Some(num_total_arb), + ); + let int_lifted_evals_4coeff: Vec> = + crate::compute_int_fold_4x_lifted_evals::( + &r_0, &trace.int, &field_cfg, + ); + // Append the 4-coeff int section. + lifted_evals.extend(int_lifted_evals_4coeff); + let _int_section_offset = num_total_bin + num_total_arb; + + let mut transcription_buf: Vec = vec![0; F::Inner::NUM_BYTES]; + for bar_u in &lifted_evals { + pcs_transcript + .fs_transcript + .absorb_random_field_slice(&bar_u.coeffs, &mut transcription_buf); + } + let gamma1: F = { + let g_chal: ZtF::Chal = pcs_transcript.fs_transcript.get_challenge(); + F::from_with_cfg(&g_chal, &field_cfg) + }; + let gamma2: F = { + let g_chal: ZtF::Chal = pcs_transcript.fs_transcript.get_challenge(); + F::from_with_cfg(&g_chal, &field_cfg) + }; + + (num_total_bin, num_total_arb, lifted_evals, gamma1, gamma2) + }, ); - let int_lifted_evals_4coeff: Vec> = - crate::compute_int_fold_4x_lifted_evals::( - &r_0, &trace.int, &field_cfg, - ); - // Append the 4-coeff int section. - lifted_evals.extend(int_lifted_evals_4coeff); - let _int_section_offset = num_total_bin + num_total_arb; - - let mut transcription_buf: Vec = vec![0; F::Inner::NUM_BYTES]; - for bar_u in &lifted_evals { - pcs_transcript - .fs_transcript - .absorb_random_field_slice(&bar_u.coeffs, &mut transcription_buf); - } - let gamma1: F = { - let g_chal: ZtF::Chal = pcs_transcript.fs_transcript.get_challenge(); - F::from_with_cfg(&g_chal, &field_cfg) - }; - let gamma2: F = { - let g_chal: ZtF::Chal = pcs_transcript.fs_transcript.get_challenge(); - F::from_with_cfg(&g_chal, &field_cfg) - }; if let Some(t) = timings.as_mut() { t.step6_lift_and_project = _t_step6.elapsed(); } // ── Step 7: PCS open ──────────────────────────────────────────────── let _t_step7 = std::time::Instant::now(); - let mut r0_ext = r_0.clone(); - r0_ext.push(gamma1); - r0_ext.push(gamma2); + trace_folded_4x_prover_phase("pcs_open", num_vars, MLE_FIRST, CHECK_FOR_OVERFLOW, || { + let mut r0_ext = r_0.clone(); + r0_ext.push(gamma1); + r0_ext.push(gamma2); - if let Some(multi) = &multi_hint { - if let Some(bd) = zip_breakdown.as_deref_mut() { - let _ = MultiZip3::< - ZtF::BinaryZt, - ZtF::ArbitraryZt, - ZtF::IntZt, - ZtF::BinaryLc, - ZtF::ArbitraryLc, - ZtF::IntLc, - >::prove_f_with_byte_breakdown::( - &mut pcs_transcript, - pp_bin_split2, - pp_arb, - pp_int_split4, - &split_binary_witness, - &witness_trace.arbitrary_poly, - &split_int_witness, - &r0_ext, - multi, - &field_cfg, - &mut bd.bin, - &mut bd.arb, - &mut bd.int, - )?; - } else { - let _ = MultiZip3::< - ZtF::BinaryZt, - ZtF::ArbitraryZt, - ZtF::IntZt, - ZtF::BinaryLc, - ZtF::ArbitraryLc, - ZtF::IntLc, - >::prove_f::( - &mut pcs_transcript, - pp_bin_split2, - pp_arb, - pp_int_split4, - &split_binary_witness, - &witness_trace.arbitrary_poly, - &split_int_witness, - &r0_ext, - multi, - &field_cfg, - )?; - } - } else { - if let Some(hint_bin) = &hint_bin_split { + if let Some(multi) = &multi_hint { if let Some(bd) = zip_breakdown.as_deref_mut() { - let _ = ZipPlus::::prove_f_with_byte_breakdown::< - _, - CHECK_FOR_OVERFLOW, - >( - &mut pcs_transcript, - pp_bin_split2, - &split_binary_witness, - &r0_ext, - hint_bin, - &field_cfg, - &mut bd.bin, - )?; - } else { - let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( + let _ = MultiZip3::< + ZtF::BinaryZt, + ZtF::ArbitraryZt, + ZtF::IntZt, + ZtF::BinaryLc, + ZtF::ArbitraryLc, + ZtF::IntLc, + >::prove_f_with_byte_breakdown::( &mut pcs_transcript, pp_bin_split2, - &split_binary_witness, - &r0_ext, - hint_bin, - &field_cfg, - )?; - } - } - if let Some(hint_arb) = &hint_arb { - if let Some(bd) = zip_breakdown.as_deref_mut() { - let _ = ZipPlus::::prove_f_with_byte_breakdown::< - _, - CHECK_FOR_OVERFLOW, - >( - &mut pcs_transcript, pp_arb, - &witness_trace.arbitrary_poly, - &r_0, - hint_arb, - &field_cfg, - &mut bd.arb, - )?; - } else { - let _ = ZipPlus::::prove_f::< - _, - CHECK_FOR_OVERFLOW, - >( - &mut pcs_transcript, - pp_arb, - &witness_trace.arbitrary_poly, - &r_0, - hint_arb, - &field_cfg, - )?; - } - } - if let Some(hint_int) = &hint_int_split { - if let Some(bd) = zip_breakdown.as_deref_mut() { - let _ = ZipPlus::::prove_f_with_byte_breakdown::< - _, - CHECK_FOR_OVERFLOW, - >( - &mut pcs_transcript, pp_int_split4, + &split_binary_witness, + &witness_trace.arbitrary_poly, &split_int_witness, &r0_ext, - hint_int, + multi, &field_cfg, + &mut bd.bin, + &mut bd.arb, &mut bd.int, )?; } else { - let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( + let _ = MultiZip3::< + ZtF::BinaryZt, + ZtF::ArbitraryZt, + ZtF::IntZt, + ZtF::BinaryLc, + ZtF::ArbitraryLc, + ZtF::IntLc, + >::prove_f::( &mut pcs_transcript, + pp_bin_split2, + pp_arb, pp_int_split4, + &split_binary_witness, + &witness_trace.arbitrary_poly, &split_int_witness, &r0_ext, - hint_int, + multi, &field_cfg, )?; } + } else { + if let Some(hint_bin) = &hint_bin_split { + if let Some(bd) = zip_breakdown.as_deref_mut() { + let _ = ZipPlus::::prove_f_with_byte_breakdown::< + _, + CHECK_FOR_OVERFLOW, + >( + &mut pcs_transcript, + pp_bin_split2, + &split_binary_witness, + &r0_ext, + hint_bin, + &field_cfg, + &mut bd.bin, + )?; + } else { + let _ = + ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( + &mut pcs_transcript, + pp_bin_split2, + &split_binary_witness, + &r0_ext, + hint_bin, + &field_cfg, + )?; + } + } + if let Some(hint_arb) = &hint_arb { + if let Some(bd) = zip_breakdown.as_deref_mut() { + let _ = + ZipPlus::::prove_f_with_byte_breakdown::< + _, + CHECK_FOR_OVERFLOW, + >( + &mut pcs_transcript, + pp_arb, + &witness_trace.arbitrary_poly, + &r_0, + hint_arb, + &field_cfg, + &mut bd.arb, + )?; + } else { + let _ = ZipPlus::::prove_f::< + _, + CHECK_FOR_OVERFLOW, + >( + &mut pcs_transcript, + pp_arb, + &witness_trace.arbitrary_poly, + &r_0, + hint_arb, + &field_cfg, + )?; + } + } + if let Some(hint_int) = &hint_int_split { + if let Some(bd) = zip_breakdown.as_deref_mut() { + let _ = ZipPlus::::prove_f_with_byte_breakdown::< + _, + CHECK_FOR_OVERFLOW, + >( + &mut pcs_transcript, + pp_int_split4, + &split_int_witness, + &r0_ext, + hint_int, + &field_cfg, + &mut bd.int, + )?; + } else { + let _ = ZipPlus::::prove_f::<_, CHECK_FOR_OVERFLOW>( + &mut pcs_transcript, + pp_int_split4, + &split_int_witness, + &r0_ext, + hint_int, + &field_cfg, + )?; + } + } } - } + + Ok::<_, ProtocolError>(()) + })?; if let Some(t) = timings.as_mut() { t.step7_pcs_open = _t_step7.elapsed(); } // ── Assemble the proof ────────────────────────────────────────────── let _t_assembly = std::time::Instant::now(); - let zip_proof = pcs_transcript.stream.into_inner(); - let commitments = (commitment_bin, commitment_arb, commitment_int); - - let pub_cols = uair_signature.public_cols(); - let num_pub_bin = pub_cols.num_binary_poly_cols(); - let num_pub_arb = pub_cols.num_arbitrary_poly_cols(); - let num_pub_int = pub_cols.num_int_cols(); - let witness = uair_signature.witness_cols(); - let witness_arb_offset = add!(num_total_bin, num_pub_arb); - let witness_arb_end = add!(witness_arb_offset, witness.num_arbitrary_poly_cols()); - let witness_int_offset = add!(add!(num_total_bin, num_total_arb), num_pub_int); - let witness_lifted_evals: Vec<_> = lifted_evals[num_pub_bin..num_total_bin] - .iter() - .chain(&lifted_evals[witness_arb_offset..witness_arb_end]) - .chain(&lifted_evals[witness_int_offset..]) - .cloned() - .collect(); + let proof = + trace_folded_4x_prover_phase("assembly", num_vars, MLE_FIRST, CHECK_FOR_OVERFLOW, || { + let zip_proof = pcs_transcript.stream.into_inner(); + let commitments = (commitment_bin, commitment_arb, commitment_int); + + let pub_cols = uair_signature.public_cols(); + let num_pub_bin = pub_cols.num_binary_poly_cols(); + let num_pub_arb = pub_cols.num_arbitrary_poly_cols(); + let num_pub_int = pub_cols.num_int_cols(); + let witness = uair_signature.witness_cols(); + let witness_arb_offset = add!(num_total_bin, num_pub_arb); + let witness_arb_end = add!(witness_arb_offset, witness.num_arbitrary_poly_cols()); + let witness_int_offset = add!(add!(num_total_bin, num_total_arb), num_pub_int); + let witness_lifted_evals: Vec<_> = lifted_evals[num_pub_bin..num_total_bin] + .iter() + .chain(&lifted_evals[witness_arb_offset..witness_arb_end]) + .chain(&lifted_evals[witness_int_offset..]) + .cloned() + .collect(); - let proof = Proof { - commitments, - ideal_check: ic_proof, - resolver: cpr_proof, - combined_sumcheck, - multipoint_eval: mp_proof, - zip: zip_proof, - witness_lifted_evals, - lookup_proof, - }; + Proof { + commitments, + ideal_check: ic_proof, + resolver: cpr_proof, + combined_sumcheck, + multipoint_eval: mp_proof, + zip: zip_proof, + witness_lifted_evals, + lookup_proof, + } + }); if let Some(t) = timings.as_mut() { t.assembly = _t_assembly.elapsed(); } From 008199ad398546bcf1d0bc13164fda8837a6a16c Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 22:11:32 -0700 Subject: [PATCH 30/49] Refactor delayed reductions into reusable algorithms --- piop/src/combined_poly_resolver.rs | 24 +- piop/src/lookup/booleanity.rs | 17 +- piop/src/neutron_nova/accumulator.rs | 50 +-- piop/src/neutron_nova/booleanity.rs | 56 +-- piop/src/neutron_nova/linear_cpr.rs | 43 +-- piop/src/neutron_nova/projection_sha.rs | 31 +- protocol/src/lib.rs | 21 +- utils/src/delayed_reduction.rs | 484 ++++++++++++++++++++++-- utils/src/inner_product.rs | 50 ++- 9 files changed, 614 insertions(+), 162 deletions(-) diff --git a/piop/src/combined_poly_resolver.rs b/piop/src/combined_poly_resolver.rs index 0ca9d04d..13bc89ea 100644 --- a/piop/src/combined_poly_resolver.rs +++ b/piop/src/combined_poly_resolver.rs @@ -39,7 +39,10 @@ use zinc_transcript::traits::{ConstTranscribable, Transcript}; use zinc_uair::{BitOp, TraceRow, Uair, ideal::ImpossibleIdeal}; use zinc_utils::{ UNCHECKED, add, cfg_iter, - delayed_reduction::{DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs}, + delayed_reduction::{ + BarrettDelayedReduction, DelayedFieldProductSum, DelayedModularReductionAlgorithm, + MontgomeryLimbs, MontgomeryProductSum4, + }, from_ref::FromRef, inner_product::{FieldFieldInnerProduct, InnerProduct}, inner_transparent_field::InnerTransparentField, @@ -178,7 +181,8 @@ where let one = F::one_with_cfg(field_cfg); let alpha_powers: Vec = powers(projecting_element_f.clone(), one, 32); let eq_table = build_eq_x_r_vec(point, field_cfg)?; - let reduction_params = F::barrett_reduction_params(field_cfg); + let reducer = BarrettDelayedReduction::::new(field_cfg); + let product_sum = MontgomeryProductSum4::::new(field_cfg); let evals = cfg_iter!(bit_op_specs) .map(|spec| { @@ -200,23 +204,15 @@ where continue; } if let Some(dst_bit) = bit_op_destination(spec.op(), src_bit) { - as DelayedModularReduction>::add(&mut buckets[dst_bit], eq_b); + reducer.add(&mut buckets[dst_bit], eq_b); } } } - let bucket_evals: Vec = buckets - .into_iter() - .map(|acc| { - as DelayedModularReduction>::reduce( - acc, - field_cfg, - &reduction_params, - ) - }) - .collect(); + let bucket_evals: Vec = buckets.into_iter().map(|acc| reducer.reduce(acc)).collect(); - FieldFieldInnerProduct::inner_product::( + FieldFieldInnerProduct::inner_product_with_algorithm::( + &product_sum, &bucket_evals, &alpha_powers, zero.clone(), diff --git a/piop/src/lookup/booleanity.rs b/piop/src/lookup/booleanity.rs index 1ac1e007..617874c2 100644 --- a/piop/src/lookup/booleanity.rs +++ b/piop/src/lookup/booleanity.rs @@ -34,7 +34,10 @@ use zinc_uair::{ }; use zinc_utils::{ UNCHECKED, cfg_into_iter, cfg_iter, - delayed_reduction::{DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs}, + delayed_reduction::{ + BarrettDelayedReduction, DelayedFieldProductSum, DelayedModularReductionAlgorithm, + MontgomeryLimbs, + }, inner_product::{FieldFieldInnerProduct, InnerProduct}, inner_transparent_field::InnerTransparentField, powers, @@ -114,7 +117,7 @@ where return Ok(Vec::new()); } let eq_table = build_eq_x_r_vec(point, field_cfg)?; - let reduction_params = F::barrett_reduction_params(field_cfg); + let reducer = BarrettDelayedReduction::::new(field_cfg); let out: Vec = cfg_iter!(shifted_specs) .flat_map(|spec| { @@ -129,19 +132,13 @@ where let eq_t = &eq_table[t]; for (bit_idx, coeff) in bp.iter().enumerate() { if coeff.into_inner() { - as DelayedModularReduction>::add(&mut accs[bit_idx], eq_t); + reducer.add(&mut accs[bit_idx], eq_t); } } } } accs.into_iter() - .map(|acc| { - as DelayedModularReduction>::reduce( - acc, - field_cfg, - &reduction_params, - ) - }) + .map(|acc| reducer.reduce(acc)) .collect::>() }) .collect(); diff --git a/piop/src/neutron_nova/accumulator.rs b/piop/src/neutron_nova/accumulator.rs index 65d7b82b..a31432c5 100644 --- a/piop/src/neutron_nova/accumulator.rs +++ b/piop/src/neutron_nova/accumulator.rs @@ -10,22 +10,13 @@ use zinc_poly::{ use zinc_utils::{ UNCHECKED, delayed_reduction::{ - BarrettReductionParams, DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs, + BarrettDelayedReduction, DelayedFieldProductSum, DelayedModularReductionAlgorithm, + MontgomeryLimbs, MontgomeryProductSum4, }, - inner_product::{FieldFieldInnerProduct, InnerProduct}, + inner_product::FieldFieldInnerProduct, powers, }; -const DMR_FLUSH_ADDS: usize = 1 << 20; - -pub(crate) fn dmr_flush_adds(reduction_params: &BarrettReductionParams) -> usize { - if reduction_params.modulus[3] == 0 { - 1 - } else { - DMR_FLUSH_ADDS - } -} - /// Errors produced by NeutronNova row-space accumulation helpers. #[derive(Clone, Debug, Error)] pub enum AccumulatorError { @@ -90,13 +81,12 @@ impl RowWeights { /// DMR-backed bit buckets for one small-value binary-polynomial column. #[derive(Clone, Debug)] -pub struct SmallValueBitAccumulator<'a, F: PrimeField, const D: usize> { +pub struct SmallValueBitAccumulator<'a, F: MontgomeryLimbs, const D: usize> { buckets: [Uint<5>; D], lane_accs: [F; D], pending_adds: usize, - flush_adds: usize, field_cfg: &'a F::Config, - reduction_params: BarrettReductionParams, + reducer: BarrettDelayedReduction<'a, F>, } impl<'a, F, const D: usize> SmallValueBitAccumulator<'a, F, D> @@ -104,16 +94,14 @@ where F: MontgomeryLimbs + Send + Sync, { pub fn new(field_cfg: &'a F::Config) -> Self { - let reduction_params = F::barrett_reduction_params(field_cfg); - let flush_adds = dmr_flush_adds(&reduction_params); + let reducer = BarrettDelayedReduction::::new(field_cfg); let zero = F::zero_with_cfg(field_cfg); Self { buckets: [Uint::zero(); D], lane_accs: array::from_fn(|_| zero.clone()), pending_adds: 0, - flush_adds, field_cfg, - reduction_params, + reducer, } } @@ -129,9 +117,9 @@ where let Some(bucket) = self.buckets.get_mut(bit_idx) else { return Err(AccumulatorError::BitIndexOutOfRange { bit_idx, degree: D }); }; - as DelayedModularReduction>::add(bucket, weight); + self.reducer.add(bucket, weight); self.pending_adds = self.pending_adds.saturating_add(1); - if self.pending_adds >= self.flush_adds { + if self.pending_adds >= self.reducer.flush_adds() { self.flush_buckets(); } Ok(()) @@ -178,11 +166,7 @@ where continue; } let pending = std::mem::replace(bucket, Uint::zero()); - *acc += as DelayedModularReduction>::reduce( - pending, - self.field_cfg, - &self.reduction_params, - ); + *acc += self.reducer.reduce(pending); } self.pending_adds = 0; } @@ -202,12 +186,16 @@ where self.flush_buckets(); let zero = F::zero_with_cfg(self.field_cfg); - Ok(FieldFieldInnerProduct::inner_product::( - &self.lane_accs, - &projection_powers[..D], - zero, + let product_sum = MontgomeryProductSum4::::new(self.field_cfg); + Ok( + FieldFieldInnerProduct::inner_product_with_algorithm::( + &product_sum, + &self.lane_accs, + &projection_powers[..D], + zero, + ) + .expect("bucket and projection-power lengths match"), ) - .expect("bucket and projection-power lengths match")) } } diff --git a/piop/src/neutron_nova/booleanity.rs b/piop/src/neutron_nova/booleanity.rs index 98612f7c..e3af721e 100644 --- a/piop/src/neutron_nova/booleanity.rs +++ b/piop/src/neutron_nova/booleanity.rs @@ -1,4 +1,4 @@ -use crate::neutron_nova::{RowWeights, accumulator::dmr_flush_adds}; +use crate::neutron_nova::RowWeights; use crypto_primitives::{FromPrimitiveWithConfig, PrimeField, crypto_bigint_uint::Uint}; use num_traits::Zero; use std::array; @@ -8,9 +8,10 @@ use zinc_uair::UairTrace; use zinc_utils::{ UNCHECKED, delayed_reduction::{ - BarrettReductionParams, DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs, + BarrettDelayedReduction, DelayedFieldProductSum, DelayedModularReductionAlgorithm, + MontgomeryLimbs, MontgomeryProductSum4, }, - inner_product::{FieldFieldInnerProduct, InnerProduct}, + inner_product::FieldFieldInnerProduct, }; const MAX_DMR_BUCKET_ARRAYS: usize = 256; @@ -235,7 +236,7 @@ where let entries_by_support_size = extended_entries_by_support_size(prefix_vars)?; let row_count = weights.row_weights.len(); let omega = precompute_row_tail_weights(weights, field_cfg)?; - let reduction_params = F::barrett_reduction_params(field_cfg); + let reducer = BarrettDelayedReduction::::new(field_cfg); let word_count = bit_word_count(D); let prefix_word_len = prefix_len .checked_mul(word_count) @@ -269,7 +270,7 @@ where &mut prefix_words, &scalar_weights.rho_powers[col_idx * D..col_idx * D + D], field_cfg, - &reduction_params, + &reducer, &mut table_values, )?; } @@ -299,7 +300,7 @@ fn accumulate_column_tile( prefix_words: &mut [u64], rho_powers: &[F], field_cfg: &F::Config, - reduction_params: &BarrettReductionParams, + reducer: &BarrettDelayedReduction<'_, F>, table_values: &mut [F], ) -> Result<(), BooleanityAccumulatorError> where @@ -319,8 +320,9 @@ where let mut lane_accs: Vec<[F; D]> = (0..bucket_count) .map(|_| array::from_fn(|_| zero.clone())) .collect(); + let product_sum = MontgomeryProductSum4::::new(field_cfg); let mut pending_adds = 0usize; - let flush_adds = dmr_flush_adds(reduction_params); + let flush_adds = reducer.flush_adds(); for tail in 0..tail_len { let omega_offset = tail * row_count; @@ -345,28 +347,25 @@ where word_count, prefix_words, weight, + reducer, &mut buckets, )); if pending_adds >= flush_adds { - flush_buckets_into_lanes( - &mut buckets, - &mut lane_accs, - field_cfg, - reduction_params, - ); + flush_buckets_into_lanes(&mut buckets, &mut lane_accs, reducer); pending_adds = 0; } } } } - flush_buckets_into_lanes(&mut buckets, &mut lane_accs, field_cfg, reduction_params); + flush_buckets_into_lanes(&mut buckets, &mut lane_accs, reducer); for (entry_offset, entry) in tile.iter().enumerate() { for magnitude in 1..=max_magnitude { let bucket_idx = entry_offset * bucket_stride + magnitude; - let projected = FieldFieldInnerProduct::inner_product::( + let projected = FieldFieldInnerProduct::inner_product_with_algorithm::( + &product_sum, &lane_accs[bucket_idx], rho_powers, zero.clone(), @@ -419,6 +418,7 @@ fn accumulate_entry_deltas( word_count: usize, prefix_words: &[u64], weight: &F, + reducer: &BarrettDelayedReduction<'_, F>, buckets: &mut [[Uint<5>; D]], ) -> usize where @@ -432,6 +432,7 @@ where word_count, prefix_words, weight, + reducer, buckets, ), 2 => accumulate_support_two::( @@ -441,6 +442,7 @@ where word_count, prefix_words, weight, + reducer, buckets, ), _ => accumulate_support_general::( @@ -450,6 +452,7 @@ where word_count, prefix_words, weight, + reducer, buckets, ), } @@ -463,6 +466,7 @@ fn accumulate_support_one( word_count: usize, prefix_words: &[u64], weight: &F, + reducer: &BarrettDelayedReduction<'_, F>, buckets: &mut [[Uint<5>; D]], ) -> usize where @@ -477,7 +481,7 @@ where for word_idx in 0..word_count { let mask = word_at(prefix_words, idx0, word_count, word_idx) ^ word_at(prefix_words, idx1, word_count, word_idx); - adds += add_mask_word_to_bucket(mask, word_idx, weight, &mut buckets[bucket_idx]); + adds += add_mask_word_to_bucket(mask, word_idx, weight, reducer, &mut buckets[bucket_idx]); } adds } @@ -490,6 +494,7 @@ fn accumulate_support_two( word_count: usize, prefix_words: &[u64], weight: &F, + reducer: &BarrettDelayedReduction<'_, F>, buckets: &mut [[Uint<5>; D]], ) -> usize where @@ -514,8 +519,8 @@ where let d11 = word_at(prefix_words, idx11, word_count, word_idx) & valid_mask; let mask_1 = ((d11 ^ d00) ^ (d10 ^ d01)) & valid_mask; let mask_2 = ((d11 & d00 & !d10 & !d01) | (!d11 & !d00 & d10 & d01)) & valid_mask; - adds += add_mask_word_to_bucket(mask_1, word_idx, weight, &mut buckets[bucket_1]); - adds += add_mask_word_to_bucket(mask_2, word_idx, weight, &mut buckets[bucket_2]); + adds += add_mask_word_to_bucket(mask_1, word_idx, weight, reducer, &mut buckets[bucket_1]); + adds += add_mask_word_to_bucket(mask_2, word_idx, weight, reducer, &mut buckets[bucket_2]); } adds @@ -529,6 +534,7 @@ fn accumulate_support_general( word_count: usize, prefix_words: &[u64], weight: &F, + reducer: &BarrettDelayedReduction<'_, F>, buckets: &mut [[Uint<5>; D]], ) -> usize where @@ -571,7 +577,7 @@ where continue; } let bucket_idx = entry_offset * bucket_stride + magnitude; - as DelayedModularReduction>::add(&mut buckets[bucket_idx][lane], weight); + reducer.add(&mut buckets[bucket_idx][lane], weight); adds += 1; } adds @@ -582,6 +588,7 @@ fn add_mask_word_to_bucket( mut mask: u64, word_idx: usize, weight: &F, + reducer: &BarrettDelayedReduction<'_, F>, bucket: &mut [Uint<5>; D], ) -> usize where @@ -592,7 +599,7 @@ where let bit = usize::try_from(mask.trailing_zeros()).expect("trailing_zeros fits usize"); let lane = word_idx * 64 + bit; if lane < D { - as DelayedModularReduction>::add(&mut bucket[lane], weight); + reducer.add(&mut bucket[lane], weight); adds += 1; } mask &= mask - 1; @@ -603,8 +610,7 @@ where fn flush_buckets_into_lanes( buckets: &mut [[Uint<5>; D]], lane_accs: &mut [[F; D]], - field_cfg: &F::Config, - reduction_params: &BarrettReductionParams, + reducer: &BarrettDelayedReduction<'_, F>, ) where F: MontgomeryLimbs + Send + Sync, { @@ -614,11 +620,7 @@ fn flush_buckets_into_lanes( continue; } let pending = std::mem::replace(bucket, Uint::zero()); - *acc += as DelayedModularReduction>::reduce( - pending, - field_cfg, - reduction_params, - ); + *acc += reducer.reduce(pending); } } } diff --git a/piop/src/neutron_nova/linear_cpr.rs b/piop/src/neutron_nova/linear_cpr.rs index 4f4173cd..272dd0d1 100644 --- a/piop/src/neutron_nova/linear_cpr.rs +++ b/piop/src/neutron_nova/linear_cpr.rs @@ -1,4 +1,4 @@ -use crate::neutron_nova::{RowWeights, accumulator::dmr_flush_adds, sumfold::checked_domain_size}; +use crate::neutron_nova::{RowWeights, sumfold::checked_domain_size}; use crate::sumcheck::{ multi_degree::{MultiDegreeSumcheckGroup, PrefixFastPath, PrefixRoundOutput}, prover::ProverState as SumcheckProverState, @@ -16,9 +16,10 @@ use zinc_uair::UairTrace; use zinc_utils::{ UNCHECKED, delayed_reduction::{ - BarrettReductionParams, DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs, + BarrettDelayedReduction, DelayedFieldProductSum, DelayedModularReductionAlgorithm, + MontgomeryLimbs, MontgomeryProductSum4, }, - inner_product::{FieldFieldInnerProduct, InnerProduct}, + inner_product::FieldFieldInnerProduct, inner_transparent_field::InnerTransparentField, }; @@ -275,7 +276,7 @@ where )?; let mut table_values = vec![F::zero_with_cfg(field_cfg); prefix_len]; - let reduction_params = F::barrett_reduction_params(field_cfg); + let reducer = BarrettDelayedReduction::::new(field_cfg); for family in &prepared { let family_weight = &scalar_weights.family_weights[family.family_idx]; @@ -297,7 +298,7 @@ where scalar_weights.scalarization_powers, family_weight, field_cfg, - &reduction_params, + &reducer, &mut table_values, )?; tile_start += tile_len; @@ -681,7 +682,7 @@ fn accumulate_family_tile( scalarization_powers: &[F], family_weight: &F, field_cfg: &F::Config, - reduction_params: &BarrettReductionParams, + reducer: &BarrettDelayedReduction<'_, F>, table_values: &mut [F], ) -> Result<(), LinearCprAccumulatorError> where @@ -697,8 +698,9 @@ where let mut lane_accs: Vec<[F; D]> = (0..bucket_count) .map(|_| array::from_fn(|_| zero.clone())) .collect(); + let product_sum = MontgomeryProductSum4::::new(field_cfg); let mut pending_adds = 0usize; - let flush_adds = dmr_flush_adds(reduction_params); + let flush_adds = reducer.flush_adds(); for tail in 0..tail_len { let tail_weight = &weights.tail_eq_weights[tail]; @@ -722,16 +724,12 @@ where pending_adds = pending_adds.saturating_add(add_poly_bits_to_bucket( poly, &omega, + reducer, &mut buckets[bucket_idx], )); if pending_adds >= flush_adds { - flush_buckets_into_lanes( - &mut buckets, - &mut lane_accs, - field_cfg, - reduction_params, - ); + flush_buckets_into_lanes(&mut buckets, &mut lane_accs, reducer); pending_adds = 0; } } @@ -739,14 +737,15 @@ where } } - flush_buckets_into_lanes(&mut buckets, &mut lane_accs, field_cfg, reduction_params); + flush_buckets_into_lanes(&mut buckets, &mut lane_accs, reducer); for prefix_offset in 0..tile_len { let prefix = tile_start + prefix_offset; let mut family_value = zero.clone(); for (coeff_idx, coeff_value) in family.coeff_values.iter().enumerate() { let bucket_idx = prefix_offset * family.coeff_values.len() + coeff_idx; - let projected = FieldFieldInnerProduct::inner_product::( + let projected = FieldFieldInnerProduct::inner_product_with_algorithm::( + &product_sum, &lane_accs[bucket_idx], &scalarization_powers[..D], zero.clone(), @@ -912,6 +911,7 @@ where fn add_poly_bits_to_bucket( poly: &BinaryPoly, weight: &F, + reducer: &BarrettDelayedReduction<'_, F>, bucket: &mut [Uint<5>; D], ) -> usize where @@ -929,7 +929,7 @@ where while bits != 0 { let bit_idx = usize::try_from(bits.trailing_zeros()).expect("trailing_zeros fits usize"); - as DelayedModularReduction>::add(&mut bucket[bit_idx], weight); + reducer.add(&mut bucket[bit_idx], weight); bits &= bits - 1; adds += 1; } @@ -938,7 +938,7 @@ where let mut adds = 0usize; for (bit_idx, coeff) in poly.iter().enumerate().take(D) { if coeff.into_inner() { - as DelayedModularReduction>::add(&mut bucket[bit_idx], weight); + reducer.add(&mut bucket[bit_idx], weight); adds += 1; } } @@ -949,8 +949,7 @@ where fn flush_buckets_into_lanes( buckets: &mut [[Uint<5>; D]], lane_accs: &mut [[F; D]], - field_cfg: &F::Config, - reduction_params: &BarrettReductionParams, + reducer: &BarrettDelayedReduction<'_, F>, ) where F: MontgomeryLimbs + Send + Sync, { @@ -960,11 +959,7 @@ fn flush_buckets_into_lanes( continue; } let pending = std::mem::replace(bucket, Uint::zero()); - *acc += as DelayedModularReduction>::reduce( - pending, - field_cfg, - reduction_params, - ); + *acc += reducer.reduce(pending); } } } diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 91a1d301..b9a4b16b 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -7,7 +7,7 @@ //! folded row check over the 128-row SHA domain. use crate::ideal_check::batched_ideal_check; -use crate::neutron_nova::{SumFoldError, accumulator::dmr_flush_adds}; +use crate::neutron_nova::SumFoldError; use crate::{ CombFn, sumcheck::multi_degree::{MultiDegreeSumcheckGroup, PrefixFastPath, PrefixRoundOutput}, @@ -26,7 +26,10 @@ use zinc_uair::{ }; use zinc_utils::{ UNCHECKED, - delayed_reduction::{DelayedFieldProductSum, DelayedModularReduction, MontgomeryLimbs}, + delayed_reduction::{ + BarrettDelayedReduction, DelayedFieldProductSum, DelayedModularReductionAlgorithm, + MontgomeryLimbs, + }, from_ref::FromRef, inner_product::{FieldFieldInnerProduct, InnerProduct}, inner_transparent_field::InnerTransparentField, @@ -1058,6 +1061,7 @@ where }); } let word_col_count = bit_slices.len() / SHA_WORD_BITS; + let reducer = BarrettDelayedReduction::::new(field_cfg); let mut words = Vec::with_capacity(word_col_count); for col_idx in 0..word_col_count { let mut out_col = Vec::with_capacity(SHA_ROW_COUNT); @@ -1073,7 +1077,7 @@ where )?); } out_col.push(project_binary_bits_conditional_add_dmr( - &bits, &powers, field_cfg, + &bits, &powers, field_cfg, &reducer, )?); } words.push(out_col); @@ -4082,6 +4086,7 @@ fn project_binary_bits_conditional_add_dmr( bits: &[F], powers: &[F], field_cfg: &F::Config, + reducer: &BarrettDelayedReduction<'_, F>, ) -> Result where F: MontgomeryLimbs + DelayedFieldProductSum + Send + Sync, @@ -4092,8 +4097,6 @@ where )); } let one = F::one_with_cfg(field_cfg); - let reduction_params = F::barrett_reduction_params(field_cfg); - let flush_adds = dmr_flush_adds(&reduction_params); let mut bucket = Uint::<5>::zero(); let mut pending_adds = 0usize; let mut acc = F::zero_with_cfg(field_cfg); @@ -4106,22 +4109,17 @@ where return project_bits_dmr(bits, powers, field_cfg); } - as DelayedModularReduction>::add(&mut bucket, power); + reducer.add(&mut bucket, power); pending_adds = pending_adds.saturating_add(1); - if pending_adds >= flush_adds { + if pending_adds >= reducer.flush_adds() { let pending = std::mem::replace(&mut bucket, Uint::zero()); - acc += as DelayedModularReduction>::reduce( - pending, - field_cfg, - &reduction_params, - ); + acc += reducer.reduce(pending); pending_adds = 0; } } if !bucket.is_zero() { - acc += - as DelayedModularReduction>::reduce(bucket, field_cfg, &reduction_params); + acc += reducer.reduce(bucket); } Ok(acc) } @@ -4430,8 +4428,9 @@ mod tests { binary_bits[31] = f(1); let binary_expected = naive_project_bits(&binary_bits, &powers); + let reducer = BarrettDelayedReduction::::new(&cfg); assert_eq!( - project_binary_bits_conditional_add_dmr(&binary_bits, &powers, &cfg).unwrap(), + project_binary_bits_conditional_add_dmr(&binary_bits, &powers, &cfg, &reducer).unwrap(), binary_expected ); assert_eq!( @@ -4444,7 +4443,7 @@ mod tests { field_bits[9] = f(11); let field_expected = naive_project_bits(&field_bits, &powers); assert_eq!( - project_binary_bits_conditional_add_dmr(&field_bits, &powers, &cfg).unwrap(), + project_binary_bits_conditional_add_dmr(&field_bits, &powers, &cfg, &reducer).unwrap(), field_expected ); assert_eq!( diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index f3467489..527cef21 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -54,7 +54,9 @@ use zinc_transcript::traits::{ConstTranscribable, GenTranscribable, Transcribabl use zinc_uair::{Uair, ideal::Ideal}; use zinc_utils::{ cfg_extend, cfg_into_iter, cfg_iter, - delayed_reduction::{DelayedModularReduction, MontgomeryLimbs}, + delayed_reduction::{ + BarrettDelayedReduction, DelayedModularReductionAlgorithm, MontgomeryLimbs, + }, named::Named, }; use zip_plus::{ @@ -550,7 +552,7 @@ where { let eq_table = zinc_poly::utils::build_eq_x_r_vec(point, field_cfg) .expect("compute_lifted_evals: eq table build failed"); - let reduction_params = F::barrett_reduction_params(field_cfg); + let reducer = BarrettDelayedReduction::::new(field_cfg); let n_bin = trace_bin_poly.len(); let zero = F::zero_with_cfg(field_cfg); @@ -574,27 +576,18 @@ where let mut remaining = bits; while remaining != 0 { let l = remaining.trailing_zeros() as usize; - as DelayedModularReduction>::add(&mut coeffs[l], eq_b); + reducer.add(&mut coeffs[l], eq_b); remaining &= remaining - 1; } } else { for (l, coeff) in entry.iter().enumerate().take(D) { if coeff.into_inner() { - as DelayedModularReduction>::add(&mut coeffs[l], eq_b); + reducer.add(&mut coeffs[l], eq_b); } } } } - let coeffs: Vec = coeffs - .into_iter() - .map(|acc| { - as DelayedModularReduction>::reduce( - acc, - field_cfg, - &reduction_params, - ) - }) - .collect(); + let coeffs: Vec = coeffs.into_iter().map(|acc| reducer.reduce(acc)).collect(); DynamicPolynomialF::new_trimmed(coeffs) }) .collect(); diff --git a/utils/src/delayed_reduction.rs b/utils/src/delayed_reduction.rs index 43cd271b..a7a3e831 100644 --- a/utils/src/delayed_reduction.rs +++ b/utils/src/delayed_reduction.rs @@ -11,6 +11,9 @@ use crypto_primitives::{ crypto_bigint_uint::Uint, }; use num_traits::Zero; +use std::marker::PhantomData; + +const DEFAULT_DMR_FLUSH_ADDS: usize = 1 << 20; /// Barrett reduction parameters modulo a 4-limb prime. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -44,6 +47,33 @@ pub trait MontgomeryLimbs: PrimeField> + Sized { fn barrett_reduction_params(cfg: &Self::Config) -> BarrettReductionParams; } +/// Algorithm object for delayed modular reduction. +pub trait DelayedModularReductionAlgorithm { + type Value; + type Accumulator; + + fn zero_accumulator(&self) -> Self::Accumulator; + fn add(&self, acc: &mut Self::Accumulator, value: &Self::Value); + fn reduce(&self, acc: Self::Accumulator) -> Self::Value; +} + +/// Algorithm object for delayed field product sums. +pub trait DelayedFieldProductSumAlgorithm { + type Value; + type Accumulator; + + fn zero_accumulator(&self) -> Self::Accumulator; + fn add_product(&self, acc: &mut Self::Accumulator, lhs: &Self::Value, rhs: &Self::Value); + fn reduce_products(&self, acc: Self::Accumulator) -> Self::Value; + fn sum_of_products(&self, lhs: &[Self::Value], rhs: &[Self::Value]) -> Self::Value; + fn sum_of_products_with_seed( + &self, + lhs: &[Self::Value], + rhs: &[Self::Value], + seed: Self::Value, + ) -> Self::Value; +} + /// Accumulator trait for delayed modular reduction. pub trait DelayedModularReduction: Zero + Clone + Send + Sync where @@ -61,30 +91,204 @@ pub trait DelayedFieldProductSum: PrimeField + Sized { fn delayed_sum_of_products(lhs: &[Self], rhs: &[Self], zero: Self) -> Self; } -impl DelayedModularReduction for Uint<5> +#[derive(Clone, Debug)] +pub struct BarrettDelayedReduction<'cfg, F> +where + F: MontgomeryLimbs, +{ + cfg: &'cfg F::Config, + params: BarrettReductionParams, + flush_adds: usize, + _field: PhantomData, +} + +impl<'cfg, F> BarrettDelayedReduction<'cfg, F> +where + F: MontgomeryLimbs, +{ + pub fn new(cfg: &'cfg F::Config) -> Self { + let params = F::barrett_reduction_params(cfg); + let flush_adds = if params.modulus[3] == 0 { + 1 + } else { + DEFAULT_DMR_FLUSH_ADDS + }; + Self { + cfg, + params, + flush_adds, + _field: PhantomData, + } + } + + pub fn flush_adds(&self) -> usize { + self.flush_adds + } + + pub fn params(&self) -> &BarrettReductionParams { + &self.params + } +} + +impl DelayedModularReductionAlgorithm for BarrettDelayedReduction<'_, F> where F: MontgomeryLimbs + Send + Sync, { + type Value = F; + type Accumulator = Uint<5>; + + fn zero_accumulator(&self) -> Self::Accumulator { + Uint::zero() + } + #[inline(always)] - fn add(&mut self, value: &F) { - let acc = self.as_mut_words(); - let rhs = value.montgomery_limbs(); - let mut carry = 0u64; - let mut i = 0; - while i < 4 { - let (sum, c0) = acc[i].overflowing_add(rhs[i]); - let (sum, c1) = sum.overflowing_add(carry); - acc[i] = sum; - carry = (c0 as u64) + (c1 as u64); - i += 1; + fn add(&self, acc: &mut Self::Accumulator, value: &Self::Value) { + add_montgomery_limbs_5(acc, value.montgomery_limbs()); + } + + #[inline(always)] + fn reduce(&self, acc: Self::Accumulator) -> Self::Value { + F::from_montgomery_limbs(barrett_reduce_5(acc.as_words(), &self.params), self.cfg) + } +} + +/// Raw accumulator for a delayed sum of 4-limb Montgomery products. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ProductAccumulator4 { + limbs: Uint<9>, + pending_products: usize, +} + +impl ProductAccumulator4 { + pub fn pending_products(&self) -> usize { + self.pending_products + } + + pub fn limbs(&self) -> &Uint<9> { + &self.limbs + } +} + +#[derive(Clone, Debug)] +pub struct MontgomeryProductSum4<'cfg, F> +where + F: MontgomeryLimbs, +{ + cfg: &'cfg F::Config, + reduction_params: BarrettReductionParams, + mod_neg_inv: u64, + flush_products: usize, + _field: PhantomData, +} + +impl<'cfg, F> MontgomeryProductSum4<'cfg, F> +where + F: MontgomeryLimbs, +{ + pub fn new(cfg: &'cfg F::Config) -> Self { + let reduction_params = F::barrett_reduction_params(cfg); + let leading_zeros = clz::<4>(&reduction_params.modulus); + let flush_products = if leading_zeros == 0 { + 1 + } else { + usize::try_from(leading_zeros) + .ok() + .and_then(|shift| 1usize.checked_shl(shift as u32)) + .unwrap_or(usize::MAX) + }; + Self::new_with_flush_products(cfg, flush_products) + } + + pub fn new_with_flush_products(cfg: &'cfg F::Config, flush_products: usize) -> Self { + let reduction_params = F::barrett_reduction_params(cfg); + Self { + cfg, + reduction_params, + mod_neg_inv: mod_neg_inv_u64(reduction_params.modulus[0]), + flush_products: flush_products.max(1), + _field: PhantomData, + } + } + + pub fn flush_products(&self) -> usize { + self.flush_products + } +} + +impl DelayedFieldProductSumAlgorithm for MontgomeryProductSum4<'_, F> +where + F: MontgomeryLimbs + Send + Sync, +{ + type Value = F; + type Accumulator = ProductAccumulator4; + + fn zero_accumulator(&self) -> Self::Accumulator { + ProductAccumulator4 { + limbs: Uint::zero(), + pending_products: 0, } + } - let old_hi = acc[4]; - acc[4] = acc[4].wrapping_add(carry); + #[inline(always)] + fn add_product(&self, acc: &mut Self::Accumulator, lhs: &Self::Value, rhs: &Self::Value) { debug_assert!( - acc[4] >= old_hi, - "Uint<5> delayed accumulator overflowed high limb" + acc.pending_products < self.flush_products, + "ProductAccumulator4 must be reduced before exceeding its flush threshold" + ); + add_montgomery_product_4x4( + &mut acc.limbs, + lhs.montgomery_limbs(), + rhs.montgomery_limbs(), + ); + acc.pending_products = acc.pending_products.saturating_add(1); + } + + fn reduce_products(&self, acc: Self::Accumulator) -> Self::Value { + if acc.pending_products == 0 { + return F::zero_with_cfg(self.cfg); + } + let reduced = montgomery_reduce_9_to_4( + acc.limbs.as_words(), + &self.reduction_params, + self.mod_neg_inv, ); + F::from_montgomery_limbs(reduced, self.cfg) + } + + fn sum_of_products(&self, lhs: &[Self::Value], rhs: &[Self::Value]) -> Self::Value { + let mut total = F::zero_with_cfg(self.cfg); + let mut acc = self.zero_accumulator(); + for (left, right) in lhs.iter().zip(rhs) { + self.add_product(&mut acc, left, right); + if acc.pending_products >= self.flush_products { + let pending = acc; + total += self.reduce_products(pending); + acc = self.zero_accumulator(); + } + } + if acc.pending_products != 0 { + total += self.reduce_products(acc); + } + total + } + + fn sum_of_products_with_seed( + &self, + lhs: &[Self::Value], + rhs: &[Self::Value], + seed: Self::Value, + ) -> Self::Value { + seed + self.sum_of_products(lhs, rhs) + } +} + +impl DelayedModularReduction for Uint<5> +where + F: MontgomeryLimbs + Send + Sync, +{ + #[inline(always)] + fn add(&mut self, value: &F) { + add_montgomery_limbs_5(self, value.montgomery_limbs()); } #[inline(always)] @@ -94,6 +298,124 @@ where } } +#[inline(always)] +fn add_montgomery_limbs_5(acc: &mut Uint<5>, rhs: &[u64; 4]) { + let acc = acc.as_mut_words(); + let mut carry = 0u64; + let mut i = 0; + while i < 4 { + let (sum, c0) = acc[i].overflowing_add(rhs[i]); + let (sum, c1) = sum.overflowing_add(carry); + acc[i] = sum; + carry = (c0 as u64) + (c1 as u64); + i += 1; + } + + let old_hi = acc[4]; + acc[4] = acc[4].wrapping_add(carry); + debug_assert!( + acc[4] >= old_hi, + "Uint<5> delayed accumulator overflowed high limb" + ); +} + +#[inline(always)] +fn mod_neg_inv_u64(modulus_limb: u64) -> u64 { + debug_assert!(modulus_limb & 1 == 1, "Montgomery modulus must be odd"); + let mut inv = 1u64; + let mut i = 0; + while i < 6 { + inv = inv.wrapping_mul(2u64.wrapping_sub(modulus_limb.wrapping_mul(inv))); + i += 1; + } + inv.wrapping_neg() +} + +#[inline(always)] +fn add_montgomery_product_4x4(acc: &mut Uint<9>, lhs: &[u64; 4], rhs: &[u64; 4]) { + let product = mul_4x4_to_8(lhs, rhs); + let acc = acc.as_mut_words(); + let mut carry = 0u64; + let mut i = 0; + while i < 8 { + let (sum, c0) = acc[i].overflowing_add(product[i]); + let (sum, c1) = sum.overflowing_add(carry); + acc[i] = sum; + carry = (c0 as u64) + (c1 as u64); + i += 1; + } + + let old_hi = acc[8]; + acc[8] = acc[8].wrapping_add(carry); + debug_assert!(acc[8] >= old_hi, "ProductAccumulator4 overflowed high limb"); +} + +#[inline(always)] +fn mul_4x4_to_8(lhs: &[u64; 4], rhs: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + let mut i = 0; + while i < 4 { + let mut carry = 0u128; + let mut j = 0; + while j < 4 { + let idx = i + j; + let prod = (lhs[i] as u128) * (rhs[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + j += 1; + } + + let mut idx = i + 4; + let mut carry_u64 = carry as u64; + while carry_u64 != 0 && idx < 8 { + let (sum, overflow) = result[idx].overflowing_add(carry_u64); + result[idx] = sum; + carry_u64 = overflow as u64; + idx += 1; + } + debug_assert!(carry_u64 == 0, "4x4 product exceeded eight limbs"); + i += 1; + } + result +} + +#[inline(always)] +fn montgomery_reduce_9_to_4( + acc: &[u64; 9], + params: &BarrettReductionParams, + mod_neg_inv: u64, +) -> [u64; 4] { + let mut t = *acc; + let mut i = 0; + while i < 4 { + let q = t[i].wrapping_mul(mod_neg_inv); + let mut carry = 0u128; + let mut j = 0; + while j < 4 { + let idx = i + j; + let sum = (q as u128) * (params.modulus[j] as u128) + (t[idx] as u128) + carry; + t[idx] = sum as u64; + carry = sum >> 64; + j += 1; + } + + let mut idx = i + 4; + let mut carry_u64 = carry as u64; + while carry_u64 != 0 { + debug_assert!(idx < 9, "Montgomery reduction carry exceeded accumulator"); + let (sum, overflow) = t[idx].overflowing_add(carry_u64); + t[idx] = sum; + carry_u64 = overflow as u64; + idx += 1; + } + debug_assert!(t[i] == 0, "Montgomery reduction did not clear low limb"); + i += 1; + } + + let reduced = [t[4], t[5], t[6], t[7], t[8]]; + barrett_reduce_5(&reduced, params) +} + impl DelayedFieldProductSum for MontyField { fn delayed_sum_of_products(lhs: &[Self], rhs: &[Self], zero: Self) -> Self { if lhs.is_empty() { @@ -418,6 +740,17 @@ mod tests { F::make_cfg(&modulus).expect("secp256k1 base field prime is valid") } + fn batched_product_cfg() -> ::Config { + let modulus = Uint::new( + crypto_bigint::Uint::<4>::from_str_radix_vartime( + "00dca94d8a1ecce3b6e8755d8999787d0524d8ca1ea755e7af84fb646fa31f27", + 16, + ) + .expect("valid modulus"), + ); + F::make_cfg(&modulus).expect("valid field config") + } + #[test] fn secp256k1_barrett_params_match_expected_modulus() { let cfg = secp256k1_cfg(); @@ -435,7 +768,7 @@ mod tests { #[test] fn delayed_sum_matches_field_addition() { let cfg = secp256k1_cfg(); - let reduction_params = F::barrett_reduction_params(&cfg); + let reducer = BarrettDelayedReduction::::new(&cfg); let values: Vec = (0..512) .map(|i| F::from_with_cfg(i as u64 + 1, &cfg)) .collect(); @@ -445,25 +778,23 @@ mod tests { expected += value; } - let mut acc = Uint::<5>::zero(); + let mut acc = reducer.zero_accumulator(); for value in &values { - as DelayedModularReduction>::add(&mut acc, value); + reducer.add(&mut acc, value); } - assert_eq!( - as DelayedModularReduction>::reduce(acc, &cfg, &reduction_params), - expected - ); + assert_eq!(reducer.reduce(acc), expected); } #[test] fn barrett_reduce_5_matches_uint_remainder_for_bounded_sum() { let cfg = secp256k1_cfg(); let reduction_params = F::barrett_reduction_params(&cfg); + let reducer = BarrettDelayedReduction::::new(&cfg); let mut acc = Uint::<5>::zero(); let max = -F::from_with_cfg(1u64, &cfg); for _ in 0..512 { - as DelayedModularReduction>::add(&mut acc, &max); + reducer.add(&mut acc, &max); } let wide = acc; @@ -482,4 +813,109 @@ mod tests { let reduced = barrett_reduce_5(acc, &reduction_params); assert_eq!(Uint::<4>::from_words(reduced), expected); } + + #[test] + fn product_accumulator_single_product_matches_field_multiplication() { + let cfg = secp256k1_cfg(); + let reducer = MontgomeryProductSum4::::new(&cfg); + let lhs = F::from_with_cfg(17u64, &cfg); + let rhs = F::from_with_cfg(23u64, &cfg); + + let mut acc = reducer.zero_accumulator(); + reducer.add_product(&mut acc, &lhs, &rhs); + + assert_eq!(reducer.reduce_products(acc), lhs * &rhs); + } + + #[test] + fn product_accumulator_multi_product_matches_naive_sum() { + let cfg = secp256k1_cfg(); + let reducer = MontgomeryProductSum4::::new(&cfg); + let lhs: Vec = (0..32).map(|idx| F::from_with_cfg(idx + 3, &cfg)).collect(); + let rhs: Vec = (0..32) + .map(|idx| F::from_with_cfg(257 - idx, &cfg)) + .collect(); + + let expected = lhs + .iter() + .zip(&rhs) + .fold(F::zero_with_cfg(&cfg), |acc, (left, right)| { + acc + left.clone() * right + }); + + assert_eq!(reducer.sum_of_products(&lhs, &rhs), expected); + } + + #[test] + fn product_accumulator_batches_near_modulus_terms_before_reduction() { + let cfg = batched_product_cfg(); + let reducer = MontgomeryProductSum4::::new(&cfg); + assert!(reducer.flush_products() > 64); + + let lhs: Vec = (0..64) + .map(|idx| -F::from_with_cfg(idx * 17 + 5, &cfg)) + .collect(); + let rhs: Vec = (0..64) + .map(|idx| -F::from_with_cfg(idx * 19 + 7, &cfg)) + .collect(); + + let mut acc = reducer.zero_accumulator(); + for (left, right) in lhs.iter().zip(&rhs) { + reducer.add_product(&mut acc, left, right); + } + assert_eq!(acc.pending_products(), lhs.len()); + + let expected = lhs + .iter() + .zip(&rhs) + .fold(F::zero_with_cfg(&cfg), |sum, (left, right)| { + sum + left.clone() * right + }); + + assert_eq!(reducer.reduce_products(acc), expected); + } + + #[test] + fn product_accumulator_seeded_sum_matches_naive_sum() { + let cfg = secp256k1_cfg(); + let reducer = MontgomeryProductSum4::::new(&cfg); + let seed = F::from_with_cfg(99u64, &cfg); + let lhs: Vec = (0..16).map(|idx| F::from_with_cfg(idx + 5, &cfg)).collect(); + let rhs: Vec = (0..16) + .map(|idx| F::from_with_cfg(131 - idx, &cfg)) + .collect(); + + let expected = lhs + .iter() + .zip(&rhs) + .fold(seed.clone(), |acc, (left, right)| { + acc + left.clone() * right + }); + + assert_eq!( + reducer.sum_of_products_with_seed(&lhs, &rhs, seed), + expected + ); + } + + #[test] + fn product_accumulator_forced_flush_matches_naive_sum() { + let cfg = secp256k1_cfg(); + let reducer = MontgomeryProductSum4::::new_with_flush_products(&cfg, 1); + let lhs: Vec = (0..32) + .map(|idx| F::from_with_cfg(idx + 11, &cfg)) + .collect(); + let rhs: Vec = (0..32) + .map(|idx| F::from_with_cfg(409 - idx, &cfg)) + .collect(); + + let expected = lhs + .iter() + .zip(&rhs) + .fold(F::zero_with_cfg(&cfg), |acc, (left, right)| { + acc + left.clone() * right + }); + + assert_eq!(reducer.sum_of_products(&lhs, &rhs), expected); + } } diff --git a/utils/src/inner_product.rs b/utils/src/inner_product.rs index d6cfda75..d9a2655b 100644 --- a/utils/src/inner_product.rs +++ b/utils/src/inner_product.rs @@ -1,5 +1,7 @@ use crate::{ - delayed_reduction::DelayedFieldProductSum, from_ref::FromRef, mul_by_scalar::MulByScalar, + delayed_reduction::{DelayedFieldProductSum, DelayedFieldProductSumAlgorithm}, + from_ref::FromRef, + mul_by_scalar::MulByScalar, }; use crypto_primitives::{FromWithConfig, PrimeField, boolean::Boolean}; use num_traits::CheckedAdd; @@ -92,6 +94,27 @@ impl MBSInnerProduct { #[derive(Clone, Debug)] pub struct FieldFieldInnerProduct; +impl FieldFieldInnerProduct { + pub fn inner_product_with_algorithm( + algorithm: &A, + lhs: &[A::Value], + rhs: &[A::Value], + seed: A::Value, + ) -> Result + where + A: DelayedFieldProductSumAlgorithm, + { + if lhs.len() != rhs.len() { + return Err(InnerProductError::LengthMismatch { + lhs: lhs.len(), + rhs: rhs.len(), + }); + } + + Ok(algorithm.sum_of_products_with_seed(lhs, rhs, seed)) + } +} + impl InnerProduct<[F], F, F> for FieldFieldInnerProduct where F: DelayedFieldProductSum, @@ -179,7 +202,7 @@ impl + CheckedAdd> InnerProduct<[Boolean], Rhs, Ou #[cfg(test)] mod test { - use crate::{CHECKED, UNCHECKED}; + use crate::{CHECKED, UNCHECKED, delayed_reduction::MontgomeryProductSum4}; use crypto_bigint::{U64, U256, const_monty_params}; use crypto_primitives::{ FromWithConfig, PrimeField, crypto_bigint_const_monty::ConstMontyField, @@ -297,6 +320,29 @@ mod test { assert_eq!(got, expected); } + #[test] + fn field_field_inner_product_with_algorithm_matches_naive() { + type F = MontyField<4>; + let cfg = dyn_field_cfg(); + let algorithm = MontgomeryProductSum4::::new(&cfg); + let lhs: Vec = (0..24).map(|idx| F::from_with_cfg(idx + 3, &cfg)).collect(); + let rhs: Vec = (0..24) + .map(|idx| F::from_with_cfg(89 - idx, &cfg)) + .collect(); + let seed = F::from_with_cfg(99u64, &cfg); + + let got = FieldFieldInnerProduct::inner_product_with_algorithm::( + &algorithm, + &lhs, + &rhs, + seed.clone(), + ) + .unwrap(); + let expected = naive_field_inner_product(&lhs, &rhs, seed); + + assert_eq!(got, expected); + } + #[test] fn field_field_inner_product_const_monty_matches_naive() { type F = ConstMontyField; From 175beda9098259f23d975ab3d486dc6de80b1c20 Mon Sep 17 00:00:00 2001 From: John Wu Date: Sun, 7 Jun 2026 22:49:50 -0700 Subject: [PATCH 31/49] Optimize ProjectionFold SHA prover overhead --- piop/src/neutron_nova/mod.rs | 16 ++- piop/src/neutron_nova/projection_sha.rs | 182 ++++++++++++++++++++---- protocol/benches/e2e.rs | 79 +++++----- protocol/src/production_sha.rs | 20 ++- 4 files changed, 228 insertions(+), 69 deletions(-) diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index 15cea98e..bd719c92 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -48,12 +48,14 @@ pub use projection_sha::{ folded_row_integrand_values_with_row_weights, folded_row_integrand_values_with_vectors, production_sha_booleanity_sources, production_sha_nonzero_families, production_sha_nonzero_ideals, reconstruct_virtual_ch_maj_at_row, scalarize_bit_slices, - sha_int_at_point, sha_int_at_point_with_weights, sha_linear_residual_row_value, - sha_linear_residual_sum, sha_public_at_point, sha_public_at_point_with_weights, - sha_scalarized_word_at_point, sha_scalarized_word_at_point_with_weights, - sha_word_bits_at_point, sha_word_bits_at_point_with_weights, - validate_fresh_sha_ideal_polys_canonical, verify_folded_row_sumcheck_claim, - verify_folded_scalarization_links, verify_folded_scalarization_links_at_point, - verify_folded_shifted_scalarization_link_at_point, verify_fresh_sha_ideal_polys, + sha_int_at_point, sha_int_at_point_with_weights, sha_int_at_point_with_weights_unchecked, + sha_linear_residual_row_value, sha_linear_residual_sum, sha_public_at_point, + sha_public_at_point_with_weights, sha_scalarized_word_at_point, + sha_scalarized_word_at_point_with_weights, sha_word_bits_at_point, + sha_word_bits_at_point_with_weights, sha_word_bits_at_point_with_weights_unchecked, + validate_fresh_sha_ideal_polys_canonical, validate_projected_trace, + verify_folded_row_sumcheck_claim, verify_folded_scalarization_links, + verify_folded_scalarization_links_at_point, verify_folded_shifted_scalarization_link_at_point, + verify_fresh_sha_ideal_polys, }; pub use sumfold::{LinearInstanceClaims, LinearPrefixTable, SumFoldError}; diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index b9a4b16b..993fc852 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -996,6 +996,12 @@ where validate_public(public)?; } + let folded_public_columns = fold_mle_tables( + "public.columns", + publics.iter().map(|public| &public.columns), + &sumfold.eq_instance_weights, + field_cfg, + )?; let folded_trace = ProjectedTrace { bit_slices: fold_mle_tables( "bit_slices", @@ -1015,20 +1021,10 @@ where &sumfold.eq_instance_weights, field_cfg, )?, - public_columns: fold_mle_tables( - "public_columns", - traces.iter().map(|trace| &trace.public_columns), - &sumfold.eq_instance_weights, - field_cfg, - )?, + public_columns: folded_public_columns.clone(), }; let folded_public = ProjectedPublic { - columns: fold_mle_tables( - "public.columns", - publics.iter().map(|public| &public.columns), - &sumfold.eq_instance_weights, - field_cfg, - )?, + columns: folded_public_columns, bit_slices: fold_optional_mle_tables( "public.bit_slices", publics.iter().map(|public| public.bit_slices.as_ref()), @@ -1701,7 +1697,7 @@ where } struct RelationSumFoldPrefixFastPath { - traces: Box<[ProjectedTrace]>, + tail_traces: Option]>>, beta: Vec, booleanity_sources: Vec, linear: BinaryPrefixTailTable, @@ -1739,17 +1735,21 @@ where prefix_vars: usize, field_cfg: &F::Config, ) -> Result { - Self::new_owned( - traces.to_vec().into_boxed_slice(), + let coeff_tables = build_linear_residual_coeff_tables(traces, publics, r_ic, field_cfg)?; + let row_weights = build_eq_x_r_vec(r_ic, field_cfg)?; + Self::new_with_linear_cache( + traces, publics, beta, r_ic, + &row_weights, a, lambda, rho, xi, booleanity_sources, prefix_vars, + &coeff_tables, field_cfg, ) } @@ -1787,6 +1787,77 @@ where ) } + #[allow(clippy::too_many_arguments)] + fn new_with_linear_cache( + traces: &[ProjectedTrace], + publics: &[ProjectedPublic], + beta: &[F], + _r_ic: &[F; SHA_ROW_VARS], + row_weights: &[F], + a: &F, + lambda: &F, + rho: &F, + xi: &F, + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + coeff_tables: &[LinearResidualCoeffTable], + field_cfg: &F::Config, + ) -> Result { + let ell = validate_sha_sumfold_inputs(traces, publics, beta)?; + if prefix_vars == 0 || prefix_vars > ell { + return Err(SumFoldError::Ell0TooLarge { + ell0: prefix_vars, + ell, + } + .into()); + } + if coeff_tables.len() != traces.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: coeff_tables.len(), + expected: traces.len(), + }); + } + if row_weights.len() != SHA_ROW_COUNT { + return Err(ShaProjectionError::ColumnRowCount { + kind: "row_weights", + col: 0, + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } + + let tail_vars = ell - prefix_vars; + let tail_len = binary_len(tail_vars); + let a_powers = build_sha_residual_eval_powers(a, field_cfg); + let lambda_powers = build_sha_lambda_powers(lambda, field_cfg); + let booleanity_weights = + build_booleanity_weights(rho, xi, booleanity_sources.len(), field_cfg); + let linear_values = build_sha_sumfold_linear_accumulator( + coeff_tables, + &a_powers, + &lambda_powers, + field_cfg, + )?; + let quadratic_values = build_sha_booleanity_prefix_tail_table( + traces, + booleanity_sources, + prefix_vars, + tail_len, + row_weights, + &booleanity_weights, + field_cfg, + ); + Self::new_with_accumulators( + traces, + beta, + &linear_values, + &quadratic_values?, + booleanity_sources, + prefix_vars, + field_cfg, + ) + } + #[allow(clippy::too_many_arguments)] fn new_owned_with_linear_cache( traces: Box<[ProjectedTrace]>, @@ -1858,8 +1929,8 @@ where ) } - fn new_owned_with_accumulators( - traces: Box<[ProjectedTrace]>, + fn new_with_accumulators( + traces: &[ProjectedTrace], beta: &[F], linear_values: &[F], quadratic_values: &[F], @@ -1867,7 +1938,7 @@ where prefix_vars: usize, field_cfg: &F::Config, ) -> Result { - let ell = validate_sha_sumfold_traces(&traces, beta)?; + let ell = validate_sha_sumfold_traces(traces, beta)?; if prefix_vars == 0 || prefix_vars > ell { return Err(SumFoldError::Ell0TooLarge { ell0: prefix_vars, @@ -1904,8 +1975,14 @@ where .push(eq_weights_or_one(&beta[round + 1..prefix_vars], field_cfg)?); } + let tail_traces = if tail_vars == 0 { + None + } else { + Some(traces.to_vec().into_boxed_slice()) + }; + Ok(Self { - traces, + tail_traces, beta: beta.to_vec(), booleanity_sources: booleanity_sources.to_vec(), linear, @@ -1918,6 +1995,30 @@ where }) } + fn new_owned_with_accumulators( + traces: Box<[ProjectedTrace]>, + beta: &[F], + linear_values: &[F], + quadratic_values: &[F], + booleanity_sources: &[ShaBooleanitySource], + prefix_vars: usize, + field_cfg: &F::Config, + ) -> Result { + let mut fast_path = Self::new_with_accumulators( + &traces, + beta, + linear_values, + quadratic_values, + booleanity_sources, + prefix_vars, + field_cfg, + )?; + if fast_path.beta.len() > fast_path.total_prefix_vars { + fast_path.tail_traces = Some(traces); + } + Ok(fast_path) + } + #[allow(clippy::arithmetic_side_effects)] fn bind_previous_round( &mut self, @@ -1998,8 +2099,12 @@ where zero_inner.clone(), )); + let traces = self + .tail_traces + .as_deref() + .expect("tail traces must be present when tail variables remain"); let source_tail_values = bind_sha_booleanity_sources_to_prefix( - &self.traces, + traces, &self.booleanity_sources, self.total_prefix_vars, tail_len, @@ -2343,8 +2448,8 @@ where ); } - let fast_path = RelationSumFoldPrefixFastPath::new_owned_with_accumulators( - traces.to_vec().into_boxed_slice(), + let fast_path = RelationSumFoldPrefixFastPath::new_with_accumulators( + traces, beta, linear_accumulator, quadratic_prefix_accumulator, @@ -2537,8 +2642,8 @@ where ); } - let fast_path = RelationSumFoldPrefixFastPath::new_owned_with_linear_cache( - traces.to_vec().into_boxed_slice(), + let fast_path = RelationSumFoldPrefixFastPath::new_with_linear_cache( + traces, publics, beta, r_ic, @@ -3425,6 +3530,19 @@ where expected: SHA_ROW_COUNT, }); } + sha_word_bits_at_point_with_weights_unchecked(trace, col, shift, row_weights, field_cfg) +} + +pub fn sha_word_bits_at_point_with_weights_unchecked( + trace: &ProjectedTrace, + col: ShaWordCol, + shift: usize, + row_weights: &[F], + field_cfg: &F::Config, +) -> Result<[F; SHA_WORD_BITS], ShaProjectionError> +where + F: PrimeField, +{ let mut bits: [F; SHA_WORD_BITS] = std::array::from_fn(|_| F::zero_with_cfg(field_cfg)); for (row, row_weight) in row_weights.iter().enumerate() { for (bit, out) in bits.iter_mut().enumerate() { @@ -3515,6 +3633,18 @@ where expected: SHA_ROW_COUNT, }); } + sha_int_at_point_with_weights_unchecked(trace, col, row_weights, field_cfg) +} + +pub fn sha_int_at_point_with_weights_unchecked( + trace: &ProjectedTrace, + col: ShaIntCol, + row_weights: &[F], + field_cfg: &F::Config, +) -> Result +where + F: PrimeField, +{ let mut value = F::zero_with_cfg(field_cfg); for (row, row_weight) in row_weights.iter().enumerate() { value += row_weight.clone() * int_scalar(trace, col, row, field_cfg)?; @@ -3746,6 +3876,10 @@ fn validate_trace(trace: &ProjectedTrace) -> Result<(), ShaProjectionError validate_table("public_columns", &trace.public_columns, ShaPublicCol::COUNT) } +pub fn validate_projected_trace(trace: &ProjectedTrace) -> Result<(), ShaProjectionError> { + validate_trace(trace) +} + fn validate_public(public: &ProjectedPublic) -> Result<(), ShaProjectionError> { validate_table("public.columns", &public.columns, ShaPublicCol::COUNT)?; if let Some(bit_slices) = &public.bit_slices { diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index 29a8fcd3..b046a5ef 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -673,6 +673,20 @@ fn projection_sha_flatten_bit_columns(columns: Vec>>) -> MleTable< projection_sha_mle_table_from_columns(flattened) } +fn projection_sha_flatten_bit_column_refs(columns: &[&[Vec]]) -> MleTable { + let mut flattened = (0..columns.len() * SHA_WORD_BITS) + .map(|_| Vec::with_capacity(SHA_ROW_COUNT)) + .collect::>(); + for (col_idx, rows) in columns.iter().enumerate() { + for bits in rows.iter() { + for (bit, value) in bits.iter().enumerate() { + flattened[bit_slice_index(col_idx, bit, SHA_WORD_BITS)].push(value.clone()); + } + } + } + projection_sha_mle_table_from_columns(flattened) +} + fn projection_sha_scalarize_bit_slices( bit_slices: &MleTable, a: &F, @@ -795,15 +809,16 @@ impl ProductionShaProjectionAdapter )?; let public_columns = projection_sha_projected_public_from_sources(&pa_a, &pa_e, &message, field_cfg); + let public_bit_columns = [ + pa_a.as_slice(), + pa_e.as_slice(), + pa_a.as_slice(), + pa_e.as_slice(), + message.as_slice(), + ]; Ok(ProjectedPublic { columns: public_columns, - bit_slices: Some(projection_sha_flatten_bit_columns(vec![ - pa_a.clone(), - pa_e.clone(), - pa_a, - pa_e, - message, - ])), + bit_slices: Some(projection_sha_flatten_bit_column_refs(&public_bit_columns)), }) } @@ -847,14 +862,18 @@ impl ProductionShaProjectionAdapter sha256_cols::PA_C_FF_E, ]; - let bit_columns = word_sources + let word_cols = word_sources .iter() - .map(|&col| { - projection_sha_project_binary_source( - projection_sha_binary_col(public_trace, witness_trace, col)?, - field_cfg, - ) - }) + .map(|&col| projection_sha_binary_col(public_trace, witness_trace, col)) + .collect::, _>>()?; + let int_cols = int_sources + .iter() + .map(|&col| projection_sha_int_col(public_trace, witness_trace, col)) + .collect::, _>>()?; + + let bit_columns = word_cols + .iter() + .map(|&col| projection_sha_project_binary_source(col, field_cfg)) .collect::, _>>()?; let bit_slices = projection_sha_flatten_bit_columns(bit_columns); let scalarized = projection_sha_scalarize_bit_slices( @@ -876,15 +895,17 @@ impl ProductionShaProjectionAdapter )?; let public_columns = projection_sha_projected_public_from_sources(&pa_a, &pa_e, &message, field_cfg); - let int_columns = int_sources + let int_columns = int_cols .iter() - .map(|&col| { - projection_sha_project_int_source( - projection_sha_int_col(public_trace, witness_trace, col)?, - field_cfg, - ) - }) + .map(|&col| projection_sha_project_int_source(col, field_cfg)) .collect::, _>>()?; + let public_bit_columns = [ + pa_a.as_slice(), + pa_e.as_slice(), + pa_a.as_slice(), + pa_e.as_slice(), + message.as_slice(), + ]; let trace = ProjectedTrace { bit_slices, @@ -894,33 +915,27 @@ impl ProductionShaProjectionAdapter }; let public = ProjectedPublic { columns: public_columns, - bit_slices: Some(projection_sha_flatten_bit_columns(vec![ - pa_a.clone(), - pa_e.clone(), - pa_a, - pa_e, - message, - ])), + bit_slices: Some(projection_sha_flatten_bit_column_refs(&public_bit_columns)), }; Ok(( trace, public, ProductionShaWitnessPolys { - binary: word_sources + binary: word_cols .iter() .map(|&col| { projection_sha_truncate_row_domain( - projection_sha_binary_col(public_trace, witness_trace, col)?, + col, "SHA binary witness row-domain projection", ) }) .collect::, _>>()?, arbitrary: Vec::new(), - int: int_sources + int: int_cols .iter() .map(|&col| { projection_sha_truncate_row_domain( - projection_sha_int_col(public_trace, witness_trace, col)?, + col, "SHA int witness row-domain projection", ) }) diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 586b31b0..e6e3cf58 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -39,8 +39,10 @@ use zinc_piop::{ build_sha_sumfold_quadratic_prefix_accumulator, derive_instance_fold_claim, expression_folded_row_sum_with_row_weights, expression_folded_row_sum_with_vectors, fold_projected_traces, folded_row_integrand_sum, production_sha_booleanity_sources, - production_sha_nonzero_families, sha_int_at_point_with_weights, sha_public_at_point, + production_sha_nonzero_families, sha_int_at_point_with_weights, + sha_int_at_point_with_weights_unchecked, sha_public_at_point, sha_public_at_point_with_weights, sha_word_bits_at_point_with_weights, + sha_word_bits_at_point_with_weights_unchecked, validate_projected_trace, verify_folded_row_sumcheck_claim, verify_fresh_sha_ideal_polys, }, sumcheck::{ @@ -1377,7 +1379,7 @@ where let ideal_check = IdealCheckProof { combined_mle_values: aggregate_ideal_polys.iter().cloned().collect(), }; - let aggregate_ideal_polys = aggregate_sha_ideal_polys_from_proof(&ideal_check)?; + #[cfg(debug_assertions)] check_aggregate_sha_ideal_membership(&aggregate_ideal_polys, field_cfg)?; absorb_aggregate_sha_ideal_polys(transcript, &aggregate_ideal_polys, field_cfg); Ok((ideal_check, aggregate_ideal_polys)) @@ -3357,16 +3359,22 @@ where expected: SHA_ROW_COUNT, }); } + validate_projected_trace(folded_trace)?; let mut lifted = Vec::with_capacity(ShaWordCol::COUNT + ShaIntCol::COUNT); for col in ShaWordCol::ALL { - let coeffs = - sha_word_bits_at_point_with_weights(folded_trace, col, 0, row_weights, field_cfg)? - .to_vec(); + let coeffs = sha_word_bits_at_point_with_weights_unchecked( + folded_trace, + col, + 0, + row_weights, + field_cfg, + )? + .to_vec(); lifted.push(DynamicPolynomialF::new_trimmed(coeffs)); } for col in ShaIntCol::ALL { lifted.push(DynamicPolynomialF::new_trimmed([ - sha_int_at_point_with_weights(folded_trace, col, row_weights, field_cfg)?, + sha_int_at_point_with_weights_unchecked(folded_trace, col, row_weights, field_cfg)?, ])); } Ok(lifted) From 6233683aef4c9202181af16218d79f030b6262a5 Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 06:56:50 -0700 Subject: [PATCH 32/49] Parallelize ProjectionFold SHA witness plumbing --- piop/src/neutron_nova/projection_sha.rs | 129 ++++++++++++++---------- protocol/Cargo.toml | 2 +- protocol/benches/e2e.rs | 122 +++++++++++----------- protocol/src/production_sha.rs | 56 ++++++---- test-uair/src/sha256.rs | 92 +++++++++-------- zip-plus/src/pcs/hyrax.rs | 40 +++++--- 6 files changed, 250 insertions(+), 191 deletions(-) diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 993fc852..5cd4f936 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -14,6 +14,8 @@ use crate::{ }; use crypto_primitives::{PrimeField, crypto_bigint_uint::Uint}; use num_traits::{ConstZero, Zero}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; use thiserror::Error; use zinc_poly::{ mle::DenseMultilinearExtension, @@ -25,7 +27,7 @@ use zinc_uair::{ ideal_collector::IdealOrZero, }; use zinc_utils::{ - UNCHECKED, + UNCHECKED, cfg_chunks, cfg_into_iter, cfg_iter, delayed_reduction::{ BarrettDelayedReduction, DelayedFieldProductSum, DelayedModularReductionAlgorithm, MontgomeryLimbs, @@ -456,8 +458,7 @@ where col: a_powers.len(), }); } - tables - .iter() + cfg_iter!(tables) .map(|table| { if table.coeffs.len() != NUM_SHA_RESIDUAL_FAMILIES { return Err(ShaProjectionError::MissingColumn { @@ -850,9 +851,8 @@ where expected: SHA_ROW_COUNT, }); } - traces - .iter() - .zip(publics) + cfg_iter!(traces) + .zip(cfg_iter!(publics)) .map(|(trace, public)| { validate_trace(trace)?; validate_public(public)?; @@ -3013,34 +3013,49 @@ where let needs_virtuals = sources_need_virtuals(booleanity_sources); let coeff_plans = ternary_coeff_plans(prefix_vars)?; - let mut source_values = - vec![F::zero_with_cfg(field_cfg); prefix_len * booleanity_sources.len()]; - for tail in 0..tail_len { - for (row, row_weight) in row_weights.iter().enumerate().take(SHA_ROW_COUNT) { - fill_booleanity_source_prefix_values( - traces, - booleanity_sources, - prefix_vars, - tail, - row, - needs_virtuals, - &mut source_values, - field_cfg, - )?; - - for (source_idx, booleanity_weight) in booleanity_weights.iter().enumerate() { - let source_weight = row_weight.clone() * booleanity_weight; - for (ternary_idx, plan) in coeff_plans.iter().enumerate() { - let coeff = booleanity_degree_two_coeff( - &source_values, - booleanity_sources.len(), - source_idx, - plan, + let source_count = booleanity_sources.len(); + let partials = cfg_chunks!(row_weights, 8) + .enumerate() + .map(|(chunk_idx, row_weight_chunk)| { + let row_offset = chunk_idx * 8; + let mut partial = vec![F::zero_with_cfg(field_cfg); ternary_len * tail_len]; + let mut source_values = vec![F::zero_with_cfg(field_cfg); prefix_len * source_count]; + for tail in 0..tail_len { + for (row_in_chunk, row_weight) in row_weight_chunk.iter().enumerate() { + let row = row_offset + row_in_chunk; + fill_booleanity_source_prefix_values( + traces, + booleanity_sources, + prefix_vars, + tail, + row, + needs_virtuals, + &mut source_values, field_cfg, - ); - table[tail * ternary_len + ternary_idx] += source_weight.clone() * coeff; + )?; + + for (source_idx, booleanity_weight) in booleanity_weights.iter().enumerate() { + let source_weight = row_weight.clone() * booleanity_weight; + for (ternary_idx, plan) in coeff_plans.iter().enumerate() { + let coeff = booleanity_degree_two_coeff( + &source_values, + source_count, + source_idx, + plan, + field_cfg, + ); + partial[tail * ternary_len + ternary_idx] += + source_weight.clone() * coeff; + } + } } } + Ok(partial) + }) + .collect::, ShaProjectionError>>()?; + for partial in partials { + for (acc, value) in table.iter_mut().zip(partial) { + *acc += value; } } Ok(table) @@ -4368,37 +4383,41 @@ where let Some(first) = tables.first() else { return Ok(Vec::new()); }; - let mut out = first - .iter() - .map(|column| DenseMultilinearExtension { - evaluations: vec![F::zero_with_cfg(field_cfg); column.evaluations.len()], - num_vars: column.num_vars, - }) - .collect::>(); - for (table, weight) in tables.iter().zip(theta) { + for table in &tables { if table.len() != first.len() { return Err(ShaProjectionError::InstanceCountMismatch { got: table.len(), expected: first.len(), }); } - for (col_idx, col) in table.iter().enumerate() { - if col.num_vars != out[col_idx].num_vars - || col.evaluations.len() != out[col_idx].evaluations.len() - { - return Err(ShaProjectionError::ColumnRowCount { - kind, - col: col_idx, - got: col.evaluations.len(), - expected: out[col_idx].evaluations.len(), - }); - } - for (out, value) in out[col_idx].evaluations.iter_mut().zip(&col.evaluations) { - *out += weight.clone() * value; - } - } } - Ok(out) + cfg_into_iter!(0..first.len()) + .map(|col_idx| { + let template = &first[col_idx]; + let mut evaluations = + vec![F::zero_with_cfg(field_cfg); template.evaluations.len()]; + for (table, weight) in tables.iter().zip(theta) { + let col = &table[col_idx]; + if col.num_vars != template.num_vars + || col.evaluations.len() != template.evaluations.len() + { + return Err(ShaProjectionError::ColumnRowCount { + kind, + col: col_idx, + got: col.evaluations.len(), + expected: template.evaluations.len(), + }); + } + for (out, value) in evaluations.iter_mut().zip(&col.evaluations) { + *out += weight.clone() * value; + } + } + Ok(DenseMultilinearExtension { + evaluations, + num_vars: template.num_vars, + }) + }) + .collect::, ShaProjectionError>>() } fn fold_optional_mle_tables<'a, F, I>( diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 4ff0965c..013985b4 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -42,7 +42,7 @@ workspace = true [features] -parallel = ["dep:rayon", "zip-plus/parallel", "zinc-piop/parallel", "zinc-poly/parallel", "zinc-uair/parallel", "zinc-utils/parallel"] +parallel = ["dep:rayon", "zip-plus/parallel", "zinc-piop/parallel", "zinc-poly/parallel", "zinc-uair/parallel", "zinc-utils/parallel", "zinc-test-uair/parallel"] simd = ["zinc-poly/simd", "zinc-piop/simd", "zip-plus/simd"] unchecked = [] # Switch the IPRS bench code from inverse-rate 4 (rate 1/4) to inverse-rate 8 diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index b046a5ef..9ab1bbf9 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -11,6 +11,8 @@ use crypto_primitives::{ PrimeField, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, }; use rand::rng; +#[cfg(feature = "parallel")] +use rayon::prelude::*; use std::{borrow::Cow, fmt::Debug, hint::black_box, marker::PhantomData, ops::Neg}; use zinc_piop::neutron_nova::{ MleTable, ProjectedPublic, ProjectedTrace, SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, @@ -58,6 +60,7 @@ use zinc_uair::{ ideal_collector::IdealOrZero, }; use zinc_utils::{ + cfg_into_iter, cfg_iter, from_ref::FromRef, inner_product::{InnerProduct, MBSInnerProduct, ScalarProduct}, mul_by_scalar::MulByScalar, @@ -583,10 +586,8 @@ fn projection_sha_project_binary_source( expected: SHA_ROW_COUNT, }); } - Ok(col - .evaluations - .iter() - .take(SHA_ROW_COUNT) + let rows = &col.evaluations[..SHA_ROW_COUNT]; + Ok(cfg_iter!(rows) .map(|poly| { poly.iter() .take(SHA_WORD_BITS) @@ -613,15 +614,13 @@ fn projection_sha_project_int_source( expected: SHA_ROW_COUNT, }); } - Ok(col - .evaluations - .iter() - .take(SHA_ROW_COUNT) + let rows = &col.evaluations[..SHA_ROW_COUNT]; + Ok(cfg_iter!(rows) .map(|value| F::from_with_cfg(value, field_cfg)) .collect()) } -fn projection_sha_truncate_row_domain( +fn projection_sha_truncate_row_domain( col: &DenseMultilinearExtension, label: &'static str, ) -> Result, ProductionShaError> { @@ -633,7 +632,9 @@ fn projection_sha_truncate_row_domain( }); } Ok(DenseMultilinearExtension { - evaluations: col.evaluations[..SHA_ROW_COUNT].to_vec(), + evaluations: cfg_iter!(&col.evaluations[..SHA_ROW_COUNT]) + .cloned() + .collect(), num_vars: SHA_ROW_VARS, }) } @@ -659,31 +660,35 @@ fn projection_sha_mle_table_from_columns(columns: Vec>) -> MleTable .collect() } -fn projection_sha_flatten_bit_columns(columns: Vec>>) -> MleTable { - let mut flattened = (0..columns.len() * SHA_WORD_BITS) - .map(|_| Vec::with_capacity(SHA_ROW_COUNT)) +fn projection_sha_flatten_bit_columns( + columns: Vec>>, +) -> MleTable { + let flattened = cfg_into_iter!(0..columns.len() * SHA_WORD_BITS) + .map(|flat_idx| { + let col_idx = flat_idx / SHA_WORD_BITS; + let bit = flat_idx % SHA_WORD_BITS; + columns[col_idx] + .iter() + .map(|row_bits| row_bits[bit].clone()) + .collect::>() + }) .collect::>(); - for (col_idx, rows) in columns.into_iter().enumerate() { - for bits in rows { - for (bit, value) in bits.into_iter().enumerate() { - flattened[bit_slice_index(col_idx, bit, SHA_WORD_BITS)].push(value); - } - } - } projection_sha_mle_table_from_columns(flattened) } -fn projection_sha_flatten_bit_column_refs(columns: &[&[Vec]]) -> MleTable { - let mut flattened = (0..columns.len() * SHA_WORD_BITS) - .map(|_| Vec::with_capacity(SHA_ROW_COUNT)) +fn projection_sha_flatten_bit_column_refs( + columns: &[&[Vec]], +) -> MleTable { + let flattened = cfg_into_iter!(0..columns.len() * SHA_WORD_BITS) + .map(|flat_idx| { + let col_idx = flat_idx / SHA_WORD_BITS; + let bit = flat_idx % SHA_WORD_BITS; + columns[col_idx] + .iter() + .map(|row_bits| row_bits[bit].clone()) + .collect::>() + }) .collect::>(); - for (col_idx, rows) in columns.iter().enumerate() { - for bits in rows.iter() { - for (bit, value) in bits.iter().enumerate() { - flattened[bit_slice_index(col_idx, bit, SHA_WORD_BITS)].push(value.clone()); - } - } - } projection_sha_mle_table_from_columns(flattened) } @@ -694,26 +699,29 @@ fn projection_sha_scalarize_bit_slices( ) -> Result, ProductionShaError> { let powers = zinc_utils::powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); let word_count = bit_slices.len() / SHA_WORD_BITS; - let mut words = Vec::with_capacity(word_count); - for col_idx in 0..word_count { - let mut out_col = Vec::with_capacity(SHA_ROW_COUNT); - for row in 0..SHA_ROW_COUNT { - let mut value = F::zero_with_cfg(field_cfg); - for (bit, power) in powers.iter().enumerate() { - let bit_col = &bit_slices[bit_slice_index(col_idx, bit, SHA_WORD_BITS)]; - if bit_col.num_vars != SHA_ROW_VARS || bit_col.evaluations.len() != SHA_ROW_COUNT { - return Err(ProductionShaError::LengthMismatch { - label: "SHA scalarized bit-slice rows", - got: bit_col.evaluations.len(), - expected: SHA_ROW_COUNT, - }); + let words = cfg_into_iter!(0..word_count) + .map(|col_idx| { + let mut out_col = Vec::with_capacity(SHA_ROW_COUNT); + for row in 0..SHA_ROW_COUNT { + let mut value = F::zero_with_cfg(field_cfg); + for (bit, power) in powers.iter().enumerate() { + let bit_col = &bit_slices[bit_slice_index(col_idx, bit, SHA_WORD_BITS)]; + if bit_col.num_vars != SHA_ROW_VARS + || bit_col.evaluations.len() != SHA_ROW_COUNT + { + return Err(ProductionShaError::LengthMismatch { + label: "SHA scalarized bit-slice rows", + got: bit_col.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + value += bit_col.evaluations[row].clone() * power; } - value += bit_col.evaluations[row].clone() * power; + out_col.push(value); } - out_col.push(value); - } - words.push(out_col); - } + Ok(out_col) + }) + .collect::, ProductionShaError>>()?; Ok(projection_sha_mle_table_from_columns(words)) } @@ -862,17 +870,14 @@ impl ProductionShaProjectionAdapter sha256_cols::PA_C_FF_E, ]; - let word_cols = word_sources - .iter() + let word_cols = cfg_iter!(&word_sources) .map(|&col| projection_sha_binary_col(public_trace, witness_trace, col)) .collect::, _>>()?; - let int_cols = int_sources - .iter() + let int_cols = cfg_iter!(&int_sources) .map(|&col| projection_sha_int_col(public_trace, witness_trace, col)) .collect::, _>>()?; - let bit_columns = word_cols - .iter() + let bit_columns = cfg_iter!(&word_cols) .map(|&col| projection_sha_project_binary_source(col, field_cfg)) .collect::, _>>()?; let bit_slices = projection_sha_flatten_bit_columns(bit_columns); @@ -895,8 +900,7 @@ impl ProductionShaProjectionAdapter )?; let public_columns = projection_sha_projected_public_from_sources(&pa_a, &pa_e, &message, field_cfg); - let int_columns = int_cols - .iter() + let int_columns = cfg_iter!(&int_cols) .map(|&col| projection_sha_project_int_source(col, field_cfg)) .collect::, _>>()?; let public_bit_columns = [ @@ -921,8 +925,7 @@ impl ProductionShaProjectionAdapter trace, public, ProductionShaWitnessPolys { - binary: word_cols - .iter() + binary: cfg_iter!(&word_cols) .map(|&col| { projection_sha_truncate_row_domain( col, @@ -931,8 +934,7 @@ impl ProductionShaProjectionAdapter }) .collect::, _>>()?, arbitrary: Vec::new(), - int: int_cols - .iter() + int: cfg_iter!(&int_cols) .map(|&col| { projection_sha_truncate_row_domain( col, diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index e6e3cf58..039db63d 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -17,6 +17,8 @@ use crate::{ use ark_ec::AffineRepr; use crypto_primitives::{FromPrimitiveWithConfig, PrimeField}; use num_traits::{ConstZero, Zero}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; use thiserror::Error; use zinc_piop::{ combined_poly_resolver::Proof as CombinedPolyResolverProof, @@ -64,8 +66,9 @@ use zinc_transcript::Blake3Transcript; use zinc_transcript::traits::{Transcribable, Transcript}; use zinc_uair::{ShiftSpec, Uair, UairSignature, UairTrace, UairWitness}; use zinc_utils::{ - UNCHECKED, delayed_reduction::DelayedFieldProductSum, inner_product::FieldFieldInnerProduct, - inner_product::InnerProduct, inner_transparent_field::InnerTransparentField, + UNCHECKED, cfg_iter, delayed_reduction::DelayedFieldProductSum, + inner_product::FieldFieldInnerProduct, inner_product::InnerProduct, + inner_transparent_field::InnerTransparentField, }; use zip_plus::{ ZipError, @@ -1081,7 +1084,7 @@ pub fn prove_linear_ideal_fold( LinearIdealFoldError, > where - U: Uair + ProductionShaProjectionAdapter, + U: Uair + ProductionShaProjectionAdapter + Sync, Zt: ZincTypes, F: InnerTransparentField + DelayedFieldProductSum @@ -1287,7 +1290,7 @@ fn prove_fresh_instances_phase( field_cfg: &F::Config, ) -> Result, ProductionShaError> where - U: Uair + ProductionShaProjectionAdapter, + U: Uair + ProductionShaProjectionAdapter + Sync, Zt: ZincTypes, F: PrimeField, P: ZincPCSTypes, @@ -1295,26 +1298,41 @@ where P::ArbitraryPCS: FoldablePCS, D>, P::IntPCS: FoldablePCS, { - let mut fresh_instances = Vec::with_capacity(witnesses.len()); - let mut instance_commitments = Vec::with_capacity(witnesses.len()); - let mut instance_prover_data = Vec::with_capacity(witnesses.len()); - let mut traces = Vec::with_capacity(witnesses.len()); - let mut publics = Vec::with_capacity(witnesses.len()); - for (instance_idx, witness) in witnesses.iter().enumerate() { let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; - let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; absorb_public_uair_trace::(transcript, instance_idx, &public_trace); + } - let (trace, public, witness_polys) = - U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg)?; - let (data, commitment) = - commit_production_sha_instance::(pcs_params, &witness_polys)?; + let artifacts = cfg_iter!(witnesses) + .map(|witness| { + let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; + let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; + let (trace, public, witness_polys) = + U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg)?; + let (data, commitment) = + commit_production_sha_instance::(pcs_params, &witness_polys)?; - fresh_instances.push(UairInstance { - public_trace: own_uair_trace(&public_trace), - commitments: commitment.clone(), - }); + Ok(( + UairInstance { + public_trace: own_uair_trace(&public_trace), + commitments: commitment.clone(), + }, + commitment, + data, + trace, + public, + )) + }) + .collect::, ProductionShaError>>()?; + + let mut fresh_instances = Vec::with_capacity(artifacts.len()); + let mut instance_commitments = Vec::with_capacity(artifacts.len()); + let mut instance_prover_data = Vec::with_capacity(artifacts.len()); + let mut traces = Vec::with_capacity(artifacts.len()); + let mut publics = Vec::with_capacity(artifacts.len()); + + for (fresh_instance, commitment, data, trace, public) in artifacts { + fresh_instances.push(fresh_instance); instance_commitments.push(commitment); instance_prover_data.push(data); traces.push(trace); diff --git a/test-uair/src/sha256.rs b/test-uair/src/sha256.rs index aa37580a..de6c6e80 100644 --- a/test-uair/src/sha256.rs +++ b/test-uair/src/sha256.rs @@ -1737,10 +1737,10 @@ where // across compression-junction boundaries: the booleanity // sumcheck checks every row, including ones the spec doesn't // care about. - let u_ef_vals: Vec = (0..n) + let u_ef_vals: Vec = cfg_into_iter!(0..n) .map(|t| if t >= 1 { e_vals[t] & e_vals[t - 1] } else { 0 }) .collect(); - let u_neg_e_g_vals: Vec = (0..n) + let u_neg_e_g_vals: Vec = cfg_into_iter!(0..n) .map(|t| { if t >= 2 { (!e_vals[t]) & e_vals[t - 2] @@ -1749,7 +1749,7 @@ where } }) .collect(); - let maj_vals: Vec = (0..n) + let maj_vals: Vec = cfg_into_iter!(0..n) .map(|t| { if t >= 2 { maj(a_vals[t], a_vals[t - 1], a_vals[t - 2]) @@ -1775,44 +1775,44 @@ where // ⇒ comp_maj[k] = AND(a[k], a[k+1]). // r_maj at k = n-1: a[k+1] = a[k+2] = 0, residual = a[k] ∈ // {0,1} already; comp_maj = 0. - let mut pa_r_ch2_comp_vals: Vec = vec![0; n]; - let mut pa_r_maj_comp_vals: Vec = vec![0; n]; - for k in 0..n { - let off_kp1 = k + 1 >= n; - let off_kp2 = k + 2 >= n; - if off_kp2 { - pa_r_ch2_comp_vals[k] = e_vals[k]; - } - if off_kp2 && !off_kp1 { - pa_r_maj_comp_vals[k] = a_vals[k] & a_vals[k + 1]; - } - } + let pa_r_ch2_comp_vals: Vec = cfg_into_iter!(0..n) + .map(|k| if k + 2 >= n { e_vals[k] } else { 0 }) + .collect(); + let pa_r_maj_comp_vals: Vec = cfg_into_iter!(0..n) + .map(|k| { + if k + 2 >= n && k + 1 < n { + a_vals[k] & a_vals[k + 1] + } else { + 0 + } + }) + .collect(); // Derived values. - let sig0_vals: Vec = a_vals.iter().copied().map(big_sigma0).collect(); - let sig1_vals: Vec = e_vals.iter().copied().map(big_sigma1).collect(); - let lsig0_vals: Vec = w_vals.iter().copied().map(small_sigma0).collect(); - let lsig1_vals: Vec = w_vals.iter().copied().map(small_sigma1).collect(); - - let ov_sig0_vals: Vec = a_vals - .iter() - .zip(&sig0_vals) - .map(|(&a, &s)| sigma0_overflow(a, s)) + let sig0_vals: Vec = cfg_into_iter!(0..n) + .map(|t| big_sigma0(a_vals[t])) + .collect(); + let sig1_vals: Vec = cfg_into_iter!(0..n) + .map(|t| big_sigma1(e_vals[t])) + .collect(); + let lsig0_vals: Vec = cfg_into_iter!(0..n) + .map(|t| small_sigma0(w_vals[t])) .collect(); - let ov_sig1_vals: Vec = e_vals - .iter() - .zip(&sig1_vals) - .map(|(&e, &s)| sigma1_overflow(e, s)) + let lsig1_vals: Vec = cfg_into_iter!(0..n) + .map(|t| small_sigma1(w_vals[t])) .collect(); - let ov_lsig0_vals: Vec = w_vals - .iter() - .zip(&lsig0_vals) - .map(|(&w, &l)| lsig0_overflow(w, l)) + + let ov_sig0_vals: Vec = cfg_into_iter!(0..n) + .map(|t| sigma0_overflow(a_vals[t], sig0_vals[t])) + .collect(); + let ov_sig1_vals: Vec = cfg_into_iter!(0..n) + .map(|t| sigma1_overflow(e_vals[t], sig1_vals[t])) .collect(); - let ov_lsig1_vals: Vec = w_vals - .iter() - .zip(&lsig1_vals) - .map(|(&w, &l)| lsig1_overflow(w, l)) + let ov_lsig0_vals: Vec = cfg_into_iter!(0..n) + .map(|t| lsig0_overflow(w_vals[t], lsig0_vals[t])) + .collect(); + let ov_lsig1_vals: Vec = cfg_into_iter!(0..n) + .map(|t| lsig1_overflow(w_vals[t], lsig1_vals[t])) .collect(); // The σ_0/σ_1 right-shift decomposition columns S_i / T_i are @@ -1829,7 +1829,7 @@ where // bits 0-1: mu_W, 2-4: mu_a, 5-7: mu_e, 8: mu_ff_a, 9: mu_ff_e. // Positions 10..31 stay 0 (pinned by C22's high-bits-zero // assert_zero on ShiftR(10)(W_MU_PACKED)). - let w_mu_packed_vals: Vec = (0..n) + let w_mu_packed_vals: Vec = cfg_into_iter!(0..n) .map(|k| { (mu_w_vals[k] & 0b11) | ((mu_a_vals[k] & 0b111) << 2) @@ -1840,7 +1840,9 @@ where .collect(); let to_bits = |v: &[u32]| -> Vec> { - v.iter().copied().map(BinaryPoly::<32>::from).collect() + cfg_into_iter!(0..v.len()) + .map(|idx| BinaryPoly::<32>::from(v[idx])) + .collect() }; let to_bin_mle = |col: Vec>| -> DenseMultilinearExtension> { @@ -1931,7 +1933,9 @@ where // junction window where the feed-forward addition holds // honestly.) - let k_col: Vec = k_vals.iter().copied().map(R::from).collect(); + let k_col: Vec = cfg_into_iter!(0..n) + .map(|idx| R::from(k_vals[idx])) + .collect(); // mu_w_vals / mu_a_vals / mu_e_vals / mu_junction_{a,e}_vals // are no longer materialized as separate int columns — they're // packed into the W_MU_PACKED binary_poly column above. @@ -1954,7 +1958,7 @@ where // C7: inner(2) = w_W[k+16] − w_W[k] − lsig0[k+1] − w_W[k+9] // − lsig1[k+14] + 2^32 · mu_W[k+16] - let pa_c_c7_col: Vec = (0..n) + let pa_c_c7_col: Vec = cfg_into_iter!(0..n) .map(|k| { let w_k16 = load(&w_vals, k + 16); let w_k = load(&w_vals, k); @@ -1975,7 +1979,7 @@ where // C8: inner(2) = w_a[k+4] − w_e[k] − sig1[k+3] − Ch[k+3] − K[k+3] // − W[k] − sig0[k+3] − maj[k+3] + 2^32 · mu_a[k] // with Ch[k+3] = u_ef[k+3] + u_{¬e,g}[k+3]. - let pa_c_c8_col: Vec = (0..n) + let pa_c_c8_col: Vec = cfg_into_iter!(0..n) .map(|k| { let w_a_k4 = load(&a_vals, k + 4); let w_e_k = load(&e_vals, k); @@ -1999,7 +2003,7 @@ where // C9: inner(2) = w_e[k+4] − w_a[k] − w_e[k] − sig1[k+3] − Ch[k+3] // − K[k+3] − W[k] + 2^32 · mu_e[k] // with Ch[k+3] = u_ef[k+3] + u_{¬e,g}[k+3]. - let pa_c_c9_col: Vec = (0..n) + let pa_c_c9_col: Vec = cfg_into_iter!(0..n) .map(|k| { let w_e_k4 = load(&e_vals, k + 4); let w_a_k = load(&a_vals, k); @@ -2025,7 +2029,7 @@ where // junction (init prefix straddle, round-update windows, slack) // it absorbs whatever inner happens to be so that // `(inner + pa_c_ff) ∈ (X − 2)` everywhere. - let pa_c_ff_a_col: Vec = (0..n) + let pa_c_ff_a_col: Vec = cfg_into_iter!(0..n) .map(|k| { let w_a_k4 = load(&a_vals, k + 4); let w_a_k = load(&a_vals, k); @@ -2036,7 +2040,7 @@ where w_a_k + &pa_a_k - &two32_mu - &w_a_k4 }) .collect(); - let pa_c_ff_e_col: Vec = (0..n) + let pa_c_ff_e_col: Vec = cfg_into_iter!(0..n) .map(|k| { let w_e_k4 = load(&e_vals, k + 4); let w_e_k = load(&e_vals, k); diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index e0c51a9c..e3cb7cbd 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -1117,19 +1117,27 @@ where let first = &commitments[0]; validate_commitment_shape::(first)?; - let mut folded = vec![C::Group::zero(); first.comm.len()]; - for (commitment, weight) in commitments.iter().zip(theta) { + let scalars = theta + .iter() + .map(F::field_to_scalar) + .collect::, _>>()?; + for commitment in commitments { validate_commitment_shape::(commitment)?; if !same_commitment_shape(first, commitment) { return Err(ZipError::InvalidPcsParam( "Hyrax commitment fold shape mismatch".to_string(), )); } - let scalar = F::field_to_scalar(weight)?; - for (out, value) in folded.iter_mut().zip(commitment.comm.iter()) { - *out += value.clone() * scalar; - } } + let folded = cfg_into_iter!(0..first.comm.len()) + .map(|idx| { + let mut acc = C::Group::zero(); + for (commitment, scalar) in commitments.iter().zip(&scalars) { + acc += commitment.comm[idx] * scalar; + } + acc + }) + .collect(); Ok(HyraxCommitment { batch_size: first.batch_size, @@ -1149,18 +1157,26 @@ where validate_fold_inputs(prover_data, theta.len(), "prover data")?; let first = &prover_data[0]; - let mut folded_blinds = vec![C::ScalarField::zero(); first.blinds.len()]; - for (data, weight) in prover_data.iter().zip(theta) { + let scalars = theta + .iter() + .map(F::field_to_scalar) + .collect::, _>>()?; + for data in prover_data { if !same_prover_data_shape(first, data) { return Err(ZipError::InvalidPcsParam( "Hyrax prover-data fold shape mismatch".to_string(), )); } - let scalar = F::field_to_scalar(weight)?; - for (out, blind) in folded_blinds.iter_mut().zip(data.blinds.iter()) { - *out += *blind * scalar; - } } + let folded_blinds = cfg_into_iter!(0..first.blinds.len()) + .map(|idx| { + let mut acc = C::ScalarField::zero(); + for (data, scalar) in prover_data.iter().zip(&scalars) { + acc += data.blinds[idx] * scalar; + } + acc + }) + .collect(); Ok(HyraxProverData { batch_size: first.batch_size, From 6addc31a8c01a2bdc4ac51e709d492f5edd48aa8 Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 06:57:59 -0700 Subject: [PATCH 33/49] Reduce sumcheck prover scratch state --- piop/src/sumcheck/prover.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/piop/src/sumcheck/prover.rs b/piop/src/sumcheck/prover.rs index d0d812cc..0ce8de21 100644 --- a/piop/src/sumcheck/prover.rs +++ b/piop/src/sumcheck/prover.rs @@ -146,7 +146,6 @@ where evals: Vec, steps: Vec, vals0: Vec, - vals1: Vec, vals: Vec, levals: Vec, } @@ -157,7 +156,6 @@ where evals: zero_vec_deg.clone(), steps: zero_vec_poly.clone(), vals0: zero_vec_poly.clone(), - vals1: zero_vec_poly.clone(), vals: zero_vec_poly.clone(), levals: zero_vec_deg.clone(), }; @@ -188,14 +186,13 @@ where s.levals[0] = comb_fn(&s.vals0); if degree > 0 { - s.vals1.iter_mut().zip(polys.iter()).for_each(|(v1, poly)| { + s.vals.iter_mut().zip(polys.iter()).for_each(|(v1, poly)| { *v1 = F::new_unchecked_with_cfg(poly[index + 1].clone(), config); }); - s.levals[1] = comb_fn(&s.vals1); + s.levals[1] = comb_fn(&s.vals); - for (i, (v1, v0)) in s.vals1.iter().zip(s.vals0.iter()).enumerate() { + for (i, (v1, v0)) in s.vals.iter().zip(s.vals0.iter()).enumerate() { s.steps[i] = v1.clone() - v0.clone(); - s.vals[i] = v1.clone(); } for eval_point in s.levals.iter_mut().take(degree + 1).skip(2) { From eac15066cb636a7a6787a66989be2599c176e30c Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 06:58:30 -0700 Subject: [PATCH 34/49] Add ProjectionFold SHA profiling benchmarks --- Cargo.toml | 6 ++ protocol/benches/e2e.rs | 182 +++++++++++++++++++++++++++------------- 2 files changed, 128 insertions(+), 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 60f149bf..225558b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,3 +55,9 @@ unwrap_used = "deny" [profile.release] lto = true # Enable Link Time Optimization codegen-units = 1 # Slower compilation but potentially better optimization + +[profile.bench] +inherits = "release" +debug = 2 +strip = false +split-debuginfo = "unpacked" diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index 9ab1bbf9..1ad98c6c 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -10,6 +10,7 @@ use crypto_primitives::{ ConstIntRing, ConstIntSemiring, ConstSemiring, Field, FixedSemiring, FromWithConfig, PrimeField, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint, }; +use num_traits::Zero; use rand::rng; #[cfg(feature = "parallel")] use rayon::prelude::*; @@ -61,6 +62,7 @@ use zinc_uair::{ }; use zinc_utils::{ cfg_into_iter, cfg_iter, + delayed_reduction::{BarrettDelayedReduction, DelayedModularReductionAlgorithm}, from_ref::FromRef, inner_product::{InnerProduct, MBSInnerProduct, ScalarProduct}, mul_by_scalar::MulByScalar, @@ -699,12 +701,12 @@ fn projection_sha_scalarize_bit_slices( ) -> Result, ProductionShaError> { let powers = zinc_utils::powers(a.clone(), F::one_with_cfg(field_cfg), SHA_WORD_BITS); let word_count = bit_slices.len() / SHA_WORD_BITS; + let one = F::one_with_cfg(field_cfg); + let reducer = BarrettDelayedReduction::::new(field_cfg); let words = cfg_into_iter!(0..word_count) .map(|col_idx| { - let mut out_col = Vec::with_capacity(SHA_ROW_COUNT); - for row in 0..SHA_ROW_COUNT { - let mut value = F::zero_with_cfg(field_cfg); - for (bit, power) in powers.iter().enumerate() { + let bit_cols = (0..SHA_WORD_BITS) + .map(|bit| { let bit_col = &bit_slices[bit_slice_index(col_idx, bit, SHA_WORD_BITS)]; if bit_col.num_vars != SHA_ROW_VARS || bit_col.evaluations.len() != SHA_ROW_COUNT @@ -715,9 +717,14 @@ fn projection_sha_scalarize_bit_slices( expected: SHA_ROW_COUNT, }); } - value += bit_col.evaluations[row].clone() * power; - } - out_col.push(value); + Ok(bit_col) + }) + .collect::, ProductionShaError>>()?; + let mut out_col = Vec::with_capacity(SHA_ROW_COUNT); + for row in 0..SHA_ROW_COUNT { + out_col.push(projection_sha_scalarize_binary_row_dmr( + &bit_cols, row, &powers, &one, field_cfg, &reducer, + )); } Ok(out_col) }) @@ -725,6 +732,54 @@ fn projection_sha_scalarize_bit_slices( Ok(projection_sha_mle_table_from_columns(words)) } +fn projection_sha_scalarize_binary_row_dmr( + bit_cols: &[&DenseMultilinearExtension], + row: usize, + powers: &[F], + one: &F, + field_cfg: &::Config, + reducer: &BarrettDelayedReduction<'_, F>, +) -> F { + let mut bucket = Uint::<5>::zero(); + let mut pending_adds = 0usize; + let mut acc = F::zero_with_cfg(field_cfg); + + for (bit_col, power) in bit_cols.iter().zip(powers) { + let bit = &bit_col.evaluations[row]; + if F::is_zero(bit) { + continue; + } + if bit != one { + return projection_sha_scalarize_row_naive(bit_cols, row, powers, field_cfg); + } + reducer.add(&mut bucket, power); + pending_adds = pending_adds.saturating_add(1); + if pending_adds >= reducer.flush_adds() { + let pending = std::mem::replace(&mut bucket, Uint::zero()); + acc += reducer.reduce(pending); + pending_adds = 0; + } + } + + if !bucket.is_zero() { + acc += reducer.reduce(bucket); + } + acc +} + +fn projection_sha_scalarize_row_naive( + bit_cols: &[&DenseMultilinearExtension], + row: usize, + powers: &[F], + field_cfg: &::Config, +) -> F { + let mut value = F::zero_with_cfg(field_cfg); + for (bit_col, power) in bit_cols.iter().zip(powers) { + value += bit_col.evaluations[row].clone() * power; + } + value +} + fn projection_sha_selector_expected( selector: ShaPublicCol, row: usize, @@ -978,40 +1033,26 @@ where .expect("Hyrax benchmark setup must be valid") } -fn projection_sha_hyrax_pcs_params_bn254() -> ( - PCSParams, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>, - PCSVerifierParams< - AllHyraxPCSTypes, - RealEcdsaBenchZincTypes, - F, - DEGREE_PLUS_ONE, - >, -) { +fn projection_sha_hyrax_pcs_params() -> ( + PCSParams, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>, + PCSVerifierParams, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>, +) +where + C: AffineRepr, +{ let width = SHA_ROW_COUNT; - let (binary_ck, binary_vk) = - projection_sha_hyrax_key_pair::(width, 0); + let (binary_ck, binary_vk) = projection_sha_hyrax_key_pair::(width, 0); let (arbitrary_ck, arbitrary_vk) = - projection_sha_hyrax_key_pair::(width, 1_000); - let (int_ck, int_vk) = - projection_sha_hyrax_key_pair::(width, 2_000); + projection_sha_hyrax_key_pair::(width, 1_000); + let (int_ck, int_vk) = projection_sha_hyrax_key_pair::(width, 2_000); ( - PCSParams::< - AllHyraxPCSTypes, - RealEcdsaBenchZincTypes, - F, - DEGREE_PLUS_ONE, - > { + PCSParams::, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE> { binary: binary_ck, arbitrary: arbitrary_ck, int: int_ck, }, - PCSVerifierParams::< - AllHyraxPCSTypes, - RealEcdsaBenchZincTypes, - F, - DEGREE_PLUS_ONE, - > { + PCSVerifierParams::, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE> { binary: binary_vk, arbitrary: arbitrary_vk, int: int_vk, @@ -2021,9 +2062,12 @@ fn bench_og_sha256_zip_compare(group: &mut BenchmarkGroup, num_vars: u ); } -fn bench_projectionfold_sha256_concise_hyrax_bn254(group: &mut BenchmarkGroup) { - type C = ark_bn254::G1Affine; - type P = AllHyraxPCSTypes; +fn bench_projectionfold_sha256_concise_hyrax(group: &mut BenchmarkGroup, label: &str) +where + C: AffineRepr + Send + Sync + 'static, + F: zip_plus::pcs::hyrax::HyraxFieldBridge, +{ + type P = AllHyraxPCSTypes; type U = ProjectionShaBenchUair; let message_blocks = real_sha256_chain_blocks(); @@ -2047,25 +2091,27 @@ fn bench_projectionfold_sha256_concise_hyrax_bn254(group: &mut BenchmarkGroup>::Fmod, C, >(); - let (pcs_params, pcs_verifier_params) = projection_sha_hyrax_pcs_params_bn254(); - let pp = LinearIdealFoldProverParams::::new( - pcs_params, - field_cfg.clone(), - 3, - ); - let vs = setup_verify_linear_ideal_fold::( - LinearIdealFoldVerifierParams::new(pcs_verifier_params, field_cfg), - shape.clone(), - ) - .expect("ProjectionFold SHA verifier setup succeeds"); + let (pcs_params, pcs_verifier_params) = projection_sha_hyrax_pcs_params::(); + let pp = + LinearIdealFoldProverParams::, U, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>::new( + pcs_params, + field_cfg.clone(), + 3, + ); + let vs = + setup_verify_linear_ideal_fold::, U, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>( + LinearIdealFoldVerifierParams::new(pcs_verifier_params, field_cfg), + shape.clone(), + ) + .expect("ProjectionFold SHA verifier setup succeeds"); - let params = format!("ProjectionFoldConcise-HyraxBn254/SHA256Chain8/row-vars={SHA_ROW_VARS}"); + let params = format!("{label}/SHA256Chain8/row-vars={SHA_ROW_VARS}"); group.bench_function(BenchmarkId::new("Prove", ¶ms), |bench| { bench.iter(|| { let mut transcript = Blake3Transcript::new(); black_box(prove_linear_ideal_fold::< - P, + P, U, RealEcdsaBenchZincTypes, F, @@ -2076,7 +2122,7 @@ fn bench_projectionfold_sha256_concise_hyrax_bn254(group: &mut BenchmarkGroup( + let output = prove_linear_ideal_fold::, U, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>( &pp, &shape, &witnesses, @@ -2085,13 +2131,14 @@ fn bench_projectionfold_sha256_concise_hyrax_bn254(group: &mut BenchmarkGroup( - &vs, - &output.fresh_instances, - &output.proof, - &mut verifier_transcript, - ) - .expect("ProjectionFold verifier preflight failed"); + let verified = + verify_linear_ideal_fold::, U, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>( + &vs, + &output.fresh_instances, + &output.proof, + &mut verifier_transcript, + ) + .expect("ProjectionFold verifier preflight failed"); assert_eq!(verified.target, output.folded_instance.target); assert_eq!(verified.public, output.folded_instance.public); @@ -2104,7 +2151,7 @@ fn bench_projectionfold_sha256_concise_hyrax_bn254(group: &mut BenchmarkGroup, U, RealEcdsaBenchZincTypes, F, @@ -2114,7 +2161,7 @@ fn bench_projectionfold_sha256_concise_hyrax_bn254(group: &mut BenchmarkGroup( + verify_linear_ideal_fold::, U, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>( &vs, &traced_output.fresh_instances, &traced_output.proof, @@ -2129,7 +2176,7 @@ fn bench_projectionfold_sha256_concise_hyrax_bn254(group: &mut BenchmarkGroup, U, RealEcdsaBenchZincTypes, F, @@ -2145,6 +2192,20 @@ fn bench_projectionfold_sha256_concise_hyrax_bn254(group: &mut BenchmarkGroup) { + bench_projectionfold_sha256_concise_hyrax::( + group, + "ProjectionFoldConcise-HyraxBn254", + ); +} + +fn bench_projectionfold_sha256_concise_hyrax_secp256k1(group: &mut BenchmarkGroup) { + bench_projectionfold_sha256_concise_hyrax::( + group, + "ProjectionFoldConcise-HyraxSecp256k1", + ); +} + fn bench_real_sha_ecdsa_e2e(group: &mut BenchmarkGroup, num_vars: usize) { type U = ShaEcdsaUair; @@ -2288,6 +2349,7 @@ fn sha256_proving_system_compare_benches(c: &mut Criterion) { bench_og_sha256_zip_compare(&mut group, REAL_SHA256_CHAIN_NUM_VARS); bench_projectionfold_sha256_concise_hyrax_bn254(&mut group); + bench_projectionfold_sha256_concise_hyrax_secp256k1(&mut group); group.finish(); } From ebadcc7946a0bc5107c41ff3c195c6f192a8789f Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 06:59:08 -0700 Subject: [PATCH 35/49] Optimize ProjectionFold SHA hot paths --- piop/src/multipoint_eval.rs | 69 +- piop/src/neutron_nova/mod.rs | 11 +- piop/src/neutron_nova/projection_sha.rs | 670 ++++++-- piop/src/sumcheck/multi_degree.rs | 4 + protocol/src/production_sha.rs | 1938 ++++++++++++++++------- zip-plus/src/pcs/generic.rs | 12 + zip-plus/src/pcs/hyrax.rs | 409 ++++- zip-plus/src/pcs/msm_commitment.rs | 16 +- 8 files changed, 2392 insertions(+), 737 deletions(-) diff --git a/piop/src/multipoint_eval.rs b/piop/src/multipoint_eval.rs index ee1320b4..de6addff 100644 --- a/piop/src/multipoint_eval.rs +++ b/piop/src/multipoint_eval.rs @@ -151,16 +151,22 @@ where // eq_r(b) = eq(b, r') // next_c_r_mle(b) = next_c_mle(r', b) let eq_r = build_eq_x_r_inner(eval_point, field_cfg)?; - let (next_mles, down_cols): (Vec<_>, Vec<_>) = shifts + let mut shift_groups: Vec<(usize, Vec<(usize, usize)>)> = Vec::new(); + for (alpha_idx, spec) in shifts.iter().enumerate() { + let amount = spec.shift_amount(); + if let Some((_, group)) = shift_groups + .iter_mut() + .find(|(candidate, _)| *candidate == amount) + { + group.push((alpha_idx, spec.source_col())); + } else { + shift_groups.push((amount, vec![(alpha_idx, spec.source_col())])); + } + } + let next_mles = shift_groups .iter() - .map(|spec| { - let next = build_next_c_r_mle(eval_point, spec.shift_amount(), field_cfg)?; - let col = trace_mles[spec.source_col()].clone(); - Ok((next, col)) - }) - .collect::, ArithErrors>>()? - .into_iter() - .unzip(); + .map(|(amount, _)| build_next_c_r_mle(eval_point, *amount, field_cfg)) + .collect::, ArithErrors>>()?; // Precombine up cols with gammas, precombined[b] = Σ_j γ_j trace[j][b]. // Multiplying eval_f by &gamma uses `Mul<&Self, Output=Self>` from @@ -190,12 +196,38 @@ where ) }; + let grouped_down_cols = shift_groups + .iter() + .map(|(_, group)| { + let evaluations: Vec<_> = cfg_into_iter!(0..1 << num_vars) + .map(|b| { + group + .iter() + .fold(zero.clone(), |acc, (alpha_idx, source_col)| { + let eval_f = F::new_unchecked_with_cfg( + trace_mles[*source_col].evaluations[b].clone(), + field_cfg, + ); + acc + eval_f * &alphas[*alpha_idx] + }) + .into_inner() + }) + .collect(); + DenseMultilinearExtension::from_evaluations_vec( + num_vars, + evaluations, + zero_inner.clone(), + ) + }) + .collect::>(); + // Step 3: Pack MLEs: [eq_r, next_mles[..], precombined, down_cols[..]] - let mut mles = Vec::with_capacity(2 + 2 * num_down_cols); + let grouped_down_cols_len = grouped_down_cols.len(); + let mut mles = Vec::with_capacity(2 + 2 * grouped_down_cols_len); mles.push(eq_r); mles.extend(next_mles); mles.push(precombined); - mles.extend(down_cols); + mles.extend(grouped_down_cols); // Step 4: Run sumcheck with degree=2. @@ -208,15 +240,12 @@ where 2, |mle_values: &[F]| { let eq_val = &mle_values[0]; - let precombined = &mle_values[num_down_cols + 1]; - alphas - .iter() - .enumerate() - .fold(eq_val.clone() * precombined, |acc, (i, alpha)| { - let next = &mle_values[1 + i]; - let down_col = &mle_values[num_down_cols + 2 + i]; - acc + alpha.clone() * next * down_col - }) + let precombined = &mle_values[grouped_down_cols_len + 1]; + (0..grouped_down_cols_len).fold(eq_val.clone() * precombined, |acc, i| { + let next = &mle_values[1 + i]; + let down_col = &mle_values[grouped_down_cols_len + 2 + i]; + acc + next.clone() * down_col + }) }, field_cfg, ); diff --git a/piop/src/neutron_nova/mod.rs b/piop/src/neutron_nova/mod.rs index bd719c92..15f6615a 100644 --- a/piop/src/neutron_nova/mod.rs +++ b/piop/src/neutron_nova/mod.rs @@ -28,11 +28,12 @@ pub use projection_sha::{ FoldedCommitments, FreshIdealEvaluationCache, InstanceFoldClaim, LinearResidualCoeffTable, MleColumn, MleTable, NUM_NONZERO_SHA_FAMILIES, NUM_SHA_RESIDUAL_FAMILIES, ProjectedPublic, ProjectedTrace, ProjectionFoldAccumulator, ProjectionFoldWitness, SHA_ROW_COUNT, SHA_ROW_VARS, - SHA_WORD_BITS, ShaBooleanitySource, ShaIntCol, ShaProductionIdeal, ShaProjectionError, - ShaPublicCol, ShaPublicWordCol, ShaResidualFamily, ShaWordCol, VirtualChMajValues, - beta_aggregate_nonzero_ideal_polys, beta_aggregate_nonzero_ideal_polys_with_weights, - bit_slice_index, build_booleanity_weights, build_dense_sha_sumfold_group, - build_dense_sha_sumfold_group_with_weights, build_expression_folded_row_sumcheck_group, + SHA_WORD_BITS, ShaBinaryFoldField, ShaBooleanitySource, ShaIntCol, ShaProductionIdeal, + ShaProjectionError, ShaPublicCol, ShaPublicWordCol, ShaResidualFamily, ShaWordCol, + VirtualChMajValues, beta_aggregate_nonzero_ideal_polys, + beta_aggregate_nonzero_ideal_polys_with_weights, bit_slice_index, build_booleanity_weights, + build_dense_sha_sumfold_group, build_dense_sha_sumfold_group_with_weights, + build_expression_folded_row_sumcheck_group, build_expression_folded_row_sumcheck_group_with_row_weights, build_folded_row_sumcheck_group, build_fresh_sha_ideal_cache, build_linear_residual_coeff_tables, build_linear_residual_coeff_tables_with_row_weights, build_production_sha_sumfold_group, diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 5cd4f936..1884d1af 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -12,7 +12,10 @@ use crate::{ CombFn, sumcheck::multi_degree::{MultiDegreeSumcheckGroup, PrefixFastPath, PrefixRoundOutput}, }; -use crypto_primitives::{PrimeField, crypto_bigint_uint::Uint}; +use crypto_primitives::{ + PrimeField, crypto_bigint_boxed_monty::BoxedMontyField, crypto_bigint_monty::MontyField, + crypto_bigint_uint::Uint, +}; use num_traits::{ConstZero, Zero}; #[cfg(feature = "parallel")] use rayon::prelude::*; @@ -48,6 +51,37 @@ const SHA_RESIDUAL_EVAL_POWER_COUNT: usize = 62; pub type MleColumn = DenseMultilinearExtension; pub type MleTable = Vec>; +pub trait ShaBinaryFoldField: PrimeField + Send + Sync + Sized { + fn fold_binary_mle_tables( + kind: &'static str, + tables: &[&MleTable], + theta: &[Self], + field_cfg: &Self::Config, + ) -> Result, ShaProjectionError>; +} + +impl ShaBinaryFoldField for MontyField<4> { + fn fold_binary_mle_tables( + kind: &'static str, + tables: &[&MleTable], + theta: &[Self], + field_cfg: &Self::Config, + ) -> Result, ShaProjectionError> { + fold_binary_mle_tables_montgomery(kind, tables, theta, field_cfg) + } +} + +impl ShaBinaryFoldField for BoxedMontyField { + fn fold_binary_mle_tables( + kind: &'static str, + tables: &[&MleTable], + theta: &[Self], + field_cfg: &Self::Config, + ) -> Result, ShaProjectionError> { + fold_binary_mle_tables_generic(kind, tables, theta, field_cfg) + } +} + const NONZERO_SHA_FAMILIES: [ShaResidualFamily; NUM_NONZERO_SHA_FAMILIES] = [ ShaResidualFamily::R0BigSigmaA, ShaResidualFamily::R1BigSigmaE, @@ -330,7 +364,7 @@ pub struct LinearResidualCoeffTable { impl LinearResidualCoeffTable where - F: DelayedFieldProductSum, + F: PrimeField, { pub fn coeffs_for_family(&self, family: ShaResidualFamily) -> Option<&DynamicPolynomialF> { self.coeffs.get(family.index()) @@ -343,7 +377,7 @@ pub fn beta_aggregate_nonzero_ideal_polys( field_cfg: &F::Config, ) -> Result<[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES], ShaProjectionError> where - F: DelayedFieldProductSum, + F: PrimeField, { let weights = build_eq_x_r_vec(beta, field_cfg)?; beta_aggregate_nonzero_ideal_polys_with_weights(tables, &weights) @@ -854,11 +888,18 @@ where cfg_iter!(traces) .zip(cfg_iter!(publics)) .map(|(trace, public)| { - validate_trace(trace)?; - validate_public(public)?; + #[cfg(debug_assertions)] + { + validate_trace(trace)?; + validate_public(public)?; + } + let rho_sig0 = sparse_poly::(&[10, 19, 30], field_cfg); + let rho_sig1 = sparse_poly::(&[7, 21, 26], field_cfg); let mut coeffs = vec![DynamicPolynomialF::ZERO; NUM_SHA_RESIDUAL_FAMILIES]; for (row, row_weight) in row_weights.iter().enumerate().take(SHA_ROW_COUNT) { - let residuals = residual_polys_at_row(trace, public, row, field_cfg)?; + let residuals = residual_polys_at_row_with_rotation_polys( + trace, public, row, &rho_sig0, &rho_sig1, field_cfg, + )?; for (family_idx, residual) in residuals.iter().enumerate() { let weighted = scale_poly(residual, row_weight); coeffs[family_idx] += &weighted; @@ -975,7 +1016,7 @@ pub fn fold_projected_traces( field_cfg: &F::Config, ) -> Result<(ProjectionFoldWitness, ProjectedPublic), ShaProjectionError> where - F: PrimeField, + F: ShaBinaryFoldField, { if traces.len() != publics.len() { return Err(ShaProjectionError::InstanceCountMismatch { @@ -989,11 +1030,14 @@ where expected: traces.len(), }); } - for trace in traces { - validate_trace(trace)?; - } - for public in publics { - validate_public(public)?; + #[cfg(debug_assertions)] + { + for trace in traces { + validate_trace(trace)?; + } + for public in publics { + validate_public(public)?; + } } let folded_public_columns = fold_mle_tables( @@ -1003,7 +1047,7 @@ where field_cfg, )?; let folded_trace = ProjectedTrace { - bit_slices: fold_mle_tables( + bit_slices: fold_binary_mle_tables( "bit_slices", traces.iter().map(|trace| &trace.bit_slices), &sumfold.eq_instance_weights, @@ -1025,7 +1069,7 @@ where }; let folded_public = ProjectedPublic { columns: folded_public_columns, - bit_slices: fold_optional_mle_tables( + bit_slices: fold_optional_binary_mle_tables( "public.bit_slices", publics.iter().map(|public| public.bit_slices.as_ref()), &sumfold.eq_instance_weights, @@ -1198,31 +1242,52 @@ where let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); let ch1 = build_virtual_bit_array(|bit| { Ok( - bit_at_shifted_or_zero(trace, ShaWordCol::E, row, 2, bit, field_cfg)? - + bit_at_shifted_or_zero(trace, ShaWordCol::E, row, 1, bit, field_cfg)? + bit_at_shifted_or_zero_fast(trace, ShaWordCol::E, row, 2, bit, field_cfg) + + bit_at_shifted_or_zero_fast(trace, ShaWordCol::E, row, 1, bit, field_cfg) - two.clone() - * bit_at_shifted_or_zero(trace, ShaWordCol::Uef, row, 2, bit, field_cfg)?, + * bit_at_shifted_or_zero_fast(trace, ShaWordCol::Uef, row, 2, bit, field_cfg), ) }); let ch2 = build_virtual_bit_array(|bit| { Ok( - bit_at_shifted_or_zero(trace, ShaWordCol::E, row, 2, bit, field_cfg)? - - bit_at_shifted_or_zero(trace, ShaWordCol::E, row, 0, bit, field_cfg)? + bit_at_shifted_or_zero_fast(trace, ShaWordCol::E, row, 2, bit, field_cfg) + - bit_at_shifted_or_zero_fast(trace, ShaWordCol::E, row, 0, bit, field_cfg) + two.clone() - * bit_at_shifted_or_zero(trace, ShaWordCol::UNegEg, row, 2, bit, field_cfg)? + * bit_at_shifted_or_zero_fast( + trace, + ShaWordCol::UNegEg, + row, + 2, + bit, + field_cfg, + ) + two.clone() - * bit_at_shifted_or_zero(trace, ShaWordCol::Ch2Comp, row, 0, bit, field_cfg)?, + * bit_at_shifted_or_zero_fast( + trace, + ShaWordCol::Ch2Comp, + row, + 0, + bit, + field_cfg, + ), ) }); let maj = build_virtual_bit_array(|bit| { Ok( - bit_at_shifted_or_zero(trace, ShaWordCol::A, row, 0, bit, field_cfg)? - + bit_at_shifted_or_zero(trace, ShaWordCol::A, row, 1, bit, field_cfg)? - + bit_at_shifted_or_zero(trace, ShaWordCol::A, row, 2, bit, field_cfg)? + bit_at_shifted_or_zero_fast(trace, ShaWordCol::A, row, 0, bit, field_cfg) + + bit_at_shifted_or_zero_fast(trace, ShaWordCol::A, row, 1, bit, field_cfg) + + bit_at_shifted_or_zero_fast(trace, ShaWordCol::A, row, 2, bit, field_cfg) - two.clone() - * bit_at_shifted_or_zero(trace, ShaWordCol::Maj, row, 2, bit, field_cfg)? + * bit_at_shifted_or_zero_fast(trace, ShaWordCol::Maj, row, 2, bit, field_cfg) - two.clone() - * bit_at_shifted_or_zero(trace, ShaWordCol::MajComp, row, 0, bit, field_cfg)?, + * bit_at_shifted_or_zero_fast( + trace, + ShaWordCol::MajComp, + row, + 0, + bit, + field_cfg, + ), ) }); @@ -2924,8 +2989,11 @@ fn validate_sha_sumfold_traces( expected: ell, }); } - for trace in traces { - validate_trace(trace)?; + #[cfg(debug_assertions)] + { + for trace in traces { + validate_trace(trace)?; + } } Ok(ell) } @@ -2972,8 +3040,11 @@ where expected: booleanity_sources.len(), }); } - for trace in traces { - validate_trace(trace)?; + #[cfg(debug_assertions)] + { + for trace in traces { + validate_trace(trace)?; + } } if prefix_vars == 0 { return Ok(Vec::new()); @@ -3011,39 +3082,121 @@ where return Ok(table); } - let needs_virtuals = sources_need_virtuals(booleanity_sources); let coeff_plans = ternary_coeff_plans(prefix_vars)?; - let source_count = booleanity_sources.len(); + let word_bit_source_count = booleanity_sources + .iter() + .take_while(|source| matches!(source, ShaBooleanitySource::WordBit { .. })) + .count(); + let suffix_sources = &booleanity_sources[word_bit_source_count..]; + let suffix_count = suffix_sources.len(); + let suffix_needs_virtuals = sources_need_virtuals(suffix_sources); + let small_square_fields: Vec = small_square_field_table(field_cfg); + let word_bit_coeff_table: Vec = if word_bit_source_count == 0 { + Vec::new() + } else { + let mask_count = 1usize << prefix_len; + let mut table = Vec::with_capacity(mask_count * ternary_len); + for mask in 0..mask_count { + let source_mask = u8::try_from(mask).map_err(|_| { + ShaProjectionError::NonCanonicalProofObject( + "booleanity prefix masks require at most eight prefix entries", + ) + })?; + for plan in &coeff_plans { + table.push(booleanity_word_bit_mask_degree_two_coeff( + source_mask, + plan, + &small_square_fields, + field_cfg, + )); + } + } + table + }; let partials = cfg_chunks!(row_weights, 8) .enumerate() .map(|(chunk_idx, row_weight_chunk)| { let row_offset = chunk_idx * 8; let mut partial = vec![F::zero_with_cfg(field_cfg); ternary_len * tail_len]; - let mut source_values = vec![F::zero_with_cfg(field_cfg); prefix_len * source_count]; + let mut suffix_values = vec![F::zero_with_cfg(field_cfg); prefix_len * suffix_count]; + let mut mask_weights = vec![ + F::zero_with_cfg(field_cfg); + if word_bit_source_count == 0 { + 0 + } else { + 1usize << prefix_len + } + ]; + let mut touched_masks = Vec::new(); for tail in 0..tail_len { for (row_in_chunk, row_weight) in row_weight_chunk.iter().enumerate() { let row = row_offset + row_in_chunk; - fill_booleanity_source_prefix_values( - traces, - booleanity_sources, - prefix_vars, - tail, - row, - needs_virtuals, - &mut source_values, - field_cfg, - )?; + for (source_idx, source) in booleanity_sources[..word_bit_source_count] + .iter() + .enumerate() + { + let ShaBooleanitySource::WordBit { col, bit } = source else { + unreachable!("word-bit prefix only contains word-bit sources"); + }; + let mask = booleanity_word_bit_prefix_mask( + traces, + *col, + *bit, + prefix_vars, + tail, + row, + field_cfg, + ); + let mask_idx = usize::from(mask); + if F::is_zero(&mask_weights[mask_idx]) { + touched_masks.push(mask_idx); + } + mask_weights[mask_idx] += booleanity_weights[source_idx].clone(); + } - for (source_idx, booleanity_weight) in booleanity_weights.iter().enumerate() { + for &mask_idx in &touched_masks { + let source_weight = row_weight.clone() * &mask_weights[mask_idx]; + let coeff_offset = mask_idx * ternary_len; + for ternary_idx in 0..ternary_len { + let coeff = &word_bit_coeff_table[coeff_offset + ternary_idx]; + if F::is_zero(&coeff) { + continue; + } + partial[tail * ternary_len + ternary_idx] += + source_weight.clone() * coeff; + } + mask_weights[mask_idx] = F::zero_with_cfg(field_cfg); + } + touched_masks.clear(); + + if suffix_count != 0 { + fill_booleanity_source_prefix_values( + traces, + suffix_sources, + prefix_vars, + tail, + row, + suffix_needs_virtuals, + &mut suffix_values, + field_cfg, + )?; + } + + for suffix_idx in 0..suffix_count { + let source_idx = word_bit_source_count + suffix_idx; + let booleanity_weight = &booleanity_weights[source_idx]; let source_weight = row_weight.clone() * booleanity_weight; for (ternary_idx, plan) in coeff_plans.iter().enumerate() { let coeff = booleanity_degree_two_coeff( - &source_values, - source_count, - source_idx, + &suffix_values, + suffix_count, + suffix_idx, plan, field_cfg, ); + if F::is_zero(&coeff) { + continue; + } partial[tail * ternary_len + ternary_idx] += source_weight.clone() * coeff; } @@ -3061,6 +3214,33 @@ where Ok(table) } +#[allow(clippy::arithmetic_side_effects)] +fn booleanity_word_bit_prefix_mask( + traces: &[ProjectedTrace], + col: ShaWordCol, + bit: usize, + prefix_vars: usize, + tail: usize, + row: usize, + field_cfg: &F::Config, +) -> u8 +where + F: PrimeField, +{ + let prefix_len = binary_len(prefix_vars); + let mut value_mask = 0u8; + for prefix in 0..prefix_len { + let instance_idx = prefix + (tail << prefix_vars); + let trace = &traces[instance_idx]; + if !F::is_zero(&bit_at_shifted_or_zero_fast( + trace, col, row, 0, bit, field_cfg, + )) { + value_mask |= 1u8 << prefix; + } + } + value_mask +} + #[allow(clippy::arithmetic_side_effects)] fn bind_sha_booleanity_sources_to_prefix( traces: &[ProjectedTrace], @@ -3193,6 +3373,57 @@ where delta.clone() * delta } +fn booleanity_word_bit_mask_degree_two_coeff( + source_mask: u8, + plan: &TernaryCoeffPlan, + small_square_fields: &[F], + field_cfg: &F::Config, +) -> F +where + F: PrimeField, +{ + if plan.support_mask == 0 { + return F::zero_with_cfg(field_cfg); + } + let mut delta = 0i32; + for (prefix, positive) in &plan.vertices { + if ((source_mask >> prefix) & 1) == 0 { + continue; + } + if *positive { + delta += 1; + } else { + delta -= 1; + } + } + let square = usize::try_from(delta * delta).expect("square is non-negative"); + small_square_fields + .get(square) + .cloned() + .unwrap_or_else(|| small_usize_to_field(square, field_cfg)) +} + +fn small_square_field_table(field_cfg: &F::Config) -> Vec +where + F: PrimeField, +{ + (0..=64) + .map(|value| small_usize_to_field(value, field_cfg)) + .collect() +} + +fn small_usize_to_field(value: usize, field_cfg: &F::Config) -> F +where + F: PrimeField, +{ + let one = F::one_with_cfg(field_cfg); + let mut out = F::zero_with_cfg(field_cfg); + for _ in 0..value { + out += &one; + } + out +} + #[allow(clippy::arithmetic_side_effects)] fn ternary_point_parts(mut index: usize, prefix_vars: usize) -> (usize, usize) { let mut support_mask = 0usize; @@ -3734,12 +3965,25 @@ pub fn residual_polys_at_row( where F: PrimeField, { - let zero = F::zero_with_cfg(field_cfg); - let one = F::one_with_cfg(field_cfg); - let two = one.clone() + &one; let rho_sig0 = sparse_poly::(&[10, 19, 30], field_cfg); let rho_sig1 = sparse_poly::(&[7, 21, 26], field_cfg); + residual_polys_at_row_with_rotation_polys(trace, public, row, &rho_sig0, &rho_sig1, field_cfg) +} +fn residual_polys_at_row_with_rotation_polys( + trace: &ProjectedTrace, + public: &ProjectedPublic, + row: usize, + rho_sig0: &DynamicPolynomialF, + rho_sig1: &DynamicPolynomialF, + field_cfg: &F::Config, +) -> Result<[DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES], ShaProjectionError> +where + F: PrimeField, +{ + let zero = F::zero_with_cfg(field_cfg); + let one = F::one_with_cfg(field_cfg); + let two = one.clone() + &one; let a = word_poly(trace, ShaWordCol::A, row, field_cfg)?; let e = word_poly(trace, ShaWordCol::E, row, field_cfg)?; let sigma0 = word_poly(trace, ShaWordCol::Sigma0, row, field_cfg)?; @@ -3751,56 +3995,81 @@ where let ov_sigma1 = word_poly(trace, ShaWordCol::OvSigma1, row, field_cfg)?; let ov_small_sigma0 = word_poly(trace, ShaWordCol::OvSmallSigma0, row, field_cfg)?; let ov_small_sigma1 = word_poly(trace, ShaWordCol::OvSmallSigma1, row, field_cfg)?; + let w_rot25 = w.rot_c(25); + let w_rot14 = w.rot_c(14); + let w_rot15 = w.rot_c(15); + let w_rot13 = w.rot_c(13); + let w_shift3 = w.shift_r_c(3); + let w_shift9 = word_poly_shifted(trace, ShaWordCol::W, row, 9, field_cfg)?; + let w_shift16 = word_poly_shifted(trace, ShaWordCol::W, row, 16, field_cfg)?; + let small_sigma0_shift1 = word_poly_shifted(trace, ShaWordCol::SmallSigma0, row, 1, field_cfg)?; + let small_sigma1_shift14 = + word_poly_shifted(trace, ShaWordCol::SmallSigma1, row, 14, field_cfg)?; + let a_shift4 = word_poly_shifted(trace, ShaWordCol::A, row, 4, field_cfg)?; + let e_shift4 = word_poly_shifted(trace, ShaWordCol::E, row, 4, field_cfg)?; + let sigma0_shift3 = word_poly_shifted(trace, ShaWordCol::Sigma0, row, 3, field_cfg)?; + let sigma1_shift3 = word_poly_shifted(trace, ShaWordCol::Sigma1, row, 3, field_cfg)?; + let uef_shift3 = word_poly_shifted(trace, ShaWordCol::Uef, row, 3, field_cfg)?; + let uneg_eg_shift3 = word_poly_shifted(trace, ShaWordCol::UNegEg, row, 3, field_cfg)?; + let maj_shift3 = word_poly_shifted(trace, ShaWordCol::Maj, row, 3, field_cfg)?; + let public_k_shift3 = public_const_poly(public, ShaPublicCol::K, row + 3, field_cfg)?; + let comp_schedule = int_const_poly(trace, ShaIntCol::CompSchedule, row, field_cfg)?; + let comp_update_a = int_const_poly(trace, ShaIntCol::CompUpdateA, row, field_cfg)?; + let comp_update_e = int_const_poly(trace, ShaIntCol::CompUpdateE, row, field_cfg)?; + let comp_ff_a = int_const_poly(trace, ShaIntCol::CompFeedForwardA, row, field_cfg)?; + let comp_ff_e = int_const_poly(trace, ShaIntCol::CompFeedForwardE, row, field_cfg)?; let r0 = (&a * &rho_sig0) - &sigma0 - &scale_poly(&ov_sigma0, &two); let r1 = (&e * &rho_sig1) - &sigma1 - &scale_poly(&ov_sigma1, &two); - let r2 = word_poly(trace, ShaWordCol::W, row, field_cfg)?.rot_c(25) - + &word_poly(trace, ShaWordCol::W, row, field_cfg)?.rot_c(14) - + &word_poly(trace, ShaWordCol::W, row, field_cfg)?.shift_r_c(3) - - &small_sigma0 - - &scale_poly(&ov_small_sigma0, &two); - let r3 = word_poly(trace, ShaWordCol::W, row, field_cfg)?.rot_c(15) - + &word_poly(trace, ShaWordCol::W, row, field_cfg)?.rot_c(13) - + &word_poly(trace, ShaWordCol::W, row, field_cfg)?.shift_r_c(10) - - &small_sigma1 - - &scale_poly(&ov_small_sigma1, &two); - - let mu_w = mu_contribution(trace, row, 0, 2, field_cfg)?; - let mu_a = mu_contribution(trace, row, 2, 5, field_cfg)?; - let mu_e = mu_contribution(trace, row, 5, 8, field_cfg)?; - let mu_ff_a = mu_contribution(trace, row, 8, 9, field_cfg)?; - let mu_ff_e = mu_contribution(trace, row, 9, 10, field_cfg)?; - - let r4 = word_poly_shifted(trace, ShaWordCol::W, row, 16, field_cfg)? - - &w - - &word_poly_shifted(trace, ShaWordCol::SmallSigma0, row, 1, field_cfg)? - - &word_poly_shifted(trace, ShaWordCol::W, row, 9, field_cfg)? - - &word_poly_shifted(trace, ShaWordCol::SmallSigma1, row, 14, field_cfg)? + let r2 = w_rot25 + &w_rot14 + &w_shift3 - &small_sigma0 - &scale_poly(&ov_small_sigma0, &two); + let r3 = + w_rot15 + &w_rot13 + &w.shift_r_c(10) - &small_sigma1 - &scale_poly(&ov_small_sigma1, &two); + + let mu_packed = word_poly(trace, ShaWordCol::MuPacked, row, field_cfg)?; + let mu_shift2 = mu_packed.shift_r_c(2); + let mu_shift5 = mu_packed.shift_r_c(5); + let mu_shift8 = mu_packed.shift_r_c(8); + let mu_shift9 = mu_packed.shift_r_c(9); + let mu_shift10 = mu_packed.shift_r_c(10); + let low_mu_coeff = pow_two(32, field_cfg); + let high_mu_w_coeff = pow_two(34, field_cfg); + let high_mu_3_bit_coeff = pow_two(35, field_cfg); + let high_mu_1_bit_coeff = pow_two(33, field_cfg); + let mu = |low: &DynamicPolynomialF, high: &DynamicPolynomialF, high_coeff: &F| { + scale_poly(low, &low_mu_coeff) - &scale_poly(high, high_coeff) + }; + let mu_w = mu(&mu_packed, &mu_shift2, &high_mu_w_coeff); + let mu_a = mu(&mu_shift2, &mu_shift5, &high_mu_3_bit_coeff); + let mu_e = mu(&mu_shift5, &mu_shift8, &high_mu_3_bit_coeff); + let mu_ff_a = mu(&mu_shift8, &mu_shift9, &high_mu_1_bit_coeff); + let mu_ff_e = mu(&mu_shift9, &mu_shift10, &high_mu_1_bit_coeff); + + let r4 = w_shift16 - &w - &small_sigma0_shift1 - &w_shift9 - &small_sigma1_shift14 + &mu_w - + &int_const_poly(trace, ShaIntCol::CompSchedule, row, field_cfg)?; + + &comp_schedule; - let r5 = word_poly_shifted(trace, ShaWordCol::A, row, 4, field_cfg)? + let r5 = a_shift4.clone() - &e - - &word_poly_shifted(trace, ShaWordCol::Sigma1, row, 3, field_cfg)? - - &word_poly_shifted(trace, ShaWordCol::Uef, row, 3, field_cfg)? - - &word_poly_shifted(trace, ShaWordCol::UNegEg, row, 3, field_cfg)? - - &public_const_poly(public, ShaPublicCol::K, row + 3, field_cfg)? + - &sigma1_shift3 + - &uef_shift3 + - &uneg_eg_shift3 + - &public_k_shift3 - &w - - &word_poly_shifted(trace, ShaWordCol::Sigma0, row, 3, field_cfg)? - - &word_poly_shifted(trace, ShaWordCol::Maj, row, 3, field_cfg)? + - &sigma0_shift3 + - &maj_shift3 + &mu_a - + &int_const_poly(trace, ShaIntCol::CompUpdateA, row, field_cfg)?; + + &comp_update_a; - let r6 = word_poly_shifted(trace, ShaWordCol::E, row, 4, field_cfg)? + let r6 = e_shift4.clone() - &a - &e - - &word_poly_shifted(trace, ShaWordCol::Sigma1, row, 3, field_cfg)? - - &word_poly_shifted(trace, ShaWordCol::Uef, row, 3, field_cfg)? - - &word_poly_shifted(trace, ShaWordCol::UNegEg, row, 3, field_cfg)? - - &public_const_poly(public, ShaPublicCol::K, row + 3, field_cfg)? + - &sigma1_shift3 + - &uef_shift3 + - &uneg_eg_shift3 + - &public_k_shift3 - &w + &mu_e - + &int_const_poly(trace, ShaIntCol::CompUpdateE, row, field_cfg)?; + + &comp_update_e; let s_init = public_scalar(public, ShaPublicCol::SInit, row, field_cfg)?; let s_msg = public_scalar(public, ShaPublicCol::SMsg, row, field_cfg)?; @@ -3824,33 +4093,23 @@ where &s_out, ); - let r9 = word_poly_shifted(trace, ShaWordCol::A, row, 4, field_cfg)? - - &a - - &public_const_poly(public, ShaPublicCol::PAIn, row, field_cfg)? + let r9 = a_shift4 - &a - &public_const_poly(public, ShaPublicCol::PAIn, row, field_cfg)? + &mu_ff_a - + &int_const_poly(trace, ShaIntCol::CompFeedForwardA, row, field_cfg)?; - let r10 = word_poly_shifted(trace, ShaWordCol::E, row, 4, field_cfg)? - - &e - - &public_const_poly(public, ShaPublicCol::PEIn, row, field_cfg)? + + &comp_ff_a; + let r10 = e_shift4 - &e - &public_const_poly(public, ShaPublicCol::PEIn, row, field_cfg)? + &mu_ff_e - + &int_const_poly(trace, ShaIntCol::CompFeedForwardE, row, field_cfg)?; + + &comp_ff_e; let r11 = scale_poly( &(w - &public_word_or_const_poly(public, ShaPublicCol::Message, row, field_cfg)?), &s_msg, ); - let comp_schedule = int_const_poly(trace, ShaIntCol::CompSchedule, row, field_cfg)?; - let comp_update_a = int_const_poly(trace, ShaIntCol::CompUpdateA, row, field_cfg)?; - let comp_update_e = int_const_poly(trace, ShaIntCol::CompUpdateE, row, field_cfg)?; - let comp_ff_a = int_const_poly(trace, ShaIntCol::CompFeedForwardA, row, field_cfg)?; - let comp_ff_e = int_const_poly(trace, ShaIntCol::CompFeedForwardE, row, field_cfg)?; - let r12 = scale_poly(&comp_schedule, &s_sched); let r13 = scale_poly(&comp_update_a, &s_upd); let r14 = scale_poly(&comp_update_e, &s_upd); let r15 = scale_poly(&comp_ff_a, &s_ff); let r16 = scale_poly(&comp_ff_e, &s_ff); - let r17 = word_poly(trace, ShaWordCol::MuPacked, row, field_cfg)?.shift_r_c(10); + let r17 = mu_shift10; let mut residuals = [ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, @@ -4004,6 +4263,30 @@ where ) } +fn bit_at_shifted_or_zero_fast( + trace: &ProjectedTrace, + col: ShaWordCol, + row: usize, + shift: usize, + bit: usize, + field_cfg: &F::Config, +) -> F +where + F: PrimeField, +{ + debug_assert!(bit < SHA_WORD_BITS); + let Some(shifted) = row.checked_add(shift) else { + return F::zero_with_cfg(field_cfg); + }; + if shifted >= SHA_ROW_COUNT { + return F::zero_with_cfg(field_cfg); + } + let table_idx = bit_slice_index(col.index(), bit, SHA_WORD_BITS); + debug_assert!(table_idx < trace.bit_slices.len()); + debug_assert!(shifted < trace.bit_slices[table_idx].evaluations.len()); + trace.bit_slices[table_idx].evaluations[shifted].clone() +} + fn int_const_poly( trace: &ProjectedTrace, col: ShaIntCol, @@ -4169,23 +4452,6 @@ fn pow_two(exp: usize, field_cfg: &F::Config) -> F { out } -fn mu_contribution( - trace: &ProjectedTrace, - row: usize, - low: usize, - high: usize, - field_cfg: &F::Config, -) -> Result, ShaProjectionError> -where - F: PrimeField, -{ - let packed = word_poly(trace, ShaWordCol::MuPacked, row, field_cfg)?.shift_r_c(low as u32); - let tail = word_poly(trace, ShaWordCol::MuPacked, row, field_cfg)?.shift_r_c(high as u32); - let low_coeff = pow_two(32, field_cfg); - let high_coeff = pow_two(32 + high - low, field_cfg); - Ok(scale_poly(&packed, &low_coeff) - &scale_poly(&tail, &high_coeff)) -} - fn evaluate_poly_at_powers_dmr( poly: &DynamicPolynomialF, powers: &[F], @@ -4363,27 +4629,51 @@ fn virtual_bit_at( .ok_or(ShaProjectionError::BitIndexOutOfRange { bit }) } -fn fold_mle_tables<'a, F, I>( +fn fold_binary_mle_tables<'a, F, I>( kind: &'static str, tables: I, theta: &[F], field_cfg: &F::Config, ) -> Result, ShaProjectionError> where - F: PrimeField + 'a, + F: ShaBinaryFoldField + 'a, I: IntoIterator>, { let tables = tables.into_iter().collect::>(); + F::fold_binary_mle_tables(kind, &tables, theta, field_cfg) +} + +fn fold_binary_mle_tables_generic( + kind: &'static str, + tables: &[&MleTable], + theta: &[F], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField, +{ + fold_mle_tables(kind, tables.iter().copied(), theta, field_cfg) +} + +fn fold_binary_mle_tables_montgomery( + kind: &'static str, + tables: &[&MleTable], + theta: &[F], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField + MontgomeryLimbs + Send + Sync, +{ if tables.len() != theta.len() { return Err(ShaProjectionError::FoldingWeightCount { got: theta.len(), expected: tables.len(), }); } - let Some(first) = tables.first() else { + let Some(&first) = tables.first() else { return Ok(Vec::new()); }; - for table in &tables { + for table in tables { if table.len() != first.len() { return Err(ShaProjectionError::InstanceCountMismatch { got: table.len(), @@ -4391,12 +4681,14 @@ where }); } } + + let one = F::one_with_cfg(field_cfg); + let reducer = BarrettDelayedReduction::::new(field_cfg); cfg_into_iter!(0..first.len()) .map(|col_idx| { let template = &first[col_idx]; - let mut evaluations = - vec![F::zero_with_cfg(field_cfg); template.evaluations.len()]; - for (table, weight) in tables.iter().zip(theta) { + let mut evaluations = vec![F::zero_with_cfg(field_cfg); template.evaluations.len()]; + for table in tables { let col = &table[col_idx]; if col.num_vars != template.num_vars || col.evaluations.len() != template.evaluations.len() @@ -4408,9 +4700,11 @@ where expected: template.evaluations.len(), }); } - for (out, value) in evaluations.iter_mut().zip(&col.evaluations) { - *out += weight.clone() * value; - } + } + for (row, out) in evaluations.iter_mut().enumerate() { + *out = fold_binary_row_values_montgomery_dmr( + tables, theta, col_idx, row, &one, field_cfg, &reducer, + ); } Ok(DenseMultilinearExtension { evaluations, @@ -4420,14 +4714,70 @@ where .collect::, ShaProjectionError>>() } -fn fold_optional_mle_tables<'a, F, I>( +fn fold_binary_row_values_montgomery_dmr( + tables: &[&MleTable], + theta: &[F], + col_idx: usize, + row: usize, + one: &F, + field_cfg: &F::Config, + reducer: &BarrettDelayedReduction<'_, F>, +) -> F +where + F: PrimeField + MontgomeryLimbs + Send + Sync, +{ + let mut bucket = Uint::<5>::zero(); + let mut pending_adds = 0usize; + let mut acc = F::zero_with_cfg(field_cfg); + + for (table, weight) in tables.iter().zip(theta) { + let value = &table[col_idx].evaluations[row]; + if F::is_zero(value) { + continue; + } + if value != one { + return fold_row_values_naive(tables, theta, col_idx, row, field_cfg); + } + reducer.add(&mut bucket, weight); + pending_adds = pending_adds.saturating_add(1); + if pending_adds >= reducer.flush_adds() { + let pending = std::mem::replace(&mut bucket, Uint::zero()); + acc += reducer.reduce(pending); + pending_adds = 0; + } + } + + if !bucket.is_zero() { + acc += reducer.reduce(bucket); + } + acc +} + +fn fold_row_values_naive( + tables: &[&MleTable], + theta: &[F], + col_idx: usize, + row: usize, + field_cfg: &F::Config, +) -> F +where + F: PrimeField, +{ + let mut acc = F::zero_with_cfg(field_cfg); + for (table, weight) in tables.iter().zip(theta) { + acc += weight.clone() * &table[col_idx].evaluations[row]; + } + acc +} + +fn fold_optional_binary_mle_tables<'a, F, I>( kind: &'static str, tables: I, theta: &[F], field_cfg: &F::Config, ) -> Result>, ShaProjectionError> where - F: PrimeField + 'a, + F: ShaBinaryFoldField + 'a, I: IntoIterator>>, { let tables = tables.into_iter().collect::>(); @@ -4441,7 +4791,63 @@ where }; present.push(table); } - fold_mle_tables(kind, present, theta, field_cfg).map(Some) + fold_binary_mle_tables(kind, present, theta, field_cfg).map(Some) +} + +fn fold_mle_tables<'a, F, I>( + kind: &'static str, + tables: I, + theta: &[F], + field_cfg: &F::Config, +) -> Result, ShaProjectionError> +where + F: PrimeField + 'a, + I: IntoIterator>, +{ + let tables = tables.into_iter().collect::>(); + if tables.len() != theta.len() { + return Err(ShaProjectionError::FoldingWeightCount { + got: theta.len(), + expected: tables.len(), + }); + } + let Some(first) = tables.first() else { + return Ok(Vec::new()); + }; + for table in &tables { + if table.len() != first.len() { + return Err(ShaProjectionError::InstanceCountMismatch { + got: table.len(), + expected: first.len(), + }); + } + } + cfg_into_iter!(0..first.len()) + .map(|col_idx| { + let template = &first[col_idx]; + let mut evaluations = vec![F::zero_with_cfg(field_cfg); template.evaluations.len()]; + for (table, weight) in tables.iter().zip(theta) { + let col = &table[col_idx]; + if col.num_vars != template.num_vars + || col.evaluations.len() != template.evaluations.len() + { + return Err(ShaProjectionError::ColumnRowCount { + kind, + col: col_idx, + got: col.evaluations.len(), + expected: template.evaluations.len(), + }); + } + for (out, value) in evaluations.iter_mut().zip(&col.evaluations) { + *out += weight.clone() * value; + } + } + Ok(DenseMultilinearExtension { + evaluations, + num_vars: template.num_vars, + }) + }) + .collect::, ShaProjectionError>>() } #[cfg(test)] diff --git a/piop/src/sumcheck/multi_degree.rs b/piop/src/sumcheck/multi_degree.rs index 3beff594..aaceed0f 100644 --- a/piop/src/sumcheck/multi_degree.rs +++ b/piop/src/sumcheck/multi_degree.rs @@ -221,6 +221,10 @@ impl MultiDegreeSumcheckProof { &self.degrees } + pub fn group_messages(&self) -> &[Vec>] { + &self.group_messages + } + #[cfg(test)] pub(crate) fn group_messages_mut_for_testing(&mut self) -> &mut [Vec>] { &mut self.group_messages diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 039db63d..82e611e1 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -20,6 +20,8 @@ use num_traits::{ConstZero, Zero}; #[cfg(feature = "parallel")] use rayon::prelude::*; use thiserror::Error; +#[cfg(debug_assertions)] +use zinc_piop::neutron_nova::validate_projected_trace; use zinc_piop::{ combined_poly_resolver::Proof as CombinedPolyResolverProof, ideal_check::Proof as IdealCheckProof, @@ -31,21 +33,20 @@ use zinc_piop::{ neutron_nova::{ InstanceFoldClaim, LinearResidualCoeffTable, MleTable, NUM_NONZERO_SHA_FAMILIES, NUM_SHA_RESIDUAL_FAMILIES, ProjectedPublic, ProjectedTrace, ProjectionFoldWitness, - SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, ShaBooleanitySource, ShaIntCol, - ShaProjectionError, ShaPublicCol, ShaPublicWordCol, ShaResidualFamily, ShaWordCol, - beta_aggregate_nonzero_ideal_polys_with_weights, bit_slice_index, build_booleanity_weights, - build_dense_sha_sumfold_group, build_folded_row_sumcheck_group, + SHA_ROW_COUNT, SHA_ROW_VARS, SHA_WORD_BITS, ShaBinaryFoldField, ShaBooleanitySource, + ShaIntCol, ShaProjectionError, ShaPublicCol, ShaPublicWordCol, ShaResidualFamily, + ShaWordCol, beta_aggregate_nonzero_ideal_polys_with_weights, bit_slice_index, + build_booleanity_weights, build_dense_sha_sumfold_group, build_folded_row_sumcheck_group, build_linear_residual_coeff_tables_with_row_weights, build_production_sha_sumfold_group_from_prefix_accumulators, build_sha_lambda_powers, build_sha_residual_eval_powers, build_sha_sumfold_linear_accumulator, build_sha_sumfold_quadratic_prefix_accumulator, derive_instance_fold_claim, expression_folded_row_sum_with_row_weights, expression_folded_row_sum_with_vectors, fold_projected_traces, folded_row_integrand_sum, production_sha_booleanity_sources, - production_sha_nonzero_families, sha_int_at_point_with_weights, - sha_int_at_point_with_weights_unchecked, sha_public_at_point, - sha_public_at_point_with_weights, sha_word_bits_at_point_with_weights, - sha_word_bits_at_point_with_weights_unchecked, validate_projected_trace, - verify_folded_row_sumcheck_claim, verify_fresh_sha_ideal_polys, + production_sha_nonzero_families, sha_int_at_point_with_weights_unchecked, + sha_public_at_point, sha_public_at_point_with_weights, + sha_word_bits_at_point_with_weights_unchecked, verify_folded_row_sumcheck_claim, + verify_fresh_sha_ideal_polys, }, sumcheck::{ SumCheckError, @@ -53,20 +54,21 @@ use zinc_piop::{ }, }; use zinc_poly::{ - EvaluationError, + EvaluatablePolynomial, EvaluationError, mle::DenseMultilinearExtension, univariate::{ binary::BinaryPoly, dense::DensePolynomial, dynamic::over_field::{DynamicPolyFInnerProduct, DynamicPolynomialF}, + nat_evaluation::NatEvaluatedPoly, }, utils::{ArithErrors, build_eq_x_r_vec, eq_eval}, }; use zinc_transcript::Blake3Transcript; -use zinc_transcript::traits::{Transcribable, Transcript}; +use zinc_transcript::traits::{GenTranscribable, Transcribable, Transcript}; use zinc_uair::{ShiftSpec, Uair, UairSignature, UairTrace, UairWitness}; use zinc_utils::{ - UNCHECKED, cfg_iter, delayed_reduction::DelayedFieldProductSum, + UNCHECKED, cfg_into_iter, cfg_iter, delayed_reduction::DelayedFieldProductSum, inner_product::FieldFieldInnerProduct, inner_product::InnerProduct, inner_transparent_field::InnerTransparentField, }; @@ -432,7 +434,7 @@ where impl ProductionShaFoldedPcsOpen for AllHyraxPCSTypes where Zt: ZincTypes, - F: HyraxFieldBridge, + F: HyraxFieldBridge + DelayedFieldProductSum, F::Inner: Transcribable, F::Modulus: Transcribable, C: AffineRepr, @@ -495,6 +497,7 @@ pub struct FoldedRowSumcheckOutput { pub r_star: Vec, pub r_star_eq_weights: Vec, pub terminal_value: F, + pub endpoint_evals: Option>, } #[derive(Debug, Error)] @@ -571,29 +574,57 @@ pub fn absorb_projected_sha_publics( F::Modulus: Transcribable, { let mut field_buf = runtime_field_transcript_buf::(field_cfg); + let mut encoded = Vec::with_capacity( + publics.len() + * ShaPublicWordCol::COUNT + * SHA_ROW_COUNT + * F::Inner::get_num_bytes(F::zero_with_cfg(field_cfg).inner()), + ); + let zero = F::zero_with_cfg(field_cfg); + + fn push_u64(buf: &mut Vec, value: usize) { + buf.extend_from_slice(&(value as u64).to_le_bytes()); + } + + fn push_field_inners(buf: &mut Vec, values: &[F], scratch: &mut [u8]) + where + F: PrimeField, + F::Inner: Transcribable, + { + for value in values { + value.inner().write_transcription_bytes_exact(scratch); + buf.extend_from_slice(scratch); + } + } + transcript.absorb_slice(b"production_sha_publics_begin"); - transcript.absorb_slice(&(publics.len() as u64).to_le_bytes()); + encoded.extend_from_slice(b"compact_v1"); + zero.modulus() + .write_transcription_bytes_exact(&mut field_buf); + encoded.extend_from_slice(&field_buf); + push_u64(&mut encoded, publics.len()); + push_u64(&mut encoded, ShaPublicCol::COUNT); + push_u64(&mut encoded, ShaPublicWordCol::COUNT); + push_u64(&mut encoded, SHA_ROW_COUNT); for (instance_idx, public) in publics.iter().enumerate() { - transcript.absorb_slice(&(instance_idx as u64).to_le_bytes()); - transcript.absorb_slice(&(public.columns.len() as u64).to_le_bytes()); - for (col_idx, col) in public.columns.iter().enumerate() { - transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); - transcript.absorb_slice(&(col.evaluations.len() as u64).to_le_bytes()); - transcript.absorb_random_field_slice(&col.evaluations, &mut field_buf); - } + push_u64(&mut encoded, instance_idx); + push_u64(&mut encoded, public.columns.len()); match &public.bit_slices { Some(bit_slices) => { - transcript.absorb_slice(&[1]); - transcript.absorb_slice(&(bit_slices.len() as u64).to_le_bytes()); - for (col_idx, col) in bit_slices.iter().enumerate() { - transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); - transcript.absorb_slice(&(col.evaluations.len() as u64).to_le_bytes()); - transcript.absorb_random_field_slice(&col.evaluations, &mut field_buf); - } + encoded.push(1); + push_u64(&mut encoded, bit_slices.len()); } - None => transcript.absorb_slice(&[0]), + None => encoded.push(0), + } + for public_col in production_sha_public_word_column_map() { + let col_idx = public_col.index(); + let col = &public.columns[col_idx]; + push_u64(&mut encoded, col_idx); + push_u64(&mut encoded, col.evaluations.len()); + push_field_inners::(&mut encoded, &col.evaluations, &mut field_buf); } } + transcript.absorb_slice(&encoded); transcript.absorb_slice(b"production_sha_publics_end"); } @@ -639,12 +670,6 @@ fn absorb_uair_column_counts( transcript.absorb_slice(&(int as u64).to_le_bytes()); } -fn absorb_transcribable(transcript: &mut impl Transcript, value: &T) { - let mut buf = vec![0u8; value.get_num_bytes() + T::LENGTH_NUM_BYTES]; - value.write_transcription_bytes_subset(&mut buf); - transcript.absorb_slice(&buf); -} - fn runtime_field_transcript_buf(field_cfg: &F::Config) -> Vec where F: PrimeField, @@ -709,37 +734,53 @@ fn absorb_public_uair_trace( ) where Zt: ZincTypes, { - transcript.absorb_slice(b"uair_public_trace_begin"); - transcript.absorb_slice(&(instance_idx as u64).to_le_bytes()); - transcript.absorb_slice(&(trace.binary_poly.len() as u64).to_le_bytes()); + fn push_u64(buf: &mut Vec, value: usize) { + buf.extend_from_slice(&(value as u64).to_le_bytes()); + } + + fn push_transcribable(buf: &mut Vec, value: &T, scratch: &mut Vec) { + let len = value.get_num_bytes() + T::LENGTH_NUM_BYTES; + scratch.resize(len, 0); + value.write_transcription_bytes_subset(scratch); + buf.extend_from_slice(scratch); + } + + let mut encoded = Vec::new(); + let mut scratch = Vec::new(); + encoded.extend_from_slice(b"compact_v1"); + push_u64(&mut encoded, instance_idx); + push_u64(&mut encoded, trace.binary_poly.len()); for (col_idx, col) in trace.binary_poly.iter().enumerate() { - transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); - transcript.absorb_slice(&(col.num_vars as u64).to_le_bytes()); - transcript.absorb_slice(&(col.evaluations.len() as u64).to_le_bytes()); + push_u64(&mut encoded, col_idx); + push_u64(&mut encoded, col.num_vars); + push_u64(&mut encoded, col.evaluations.len()); for value in &col.evaluations { - absorb_transcribable(transcript, value); + push_transcribable(&mut encoded, value, &mut scratch); } } - transcript.absorb_slice(&(trace.arbitrary_poly.len() as u64).to_le_bytes()); + push_u64(&mut encoded, trace.arbitrary_poly.len()); for (col_idx, col) in trace.arbitrary_poly.iter().enumerate() { - transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); - transcript.absorb_slice(&(col.num_vars as u64).to_le_bytes()); - transcript.absorb_slice(&(col.evaluations.len() as u64).to_le_bytes()); + push_u64(&mut encoded, col_idx); + push_u64(&mut encoded, col.num_vars); + push_u64(&mut encoded, col.evaluations.len()); for poly in &col.evaluations { for coeff in poly.iter() { - absorb_transcribable(transcript, coeff); + push_transcribable(&mut encoded, coeff, &mut scratch); } } } - transcript.absorb_slice(&(trace.int.len() as u64).to_le_bytes()); + push_u64(&mut encoded, trace.int.len()); for (col_idx, col) in trace.int.iter().enumerate() { - transcript.absorb_slice(&(col_idx as u64).to_le_bytes()); - transcript.absorb_slice(&(col.num_vars as u64).to_le_bytes()); - transcript.absorb_slice(&(col.evaluations.len() as u64).to_le_bytes()); + push_u64(&mut encoded, col_idx); + push_u64(&mut encoded, col.num_vars); + push_u64(&mut encoded, col.evaluations.len()); for value in &col.evaluations { - absorb_transcribable(transcript, value); + push_transcribable(&mut encoded, value, &mut scratch); } } + + transcript.absorb_slice(b"uair_public_trace_begin"); + transcript.absorb_slice(&encoded); transcript.absorb_slice(b"uair_public_trace_end"); } @@ -1088,6 +1129,7 @@ where Zt: ZincTypes, F: InnerTransparentField + DelayedFieldProductSum + + ShaBinaryFoldField + FromPrimitiveWithConfig + Send + Sync @@ -1125,12 +1167,26 @@ where )?; validate_production_sha_publics(&publics, field_cfg)?; - absorb_production_sha_commitments::( - transcript, - b"production_sha_fresh_commitments", - &instance_commitments, - ); - absorb_projected_sha_publics(transcript, &publics, field_cfg); + tracing::info_span!( + target: "zinc_protocol::production_sha", + "absorb_fresh_commitments", + side = "prove", + phase = "absorb_fresh_commitments", + ) + .in_scope(|| { + absorb_production_sha_commitments::( + transcript, + b"production_sha_fresh_commitments", + &instance_commitments, + ) + }); + tracing::info_span!( + target: "zinc_protocol::production_sha", + "absorb_projected_publics", + side = "prove", + phase = "absorb_projected_publics", + ) + .in_scope(|| absorb_projected_sha_publics(transcript, &publics, field_cfg)); let r_ic = sample_pre_ideal_challenge(transcript, field_cfg); let r_ic_eq_weights = build_eq_x_r_vec(&r_ic, field_cfg)?; @@ -1307,10 +1363,24 @@ where .map(|witness| { let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; - let (trace, public, witness_polys) = - U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg)?; - let (data, commitment) = - commit_production_sha_instance::(pcs_params, &witness_polys)?; + let (trace, public, witness_polys) = tracing::info_span!( + target: "zinc_protocol::production_sha", + "fresh_project_instance", + side = "prove", + phase = "fresh_project_instance", + ) + .in_scope(|| { + U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg) + })?; + let (data, commitment) = tracing::info_span!( + target: "zinc_protocol::production_sha", + "fresh_commit_instance", + side = "prove", + phase = "fresh_commit_instance", + ) + .in_scope(|| { + commit_production_sha_instance::(pcs_params, &witness_polys) + })?; Ok(( UairInstance { @@ -1432,28 +1502,51 @@ where F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, F::Inner: Zero, { - let linear_accumulator = - build_sha_sumfold_linear_accumulator(coeff_tables, a_powers, lambda_powers, field_cfg)?; - let quadratic_prefix_accumulator = build_sha_sumfold_quadratic_prefix_accumulator( - traces, - booleanity_sources, - prefix_vars, - r_ic_eq_weights, - booleanity_weights, - field_cfg, - )?; - let sumfold_group = build_production_sha_sumfold_group_from_prefix_accumulators( - traces, - beta, - beta_eq_weights, - r_ic_eq_weights, - &linear_accumulator, - &quadratic_prefix_accumulator, - booleanity_weights, - booleanity_sources, - prefix_vars, - field_cfg, - )?; + let linear_accumulator = tracing::info_span!( + target: "zinc_protocol::production_sha", + "sumfold_linear_accumulator", + side = "prove", + phase = "sumfold_linear_accumulator", + ) + .in_scope(|| { + build_sha_sumfold_linear_accumulator(coeff_tables, a_powers, lambda_powers, field_cfg) + })?; + let quadratic_prefix_accumulator = tracing::info_span!( + target: "zinc_protocol::production_sha", + "sumfold_quadratic_prefix_accumulator", + side = "prove", + phase = "sumfold_quadratic_prefix_accumulator", + ) + .in_scope(|| { + build_sha_sumfold_quadratic_prefix_accumulator( + traces, + booleanity_sources, + prefix_vars, + r_ic_eq_weights, + booleanity_weights, + field_cfg, + ) + })?; + let sumfold_group = tracing::info_span!( + target: "zinc_protocol::production_sha", + "sumfold_group", + side = "prove", + phase = "sumfold_group", + ) + .in_scope(|| { + build_production_sha_sumfold_group_from_prefix_accumulators( + traces, + beta, + beta_eq_weights, + r_ic_eq_weights, + &linear_accumulator, + &quadratic_prefix_accumulator, + booleanity_weights, + booleanity_sources, + prefix_vars, + field_cfg, + ) + })?; Ok(( linear_accumulator, quadratic_prefix_accumulator, @@ -1517,42 +1610,79 @@ fn prove_fold_after_sumfold_phase( ) -> Result, ProductionShaError> where Zt: ZincTypes, - F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F: InnerTransparentField + DelayedFieldProductSum + ShaBinaryFoldField + Send + Sync + 'static, F::Inner: Zero, P: ZincPCSTypes, P::BinaryPCS: FoldablePCS, D>, P::ArbitraryPCS: FoldablePCS, D>, P::IntPCS: FoldablePCS, { - let (folded, folded_public) = - fold_projected_traces(traces, publics, provisional_sumfold_output, field_cfg)?; - let row_claim = expression_folded_row_sum_with_vectors( - &folded.trace, - &folded_public, - r_ic_eq_weights, - a_powers, - lambda_powers, - booleanity_weights, - booleanity_sources, - field_cfg, - )?; - let sumfold_output = derive_instance_fold_claim_from_row_claim( - beta, - sumfold_r_b, - &row_claim, - traces.len(), - field_cfg, - )?; - let folded_commitments = fold_pcs_commitments::( - instance_commitments, - sumfold_output.eq_instance_weights(), - field_cfg, - )?; - let folded_prover_data = fold_pcs_prover_data::( - instance_prover_data, - sumfold_output.eq_instance_weights(), - field_cfg, - )?; + let (folded, folded_public) = tracing::info_span!( + target: "zinc_protocol::production_sha", + "fold_projected_traces", + side = "prove", + phase = "fold_projected_traces", + ) + .in_scope(|| fold_projected_traces(traces, publics, provisional_sumfold_output, field_cfg))?; + let row_claim = tracing::info_span!( + target: "zinc_protocol::production_sha", + "fold_row_claim", + side = "prove", + phase = "fold_row_claim", + ) + .in_scope(|| { + production_sha_folded_row_sum_fast( + &folded.trace, + &folded_public, + r_ic_eq_weights, + a_powers, + lambda_powers, + booleanity_weights, + booleanity_sources, + field_cfg, + ) + })?; + let sumfold_output = tracing::info_span!( + target: "zinc_protocol::production_sha", + "fold_claim", + side = "prove", + phase = "fold_claim", + ) + .in_scope(|| { + derive_instance_fold_claim_from_row_claim( + beta, + sumfold_r_b, + &row_claim, + traces.len(), + field_cfg, + ) + })?; + let folded_commitments = tracing::info_span!( + target: "zinc_protocol::production_sha", + "fold_commitments", + side = "prove", + phase = "fold_commitments", + ) + .in_scope(|| { + fold_pcs_commitments::( + instance_commitments, + sumfold_output.eq_instance_weights(), + field_cfg, + ) + })?; + let folded_prover_data = tracing::info_span!( + target: "zinc_protocol::production_sha", + "fold_prover_data", + side = "prove", + phase = "fold_prover_data", + ) + .in_scope(|| { + fold_pcs_prover_data::( + instance_prover_data, + sumfold_output.eq_instance_weights(), + field_cfg, + ) + })?; Ok(( folded, @@ -1564,116 +1694,486 @@ where )) } -#[tracing::instrument( - target = "zinc_protocol::production_sha", - level = "info", - skip_all, - fields(side = "prove", phase = "row_sumcheck") -)] #[allow(clippy::too_many_arguments)] -fn prove_row_sumcheck_phase( - transcript: &mut impl Transcript, +fn production_sha_folded_row_sum_fast( trace: &ProjectedTrace, public: &ProjectedPublic, - r_ic: &[F; SHA_ROW_VARS], - r_ic_eq_weights: &[F], + row_weights: &[F], a_powers: &[F], lambda_powers: &[F], booleanity_weights: &[F], booleanity_sources: &[ShaBooleanitySource], - row_claim: &F, field_cfg: &F::Config, -) -> Result<(MultiDegreeSumcheckProof, FoldedRowSumcheckOutput), ProductionShaError> +) -> Result> where - F: InnerTransparentField - + DelayedFieldProductSum - + FromPrimitiveWithConfig - + Send - + Sync - + 'static, - F::Inner: Transcribable + Zero, - F::Modulus: Transcribable, + F: InnerTransparentField + DelayedFieldProductSum + Send + Sync + 'static, + F::Inner: Zero, { - let (combined_sumcheck, row_output) = - prove_expression_folded_row_sumcheck_with_output_and_vectors( - transcript, - trace, - public, - r_ic, - r_ic_eq_weights, - a_powers, - lambda_powers, - booleanity_weights, - booleanity_sources, - field_cfg, - )?; - verify_folded_row_sumcheck_claim(&combined_sumcheck.claimed_sums()[0], row_claim)?; - Ok((combined_sumcheck, row_output)) -} + #[cfg(debug_assertions)] + validate_projected_trace(trace)?; + if row_weights.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "row weights", + got: row_weights.len(), + expected: SHA_ROW_COUNT, + }); + } + if a_powers.len() < SHA_IDEAL_EVAL_POWER_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "a powers", + got: a_powers.len(), + expected: SHA_IDEAL_EVAL_POWER_COUNT, + }); + } + if lambda_powers.len() != NUM_SHA_RESIDUAL_FAMILIES { + return Err(ProductionShaError::LengthMismatch { + label: "lambda powers", + got: lambda_powers.len(), + expected: NUM_SHA_RESIDUAL_FAMILIES, + }); + } + if booleanity_weights.len() != booleanity_sources.len() { + return Err(ProductionShaError::LengthMismatch { + label: "booleanity weights", + got: booleanity_weights.len(), + expected: booleanity_sources.len(), + }); + } -#[tracing::instrument( - target = "zinc_protocol::production_sha", - level = "info", - skip_all, - fields(side = "prove", phase = "endpoint_multipoint") -)] -#[allow(clippy::too_many_arguments)] -fn prove_endpoint_multipoint_phase( - transcript: &mut impl Transcript, - trace: &ProjectedTrace, - folded_public: &ProjectedPublic, - row_output: &FoldedRowSumcheckOutput, - r_ic: &[F; SHA_ROW_VARS], - a: &F, - a_powers: &[F], - lambda_powers: &[F], - booleanity_weights: &[F], - booleanity_sources: &[ShaBooleanitySource], - field_cfg: &F::Config, -) -> Result, ProductionShaError> -where - F: InnerTransparentField - + DelayedFieldProductSum - + FromPrimitiveWithConfig - + Send - + Sync - + 'static, - F::Inner: Transcribable + Zero + Default + Send + Sync, - F::Modulus: Transcribable, -{ - let endpoint_evals = build_sha_endpoint_evals_from_trace_with_row_weights( - trace, - &row_output.r_star_eq_weights, - a, - field_cfg, - )?; - let resolver = sha_resolver_from_endpoint_evals(&endpoint_evals)?; - absorb_sha_resolver_proof(transcript, &resolver, field_cfg); - let resolver_endpoint_evals = sha_endpoint_evals_from_resolver(&resolver, a, field_cfg)?; - let terminal = reconstruct_folded_row_terminal_from_endpoints_with_vectors( - &resolver_endpoint_evals, - folded_public, - r_ic, - &row_output.r_star, - &row_output.r_star_eq_weights, - a_powers, - lambda_powers, - booleanity_weights, - booleanity_sources, - field_cfg, - )?; - verify_folded_row_terminal_value(row_output, &terminal)?; + let weight_vec = |shift: usize| { + (0..SHA_WORD_BITS) + .map(|bit| { + if bit >= shift { + a_powers[bit - shift].clone() + } else { + F::zero_with_cfg(field_cfg) + } + }) + .collect::>() + }; + let rot_vec = |shift: usize| { + (0..SHA_WORD_BITS) + .map(|bit| a_powers[(bit + shift) % SHA_WORD_BITS].clone()) + .collect::>() + }; - let (multipoint_eval, r_0) = prove_sha_endpoint_multipoint_with_row_weights( - transcript, - trace, - folded_public, - &resolver_endpoint_evals, - &row_output.r_star, - &row_output.r_star_eq_weights, - field_cfg, - )?; - Ok((resolver, resolver_endpoint_evals, multipoint_eval, r_0)) + let word_weights = a_powers[..SHA_WORD_BITS].to_vec(); + let rot25_weights = rot_vec(25); + let rot14_weights = rot_vec(14); + let rot15_weights = rot_vec(15); + let rot13_weights = rot_vec(13); + let shift0_weights = weight_vec(0); + let shift2_weights = weight_vec(2); + let shift3_weights = weight_vec(3); + let shift5_weights = weight_vec(5); + let shift8_weights = weight_vec(8); + let shift9_weights = weight_vec(9); + let shift10_weights = weight_vec(10); + let rho_sig0 = a_powers[10].clone() + &a_powers[19] + &a_powers[30]; + let rho_sig1 = a_powers[7].clone() + &a_powers[21] + &a_powers[26]; + let low_mu_coeff = production_sha_pow_two(32, field_cfg); + let high_mu_w_coeff = production_sha_pow_two(34, field_cfg); + let high_mu_3_bit_coeff = production_sha_pow_two(35, field_cfg); + let high_mu_1_bit_coeff = production_sha_pow_two(33, field_cfg); + let one = F::one_with_cfg(field_cfg); + let two = one.clone() + &one; + + let values = cfg_iter!(row_weights) + .enumerate() + .map(|(row, row_weight)| { + let word_eval_with = |col: ShaWordCol, shift: usize, weights: &[F]| { + trace_word_eval_at_row_with_weights(trace, col, row, shift, weights, field_cfg) + }; + let word_eval = + |col: ShaWordCol, shift: usize| word_eval_with(col, shift, &word_weights); + let public_word_eval = |col: ShaPublicCol| { + public_word_or_const_eval_at_row(public, col, row, &word_weights, field_cfg) + }; + + let a_word = word_eval(ShaWordCol::A, 0)?; + let e_word = word_eval(ShaWordCol::E, 0)?; + let sigma0 = word_eval(ShaWordCol::Sigma0, 0)?; + let sigma1 = word_eval(ShaWordCol::Sigma1, 0)?; + let w = word_eval(ShaWordCol::W, 0)?; + let small_sigma0 = word_eval(ShaWordCol::SmallSigma0, 0)?; + let small_sigma1 = word_eval(ShaWordCol::SmallSigma1, 0)?; + let ov_sigma0 = word_eval(ShaWordCol::OvSigma0, 0)?; + let ov_sigma1 = word_eval(ShaWordCol::OvSigma1, 0)?; + let ov_small_sigma0 = word_eval(ShaWordCol::OvSmallSigma0, 0)?; + let ov_small_sigma1 = word_eval(ShaWordCol::OvSmallSigma1, 0)?; + + let mu = |low_weights: &[F], high_weights: &[F], high_coeff: &F| { + Ok::>( + word_eval_with(ShaWordCol::MuPacked, 0, low_weights)? * &low_mu_coeff + - word_eval_with(ShaWordCol::MuPacked, 0, high_weights)? * high_coeff, + ) + }; + let mu_w = mu(&shift0_weights, &shift2_weights, &high_mu_w_coeff)?; + let mu_a = mu(&shift2_weights, &shift5_weights, &high_mu_3_bit_coeff)?; + let mu_e = mu(&shift5_weights, &shift8_weights, &high_mu_3_bit_coeff)?; + let mu_ff_a = mu(&shift8_weights, &shift9_weights, &high_mu_1_bit_coeff)?; + let mu_ff_e = mu(&shift9_weights, &shift10_weights, &high_mu_1_bit_coeff)?; + + let r0 = a_word.clone() * &rho_sig0 - &sigma0 - two.clone() * &ov_sigma0; + let r1 = e_word.clone() * &rho_sig1 - &sigma1 - two.clone() * &ov_sigma1; + let r2 = word_eval_with(ShaWordCol::W, 0, &rot25_weights)? + + word_eval_with(ShaWordCol::W, 0, &rot14_weights)? + + word_eval_with(ShaWordCol::W, 0, &shift3_weights)? + - &small_sigma0 + - two.clone() * &ov_small_sigma0; + let r3 = word_eval_with(ShaWordCol::W, 0, &rot15_weights)? + + word_eval_with(ShaWordCol::W, 0, &rot13_weights)? + + word_eval_with(ShaWordCol::W, 0, &shift10_weights)? + - &small_sigma1 + - two.clone() * &ov_small_sigma1; + let r4 = word_eval(ShaWordCol::W, 16)? + - &w + - word_eval(ShaWordCol::SmallSigma0, 1)? + - word_eval(ShaWordCol::W, 9)? + - word_eval(ShaWordCol::SmallSigma1, 14)? + + &mu_w + + trace_int_at_row(trace, ShaIntCol::CompSchedule, row, field_cfg)?; + let r5 = word_eval(ShaWordCol::A, 4)? + - &e_word + - word_eval(ShaWordCol::Sigma1, 3)? + - word_eval(ShaWordCol::Uef, 3)? + - word_eval(ShaWordCol::UNegEg, 3)? + - public_scalar_at_row(public, ShaPublicCol::K, row, 3, field_cfg)? + - &w + - word_eval(ShaWordCol::Sigma0, 3)? + - word_eval(ShaWordCol::Maj, 3)? + + &mu_a + + trace_int_at_row(trace, ShaIntCol::CompUpdateA, row, field_cfg)?; + let r6 = word_eval(ShaWordCol::E, 4)? + - &a_word + - &e_word + - word_eval(ShaWordCol::Sigma1, 3)? + - word_eval(ShaWordCol::Uef, 3)? + - word_eval(ShaWordCol::UNegEg, 3)? + - public_scalar_at_row(public, ShaPublicCol::K, row, 3, field_cfg)? + - &w + + &mu_e + + trace_int_at_row(trace, ShaIntCol::CompUpdateE, row, field_cfg)?; + + let s_init = public_scalar_at_row(public, ShaPublicCol::SInit, row, 0, field_cfg)?; + let s_msg = public_scalar_at_row(public, ShaPublicCol::SMsg, row, 0, field_cfg)?; + let s_sched = public_scalar_at_row(public, ShaPublicCol::SSched, row, 0, field_cfg)?; + let s_upd = public_scalar_at_row(public, ShaPublicCol::SUpd, row, 0, field_cfg)?; + let s_ff = public_scalar_at_row(public, ShaPublicCol::SFf, row, 0, field_cfg)?; + let s_out = public_scalar_at_row(public, ShaPublicCol::SOut, row, 0, field_cfg)?; + + let r7 = (a_word.clone() - public_word_eval(ShaPublicCol::PAIn)?) * &s_init + + (a_word.clone() - public_word_eval(ShaPublicCol::PAOut)?) * &s_out; + let r8 = (e_word.clone() - public_word_eval(ShaPublicCol::PEIn)?) * &s_init + + (e_word.clone() - public_word_eval(ShaPublicCol::PEOut)?) * &s_out; + let r9 = word_eval(ShaWordCol::A, 4)? + - &a_word + - public_scalar_at_row(public, ShaPublicCol::PAIn, row, 0, field_cfg)? + + &mu_ff_a + + trace_int_at_row(trace, ShaIntCol::CompFeedForwardA, row, field_cfg)?; + let r10 = word_eval(ShaWordCol::E, 4)? + - &e_word + - public_scalar_at_row(public, ShaPublicCol::PEIn, row, 0, field_cfg)? + + &mu_ff_e + + trace_int_at_row(trace, ShaIntCol::CompFeedForwardE, row, field_cfg)?; + let r11 = (w - public_word_eval(ShaPublicCol::Message)?) * &s_msg; + let r12 = trace_int_at_row(trace, ShaIntCol::CompSchedule, row, field_cfg)? * &s_sched; + let r13 = trace_int_at_row(trace, ShaIntCol::CompUpdateA, row, field_cfg)? * &s_upd; + let r14 = trace_int_at_row(trace, ShaIntCol::CompUpdateE, row, field_cfg)? * &s_upd; + let r15 = trace_int_at_row(trace, ShaIntCol::CompFeedForwardA, row, field_cfg)? * &s_ff; + let r16 = trace_int_at_row(trace, ShaIntCol::CompFeedForwardE, row, field_cfg)? * &s_ff; + let r17 = word_eval_with(ShaWordCol::MuPacked, 0, &shift10_weights)?; + + let residuals = [ + r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, + ]; + let linear = FieldFieldInnerProduct::inner_product::( + &residuals, + lambda_powers, + F::zero_with_cfg(field_cfg), + ) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject( + "production SHA row residual dot product failed", + ) + })?; + + let mut bool_sum = F::zero_with_cfg(field_cfg); + for (source, weight) in booleanity_sources.iter().zip(booleanity_weights.iter()) { + let d = booleanity_source_value_at_fast(trace, row, source, field_cfg)?; + bool_sum += weight.clone() * (d.clone() * (d - one.clone())); + } + + Ok(row_weight.clone() * (linear + bool_sum)) + }) + .collect::, ProductionShaError>>()?; + + folded_row_integrand_sum(&values, field_cfg).map_err(ProductionShaError::from) +} + +fn trace_word_eval_at_row_with_weights( + trace: &ProjectedTrace, + col: ShaWordCol, + row: usize, + shift: usize, + weights: &[F], + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + let mut acc = F::zero_with_cfg(field_cfg); + for (bit, weight) in weights.iter().enumerate().take(SHA_WORD_BITS) { + acc += trace_word_bit_at_row(trace, col, row, shift, bit, field_cfg)? * weight; + } + Ok(acc) +} + +fn public_word_or_const_eval_at_row( + public: &ProjectedPublic, + col: ShaPublicCol, + row: usize, + weights: &[F], + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + let Some(_col_idx) = public_word_col_index(col) else { + return public_scalar_at_row(public, col, row, 0, field_cfg); + }; + if public.bit_slices.is_none() { + return public_scalar_at_row(public, col, row, 0, field_cfg); + } + let mut acc = F::zero_with_cfg(field_cfg); + for (bit, weight) in weights.iter().enumerate().take(SHA_WORD_BITS) { + acc += public_word_bit_at_row(public, col, row, bit, field_cfg)? * weight; + } + Ok(acc) +} + +fn booleanity_source_value_at_fast( + trace: &ProjectedTrace, + row: usize, + source: &ShaBooleanitySource, + field_cfg: &F::Config, +) -> Result> +where + F: PrimeField, +{ + let one = F::one_with_cfg(field_cfg); + let two = one.clone() + &one; + match *source { + ShaBooleanitySource::WordBit { col, bit } => { + trace_word_bit_at_row(trace, col, row, 0, bit, field_cfg) + } + ShaBooleanitySource::VirtualCh1 { bit } => { + Ok( + trace_word_bit_at_row(trace, ShaWordCol::E, row, 2, bit, field_cfg)? + + &trace_word_bit_at_row(trace, ShaWordCol::E, row, 1, bit, field_cfg)? + - two.clone() + * trace_word_bit_at_row(trace, ShaWordCol::Uef, row, 2, bit, field_cfg)?, + ) + } + ShaBooleanitySource::VirtualCh2 { bit } => { + Ok( + trace_word_bit_at_row(trace, ShaWordCol::E, row, 2, bit, field_cfg)? + - &trace_word_bit_at_row(trace, ShaWordCol::E, row, 0, bit, field_cfg)? + + two.clone() + * trace_word_bit_at_row(trace, ShaWordCol::UNegEg, row, 2, bit, field_cfg)? + + two.clone() + * trace_word_bit_at_row( + trace, + ShaWordCol::Ch2Comp, + row, + 0, + bit, + field_cfg, + )?, + ) + } + ShaBooleanitySource::VirtualMaj { bit } => { + Ok( + trace_word_bit_at_row(trace, ShaWordCol::A, row, 0, bit, field_cfg)? + + &trace_word_bit_at_row(trace, ShaWordCol::A, row, 1, bit, field_cfg)? + + &trace_word_bit_at_row(trace, ShaWordCol::A, row, 2, bit, field_cfg)? + - two.clone() + * trace_word_bit_at_row(trace, ShaWordCol::Maj, row, 2, bit, field_cfg)? + - two.clone() + * trace_word_bit_at_row( + trace, + ShaWordCol::MajComp, + row, + 0, + bit, + field_cfg, + )?, + ) + } + } +} + +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "prove", phase = "row_sumcheck") +)] +#[allow(clippy::too_many_arguments)] +fn prove_row_sumcheck_phase( + transcript: &mut impl Transcript, + trace: &ProjectedTrace, + public: &ProjectedPublic, + r_ic: &[F; SHA_ROW_VARS], + r_ic_eq_weights: &[F], + a_powers: &[F], + lambda_powers: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + row_claim: &F, + field_cfg: &F::Config, +) -> Result<(MultiDegreeSumcheckProof, FoldedRowSumcheckOutput), ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, +{ + let (combined_sumcheck, row_output) = + prove_expression_folded_row_sumcheck_with_output_and_vectors( + transcript, + trace, + public, + r_ic, + r_ic_eq_weights, + a_powers, + lambda_powers, + booleanity_weights, + booleanity_sources, + field_cfg, + )?; + verify_folded_row_sumcheck_claim(&combined_sumcheck.claimed_sums()[0], row_claim)?; + Ok((combined_sumcheck, row_output)) +} + +#[tracing::instrument( + target = "zinc_protocol::production_sha", + level = "info", + skip_all, + fields(side = "prove", phase = "endpoint_multipoint") +)] +#[allow(clippy::too_many_arguments)] +fn prove_endpoint_multipoint_phase( + transcript: &mut impl Transcript, + trace: &ProjectedTrace, + folded_public: &ProjectedPublic, + row_output: &FoldedRowSumcheckOutput, + r_ic: &[F; SHA_ROW_VARS], + a: &F, + a_powers: &[F], + lambda_powers: &[F], + booleanity_weights: &[F], + booleanity_sources: &[ShaBooleanitySource], + field_cfg: &F::Config, +) -> Result, ProductionShaError> +where + F: InnerTransparentField + + DelayedFieldProductSum + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, +{ + #[cfg(not(debug_assertions))] + let _ = ( + r_ic, + a, + a_powers, + lambda_powers, + booleanity_weights, + booleanity_sources, + ); + + let endpoint_evals = tracing::info_span!( + target: "zinc_protocol::production_sha", + "endpoint_build_evals", + side = "prove", + phase = "endpoint_build_evals", + ) + .in_scope(|| { + row_output.endpoint_evals.clone().map_or_else( + || { + build_sha_endpoint_evals_from_trace_with_row_weights( + trace, + &row_output.r_star_eq_weights, + a, + field_cfg, + ) + }, + Ok, + ) + })?; + let resolver = tracing::info_span!( + target: "zinc_protocol::production_sha", + "endpoint_resolver", + side = "prove", + phase = "endpoint_resolver", + ) + .in_scope(|| { + let resolver = sha_resolver_from_endpoint_evals(&endpoint_evals)?; + absorb_sha_resolver_proof(transcript, &resolver, field_cfg); + Ok::<_, ProductionShaError>(resolver) + })?; + #[cfg(debug_assertions)] + { + let resolver_endpoint_evals = sha_endpoint_evals_from_resolver(&resolver, a, field_cfg)?; + let terminal = tracing::info_span!( + target: "zinc_protocol::production_sha", + "endpoint_terminal", + side = "prove", + phase = "endpoint_terminal", + ) + .in_scope(|| { + reconstruct_folded_row_terminal_from_endpoints_with_vectors( + &resolver_endpoint_evals, + folded_public, + r_ic, + &row_output.r_star, + &row_output.r_star_eq_weights, + a_powers, + lambda_powers, + booleanity_weights, + booleanity_sources, + field_cfg, + ) + })?; + verify_folded_row_terminal_value(row_output, &terminal)?; + } + + let (multipoint_eval, r_0) = tracing::info_span!( + target: "zinc_protocol::production_sha", + "endpoint_reduce", + side = "prove", + phase = "endpoint_reduce", + ) + .in_scope(|| { + prove_sha_endpoint_multipoint_with_row_weights( + transcript, + trace, + folded_public, + &endpoint_evals, + &row_output.r_star, + &row_output.r_star_eq_weights, + field_cfg, + ) + })?; + Ok((resolver, endpoint_evals, multipoint_eval, r_0)) } #[tracing::instrument( @@ -1704,21 +2204,39 @@ where P::ArbitraryPCS: FoldablePCS, D>, P::IntPCS: FoldablePCS, { - let witness_lifted_evals = build_folded_sha_pcs_lifted_evals_with_row_weights( - folded_trace, - r_0_eq_weights, - field_cfg, - )?; - absorb_folded_lifted_evals(transcript, &witness_lifted_evals, field_cfg); - let opening_proof = P::prove_folded_pcs_opening( - pcs_params, - folded_commitments, - folded_trace, - folded_prover_data, - r_0, - &witness_lifted_evals, - field_cfg, - )?; + let witness_lifted_evals = tracing::info_span!( + target: "zinc_protocol::production_sha", + "pcs_lifted_evals", + side = "prove", + phase = "pcs_lifted_evals", + ) + .in_scope(|| { + build_folded_sha_pcs_lifted_evals_with_row_weights(folded_trace, r_0_eq_weights, field_cfg) + })?; + tracing::info_span!( + target: "zinc_protocol::production_sha", + "pcs_absorb_lifted_evals", + side = "prove", + phase = "pcs_absorb_lifted_evals", + ) + .in_scope(|| absorb_folded_lifted_evals(transcript, &witness_lifted_evals, field_cfg)); + let opening_proof = tracing::info_span!( + target: "zinc_protocol::production_sha", + "pcs_open_core", + side = "prove", + phase = "pcs_open_core", + ) + .in_scope(|| { + P::prove_folded_pcs_opening( + pcs_params, + folded_commitments, + folded_trace, + folded_prover_data, + r_0, + &witness_lifted_evals, + field_cfg, + ) + })?; Ok((witness_lifted_evals, opening_proof)) } @@ -2391,7 +2909,7 @@ fn sha_endpoint_evals_from_resolver( field_cfg: &F::Config, ) -> Result, ProductionShaError> where - F: PrimeField, + F: DelayedFieldProductSum, { if !resolver.down_evals.is_empty() { return Err(ProductionShaError::LengthMismatch { @@ -2601,7 +3119,7 @@ fn prove_production_sha_hyrax_pcs_opening( ) -> Result, Zt, F, D>, ProductionShaError> where Zt: ZincTypes, - F: HyraxFieldBridge, + F: HyraxFieldBridge + DelayedFieldProductSum, F::Inner: Transcribable, F::Modulus: Transcribable, C: AffineRepr, @@ -2639,9 +3157,9 @@ where let (binary_lifted, int_lifted) = split_folded_sha_pcs_lifted_evals(folded_lifted_evals)?; let arbitrary_lifted: &[DynamicPolynomialF] = &[]; - let binary_scalar_lanes = folded_sha_binary_scalar_lanes::(folded_trace)?; let arbitrary_scalar_lanes: Vec>> = Vec::new(); - let int_scalar_lanes = folded_sha_int_scalar_lanes::(folded_trace)?; + let binary_field_lanes = folded_sha_binary_field_lanes(folded_trace); + let int_field_lanes = folded_sha_int_field_lanes(folded_trace); let mut transcript = PcsProverTranscript { fs_transcript: Blake3Transcript::default(), @@ -2659,10 +3177,10 @@ where &mut transcription_buf, ); let binary_start = transcript.stream.position() as usize; - HyraxPCS::::prove_open_scalar_lanes::( + HyraxPCS::::prove_open_field_lanes_single_row::( &mut transcript, &pcs_params.binary, - &binary_scalar_lanes, + &binary_field_lanes, r_0, &folded_prover_data.binary, field_cfg, @@ -2701,10 +3219,10 @@ where &mut transcription_buf, ); let int_start = transcript.stream.position() as usize; - HyraxPCS::::prove_open_scalar_lanes::( + HyraxPCS::::prove_open_field_lanes_single_row::( &mut transcript, &pcs_params.int, - &int_scalar_lanes, + &int_field_lanes, r_0, &folded_prover_data.int, field_cfg, @@ -3377,25 +3895,29 @@ where expected: SHA_ROW_COUNT, }); } + #[cfg(debug_assertions)] validate_projected_trace(folded_trace)?; - let mut lifted = Vec::with_capacity(ShaWordCol::COUNT + ShaIntCol::COUNT); - for col in ShaWordCol::ALL { - let coeffs = sha_word_bits_at_point_with_weights_unchecked( - folded_trace, - col, - 0, - row_weights, - field_cfg, - )? - .to_vec(); - lifted.push(DynamicPolynomialF::new_trimmed(coeffs)); - } - for col in ShaIntCol::ALL { - lifted.push(DynamicPolynomialF::new_trimmed([ - sha_int_at_point_with_weights_unchecked(folded_trace, col, row_weights, field_cfg)?, - ])); - } - Ok(lifted) + let word_lifted = cfg_iter!(&ShaWordCol::ALL) + .map(|&col| { + let coeffs = sha_word_bits_at_point_with_weights_unchecked( + folded_trace, + col, + 0, + row_weights, + field_cfg, + )? + .to_vec(); + Ok(DynamicPolynomialF::new_trimmed(coeffs)) + }) + .collect::, ProductionShaError>>()?; + let int_lifted = cfg_iter!(&ShaIntCol::ALL) + .map(|&col| { + Ok(DynamicPolynomialF::new_trimmed([ + sha_int_at_point_with_weights_unchecked(folded_trace, col, row_weights, field_cfg)?, + ])) + }) + .collect::, ProductionShaError>>()?; + Ok(word_lifted.into_iter().chain(int_lifted).collect()) } fn split_folded_sha_pcs_lifted_evals( @@ -3450,20 +3972,33 @@ where C: AffineRepr, F: HyraxFieldBridge, { - ShaWordCol::ALL - .iter() - .map(|col| { - (0..32) - .map(|bit| { - let column = - &folded_trace.bit_slices[bit_slice_index(col.index(), bit, SHA_WORD_BITS)]; - (0..SHA_ROW_COUNT) - .map(|row| F::field_to_scalar(&column.evaluations[row])) - .collect::, _>>() - }) + let lanes = cfg_into_iter!(0..ShaWordCol::COUNT * SHA_WORD_BITS) + .map(|flat_idx| { + let col_idx = flat_idx / SHA_WORD_BITS; + let bit = flat_idx % SHA_WORD_BITS; + let column = &folded_trace.bit_slices[bit_slice_index(col_idx, bit, SHA_WORD_BITS)]; + column + .evaluations + .iter() + .map(F::field_to_scalar) .collect::, _>>() }) - .collect() + .collect::, _>>()?; + + let mut out = Vec::with_capacity(ShaWordCol::COUNT); + let mut lanes = lanes.into_iter(); + for _ in 0..ShaWordCol::COUNT { + let mut col_lanes = Vec::with_capacity(SHA_WORD_BITS); + for _ in 0..SHA_WORD_BITS { + col_lanes.push( + lanes + .next() + .expect("flat binary scalar lane count is exact"), + ); + } + out.push(col_lanes); + } + Ok(out) } #[allow(dead_code)] @@ -3474,18 +4009,47 @@ where C: AffineRepr, F: HyraxFieldBridge, { - ShaIntCol::ALL - .iter() + cfg_iter!(&ShaIntCol::ALL) .map(|col| { let column = &folded_trace.int_columns[col.index()]; - let lane = (0..SHA_ROW_COUNT) - .map(|row| F::field_to_scalar(&column.evaluations[row])) + let lane = column + .evaluations + .iter() + .map(F::field_to_scalar) .collect::, _>>()?; Ok(vec![lane]) }) .collect() } +fn folded_sha_binary_field_lanes(folded_trace: &ProjectedTrace) -> Vec> +where + F: PrimeField, +{ + ShaWordCol::ALL + .iter() + .map(|col| { + (0..SHA_WORD_BITS) + .map(|bit| { + folded_trace.bit_slices[bit_slice_index(col.index(), bit, SHA_WORD_BITS)] + .evaluations + .as_slice() + }) + .collect::>() + }) + .collect() +} + +fn folded_sha_int_field_lanes(folded_trace: &ProjectedTrace) -> Vec> +where + F: PrimeField, +{ + ShaIntCol::ALL + .iter() + .map(|col| vec![folded_trace.int_columns[col.index()].evaluations.as_slice()]) + .collect() +} + fn absorb_pcs_lifted_evals( transcript: &mut impl Transcript, lifted_evals: &[DynamicPolynomialF], @@ -3626,6 +4190,7 @@ pub fn prove_full_sha_sumfold( where F: InnerTransparentField + DelayedFieldProductSum + + ShaBinaryFoldField + FromPrimitiveWithConfig + Send + Sync @@ -3710,6 +4275,7 @@ pub fn prove_optimized_sha_sumfold( where F: InnerTransparentField + DelayedFieldProductSum + + ShaBinaryFoldField + FromPrimitiveWithConfig + Send + Sync @@ -3897,20 +4463,20 @@ where } let binary = commitments .iter() - .map(|commitment| commitment.binary.clone()) + .map(|commitment| &commitment.binary) .collect::>(); let arbitrary = commitments .iter() - .map(|commitment| commitment.arbitrary.clone()) + .map(|commitment| &commitment.arbitrary) .collect::>(); let int = commitments .iter() - .map(|commitment| commitment.int.clone()) + .map(|commitment| &commitment.int) .collect::>(); Ok(PCSCommitments { - binary: P::BinaryPCS::fold_commitments(&binary, theta, field_cfg)?, - arbitrary: P::ArbitraryPCS::fold_commitments(&arbitrary, theta, field_cfg)?, - int: P::IntPCS::fold_commitments(&int, theta, field_cfg)?, + binary: P::BinaryPCS::fold_commitment_refs(&binary, theta, field_cfg)?, + arbitrary: P::ArbitraryPCS::fold_commitment_refs(&arbitrary, theta, field_cfg)?, + int: P::IntPCS::fold_commitment_refs(&int, theta, field_cfg)?, }) } @@ -3977,6 +4543,18 @@ where Ok(proof) } +#[derive(Clone)] +struct RowExpressionOffsets { + word: [[usize; ROW_EXPR_WORD_SHIFT_SLOTS]; ShaWordCol::COUNT], + int: [usize; ShaIntCol::COUNT], + public_scalar: [[usize; ROW_EXPR_PUBLIC_SHIFT_SLOTS]; ShaPublicCol::COUNT], + public_word: [usize; ShaPublicCol::COUNT], +} + +const ROW_EXPR_MISSING_SOURCE: usize = usize::MAX; +const ROW_EXPR_WORD_SHIFT_SLOTS: usize = 17; +const ROW_EXPR_PUBLIC_SHIFT_SLOTS: usize = 4; + #[derive(Clone)] struct RowExpressionLayout { word_sources: Vec<(ShaWordCol, usize)>, @@ -4022,10 +4600,34 @@ impl RowExpressionLayout { } } - fn public_word_index(&self, col: ShaPublicCol) -> Option { - self.public_word_sources - .iter() - .position(|candidate| *candidate == col) + fn offsets(&self) -> RowExpressionOffsets { + let mut word = [[ROW_EXPR_MISSING_SOURCE; ROW_EXPR_WORD_SHIFT_SLOTS]; ShaWordCol::COUNT]; + for (idx, &(col, shift)) in self.word_sources.iter().enumerate() { + word[col.index()][shift] = idx; + } + + let mut int = [ROW_EXPR_MISSING_SOURCE; ShaIntCol::COUNT]; + for (idx, &col) in self.int_sources.iter().enumerate() { + int[col.index()] = idx; + } + + let mut public_scalar = + [[ROW_EXPR_MISSING_SOURCE; ROW_EXPR_PUBLIC_SHIFT_SLOTS]; ShaPublicCol::COUNT]; + for (idx, &(col, shift)) in self.public_scalar_sources.iter().enumerate() { + public_scalar[col.index()][shift] = idx; + } + + let mut public_word = [ROW_EXPR_MISSING_SOURCE; ShaPublicCol::COUNT]; + for (idx, &col) in self.public_word_sources.iter().enumerate() { + public_word[col.index()] = idx; + } + + RowExpressionOffsets { + word, + int, + public_scalar, + public_word, + } } } @@ -4182,6 +4784,50 @@ where out } +fn row_expr_mle_from_table_shift( + label: &'static str, + table: &MleTable, + col_idx: usize, + shift: usize, + zero: &F, + zero_inner: &F::Inner, +) -> Result, ProductionShaError> +where + F: InnerTransparentField, +{ + let column = table + .get(col_idx) + .ok_or(ProductionShaError::LengthMismatch { + label, + got: col_idx, + expected: table.len(), + })?; + if column.num_vars != SHA_ROW_VARS || column.evaluations.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label, + got: column.evaluations.len(), + expected: SHA_ROW_COUNT, + }); + } + + let mut evaluations = Vec::with_capacity(SHA_ROW_COUNT); + if shift < SHA_ROW_COUNT { + evaluations.extend( + column.evaluations[shift..] + .iter() + .map(|value| value.inner().clone()), + ); + } + evaluations.resize(SHA_ROW_COUNT, zero_inner.clone()); + debug_assert_eq!(evaluations.len(), SHA_ROW_COUNT); + let _ = zero; + Ok(DenseMultilinearExtension::from_evaluations_vec( + SHA_ROW_VARS, + evaluations, + zero_inner.clone(), + )) +} + #[allow(clippy::too_many_arguments)] fn build_production_sha_row_expression_sumcheck_group( trace: &ProjectedTrace, @@ -4307,60 +4953,98 @@ where for &(col, shift) in &layout.word_sources { for bit in 0..SHA_WORD_BITS { - let values = (0..SHA_ROW_COUNT) - .map(|row| trace_word_bit_at_row(trace, col, row, shift, bit, field_cfg)) - .collect::, _>>()?; - mles.push(DenseMultilinearExtension::from_evaluations_vec( - SHA_ROW_VARS, - values.iter().map(|value| value.inner().clone()).collect(), - zero_inner.clone(), - )); + mles.push(row_expr_mle_from_table_shift( + "SHA trace word bit", + &trace.bit_slices, + bit_slice_index(col.index(), bit, SHA_WORD_BITS), + shift, + &zero, + &zero_inner, + )?); } } for &col in &layout.int_sources { - let values = (0..SHA_ROW_COUNT) - .map(|row| trace_int_at_row(trace, col, row, field_cfg)) - .collect::, _>>()?; - mles.push(DenseMultilinearExtension::from_evaluations_vec( - SHA_ROW_VARS, - values.iter().map(|value| value.inner().clone()).collect(), - zero_inner.clone(), - )); + mles.push(row_expr_mle_from_table_shift( + "SHA trace int", + &trace.int_columns, + col.index(), + 0, + &zero, + &zero_inner, + )?); } for &(col, shift) in &layout.public_scalar_sources { - let values = (0..SHA_ROW_COUNT) - .map(|row| public_scalar_at_row(public, col, row, shift, field_cfg)) - .collect::, _>>()?; - mles.push(DenseMultilinearExtension::from_evaluations_vec( - SHA_ROW_VARS, - values.iter().map(|value| value.inner().clone()).collect(), - zero_inner.clone(), - )); + mles.push(row_expr_mle_from_table_shift( + "SHA public scalar", + &public.columns, + col.index(), + shift, + &zero, + &zero_inner, + )?); } for &col in &layout.public_word_sources { + let bit_slices = + public + .bit_slices + .as_ref() + .ok_or(ProductionShaError::NonCanonicalProofObject( + "production SHA public word columns are required", + ))?; + let col_idx = public_word_col_index(col).ok_or( + ProductionShaError::NonCanonicalProofObject("SHA public column is not a public word"), + )?; for bit in 0..SHA_WORD_BITS { - let values = (0..SHA_ROW_COUNT) - .map(|row| public_word_bit_at_row(public, col, row, bit, field_cfg)) - .collect::, _>>()?; - mles.push(DenseMultilinearExtension::from_evaluations_vec( - SHA_ROW_VARS, - values.iter().map(|value| value.inner().clone()).collect(), - zero_inner.clone(), - )); + mles.push(row_expr_mle_from_table_shift( + "SHA public word bit", + bit_slices, + bit_slice_index(col_idx, bit, SHA_WORD_BITS), + 0, + &zero, + &zero_inner, + )?); } } - let a_powers = a_powers.to_vec(); + let offsets = layout.offsets(); + let word_weights = a_powers[..SHA_WORD_BITS].to_vec(); + let rot_weights = |shift: usize| { + (0..SHA_WORD_BITS) + .map(|bit| a_powers[(bit + shift) % SHA_WORD_BITS].clone()) + .collect::>() + }; + let shift_weights = |shift: usize| { + (0..SHA_WORD_BITS) + .map(|bit| { + if bit >= shift { + a_powers[bit - shift].clone() + } else { + F::zero_with_cfg(field_cfg) + } + }) + .collect::>() + }; + let rot25_weights = rot_weights(25); + let rot14_weights = rot_weights(14); + let rot15_weights = rot_weights(15); + let rot13_weights = rot_weights(13); + let shift3_weights = shift_weights(3); + let shift10_weights = shift_weights(10); + let shift0_weights = shift_weights(0); + let shift2_weights = shift_weights(2); + let shift5_weights = shift_weights(5); + let shift8_weights = shift_weights(8); + let shift9_weights = shift_weights(9); let lambda_powers = lambda_powers.to_vec(); let booleanity_weights = booleanity_weights.to_vec(); let booleanity_sources = booleanity_sources.to_vec(); let one = F::one_with_cfg(field_cfg); let two = one.clone() + &one; - let rho_sig0 = sparse_endpoint_poly::(&[10, 19, 30], field_cfg); - let rho_sig1 = sparse_endpoint_poly::(&[7, 21, 26], field_cfg); + let rho_sig0 = a_powers[10].clone() + &a_powers[19] + &a_powers[30]; + let rho_sig1 = a_powers[7].clone() + &a_powers[21] + &a_powers[26]; let low_mu_coeff = production_sha_pow_two(32, field_cfg); let high_mu_w_coeff = production_sha_pow_two(34, field_cfg); let high_mu_3_bit_coeff = production_sha_pow_two(35, field_cfg); @@ -4371,142 +5055,108 @@ where mles, Box::new(move |values: &[F]| { let zero = zero.clone(); - let const_poly = |value: F| { - if F::is_zero(&value) { - DynamicPolynomialF::ZERO - } else { - DynamicPolynomialF::new_trimmed([value]) - } + let dot = |lhs: &[F], rhs: &[F]| { + FieldFieldInnerProduct::inner_product::(lhs, rhs, zero.clone()) + .expect("row expression dot product lengths match") }; - let eval_poly = |poly: &DynamicPolynomialF| { - if poly.coeffs.is_empty() { - return zero.clone(); - } - DynamicPolyFInnerProduct::inner_product::( - &poly.coeffs, - &a_powers[..poly.coeffs.len()], - zero.clone(), - ) - .expect("row expression polynomial degree is bounded") - }; - - let word_bits_by_source = (0..layout.word_sources.len()) - .map(|source_idx| { - std::array::from_fn(|bit| { - values[layout.word_offset + source_idx * SHA_WORD_BITS + bit].clone() - }) - }) - .collect::>(); - let public_word_bits_by_source = (0..layout.public_word_sources.len()) - .map(|source_idx| { - std::array::from_fn(|bit| { - values[layout.public_word_offset + source_idx * SHA_WORD_BITS + bit].clone() - }) - }) - .collect::>(); - let word_source_idx = |col: ShaWordCol, shift: usize| { - layout - .word_sources - .iter() - .position(|source| *source == (col, shift)) - .expect("row expression word source is present") - }; - let word_bits = |col: ShaWordCol, shift: usize| -> &[F; SHA_WORD_BITS] { - &word_bits_by_source[word_source_idx(col, shift)] + let idx = offsets.word[col.index()][shift]; + debug_assert_ne!(idx, ROW_EXPR_MISSING_SOURCE); + idx }; - let word_poly = |col: ShaWordCol, shift: usize| { - DynamicPolynomialF::new_trimmed(word_bits(col, shift).to_vec()) + let word_bits = |col: ShaWordCol, shift: usize| { + let source_idx = word_source_idx(col, shift); + let base = layout.word_offset + source_idx * SHA_WORD_BITS; + &values[base..base + SHA_WORD_BITS] }; + let word_eval = + |col: ShaWordCol, shift: usize| dot(word_bits(col, shift), &word_weights); + let word_eval_with = + |col: ShaWordCol, shift: usize, weights: &[F]| dot(word_bits(col, shift), weights); + let word_bit = + |col: ShaWordCol, shift: usize, bit: usize| word_bits(col, shift)[bit].clone(); let int_value = |col: ShaIntCol| { - let idx = layout - .int_sources - .iter() - .position(|candidate| *candidate == col) - .expect("row expression int source is present"); + let idx = offsets.int[col.index()]; + debug_assert_ne!(idx, ROW_EXPR_MISSING_SOURCE); values[layout.int_offset + idx].clone() }; - let int_poly = |col: ShaIntCol| const_poly(int_value(col)); let public_scalar = |col: ShaPublicCol, shift: usize| { - let idx = layout - .public_scalar_sources - .iter() - .position(|source| *source == (col, shift)) - .expect("row expression public scalar source is present"); + let idx = offsets.public_scalar[col.index()][shift]; + debug_assert_ne!(idx, ROW_EXPR_MISSING_SOURCE); values[layout.public_scalar_offset + idx].clone() }; - let public_word_or_const_poly = |col: ShaPublicCol| { - if let Some(idx) = layout.public_word_index(col) { - DynamicPolynomialF::new_trimmed(public_word_bits_by_source[idx].to_vec()) + let public_word_or_const_eval = |col: ShaPublicCol| { + let idx = offsets.public_word[col.index()]; + if idx == ROW_EXPR_MISSING_SOURCE { + public_scalar(col, 0) } else { - const_poly(public_scalar(col, 0)) + let base = layout.public_word_offset + idx * SHA_WORD_BITS; + dot(&values[base..base + SHA_WORD_BITS], &word_weights) } }; - let a_word = word_poly(ShaWordCol::A, 0); - let e_word = word_poly(ShaWordCol::E, 0); - let sigma0 = word_poly(ShaWordCol::Sigma0, 0); - let sigma1 = word_poly(ShaWordCol::Sigma1, 0); - let w = word_poly(ShaWordCol::W, 0); - let small_sigma0 = word_poly(ShaWordCol::SmallSigma0, 0); - let small_sigma1 = word_poly(ShaWordCol::SmallSigma1, 0); - let ov_sigma0 = word_poly(ShaWordCol::OvSigma0, 0); - let ov_sigma1 = word_poly(ShaWordCol::OvSigma1, 0); - let ov_small_sigma0 = word_poly(ShaWordCol::OvSmallSigma0, 0); - let ov_small_sigma1 = word_poly(ShaWordCol::OvSmallSigma1, 0); - - let mu = |low: u32, high: u32, high_coeff: &F| { - let packed = word_poly(ShaWordCol::MuPacked, 0).shift_r_c(low); - let tail = word_poly(ShaWordCol::MuPacked, 0).shift_r_c(high); - scale_endpoint_poly(&packed, &low_mu_coeff) - - &scale_endpoint_poly(&tail, high_coeff) + let a_word = word_eval(ShaWordCol::A, 0); + let e_word = word_eval(ShaWordCol::E, 0); + let sigma0 = word_eval(ShaWordCol::Sigma0, 0); + let sigma1 = word_eval(ShaWordCol::Sigma1, 0); + let w = word_eval(ShaWordCol::W, 0); + let small_sigma0 = word_eval(ShaWordCol::SmallSigma0, 0); + let small_sigma1 = word_eval(ShaWordCol::SmallSigma1, 0); + let ov_sigma0 = word_eval(ShaWordCol::OvSigma0, 0); + let ov_sigma1 = word_eval(ShaWordCol::OvSigma1, 0); + let ov_small_sigma0 = word_eval(ShaWordCol::OvSmallSigma0, 0); + let ov_small_sigma1 = word_eval(ShaWordCol::OvSmallSigma1, 0); + + let mu = |low_weights: &[F], high_weights: &[F], high_coeff: &F| { + word_eval_with(ShaWordCol::MuPacked, 0, low_weights) * &low_mu_coeff + - word_eval_with(ShaWordCol::MuPacked, 0, high_weights) * high_coeff }; - let mu_w = mu(0, 2, &high_mu_w_coeff); - let mu_a = mu(2, 5, &high_mu_3_bit_coeff); - let mu_e = mu(5, 8, &high_mu_3_bit_coeff); - let mu_ff_a = mu(8, 9, &high_mu_1_bit_coeff); - let mu_ff_e = mu(9, 10, &high_mu_1_bit_coeff); - - let r0 = (&a_word * &rho_sig0) - &sigma0 - &scale_endpoint_poly(&ov_sigma0, &two); - let r1 = (&e_word * &rho_sig1) - &sigma1 - &scale_endpoint_poly(&ov_sigma1, &two); - let r2 = word_poly(ShaWordCol::W, 0).rot_c(25) - + &word_poly(ShaWordCol::W, 0).rot_c(14) - + &word_poly(ShaWordCol::W, 0).shift_r_c(3) + let mu_w = mu(&shift0_weights, &shift2_weights, &high_mu_w_coeff); + let mu_a = mu(&shift2_weights, &shift5_weights, &high_mu_3_bit_coeff); + let mu_e = mu(&shift5_weights, &shift8_weights, &high_mu_3_bit_coeff); + let mu_ff_a = mu(&shift8_weights, &shift9_weights, &high_mu_1_bit_coeff); + let mu_ff_e = mu(&shift9_weights, &shift10_weights, &high_mu_1_bit_coeff); + + let r0 = a_word.clone() * &rho_sig0 - &sigma0 - two.clone() * &ov_sigma0; + let r1 = e_word.clone() * &rho_sig1 - &sigma1 - two.clone() * &ov_sigma1; + let r2 = word_eval_with(ShaWordCol::W, 0, &rot25_weights) + + word_eval_with(ShaWordCol::W, 0, &rot14_weights) + + word_eval_with(ShaWordCol::W, 0, &shift3_weights) - &small_sigma0 - - &scale_endpoint_poly(&ov_small_sigma0, &two); - let r3 = word_poly(ShaWordCol::W, 0).rot_c(15) - + &word_poly(ShaWordCol::W, 0).rot_c(13) - + &word_poly(ShaWordCol::W, 0).shift_r_c(10) + - two.clone() * &ov_small_sigma0; + let r3 = word_eval_with(ShaWordCol::W, 0, &rot15_weights) + + word_eval_with(ShaWordCol::W, 0, &rot13_weights) + + word_eval_with(ShaWordCol::W, 0, &shift10_weights) - &small_sigma1 - - &scale_endpoint_poly(&ov_small_sigma1, &two); - let r4 = word_poly(ShaWordCol::W, 16) + - two.clone() * &ov_small_sigma1; + let r4 = word_eval(ShaWordCol::W, 16) - &w - - &word_poly(ShaWordCol::SmallSigma0, 1) - - &word_poly(ShaWordCol::W, 9) - - &word_poly(ShaWordCol::SmallSigma1, 14) + - word_eval(ShaWordCol::SmallSigma0, 1) + - word_eval(ShaWordCol::W, 9) + - word_eval(ShaWordCol::SmallSigma1, 14) + &mu_w - + &int_poly(ShaIntCol::CompSchedule); - let r5 = word_poly(ShaWordCol::A, 4) + + int_value(ShaIntCol::CompSchedule); + let r5 = word_eval(ShaWordCol::A, 4) - &e_word - - &word_poly(ShaWordCol::Sigma1, 3) - - &word_poly(ShaWordCol::Uef, 3) - - &word_poly(ShaWordCol::UNegEg, 3) - - &const_poly(public_scalar(ShaPublicCol::K, 3)) + - word_eval(ShaWordCol::Sigma1, 3) + - word_eval(ShaWordCol::Uef, 3) + - word_eval(ShaWordCol::UNegEg, 3) + - public_scalar(ShaPublicCol::K, 3) - &w - - &word_poly(ShaWordCol::Sigma0, 3) - - &word_poly(ShaWordCol::Maj, 3) + - word_eval(ShaWordCol::Sigma0, 3) + - word_eval(ShaWordCol::Maj, 3) + &mu_a - + &int_poly(ShaIntCol::CompUpdateA); - let r6 = word_poly(ShaWordCol::E, 4) + + int_value(ShaIntCol::CompUpdateA); + let r6 = word_eval(ShaWordCol::E, 4) - &a_word - &e_word - - &word_poly(ShaWordCol::Sigma1, 3) - - &word_poly(ShaWordCol::Uef, 3) - - &word_poly(ShaWordCol::UNegEg, 3) - - &const_poly(public_scalar(ShaPublicCol::K, 3)) + - word_eval(ShaWordCol::Sigma1, 3) + - word_eval(ShaWordCol::Uef, 3) + - word_eval(ShaWordCol::UNegEg, 3) + - public_scalar(ShaPublicCol::K, 3) - &w + &mu_e - + &int_poly(ShaIntCol::CompUpdateE); + + int_value(ShaIntCol::CompUpdateE); let s_init = public_scalar(ShaPublicCol::SInit, 0); let s_msg = public_scalar(ShaPublicCol::SMsg, 0); @@ -4515,85 +5165,56 @@ where let s_ff = public_scalar(ShaPublicCol::SFf, 0); let s_out = public_scalar(ShaPublicCol::SOut, 0); - let r7 = scale_endpoint_poly( - &(a_word.clone() - &public_word_or_const_poly(ShaPublicCol::PAIn)), - &s_init, - ) + &scale_endpoint_poly( - &(a_word.clone() - &public_word_or_const_poly(ShaPublicCol::PAOut)), - &s_out, - ); - let r8 = scale_endpoint_poly( - &(e_word.clone() - &public_word_or_const_poly(ShaPublicCol::PEIn)), - &s_init, - ) + &scale_endpoint_poly( - &(e_word.clone() - &public_word_or_const_poly(ShaPublicCol::PEOut)), - &s_out, - ); - let r9 = word_poly(ShaWordCol::A, 4) - - &a_word - - &const_poly(public_scalar(ShaPublicCol::PAIn, 0)) + let r7 = (a_word.clone() - public_word_or_const_eval(ShaPublicCol::PAIn)) * &s_init + + (a_word.clone() - public_word_or_const_eval(ShaPublicCol::PAOut)) * &s_out; + let r8 = (e_word.clone() - public_word_or_const_eval(ShaPublicCol::PEIn)) * &s_init + + (e_word.clone() - public_word_or_const_eval(ShaPublicCol::PEOut)) * &s_out; + let r9 = word_eval(ShaWordCol::A, 4) - &a_word - public_scalar(ShaPublicCol::PAIn, 0) + &mu_ff_a - + &int_poly(ShaIntCol::CompFeedForwardA); - let r10 = word_poly(ShaWordCol::E, 4) - - &e_word - - &const_poly(public_scalar(ShaPublicCol::PEIn, 0)) + + int_value(ShaIntCol::CompFeedForwardA); + let r10 = word_eval(ShaWordCol::E, 4) - &e_word - public_scalar(ShaPublicCol::PEIn, 0) + &mu_ff_e - + &int_poly(ShaIntCol::CompFeedForwardE); - let r11 = scale_endpoint_poly( - &(w - &public_word_or_const_poly(ShaPublicCol::Message)), - &s_msg, - ); - let r12 = scale_endpoint_poly(&int_poly(ShaIntCol::CompSchedule), &s_sched); - let r13 = scale_endpoint_poly(&int_poly(ShaIntCol::CompUpdateA), &s_upd); - let r14 = scale_endpoint_poly(&int_poly(ShaIntCol::CompUpdateE), &s_upd); - let r15 = scale_endpoint_poly(&int_poly(ShaIntCol::CompFeedForwardA), &s_ff); - let r16 = scale_endpoint_poly(&int_poly(ShaIntCol::CompFeedForwardE), &s_ff); - let r17 = word_poly(ShaWordCol::MuPacked, 0).shift_r_c(10); + + int_value(ShaIntCol::CompFeedForwardE); + let r11 = (w - public_word_or_const_eval(ShaPublicCol::Message)) * &s_msg; + let r12 = int_value(ShaIntCol::CompSchedule) * &s_sched; + let r13 = int_value(ShaIntCol::CompUpdateA) * &s_upd; + let r14 = int_value(ShaIntCol::CompUpdateE) * &s_upd; + let r15 = int_value(ShaIntCol::CompFeedForwardA) * &s_ff; + let r16 = int_value(ShaIntCol::CompFeedForwardE) * &s_ff; + let r17 = word_eval_with(ShaWordCol::MuPacked, 0, &shift10_weights); let residuals = [ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, ]; - let residual_evals: [F; NUM_SHA_RESIDUAL_FAMILIES] = - std::array::from_fn(|idx| eval_poly(&residuals[idx])); let linear = FieldFieldInnerProduct::inner_product::( - &residual_evals, + &residuals, &lambda_powers, zero.clone(), ) .expect("row expression residual dot product lengths match"); - let source_bit = - |col: ShaWordCol, shift: usize, bit: usize| word_bits(col, shift)[bit].clone(); - let mut bool_terms = Vec::with_capacity(booleanity_sources.len()); - for source in &booleanity_sources { + let mut bool_sum = zero.clone(); + for (source, weight) in booleanity_sources.iter().zip(booleanity_weights.iter()) { let d = match *source { - ShaBooleanitySource::WordBit { col, bit } => source_bit(col, 0, bit), + ShaBooleanitySource::WordBit { col, bit } => word_bit(col, 0, bit), ShaBooleanitySource::VirtualCh1 { bit: bit_idx } => { - source_bit(ShaWordCol::E, 2, bit_idx) - + &source_bit(ShaWordCol::E, 1, bit_idx) - - two.clone() * source_bit(ShaWordCol::Uef, 2, bit_idx) + word_bit(ShaWordCol::E, 2, bit_idx) + &word_bit(ShaWordCol::E, 1, bit_idx) + - two.clone() * word_bit(ShaWordCol::Uef, 2, bit_idx) } ShaBooleanitySource::VirtualCh2 { bit: bit_idx } => { - source_bit(ShaWordCol::E, 2, bit_idx) - - &source_bit(ShaWordCol::E, 0, bit_idx) - + two.clone() * source_bit(ShaWordCol::UNegEg, 2, bit_idx) - + two.clone() * source_bit(ShaWordCol::Ch2Comp, 0, bit_idx) + word_bit(ShaWordCol::E, 2, bit_idx) - &word_bit(ShaWordCol::E, 0, bit_idx) + + two.clone() * word_bit(ShaWordCol::UNegEg, 2, bit_idx) + + two.clone() * word_bit(ShaWordCol::Ch2Comp, 0, bit_idx) } ShaBooleanitySource::VirtualMaj { bit: bit_idx } => { - source_bit(ShaWordCol::A, 0, bit_idx) - + &source_bit(ShaWordCol::A, 1, bit_idx) - + &source_bit(ShaWordCol::A, 2, bit_idx) - - two.clone() * source_bit(ShaWordCol::Maj, 2, bit_idx) - - two.clone() * source_bit(ShaWordCol::MajComp, 0, bit_idx) + word_bit(ShaWordCol::A, 0, bit_idx) + + &word_bit(ShaWordCol::A, 1, bit_idx) + + &word_bit(ShaWordCol::A, 2, bit_idx) + - two.clone() * word_bit(ShaWordCol::Maj, 2, bit_idx) + - two.clone() * word_bit(ShaWordCol::MajComp, 0, bit_idx) } }; - bool_terms.push(d.clone() * (d - one.clone())); + bool_sum += weight.clone() * (d.clone() * (d - one.clone())); } - let bool_sum = FieldFieldInnerProduct::inner_product::( - &booleanity_weights, - &bool_terms, - zero.clone(), - ) - .expect("row expression booleanity dot product lengths match"); values[0].clone() * (linear + bool_sum) }), @@ -4747,6 +5368,68 @@ where ) } +fn row_sumcheck_terminal_from_proof( + proof: &MultiDegreeSumcheckProof, + challenges: &[F], +) -> Result> +where + F: FromPrimitiveWithConfig, +{ + if proof.group_messages().len() != 1 { + return Err(ProductionShaError::UnexpectedSumcheckGroupCount { + label: "row sumcheck terminal", + got: proof.group_messages().len(), + }); + } + if proof.claimed_sums().len() != 1 { + return Err(ProductionShaError::UnexpectedSumcheckGroupCount { + label: "row sumcheck terminal claimed sums", + got: proof.claimed_sums().len(), + }); + } + let degree = proof + .degrees() + .first() + .ok_or(ProductionShaError::LengthMismatch { + label: "row sumcheck terminal degrees", + got: 0, + expected: 1, + })?; + let messages = proof + .group_messages() + .first() + .expect("checked row sumcheck group count"); + if messages.len() != challenges.len() { + return Err(ProductionShaError::LengthMismatch { + label: "row sumcheck terminal rounds", + got: messages.len(), + expected: challenges.len(), + }); + } + + let mut expected = proof.claimed_sums()[0].clone(); + for (message, challenge) in messages.iter().zip(challenges) { + let tail = &message.0.tail_evaluations; + if tail.len() != *degree { + return Err(ProductionShaError::LengthMismatch { + label: "row sumcheck terminal degree", + got: tail.len(), + expected: *degree, + }); + } + let constant = match tail.first() { + Some(p1) => expected.clone() - p1, + None => expected.clone(), + }; + let mut evaluations = Vec::with_capacity(tail.len() + 1); + evaluations.push(constant); + evaluations.extend_from_slice(tail); + expected = NatEvaluatedPoly::new(evaluations).evaluate_at_point(challenge)?; + } + + Ok(expected) +} + #[allow(clippy::too_many_arguments)] pub fn prove_expression_folded_row_sumcheck_with_output_and_vectors( transcript: &mut impl Transcript, @@ -4770,18 +5453,36 @@ where F::Inner: Transcribable + Zero, F::Modulus: Transcribable, { - let group = build_production_sha_row_expression_sumcheck_group_with_vectors( - trace, - public, - r_ic_eq_weights, - a_powers, - lambda_powers, - booleanity_weights, - booleanity_sources, - field_cfg, - )?; - let (proof, states) = - MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], SHA_ROW_VARS, field_cfg); + #[cfg(not(debug_assertions))] + let _ = r_ic; + + let group = tracing::info_span!( + target: "zinc_protocol::production_sha", + "row_sumcheck_build_group", + side = "prove", + phase = "row_sumcheck_build_group", + ) + .in_scope(|| { + build_production_sha_row_expression_sumcheck_group_with_vectors( + trace, + public, + r_ic_eq_weights, + a_powers, + lambda_powers, + booleanity_weights, + booleanity_sources, + field_cfg, + ) + })?; + let (proof, states) = tracing::info_span!( + target: "zinc_protocol::production_sha", + "row_sumcheck_prove_core", + side = "prove", + phase = "row_sumcheck_prove_core", + ) + .in_scope(|| { + MultiDegreeSumcheck::prove_as_subprotocol(transcript, vec![group], SHA_ROW_VARS, field_cfg) + }); let r_star = states .first() .ok_or(ProductionShaError::LengthMismatch { @@ -4797,30 +5498,60 @@ where got: a_powers.len(), expected: 2, })?; - let endpoint_evals = build_sha_endpoint_evals_from_trace_with_row_weights( - trace, - &r_star_eq_weights, - a, - field_cfg, - )?; - let terminal_value = reconstruct_folded_row_terminal_from_endpoints_with_vectors( - &endpoint_evals, - public, - r_ic, - &r_star, - &r_star_eq_weights, - a_powers, - lambda_powers, - booleanity_weights, - booleanity_sources, - field_cfg, - )?; + let endpoint_evals = tracing::info_span!( + target: "zinc_protocol::production_sha", + "row_sumcheck_endpoint_evals", + side = "prove", + phase = "row_sumcheck_endpoint_evals", + ) + .in_scope(|| { + build_sha_endpoint_evals_from_trace_with_row_weights( + trace, + &r_star_eq_weights, + a, + field_cfg, + ) + })?; + let terminal_value = tracing::info_span!( + target: "zinc_protocol::production_sha", + "row_sumcheck_terminal", + side = "prove", + phase = "row_sumcheck_terminal", + ) + .in_scope(|| row_sumcheck_terminal_from_proof(&proof, &r_star))?; + #[cfg(debug_assertions)] + { + let reconstructed_terminal = tracing::info_span!( + target: "zinc_protocol::production_sha", + "row_sumcheck_terminal_debug", + side = "prove", + phase = "row_sumcheck_terminal_debug", + ) + .in_scope(|| { + reconstruct_folded_row_terminal_from_endpoints_with_vectors( + &endpoint_evals, + public, + r_ic, + &r_star, + &r_star_eq_weights, + a_powers, + lambda_powers, + booleanity_weights, + booleanity_sources, + field_cfg, + ) + })?; + if terminal_value != reconstructed_terminal { + return Err(ProductionShaError::RowSumcheckTerminalMismatch); + } + } Ok(( proof, FoldedRowSumcheckOutput { r_star, r_star_eq_weights, terminal_value, + endpoint_evals: Some(endpoint_evals), }, )) } @@ -4863,6 +5594,7 @@ where r_star, r_star_eq_weights, terminal_value: subclaims.expected_evaluations()[0].clone(), + endpoint_evals: None, }) } @@ -4962,7 +5694,7 @@ pub fn build_sha_endpoint_evals_from_trace( field_cfg: &F::Config, ) -> Result, ProductionShaError> where - F: PrimeField, + F: DelayedFieldProductSum, { let row_weights = build_eq_x_r_vec(r_star, field_cfg)?; build_sha_endpoint_evals_from_trace_with_row_weights(trace, &row_weights, a, field_cfg) @@ -4975,7 +5707,7 @@ pub fn build_sha_endpoint_evals_from_trace_with_row_weights( field_cfg: &F::Config, ) -> Result, ProductionShaError> where - F: PrimeField, + F: DelayedFieldProductSum, { if row_weights.len() != SHA_ROW_COUNT { return Err(ProductionShaError::LengthMismatch { @@ -4984,9 +5716,12 @@ where expected: SHA_ROW_COUNT, }); } + #[cfg(debug_assertions)] + validate_projected_trace(trace)?; let mut sources = Vec::new(); for (col, shift) in production_sha_endpoint_word_sources() { - let bits = sha_word_bits_at_point_with_weights(trace, col, shift, row_weights, field_cfg)?; + let bits = + sha_endpoint_word_bits_with_row_weights(trace, col, shift, row_weights, field_cfg)?; sources.push(ShaSourceEndpointEval { col, shift, @@ -4998,7 +5733,7 @@ where for col in production_sha_endpoint_int_sources() { int_sources.push(ShaIntEndpointEval { col, - scalar: sha_int_at_point_with_weights(trace, col, row_weights, field_cfg)?, + scalar: sha_endpoint_int_with_row_weights(trace, col, row_weights, field_cfg)?, }); } Ok(ShaEndpointEvals { @@ -5007,6 +5742,84 @@ where }) } +fn sha_endpoint_word_bits_with_row_weights( + trace: &ProjectedTrace, + col: ShaWordCol, + shift: usize, + row_weights: &[F], + field_cfg: &F::Config, +) -> Result<[F; SHA_WORD_BITS], ProductionShaError> +where + F: DelayedFieldProductSum, +{ + let active_len = SHA_ROW_COUNT.saturating_sub(shift); + if active_len == 0 { + return Ok(std::array::from_fn(|_| F::zero_with_cfg(field_cfg))); + } + let weights = &row_weights[..active_len]; + let values_start = shift; + let values_end = shift + active_len; + let bits = cfg_into_iter!(0..SHA_WORD_BITS) + .map(|bit| { + let table_idx = bit_slice_index(col.index(), bit, SHA_WORD_BITS); + let column = + trace + .bit_slices + .get(table_idx) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA endpoint bit column", + got: table_idx, + expected: trace.bit_slices.len(), + })?; + FieldFieldInnerProduct::inner_product::( + weights, + &column.evaluations[values_start..values_end], + F::zero_with_cfg(field_cfg), + ) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject( + "SHA endpoint bit row-weight dot product failed", + ) + }) + }) + .collect::, _>>()?; + bits.try_into() + .map_err(|bits: Vec| ProductionShaError::LengthMismatch { + label: "SHA endpoint word bits", + got: bits.len(), + expected: SHA_WORD_BITS, + }) +} + +fn sha_endpoint_int_with_row_weights( + trace: &ProjectedTrace, + col: ShaIntCol, + row_weights: &[F], + field_cfg: &F::Config, +) -> Result> +where + F: DelayedFieldProductSum, +{ + let column = trace + .int_columns + .get(col.index()) + .ok_or(ProductionShaError::LengthMismatch { + label: "SHA endpoint int column", + got: col.index(), + expected: trace.int_columns.len(), + })?; + FieldFieldInnerProduct::inner_product::( + row_weights, + &column.evaluations, + F::zero_with_cfg(field_cfg), + ) + .map_err(|_| { + ProductionShaError::NonCanonicalProofObject( + "SHA endpoint int row-weight dot product failed", + ) + }) +} + pub fn validate_sha_endpoint_layout( endpoint_evals: &ShaEndpointEvals, ) -> Result<(), ProductionShaError> @@ -5104,32 +5917,73 @@ where }); } validate_sha_endpoint_layout(endpoint_evals)?; - let layout = production_sha_multipoint_layout(); - let trace_mles = sha_multipoint_trace_mles(folded_trace, folded_public, &layout, field_cfg)?; - let up_evals = sha_multipoint_up_evals_with_row_weights( - endpoint_evals, - folded_public, - row_weights, - &layout, - field_cfg, - )?; - let (shift_specs, down_evals) = sha_multipoint_shift_specs_and_down_evals_with_row_weights( - endpoint_evals, - folded_public, - row_weights, - &layout, - field_cfg, - )?; - prove_multipoint_reduction( - transcript, - &trace_mles, - r_star, - &up_evals, - &down_evals, - &shift_specs, - field_cfg, + let layout = tracing::info_span!( + target: "zinc_protocol::production_sha", + "multipoint_layout", + side = "prove", + phase = "multipoint_layout", + ) + .in_scope(production_sha_multipoint_layout); + let trace_mles = tracing::info_span!( + target: "zinc_protocol::production_sha", + "multipoint_trace_mles", + side = "prove", + phase = "multipoint_trace_mles", + sources = layout.sources.len(), ) - .map_err(ProductionShaError::from) + .in_scope(|| sha_multipoint_trace_mles(folded_trace, folded_public, &layout, field_cfg))?; + let up_evals = tracing::info_span!( + target: "zinc_protocol::production_sha", + "multipoint_up_evals", + side = "prove", + phase = "multipoint_up_evals", + sources = layout.sources.len(), + ) + .in_scope(|| { + sha_multipoint_up_evals_with_row_weights( + endpoint_evals, + folded_public, + row_weights, + &layout, + field_cfg, + ) + })?; + let (shift_specs, down_evals) = tracing::info_span!( + target: "zinc_protocol::production_sha", + "multipoint_down_evals", + side = "prove", + phase = "multipoint_down_evals", + shifts = layout.shifts.len(), + ) + .in_scope(|| { + sha_multipoint_shift_specs_and_down_evals_with_row_weights( + endpoint_evals, + folded_public, + row_weights, + &layout, + field_cfg, + ) + })?; + tracing::info_span!( + target: "zinc_protocol::production_sha", + "multipoint_sumcheck", + side = "prove", + phase = "multipoint_sumcheck", + sources = trace_mles.len(), + shifts = shift_specs.len(), + ) + .in_scope(|| { + prove_multipoint_reduction( + transcript, + &trace_mles, + r_star, + &up_evals, + &down_evals, + &shift_specs, + field_cfg, + ) + .map_err(ProductionShaError::from) + }) } pub fn verify_sha_endpoint_multipoint( diff --git a/zip-plus/src/pcs/generic.rs b/zip-plus/src/pcs/generic.rs index ae27a3ec..f677b795 100644 --- a/zip-plus/src/pcs/generic.rs +++ b/zip-plus/src/pcs/generic.rs @@ -93,6 +93,18 @@ where field_cfg: &F::Config, ) -> Result; + fn fold_commitment_refs( + commitments: &[&Self::Commitment], + theta: &[F], + field_cfg: &F::Config, + ) -> Result { + let owned = commitments + .iter() + .map(|commitment| (*commitment).clone()) + .collect::>(); + Self::fold_commitments(&owned, theta, field_cfg) + } + fn fold_prover_data( prover_data: &[Self::ProverData], theta: &[F], diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index e3cb7cbd..2a91f521 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -8,7 +8,7 @@ use std::{ }; use ark_ec::{AffineRepr, CurveGroup, VariableBaseMSM}; -use ark_ff::{BigInteger, PrimeField as ArkPrimeField, UniformRand, Zero}; +use ark_ff::{AdditiveGroup, BigInteger, PrimeField as ArkPrimeField, UniformRand, Zero}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress}; use crypto_bigint::{BoxedUint, modular::BoxedMontyForm}; use crypto_primitives::{ @@ -23,7 +23,7 @@ use zinc_poly::{ }, }; use zinc_transcript::traits::{ConstTranscribable, GenTranscribable, Transcribable, Transcript}; -use zinc_utils::{cfg_into_iter, cfg_iter}; +use zinc_utils::{cfg_into_iter, cfg_iter, delayed_reduction::DelayedFieldProductSum}; #[cfg(feature = "parallel")] use rayon::prelude::*; @@ -152,6 +152,89 @@ where Ok(()) } + + /// Open a folded single-row Hyrax commitment from protocol-field lanes. + /// + /// ProjectionFold folds binary witnesses by protocol-field challenges, so + /// folded bit lanes are already field elements rather than booleans. The + /// generic scalar-lane path first converts every lane entry into the curve + /// scalar field and then scans the matrix twice. For the SHA benchmark the + /// Hyrax width is the whole row domain (`num_rows == 1`), so we can compute + /// the transcript's combined row directly in the protocol field, derive the + /// single `b` value from it, and convert only the final row entries that are + /// written to the proof stream. + #[allow(clippy::arithmetic_side_effects)] + pub fn prove_open_field_lanes_single_row( + transcript: &mut PcsProverTranscript, + ck: &HyraxCommitmentKey, + field_lanes: &[Vec<&[F]>], + point: &[F], + prover_data: &HyraxProverData, + field_cfg: &F::Config, + ) -> Result<(), ZipError> + where + F: HyraxFieldBridge + DelayedFieldProductSum, + F::Inner: Transcribable, + F::Modulus: Transcribable, + Lanes: Clone + Debug + Send + Sync, + { + let _ = CHECK_FOR_OVERFLOW; + if field_lanes.is_empty() { + return Ok(()); + } + validate_field_lanes::(ck, field_lanes, point.len(), prover_data)?; + if prover_data.num_rows != 1 { + return Err(ZipError::InvalidPcsParam( + "Hyrax field-lane fast opening requires a single row".to_string(), + )); + } + + let q1 = eq_tensor_f::(point, field_cfg); + let alphas = sample_scalars::( + &mut transcript.fs_transcript, + field_lanes.len() * prover_data.num_lanes, + ); + let alpha_fields = alphas + .iter() + .map(|alpha| F::scalar_to_field(alpha, field_cfg)) + .collect::, _>>()?; + + let mut combined_row = vec![F::zero_with_cfg(field_cfg); ck.num_cols]; + let mut rho_star = C::ScalarField::zero(); + for (poly_idx, lanes) in field_lanes.iter().enumerate() { + for (lane, values) in lanes.iter().enumerate() { + let alpha_idx = alpha_index_dynamic(prover_data.num_lanes, poly_idx, lane); + let alpha = &alpha_fields[alpha_idx]; + for (acc, value) in combined_row.iter_mut().zip(values.iter()) { + *acc += value.clone() * alpha.clone(); + } + if ck.blinding_mode.is_blinded() { + let blind_idx = commitment_index_dynamic( + prover_data.num_lanes, + poly_idx, + lane, + 0, + prover_data.num_rows, + ); + rho_star += alphas[alpha_idx] * prover_data.blinds[blind_idx]; + } + } + } + + let b = F::delayed_sum_of_products(&combined_row, &q1, F::zero_with_cfg(field_cfg)); + transcript.write_field_elements(&[b])?; + + let combined_scalars = combined_row + .iter() + .map(F::field_to_scalar) + .collect::, _>>()?; + write_scalars::(transcript, &combined_scalars)?; + if ck.blinding_mode.is_blinded() { + write_scalar::(transcript, &rho_star)?; + } + + Ok(()) + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -201,6 +284,8 @@ pub struct HyraxCommitment { pub(crate) num_rows: usize, pub(crate) blinding_mode: HyraxBlindingMode, pub(crate) comm: Vec, + pub(crate) comm_affine: Vec, + pub(crate) comm_bytes: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -676,6 +761,8 @@ where num_rows: 0, blinding_mode: ck.blinding_mode, comm: Vec::new(), + comm_affine: Vec::new(), + comm_bytes: Vec::new(), }, )); } @@ -702,6 +789,9 @@ where all_blinds.extend(blinds); } + let all_affine = C::Group::normalize_batch(&all_comm); + let all_bytes = affine_points_bytes::(&all_affine)?; + Ok(( HyraxProverData { batch_size: polys.len(), @@ -716,6 +806,8 @@ where num_rows, blinding_mode: ck.blinding_mode, comm: all_comm, + comm_affine: all_affine, + comm_bytes: all_bytes, }, )) } @@ -726,10 +818,7 @@ where transcript.absorb_slice(&(commitment.num_lanes as u64).to_le_bytes()); transcript.absorb_slice(&(commitment.num_rows as u64).to_le_bytes()); transcript.absorb_slice(&[commitment.blinding_mode.as_u8()]); - for comm in &commitment.comm { - let bytes = group_bytes::(comm).unwrap_or_default(); - transcript.absorb_slice(&bytes); - } + transcript.absorb_slice(&commitment.comm_bytes); transcript.absorb_slice(b"hyrax_commitment_end"); } @@ -743,10 +832,7 @@ where buf.extend_from_slice(&(commitment.num_lanes as u64).to_le_bytes()); buf.extend_from_slice(&(commitment.num_rows as u64).to_le_bytes()); buf.push(commitment.blinding_mode.as_u8()); - for comm in &commitment.comm { - let bytes = group_bytes::(comm).expect("Hyrax commitment must serialize"); - buf.extend_from_slice(&bytes); - } + buf.extend_from_slice(&commitment.comm_bytes); } fn batch_size(commitment: &Self::Commitment) -> usize { @@ -1071,9 +1157,8 @@ where )); } - let comm_bases = C::Group::normalize_batch(&commitment.comm); let comm_lc = if commitment.num_rows == 1 { - msm_unchecked::(&comm_bases, &alphas)? + msm_unchecked::(&commitment.comm_affine, &alphas)? } else { let mut comm_lc_scalars = Vec::with_capacity(commitment.comm.len()); for poly_idx in 0..commitment.batch_size { @@ -1082,7 +1167,7 @@ where comm_lc_scalars.extend(row_coeffs.iter().map(|row_coeff| alpha * row_coeff)); } } - msm_unchecked::(&comm_bases, &comm_lc_scalars)? + msm_unchecked::(&commitment.comm_affine, &comm_lc_scalars)? }; let mut expected = msm_unchecked::(&vk.bases[..combined_row.len()], &combined_row)?; @@ -1111,17 +1196,26 @@ where commitments: &[Self::Commitment], theta: &[F], field_cfg: &F::Config, + ) -> Result { + let refs = commitments.iter().collect::>(); + Self::fold_commitment_refs(&refs, theta, field_cfg) + } + + fn fold_commitment_refs( + commitments: &[&Self::Commitment], + theta: &[F], + field_cfg: &F::Config, ) -> Result { let _ = field_cfg; validate_fold_inputs(commitments, theta.len(), "commitments")?; - let first = &commitments[0]; + let first = commitments[0]; validate_commitment_shape::(first)?; let scalars = theta .iter() .map(F::field_to_scalar) .collect::, _>>()?; - for commitment in commitments { + for &commitment in commitments { validate_commitment_shape::(commitment)?; if !same_commitment_shape(first, commitment) { return Err(ZipError::InvalidPcsParam( @@ -1129,15 +1223,9 @@ where )); } } - let folded = cfg_into_iter!(0..first.comm.len()) - .map(|idx| { - let mut acc = C::Group::zero(); - for (commitment, scalar) in commitments.iter().zip(&scalars) { - acc += commitment.comm[idx] * scalar; - } - acc - }) - .collect(); + let folded = msm_shared_weight_commitments_unchecked::(&scalars, commitments)?; + let folded_affine = C::Group::normalize_batch(&folded); + let folded_bytes = affine_points_bytes::(&folded_affine)?; Ok(HyraxCommitment { batch_size: first.batch_size, @@ -1145,6 +1233,8 @@ where num_rows: first.num_rows, blinding_mode: first.blinding_mode, comm: folded, + comm_affine: folded_affine, + comm_bytes: folded_bytes, }) } @@ -1255,6 +1345,60 @@ where Ok(()) } +fn validate_field_lanes<'a, C, F>( + ck: &HyraxCommitmentKey, + field_lanes: &[Vec<&'a [F]>], + point_len: usize, + prover_data: &HyraxProverData, +) -> Result<(), ZipError> +where + C: AffineRepr, + F: PrimeField + 'a, +{ + let expected_n = 1usize + .checked_shl(u32::try_from(point_len).map_err(|_| { + ZipError::InvalidPcsParam(format!("Hyrax point length {point_len} is too large")) + })?) + .ok_or_else(|| { + ZipError::InvalidPcsParam(format!("Hyrax point length {point_len} is too large")) + })?; + let expected_rows = num_rows(expected_n, ck.num_cols)?; + if prover_data.batch_size != field_lanes.len() + || prover_data.num_rows != expected_rows + || prover_data.blinding_mode != ck.blinding_mode + { + return Err(ZipError::InvalidPcsParam( + "Hyrax field-lane prover data shape mismatch".to_string(), + )); + } + let expected_blinds = if ck.blinding_mode.is_blinded() { + prover_data.batch_size * prover_data.num_lanes * prover_data.num_rows + } else { + 0 + }; + if prover_data.blinds.len() != expected_blinds { + return Err(ZipError::InvalidPcsParam( + "Hyrax field-lane blind count mismatch".to_string(), + )); + } + for lanes in field_lanes { + if lanes.len() != prover_data.num_lanes { + return Err(ZipError::InvalidPcsParam( + "Hyrax field-lane count mismatch".to_string(), + )); + } + for values in lanes { + if values.len() != expected_n { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax field-lane length mismatch: got {}, expected {expected_n}", + values.len() + ))); + } + } + } + Ok(()) +} + fn same_commitment_shape( lhs: &HyraxCommitment, rhs: &HyraxCommitment, @@ -1264,6 +1408,8 @@ fn same_commitment_shape( && lhs.num_rows == rhs.num_rows && lhs.blinding_mode == rhs.blinding_mode && lhs.comm.len() == rhs.comm.len() + && lhs.comm_affine.len() == rhs.comm_affine.len() + && lhs.comm_bytes.len() == rhs.comm_bytes.len() } fn same_prover_data_shape( @@ -1376,6 +1522,19 @@ where commitment.comm.len() ))); } + if commitment.comm_affine.len() != expected { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax commitment expected {expected} affine row commitments, got {}", + commitment.comm_affine.len() + ))); + } + let expected_bytes = expected * C::zero().serialized_size(Compress::Yes); + if commitment.comm_bytes.len() != expected_bytes { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax commitment expected {expected_bytes} commitment bytes, got {}", + commitment.comm_bytes.len() + ))); + } Ok(()) } @@ -1624,6 +1783,194 @@ fn msm_unchecked( )) } +fn msm_shared_weight_commitments_unchecked( + scalars: &[C::ScalarField], + commitments: &[&HyraxCommitment], +) -> Result, ZipError> { + if commitments.is_empty() { + return Ok(Vec::new()); + } + let row_count = commitments[0].comm_affine.len(); + debug_assert!( + commitments + .iter() + .all(|commitment| commitment.comm_affine.len() == row_count) + ); + + msm_shared_weights_indexed_unchecked::(scalars, row_count, |row_idx, scalar_idx| { + commitments[scalar_idx].comm_affine[row_idx] + }) +} + +fn msm_shared_weights_indexed_unchecked( + scalars: &[C::ScalarField], + row_count: usize, + base_at: BaseAt, +) -> Result, ZipError> +where + C: AffineRepr, + BaseAt: Fn(usize, usize) -> C + Sync, +{ + if row_count == 0 { + return Ok(Vec::new()); + } + + let one = C::ScalarField::from(1u64); + let mut unit_indices = Vec::new(); + let mut general_indices = Vec::new(); + let mut general_scalars = Vec::new(); + for (idx, scalar) in scalars.iter().enumerate() { + if scalar.is_zero() { + continue; + } + if *scalar == one { + unit_indices.push(idx); + } else { + general_indices.push(idx); + general_scalars.push(scalar.into_bigint()); + } + } + + if general_indices.is_empty() { + return Ok(cfg_into_iter!(0..row_count) + .map(|row_idx| { + let mut acc = C::Group::zero(); + for &idx in &unit_indices { + acc += base_at(row_idx, idx); + } + acc + }) + .collect()); + } + + let window_bits = shared_weight_window_bits(scalars.len()); + let half_window = 1usize << (window_bits - 1); + let full_window = 1usize << window_bits; + let bucket_len = half_window; + let segments = + ::div_ceil(&(C::ScalarField::MODULUS_BIT_SIZE as usize), &window_bits) + + 1; + let mut carries = vec![0u8; general_scalars.len()]; + let mut signed_windows = Vec::with_capacity(segments); + for segment in 0..segments { + let offset = segment * window_bits; + let mut digits = Vec::with_capacity(general_scalars.len()); + for (idx, scalar) in general_scalars.iter().enumerate() { + let raw = window_value_from_limbs(scalar.as_ref(), offset, window_bits) + + usize::from(carries[idx]); + if raw >= half_window { + digits.push(-((full_window - raw) as i16)); + carries[idx] = 1; + } else { + digits.push(raw as i16); + carries[idx] = 0; + } + } + signed_windows.push(digits); + } + + if bucket_len == 4 { + return Ok(cfg_into_iter!(0..row_count) + .map(|row_idx| { + let mut unit_sum = C::Group::zero(); + for &idx in &unit_indices { + unit_sum += base_at(row_idx, idx); + } + + let mut buckets: [C::Group; 4] = std::array::from_fn(|_| C::Group::zero()); + let mut acc = C::Group::zero(); + for digits in signed_windows.iter().rev() { + for _ in 0..window_bits { + acc.double_in_place(); + } + for bucket in &mut buckets { + *bucket = C::Group::zero(); + } + for (general_idx, digit) in digits.iter().enumerate() { + if *digit > 0 { + buckets[*digit as usize - 1] += + base_at(row_idx, general_indices[general_idx]); + } else if *digit < 0 { + buckets[(-*digit) as usize - 1] -= + base_at(row_idx, general_indices[general_idx]); + } + } + acc += bucket_running_sum(&buckets); + } + + unit_sum + acc + }) + .collect()); + } + + Ok(cfg_into_iter!(0..row_count) + .map(|row_idx| { + let mut unit_sum = C::Group::zero(); + for &idx in &unit_indices { + unit_sum += base_at(row_idx, idx); + } + + let mut buckets = vec![C::Group::zero(); bucket_len]; + let mut acc = C::Group::zero(); + for digits in signed_windows.iter().rev() { + for _ in 0..window_bits { + acc.double_in_place(); + } + for bucket in &mut buckets { + *bucket = C::Group::zero(); + } + for (general_idx, digit) in digits.iter().enumerate() { + if *digit > 0 { + buckets[*digit as usize - 1] += + base_at(row_idx, general_indices[general_idx]); + } else if *digit < 0 { + buckets[(-*digit) as usize - 1] -= + base_at(row_idx, general_indices[general_idx]); + } + } + acc += bucket_running_sum(&buckets); + } + + unit_sum + acc + }) + .collect()) +} + +fn shared_weight_window_bits(n: usize) -> usize { + if n < 32 { + 3 + } else { + (usize::BITS - n.leading_zeros()) as usize + } +} + +fn bucket_running_sum(buckets: &[G]) -> G { + let mut acc = G::zero(); + let mut running_sum = G::zero(); + for bucket in buckets.iter().rev() { + running_sum += bucket; + acc += running_sum; + } + acc +} + +fn window_value_from_limbs(limbs: &[u64], start: usize, width: usize) -> usize { + (0..width).fold(0usize, |value, bit_idx| { + let absolute_bit = start + bit_idx; + let limb_idx = absolute_bit / u64::BITS as usize; + let limb_bit = absolute_bit % u64::BITS as usize; + if limbs + .get(limb_idx) + .map(|limb| ((limb >> limb_bit) & 1) == 1) + .unwrap_or(false) + { + value | (1usize << bit_idx) + } else { + value + } + }) +} + fn num_rows(n: usize, width: usize) -> Result { if width == 0 { return Err(ZipError::InvalidPcsParam( @@ -1746,10 +2093,16 @@ fn scalar_bytes(scalar: &C::ScalarField) -> Result, ZipEr Ok(bytes) } -fn group_bytes(group: &C::Group) -> Result, ZipError> { - let affine = group.into_affine(); - let mut bytes = Vec::with_capacity(affine.serialized_size(Compress::Yes)); - affine.serialize_compressed(&mut bytes).map_err(ark_err)?; +fn affine_bytes_into(affine: &C, bytes: &mut Vec) -> Result<(), ZipError> { + affine.serialize_compressed(bytes).map_err(ark_err) +} + +fn affine_points_bytes(points: &[C]) -> Result, ZipError> { + let point_size = C::zero().serialized_size(Compress::Yes); + let mut bytes = Vec::with_capacity(points.len() * point_size); + for point in points { + affine_bytes_into::(point, &mut bytes)?; + } Ok(bytes) } diff --git a/zip-plus/src/pcs/msm_commitment.rs b/zip-plus/src/pcs/msm_commitment.rs index 569b63d7..1524ae28 100644 --- a/zip-plus/src/pcs/msm_commitment.rs +++ b/zip-plus/src/pcs/msm_commitment.rs @@ -661,11 +661,7 @@ fn window_value_from_limbs(limbs: &[u64], start: usize, width: usize) -> usize { }) } -fn window_value_from_words( - words: &[crypto_bigint::Word], - start: usize, - width: usize, -) -> usize { +fn window_value_from_words(words: &[crypto_bigint::Word], start: usize, width: usize) -> usize { let word_bits = core::mem::size_of::() * 8; (0..width).fold(0usize, |value, bit_idx| { let absolute_bit = start + bit_idx; @@ -915,11 +911,11 @@ mod tests { let scalars = scalars_from_int(&values); let blind = blind(width, n); - let int_comm = - MsmCommitmentEngine::::commit_with::, SignedIntPippengerMsm>( - &ck, &values, &blind, - ) - .expect("signed int commit must succeed"); + let int_comm = MsmCommitmentEngine::::commit_with::< + Int<1>, + SignedIntPippengerMsm, + >(&ck, &values, &blind) + .expect("signed int commit must succeed"); let scalar_comm = MsmCommitmentEngine::::commit(&ck, &scalars, &blind) .expect("scalar commit must succeed"); From 53433f693aa09014fb40be905f8e5191f4bf29ae Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 07:24:51 -0700 Subject: [PATCH 36/49] Defer ProjectionFold folded commitments --- protocol/src/production_sha.rs | 211 ++++++++++++++++++++++++--------- 1 file changed, 153 insertions(+), 58 deletions(-) diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 82e611e1..c9af4087 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -378,7 +378,6 @@ type ProductionShaFoldAfterSumfold = ( ProjectedPublic, F, InstanceFoldClaim, - PCSCommitments, PCSProverData, ); @@ -420,7 +419,8 @@ where #[allow(clippy::too_many_arguments)] fn prove_folded_pcs_opening( pcs_params: &PCSParams, - folded_commitments: &PCSCommitments, + instance_commitments: &[PCSCommitments], + fold_weights: &[F], folded_trace: &ProjectedTrace, folded_prover_data: &PCSProverData, r_0: &[F], @@ -465,7 +465,8 @@ where { fn prove_folded_pcs_opening( pcs_params: &PCSParams, - folded_commitments: &PCSCommitments, + instance_commitments: &[PCSCommitments], + fold_weights: &[F], folded_trace: &ProjectedTrace, folded_prover_data: &PCSProverData, r_0: &[F], @@ -474,7 +475,8 @@ where ) -> Result, ProductionShaError> { prove_production_sha_hyrax_pcs_opening::( pcs_params, - folded_commitments, + instance_commitments, + fold_weights, folded_trace, folded_prover_data, r_0, @@ -855,6 +857,32 @@ pub fn absorb_production_sha_commitments( } } +pub fn absorb_derived_production_sha_commitments( + transcript: &mut impl Transcript, + label: &'static [u8], + commitments: &[PCSCommitments], + weights: &[F], + field_cfg: &F::Config, +) where + Zt: ZincTypes, + F: PrimeField, + F::Inner: Transcribable, + F::Modulus: Transcribable, + P: ZincPCSTypes, +{ + let mut field_buf = runtime_field_transcript_buf::(field_cfg); + transcript.absorb_slice(label); + transcript.absorb_slice(b"derived_from_fresh_v1"); + transcript.absorb_slice(&(commitments.len() as u64).to_le_bytes()); + transcript.absorb_slice(&(weights.len() as u64).to_le_bytes()); + transcript.absorb_random_field_slice(weights, &mut field_buf); + absorb_production_sha_commitments::( + transcript, + b"production_sha_derived_folded_commitment_sources", + commitments, + ); +} + pub fn absorb_fresh_sha_ideal_polys( transcript: &mut impl Transcript, ideal_polys: &[[DynamicPolynomialF; NUM_NONZERO_SHA_FAMILIES]], @@ -1118,7 +1146,7 @@ pub fn prove_linear_ideal_fold( ) -> Result< LinearIdealFoldProveOutput< UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, - FoldedLinearIdealInstance, ProjectedPublic>, + FoldedLinearIdealInstance>, FoldedLinearIdealWitness>, ProductionLinearIdealFoldProof, >, @@ -1241,7 +1269,7 @@ where field_cfg, )?; - let (folded, folded_public, row_claim, sumfold_output, folded_commitments, folded_prover_data) = + let (folded, folded_public, row_claim, sumfold_output, folded_prover_data) = prove_fold_after_sumfold_phase::( &traces, &publics, @@ -1253,14 +1281,15 @@ where &lambda_powers, &booleanity_weights, &booleanity_sources, - &instance_commitments, &instance_prover_data, field_cfg, )?; - absorb_production_sha_commitments::( + absorb_derived_production_sha_commitments::( transcript, b"production_sha_derived_folded_commitments", - std::slice::from_ref(&folded_commitments), + &instance_commitments, + sumfold_output.eq_instance_weights(), + field_cfg, ); verify_folded_row_sumcheck_claim(&row_claim, sumfold_output.final_round_sumcheck_claim())?; @@ -1297,7 +1326,8 @@ where let (witness_lifted_evals, opening_proof) = prove_pcs_opening_phase::( transcript, &folded.trace, - &folded_commitments, + &instance_commitments, + sumfold_output.eq_instance_weights(), &folded_prover_data, &r_0, &r_0_eq_weights, @@ -1309,7 +1339,7 @@ where fresh_instances, folded_instance: FoldedLinearIdealInstance { target: sumfold_output.final_round_sumcheck_claim().clone(), - commitments: folded_commitments, + commitments: (), public: folded_public, }, folded_witness: FoldedLinearIdealWitness { @@ -1604,7 +1634,6 @@ fn prove_fold_after_sumfold_phase( lambda_powers: &[F], booleanity_weights: &[F], booleanity_sources: &[ShaBooleanitySource], - instance_commitments: &[PCSCommitments], instance_prover_data: &[PCSProverData], field_cfg: &F::Config, ) -> Result, ProductionShaError> @@ -1657,19 +1686,6 @@ where field_cfg, ) })?; - let folded_commitments = tracing::info_span!( - target: "zinc_protocol::production_sha", - "fold_commitments", - side = "prove", - phase = "fold_commitments", - ) - .in_scope(|| { - fold_pcs_commitments::( - instance_commitments, - sumfold_output.eq_instance_weights(), - field_cfg, - ) - })?; let folded_prover_data = tracing::info_span!( target: "zinc_protocol::production_sha", "fold_prover_data", @@ -1689,7 +1705,6 @@ where folded_public, row_claim, sumfold_output, - folded_commitments, folded_prover_data, )) } @@ -2186,7 +2201,8 @@ where fn prove_pcs_opening_phase( transcript: &mut impl Transcript, folded_trace: &ProjectedTrace, - folded_commitments: &PCSCommitments, + instance_commitments: &[PCSCommitments], + fold_weights: &[F], folded_prover_data: &PCSProverData, r_0: &[F], r_0_eq_weights: &[F], @@ -2229,7 +2245,8 @@ where .in_scope(|| { P::prove_folded_pcs_opening( pcs_params, - folded_commitments, + instance_commitments, + fold_weights, folded_trace, folded_prover_data, r_0, @@ -2477,15 +2494,12 @@ where field_cfg, )?; - let folded_commitments = verify_fold_commitments_phase::( + absorb_derived_production_sha_commitments::( + transcript, + b"production_sha_derived_folded_commitments", &proof.instance_commitments, sumfold_output.eq_instance_weights(), field_cfg, - )?; - absorb_production_sha_commitments::( - transcript, - b"production_sha_derived_folded_commitments", - std::slice::from_ref(&folded_commitments), ); let row_output = verify_row_sumcheck_phase( @@ -2512,9 +2526,16 @@ where field_cfg, )?; + let folded_commitments = verify_fold_commitments_phase::( + &proof.instance_commitments, + sumfold_output.eq_instance_weights(), + field_cfg, + )?; verify_pcs_phase::( transcript, &vs.pcs_params, + &proof.instance_commitments, + sumfold_output.eq_instance_weights(), &folded_commitments, &subclaim.sumcheck_subclaim.point, &proof.witness_lifted_evals, @@ -2786,6 +2807,8 @@ where fn verify_pcs_phase( transcript: &mut impl Transcript, pcs_params: &PCSVerifierParams, + instance_commitments: &[PCSCommitments], + fold_weights: &[F], folded_commitments: &PCSCommitments, point: &[F], witness_lifted_evals: &[DynamicPolynomialF], @@ -2805,6 +2828,8 @@ where absorb_folded_lifted_evals(transcript, witness_lifted_evals, field_cfg); verify_production_sha_pcs_opening::( pcs_params, + instance_commitments, + fold_weights, folded_commitments, point, witness_lifted_evals, @@ -3023,8 +3048,36 @@ where }) } +fn absorb_derived_pcs_commitment( + transcript: &mut impl Transcript, + label: &'static [u8], + commitments: &[&Pcs::Commitment], + weights: &[F], + field_cfg: &F::Config, +) where + Pcs: PCS, + F: PrimeField, + F::Inner: Transcribable, + F::Modulus: Transcribable, + Eval: Clone + std::fmt::Debug + Send + Sync, +{ + let mut field_buf = runtime_field_transcript_buf::(field_cfg); + transcript.absorb_slice(label); + transcript.absorb_slice(b"derived_pcs_commitment_v1"); + transcript.absorb_slice(&(commitments.len() as u64).to_le_bytes()); + transcript.absorb_slice(&(weights.len() as u64).to_le_bytes()); + transcript.absorb_random_field_slice(weights, &mut field_buf); + for (idx, commitment) in commitments.iter().enumerate() { + transcript.absorb_slice(&(idx as u64).to_le_bytes()); + Pcs::absorb_commitment(transcript, commitment); + } + transcript.absorb_slice(b"derived_pcs_commitment_end"); +} + fn verify_production_sha_pcs_opening( pcs_params: &PCSVerifierParams, + instance_commitments: &[PCSCommitments], + fold_weights: &[F], folded_commitments: &PCSCommitments, r_0: &[F], folded_lifted_evals: &[DynamicPolynomialF], @@ -3053,7 +3106,17 @@ where }; let mut transcription_buf = vec![0u8; F::zero_with_cfg(field_cfg).inner().get_num_bytes()]; - P::BinaryPCS::absorb_commitment(&mut transcript.fs_transcript, &folded_commitments.binary); + let binary_commitments = instance_commitments + .iter() + .map(|commitment| &commitment.binary) + .collect::>(); + absorb_derived_pcs_commitment::, D>( + &mut transcript.fs_transcript, + b"production_sha_pcs_binary", + &binary_commitments, + fold_weights, + field_cfg, + ); absorb_pcs_lifted_evals( &mut transcript.fs_transcript, binary_lifted, @@ -3069,9 +3132,16 @@ where field_cfg, )?; - P::ArbitraryPCS::absorb_commitment( + let arbitrary_commitments = instance_commitments + .iter() + .map(|commitment| &commitment.arbitrary) + .collect::>(); + absorb_derived_pcs_commitment::, D>( &mut transcript.fs_transcript, - &folded_commitments.arbitrary, + b"production_sha_pcs_arbitrary", + &arbitrary_commitments, + fold_weights, + field_cfg, ); absorb_pcs_lifted_evals( &mut transcript.fs_transcript, @@ -3088,7 +3158,17 @@ where field_cfg, )?; - P::IntPCS::absorb_commitment(&mut transcript.fs_transcript, &folded_commitments.int); + let int_commitments = instance_commitments + .iter() + .map(|commitment| &commitment.int) + .collect::>(); + absorb_derived_pcs_commitment::( + &mut transcript.fs_transcript, + b"production_sha_pcs_int", + &int_commitments, + fold_weights, + field_cfg, + ); absorb_pcs_lifted_evals( &mut transcript.fs_transcript, int_lifted, @@ -3110,7 +3190,8 @@ where #[allow(clippy::too_many_arguments)] fn prove_production_sha_hyrax_pcs_opening( pcs_params: &PCSParams, Zt, F, D>, - folded_commitments: &PCSCommitments, Zt, F, D>, + instance_commitments: &[PCSCommitments, Zt, F, D>], + fold_weights: &[F], folded_trace: &ProjectedTrace, folded_prover_data: &PCSProverData, Zt, F, D>, r_0: &[F], @@ -3149,11 +3230,7 @@ where >, { ensure_production_sha_word_degree::()?; - validate_production_sha_batch_sizes::( - HyraxPCS::::batch_size(&folded_commitments.binary), - HyraxPCS::::batch_size(&folded_commitments.arbitrary), - HyraxPCS::::batch_size(&folded_commitments.int), - )?; + validate_production_sha_batch_sizes::(ShaWordCol::COUNT, 0, ShaIntCol::COUNT)?; let (binary_lifted, int_lifted) = split_folded_sha_pcs_lifted_evals(folded_lifted_evals)?; let arbitrary_lifted: &[DynamicPolynomialF] = &[]; @@ -3167,9 +3244,16 @@ where }; let mut transcription_buf = vec![0u8; F::zero_with_cfg(field_cfg).inner().get_num_bytes()]; - HyraxPCS::::absorb_commitment( + let binary_commitments = instance_commitments + .iter() + .map(|commitment| &commitment.binary) + .collect::>(); + absorb_derived_pcs_commitment::, F, BinaryPoly, D>( &mut transcript.fs_transcript, - &folded_commitments.binary, + b"production_sha_pcs_binary", + &binary_commitments, + fold_weights, + field_cfg, ); absorb_pcs_lifted_evals( &mut transcript.fs_transcript, @@ -3188,9 +3272,21 @@ where let binary_end = transcript.stream.position() as usize; let binary = transcript.stream.get_ref()[binary_start..binary_end].to_vec(); - HyraxPCS::::absorb_commitment( + let arbitrary_commitments = instance_commitments + .iter() + .map(|commitment| &commitment.arbitrary) + .collect::>(); + absorb_derived_pcs_commitment::< + HyraxPCS, + F, + DensePolynomial, + D, + >( &mut transcript.fs_transcript, - &folded_commitments.arbitrary, + b"production_sha_pcs_arbitrary", + &arbitrary_commitments, + fold_weights, + field_cfg, ); absorb_pcs_lifted_evals( &mut transcript.fs_transcript, @@ -3209,9 +3305,16 @@ where let arbitrary_end = transcript.stream.position() as usize; let arbitrary = transcript.stream.get_ref()[arbitrary_start..arbitrary_end].to_vec(); - HyraxPCS::::absorb_commitment( + let int_commitments = instance_commitments + .iter() + .map(|commitment| &commitment.int) + .collect::>(); + absorb_derived_pcs_commitment::, F, Zt::Int, D>( &mut transcript.fs_transcript, - &folded_commitments.int, + b"production_sha_pcs_int", + &int_commitments, + fold_weights, + field_cfg, ); absorb_pcs_lifted_evals( &mut transcript.fs_transcript, @@ -7838,14 +7941,6 @@ mod tests { assert_eq!(verified.target, output.folded_instance.target); assert_eq!(verified.public, output.folded_instance.public); - assert!(pcs_commitments_match::< - P, - TestShaZincTypes, - F, - TEST_DEGREE_PLUS_ONE, - >( - &verified.commitments, &output.folded_instance.commitments - )); } #[test] From 95cd3c9cb2727bd56d7e65cf824e7fa736ae4052 Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 07:54:59 -0700 Subject: [PATCH 37/49] Derive ProjectionFold row claim from SumFold terminal --- piop/src/sumcheck/multi_degree.rs | 77 +++++++++++++- protocol/src/production_sha.rs | 169 +++++++++++++++--------------- 2 files changed, 160 insertions(+), 86 deletions(-) diff --git a/piop/src/sumcheck/multi_degree.rs b/piop/src/sumcheck/multi_degree.rs index aaceed0f..2c190c64 100644 --- a/piop/src/sumcheck/multi_degree.rs +++ b/piop/src/sumcheck/multi_degree.rs @@ -16,7 +16,10 @@ use crypto_primitives::{FromPrimitiveWithConfig, PrimeField}; use num_traits::Zero; use std::marker::PhantomData; -use zinc_poly::mle::DenseMultilinearExtension; +use zinc_poly::{ + EvaluatablePolynomial, mle::DenseMultilinearExtension, + univariate::nat_evaluation::NatEvaluatedPoly, +}; use zinc_transcript::traits::{ConstTranscribable, GenTranscribable, Transcribable, Transcript}; use zinc_utils::{add, inner_transparent_field::InnerTransparentField, mul}; @@ -410,6 +413,30 @@ impl MultiDegreeSumcheck { num_vars: usize, config: &F::Config, ) -> (MultiDegreeSumcheckProof, Vec>) + where + F: InnerTransparentField + Send + Sync, + F::Inner: Transcribable + Zero, + F::Modulus: Transcribable, + { + let (proof, states, _) = Self::prove_as_subprotocol_with_expected_evaluations( + transcript, groups, num_vars, config, + ); + (proof, states) + } + + /// Prove a multi-degree sumcheck and also return each group's expected + /// terminal evaluation at the sampled point. + #[allow(clippy::type_complexity)] + pub fn prove_as_subprotocol_with_expected_evaluations( + transcript: &mut impl Transcript, + groups: Vec>, + num_vars: usize, + config: &F::Config, + ) -> ( + MultiDegreeSumcheckProof, + Vec>, + Vec, + ) where F: InnerTransparentField + Send + Sync, F::Inner: Transcribable + Zero, @@ -532,7 +559,18 @@ impl MultiDegreeSumcheck { } }); - let degrees = prover_states.iter().map(|s| s.max_degree).collect(); + let degrees = prover_states + .iter() + .map(|s| s.max_degree) + .collect::>(); + let expected_evaluations = group_messages + .iter() + .zip(claimed_sums.iter()) + .zip(degrees.iter()) + .map(|((messages, claimed_sum), degree)| { + Self::expected_evaluation_from_messages(claimed_sum, messages, *degree, &challenges) + }) + .collect(); ( MultiDegreeSumcheckProof { @@ -541,9 +579,44 @@ impl MultiDegreeSumcheck { degrees, }, prover_states, + expected_evaluations, ) } + fn expected_evaluation_from_messages( + claimed_sum: &F, + messages: &[SumcheckProverMsg], + degree: usize, + challenges: &[F], + ) -> F { + assert_eq!( + messages.len(), + challenges.len(), + "generated sumcheck proof should have one message per challenge" + ); + let mut expected = claimed_sum.clone(); + for (message, challenge) in messages.iter().zip(challenges.iter()) { + let tail = &message.0.tail_evaluations; + assert_eq!( + tail.len(), + degree, + "generated sumcheck message should match group degree" + ); + let constant_term = if degree == 0 { + expected.clone() + } else { + expected.clone() - tail[0].clone() + }; + let mut reconstructed_evaluations = Vec::with_capacity(tail.len() + 1); + reconstructed_evaluations.push(constant_term); + reconstructed_evaluations.extend_from_slice(tail); + expected = NatEvaluatedPoly::new(reconstructed_evaluations) + .evaluate_at_point(challenge) + .expect("generated sumcheck message should interpolate"); + } + expected + } + /// Multi-degree sumcheck verifier. /// /// Runs the verifier side of the sumcheck protocol for G degree groups diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index c9af4087..d371fbca 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -21,6 +21,8 @@ use num_traits::{ConstZero, Zero}; use rayon::prelude::*; use thiserror::Error; #[cfg(debug_assertions)] +use zinc_piop::neutron_nova::expression_folded_row_sum_with_vectors; +#[cfg(debug_assertions)] use zinc_piop::neutron_nova::validate_projected_trace; use zinc_piop::{ combined_poly_resolver::Proof as CombinedPolyResolverProof, @@ -41,8 +43,8 @@ use zinc_piop::{ build_production_sha_sumfold_group_from_prefix_accumulators, build_sha_lambda_powers, build_sha_residual_eval_powers, build_sha_sumfold_linear_accumulator, build_sha_sumfold_quadratic_prefix_accumulator, derive_instance_fold_claim, - expression_folded_row_sum_with_row_weights, expression_folded_row_sum_with_vectors, - fold_projected_traces, folded_row_integrand_sum, production_sha_booleanity_sources, + expression_folded_row_sum_with_row_weights, fold_projected_traces, + folded_row_integrand_sum, production_sha_booleanity_sources, production_sha_nonzero_families, sha_int_at_point_with_weights_unchecked, sha_public_at_point, sha_public_at_point_with_weights, sha_word_bits_at_point_with_weights_unchecked, verify_folded_row_sumcheck_claim, @@ -1253,7 +1255,7 @@ where field_cfg, )?; - let (sumfold_proof, sumfold_r_b) = prove_sumfold_phase( + let (sumfold_proof, sumfold_r_b, sumfold_c_sf) = prove_sumfold_phase( transcript, sumfold_group, &initial_claim, @@ -1261,10 +1263,10 @@ where field_cfg, )?; - let provisional_sumfold_output = derive_instance_fold_claim( + let sumfold_output = derive_instance_fold_claim( &beta, sumfold_r_b.clone(), - F::one_with_cfg(field_cfg), + sumfold_c_sf, witnesses.len(), field_cfg, )?; @@ -1273,9 +1275,7 @@ where prove_fold_after_sumfold_phase::( &traces, &publics, - &provisional_sumfold_output, - &beta, - sumfold_r_b, + sumfold_output, &r_ic_eq_weights, &a_powers, &lambda_powers, @@ -1596,7 +1596,7 @@ fn prove_sumfold_phase( initial_claim: &F, instance_vars: usize, field_cfg: &F::Config, -) -> Result<(MultiDegreeSumcheckProof, Vec), ProductionShaError> +) -> Result<(MultiDegreeSumcheckProof, Vec, F), ProductionShaError> where F: InnerTransparentField + DelayedFieldProductSum @@ -1626,9 +1626,7 @@ where fn prove_fold_after_sumfold_phase( traces: &[ProjectedTrace], publics: &[ProjectedPublic], - provisional_sumfold_output: &InstanceFoldClaim, - beta: &[F], - sumfold_r_b: Vec, + sumfold_output: InstanceFoldClaim, r_ic_eq_weights: &[F], a_powers: &[F], lambda_powers: &[F], @@ -1652,39 +1650,32 @@ where side = "prove", phase = "fold_projected_traces", ) - .in_scope(|| fold_projected_traces(traces, publics, provisional_sumfold_output, field_cfg))?; + .in_scope(|| fold_projected_traces(traces, publics, &sumfold_output, field_cfg))?; let row_claim = tracing::info_span!( target: "zinc_protocol::production_sha", "fold_row_claim", side = "prove", phase = "fold_row_claim", ) - .in_scope(|| { - production_sha_folded_row_sum_fast( - &folded.trace, - &folded_public, - r_ic_eq_weights, - a_powers, - lambda_powers, - booleanity_weights, - booleanity_sources, - field_cfg, - ) - })?; - let sumfold_output = tracing::info_span!( - target: "zinc_protocol::production_sha", - "fold_claim", - side = "prove", - phase = "fold_claim", - ) - .in_scope(|| { - derive_instance_fold_claim_from_row_claim( - beta, - sumfold_r_b, - &row_claim, - traces.len(), - field_cfg, - ) + .in_scope(|| -> Result> { + let row_claim = sumfold_output.final_round_sumcheck_claim().clone(); + #[cfg(debug_assertions)] + { + let recomputed = production_sha_folded_row_sum_fast( + &folded.trace, + &folded_public, + r_ic_eq_weights, + a_powers, + lambda_powers, + booleanity_weights, + booleanity_sources, + field_cfg, + )?; + if recomputed != row_claim { + return Err(ShaProjectionError::FoldedRowClaimMismatch.into()); + } + } + Ok(row_claim) })?; let folded_prover_data = tracing::info_span!( target: "zinc_protocol::production_sha", @@ -1710,6 +1701,7 @@ where } #[allow(clippy::too_many_arguments)] +#[cfg(debug_assertions)] fn production_sha_folded_row_sum_fast( trace: &ProjectedTrace, public: &ProjectedPublic, @@ -1926,6 +1918,7 @@ where folded_row_integrand_sum(&values, field_cfg).map_err(ProductionShaError::from) } +#[cfg(debug_assertions)] fn trace_word_eval_at_row_with_weights( trace: &ProjectedTrace, col: ShaWordCol, @@ -1944,6 +1937,7 @@ where Ok(acc) } +#[cfg(debug_assertions)] fn public_word_or_const_eval_at_row( public: &ProjectedPublic, col: ShaPublicCol, @@ -1967,6 +1961,7 @@ where Ok(acc) } +#[cfg(debug_assertions)] fn booleanity_source_value_at_fast( trace: &ProjectedTrace, row: usize, @@ -4413,35 +4408,38 @@ where prefix_vars, field_cfg, )?; - let (proof, r_b) = prove_optimized_sha_sumfold_with_weights( + let (proof, r_b, c_sf) = prove_optimized_sha_sumfold_with_weights( transcript, group, initial_claim, beta.len(), field_cfg, )?; - let provisional = derive_instance_fold_claim( - beta, - r_b.clone(), - F::one_with_cfg(field_cfg), - traces.len(), - field_cfg, - )?; - let (folded, folded_public) = - zinc_piop::neutron_nova::fold_projected_traces(traces, publics, &provisional, field_cfg)?; - let row_claim = expression_folded_row_sum_with_vectors( - &folded.trace, - &folded_public, - &r_ic_eq_weights, - &a_powers, - &lambda_powers, - &booleanity_weights, - booleanity_sources, - field_cfg, - )?; + #[cfg(debug_assertions)] + { + let provisional = + derive_instance_fold_claim(beta, r_b.clone(), c_sf.clone(), traces.len(), field_cfg)?; + let (folded, folded_public) = zinc_piop::neutron_nova::fold_projected_traces( + traces, + publics, + &provisional, + field_cfg, + )?; + let row_claim = expression_folded_row_sum_with_vectors( + &folded.trace, + &folded_public, + &r_ic_eq_weights, + &a_powers, + &lambda_powers, + &booleanity_weights, + booleanity_sources, + field_cfg, + )?; + verify_folded_row_sumcheck_claim(provisional.final_round_sumcheck_claim(), &row_claim)?; + } Ok(( proof, - derive_instance_fold_claim_from_row_claim(beta, r_b, &row_claim, traces.len(), field_cfg)?, + derive_instance_fold_claim(beta, r_b, c_sf, traces.len(), field_cfg)?, )) } @@ -4451,7 +4449,7 @@ pub fn prove_optimized_sha_sumfold_with_weights( initial_claim: &F, instance_vars: usize, field_cfg: &F::Config, -) -> Result<(MultiDegreeSumcheckProof, Vec), ProductionShaError> +) -> Result<(MultiDegreeSumcheckProof, Vec, F), ProductionShaError> where F: InnerTransparentField + DelayedFieldProductSum @@ -4462,16 +4460,24 @@ where F::Inner: Transcribable + Zero, F::Modulus: Transcribable, { - let (proof, states) = MultiDegreeSumcheck::prove_as_subprotocol( - transcript, - vec![group], - instance_vars, - field_cfg, - ); + let (proof, states, expected_evaluations) = + MultiDegreeSumcheck::prove_as_subprotocol_with_expected_evaluations( + transcript, + vec![group], + instance_vars, + field_cfg, + ); require_single_sumcheck_group(&proof, "SHA SumFold")?; if proof.claimed_sums()[0] != *initial_claim { return Err(ProductionShaError::SumFoldTerminalMismatch); } + if expected_evaluations.len() != 1 { + return Err(ProductionShaError::LengthMismatch { + label: "sumfold expected evaluations", + got: expected_evaluations.len(), + expected: 1, + }); + } Ok(( proof, @@ -4484,6 +4490,7 @@ where })? .randomness .clone(), + expected_evaluations[0].clone(), )) } @@ -4734,6 +4741,7 @@ impl RowExpressionLayout { } } +#[cfg(debug_assertions)] fn trace_word_bit_at_row( trace: &ProjectedTrace, col: ShaWordCol, @@ -4770,6 +4778,7 @@ where }) } +#[cfg(debug_assertions)] fn trace_int_at_row( trace: &ProjectedTrace, col: ShaIntCol, @@ -4794,6 +4803,7 @@ where }) } +#[cfg(debug_assertions)] fn public_scalar_at_row( public: &ProjectedPublic, col: ShaPublicCol, @@ -4822,6 +4832,7 @@ where }) } +#[cfg(debug_assertions)] fn public_word_col_index(col: ShaPublicCol) -> Option { match col { ShaPublicCol::PAIn => Some(0), @@ -4833,6 +4844,7 @@ fn public_word_col_index(col: ShaPublicCol) -> Option { } } +#[cfg(debug_assertions)] fn public_word_bit_at_row( public: &ProjectedPublic, col: ShaPublicCol, @@ -8051,7 +8063,7 @@ mod tests { .unwrap(); let mut sumfold_prover_transcript = Blake3Transcript::new(); sumfold_prover_transcript.absorb_slice(b"sha-sumfold-row-bridge"); - let (sumfold_proof, r_b) = prove_optimized_sha_sumfold_with_weights( + let (sumfold_proof, r_b, c_sf) = prove_optimized_sha_sumfold_with_weights( &mut sumfold_prover_transcript, group, &initial_claim, @@ -8072,14 +8084,9 @@ mod tests { .unwrap(); assert_eq!(verified_sumfold.r_b, r_b); - let provisional = derive_instance_fold_claim( - &beta, - r_b.clone(), - F::one_with_cfg(&field_cfg), - traces.len(), - &field_cfg, - ) - .unwrap(); + let provisional = + derive_instance_fold_claim(&beta, r_b.clone(), c_sf.clone(), traces.len(), &field_cfg) + .unwrap(); let (folded, folded_public) = fold_projected_traces(&traces, &publics, &provisional, &field_cfg).unwrap(); let row_claim = expression_folded_row_sum_with_vectors( @@ -8093,14 +8100,8 @@ mod tests { &field_cfg, ) .unwrap(); - let sumfold_output = derive_instance_fold_claim_from_row_claim( - &beta, - r_b, - &row_claim, - traces.len(), - &field_cfg, - ) - .unwrap(); + let sumfold_output = + derive_instance_fold_claim(&beta, r_b, c_sf, traces.len(), &field_cfg).unwrap(); assert_eq!(verified_sumfold.c_sf, *sumfold_output.c_sf()); assert_eq!(sumfold_output.final_round_sumcheck_claim(), &row_claim); From c4adb7c05d14716038bd7ac0f0e7aaf6af6ae6ea Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 08:13:04 -0700 Subject: [PATCH 38/49] Clean ProjectionFold row claim release build --- protocol/src/production_sha.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index d371fbca..09746b76 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -1627,11 +1627,11 @@ fn prove_fold_after_sumfold_phase( traces: &[ProjectedTrace], publics: &[ProjectedPublic], sumfold_output: InstanceFoldClaim, - r_ic_eq_weights: &[F], - a_powers: &[F], - lambda_powers: &[F], - booleanity_weights: &[F], - booleanity_sources: &[ShaBooleanitySource], + _r_ic_eq_weights: &[F], + _a_powers: &[F], + _lambda_powers: &[F], + _booleanity_weights: &[F], + _booleanity_sources: &[ShaBooleanitySource], instance_prover_data: &[PCSProverData], field_cfg: &F::Config, ) -> Result, ProductionShaError> @@ -1664,11 +1664,11 @@ where let recomputed = production_sha_folded_row_sum_fast( &folded.trace, &folded_public, - r_ic_eq_weights, - a_powers, - lambda_powers, - booleanity_weights, - booleanity_sources, + _r_ic_eq_weights, + _a_powers, + _lambda_powers, + _booleanity_weights, + _booleanity_sources, field_cfg, )?; if recomputed != row_claim { @@ -4357,7 +4357,7 @@ where pub fn prove_optimized_sha_sumfold( transcript: &mut impl Transcript, traces: &[ProjectedTrace], - publics: &[ProjectedPublic], + _publics: &[ProjectedPublic], initial_claim: &F, beta: &[F], r_ic: &[F; SHA_ROW_VARS], @@ -4421,7 +4421,7 @@ where derive_instance_fold_claim(beta, r_b.clone(), c_sf.clone(), traces.len(), field_cfg)?; let (folded, folded_public) = zinc_piop::neutron_nova::fold_projected_traces( traces, - publics, + _publics, &provisional, field_cfg, )?; @@ -4832,7 +4832,6 @@ where }) } -#[cfg(debug_assertions)] fn public_word_col_index(col: ShaPublicCol) -> Option { match col { ShaPublicCol::PAIn => Some(0), From 33c9bf26ba7b30df4a74420aee141c2dcbfc2754 Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 08:32:21 -0700 Subject: [PATCH 39/49] Avoid ProjectionFold polynomial scaling temporaries --- piop/src/neutron_nova/projection_sha.rs | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 1884d1af..44d92aae 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -409,8 +409,7 @@ where kind: "linear_residual_coeffs", col: family.index(), })?; - let weighted = scale_poly(residual, weight); - aggregate[slot] += &weighted; + add_scaled_poly_assign(&mut aggregate[slot], residual, weight); } } aggregate.iter_mut().for_each(DynamicPolynomialF::trim); @@ -750,8 +749,7 @@ where for (row, row_weight) in row_weights.iter().enumerate().take(SHA_ROW_COUNT) { let residuals = residual_polys_at_row(trace, public, row, field_cfg)?; for (slot, family) in NONZERO_SHA_FAMILIES.iter().enumerate() { - let weighted = scale_poly(&residuals[family.index()], row_weight); - out[slot] += &weighted; + add_scaled_poly_assign(&mut out[slot], &residuals[family.index()], row_weight); } } out.iter_mut().for_each(DynamicPolynomialF::trim); @@ -901,8 +899,7 @@ where trace, public, row, &rho_sig0, &rho_sig1, field_cfg, )?; for (family_idx, residual) in residuals.iter().enumerate() { - let weighted = scale_poly(residual, row_weight); - coeffs[family_idx] += &weighted; + add_scaled_poly_assign(&mut coeffs[family_idx], residual, row_weight); } } coeffs.iter_mut().for_each(DynamicPolynomialF::trim); @@ -4443,6 +4440,23 @@ fn scale_poly(poly: &DynamicPolynomialF, scalar: &F) -> Dynami ) } +fn add_scaled_poly_assign( + acc: &mut DynamicPolynomialF, + poly: &DynamicPolynomialF, + scalar: &F, +) { + if poly.is_zero() || F::is_zero(scalar) { + return; + } + if acc.coeffs.len() < poly.coeffs.len() { + acc.coeffs + .resize_with(poly.coeffs.len(), || F::zero_with_cfg(scalar.cfg())); + } + for (dst, coeff) in acc.coeffs.iter_mut().zip(&poly.coeffs) { + *dst += coeff.clone() * scalar; + } +} + fn pow_two(exp: usize, field_cfg: &F::Config) -> F { let two = F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg); let mut out = F::one_with_cfg(field_cfg); From d4cfc6fd3b030d094babf9791603459a9ef7f90b Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 08:46:42 -0700 Subject: [PATCH 40/49] Cache ProjectionFold row expression evaluations --- protocol/src/production_sha.rs | 63 +++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 09746b76..8daf6c43 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -5231,43 +5231,52 @@ where let mu_ff_a = mu(&shift8_weights, &shift9_weights, &high_mu_1_bit_coeff); let mu_ff_e = mu(&shift9_weights, &shift10_weights, &high_mu_1_bit_coeff); + let w_rot25 = word_eval_with(ShaWordCol::W, 0, &rot25_weights); + let w_rot14 = word_eval_with(ShaWordCol::W, 0, &rot14_weights); + let w_shift3 = word_eval_with(ShaWordCol::W, 0, &shift3_weights); + let w_rot15 = word_eval_with(ShaWordCol::W, 0, &rot15_weights); + let w_rot13 = word_eval_with(ShaWordCol::W, 0, &rot13_weights); + let w_shift10 = word_eval_with(ShaWordCol::W, 0, &shift10_weights); + let w_shift16 = word_eval(ShaWordCol::W, 16); + let w_shift9 = word_eval(ShaWordCol::W, 9); + let small_sigma0_shift1 = word_eval(ShaWordCol::SmallSigma0, 1); + let small_sigma1_shift14 = word_eval(ShaWordCol::SmallSigma1, 14); + let a_shift4 = word_eval(ShaWordCol::A, 4); + let e_shift4 = word_eval(ShaWordCol::E, 4); + let sigma0_shift3 = word_eval(ShaWordCol::Sigma0, 3); + let sigma1_shift3 = word_eval(ShaWordCol::Sigma1, 3); + let uef_shift3 = word_eval(ShaWordCol::Uef, 3); + let uneg_eg_shift3 = word_eval(ShaWordCol::UNegEg, 3); + let maj_shift3 = word_eval(ShaWordCol::Maj, 3); + let public_k_shift3 = public_scalar(ShaPublicCol::K, 3); + let r0 = a_word.clone() * &rho_sig0 - &sigma0 - two.clone() * &ov_sigma0; let r1 = e_word.clone() * &rho_sig1 - &sigma1 - two.clone() * &ov_sigma1; - let r2 = word_eval_with(ShaWordCol::W, 0, &rot25_weights) - + word_eval_with(ShaWordCol::W, 0, &rot14_weights) - + word_eval_with(ShaWordCol::W, 0, &shift3_weights) - - &small_sigma0 + let r2 = w_rot25 + w_rot14 + w_shift3 - &small_sigma0 - two.clone() * &ov_small_sigma0; - let r3 = word_eval_with(ShaWordCol::W, 0, &rot15_weights) - + word_eval_with(ShaWordCol::W, 0, &rot13_weights) - + word_eval_with(ShaWordCol::W, 0, &shift10_weights) - - &small_sigma1 + let r3 = w_rot15 + w_rot13 + w_shift10 - &small_sigma1 - two.clone() * &ov_small_sigma1; - let r4 = word_eval(ShaWordCol::W, 16) - - &w - - word_eval(ShaWordCol::SmallSigma0, 1) - - word_eval(ShaWordCol::W, 9) - - word_eval(ShaWordCol::SmallSigma1, 14) + let r4 = w_shift16 - &w - small_sigma0_shift1 - w_shift9 - small_sigma1_shift14 + &mu_w + int_value(ShaIntCol::CompSchedule); - let r5 = word_eval(ShaWordCol::A, 4) + let r5 = a_shift4.clone() - &e_word - - word_eval(ShaWordCol::Sigma1, 3) - - word_eval(ShaWordCol::Uef, 3) - - word_eval(ShaWordCol::UNegEg, 3) - - public_scalar(ShaPublicCol::K, 3) + - &sigma1_shift3 + - &uef_shift3 + - &uneg_eg_shift3 + - &public_k_shift3 - &w - - word_eval(ShaWordCol::Sigma0, 3) - - word_eval(ShaWordCol::Maj, 3) + - &sigma0_shift3 + - &maj_shift3 + &mu_a + int_value(ShaIntCol::CompUpdateA); - let r6 = word_eval(ShaWordCol::E, 4) + let r6 = e_shift4.clone() - &a_word - &e_word - - word_eval(ShaWordCol::Sigma1, 3) - - word_eval(ShaWordCol::Uef, 3) - - word_eval(ShaWordCol::UNegEg, 3) - - public_scalar(ShaPublicCol::K, 3) + - &sigma1_shift3 + - &uef_shift3 + - &uneg_eg_shift3 + - &public_k_shift3 - &w + &mu_e + int_value(ShaIntCol::CompUpdateE); @@ -5283,10 +5292,10 @@ where + (a_word.clone() - public_word_or_const_eval(ShaPublicCol::PAOut)) * &s_out; let r8 = (e_word.clone() - public_word_or_const_eval(ShaPublicCol::PEIn)) * &s_init + (e_word.clone() - public_word_or_const_eval(ShaPublicCol::PEOut)) * &s_out; - let r9 = word_eval(ShaWordCol::A, 4) - &a_word - public_scalar(ShaPublicCol::PAIn, 0) + let r9 = a_shift4 - &a_word - public_scalar(ShaPublicCol::PAIn, 0) + &mu_ff_a + int_value(ShaIntCol::CompFeedForwardA); - let r10 = word_eval(ShaWordCol::E, 4) - &e_word - public_scalar(ShaPublicCol::PEIn, 0) + let r10 = e_shift4 - &e_word - public_scalar(ShaPublicCol::PEIn, 0) + &mu_ff_e + int_value(ShaIntCol::CompFeedForwardE); let r11 = (w - public_word_or_const_eval(ShaPublicCol::Message)) * &s_msg; From 0de2cd011860e0076c2331278baab0fa300f7e7b Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 08:58:14 -0700 Subject: [PATCH 41/49] Specialize ProjectionFold row expression dot products --- protocol/src/production_sha.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 8daf6c43..94fd99c8 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -5170,8 +5170,12 @@ where Box::new(move |values: &[F]| { let zero = zero.clone(); let dot = |lhs: &[F], rhs: &[F]| { - FieldFieldInnerProduct::inner_product::(lhs, rhs, zero.clone()) - .expect("row expression dot product lengths match") + debug_assert_eq!(lhs.len(), rhs.len()); + lhs.iter() + .zip(rhs.iter()) + .fold(zero.clone(), |acc, (left, right)| { + acc + left.clone() * right + }) }; let word_source_idx = |col: ShaWordCol, shift: usize| { let idx = offsets.word[col.index()][shift]; From 941a383743a6a5852c173ac225ff27ba2997598e Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 09:27:33 -0700 Subject: [PATCH 42/49] Add prepared ProjectionFold SHA prover path --- protocol/benches/e2e.rs | 33 ++++++-- protocol/src/production_sha.rs | 138 +++++++++++++++++++++++++-------- 2 files changed, 133 insertions(+), 38 deletions(-) diff --git a/protocol/benches/e2e.rs b/protocol/benches/e2e.rs index 1ad98c6c..d1098eed 100644 --- a/protocol/benches/e2e.rs +++ b/protocol/benches/e2e.rs @@ -40,7 +40,8 @@ use zinc_protocol::{ production_sha::{ LinearIdealFoldProverParams, LinearIdealFoldVerifierParams, ProductionShaError, ProductionShaProjectionAdapter, ProductionShaWitnessPolys, UairShape, - prove_linear_ideal_fold, setup_verify_linear_ideal_fold, verify_linear_ideal_fold, + prepare_linear_ideal_fold_witnesses, prove_prepared_linear_ideal_fold, + setup_verify_linear_ideal_fold, verify_linear_ideal_fold, }, }; use zinc_test_uair::{ @@ -2106,26 +2107,39 @@ where .expect("ProjectionFold SHA verifier setup succeeds"); let params = format!("{label}/SHA256Chain8/row-vars={SHA_ROW_VARS}"); + let prepared_instances = prepare_linear_ideal_fold_witnesses::< + U, + RealEcdsaBenchZincTypes, + F, + DEGREE_PLUS_ONE, + >(&shape, &witnesses, &pp.field_cfg) + .expect("ProjectionFold SHA witness preparation should succeed"); group.bench_function(BenchmarkId::new("Prove", ¶ms), |bench| { bench.iter(|| { let mut transcript = Blake3Transcript::new(); - black_box(prove_linear_ideal_fold::< + black_box(prove_prepared_linear_ideal_fold::< P, U, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE, - >(&pp, &shape, &witnesses, &mut transcript)) + >(&pp, &shape, &prepared_instances, &mut transcript)) .expect("ProjectionFold Concise prover failed"); }); }); let mut prover_transcript = Blake3Transcript::new(); - let output = prove_linear_ideal_fold::, U, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE>( + let output = prove_prepared_linear_ideal_fold::< + P, + U, + RealEcdsaBenchZincTypes, + F, + DEGREE_PLUS_ONE, + >( &pp, &shape, - &witnesses, + &prepared_instances, &mut prover_transcript, ) .expect("proof generation for ProjectionFold verifier bench"); @@ -2150,13 +2164,18 @@ where .finish(); tracing::subscriber::with_default(subscriber, || { let mut prover_transcript = Blake3Transcript::new(); - let traced_output = prove_linear_ideal_fold::< + let traced_output = prove_prepared_linear_ideal_fold::< P, U, RealEcdsaBenchZincTypes, F, DEGREE_PLUS_ONE, - >(&pp, &shape, &witnesses, &mut prover_transcript) + >( + &pp, + &shape, + &prepared_instances, + &mut prover_transcript, + ) .expect("ProjectionFold traced prover failed"); let mut verifier_transcript = Blake3Transcript::new(); diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 94fd99c8..8478c196 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -126,6 +126,16 @@ where pub witness_polys: ProductionShaWitnessPolys, } +#[derive(Clone, Debug)] +pub struct PreparedProductionShaProverInstance +where + Zt: ZincTypes, + F: PrimeField, +{ + pub public_trace: UairTrace<'static, Zt::Int, Zt::Int, D>, + pub instance: ProductionShaProverInstance, +} + pub trait ProductionShaProjectionAdapter: Uair where Zt: ZincTypes, @@ -1174,6 +1184,21 @@ where { let field_cfg = &pp.field_cfg; ensure_production_sha_word_degree::()?; + let prepared = + prepare_linear_ideal_fold_witnesses::(shape, witnesses, field_cfg)?; + prove_prepared_linear_ideal_fold::(pp, shape, &prepared, transcript) +} + +pub fn prepare_linear_ideal_fold_witnesses( + shape: &UairShape, + witnesses: &[UairWitness<'_, Zt::Int, Zt::Int, D>], + field_cfg: &F::Config, +) -> Result>, LinearIdealFoldError> +where + U: Uair + ProductionShaProjectionAdapter + Sync, + Zt: ZincTypes, + F: PrimeField + Send + Sync, +{ if witnesses.len() < 2 { return Err(ProductionShaError::InstanceCountTooSmall(witnesses.len())); } @@ -1183,15 +1208,77 @@ where )); } + cfg_iter!(witnesses) + .map(|witness| { + let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; + let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; + let (trace, public, witness_polys) = + U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg)?; + Ok(PreparedProductionShaProverInstance { + public_trace: own_uair_trace(&public_trace), + instance: ProductionShaProverInstance { + trace, + public, + witness_polys, + }, + }) + }) + .collect() +} + +#[allow(clippy::too_many_arguments)] +pub fn prove_prepared_linear_ideal_fold( + pp: &LinearIdealFoldProverParams, + shape: &UairShape, + prepared_instances: &[PreparedProductionShaProverInstance], + transcript: &mut impl Transcript, +) -> Result< + LinearIdealFoldProveOutput< + UairInstance<'static, Zt::Int, Zt::Int, PCSCommitments, D>, + FoldedLinearIdealInstance>, + FoldedLinearIdealWitness>, + ProductionLinearIdealFoldProof, + >, + LinearIdealFoldError, +> +where + U: Uair, + Zt: ZincTypes, + F: InnerTransparentField + + DelayedFieldProductSum + + ShaBinaryFoldField + + FromPrimitiveWithConfig + + Send + + Sync + + 'static, + F::Inner: Transcribable + Zero + Default + Send + Sync, + F::Modulus: Transcribable, + P: ZincPCSTypes, + P: ProductionShaFoldedPcsOpen, + P::BinaryPCS: FoldablePCS, D>, + P::ArbitraryPCS: FoldablePCS, D>, + P::IntPCS: FoldablePCS, +{ + let field_cfg = &pp.field_cfg; + ensure_production_sha_word_degree::()?; + let instance_count = prepared_instances.len(); + if instance_count < 2 { + return Err(ProductionShaError::InstanceCountTooSmall(instance_count)); + } + if !instance_count.is_power_of_two() { + return Err(ProductionShaError::InstanceCountNotPowerOfTwo( + instance_count, + )); + } + let booleanity_sources = production_sha_booleanity_sources(); absorb_production_sha_statement_metadata(transcript); absorb_uair_shape_metadata(transcript, shape); let (fresh_instances, instance_commitments, instance_prover_data, traces, publics) = - prove_fresh_instances_phase::( + prove_prepared_fresh_instances_phase::( &pp.pcs_params, - shape, - witnesses, + prepared_instances, transcript, field_cfg, )?; @@ -1223,7 +1310,7 @@ where let coeff_tables = build_residual_coeff_tables_phase(&traces, &publics, &r_ic_eq_weights, field_cfg)?; - let beta = sample_instance_batch_challenge(transcript, witnesses.len(), field_cfg)?; + let beta = sample_instance_batch_challenge(transcript, instance_count, field_cfg)?; let beta_eq_weights = build_eq_x_r_vec(&beta, field_cfg)?; let (ideal_check, aggregate_ideal_polys) = prove_aggregate_ideal_phase(&coeff_tables, &beta_eq_weights, transcript, field_cfg)?; @@ -1267,7 +1354,7 @@ where &beta, sumfold_r_b.clone(), sumfold_c_sf, - witnesses.len(), + instance_count, field_cfg, )?; @@ -1365,18 +1452,16 @@ where target = "zinc_protocol::production_sha", level = "info", skip_all, - fields(side = "prove", phase = "fresh_instances", instances = witnesses.len()) + fields(side = "prove", phase = "fresh_instances", instances = prepared_instances.len()) )] #[allow(clippy::too_many_arguments)] -fn prove_fresh_instances_phase( +fn prove_prepared_fresh_instances_phase( pcs_params: &PCSParams, - shape: &UairShape, - witnesses: &[UairWitness<'_, Zt::Int, Zt::Int, D>], + prepared_instances: &[PreparedProductionShaProverInstance], transcript: &mut impl Transcript, - field_cfg: &F::Config, + _field_cfg: &F::Config, ) -> Result, ProductionShaError> where - U: Uair + ProductionShaProjectionAdapter + Sync, Zt: ZincTypes, F: PrimeField, P: ZincPCSTypes, @@ -1384,24 +1469,12 @@ where P::ArbitraryPCS: FoldablePCS, D>, P::IntPCS: FoldablePCS, { - for (instance_idx, witness) in witnesses.iter().enumerate() { - let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; - absorb_public_uair_trace::(transcript, instance_idx, &public_trace); + for (instance_idx, prepared) in prepared_instances.iter().enumerate() { + absorb_public_uair_trace::(transcript, instance_idx, &prepared.public_trace); } - let artifacts = cfg_iter!(witnesses) - .map(|witness| { - let public_trace = public_uair_trace_view(&witness.trace, &shape.signature)?; - let witness_trace = witness_uair_trace_view(&witness.trace, &shape.signature)?; - let (trace, public, witness_polys) = tracing::info_span!( - target: "zinc_protocol::production_sha", - "fresh_project_instance", - side = "prove", - phase = "fresh_project_instance", - ) - .in_scope(|| { - U::project_production_sha_witness(shape, &public_trace, &witness_trace, field_cfg) - })?; + let artifacts = cfg_iter!(prepared_instances) + .map(|prepared| { let (data, commitment) = tracing::info_span!( target: "zinc_protocol::production_sha", "fresh_commit_instance", @@ -1409,18 +1482,21 @@ where phase = "fresh_commit_instance", ) .in_scope(|| { - commit_production_sha_instance::(pcs_params, &witness_polys) + commit_production_sha_instance::( + pcs_params, + &prepared.instance.witness_polys, + ) })?; Ok(( UairInstance { - public_trace: own_uair_trace(&public_trace), + public_trace: prepared.public_trace.clone(), commitments: commitment.clone(), }, commitment, data, - trace, - public, + prepared.instance.trace.clone(), + prepared.instance.public.clone(), )) }) .collect::, ProductionShaError>>()?; From a52cc9ebcfe62e32732a9d32043fd041b2d92b6d Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 09:50:15 -0700 Subject: [PATCH 43/49] Optimize ProjectionFold endpoint MLE construction --- piop/src/neutron_nova/projection_sha.rs | 86 ++++++++++++++++------ protocol/src/production_sha.rs | 94 +++++++++++-------------- 2 files changed, 106 insertions(+), 74 deletions(-) diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 44d92aae..90d6f9ad 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -891,12 +891,11 @@ where validate_trace(trace)?; validate_public(public)?; } - let rho_sig0 = sparse_poly::(&[10, 19, 30], field_cfg); - let rho_sig1 = sparse_poly::(&[7, 21, 26], field_cfg); + let constants = ShaResidualPolyConstants::new(field_cfg); let mut coeffs = vec![DynamicPolynomialF::ZERO; NUM_SHA_RESIDUAL_FAMILIES]; for (row, row_weight) in row_weights.iter().enumerate().take(SHA_ROW_COUNT) { - let residuals = residual_polys_at_row_with_rotation_polys( - trace, public, row, &rho_sig0, &rho_sig1, field_cfg, + let residuals = residual_polys_at_row_with_constants( + trace, public, row, &constants, field_cfg, )?; for (family_idx, residual) in residuals.iter().enumerate() { add_scaled_poly_assign(&mut coeffs[family_idx], residual, row_weight); @@ -3978,9 +3977,52 @@ fn residual_polys_at_row_with_rotation_polys( where F: PrimeField, { - let zero = F::zero_with_cfg(field_cfg); - let one = F::one_with_cfg(field_cfg); - let two = one.clone() + &one; + let constants = ShaResidualPolyConstants { + rho_sig0: rho_sig0.clone(), + rho_sig1: rho_sig1.clone(), + two: F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg), + low_mu_coeff: pow_two(32, field_cfg), + high_mu_w_coeff: pow_two(34, field_cfg), + high_mu_3_bit_coeff: pow_two(35, field_cfg), + high_mu_1_bit_coeff: pow_two(33, field_cfg), + }; + residual_polys_at_row_with_constants(trace, public, row, &constants, field_cfg) +} + +struct ShaResidualPolyConstants { + rho_sig0: DynamicPolynomialF, + rho_sig1: DynamicPolynomialF, + two: F, + low_mu_coeff: F, + high_mu_w_coeff: F, + high_mu_3_bit_coeff: F, + high_mu_1_bit_coeff: F, +} + +impl ShaResidualPolyConstants { + fn new(field_cfg: &F::Config) -> Self { + Self { + rho_sig0: sparse_poly::(&[10, 19, 30], field_cfg), + rho_sig1: sparse_poly::(&[7, 21, 26], field_cfg), + two: F::one_with_cfg(field_cfg) + F::one_with_cfg(field_cfg), + low_mu_coeff: pow_two(32, field_cfg), + high_mu_w_coeff: pow_two(34, field_cfg), + high_mu_3_bit_coeff: pow_two(35, field_cfg), + high_mu_1_bit_coeff: pow_two(33, field_cfg), + } + } +} + +fn residual_polys_at_row_with_constants( + trace: &ProjectedTrace, + public: &ProjectedPublic, + row: usize, + constants: &ShaResidualPolyConstants, + field_cfg: &F::Config, +) -> Result<[DynamicPolynomialF; NUM_SHA_RESIDUAL_FAMILIES], ShaProjectionError> +where + F: PrimeField, +{ let a = word_poly(trace, ShaWordCol::A, row, field_cfg)?; let e = word_poly(trace, ShaWordCol::E, row, field_cfg)?; let sigma0 = word_poly(trace, ShaWordCol::Sigma0, row, field_cfg)?; @@ -4016,11 +4058,14 @@ where let comp_ff_a = int_const_poly(trace, ShaIntCol::CompFeedForwardA, row, field_cfg)?; let comp_ff_e = int_const_poly(trace, ShaIntCol::CompFeedForwardE, row, field_cfg)?; - let r0 = (&a * &rho_sig0) - &sigma0 - &scale_poly(&ov_sigma0, &two); - let r1 = (&e * &rho_sig1) - &sigma1 - &scale_poly(&ov_sigma1, &two); - let r2 = w_rot25 + &w_rot14 + &w_shift3 - &small_sigma0 - &scale_poly(&ov_small_sigma0, &two); - let r3 = - w_rot15 + &w_rot13 + &w.shift_r_c(10) - &small_sigma1 - &scale_poly(&ov_small_sigma1, &two); + let r0 = (&a * &constants.rho_sig0) - &sigma0 - &scale_poly(&ov_sigma0, &constants.two); + let r1 = (&e * &constants.rho_sig1) - &sigma1 - &scale_poly(&ov_sigma1, &constants.two); + let r2 = w_rot25 + &w_rot14 + &w_shift3 + - &small_sigma0 + - &scale_poly(&ov_small_sigma0, &constants.two); + let r3 = w_rot15 + &w_rot13 + &w.shift_r_c(10) + - &small_sigma1 + - &scale_poly(&ov_small_sigma1, &constants.two); let mu_packed = word_poly(trace, ShaWordCol::MuPacked, row, field_cfg)?; let mu_shift2 = mu_packed.shift_r_c(2); @@ -4028,18 +4073,14 @@ where let mu_shift8 = mu_packed.shift_r_c(8); let mu_shift9 = mu_packed.shift_r_c(9); let mu_shift10 = mu_packed.shift_r_c(10); - let low_mu_coeff = pow_two(32, field_cfg); - let high_mu_w_coeff = pow_two(34, field_cfg); - let high_mu_3_bit_coeff = pow_two(35, field_cfg); - let high_mu_1_bit_coeff = pow_two(33, field_cfg); let mu = |low: &DynamicPolynomialF, high: &DynamicPolynomialF, high_coeff: &F| { - scale_poly(low, &low_mu_coeff) - &scale_poly(high, high_coeff) + scale_poly(low, &constants.low_mu_coeff) - &scale_poly(high, high_coeff) }; - let mu_w = mu(&mu_packed, &mu_shift2, &high_mu_w_coeff); - let mu_a = mu(&mu_shift2, &mu_shift5, &high_mu_3_bit_coeff); - let mu_e = mu(&mu_shift5, &mu_shift8, &high_mu_3_bit_coeff); - let mu_ff_a = mu(&mu_shift8, &mu_shift9, &high_mu_1_bit_coeff); - let mu_ff_e = mu(&mu_shift9, &mu_shift10, &high_mu_1_bit_coeff); + let mu_w = mu(&mu_packed, &mu_shift2, &constants.high_mu_w_coeff); + let mu_a = mu(&mu_shift2, &mu_shift5, &constants.high_mu_3_bit_coeff); + let mu_e = mu(&mu_shift5, &mu_shift8, &constants.high_mu_3_bit_coeff); + let mu_ff_a = mu(&mu_shift8, &mu_shift9, &constants.high_mu_1_bit_coeff); + let mu_ff_e = mu(&mu_shift9, &mu_shift10, &constants.high_mu_1_bit_coeff); let r4 = w_shift16 - &w - &small_sigma0_shift1 - &w_shift9 - &small_sigma1_shift14 + &mu_w @@ -4113,7 +4154,6 @@ where ]; residuals.iter_mut().for_each(DynamicPolynomialF::trim); debug_assert_eq!(residuals.len(), NUM_SHA_RESIDUAL_FAMILIES); - let _ = zero; Ok(residuals) } diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index 8478c196..c3376e9f 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -6574,21 +6574,18 @@ fn sha_multipoint_trace_mles( field_cfg: &F::Config, ) -> Result>, ProductionShaError> where - F: InnerTransparentField, + F: InnerTransparentField + Sync, + F::Inner: Send + Sync, { let zero_inner = F::zero_with_cfg(field_cfg).inner().clone(); - layout - .sources - .iter() + cfg_iter!(layout.sources) .map(|source| { - let values = (0..SHA_ROW_COUNT) - .map(|row| { - sha_mp_source_row_value(folded_trace, folded_public, *source, row, field_cfg) - }) - .collect::, _>>()?; Ok(DenseMultilinearExtension::from_evaluations_vec( SHA_ROW_VARS, - values.iter().map(|value| value.inner().clone()).collect(), + sha_mp_source_column_values(folded_trace, folded_public, *source)? + .iter() + .map(|value| value.inner().clone()) + .collect(), zero_inner.clone(), )) }) @@ -6713,52 +6710,47 @@ where Ok((specs, evals)) } -fn sha_mp_source_row_value( - folded_trace: &ProjectedTrace, - folded_public: &ProjectedPublic, +fn sha_mp_source_column_values<'a, F>( + folded_trace: &'a ProjectedTrace, + folded_public: &'a ProjectedPublic, source: ShaMpSource, - row: usize, - field_cfg: &F::Config, -) -> Result> +) -> Result<&'a [F], ProductionShaError> where F: PrimeField, { - match source { - ShaMpSource::WordBit { col, bit } => folded_trace - .bit_slices - .get(bit_slice_index(col.index(), bit, SHA_WORD_BITS)) - .and_then(|column| column.evaluations.get(row)) - .cloned() - .ok_or(ProductionShaError::LengthMismatch { - label: "multipoint word bit source", - got: row, - expected: SHA_ROW_COUNT, - }), - ShaMpSource::Int { col } => folded_trace - .int_columns - .get(col.index()) - .and_then(|column| column.evaluations.get(row)) - .cloned() - .ok_or(ProductionShaError::LengthMismatch { - label: "multipoint int source", - got: row, - expected: SHA_ROW_COUNT, - }), - ShaMpSource::Public { col } => folded_public - .columns - .get(col.index()) - .and_then(|column| column.evaluations.get(row)) - .cloned() - .ok_or(ProductionShaError::LengthMismatch { - label: "multipoint public source", - got: row, - expected: SHA_ROW_COUNT, - }), + let values = + match source { + ShaMpSource::WordBit { col, bit } => folded_trace + .bit_slices + .get(bit_slice_index(col.index(), bit, SHA_WORD_BITS)) + .ok_or(ProductionShaError::LengthMismatch { + label: "multipoint word bit source", + got: bit_slice_index(col.index(), bit, SHA_WORD_BITS), + expected: folded_trace.bit_slices.len(), + })?, + ShaMpSource::Int { col } => folded_trace.int_columns.get(col.index()).ok_or( + ProductionShaError::LengthMismatch { + label: "multipoint int source", + got: col.index(), + expected: folded_trace.int_columns.len(), + }, + )?, + ShaMpSource::Public { col } => folded_public.columns.get(col.index()).ok_or( + ProductionShaError::LengthMismatch { + label: "multipoint public source", + got: col.index(), + expected: folded_public.columns.len(), + }, + )?, + }; + if values.num_vars != SHA_ROW_VARS || values.evaluations.len() != SHA_ROW_COUNT { + return Err(ProductionShaError::LengthMismatch { + label: "multipoint source rows", + got: values.evaluations.len(), + expected: SHA_ROW_COUNT, + }); } - .map(|value| { - let _ = field_cfg; - value - }) + Ok(&values.evaluations) } fn sha_mp_source_endpoint_value_with_row_weights( From c84919cf8c7b9af31c2c2b2cd3f9853d338191ac Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 11:18:32 -0700 Subject: [PATCH 44/49] Prune ProjectionFold SHA endpoint multipoint sources --- protocol/src/production_sha.rs | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/protocol/src/production_sha.rs b/protocol/src/production_sha.rs index c3376e9f..32a707fa 100644 --- a/protocol/src/production_sha.rs +++ b/protocol/src/production_sha.rs @@ -5830,7 +5830,7 @@ pub fn production_sha_endpoint_word_sources() -> Vec<(ShaWordCol, usize)> { (ShaWordCol::E, &[1usize, 2, 4][..]), (ShaWordCol::Sigma0, &[3usize][..]), (ShaWordCol::Sigma1, &[3usize][..]), - (ShaWordCol::W, &[3usize, 9, 16][..]), + (ShaWordCol::W, &[9usize, 16][..]), (ShaWordCol::SmallSigma0, &[1usize][..]), (ShaWordCol::SmallSigma1, &[14usize][..]), (ShaWordCol::Uef, &[2usize, 3][..]), @@ -5864,9 +5864,6 @@ pub fn production_sha_multipoint_layout() -> ShaMultipointLayout { for col in production_sha_endpoint_int_sources() { push_source(ShaMpSource::Int { col }); } - for col in ShaPublicCol::ALL { - push_source(ShaMpSource::Public { col }); - } let mut shifts = Vec::new(); let mut push_shift = |shift| { @@ -5882,10 +5879,6 @@ pub fn production_sha_multipoint_layout() -> ShaMultipointLayout { push_shift(ShaMpShiftSource::WordBit { col, bit, shift }); } } - push_shift(ShaMpShiftSource::Public { - col: ShaPublicCol::K, - shift: 3, - }); ShaMultipointLayout { sources, shifts } } @@ -8917,20 +8910,19 @@ mod tests { } }) .unwrap(); - let public_idx = layout - .sources - .iter() - .position(|source| { - *source - == ShaMpSource::Public { - col: ShaPublicCol::K, - } - }) - .unwrap(); + assert!(!layout.sources.iter().any(|source| matches!( + source, + ShaMpSource::Public { + col: ShaPublicCol::K + } + ))); assert_eq!(open_evals[a0_idx], f(3)); assert_eq!(open_evals[int_idx], f(7)); - assert_eq!(open_evals[public_idx], f(99)); + assert_eq!( + sha_public_at_point(&public, ShaPublicCol::K, 0, &r_0, &field_cfg).unwrap(), + f(99) + ); } #[test] From c213562f6fe4c72ffdc0b18b030bc5fcf78ba9e2 Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 11:32:52 -0700 Subject: [PATCH 45/49] Parallelize ProjectionFold SHA residual table rows --- piop/src/neutron_nova/projection_sha.rs | 35 ++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 90d6f9ad..229d0ae2 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -893,12 +893,35 @@ where } let constants = ShaResidualPolyConstants::new(field_cfg); let mut coeffs = vec![DynamicPolynomialF::ZERO; NUM_SHA_RESIDUAL_FAMILIES]; - for (row, row_weight) in row_weights.iter().enumerate().take(SHA_ROW_COUNT) { - let residuals = residual_polys_at_row_with_constants( - trace, public, row, &constants, field_cfg, - )?; - for (family_idx, residual) in residuals.iter().enumerate() { - add_scaled_poly_assign(&mut coeffs[family_idx], residual, row_weight); + let partials = cfg_chunks!(row_weights, 8) + .enumerate() + .map(|(chunk_idx, row_weight_chunk)| { + let mut partial = vec![DynamicPolynomialF::ZERO; NUM_SHA_RESIDUAL_FAMILIES]; + let row_offset = chunk_idx * 8; + for (row_in_chunk, row_weight) in row_weight_chunk.iter().enumerate() { + let row = row_offset + row_in_chunk; + let residuals = residual_polys_at_row_with_constants( + trace, public, row, &constants, field_cfg, + )?; + for (family_idx, residual) in residuals.iter().enumerate() { + add_scaled_poly_assign(&mut partial[family_idx], residual, row_weight); + } + } + Ok(partial) + }) + .collect::, ShaProjectionError>>()?; + for partial in partials { + for (dst, residual) in coeffs.iter_mut().zip(partial) { + if residual.is_zero() { + continue; + } + if dst.coeffs.len() < residual.coeffs.len() { + dst.coeffs + .resize_with(residual.coeffs.len(), || F::zero_with_cfg(field_cfg)); + } + for (dst_coeff, coeff) in dst.coeffs.iter_mut().zip(residual.coeffs) { + *dst_coeff += coeff; + } } } coeffs.iter_mut().for_each(DynamicPolynomialF::trim); From 1247c4c3e2556b4038c8bd6d7f4e35c5d2b7ef43 Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 11:46:04 -0700 Subject: [PATCH 46/49] Avoid temporary bool rows in Hyrax binary commits --- zip-plus/src/pcs/hyrax.rs | 30 ++++++------ zip-plus/src/pcs/msm_commitment.rs | 74 ++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index 2a91f521..dec8b43d 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -498,20 +498,22 @@ impl HyraxLanes, D> for BinaryLa let row_idx = job_idx % num_rows; let lower = row_idx * ck.num_cols; let upper = (lower + ck.num_cols).min(poly.evaluations.len()); - let row = &poly.evaluations[lower..upper]; - let values = row - .iter() - .map(|eval| { - , D>>::lane_value(eval, lane) - }) - .collect::, _>>()?; - - let mut row_comm = if values.iter().copied().any(|bit| bit) { - BoolSubsetMsm::<6>::msm_bool_row(&ck.msm_ck, &values, use_inner_parallelism) - .map_err(msm_err)? - } else { - C::Group::zero() - }; + let row_len = upper - lower; + let mut row_comm = BoolSubsetMsm::<6>::msm_bool_row_from_window_masks( + &ck.msm_ck, + row_len, + use_inner_parallelism, + |offset, len| { + let mut mask = 0usize; + for bit_idx in 0..len { + if poly.evaluations[lower + offset + bit_idx].coeff(lane) { + mask |= 1usize << bit_idx; + } + } + mask + }, + ) + .map_err(msm_err)?; if ck.blinding_mode.is_blinded() { row_comm += ck.msm_ck.h * blinds[job_idx]; diff --git a/zip-plus/src/pcs/msm_commitment.rs b/zip-plus/src/pcs/msm_commitment.rs index 1524ae28..ef329efb 100644 --- a/zip-plus/src/pcs/msm_commitment.rs +++ b/zip-plus/src/pcs/msm_commitment.rs @@ -148,6 +148,46 @@ impl BoolWindowTable { } acc } + + fn msm_row_from_window_masks( + &self, + value_len: usize, + window_bits: usize, + _use_parallelism_internally: bool, + mask_at: M, + ) -> C::Group + where + M: Fn(usize, usize) -> usize + Sync, + { + #[cfg(feature = "parallel")] + if _use_parallelism_internally && self.lens.len() > 1 { + return self + .lens + .par_iter() + .copied() + .enumerate() + .map(|(window_idx, len)| { + let offset = window_idx * window_bits; + if offset >= value_len { + return C::Group::zero(); + } + let end = (offset + len).min(value_len); + self.tables[window_idx][mask_at(offset, end - offset)] + }) + .reduce(C::Group::zero, |acc, point| acc + point); + } + + let mut acc = C::Group::zero(); + for (window_idx, len) in self.lens.iter().copied().enumerate() { + let offset = window_idx * window_bits; + if offset >= value_len { + break; + } + let end = (offset + len).min(value_len); + acc += self.tables[window_idx][mask_at(offset, end - offset)]; + } + acc + } } impl MsmCommitmentEngine { @@ -343,6 +383,40 @@ impl BoolSubsetMsm { } Ok(acc) } + + pub(crate) fn msm_bool_row_from_window_masks( + ck: &MsmCommitmentKey, + value_len: usize, + use_parallelism_internally: bool, + mask_at: M, + ) -> Result + where + C: AffineRepr, + M: Fn(usize, usize) -> usize + Sync, + { + validate_row_len(ck, value_len)?; + validate_window_bits(WINDOW_BITS)?; + + if WINDOW_BITS == DEFAULT_BOOL_WINDOW_BITS { + return Ok(ck + .bool_tables_6 + .get_or_init(|| BoolWindowTable::new(&ck.bases, DEFAULT_BOOL_WINDOW_BITS)) + .msm_row_from_window_masks( + value_len, + DEFAULT_BOOL_WINDOW_BITS, + use_parallelism_internally, + mask_at, + )); + } + + let mut acc = C::Group::zero(); + for (window_idx, window) in ck.bases[..value_len].chunks(WINDOW_BITS).enumerate() { + let offset = window_idx * WINDOW_BITS; + let table = subset_table::(window)?; + acc += table[mask_at(offset, window.len())]; + } + Ok(acc) + } } impl RowMsmStrategy for U8BucketMsm { From 40473ea75522bdddc8bfe5b52c9c9127b4da3070 Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 11:51:50 -0700 Subject: [PATCH 47/49] Avoid zero-filling sumcheck scratch vectors --- piop/src/sumcheck/prover.rs | 52 +++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/piop/src/sumcheck/prover.rs b/piop/src/sumcheck/prover.rs index 0ce8de21..e6407c33 100644 --- a/piop/src/sumcheck/prover.rs +++ b/piop/src/sumcheck/prover.rs @@ -151,13 +151,12 @@ where } let zero = F::zero_with_cfg(config); let zero_vec_deg = vec![zero.clone(); degree + 1]; - let zero_vec_poly = vec![zero.clone(); polys.len()]; let scratch = || Scratch { evals: zero_vec_deg.clone(), - steps: zero_vec_poly.clone(), - vals0: zero_vec_poly.clone(), - vals: zero_vec_poly.clone(), - levals: zero_vec_deg.clone(), + steps: Vec::with_capacity(polys.len()), + vals0: Vec::with_capacity(polys.len()), + vals: Vec::with_capacity(polys.len()), + levals: Vec::with_capacity(degree + 1), }; #[cfg(not(feature = "parallel"))] @@ -180,26 +179,35 @@ where // My bet is that it won't affect running time, but better safe than // sorry. - s.vals0.iter_mut().zip(polys.iter()).for_each(|(v0, poly)| { - *v0 = F::new_unchecked_with_cfg(poly[index].clone(), config); - }); - s.levals[0] = comb_fn(&s.vals0); + s.vals0.clear(); + s.vals0.extend( + polys + .iter() + .map(|poly| F::new_unchecked_with_cfg(poly[index].clone(), config)), + ); + s.levals.clear(); + s.levals.push(comb_fn(&s.vals0)); if degree > 0 { - s.vals.iter_mut().zip(polys.iter()).for_each(|(v1, poly)| { - *v1 = F::new_unchecked_with_cfg(poly[index + 1].clone(), config); - }); - s.levals[1] = comb_fn(&s.vals); - - for (i, (v1, v0)) in s.vals.iter().zip(s.vals0.iter()).enumerate() { - s.steps[i] = v1.clone() - v0.clone(); - } - - for eval_point in s.levals.iter_mut().take(degree + 1).skip(2) { - for poly_i in 0..polys.len() { - s.vals[poly_i] += &s.steps[poly_i]; + s.vals.clear(); + s.vals.extend( + polys + .iter() + .map(|poly| F::new_unchecked_with_cfg(poly[index + 1].clone(), config)), + ); + s.levals.push(comb_fn(&s.vals)); + + s.steps.clear(); + s.steps + .extend(s.vals.iter().zip(s.vals0.iter()).map(|(v1, v0)| { + v1.clone() - v0.clone() + })); + + for _ in 2..=degree { + for (value, step) in s.vals.iter_mut().zip(s.steps.iter()) { + *value += step; } - *eval_point = comb_fn(&s.vals); + s.levals.push(comb_fn(&s.vals)); } } From 50bcc990c8496980c342d45975d7df10457193df Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 11:58:12 -0700 Subject: [PATCH 48/49] Batch Hyrax binary lane mask commits --- zip-plus/src/pcs/hyrax.rs | 37 ++++++++++++++++------- zip-plus/src/pcs/msm_commitment.rs | 47 +++++++++++++++++++++--------- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index dec8b43d..0820b206 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -492,35 +492,50 @@ impl HyraxLanes, D> for BinaryLa Some((|| { let use_inner_parallelism = use_inner_bool_parallelism(expected_comm); - let comm = cfg_into_iter!(0..expected_comm) - .map(|job_idx| { - let lane = job_idx / num_rows; - let row_idx = job_idx % num_rows; + let per_row = cfg_into_iter!(0..num_rows) + .map(|row_idx| { let lower = row_idx * ck.num_cols; let upper = (lower + ck.num_cols).min(poly.evaluations.len()); let row_len = upper - lower; - let mut row_comm = BoolSubsetMsm::<6>::msm_bool_row_from_window_masks( + let mut row_comms = BoolSubsetMsm::<6>::msm_bool_rows_from_window_masks::< + C, + D, + _, + >( &ck.msm_ck, row_len, use_inner_parallelism, |offset, len| { - let mut mask = 0usize; + let mut masks = [0usize; D]; for bit_idx in 0..len { - if poly.evaluations[lower + offset + bit_idx].coeff(lane) { - mask |= 1usize << bit_idx; + let eval = &poly.evaluations[lower + offset + bit_idx]; + for (lane, mask) in masks.iter_mut().enumerate() { + if eval.coeff(lane) { + *mask |= 1usize << bit_idx; + } } } - mask + masks }, ) .map_err(msm_err)?; if ck.blinding_mode.is_blinded() { - row_comm += ck.msm_ck.h * blinds[job_idx]; + for (lane, row_comm) in row_comms.iter_mut().enumerate() { + let blind_idx = lane * num_rows + row_idx; + *row_comm += ck.msm_ck.h * blinds[blind_idx]; + } } - Ok::(row_comm) + Ok::<[C::Group; D], ZipError>(row_comms) }) .collect::, _>>()?; + + let mut comm = Vec::with_capacity(expected_comm); + for lane in 0..D { + for row_comms in &per_row { + comm.push(row_comms[lane]); + } + } Ok((comm, blinds)) })()) } diff --git a/zip-plus/src/pcs/msm_commitment.rs b/zip-plus/src/pcs/msm_commitment.rs index ef329efb..69e9d732 100644 --- a/zip-plus/src/pcs/msm_commitment.rs +++ b/zip-plus/src/pcs/msm_commitment.rs @@ -149,15 +149,15 @@ impl BoolWindowTable { acc } - fn msm_row_from_window_masks( + fn msm_rows_from_window_masks( &self, value_len: usize, window_bits: usize, _use_parallelism_internally: bool, mask_at: M, - ) -> C::Group + ) -> [C::Group; LANES] where - M: Fn(usize, usize) -> usize + Sync, + M: Fn(usize, usize) -> [usize; LANES] + Sync, { #[cfg(feature = "parallel")] if _use_parallelism_internally && self.lens.len() > 1 { @@ -167,24 +167,40 @@ impl BoolWindowTable { .copied() .enumerate() .map(|(window_idx, len)| { + let mut partial = std::array::from_fn(|_| C::Group::zero()); let offset = window_idx * window_bits; if offset >= value_len { - return C::Group::zero(); + return partial; } let end = (offset + len).min(value_len); - self.tables[window_idx][mask_at(offset, end - offset)] + let masks = mask_at(offset, end - offset); + for lane in 0..LANES { + partial[lane] += self.tables[window_idx][masks[lane]]; + } + partial }) - .reduce(C::Group::zero, |acc, point| acc + point); + .reduce( + || std::array::from_fn(|_| C::Group::zero()), + |mut acc, partial| { + for lane in 0..LANES { + acc[lane] += partial[lane]; + } + acc + }, + ); } - let mut acc = C::Group::zero(); + let mut acc = std::array::from_fn(|_| C::Group::zero()); for (window_idx, len) in self.lens.iter().copied().enumerate() { let offset = window_idx * window_bits; if offset >= value_len { break; } let end = (offset + len).min(value_len); - acc += self.tables[window_idx][mask_at(offset, end - offset)]; + let masks = mask_at(offset, end - offset); + for lane in 0..LANES { + acc[lane] += self.tables[window_idx][masks[lane]]; + } } acc } @@ -384,15 +400,15 @@ impl BoolSubsetMsm { Ok(acc) } - pub(crate) fn msm_bool_row_from_window_masks( + pub(crate) fn msm_bool_rows_from_window_masks( ck: &MsmCommitmentKey, value_len: usize, use_parallelism_internally: bool, mask_at: M, - ) -> Result + ) -> Result<[C::Group; LANES], MsmError> where C: AffineRepr, - M: Fn(usize, usize) -> usize + Sync, + M: Fn(usize, usize) -> [usize; LANES] + Sync, { validate_row_len(ck, value_len)?; validate_window_bits(WINDOW_BITS)?; @@ -401,7 +417,7 @@ impl BoolSubsetMsm { return Ok(ck .bool_tables_6 .get_or_init(|| BoolWindowTable::new(&ck.bases, DEFAULT_BOOL_WINDOW_BITS)) - .msm_row_from_window_masks( + .msm_rows_from_window_masks( value_len, DEFAULT_BOOL_WINDOW_BITS, use_parallelism_internally, @@ -409,11 +425,14 @@ impl BoolSubsetMsm { )); } - let mut acc = C::Group::zero(); + let mut acc = std::array::from_fn(|_| C::Group::zero()); for (window_idx, window) in ck.bases[..value_len].chunks(WINDOW_BITS).enumerate() { let offset = window_idx * WINDOW_BITS; + let masks = mask_at(offset, window.len()); let table = subset_table::(window)?; - acc += table[mask_at(offset, window.len())]; + for lane in 0..LANES { + acc[lane] += table[masks[lane]]; + } } Ok(acc) } From 9dd568d9e16874c07137c90cbc32dc21f1a20cef Mon Sep 17 00:00:00 2001 From: John Wu Date: Mon, 8 Jun 2026 16:56:23 -0700 Subject: [PATCH 49/49] Optimize ProjectionFold SHA prover hot paths --- piop/src/neutron_nova/projection_sha.rs | 1046 +++++++++++++++++++++-- zip-plus/src/pcs/hyrax.rs | 332 ++++++- 2 files changed, 1305 insertions(+), 73 deletions(-) diff --git a/piop/src/neutron_nova/projection_sha.rs b/piop/src/neutron_nova/projection_sha.rs index 229d0ae2..0e8420ae 100644 --- a/piop/src/neutron_nova/projection_sha.rs +++ b/piop/src/neutron_nova/projection_sha.rs @@ -892,40 +892,39 @@ where validate_public(public)?; } let constants = ShaResidualPolyConstants::new(field_cfg); - let mut coeffs = vec![DynamicPolynomialF::ZERO; NUM_SHA_RESIDUAL_FAMILIES]; - let partials = cfg_chunks!(row_weights, 8) + let partials = cfg_chunks!(row_weights, 64) .enumerate() .map(|(chunk_idx, row_weight_chunk)| { - let mut partial = vec![DynamicPolynomialF::ZERO; NUM_SHA_RESIDUAL_FAMILIES]; - let row_offset = chunk_idx * 8; + let mut partial = FixedResidualCoeffAccumulator::new( + NUM_SHA_RESIDUAL_FAMILIES, + SHA_RESIDUAL_EVAL_POWER_COUNT, + field_cfg, + ); + let row_offset = chunk_idx * 64; for (row_in_chunk, row_weight) in row_weight_chunk.iter().enumerate() { let row = row_offset + row_in_chunk; - let residuals = residual_polys_at_row_with_constants( - trace, public, row, &constants, field_cfg, + accumulate_residual_row_fixed( + &mut partial, + trace, + public, + row, + row_weight, + &constants, + field_cfg, )?; - for (family_idx, residual) in residuals.iter().enumerate() { - add_scaled_poly_assign(&mut partial[family_idx], residual, row_weight); - } } Ok(partial) }) .collect::, ShaProjectionError>>()?; + let mut coeffs = FixedResidualCoeffAccumulator::new( + NUM_SHA_RESIDUAL_FAMILIES, + SHA_RESIDUAL_EVAL_POWER_COUNT, + field_cfg, + ); for partial in partials { - for (dst, residual) in coeffs.iter_mut().zip(partial) { - if residual.is_zero() { - continue; - } - if dst.coeffs.len() < residual.coeffs.len() { - dst.coeffs - .resize_with(residual.coeffs.len(), || F::zero_with_cfg(field_cfg)); - } - for (dst_coeff, coeff) in dst.coeffs.iter_mut().zip(residual.coeffs) { - *dst_coeff += coeff; - } - } + coeffs.add_assign(partial); } - coeffs.iter_mut().for_each(DynamicPolynomialF::trim); - Ok(LinearResidualCoeffTable { coeffs }) + Ok(coeffs.into_table()) }) .collect::, _>>() } @@ -3110,42 +3109,31 @@ where let suffix_count = suffix_sources.len(); let suffix_needs_virtuals = sources_need_virtuals(suffix_sources); let small_square_fields: Vec = small_square_field_table(field_cfg); - let word_bit_coeff_table: Vec = if word_bit_source_count == 0 { - Vec::new() - } else { - let mask_count = 1usize << prefix_len; - let mut table = Vec::with_capacity(mask_count * ternary_len); - for mask in 0..mask_count { - let source_mask = u8::try_from(mask).map_err(|_| { - ShaProjectionError::NonCanonicalProofObject( - "booleanity prefix masks require at most eight prefix entries", - ) - })?; - for plan in &coeff_plans { - table.push(booleanity_word_bit_mask_degree_two_coeff( - source_mask, - plan, - &small_square_fields, - field_cfg, - )); - } + let mask_count = 1usize << prefix_len; + let mut mask_coeff_table = Vec::with_capacity(mask_count * ternary_len); + for mask in 0..mask_count { + let source_mask = u8::try_from(mask).map_err(|_| { + ShaProjectionError::NonCanonicalProofObject( + "booleanity prefix masks require at most eight prefix entries", + ) + })?; + for plan in &coeff_plans { + mask_coeff_table.push(booleanity_word_bit_mask_degree_two_coeff( + source_mask, + plan, + &small_square_fields, + field_cfg, + )); } - table - }; + } + let one = F::one_with_cfg(field_cfg); let partials = cfg_chunks!(row_weights, 8) .enumerate() .map(|(chunk_idx, row_weight_chunk)| { let row_offset = chunk_idx * 8; let mut partial = vec![F::zero_with_cfg(field_cfg); ternary_len * tail_len]; let mut suffix_values = vec![F::zero_with_cfg(field_cfg); prefix_len * suffix_count]; - let mut mask_weights = vec![ - F::zero_with_cfg(field_cfg); - if word_bit_source_count == 0 { - 0 - } else { - 1usize << prefix_len - } - ]; + let mut mask_weights = vec![F::zero_with_cfg(field_cfg); mask_count]; let mut touched_masks = Vec::new(); for tail in 0..tail_len { for (row_in_chunk, row_weight) in row_weight_chunk.iter().enumerate() { @@ -3177,7 +3165,7 @@ where let source_weight = row_weight.clone() * &mask_weights[mask_idx]; let coeff_offset = mask_idx * ternary_len; for ternary_idx in 0..ternary_len { - let coeff = &word_bit_coeff_table[coeff_offset + ternary_idx]; + let coeff = &mask_coeff_table[coeff_offset + ternary_idx]; if F::is_zero(&coeff) { continue; } @@ -3201,7 +3189,41 @@ where )?; } + let mut generic_suffixes = Vec::new(); for suffix_idx in 0..suffix_count { + let source_idx = word_bit_source_count + suffix_idx; + let booleanity_weight = &booleanity_weights[source_idx]; + if let Some(mask_idx) = booleanity_prefix_values_binary_mask( + &suffix_values, + suffix_count, + suffix_idx, + &one, + ) { + if F::is_zero(&mask_weights[mask_idx]) { + touched_masks.push(mask_idx); + } + mask_weights[mask_idx] += booleanity_weight.clone(); + } else { + generic_suffixes.push(suffix_idx); + } + } + + for &mask_idx in &touched_masks { + let source_weight = row_weight.clone() * &mask_weights[mask_idx]; + let coeff_offset = mask_idx * ternary_len; + for ternary_idx in 0..ternary_len { + let coeff = &mask_coeff_table[coeff_offset + ternary_idx]; + if F::is_zero(&coeff) { + continue; + } + partial[tail * ternary_len + ternary_idx] += + source_weight.clone() * coeff; + } + mask_weights[mask_idx] = F::zero_with_cfg(field_cfg); + } + touched_masks.clear(); + + for suffix_idx in generic_suffixes { let source_idx = word_bit_source_count + suffix_idx; let booleanity_weight = &booleanity_weights[source_idx]; let source_weight = row_weight.clone() * booleanity_weight; @@ -3392,6 +3414,37 @@ where delta.clone() * delta } +fn booleanity_prefix_values_binary_mask( + source_values: &[F], + source_count: usize, + source_idx: usize, + one: &F, +) -> Option +where + F: PrimeField, +{ + if source_count == 0 { + return Some(0); + } + let prefix_len = source_values.len() / source_count; + if prefix_len > usize::BITS as usize { + return None; + } + let mut mask = 0usize; + for prefix in 0..prefix_len { + let value = &source_values[prefix * source_count + source_idx]; + if F::is_zero(value) { + continue; + } + if value == one { + mask |= 1usize << prefix; + } else { + return None; + } + } + Some(mask) +} + fn booleanity_word_bit_mask_degree_two_coeff( source_mask: u8, plan: &TernaryCoeffPlan, @@ -4036,6 +4089,834 @@ impl ShaResidualPolyConstants { } } +#[derive(Clone, Debug)] +struct FixedResidualCoeffAccumulator { + coeffs: Vec>, +} + +impl FixedResidualCoeffAccumulator +where + F: PrimeField, +{ + fn new(family_count: usize, coeff_count: usize, field_cfg: &F::Config) -> Self { + Self { + coeffs: (0..family_count) + .map(|_| vec![F::zero_with_cfg(field_cfg); coeff_count]) + .collect(), + } + } + + fn add_assign(&mut self, rhs: Self) { + for (dst_family, rhs_family) in self.coeffs.iter_mut().zip(rhs.coeffs) { + for (dst, rhs) in dst_family.iter_mut().zip(rhs_family) { + *dst += rhs; + } + } + } + + fn into_table(mut self) -> LinearResidualCoeffTable { + let coeffs = self + .coeffs + .drain(..) + .map(|mut coeffs| { + while coeffs.last().is_some_and(F::is_zero) { + coeffs.pop(); + } + DynamicPolynomialF { coeffs } + }) + .collect(); + LinearResidualCoeffTable { coeffs } + } + + #[inline(always)] + fn add_scaled_to_family_idx( + &mut self, + family_idx: usize, + coeff_idx: usize, + value: &F, + scale: &F, + ) { + if F::is_zero(value) || F::is_zero(scale) { + return; + } + debug_assert!(family_idx < self.coeffs.len()); + debug_assert!(coeff_idx < self.coeffs[family_idx].len()); + self.coeffs[family_idx][coeff_idx] += value.clone() * scale; + } + + #[inline(always)] + fn add_scaled(&mut self, family: ShaResidualFamily, coeff_idx: usize, value: &F, scale: &F) { + self.add_scaled_to_family_idx(family.index(), coeff_idx, value, scale); + } + + #[inline(always)] + fn add_const_scaled(&mut self, family: ShaResidualFamily, value: &F, scale: &F) { + self.add_scaled(family, 0, value, scale); + } + + fn add_trace_word_scaled( + &mut self, + family: ShaResidualFamily, + trace: &ProjectedTrace, + col: ShaWordCol, + row: usize, + row_shift: usize, + scale: &F, + field_cfg: &F::Config, + ) -> Result<(), ShaProjectionError> { + if F::is_zero(scale) { + return Ok(()); + } + for bit in 0..SHA_WORD_BITS { + let value = bit_at_shifted_or_zero(trace, col, row, row_shift, bit, field_cfg)?; + self.add_scaled(family, bit, &value, scale); + } + Ok(()) + } + + fn add_trace_word_rot_scaled( + &mut self, + family: ShaResidualFamily, + trace: &ProjectedTrace, + col: ShaWordCol, + row: usize, + rot: usize, + scale: &F, + field_cfg: &F::Config, + ) -> Result<(), ShaProjectionError> { + debug_assert!(rot < SHA_WORD_BITS); + if F::is_zero(scale) { + return Ok(()); + } + for bit in 0..SHA_WORD_BITS { + let value = bit_at_shifted_or_zero(trace, col, row, 0, bit, field_cfg)?; + self.add_scaled(family, (bit + rot) % SHA_WORD_BITS, &value, scale); + } + Ok(()) + } + + fn add_trace_word_shift_r_scaled( + &mut self, + family: ShaResidualFamily, + trace: &ProjectedTrace, + col: ShaWordCol, + row: usize, + shift: usize, + scale: &F, + field_cfg: &F::Config, + ) -> Result<(), ShaProjectionError> { + debug_assert!(shift < SHA_WORD_BITS); + if F::is_zero(scale) { + return Ok(()); + } + for out_bit in 0..(SHA_WORD_BITS - shift) { + let value = bit_at_shifted_or_zero(trace, col, row, 0, out_bit + shift, field_cfg)?; + self.add_scaled(family, out_bit, &value, scale); + } + Ok(()) + } + + fn add_trace_word_sparse_product_scaled( + &mut self, + family: ShaResidualFamily, + trace: &ProjectedTrace, + col: ShaWordCol, + row: usize, + shifts: &[usize], + scale: &F, + field_cfg: &F::Config, + ) -> Result<(), ShaProjectionError> { + if F::is_zero(scale) { + return Ok(()); + } + for bit in 0..SHA_WORD_BITS { + let value = bit_at_shifted_or_zero(trace, col, row, 0, bit, field_cfg)?; + for &shift in shifts { + self.add_scaled(family, bit + shift, &value, scale); + } + } + Ok(()) + } + + fn add_trace_int_const_scaled( + &mut self, + family: ShaResidualFamily, + trace: &ProjectedTrace, + col: ShaIntCol, + row: usize, + scale: &F, + field_cfg: &F::Config, + ) -> Result<(), ShaProjectionError> { + let value = int_scalar(trace, col, row, field_cfg)?; + self.add_const_scaled(family, &value, scale); + Ok(()) + } + + fn add_public_scalar_const_scaled( + &mut self, + family: ShaResidualFamily, + public: &ProjectedPublic, + col: ShaPublicCol, + row: usize, + scale: &F, + field_cfg: &F::Config, + ) -> Result<(), ShaProjectionError> { + let value = public_scalar(public, col, row, field_cfg)?; + self.add_const_scaled(family, &value, scale); + Ok(()) + } + + fn add_public_word_or_const_scaled( + &mut self, + family: ShaResidualFamily, + public: &ProjectedPublic, + col: ShaPublicCol, + row: usize, + scale: &F, + field_cfg: &F::Config, + ) -> Result<(), ShaProjectionError> { + if F::is_zero(scale) { + return Ok(()); + } + let Some(word_col) = col.public_word_col() else { + return self.add_public_scalar_const_scaled(family, public, col, row, scale, field_cfg); + }; + let Some(bit_slices) = &public.bit_slices else { + return self.add_public_scalar_const_scaled(family, public, col, row, scale, field_cfg); + }; + if row >= SHA_ROW_COUNT { + return Ok(()); + } + let col_idx = word_col.index(); + for bit in 0..SHA_WORD_BITS { + let value = scalar_from_table( + "public.bit_slices", + bit_slices, + bit_slice_index(col_idx, bit, SHA_WORD_BITS), + row, + field_cfg, + )?; + self.add_scaled(family, bit, &value, scale); + } + Ok(()) + } +} + +#[inline(always)] +fn neg(value: &F) -> F { + -value.clone() +} + +#[inline(always)] +fn scaled(lhs: &F, rhs: &F) -> F { + lhs.clone() * rhs +} + +fn add_mu_contribution( + acc: &mut FixedResidualCoeffAccumulator, + family: ShaResidualFamily, + trace: &ProjectedTrace, + row: usize, + low_shift: usize, + high_shift: usize, + high_coeff: &F, + row_weight: &F, + constants: &ShaResidualPolyConstants, + field_cfg: &F::Config, +) -> Result<(), ShaProjectionError> +where + F: PrimeField, +{ + let low_scale = scaled(row_weight, &constants.low_mu_coeff); + let high_scale = neg(&scaled(row_weight, high_coeff)); + acc.add_trace_word_shift_r_scaled( + family, + trace, + ShaWordCol::MuPacked, + row, + low_shift, + &low_scale, + field_cfg, + )?; + acc.add_trace_word_shift_r_scaled( + family, + trace, + ShaWordCol::MuPacked, + row, + high_shift, + &high_scale, + field_cfg, + ) +} + +#[allow(clippy::arithmetic_side_effects)] +fn accumulate_residual_row_fixed( + acc: &mut FixedResidualCoeffAccumulator, + trace: &ProjectedTrace, + public: &ProjectedPublic, + row: usize, + row_weight: &F, + constants: &ShaResidualPolyConstants, + field_cfg: &F::Config, +) -> Result<(), ShaProjectionError> +where + F: PrimeField, +{ + if F::is_zero(row_weight) { + return Ok(()); + } + + let minus_row = neg(row_weight); + let minus_two_row = neg(&scaled(row_weight, &constants.two)); + + // R0/R1: big-sigma residuals. Multiplication by the sparse rotation + // polynomial is just three coefficient shifts. + acc.add_trace_word_sparse_product_scaled( + ShaResidualFamily::R0BigSigmaA, + trace, + ShaWordCol::A, + row, + &[10, 19, 30], + row_weight, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R0BigSigmaA, + trace, + ShaWordCol::Sigma0, + row, + 0, + &minus_row, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R0BigSigmaA, + trace, + ShaWordCol::OvSigma0, + row, + 0, + &minus_two_row, + field_cfg, + )?; + + acc.add_trace_word_sparse_product_scaled( + ShaResidualFamily::R1BigSigmaE, + trace, + ShaWordCol::E, + row, + &[7, 21, 26], + row_weight, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R1BigSigmaE, + trace, + ShaWordCol::Sigma1, + row, + 0, + &minus_row, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R1BigSigmaE, + trace, + ShaWordCol::OvSigma1, + row, + 0, + &minus_two_row, + field_cfg, + )?; + + // R2/R3: small-sigma residuals over the message schedule word. + for rot in [25usize, 14] { + acc.add_trace_word_rot_scaled( + ShaResidualFamily::R2SmallSigma0, + trace, + ShaWordCol::W, + row, + rot, + row_weight, + field_cfg, + )?; + } + acc.add_trace_word_shift_r_scaled( + ShaResidualFamily::R2SmallSigma0, + trace, + ShaWordCol::W, + row, + 3, + row_weight, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R2SmallSigma0, + trace, + ShaWordCol::SmallSigma0, + row, + 0, + &minus_row, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R2SmallSigma0, + trace, + ShaWordCol::OvSmallSigma0, + row, + 0, + &minus_two_row, + field_cfg, + )?; + + for rot in [15usize, 13] { + acc.add_trace_word_rot_scaled( + ShaResidualFamily::R3SmallSigma1, + trace, + ShaWordCol::W, + row, + rot, + row_weight, + field_cfg, + )?; + } + acc.add_trace_word_shift_r_scaled( + ShaResidualFamily::R3SmallSigma1, + trace, + ShaWordCol::W, + row, + 10, + row_weight, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R3SmallSigma1, + trace, + ShaWordCol::SmallSigma1, + row, + 0, + &minus_row, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R3SmallSigma1, + trace, + ShaWordCol::OvSmallSigma1, + row, + 0, + &minus_two_row, + field_cfg, + )?; + + // R4: schedule transition. + acc.add_trace_word_scaled( + ShaResidualFamily::R4Schedule, + trace, + ShaWordCol::W, + row, + 16, + row_weight, + field_cfg, + )?; + for (col, shift) in [ + (ShaWordCol::W, 0usize), + (ShaWordCol::SmallSigma0, 1), + (ShaWordCol::W, 9), + (ShaWordCol::SmallSigma1, 14), + ] { + acc.add_trace_word_scaled( + ShaResidualFamily::R4Schedule, + trace, + col, + row, + shift, + &minus_row, + field_cfg, + )?; + } + add_mu_contribution( + acc, + ShaResidualFamily::R4Schedule, + trace, + row, + 0, + 2, + &constants.high_mu_w_coeff, + row_weight, + constants, + field_cfg, + )?; + acc.add_trace_int_const_scaled( + ShaResidualFamily::R4Schedule, + trace, + ShaIntCol::CompSchedule, + row, + row_weight, + field_cfg, + )?; + + // R5/R6: compression round updates. + acc.add_trace_word_scaled( + ShaResidualFamily::R5UpdateA, + trace, + ShaWordCol::A, + row, + 4, + row_weight, + field_cfg, + )?; + for (col, shift) in [ + (ShaWordCol::E, 0usize), + (ShaWordCol::Sigma1, 3), + (ShaWordCol::Uef, 3), + (ShaWordCol::UNegEg, 3), + (ShaWordCol::W, 0), + (ShaWordCol::Sigma0, 3), + (ShaWordCol::Maj, 3), + ] { + acc.add_trace_word_scaled( + ShaResidualFamily::R5UpdateA, + trace, + col, + row, + shift, + &minus_row, + field_cfg, + )?; + } + acc.add_public_scalar_const_scaled( + ShaResidualFamily::R5UpdateA, + public, + ShaPublicCol::K, + row + 3, + &minus_row, + field_cfg, + )?; + add_mu_contribution( + acc, + ShaResidualFamily::R5UpdateA, + trace, + row, + 2, + 5, + &constants.high_mu_3_bit_coeff, + row_weight, + constants, + field_cfg, + )?; + acc.add_trace_int_const_scaled( + ShaResidualFamily::R5UpdateA, + trace, + ShaIntCol::CompUpdateA, + row, + row_weight, + field_cfg, + )?; + + acc.add_trace_word_scaled( + ShaResidualFamily::R6UpdateE, + trace, + ShaWordCol::E, + row, + 4, + row_weight, + field_cfg, + )?; + for (col, shift) in [ + (ShaWordCol::A, 0usize), + (ShaWordCol::E, 0), + (ShaWordCol::Sigma1, 3), + (ShaWordCol::Uef, 3), + (ShaWordCol::UNegEg, 3), + (ShaWordCol::W, 0), + ] { + acc.add_trace_word_scaled( + ShaResidualFamily::R6UpdateE, + trace, + col, + row, + shift, + &minus_row, + field_cfg, + )?; + } + acc.add_public_scalar_const_scaled( + ShaResidualFamily::R6UpdateE, + public, + ShaPublicCol::K, + row + 3, + &minus_row, + field_cfg, + )?; + add_mu_contribution( + acc, + ShaResidualFamily::R6UpdateE, + trace, + row, + 5, + 8, + &constants.high_mu_3_bit_coeff, + row_weight, + constants, + field_cfg, + )?; + acc.add_trace_int_const_scaled( + ShaResidualFamily::R6UpdateE, + trace, + ShaIntCol::CompUpdateE, + row, + row_weight, + field_cfg, + )?; + + // R7/R8: pin input/output public words at active selector rows. + let s_init = public_scalar(public, ShaPublicCol::SInit, row, field_cfg)?; + let s_out = public_scalar(public, ShaPublicCol::SOut, row, field_cfg)?; + let init_scale = scaled(row_weight, &s_init); + let out_scale = scaled(row_weight, &s_out); + let neg_init_scale = neg(&init_scale); + let neg_out_scale = neg(&out_scale); + acc.add_trace_word_scaled( + ShaResidualFamily::R7PinA, + trace, + ShaWordCol::A, + row, + 0, + &init_scale, + field_cfg, + )?; + acc.add_public_word_or_const_scaled( + ShaResidualFamily::R7PinA, + public, + ShaPublicCol::PAIn, + row, + &neg_init_scale, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R7PinA, + trace, + ShaWordCol::A, + row, + 0, + &out_scale, + field_cfg, + )?; + acc.add_public_word_or_const_scaled( + ShaResidualFamily::R7PinA, + public, + ShaPublicCol::PAOut, + row, + &neg_out_scale, + field_cfg, + )?; + + acc.add_trace_word_scaled( + ShaResidualFamily::R8PinE, + trace, + ShaWordCol::E, + row, + 0, + &init_scale, + field_cfg, + )?; + acc.add_public_word_or_const_scaled( + ShaResidualFamily::R8PinE, + public, + ShaPublicCol::PEIn, + row, + &neg_init_scale, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R8PinE, + trace, + ShaWordCol::E, + row, + 0, + &out_scale, + field_cfg, + )?; + acc.add_public_word_or_const_scaled( + ShaResidualFamily::R8PinE, + public, + ShaPublicCol::PEOut, + row, + &neg_out_scale, + field_cfg, + )?; + + // R9/R10: feed-forward rows. + acc.add_trace_word_scaled( + ShaResidualFamily::R9FeedForwardA, + trace, + ShaWordCol::A, + row, + 4, + row_weight, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R9FeedForwardA, + trace, + ShaWordCol::A, + row, + 0, + &minus_row, + field_cfg, + )?; + acc.add_public_scalar_const_scaled( + ShaResidualFamily::R9FeedForwardA, + public, + ShaPublicCol::PAIn, + row, + &minus_row, + field_cfg, + )?; + add_mu_contribution( + acc, + ShaResidualFamily::R9FeedForwardA, + trace, + row, + 8, + 9, + &constants.high_mu_1_bit_coeff, + row_weight, + constants, + field_cfg, + )?; + acc.add_trace_int_const_scaled( + ShaResidualFamily::R9FeedForwardA, + trace, + ShaIntCol::CompFeedForwardA, + row, + row_weight, + field_cfg, + )?; + + acc.add_trace_word_scaled( + ShaResidualFamily::R10FeedForwardE, + trace, + ShaWordCol::E, + row, + 4, + row_weight, + field_cfg, + )?; + acc.add_trace_word_scaled( + ShaResidualFamily::R10FeedForwardE, + trace, + ShaWordCol::E, + row, + 0, + &minus_row, + field_cfg, + )?; + acc.add_public_scalar_const_scaled( + ShaResidualFamily::R10FeedForwardE, + public, + ShaPublicCol::PEIn, + row, + &minus_row, + field_cfg, + )?; + add_mu_contribution( + acc, + ShaResidualFamily::R10FeedForwardE, + trace, + row, + 9, + 10, + &constants.high_mu_1_bit_coeff, + row_weight, + constants, + field_cfg, + )?; + acc.add_trace_int_const_scaled( + ShaResidualFamily::R10FeedForwardE, + trace, + ShaIntCol::CompFeedForwardE, + row, + row_weight, + field_cfg, + )?; + + // R11-R17: selector and high-bit/carry residuals. + let s_msg = public_scalar(public, ShaPublicCol::SMsg, row, field_cfg)?; + let msg_scale = scaled(row_weight, &s_msg); + let neg_msg_scale = neg(&msg_scale); + acc.add_trace_word_scaled( + ShaResidualFamily::R11MessagePin, + trace, + ShaWordCol::W, + row, + 0, + &msg_scale, + field_cfg, + )?; + acc.add_public_word_or_const_scaled( + ShaResidualFamily::R11MessagePin, + public, + ShaPublicCol::Message, + row, + &neg_msg_scale, + field_cfg, + )?; + + let s_sched = public_scalar(public, ShaPublicCol::SSched, row, field_cfg)?; + let s_upd = public_scalar(public, ShaPublicCol::SUpd, row, field_cfg)?; + let s_ff = public_scalar(public, ShaPublicCol::SFf, row, field_cfg)?; + acc.add_trace_int_const_scaled( + ShaResidualFamily::R12CompSchedule, + trace, + ShaIntCol::CompSchedule, + row, + &scaled(row_weight, &s_sched), + field_cfg, + )?; + acc.add_trace_int_const_scaled( + ShaResidualFamily::R13CompUpdateA, + trace, + ShaIntCol::CompUpdateA, + row, + &scaled(row_weight, &s_upd), + field_cfg, + )?; + acc.add_trace_int_const_scaled( + ShaResidualFamily::R14CompUpdateE, + trace, + ShaIntCol::CompUpdateE, + row, + &scaled(row_weight, &s_upd), + field_cfg, + )?; + acc.add_trace_int_const_scaled( + ShaResidualFamily::R15CompFeedForwardA, + trace, + ShaIntCol::CompFeedForwardA, + row, + &scaled(row_weight, &s_ff), + field_cfg, + )?; + acc.add_trace_int_const_scaled( + ShaResidualFamily::R16CompFeedForwardE, + trace, + ShaIntCol::CompFeedForwardE, + row, + &scaled(row_weight, &s_ff), + field_cfg, + )?; + acc.add_trace_word_shift_r_scaled( + ShaResidualFamily::R17CarryHighBits, + trace, + ShaWordCol::MuPacked, + row, + 10, + row_weight, + field_cfg, + )?; + + Ok(()) +} + fn residual_polys_at_row_with_constants( trace: &ProjectedTrace, public: &ProjectedPublic, @@ -5109,6 +5990,65 @@ mod tests { } } + #[test] + fn fixed_residual_coeff_table_matches_dynamic_reference() { + let cfg = test_config(); + let a = f(5); + let mut trace = synthetic_boolean_trace(11, &a); + for (col_idx, column) in trace.int_columns.iter_mut().enumerate() { + for (row_idx, value) in column.evaluations.iter_mut().enumerate() { + *value = f(u64::try_from((col_idx + 3) * (row_idx % 17 + 1)).unwrap()); + } + } + + let mut public = zero_public(); + for (col_idx, column) in public.columns.iter_mut().enumerate() { + for (row_idx, value) in column.evaluations.iter_mut().enumerate() { + *value = f(u64::try_from((col_idx + 5) * (row_idx % 19 + 1)).unwrap()); + } + } + + let zero = F::zero_with_cfg(&cfg); + let mut public_bits = + vec![vec![vec![zero; SHA_WORD_BITS]; SHA_ROW_COUNT]; ShaPublicWordCol::COUNT]; + for (col_idx, col) in public_bits.iter_mut().enumerate() { + for (row_idx, row) in col.iter_mut().enumerate() { + for (bit_idx, bit) in row.iter_mut().enumerate() { + if (col_idx + row_idx + bit_idx) % 3 == 0 { + *bit = f(1); + } + } + } + } + public.bit_slices = + Some(flatten_bit_columns(public_bits, SHA_WORD_BITS, SHA_ROW_VARS, "public").unwrap()); + + let row_weights = (0..SHA_ROW_COUNT) + .map(|row| f(u64::try_from(row % 23 + 1).unwrap())) + .collect::>(); + let fixed = build_linear_residual_coeff_tables_with_row_weights( + &[trace.clone()], + &[public.clone()], + &row_weights, + &cfg, + ) + .unwrap(); + + let constants = ShaResidualPolyConstants::new(&cfg); + let mut expected = vec![DynamicPolynomialF::ZERO; NUM_SHA_RESIDUAL_FAMILIES]; + for (row, row_weight) in row_weights.iter().enumerate() { + let residuals = + residual_polys_at_row_with_constants(&trace, &public, row, &constants, &cfg) + .unwrap(); + for (family_idx, residual) in residuals.iter().enumerate() { + add_scaled_poly_assign(&mut expected[family_idx], residual, row_weight); + } + } + expected.iter_mut().for_each(DynamicPolynomialF::trim); + + assert_eq!(fixed[0].coeffs, expected); + } + #[test] fn dmr_fresh_sha_targets_match_reference_evaluation() { let cfg = test_config(); diff --git a/zip-plus/src/pcs/hyrax.rs b/zip-plus/src/pcs/hyrax.rs index 0820b206..695c056b 100644 --- a/zip-plus/src/pcs/hyrax.rs +++ b/zip-plus/src/pcs/hyrax.rs @@ -235,6 +235,268 @@ where Ok(()) } + + /// Open two folded Hyrax commitments that share the same row bases as one + /// mixed single-row proof. + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::too_many_arguments)] + pub fn prove_open_two_field_lane_groups_single_row( + transcript: &mut PcsProverTranscript, + ck_a: &HyraxCommitmentKey, + field_lanes_a: &[Vec<&[F]>], + prover_data_a: &HyraxProverData, + ck_b: &HyraxCommitmentKey, + field_lanes_b: &[Vec<&[F]>], + prover_data_b: &HyraxProverData, + point: &[F], + field_cfg: &F::Config, + ) -> Result<(), ZipError> + where + F: HyraxFieldBridge + DelayedFieldProductSum, + F::Inner: Transcribable, + F::Modulus: Transcribable, + { + let _ = CHECK_FOR_OVERFLOW; + if field_lanes_a.is_empty() || field_lanes_b.is_empty() { + return Err(ZipError::InvalidPcsParam( + "Hyrax mixed field-lane opening expects two non-empty groups".to_string(), + )); + } + validate_field_lanes::(ck_a, field_lanes_a, point.len(), prover_data_a)?; + validate_field_lanes::(ck_b, field_lanes_b, point.len(), prover_data_b)?; + validate_shared_commitment_keys(ck_a, ck_b)?; + if prover_data_a.num_rows != 1 || prover_data_b.num_rows != 1 { + return Err(ZipError::InvalidPcsParam( + "Hyrax mixed field-lane opening requires a single row".to_string(), + )); + } + + let q1 = eq_tensor_f::(point, field_cfg); + let alpha_count_a = field_lanes_a.len() * prover_data_a.num_lanes; + let alpha_count_b = field_lanes_b.len() * prover_data_b.num_lanes; + let alphas = + sample_scalars::(&mut transcript.fs_transcript, alpha_count_a + alpha_count_b); + let alpha_fields = alphas + .iter() + .map(|alpha| F::scalar_to_field(alpha, field_cfg)) + .collect::, _>>()?; + + let mut combined_row = vec![F::zero_with_cfg(field_cfg); ck_a.num_cols]; + let mut rho_star = C::ScalarField::zero(); + for (poly_idx, lanes) in field_lanes_a.iter().enumerate() { + for (lane, values) in lanes.iter().enumerate() { + let alpha_idx = alpha_index_dynamic(prover_data_a.num_lanes, poly_idx, lane); + let alpha = &alpha_fields[alpha_idx]; + for (acc, value) in combined_row.iter_mut().zip(values.iter()) { + *acc += value.clone() * alpha.clone(); + } + if ck_a.blinding_mode.is_blinded() { + let blind_idx = commitment_index_dynamic( + prover_data_a.num_lanes, + poly_idx, + lane, + 0, + prover_data_a.num_rows, + ); + rho_star += alphas[alpha_idx] * prover_data_a.blinds[blind_idx]; + } + } + } + for (poly_idx, lanes) in field_lanes_b.iter().enumerate() { + for (lane, values) in lanes.iter().enumerate() { + let local_alpha_idx = alpha_index_dynamic(prover_data_b.num_lanes, poly_idx, lane); + let alpha_idx = alpha_count_a + local_alpha_idx; + let alpha = &alpha_fields[alpha_idx]; + for (acc, value) in combined_row.iter_mut().zip(values.iter()) { + *acc += value.clone() * alpha.clone(); + } + if ck_b.blinding_mode.is_blinded() { + let blind_idx = commitment_index_dynamic( + prover_data_b.num_lanes, + poly_idx, + lane, + 0, + prover_data_b.num_rows, + ); + rho_star += alphas[alpha_idx] * prover_data_b.blinds[blind_idx]; + } + } + } + + let b = F::delayed_sum_of_products(&combined_row, &q1, F::zero_with_cfg(field_cfg)); + transcript.write_field_elements(&[b])?; + + let combined_scalars = combined_row + .iter() + .map(F::field_to_scalar) + .collect::, _>>()?; + write_scalars::(transcript, &combined_scalars)?; + if ck_a.blinding_mode.is_blinded() { + write_scalar::(transcript, &rho_star)?; + } + + Ok(()) + } + + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::too_many_arguments)] + pub fn verify_open_two_field_lane_groups_single_row< + F, + EvalA, + LanesA, + EvalB, + LanesB, + const CHECK_FOR_OVERFLOW: bool, + const D: usize, + >( + transcript: &mut PcsVerifierTranscript, + vk_a: &HyraxVerifierKey, + commitment_a: &HyraxCommitment, + lifted_evals_a: &[DynamicPolynomialF], + vk_b: &HyraxVerifierKey, + commitment_b: &HyraxCommitment, + lifted_evals_b: &[DynamicPolynomialF], + point: &[F], + opening_proof: &[u8], + field_cfg: &F::Config, + ) -> Result<(), ZipError> + where + F: HyraxFieldBridge, + F::Inner: Transcribable, + F::Modulus: Transcribable, + EvalA: Clone + Debug + Send + Sync, + EvalB: Clone + Debug + Send + Sync, + LanesA: HyraxLanes, + LanesB: HyraxLanes, + { + let _ = CHECK_FOR_OVERFLOW; + let original_stream = + std::mem::replace(&mut transcript.stream, Cursor::new(opening_proof.to_vec())); + let result = (|| { + if commitment_a.blinding_mode != vk_a.blinding_mode + || commitment_b.blinding_mode != vk_b.blinding_mode + { + return Err(ZipError::InvalidPcsParam( + "Hyrax commitment blinding mode mismatch".to_string(), + )); + } + validate_commitment_shape::(commitment_a)?; + validate_commitment_shape::(commitment_b)?; + validate_shared_verifier_keys(vk_a, vk_b)?; + if lifted_evals_a.len() != commitment_a.batch_size { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax verifier expected {} left lifted evals, got {}", + commitment_a.batch_size, + lifted_evals_a.len() + ))); + } + if lifted_evals_b.len() != commitment_b.batch_size { + return Err(ZipError::InvalidPcsParam(format!( + "Hyrax verifier expected {} right lifted evals, got {}", + commitment_b.batch_size, + lifted_evals_b.len() + ))); + } + if commitment_a.batch_size == 0 || commitment_b.batch_size == 0 { + return Err(ZipError::InvalidPcsParam( + "Hyrax mixed opening expects two non-empty commitment groups".to_string(), + )); + } + + let n = 1usize << point.len(); + let expected_rows = num_rows(n, vk_a.num_cols)?; + if expected_rows != 1 || commitment_a.num_rows != 1 || commitment_b.num_rows != 1 { + return Err(ZipError::InvalidPcsParam( + "Hyrax mixed opening verifier requires a single row".to_string(), + )); + } + + let point_scalar = point + .iter() + .map(F::field_to_scalar) + .collect::, _>>()?; + let q1_scalar = eq_tensor_scalar::(&point_scalar); + let alpha_count_a = commitment_a.batch_size * commitment_a.num_lanes; + let alpha_count_b = commitment_b.batch_size * commitment_b.num_lanes; + let alphas = + sample_scalars::(&mut transcript.fs_transcript, alpha_count_a + alpha_count_b); + + let b_f = transcript.read_field_elements::(1)?; + let mut expected_eval = F::zero_with_cfg(field_cfg); + for (poly_idx, lifted_eval) in lifted_evals_a.iter().enumerate() { + for lane in 0..commitment_a.num_lanes { + let alpha_idx = alpha_index_dynamic(commitment_a.num_lanes, poly_idx, lane); + let alpha = F::scalar_to_field(&alphas[alpha_idx], field_cfg)?; + let mut term = LanesA::lifted_eval::(lifted_eval, lane, field_cfg)?; + term *= α + expected_eval += &term; + } + } + for (poly_idx, lifted_eval) in lifted_evals_b.iter().enumerate() { + for lane in 0..commitment_b.num_lanes { + let local_alpha_idx = + alpha_index_dynamic(commitment_b.num_lanes, poly_idx, lane); + let alpha_idx = alpha_count_a + local_alpha_idx; + let alpha = F::scalar_to_field(&alphas[alpha_idx], field_cfg)?; + let mut term = LanesB::lifted_eval::(lifted_eval, lane, field_cfg)?; + term *= α + expected_eval += &term; + } + } + if b_f[0] != expected_eval { + return Err(ZipError::InvalidPcsOpen( + "Hyrax mixed evaluation consistency failure".to_string(), + )); + } + + let b_scalar = F::field_to_scalar(&b_f[0])?; + let combined_row = read_scalars::(transcript, vk_a.num_cols)?; + let rho_star = if vk_a.blinding_mode.is_blinded() { + Some(read_scalar::(transcript)?) + } else { + None + }; + + let mut lhs = C::ScalarField::zero(); + for (value, weight) in combined_row.iter().zip(q1_scalar.iter()) { + lhs += *value * weight; + } + if lhs != b_scalar { + return Err(ZipError::InvalidPcsOpen( + "Hyrax mixed row coherence failure".to_string(), + )); + } + + let mut comm_bases = + Vec::with_capacity(commitment_a.comm_affine.len() + commitment_b.comm_affine.len()); + comm_bases.extend_from_slice(&commitment_a.comm_affine); + comm_bases.extend_from_slice(&commitment_b.comm_affine); + let comm_lc = msm_unchecked::(&comm_bases, &alphas)?; + + let mut expected = + msm_unchecked::(&vk_a.bases[..combined_row.len()], &combined_row)?; + if let Some(rho_star) = rho_star { + expected += vk_a.h * rho_star; + } + + if comm_lc != expected { + return Err(ZipError::InvalidPcsOpen( + "Hyrax mixed commitment opening failure".to_string(), + )); + } + + Ok(()) + })(); + let consumed = transcript.stream.position() == opening_proof.len() as u64; + transcript.stream = original_stream; + result?; + if !consumed { + return Err(ZipError::InvalidPcsOpen( + "PCS mixed opening proof has trailing bytes".to_string(), + )); + } + Ok(()) + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -497,28 +759,25 @@ impl HyraxLanes, D> for BinaryLa let lower = row_idx * ck.num_cols; let upper = (lower + ck.num_cols).min(poly.evaluations.len()); let row_len = upper - lower; - let mut row_comms = BoolSubsetMsm::<6>::msm_bool_rows_from_window_masks::< - C, - D, - _, - >( - &ck.msm_ck, - row_len, - use_inner_parallelism, - |offset, len| { - let mut masks = [0usize; D]; - for bit_idx in 0..len { - let eval = &poly.evaluations[lower + offset + bit_idx]; - for (lane, mask) in masks.iter_mut().enumerate() { - if eval.coeff(lane) { - *mask |= 1usize << bit_idx; + let mut row_comms = + BoolSubsetMsm::<6>::msm_bool_rows_from_window_masks::( + &ck.msm_ck, + row_len, + use_inner_parallelism, + |offset, len| { + let mut masks = [0usize; D]; + for bit_idx in 0..len { + let eval = &poly.evaluations[lower + offset + bit_idx]; + for (lane, mask) in masks.iter_mut().enumerate() { + if eval.coeff(lane) { + *mask |= 1usize << bit_idx; + } } } - } - masks - }, - ) - .map_err(msm_err)?; + masks + }, + ) + .map_err(msm_err)?; if ck.blinding_mode.is_blinded() { for (lane, row_comm) in row_comms.iter_mut().enumerate() { @@ -1440,6 +1699,39 @@ fn same_prover_data_shape( && lhs.blinds.len() == rhs.blinds.len() } +fn validate_shared_commitment_keys( + lhs: &HyraxCommitmentKey, + rhs: &HyraxCommitmentKey, +) -> Result<(), ZipError> { + if lhs.num_cols != rhs.num_cols + || lhs.blinding_mode != rhs.blinding_mode + || lhs.msm_ck.num_cols != rhs.msm_ck.num_cols + || lhs.msm_ck.bases != rhs.msm_ck.bases + || lhs.msm_ck.h != rhs.msm_ck.h + { + return Err(ZipError::InvalidPcsParam( + "Hyrax mixed opening requires shared commitment bases".to_string(), + )); + } + Ok(()) +} + +fn validate_shared_verifier_keys( + lhs: &HyraxVerifierKey, + rhs: &HyraxVerifierKey, +) -> Result<(), ZipError> { + if lhs.num_cols != rhs.num_cols + || lhs.blinding_mode != rhs.blinding_mode + || lhs.bases != rhs.bases + || lhs.h != rhs.h + { + return Err(ZipError::InvalidPcsParam( + "Hyrax mixed opening requires shared verifier bases".to_string(), + )); + } + Ok(()) +} + fn validate_trusted_bases( width: usize, bases: &[C],

8r`Gv+be^V|`QMy>#)7m( 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

yNRQ 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