diff --git a/piop/src/lookup/logup.rs b/piop/src/lookup/logup.rs new file mode 100644 index 00000000..e8092330 --- /dev/null +++ b/piop/src/lookup/logup.rs @@ -0,0 +1,597 @@ +//! LogUp sumcheck group construction and verifier finalization. +//! +//! Provides building blocks for the LogUp lookup argument within the +//! Zinc+ multi-degree sumcheck pipeline. The LogUp identity is: +//! +//! ```text +//! Σ_i 1/(β − w_i) = Σ_j m_j/(β − T_j) +//! ``` +//! +//! The prover commits multiplicities `m` and inverse witnesses +//! `u = 1/(β − w)` via Zip+ PCS. The table inverse `v = 1/(β − T)` is +//! NOT committed — the verifier computes it from the public table. +//! +//! ## γ-Batched sumcheck groups +//! +//! For L witness columns sharing the same table, a random challenge γ +//! collapses 2·L individual groups into exactly **2 groups**: +//! +//! - **Group 0** (zerocheck, degree 3): `(Σ_l γ^l · (d_l·u_l − 1)) · eq(r, y)` +//! where `d_l = β − w_l`. Enforces `u_l = 1/(β − w_l)` pointwise for all L +//! columns. +//! - **Group 1** (sumcheck, degree 2, claimed_sum = 0): `Σ_l γ^l · (u_l − +//! m_l·v)`. Enforces `Σ u_l = Σ m_l·v` for all L columns simultaneously. +//! +//! When L=1 this degenerates to the unbatched case (γ^0 = 1). +//! +//! ## Committed model +//! +//! In the full pipeline (`protocol/src/prover.rs`), m and u are committed +//! via PCS and opened at `r_0` through the multipoint eval sumcheck. +//! The functions here build sumcheck groups and check subclaim evaluations. + +use crypto_primitives::FromPrimitiveWithConfig; +use num_traits::Zero; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use std::marker::PhantomData; +use zinc_poly::{ + mle::DenseMultilinearExtension, + utils::{build_eq_x_r_inner, build_eq_x_r_vec, eq_eval}, +}; +use zinc_transcript::traits::{ConstTranscribable, Transcript}; +use zinc_uair::LookupTableType; +use zinc_utils::{cfg_iter, inner_transparent_field::InnerTransparentField, log2}; + +use crate::{ + lookup::{ + LogupFinalizerInput, LogupProverAncillary, LookupGroup, utils::batch_inverse_shifted, + }, + sumcheck::multi_degree::MultiDegreeSumcheckGroup, +}; + +use super::{ + structs::{LogupVerifierPreSumcheckData, LookupError}, + utils::{generate_bitpoly_table, generate_word_table}, +}; + +/// LogUp sumcheck group builder and verifier finalizer. +pub struct LogupProtocol(PhantomData); + +impl LogupProtocol { + /// Build two γ-batched LogUp sumcheck groups from L columns. + /// + /// Takes L witnesses and their auxiliary vectors. Returns exactly + /// 2 groups regardless of L: `[group_0, group_1]` + /// + /// - Group 0 (degree 3, zerocheck): `(Σ_l γ^l · (d_l·u_l − 1)) · eq(r, y)` + /// - Group 1 (degree 2, sumcheck, claimed_sum = 0): `Σ_l γ^l · (u_l − + /// m_l·v)` + /// + /// When L=1, γ^0=1 so this degenerates to the unbatched case. + /// The caller is responsible for PCS commitment, transcript operations, + /// and squeezing β and r before calling this function. + #[allow(clippy::arithmetic_side_effects)] + pub fn build_sumcheck_groups( + witnesses: &[&[F]], + table: &[F], + auxs: &[LogupProverAncillary<'_, F>], + beta: &F, + gamma: &F, + r: &[F], + field_cfg: &F::Config, + ) -> Result>, LookupError> + where + F::Inner: Zero + Default + Send + Sync, + F: 'static, + { + let num_cols = witnesses.len(); + assert!(num_cols > 0, "at least one witness column is required"); + assert_eq!( + num_cols, + auxs.len(), + "witnesses and auxs must have same length" + ); + assert!( + witnesses.iter().all(|w| w.len() == witnesses[0].len()), + "all witness columns must have the same length" + ); + + let zero = F::zero_with_cfg(field_cfg); + let one = F::one_with_cfg(field_cfg); + + let w_num_vars = log2(witnesses[0].len().next_power_of_two()) as usize; + let t_num_vars = log2(table.len().next_power_of_two()) as usize; + let num_vars = w_num_vars.max(t_num_vars); + + let eq_r = build_eq_x_r_inner(r, field_cfg)?; + + // ---- Build MLEs ---- + let inner_zero = zero.inner().clone(); + let mk_mle = |data: &[F]| -> DenseMultilinearExtension { + DenseMultilinearExtension::from_evaluations_vec( + num_vars, + cfg_iter!(data).map(|x| x.inner().clone()).collect(), + inner_zero.clone(), + ) + }; + + let beta_inner = beta.inner(); + + let gamma_pows = zinc_utils::powers(gamma.clone(), one.clone(), num_cols); + + // Group 0 MLEs: [eq, d_0, u_0, d_1, u_1, ...] where + // d = (β − w): denominator for witness inverse check + let mut group0_mles = Vec::with_capacity(1 + 2 * num_cols); + group0_mles.push(eq_r); + for (witness, aux) in witnesses.iter().zip(auxs.iter()) { + let d_mle = DenseMultilinearExtension::from_evaluations_vec( + num_vars, + cfg_iter!(*witness) + .map(|w_i| F::sub_inner(beta_inner, w_i.inner(), field_cfg)) + .collect(), + inner_zero.clone(), + ); + group0_mles.push(d_mle); + group0_mles.push(mk_mle(aux.inverse_witness)); + } + + // Group 1 MLEs: [u_0, m_0, u_1, m_1, ..., v] + let mut group1_mles = Vec::with_capacity(2 * num_cols + 1); + for aux in auxs.iter() { + group1_mles.push(mk_mle(aux.inverse_witness)); + group1_mles.push(mk_mle(aux.multiplicities)); + } + group1_mles.push(mk_mle(auxs[0].inverse_table)); + + // group0: (d_l·u_l − 1) · eq where d_l = (β − w_l) + let one_g0 = one.clone(); + let zero_g0 = zero.clone(); + let gamma_powers_g0 = gamma_pows.clone(); + let l0 = num_cols; + let group_0 = MultiDegreeSumcheckGroup::new( + 3, + group0_mles, + Box::new(move |v: &[F]| { + // v[0] = eq, v[1+2l] = d_l, v[2+2l] = u_l + let mut sum = zero_g0.clone(); + for l in 0..l0 { + let d = &v[1 + 2 * l]; + let u = &v[2 + 2 * l]; + let term = d.clone() * u - &one_g0; + sum += &(gamma_powers_g0[l].clone() * &term); + } + sum * &v[0] + }), + ); + + // group1: (u − m·v) + let zero_g1 = zero; + let gp1 = gamma_pows; + let l1 = num_cols; + let group_1 = MultiDegreeSumcheckGroup::new( + 2, + group1_mles, + Box::new(move |v: &[F]| { + // v[2l] = u_l, v[2l+1] = m_l, v[2*L] = v (shared) + let v_shared = &v[2 * l1]; + let mut sum = zero_g1.clone(); + for l in 0..l1 { + let u = &v[2 * l]; + let m = &v[2 * l + 1]; + let term = u.clone() - &(m.clone() * v_shared); + sum += &(gp1[l].clone() * &term); + } + sum + }), + ); + + Ok(vec![group_0, group_1]) + } + + /// Extract witness columns and the shared lookup table for a group. + /// + /// Returns `(witnesses, table)` where `witnesses[i]` corresponds to + /// `group_info.column_indices[i]`. The table is generated from + /// `group_info.table_type`. + pub fn extract_witnesses_and_table( + projected_trace_f: &[DenseMultilinearExtension], + group_info: &super::LookupGroup, + projecting_element_f: &F, + field_cfg: &F::Config, + ) -> (Vec>, Vec) { + let witnesses: Vec> = group_info + .column_indices + .iter() + .map(|&col_idx| { + projected_trace_f[col_idx] + .iter() + .map(|inner| F::new_unchecked_with_cfg(inner.clone(), field_cfg)) + .collect() + }) + .collect(); + + let table: Vec = match &group_info.table_type { + LookupTableType::BitPoly { width, .. } => { + generate_bitpoly_table(*width, projecting_element_f, field_cfg) + } + LookupTableType::Word { width, .. } => generate_word_table(*width, field_cfg), + }; + + (witnesses, table) + } + + /// Pre-sumcheck transcript sync for LogUp. + /// + /// Absorbs PCS commitment roots into the transcript + /// and squeezes the challenges β and r, mirroring the prover's + /// `prepare_lookup_groups`. Must run BEFORE the multi-degree sumcheck + /// verify to keep the transcript in sync. + pub fn build_verifier_pre_sumcheck( + transcript: &mut impl Transcript, + comm_m_root: &[u8], + comm_u_root: &[u8], + num_vars: usize, + field_cfg: &F::Config, + ) -> LogupVerifierPreSumcheckData + where + F::Inner: ConstTranscribable, + { + // Absorb multiplicity commitment root, squeeze β + transcript.absorb_slice(comm_m_root); + let beta: F = transcript.get_field_challenge(field_cfg); + + // Absorb inverse witness root, squeeze r then γ + transcript.absorb_slice(comm_u_root); + let r: Vec = transcript.get_field_challenges(num_vars, field_cfg); + let gamma: F = transcript.get_field_challenge(field_cfg); + + LogupVerifierPreSumcheckData { r, beta, gamma } + } + + /// Post-sumcheck finalization for the γ-batched LogUp verifier. + /// + /// Given the subclaim point `x*` and two expected evaluations (one + /// per γ-batched group), checks: + /// + /// - Group 0: `Σ_l γ^l · (d_l·u_l − 1) · eq_val == expected[0]` where d_l = + /// (β − w_l) + /// - Group 1: `Σ_l γ^l · (u_l − m_l·v_eval) == expected[1]` + /// + /// `v_eval` is computed once from the public table + β. + #[allow(clippy::arithmetic_side_effects)] + pub fn finalize_verifier( + pre_sumcheck_data: &LogupVerifierPreSumcheckData, + input: LogupFinalizerInput<'_, F>, + group_info: &LookupGroup, + projecting_element_f: &F, + field_cfg: &F::Config, + ) -> Result<(), LookupError> + where + F::Inner: ConstTranscribable + Zero, + { + let one = F::one_with_cfg(field_cfg); + let LogupFinalizerInput { + subclaim_point, + expected_evaluations, + w_evals, + aux_evals, + } = input; + let LogupVerifierPreSumcheckData { r, beta, gamma } = pre_sumcheck_data; + let num_cols = w_evals.len(); + if num_cols != aux_evals.len() { + return Err(LookupError::EvalLengthMismatch { + w: num_cols, + aux: aux_evals.len(), + }); + } + if expected_evaluations.len() < 2 { + return Err(LookupError::WrongEvaluationCount { + expected: 2, + got: expected_evaluations.len(), + }); + } + + // Evaluate eq(x*, r) + let eq_val = eq_eval(subclaim_point, r, one.clone())?; + + // Compute v_eval from public table + let table: Vec = match &group_info.table_type { + LookupTableType::BitPoly { width, .. } => { + generate_bitpoly_table(*width, projecting_element_f, field_cfg) + } + LookupTableType::Word { width, .. } => generate_word_table(*width, field_cfg), + }; + + let eq_at_point = build_eq_x_r_vec(subclaim_point, field_cfg)?; + let zero = F::zero_with_cfg(field_cfg); + + // Panics if β equals a table entry. β is a Fiat-Shamir challenge, + // so collision probability is ~1/q (negligible for ≥128-bit fields). + let v = batch_inverse_shifted(beta, &table); + let v_eval: F = v + .iter() + .zip(eq_at_point.iter()) + .fold(zero.clone(), |acc, (v, e)| acc + &(v.clone() * e)); + + let gamma_pows = zinc_utils::powers(gamma.clone(), one.clone(), num_cols); + + // Group 0: Σ_l γ^l · (d_l·u_l − 1) · eq_val, where d_l = (β − w_l) + let mut computed_0 = zero.clone(); + for l in 0..num_cols { + let w_l = &w_evals[l]; + let u_l = &aux_evals[l].u_eval; + let term = (beta.clone() - w_l) * u_l - &one; + computed_0 += &(gamma_pows[l].clone() * &term); + } + computed_0 *= &eq_val; + if computed_0 != expected_evaluations[0] { + return Err(LookupError::FinalEvaluationMismatch); + } + + // Group 1: Σ_l γ^l · (u_l − m_l · v_eval) + let mut computed_1 = zero; + for l in 0..num_cols { + let u_l = &aux_evals[l].u_eval; + let m_l = &aux_evals[l].m_eval; + let term = u_l.clone() - &(m_l.clone() * &v_eval); + computed_1 += &(gamma_pows[l].clone() * &term); + } + if computed_1 != expected_evaluations[1] { + return Err(LookupError::FinalEvaluationMismatch); + } + + Ok(()) + } +} + +#[cfg(test)] +#[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] +mod tests { + use super::*; + use crate::sumcheck::multi_degree::MultiDegreeSumcheck; + use crypto_bigint::{U128, const_monty_params}; + use crypto_primitives::crypto_bigint_const_monty::ConstMontyField; + use num_traits::ConstZero; + use zinc_poly::mle::MultilinearExtensionWithConfig; + use zinc_transcript::Blake3Transcript; + + use super::super::{ + LogupFinalizerInput, LookupAuxEvals, LookupGroup, + utils::{batch_inverse_shifted, compute_multiplicities}, + }; + + const N: usize = 2; + const_monty_params!(TestParams, U128, "00000000b933426489189cb5b47d567f"); + type F = ConstMontyField; + + fn make_transcript() -> Blake3Transcript { + let mut t = Blake3Transcript::default(); + t.absorb_slice(b"logup-test"); + t + } + + struct LogupTestHarness { + w_evals: Vec, + aux_evals: Vec>, + pre: LogupVerifierPreSumcheckData, + expected_evaluations: Vec, + x_star: Vec, + group_info: LookupGroup, + } + + fn setup_logup_harness(witnesses: &[Vec], table: &[F]) -> LogupTestHarness { + let num_vars = zinc_utils::log2(witnesses[0].len().next_power_of_two()) as usize; + let num_cols = witnesses.len(); + let beta = F::from(17u32); + let gamma = F::from(7u32); + + let mut m_vecs = Vec::with_capacity(num_cols); + let mut u_vecs = Vec::with_capacity(num_cols); + let v = batch_inverse_shifted(&beta, table); + + for w in witnesses { + let m = compute_multiplicities(w, table, &()).expect("witness should be in table"); + let u = batch_inverse_shifted(&beta, w); + m_vecs.push(m); + u_vecs.push(u); + } + + let r: Vec = (0..num_vars).map(|i| F::from((i + 3) as u32)).collect(); + + let witness_refs: Vec<&[F]> = witnesses.iter().map(|w| w.as_slice()).collect(); + let auxs: Vec> = m_vecs + .iter() + .zip(u_vecs.iter()) + .map(|(m, u)| super::super::LogupProverAncillary { + multiplicities: m, + inverse_witness: u, + inverse_table: &v, + }) + .collect(); + + let groups = LogupProtocol::build_sumcheck_groups( + &witness_refs, + table, + &auxs, + &beta, + &gamma, + &r, + &(), + ) + .expect("build_sumcheck_groups should succeed"); + + assert_eq!(groups.len(), 2); + + let mut pt = make_transcript(); + let (md_proof, _) = + MultiDegreeSumcheck::prove_as_subprotocol(&mut pt, groups, num_vars, &()); + let mut vt = make_transcript(); + let md_sub = MultiDegreeSumcheck::verify_as_subprotocol(&mut vt, num_vars, &md_proof, &()) + .expect("sumcheck verify should succeed"); + + let x_star = md_sub.point().to_vec(); + let inner_zero = F::ZERO.into_inner(); + let mk_mle = |data: &[F]| -> DenseMultilinearExtension<_> { + DenseMultilinearExtension::from_evaluations_vec( + num_vars, + data.iter().map(|f| f.into_inner()).collect(), + inner_zero, + ) + }; + + let w_evals: Vec = witnesses + .iter() + .map(|w| mk_mle(w).evaluate_with_config(&x_star, &()).unwrap()) + .collect(); + let aux_evals: Vec> = m_vecs + .iter() + .zip(u_vecs.iter()) + .map(|(m, u)| LookupAuxEvals { + u_eval: mk_mle(u).evaluate_with_config(&x_star, &()).unwrap(), + m_eval: mk_mle(m).evaluate_with_config(&x_star, &()).unwrap(), + }) + .collect(); + + let table_width = zinc_utils::log2(table.len().next_power_of_two()) as usize; + LogupTestHarness { + w_evals, + aux_evals, + pre: LogupVerifierPreSumcheckData { r, beta, gamma }, + expected_evaluations: md_sub.expected_evaluations().to_vec(), + x_star, + group_info: LookupGroup { + table_type: LookupTableType::Word { + width: table_width, + chunk_width: None, + }, + column_indices: (0..num_cols).collect(), + }, + } + } + + impl LogupTestHarness { + fn finalize_with( + &self, + w_evals: &[F], + aux_evals: &[LookupAuxEvals], + ) -> Result<(), LookupError> { + let fin = LogupFinalizerInput { + subclaim_point: &self.x_star, + expected_evaluations: &self.expected_evaluations, + w_evals, + aux_evals, + }; + LogupProtocol::::finalize_verifier(&self.pre, fin, &self.group_info, &F::ZERO, &()) + } + + fn finalize_honest(&self) -> Result<(), LookupError> { + self.finalize_with(&self.w_evals, &self.aux_evals) + } + } + + /// Full γ-batched LogUp pipeline: `setup_logup_harness` then honest + /// finalize. + fn run_logup_roundtrip(witnesses: &[Vec], table: &[F]) { + setup_logup_harness(witnesses, table) + .finalize_honest() + .expect("finalize_verifier should succeed"); + } + + #[test] + fn logup_roundtrip_small() { + let table: Vec = (0..4u32).map(F::from).collect(); + let witness: Vec = vec![0u32, 1, 1, 3].into_iter().map(F::from).collect(); + run_logup_roundtrip(&[witness], &table); + } + + #[test] + fn logup_roundtrip_all_same() { + let table: Vec = (0..4u32).map(F::from).collect(); + let witness: Vec = vec![2u32; 4].into_iter().map(F::from).collect(); + run_logup_roundtrip(&[witness], &table); + } + + #[test] + fn logup_roundtrip_full_table() { + let table: Vec = (0..8u32).map(F::from).collect(); + let witness: Vec = (0..8u32).map(F::from).collect(); + run_logup_roundtrip(&[witness], &table); + } + + #[test] + fn logup_roundtrip_multi_column() { + let table: Vec = (0..4u32).map(F::from).collect(); + let w0: Vec = vec![0u32, 1, 1, 3].into_iter().map(F::from).collect(); + let w1: Vec = vec![3u32, 3, 0, 2].into_iter().map(F::from).collect(); + let w2: Vec = vec![1u32, 2, 2, 0].into_iter().map(F::from).collect(); + run_logup_roundtrip(&[w0, w1, w2], &table); + } + + #[test] + fn logup_reject_invalid_witness() { + let table: Vec = (0..4u32).map(F::from).collect(); + let witness: Vec = vec![0u32, 5].into_iter().map(F::from).collect(); + + let result = compute_multiplicities(&witness, &table, &()); + assert!( + result.is_none(), + "witness entry 5 not in table should return None" + ); + } + + fn single_col_harness() -> LogupTestHarness { + let table: Vec = (0..4u32).map(F::from).collect(); + let witness: Vec = vec![0u32, 1, 1, 3].into_iter().map(F::from).collect(); + setup_logup_harness(&[witness], &table) + } + + fn two_col_harness() -> LogupTestHarness { + let table: Vec = (0..4u32).map(F::from).collect(); + let w0: Vec = vec![0u32, 1, 1, 3].into_iter().map(F::from).collect(); + let w1: Vec = vec![3u32, 3, 0, 2].into_iter().map(F::from).collect(); + setup_logup_harness(&[w0, w1], &table) + } + + #[test] + fn logup_wrong_u_eval_rejected() { + let h = single_col_harness(); + assert!(h.finalize_honest().is_ok()); + let mut bad = h.aux_evals.clone(); + bad[0].u_eval += F::from(1u32); + assert!(h.finalize_with(&h.w_evals, &bad).is_err()); + } + + #[test] + fn logup_wrong_m_eval_rejected() { + let h = single_col_harness(); + let mut bad = h.aux_evals.clone(); + bad[0].m_eval += F::from(1u32); + assert!(h.finalize_with(&h.w_evals, &bad).is_err()); + } + + #[test] + fn logup_wrong_w_eval_rejected() { + let h = single_col_harness(); + let mut bad_w = h.w_evals.clone(); + bad_w[0] += F::from(1u32); + assert!(h.finalize_with(&bad_w, &h.aux_evals).is_err()); + } + + #[test] + fn logup_wrong_gamma_rejected() { + let mut h = two_col_harness(); + assert!(h.finalize_honest().is_ok()); + h.pre.gamma = F::from(999u32); + assert!(h.finalize_honest().is_err()); + } + + #[test] + fn logup_multi_col_wrong_single_aux_rejected() { + let h = two_col_harness(); + assert!(h.finalize_honest().is_ok()); + let mut bad = h.aux_evals.clone(); + bad[1].m_eval += F::from(1u32); + assert!(h.finalize_with(&h.w_evals, &bad).is_err()); + } +} diff --git a/piop/src/lookup/mod.rs b/piop/src/lookup/mod.rs index 54830ae6..b95bd5ca 100644 --- a/piop/src/lookup/mod.rs +++ b/piop/src/lookup/mod.rs @@ -1,4 +1,5 @@ //! Lookup argument for the Zinc+ PIOP. +pub mod logup; pub mod structs; - +pub mod utils; pub use structs::*; diff --git a/piop/src/lookup/structs.rs b/piop/src/lookup/structs.rs index 87cea7c5..29714ff3 100644 --- a/piop/src/lookup/structs.rs +++ b/piop/src/lookup/structs.rs @@ -1,12 +1,13 @@ //! Data structures for the lookup protocol. //! -//! Proof types, prover/verifier intermediates, instance types, and error +//! Proof types, prover/verifier intermediates and error //! definitions live here. Specification types (`LookupTableType`, //! `LookupColumnSpec`) live in `zinc-uair` and are re-exported from the //! parent module. use std::collections::BTreeMap; use thiserror::Error; +use zinc_poly::utils::ArithErrors; use zinc_uair::LookupTableType; // --------------------------------------------------------------------------- @@ -90,6 +91,74 @@ pub struct BatchedLookupProof { pub group_meta: Vec, } +// --------------------------------------------------------------------------- +// Core LogUp types +// --------------------------------------------------------------------------- + +/// Verifier subclaim from the LogUp protocol. +#[derive(Clone, Debug)] +pub struct LogupVerifierSubClaim { + /// Evaluation point from the sumcheck subclaim. + pub evaluation_point: Vec, + /// Expected evaluation at the subclaim point. + pub expected_evaluation: F, +} + +/// Ancillary prover data for LogUp sumcheck group construction. +/// +/// Borrows the pre-computed auxiliary vectors needed by +/// [`LogupProtocol::build_sumcheck_groups`]. +#[derive(Clone, Debug)] +pub struct LogupProverAncillary<'a, F> { + /// Multiplicity vector. + pub multiplicities: &'a [F], + /// Inverse witness vector `u = 1/(β − w)`. + pub inverse_witness: &'a [F], + /// Inverse table vector `v = 1/(β − T)`. + pub inverse_table: &'a [F], +} + +/// Pre-sumcheck verifier data for the core LogUp protocol. +/// +/// Produced by `build_verifier_pre_sumcheck`, holds the transcript +/// challenges needed for `finalize_verifier`. +#[derive(Clone, Debug)] +pub struct LogupVerifierPreSumcheckData { + /// Random evaluation point `r` for `eq(y, r)`. + pub r: Vec, + /// The β challenge for LogUp. + pub beta: F, + /// The γ challenge for batching multiple lookup columns. + pub gamma: F, +} + +/// Scalar MLE evaluations of lookup auxiliary columns at the subclaim +/// point x*, obtained from PCS openings via MultipointEval. +/// +/// Used by `finalize_verifier` to check the LogUp identities +#[derive(Clone, Debug)] +pub struct LookupAuxEvals { + pub u_eval: F, + pub m_eval: F, +} + +/// Bundle of per-group evaluation data passed to +/// [`LogupProtocol::finalize_verifier`]. +/// +/// Carries L columns' evaluation data for the two γ-batched groups. +#[derive(Clone, Debug)] +pub struct LogupFinalizerInput<'a, F> { + /// The multi-degree sumcheck subclaim point (x*). + pub subclaim_point: &'a [F], + /// Expected evaluations from the sumcheck subclaim (len=2, one + /// per γ-batched group). + pub expected_evaluations: &'a [F], + /// Evaluations of the L witness column polynomials at x*. + pub w_evals: &'a [F], + /// Per-column auxiliary evaluations (u, m) at x*. + pub aux_evals: &'a [LookupAuxEvals], +} + // --------------------------------------------------------------------------- // Grouping utility // --------------------------------------------------------------------------- @@ -129,7 +198,6 @@ pub fn group_lookup_specs(specs: &[zinc_uair::LookupColumnSpec]) -> Vec, usize>; + +/// Generate the projected `BitPoly(w)` table over F_q. +/// +/// For `BitPoly(w)`, the table has `2^w` entries. Each entry corresponds +/// to a binary polynomial `b_{w-1} X^{w-1} + … + b_0` evaluated at the +/// projecting element `a`: +/// +/// ```text +/// T[n] = Σ_{k=0}^{w-1} bit_k(n) · a^k +/// ``` +/// +/// Uses the recursive structure `T[n + 2^k] = T[n] + a^k` for +/// efficient computation. +#[allow(clippy::arithmetic_side_effects)] +pub fn generate_bitpoly_table( + width: usize, + projecting_element: &F, + field_cfg: &F::Config, +) -> Vec { + let size = 1usize << width; + let mut table = vec![F::zero_with_cfg(field_cfg); size]; + + // Precompute powers of `a`: a^0, a^1, ..., a^{w-1}. + let one = F::one_with_cfg(field_cfg); + let mut powers_of_a = Vec::with_capacity(width); + let mut current = one; + for _ in 0..width { + powers_of_a.push(current.clone()); + current *= projecting_element; + } + + // Build the table recursively: T[n + 2^k] = T[n] + a^k. + // Start with T[0] = 0. + // For each bit position k, the entries with bit k set are + // obtained by adding a^k to the entries without bit k set. + for (k, power) in powers_of_a.iter().enumerate() { + let step = 1usize << k; + for n in 0..step { + table[n + step] = table[n].clone() + power; + } + } + + table +} + +/// Generate the projected `Word(w)` table over F_q. +/// +/// For `Word(w)`, the table has `2^w` entries: +/// `{0, 1, …, 2^w − 1} mod q`. +#[allow(clippy::arithmetic_side_effects)] +pub fn generate_word_table( + width: usize, + field_cfg: &F::Config, +) -> Vec +where + F::Config: Sync, +{ + let size = 1usize << width; + cfg_into_iter!(0..size) + .map(|i| F::from_with_cfg(i as u64, field_cfg)) + .collect() +} + +/// Compute batch multiplicative inverses using a two-phase Montgomery trick. +/// +/// Given `values = [v_0, v_1, …, v_{n-1}]`, returns +/// `[v_0^{-1}, v_1^{-1}, …, v_{n-1}^{-1}]`. +/// +/// Performs exactly one field inversion and `O(n)` multiplications, +/// even when parallelised across multiple chunks. The three phases: +/// +/// 1. **Forward (parallel):** each chunk computes local prefix products. +/// 2. **Reduce (sequential, O(K) muls):** Montgomery trick on the K chunk +/// products → one inversion → per-chunk starting accumulators. +/// 3. **Backward (parallel):** each chunk sweeps to produce inverses. +/// +/// # Panics +/// +/// Panics if any element is zero (non-invertible). +#[allow(clippy::arithmetic_side_effects)] +pub fn batch_inverse(values: &[F]) -> Vec +where + F::Config: Sync, +{ + if values.is_empty() { + return Vec::new(); + } + + let n = values.len(); + let cfg = values[0].cfg(); + let one = F::one_with_cfg(cfg); + let chunk_size = cfg_chunk_size!(n, 4096); + + // Phase 1: per-chunk prefix products (parallel). + let prefixes: Vec> = cfg_chunks!(values, chunk_size) + .map(|chunk| { + let mut prefix = Vec::with_capacity(chunk.len()); + prefix.push(chunk[0].clone()); + for i in 1..chunk.len() { + let prev = prefix[i - 1].clone(); + prefix.push(prev * &chunk[i]); + } + prefix + }) + .collect(); + + // Phase 2: single global inversion over chunk products (sequential). + let chunk_inv_accs = reduce_chunk_products(&prefixes, &one); + + // Phase 3: per-chunk backward sweep (parallel). + let mut result = vec![F::zero_with_cfg(cfg); n]; + cfg_chunks_mut!(result, chunk_size) + .zip(cfg_chunks!(values, chunk_size)) + .enumerate() + .for_each(|(k, (res_chunk, val_chunk))| { + let cn = val_chunk.len(); + let prefix = &prefixes[k]; + let mut inv_acc = chunk_inv_accs[k].clone(); + for i in (1..cn).rev() { + res_chunk[i] = inv_acc.clone() * &prefix[i - 1]; + inv_acc *= &val_chunk[i]; + } + res_chunk[0] = inv_acc; + }); + + result +} + +/// Montgomery trick on K chunk products: one inversion, returns the +/// starting `inv_acc` for each chunk's backward sweep. +#[allow(clippy::arithmetic_side_effects)] +fn reduce_chunk_products(prefixes: &[Vec], one: &F) -> Vec { + let k = prefixes.len(); + let last = |i: usize| -> &F { + prefixes[i] + .last() + .expect("chunk prefix is non-empty by construction") + }; + + if k == 1 { + // Panics if the sole chunk product is zero (i.e. a zero input element). + return vec![one.clone() / last(0)]; + } + + let mut global_prefix = Vec::with_capacity(k); + global_prefix.push(last(0).clone()); + for i in 1..k { + let prev = global_prefix[i - 1].clone(); + global_prefix.push(prev * last(i)); + } + + // Single field inversion — panics here if any input to + // `batch_inverse` / `batch_inverse_shifted` was zero (or β − v_i = 0), + // since the global prefix product will be zero. + let mut inv_acc = one.clone() / &global_prefix[k - 1]; + let mut accs = vec![F::zero_with_cfg(one.cfg()); k]; + for i in (1..k).rev() { + accs[i] = inv_acc.clone() * &global_prefix[i - 1]; + inv_acc *= last(i); + } + accs[0] = inv_acc; + accs +} + +/// Build a reusable table index mapping each table entry's byte +/// representation to its position. +/// +/// Use with [`compute_multiplicities_with_index`] to avoid rebuilding +/// the hash map when computing multiplicities for multiple witnesses +/// against the same table. +pub fn build_table_index(table: &[F]) -> TableIndex +where + F::Inner: ConstTranscribable, +{ + table + .iter() + .enumerate() + .map(|(i, t)| { + let mut bytes = vec![0u8; F::Inner::NUM_BYTES]; + t.inner().write_transcription_bytes_exact(&mut bytes); + (bytes, i) + }) + .collect() +} + +/// Compute multiplicity vector using a prebuilt [`TableIndex`]. +/// +/// Returns `m` such that `m[j]` is the number of occurrences of `T[j]` +/// in `witness`. +/// +/// # Errors +/// +/// Returns `None` if any witness entry is not found in the table. +#[allow(clippy::arithmetic_side_effects)] +pub fn compute_multiplicities_with_index( + witness: &[F], + table_index: &TableIndex, + table_len: usize, + field_cfg: &F::Config, +) -> Option> +where + F::Config: Sync, + F::Inner: ConstTranscribable, +{ + if witness.is_empty() { + return Some( + (0..table_len) + .map(|_| F::from_with_cfg(0u64, field_cfg)) + .collect(), + ); + } + + let chunk_size = cfg_chunk_size!(witness.len(), 4096); + + let local_counts: Vec> = cfg_chunks!(witness, chunk_size) + .map(|chunk| { + let mut counts = vec![0u64; table_len]; + let mut bytes = vec![0u8; F::Inner::NUM_BYTES]; + for w in chunk { + w.inner().write_transcription_bytes_exact(&mut bytes); + match table_index.get(&bytes) { + Some(&idx) => counts[idx] += 1, + // Empty vec: valid counts always have + // length table_len ≥ 1, so is_empty() detects this below. + None => return vec![], + } + } + counts + }) + .collect(); + + let mut counts = vec![0u64; table_len]; + for local in &local_counts { + if local.is_empty() { + return None; + } + for (i, &c) in local.iter().enumerate() { + counts[i] += c; + } + } + + Some( + counts + .into_iter() + .map(|c| F::from_with_cfg(c, field_cfg)) + .collect(), + ) +} + +/// Compute multiplicity vector: for each table entry, count how many +/// times it appears in the witness. +/// +/// Returns `m` such that `m[j]` is the number of occurrences of `T[j]` +/// in `witness`. +/// +/// # Errors +/// +/// Returns `None` if any witness entry is not found in the table. +#[allow(clippy::arithmetic_side_effects)] +pub fn compute_multiplicities( + witness: &[F], + table: &[F], + field_cfg: &F::Config, +) -> Option> +where + F::Config: Sync, + F::Inner: ConstTranscribable, +{ + let index = build_table_index(table); + compute_multiplicities_with_index(witness, &index, table.len(), field_cfg) +} + +/// Compute `[1/(β − v_0), 1/(β − v_1), …, 1/(β − v_{n-1})]` with +/// fused subtraction — avoids materializing a separate `β − v_i` vector. +/// +/// Uses the same two-phase Montgomery trick as [`batch_inverse`]: +/// exactly one field inversion, `O(n)` multiplications, parallel +/// forward/backward sweeps. +/// +/// # Panics +/// +/// Panics if any `β − v_i` is zero. +#[allow(clippy::arithmetic_side_effects)] +pub fn batch_inverse_shifted(beta: &F, values: &[F]) -> Vec +where + F::Config: Sync, +{ + if values.is_empty() { + return Vec::new(); + } + + let n = values.len(); + let cfg = beta.cfg(); + let one = F::one_with_cfg(cfg); + let chunk_size = cfg_chunk_size!(n, 4096); + + // Phase 1: per-chunk prefix products of (β − v_i) (parallel). + let prefixes: Vec> = cfg_chunks!(values, chunk_size) + .map(|chunk| { + let mut prefix = Vec::with_capacity(chunk.len()); + prefix.push(beta.clone() - &chunk[0]); + for i in 1..chunk.len() { + let diff = beta.clone() - &chunk[i]; + let prev = prefix[i - 1].clone(); + prefix.push(prev * &diff); + } + prefix + }) + .collect(); + + // Phase 2: single global inversion (sequential). + let chunk_inv_accs = reduce_chunk_products(&prefixes, &one); + + // Phase 3: per-chunk backward sweep (parallel). + let mut result = vec![F::zero_with_cfg(cfg); n]; + cfg_chunks_mut!(result, chunk_size) + .zip(cfg_chunks!(values, chunk_size)) + .enumerate() + .for_each(|(k, (res_chunk, val_chunk))| { + let cn = val_chunk.len(); + let prefix = &prefixes[k]; + let mut inv_acc = chunk_inv_accs[k].clone(); + for i in (1..cn).rev() { + res_chunk[i] = inv_acc.clone() * &prefix[i - 1]; + let diff = beta.clone() - &val_chunk[i]; + inv_acc *= &diff; + } + res_chunk[0] = inv_acc; + }); + + result +} + +#[cfg(test)] +mod tests { + use super::*; + use crypto_bigint::{U128, const_monty_params}; + use crypto_primitives::crypto_bigint_const_monty::ConstMontyField; + + const N: usize = 2; + const_monty_params!(TestParams, U128, "00000000b933426489189cb5b47d567f"); + type F = ConstMontyField; + + #[test] + fn bitpoly_table_size_and_values() { + let a = F::from(3u32); + let table = generate_bitpoly_table(4, &a, &()); + + assert_eq!(table.len(), 16); // 2^4 + + // T[0] = 0 + assert_eq!(table[0], F::from(0u32)); + // T[1] = a^0 = 1 + assert_eq!(table[1], F::from(1u32)); + // T[2] = a^1 = 3 + assert_eq!(table[2], F::from(3u32)); + // T[3] = a^0 + a^1 = 1 + 3 = 4 + assert_eq!(table[3], F::from(4u32)); + // T[5] = a^0 + a^2 = 1 + 9 = 10 + assert_eq!(table[5], F::from(10u32)); + } + + #[test] + fn word_table_values() { + let table = generate_word_table::(4, &()); + + assert_eq!(table.len(), 16); + for i in 0..16u32 { + assert_eq!(table[i as usize], F::from(i)); + } + } + + #[test] + fn batch_inverse_correctness() { + let values: Vec = (1..=5u32).map(F::from).collect(); + let inverses = batch_inverse(&values); + + let one = F::from(1u32); + for (v, inv) in values.iter().zip(inverses.iter()) { + assert_eq!(v * inv, one); + } + } + + #[test] + fn multiplicity_computation() { + let table: Vec = (0..4u32).map(F::from).collect(); + let witness: Vec = vec![0u32, 1, 1, 3, 0].into_iter().map(F::from).collect(); + let m = compute_multiplicities(&witness, &table, &()).unwrap(); + assert_eq!(m[0], F::from(2u32)); + assert_eq!(m[1], F::from(2u32)); + assert_eq!(m[2], F::from(0u32)); + assert_eq!(m[3], F::from(1u32)); + } + + #[test] + fn multiplicity_rejects_invalid_witness() { + let table: Vec = (0..4u32).map(F::from).collect(); + let witness: Vec = vec![0u32, 5].into_iter().map(F::from).collect(); + assert!(compute_multiplicities(&witness, &table, &()).is_none()); + } + + #[test] + fn multiplicity_rejects_single_invalid_entry() { + let table: Vec = (0..4u32).map(F::from).collect(); + let witness: Vec = vec![0u32, 1, 2, 3, 99].into_iter().map(F::from).collect(); + assert!(compute_multiplicities(&witness, &table, &()).is_none()); + } + + #[test] + fn multiplicity_empty_witness() { + let table: Vec = (0..4u32).map(F::from).collect(); + let witness: Vec = vec![]; + let m = compute_multiplicities(&witness, &table, &()).unwrap(); + assert!(m.iter().all(|x| *x == F::from(0u32))); + } + + #[test] + fn multiplicity_all_same_entry() { + let table: Vec = (0..4u32).map(F::from).collect(); + let witness: Vec = vec![2u32; 8].into_iter().map(F::from).collect(); + let m = compute_multiplicities(&witness, &table, &()).unwrap(); + assert_eq!(m[0], F::from(0u32)); + assert_eq!(m[1], F::from(0u32)); + assert_eq!(m[2], F::from(8u32)); + assert_eq!(m[3], F::from(0u32)); + } + + #[test] + fn batch_inverse_shifted_correctness() { + let beta = F::from(100u32); + let values: Vec = (0..5u32).map(F::from).collect(); + let result = batch_inverse_shifted(&beta, &values); + + let one = F::from(1u32); + for (v, inv) in values.iter().zip(result.iter()) { + assert_eq!((beta - v) * inv, one); + } + } + + #[test] + fn batch_inverse_shifted_empty() { + let beta = F::from(100u32); + let result = batch_inverse_shifted(&beta, &[]); + assert!(result.is_empty()); + } + + #[test] + fn batch_inverse_empty() { + let result = batch_inverse::(&[]); + assert!(result.is_empty()); + } + + #[test] + fn batch_inverse_large() { + let values: Vec = (1..=8192u32).map(F::from).collect(); + let inverses = batch_inverse(&values); + let one = F::from(1u32); + for (v, inv) in values.iter().zip(inverses.iter()) { + assert_eq!(v * inv, one); + } + } + + #[test] + fn batch_inverse_shifted_large() { + let beta = F::from(100_000u32); + let values: Vec = (0..8192u32).map(F::from).collect(); + let result = batch_inverse_shifted(&beta, &values); + let one = F::from(1u32); + for (v, inv) in values.iter().zip(result.iter()) { + assert_eq!((beta - v) * inv, one); + } + } +} diff --git a/test-uair/src/lib.rs b/test-uair/src/lib.rs index dcdb0ca7..ebf9f9e4 100644 --- a/test-uair/src/lib.rs +++ b/test-uair/src/lib.rs @@ -19,8 +19,8 @@ use zinc_poly::{ }, }; use zinc_uair::{ - ConstraintBuilder, PublicColumnLayout, ShiftSpec, TotalColumnLayout, TraceRow, Uair, - UairSignature, UairTrace, + ConstraintBuilder, LookupColumnSpec, LookupTableType, PublicColumnLayout, ShiftSpec, + TotalColumnLayout, TraceRow, Uair, UairSignature, UairTrace, ideal::{ImpossibleIdeal, degree_one::DegreeOneIdeal}, }; use zinc_utils::from_ref::FromRef; @@ -589,6 +589,302 @@ where } } +/// Minimal UAIR with a lookup constraint: one int column looked up +/// against `Word(4)` (values in {0, ..., 15}). Two int columns total, +/// with a trivial constraint `a - b = 0` to keep the CPR non-degenerate. +pub struct SimpleLookupUair(PhantomData); + +impl Uair for SimpleLookupUair +where + R: ConstSemiring + From + 'static, +{ + type Ideal = ImpossibleIdeal; + type Scalar = DensePolynomial; + + fn signature() -> UairSignature { + let total = TotalColumnLayout::new(0, 0, 2); + UairSignature::new( + total, + PublicColumnLayout::default(), + vec![], + vec![LookupColumnSpec { + column_index: 0, + table_type: LookupTableType::Word { + width: 4, + chunk_width: None, + }, + }], + ) + } + + fn constrain_general( + b: &mut B, + up: TraceRow, + _down: TraceRow, + _from_ref: FromR, + _mbs: MulByScalar, + _ideal_from_ref: IFromR, + ) where + B: ConstraintBuilder, + IFromR: Fn(&Self::Ideal) -> B::Ideal, + { + b.assert_zero(up.int[0].clone() - &up.int[1]); + } +} + +/// Generate a `GenerateRandomTrace` impl for a UAIR with 2 int columns where +/// column 0 = column 1, values drawn uniformly from `0..modulus`. +macro_rules! impl_int_copy_trace { + ($name:ident, $modulus:expr) => { + impl GenerateRandomTrace<32> for $name + where + R: ConstSemiring + From + 'static, + { + type PolyCoeff = R; + type Int = R; + + fn generate_random_trace( + num_vars: usize, + rng: &mut Rng, + ) -> UairTrace<'static, R, R, 32> { + let n = 1 << num_vars; + let vals: Vec = (0..n).map(|_| R::from(rng.next_u32() % $modulus)).collect(); + let a: DenseMultilinearExtension = vals.clone().into_iter().collect(); + let b: DenseMultilinearExtension = vals.into_iter().collect(); + UairTrace { + int: vec![a, b].into(), + ..Default::default() + } + } + } + }; +} + +impl_int_copy_trace!(SimpleLookupUair, 16); + +/// UAIR with two int columns both looked up against the same `Word(4)` table. +/// Tests L>1 (multi-column) lookup within a single group. +/// 3 int columns total: a, b looked up; c = a + b (trivial constraint). +pub struct MultiColLookupUair(PhantomData); + +impl Uair for MultiColLookupUair +where + R: ConstSemiring + From + 'static, +{ + type Ideal = ImpossibleIdeal; + type Scalar = DensePolynomial; + + fn signature() -> UairSignature { + let total = TotalColumnLayout::new(0, 0, 3); + UairSignature::new( + total, + PublicColumnLayout::default(), + vec![], + vec![ + LookupColumnSpec { + column_index: 0, + table_type: LookupTableType::Word { + width: 4, + chunk_width: None, + }, + }, + LookupColumnSpec { + column_index: 1, + table_type: LookupTableType::Word { + width: 4, + chunk_width: None, + }, + }, + ], + ) + } + + fn constrain_general( + b: &mut B, + up: TraceRow, + _down: TraceRow, + _from_ref: FromR, + _mbs: MulByScalar, + _ideal_from_ref: IFromR, + ) where + B: ConstraintBuilder, + IFromR: Fn(&Self::Ideal) -> B::Ideal, + { + b.assert_zero(up.int[0].clone() + &up.int[1] - &up.int[2]); + } +} + +/// Generate a `GenerateRandomTrace` impl for a UAIR with 3 int columns where +/// `c = a + b`, with `a` in `0..range_a` and `b` in `0..range_b`. +macro_rules! impl_sum_pair_trace { + ($name:ident, $range_a:expr, $range_b:expr) => { + impl GenerateRandomTrace<32> for $name + where + R: ConstSemiring + From + 'static, + { + type PolyCoeff = R; + type Int = R; + + fn generate_random_trace( + num_vars: usize, + rng: &mut Rng, + ) -> UairTrace<'static, R, R, 32> { + let n = 1 << num_vars; + let a_vals: Vec = (0..n).map(|_| R::from(rng.next_u32() % $range_a)).collect(); + let b_vals: Vec = (0..n).map(|_| R::from(rng.next_u32() % $range_b)).collect(); + let c_vals: Vec = a_vals + .iter() + .zip(b_vals.iter()) + .map(|(a, b)| a.clone() + b) + .collect(); + let a: DenseMultilinearExtension = a_vals.into_iter().collect(); + let b: DenseMultilinearExtension = b_vals.into_iter().collect(); + let c: DenseMultilinearExtension = c_vals.into_iter().collect(); + UairTrace { + int: vec![a, b, c].into(), + ..Default::default() + } + } + } + }; +} + +impl_sum_pair_trace!(MultiColLookupUair, 16, 16); + +/// UAIR with two columns looking up different table types: +/// column 0 → Word(4), column 1 → Word(8). Tests multiple lookup groups. +/// 3 int columns: a in {0..15}, b in {0..255}, c = a + b. +pub struct MultiGroupLookupUair(PhantomData); + +impl Uair for MultiGroupLookupUair +where + R: ConstSemiring + From + 'static, +{ + type Ideal = ImpossibleIdeal; + type Scalar = DensePolynomial; + + fn signature() -> UairSignature { + let total = TotalColumnLayout::new(0, 0, 3); + UairSignature::new( + total, + PublicColumnLayout::default(), + vec![], + vec![ + LookupColumnSpec { + column_index: 0, + table_type: LookupTableType::Word { + width: 4, + chunk_width: None, + }, + }, + LookupColumnSpec { + column_index: 1, + table_type: LookupTableType::Word { + width: 8, + chunk_width: None, + }, + }, + ], + ) + } + + fn constrain_general( + b: &mut B, + up: TraceRow, + _down: TraceRow, + _from_ref: FromR, + _mbs: MulByScalar, + _ideal_from_ref: IFromR, + ) where + B: ConstraintBuilder, + IFromR: Fn(&Self::Ideal) -> B::Ideal, + { + b.assert_zero(up.int[0].clone() + &up.int[1] - &up.int[2]); + } +} + +impl_sum_pair_trace!(MultiGroupLookupUair, 16, 256); + +/// UAIR with a BitPoly lookup: one binary_poly column looked up against +/// `BitPoly(8)`. Tests the BitPoly table type. +/// 1 binary_poly column + 1 int column, constraint: binary_poly - int ∈ . +pub struct BitPolyLookupUair(PhantomData); + +impl Uair for BitPolyLookupUair +where + R: ConstSemiring + From + 'static, +{ + type Ideal = DegreeOneIdeal; + type Scalar = DensePolynomial; + + fn signature() -> UairSignature { + let total = TotalColumnLayout::new(1, 0, 1); + UairSignature::new( + total, + PublicColumnLayout::default(), + vec![], + vec![LookupColumnSpec { + column_index: 0, + table_type: LookupTableType::BitPoly { + width: 8, + chunk_width: None, + }, + }], + ) + } + + fn constrain_general( + b: &mut B, + up: TraceRow, + _down: TraceRow, + _from_ref: FromR, + _mbs: MulByScalar, + ideal_from_ref: IFromR, + ) where + B: ConstraintBuilder, + FromR: Fn(&Self::Scalar) -> B::Expr, + MulByScalar: Fn(&B::Expr, &Self::Scalar) -> Option, + IFromR: Fn(&Self::Ideal) -> B::Ideal, + { + b.assert_in_ideal( + up.binary_poly[0].clone() - &up.int[0], + &ideal_from_ref(&DegreeOneIdeal::new(R::from(2))), + ); + } +} + +/// Generate a `GenerateRandomTrace` impl for a UAIR with 1 binary_poly column +/// and 1 int column, both carrying the same values in `0..modulus`. +macro_rules! impl_bitpoly_int_trace { + ($name:ident, $modulus:expr) => { + impl GenerateRandomTrace<32> for $name + where + R: ConstSemiring + From + 'static, + { + type PolyCoeff = R; + type Int = R; + + fn generate_random_trace( + num_vars: usize, + rng: &mut Rng, + ) -> UairTrace<'static, R, R, 32> { + let n = 1 << num_vars; + let values: Vec = (0..n).map(|_| rng.next_u32() % $modulus).collect(); + let bp: DenseMultilinearExtension> = + values.iter().map(|&v| BinaryPoly::from(v)).collect(); + let int: DenseMultilinearExtension = values.into_iter().map(R::from).collect(); + UairTrace { + binary_poly: vec![bp].into(), + int: vec![int].into(), + ..Default::default() + } + } + } + }; +} + +impl_bitpoly_int_trace!(BitPolyLookupUair, 256); + #[cfg(test)] mod tests { use crypto_primitives::crypto_bigint_int::Int; @@ -619,6 +915,10 @@ mod tests { assert_uair_shape::>(&[1]); assert_uair_shape::>(&[1; 17]); assert_uair_shape::>>(&[1, 1]); + assert_uair_shape::>(&[1]); + assert_uair_shape::>(&[1]); + assert_uair_shape::>(&[1]); + assert_uair_shape::>(&[1]); } #[test] diff --git a/utils/src/parallel.rs b/utils/src/parallel.rs index fbc871f6..14f49cdb 100644 --- a/utils/src/parallel.rs +++ b/utils/src/parallel.rs @@ -134,6 +134,33 @@ macro_rules! cfg_extend { }}; } +/// Compute chunk size for parallel/sequential dispatch. +/// +/// When the "parallel" feature is enabled and `$len >= $threshold`, +/// returns `$len / num_threads`. Otherwise returns `$len` (one chunk = +/// sequential). +/// +/// ```ignore +/// let chunk_size = cfg_chunk_size!(n, 4096); +/// cfg_chunks_mut!(result, chunk_size).zip(cfg_chunks!(values, chunk_size)).for_each(|..| ..); +/// ``` +#[macro_export] +macro_rules! cfg_chunk_size { + ($len:expr, $threshold:expr) => {{ + #[cfg(feature = "parallel")] + let _cs = if $len >= $threshold { + $len.div_ceil(rayon::current_num_threads()) + } else { + $len + }; + + #[cfg(not(feature = "parallel"))] + let _cs = $len; + + _cs + }}; +} + /// Conditionally fork expressions into parallel tasks using `rayon::join`. /// /// When the "parallel" feature is enabled, uses right-leaning nested