From 9b69e839b16ab0d89e7b91c171e89f568419185e Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:10:53 +0200 Subject: [PATCH 1/7] integrate new effsc API --- .gitignore | 3 +- Cargo.toml | 2 +- benches/utils/domainsep.rs | 2 +- src/error.rs | 20 ++ src/lib.rs | 498 ++++++++++++++++--------------- src/protocol/domainsep/mod.rs | 101 +++---- src/protocol/effsc_transcript.rs | 25 ++ src/protocol/mod.rs | 3 + src/relations/r1cs/mod.rs | 7 +- src/utils/poly.rs | 32 +- 10 files changed, 356 insertions(+), 337 deletions(-) create mode 100644 src/protocol/effsc_transcript.rs diff --git a/.gitignore b/.gitignore index 7b4eda9..d96bf3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target /Cargo.lock .vscode -.DS_Store \ No newline at end of file +.DS_Store +.claude/ diff --git a/Cargo.toml b/Cargo.toml index 984c8c8..d75499b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ serde_json = "1.0" spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "smallfp-support", features = [ "ark-ff", ] } -efficient-sumcheck = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git" } +effsc = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git", branch = "z-tech/rewrite-v2" } thiserror = "2.0.16" ark-codes = { git = "https://github.com/dmpierre/ark-codes.git" } diff --git a/benches/utils/domainsep.rs b/benches/utils/domainsep.rs index 9c537ab..5ef1355 100644 --- a/benches/utils/domainsep.rs +++ b/benches/utils/domainsep.rs @@ -2,5 +2,5 @@ use spongefish::ProverState; pub fn init_prover_state() -> ProverState { let domainsep = spongefish::domain_separator!("warp::rs"); - domainsep.instance(&0u32).std_prover() + domainsep.without_session().instance(&0u32).std_prover() } diff --git a/src/error.rs b/src/error.rs index 3f016ab..fb6aa08 100644 --- a/src/error.rs +++ b/src/error.rs @@ -73,6 +73,26 @@ impl From for VerifierError { } } +impl From for VerifierError { + fn from(err: effsc::proof::SumcheckError) -> Self { + use effsc::proof::SumcheckError; + match err { + // Round consistency or degree mismatch detected by the library. + SumcheckError::ConsistencyCheck { .. } | SumcheckError::DegreeMismatch { .. } => { + Self::SumcheckRound + } + // Not raised by `sumcheck_verify` any more (the library dropped + // the internal oracle check); warp does the final-claim check + // itself. Retained for enum completeness. + SumcheckError::FinalEvaluation => Self::Target, + // Ran out of transcript or malformed bytes. + SumcheckError::TranscriptError { .. } => Self::SpongeFish, + // Unreachable: warp always passes a noop hook. + SumcheckError::HookError { .. } => Self::SumcheckRound, + } + } +} + #[derive(Error, Debug)] pub enum DeciderError { #[error("Invalid merkle root")] diff --git a/src/lib.rs b/src/lib.rs index 698dbdc..d90a956 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ use ark_crypto_primitives::{ crh::{CRHScheme, TwoToOneCRHScheme}, merkle_tree::{Config, MerkleTree, Path}, }; -use ark_ff::{Field, PrimeField, Zero}; +use ark_ff::{Field, PrimeField}; use ark_poly::{ univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial, MultilinearExtension, Polynomial, @@ -14,17 +14,20 @@ use ark_poly::{ use ark_std::log2; use crypto::merkle::build_codeword_leaves; use crypto::merkle::compute_auth_paths; -use efficient_sumcheck::{ - accumulate_sparse_evaluations, batched_constraint_poly, - coefficient_sumcheck::coefficient_sumcheck, +use effsc::{ + coefficient_sumcheck::RoundPolyEvaluator, folding::protogalaxy, - hypercube::{compute_hypercube_eq_evals, Hypercube}, - inner_product_sumcheck, - order_strategy::AscendingOrder, + hypercube::{compute_hypercube_eq_evals, Ascending}, + noop_hook, + provers::{coefficient_lsb::CoefficientProverLSB, inner_product::InnerProductProver}, + runner::sumcheck, + verifier::sumcheck_verify, }; -use protocol::domainsep::parse_statement; +use protocol::domainsep::{derive_between_sumchecks, derive_pre_twin_constraint, parse_statement}; +use protocol::EffscVerifierTranscript; use relations::{r1cs::R1CSConstraints, BundledPESAT}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::collections::HashMap; use std::marker::PhantomData; use utils::binary_field_elements_to_usize; use utils::byte_to_binary_field_array; @@ -35,7 +38,7 @@ use utils::{ }; use config::WARPConfig; -use protocol::domainsep::{absorb_accumulated_instances, absorb_instances, derive_randomness}; +use protocol::domainsep::{absorb_accumulated_instances, absorb_instances}; pub mod config; pub mod constraints; @@ -71,56 +74,98 @@ fn eval_r1cs_constraint_poly( DensePolynomial::from_coefficients_vec(vec![a0 * b0 - c0, a0 * b1 + a1 * b0 - c1, a1 * b1]) } -/// Compute the twin-constraint round polynomial `h(X)`. -/// -/// Proves `∑_i τ(i) · (f(i) + ω · p(i)) = 0` via protogalaxy folding, where: -/// - `f(X)` = `fold(α, oracle_evals)` — folded codeword check -/// - `p(X)` = `fold(β, Az·Bz - Cz)` — folded R1CS constraint check -/// - `t(X)` = linear interpolation of τ — equality polynomial -/// -/// Returns `h(X) = Σ (f(X) + ω·p(X)) · t(X)`. -/// -/// `expected_num_coeffs` is the number of coefficients the verifier expects to read. -/// The result is padded with zeros to ensure the prover always writes exactly that many. -fn twin_constraint_round_poly( - tablewise: &[Vec>], - pairwise: &[Vec], - r1cs: &R1CSConstraints, +/// `RoundPolyEvaluator` for the twin-constraint sumcheck. Fuses the α-fold, +/// β-fold, and τ-linear multiplication in a single pass over even/odd pairs +/// of the input tables. Replaces the closure `twin_constraint_round_poly` +/// previously handed to `coefficient_sumcheck`. +struct TwinConstraintEvaluator<'a, F: Field> { + r1cs: &'a R1CSConstraints, omega: F, - expected_num_coeffs: usize, -) -> DensePolynomial { - let (u, z, a, b) = (&tablewise[0], &tablewise[1], &tablewise[2], &tablewise[3]); - let tau = &pairwise[0]; - - let f_iter = u.chunks(2).zip(a.chunks(2)).map(|(u, a)| { - protogalaxy::fold( - a[0].iter().zip(&a[1]).map(|(&l, &r)| (l, r - l)), - u[0].iter() - .zip(&u[1]) + degree: usize, +} + +impl<'a, F: Field> RoundPolyEvaluator for TwinConstraintEvaluator<'a, F> { + fn degree(&self) -> usize { + self.degree + } + + fn accumulate_pair(&self, coeffs: &mut [F], tw: &[(&[F], &[F])], pw: &[(F, F)]) { + // tw[0]=(u_even,u_odd), tw[1]=(z_even,z_odd), + // tw[2]=(a_even,a_odd), tw[3]=(b_even,b_odd); pw[0]=(tau_even,tau_odd). + let (u_even, u_odd) = tw[0]; + let (z_even, z_odd) = tw[1]; + let (a_even, a_odd) = tw[2]; + let (b_even, b_odd) = tw[3]; + let (tau_even, tau_odd) = pw[0]; + + let f = protogalaxy::fold( + a_even.iter().zip(a_odd).map(|(&l, &r)| (l, r - l)), + u_even + .iter() + .zip(u_odd) .map(|(&l, &r)| linear_poly(l, r)) .collect(), - ) - }); - let p_iter = b.chunks(2).zip(z.chunks(2)).map(|(b, z)| { - protogalaxy::fold( - b[0].iter().zip(&b[1]).map(|(&l, &r)| (l, r - l)), - r1cs.iter() - .map(|c| eval_r1cs_constraint_poly(c, &z[0], &z[1])) + ); + + let p = protogalaxy::fold( + b_even.iter().zip(b_odd).map(|(&l, &r)| (l, r - l)), + self.r1cs + .iter() + .map(|c| eval_r1cs_constraint_poly(c, z_even, z_odd)) .collect(), - ) - }); - let t_iter = tau.chunks(2).map(|t| linear_poly(t[0], t[1])); - - let mut h = f_iter - .zip(p_iter) - .zip(t_iter) - .map(|((f, p), t)| (f + p * omega).naive_mul(&t)) - .fold(DensePolynomial::zero(), |acc, r| acc + r); - - // Pad coefficients to the expected count so the prover writes exactly - // as many field elements as the verifier reads in derive_randomness. - h.coeffs.resize(expected_num_coeffs, F::zero()); - h + ); + + let t = linear_poly(tau_even, tau_odd); + let h = (f + p * self.omega).naive_mul(&t); + + for (c, &hc) in coeffs.iter_mut().zip(h.coeffs.iter()) { + *c += hc; + } + } +} + +/// [CBBZ23] / HyperPlonk sparse-evaluation optimization: for shift-query +/// zetas (indices `1+s..r`), each ζ is a 0/1 vector representing a single +/// hypercube point. Accumulates the corresponding `eq_evals[i]` into a sparse +/// map keyed by that point's index. Reimplemented locally; the equivalent +/// helper in the old effsc API was removed in the rewrite. +fn accumulate_sparse_evaluations( + zetas: Vec<&[F]>, + eq_evals: Vec, + s: usize, + r: usize, +) -> HashMap { + let mut result: HashMap = HashMap::new(); + for i in 1 + s..r { + let index = zetas[i] + .iter() + .enumerate() + .filter_map(|(j, bit)| bit.is_one().then_some(1 << j)) + .sum::(); + *result.entry(index).or_insert_with(F::zero) += eq_evals[i]; + } + result +} + +/// Build the `g` side of the batching inner-product sumcheck: column-sum the +/// dense OOD evaluation matrix and add the sparse shift-query contributions. +fn batched_constraint_poly( + dense_polys: &[Vec], + sparse_polys: &HashMap, +) -> Vec { + if dense_polys.is_empty() { + return Vec::new(); + } + let mut result = vec![F::ZERO; dense_polys[0].len()]; + for row in dense_polys { + for (i, val) in row.iter().enumerate() { + result[i] += *val; + } + } + for (k, v) in sparse_polys.iter() { + result[*k] += *v; + } + result } pub trait BoolResult { @@ -304,8 +349,8 @@ impl< // b. define [...] // c. sumcheck protocol - let tau_eq_evals = Hypercube::::new(log_l) - .map(|(index, _point)| eq_poly(&tau, index)) + let tau_eq_evals = Ascending::new(log_l) + .map(|p| eq_poly(&tau, p.index)) .collect::>(); let alpha_vecs = concat_slices(&acc_instances.1, &vec![vec![F::zero(); log_n]; l1]); @@ -322,34 +367,39 @@ impl< let beta_vecs: Vec> = acc_instances.3 .0.into_iter().chain(taus).collect(); - // Twin Constraint sumcheck - let mut tablewise = [ + // Twin Constraint sumcheck via the new `SumcheckProver` trait. + // Wire format: `d+1` evaluations per round (vs. the old `d+1` + // coefficient-form emission). `CoefficientProverLSB` keeps the + // LSB/pair-split semantics the `TwinConstraintEvaluator` was built + // for. + let tablewise = vec![ concat_slices(&acc_witnesses.1, &codewords), // u z_vecs, // z alpha_vecs, // a beta_vecs, // b ]; - let mut pw = [tau_eq_evals]; // tau + let pw = vec![tau_eq_evals]; // tau let r1cs = self.p.constraints(); - let expected_num_coeffs = 2 + (log_n + 1).max(log_M + 2); - let sc = coefficient_sumcheck( - |tw, pw| twin_constraint_round_poly(tw, pw, r1cs, omega, expected_num_coeffs), - &mut tablewise, - &mut pw, - log_l, - prover_state, - ); - let gamma = sc.verifier_messages; - + let degree = 1 + (log_n + 1).max(log_M + 2); + let evaluator = TwinConstraintEvaluator { + r1cs, + omega, + degree, + }; + let mut cc = CoefficientProverLSB::new(&evaluator, tablewise, pw); + let gamma = sumcheck(&mut cc, log_l, prover_state, noop_hook).challenges; debug_assert_eq!(gamma.len(), log_l); - // e. new oracle and target — after log_l rounds each group has one table left - let [mut u_red, mut z_red, mut a_red, mut b_red] = tablewise; - let f = u_red.pop().unwrap(); - let z = z_red.pop().unwrap(); - let zeta_0 = a_red.pop().unwrap(); - let beta_tau = b_red.pop().unwrap(); + // e. new oracle and target — after log_l rounds each group has one + // row left; extract by clone. `CoefficientProverLSB` exposes the + // reduced tables read-only. + let reduced_tw = cc.tablewise(); + debug_assert!(reduced_tw.iter().all(|t| t.len() == 1)); + let f = reduced_tw[0][0].clone(); + let z = reduced_tw[1][0].clone(); + let zeta_0 = reduced_tw[2][0].clone(); + let beta_tau = reduced_tw[3][0].clone(); // eval the bundled r1cs let beta_eq_evals = (0..M).map(|i| eq_poly(&beta_tau, i)).collect::>(); @@ -441,13 +491,16 @@ impl< let id_non_0_eval_sums = accumulate_sparse_evaluations(zetas, xi_eq_evals, self.config.s, r); - // call efficient sumcheck for batched_constraint checks - let alpha = inner_product_sumcheck( - &mut f.clone(), - &mut batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), - prover_state, - ) - .verifier_messages; + // Batching inner-product sumcheck via the new `SumcheckProver` trait. + // `InnerProductProver` is MSB half-split, so the challenge vector is + // returned in MSB order; reverse once to arkworks' LSB-first + // convention before feeding it to the MLE. + let mut ip = InnerProductProver::new( + f.clone(), + batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), + ); + let mut alpha = sumcheck(&mut ip, log_n, prover_state, noop_hook).challenges; + alpha.reverse(); // m. new target let mu = f_hat.fix_variables(&alpha)[0]; @@ -502,147 +555,88 @@ impl< let (l1, l) = (self.config.l1, self.config.l); let l2 = l - l1; - //////////////////////// - // 1. Parsing phase - //////////////////////// - // a. verification key #[allow(non_snake_case)] let (M, N, k) = (vk.0, vk.1, vk.2); #[allow(non_snake_case)] let (log_M, log_l) = (log2(M) as usize, log2(l) as usize); - let n = self.code.code_len(); let log_n = log2(n) as usize; - - // f. absorb parameters - let (l1_xs, (l2_roots, l2_alphas, l2_mus, (l2_taus, l2_xs), l2_etas)) = - parse_statement::(verifier_state, l1, l2, N - k, log_n, log_M)?; - - //////////////////////// - // 2. Derive randomness - //////////////////////// - let ( - rt_0, - l1_mus, - l1_taus, - omega, - tau, - gamma_sumcheck, - coeffs_twinc_sumcheck, - _rt, - eta, - mut nus, - ood_samples, - bytes_shift_queries, - xi, - alpha_sumcheck, - sums_batching_sumcheck, - ) = derive_randomness::( - verifier_state, - l1, - log_n, - log_l, - self.config.s, - self.config.t, - log_M, - )?; - let r = 1 + self.config.s + self.config.t; let log_r = log2(r) as usize; - //////////////////////// - // 3. Derive values - //////////////////////// - // b. - let alpha_vecs = concat_slices(&l2_alphas, &vec![vec![F::zero(); log_n]; l1]); - - let gamma_eq_evals = compute_hypercube_eq_evals(log_l, &gamma_sumcheck); - - let zeta_0 = scale_and_sum(&alpha_vecs, &gamma_eq_evals); - - // compute \eta_{s + k} - let mut nu_s_t = vec![F::default(); self.config.t]; - for (i, v_jk) in proof.6.iter().enumerate() { - let res = v_jk - .iter() - .zip(&gamma_eq_evals) - .fold(F::zero(), |acc, (v, eq)| acc + *eq * *v); - nu_s_t[i] = res; - } + // 1. Parse statement. + let (l1_xs, (l2_roots, l2_alphas, l2_mus, (l2_taus, l2_xs), l2_etas)) = + parse_statement::(verifier_state, l1, l2, N - k, log_n, log_M)?; - nus.extend(nu_s_t); + // 2. Pre-twin-constraint transcript reads. + let (rt_0, l1_mus, l1_taus, omega, tau) = + derive_pre_twin_constraint::(verifier_state, l1, log_l, log_M)?; - // d. set \sigma^{(1)} and \sigma^{(2)} - // compute eq(\tau, i) and eq(\xi, i) + // 3. σ₁ = Σ_i τ_eq(i) · (μ_i + ω·η_i). let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); - - let etas = concat_slices(&l2_etas, &vec![F::zero(); l1]); - + let etas_chain = concat_slices(&l2_etas, &vec![F::zero(); l1]); let sigma_1 = tau_eq_evals .into_iter() - .zip(l2_mus.into_iter().chain(l1_mus.to_vec()).zip(etas)) - .fold(F::zero(), |acc, (eq_tau, (mu, eta))| { - acc + eq_tau * (mu + omega * eta) + .zip( + l2_mus + .iter() + .copied() + .chain(l1_mus.iter().copied()) + .zip(etas_chain), + ) + .fold(F::zero(), |acc, (eq_tau, (mu, eta_i))| { + acc + eq_tau * (mu + omega * eta_i) }); - let xi_eq_evals = compute_hypercube_eq_evals(log_r, &xi); - - let sigma_2 = xi_eq_evals - .iter() - .zip(&nus) - .fold(F::zero(), |acc, (xi_eq, nu)| acc + *xi_eq * nu); - - //////////////////////// - // 4. Decision phase - //////////////////////// - // a. new code evaluation point - (acc_instance.1[0] == alpha_sumcheck).ok_or_err(VerifierError::CodeEvaluationPoint)?; - - // b. new circuit evaluation point - let betas = l2_taus - .into_iter() - .chain(l1_taus) - .zip(l2_xs.clone().into_iter().chain(l1_xs)) - .map(|(tau, x)| concat_slices(&tau, &x)) - .collect::>>(); - let beta = scale_and_sum(&betas, &gamma_eq_evals); - let expected_beta = concat_slices(&acc_instance.3 .0[0], &acc_instance.3 .1[0]); - (expected_beta == beta).ok_or_err(VerifierError::CircuitEvaluationPoint)?; + // 4. Twin-constraint sumcheck via `effsc::sumcheck_verify`. The + // library enforces round consistency (`q(0)+q(1)==claim`) and + // returns `(challenges, final_claim)`. The oracle check — verifying + // `final_claim == eq(τ,γ)·(ν₀ + ω·η)` — is warp's responsibility + // and runs below once η and ν₀ have been read from the transcript. + let tc_degree = 1 + (log_n + 1).max(log_M + 2); + let (gamma_sumcheck, tc_final_claim) = { + let mut wrap = EffscVerifierTranscript(verifier_state); + let res = sumcheck_verify(sigma_1, tc_degree, log_l, &mut wrap, |_, _| Ok(()))?; + (res.challenges, res.final_claim) + }; + + // 5. Between-sumchecks reads: td, η, ν₀, OOD, shift bytes, ξ. + let (_td, eta, mut nus, ood_samples, bytes_shift_queries, xi) = + derive_between_sumchecks::(verifier_state, log_n, self.config.s, self.config.t)?; + + // 6. Deferred twin-constraint oracle check. + (eq_poly_non_binary(&tau, &gamma_sumcheck) * (nus[0] + omega * eta) == tc_final_claim) + .ok_or_err(VerifierError::Target)?; - // c. check auth paths - let binary_shift_queries = bytes_shift_queries + // 7. Proximity check. Must run *before* the batching sumcheck because + // σ₂ consumes `proof.6` (shift-query answers); a tampered row + // would otherwise surface as a batching-round consistency failure + // rather than the expected ShiftQuery/NumShiftQueries variant. + let binary_shift_queries_flat = bytes_shift_queries .iter() .flat_map(byte_to_binary_field_array) .take(self.config.t * log_n) .collect::>(); - - let binary_shift_queries = binary_shift_queries.chunks(log_n).collect::>(); - + let binary_shift_queries = binary_shift_queries_flat + .chunks(log_n) + .collect::>(); let shift_queries_indexes: Vec = binary_shift_queries .iter() .map(|vals| binary_field_elements_to_usize(vals)) .collect(); - // check: - // that the leaf index corresponds to the shift query - // that the path is correct (proof.6.len() == self.config.t).ok_or_err(VerifierError::NumShiftQueries)?; - - // proof.4 is auth_0 for (i, path) in proof.4.iter().enumerate() { (path.leaf_index == shift_queries_indexes[i]) .ok_or_err(VerifierError::ShiftQueryIndex)?; - let is_valid = path.verify( &self.mt_leaf_hash_params, &self.mt_two_to_one_hash_params, &rt_0, - &proof.6[i][l2..], // leaves are evaluations of the l1 codewords + &proof.6[i][l2..], )?; - is_valid.ok_or_err(VerifierError::ShiftQuery)? + is_valid.ok_or_err(VerifierError::ShiftQuery)?; } - - // proof.5 holds merkle proofs for l2 accumulated instances (proof.5.len() == l2).ok_or_err(VerifierError::NumL2Instances)?; for (i, paths) in proof.5.iter().enumerate() { (paths.len() == self.config.t).ok_or_err(VerifierError::NumShiftQueries)?; @@ -654,66 +648,72 @@ impl< &self.mt_leaf_hash_params, &self.mt_two_to_one_hash_params, root, - [proof.6[j][i]], // proof.6[j][i] holds f_i(x_j) + [proof.6[j][i]], )?; - - is_valid.ok_or_err(VerifierError::ShiftQuery)? + is_valid.ok_or_err(VerifierError::ShiftQuery)?; } } - // d. sumcheck decisions - // twin constraints sumcheck - (coeffs_twinc_sumcheck.len() == log_l).ok_or_err(VerifierError::NumSumcheckRounds)?; - - let mut target_1 = sigma_1; - for (coeffs, gamma) in coeffs_twinc_sumcheck.into_iter().zip(&gamma_sumcheck) { - let h = DensePolynomial::from_coefficients_vec(coeffs); - (h.evaluate(&F::one()) + h.evaluate(&F::zero()) == target_1) - .ok_or_err(VerifierError::SumcheckRound)?; - target_1 = h.evaluate(gamma); - } + // 8. Derive σ₂ and the reduced α/μ expectation. + let gamma_eq_evals = compute_hypercube_eq_evals(log_l, &gamma_sumcheck); + let alpha_vecs = concat_slices(&l2_alphas, &vec![vec![F::zero(); log_n]; l1]); + let zeta_0 = scale_and_sum(&alpha_vecs, &gamma_eq_evals); - // multilinear batching sumcheck - (sums_batching_sumcheck.len() == log_n).ok_or_err(VerifierError::NumSumcheckRounds)?; - let mut target_2 = sigma_2; - for ([sum_00, sum_11, sum_0110], alpha) in - sums_batching_sumcheck.into_iter().zip(&alpha_sumcheck) - { - (sum_00 + sum_11 == target_2).ok_or_err(VerifierError::SumcheckRound)?; - target_2 = (target_2 - sum_0110) * alpha.square() - + sum_00 * (F::one() - alpha.double()) - + sum_0110 * alpha; + let mut nu_s_t = vec![F::default(); self.config.t]; + for (i, v_jk) in proof.6.iter().enumerate() { + nu_s_t[i] = v_jk + .iter() + .zip(&gamma_eq_evals) + .fold(F::zero(), |acc, (v, eq)| acc + *eq * *v); } + nus.extend(nu_s_t); - // e. new target decision - // build eq^{\star}(\alpha) - (eq_poly_non_binary(&tau, &gamma_sumcheck) * (nus[0] + omega * eta) == target_1) - .ok_or_err(VerifierError::Target)?; - - let mut zeta_eqs = vec![eq_poly_non_binary(&zeta_0, &alpha_sumcheck)]; - - zeta_eqs.extend( - ood_samples - .chunks(log_n) - .map(|zeta| eq_poly_non_binary(zeta, &alpha_sumcheck)) - .collect::>(), - ); - zeta_eqs.extend( - binary_shift_queries - .iter() - .map(|zeta| eq_poly_non_binary(zeta, &alpha_sumcheck)) - .collect::>(), - ); - (zeta_eqs.len() == r).ok_or_err(VerifierError::NumShiftQueries)?; + let xi_eq_evals = compute_hypercube_eq_evals(log_r, &xi); + let sigma_2 = xi_eq_evals + .iter() + .zip(&nus) + .fold(F::zero(), |acc, (xi_eq, nu)| acc + *xi_eq * nu); - // mul by \mu and compare to target_2 - (acc_instance.2[0] + // 9. Batching (inner-product) sumcheck. Library handles round + // consistency; warp performs the oracle check: + // `final_claim == μ · Σ eq(ζ_i, α_lsb)·ξ_eq(i)`. MSB half-split + // → reverse the challenge vector once before feeding to + // eq_poly_non_binary (arkworks MLE convention). + let (alpha_sumcheck_msb, batching_final_claim) = { + let mut wrap = EffscVerifierTranscript(verifier_state); + let res = sumcheck_verify(sigma_2, 2, log_n, &mut wrap, |_, _| Ok(()))?; + (res.challenges, res.final_claim) + }; + let alpha_sumcheck_lsb: Vec = alpha_sumcheck_msb.iter().rev().copied().collect(); + + let mut zeta_eqs = Vec::with_capacity(r); + zeta_eqs.push(eq_poly_non_binary(&zeta_0, &alpha_sumcheck_lsb)); + for chunk in ood_samples.chunks(log_n) { + zeta_eqs.push(eq_poly_non_binary(chunk, &alpha_sumcheck_lsb)); + } + for zeta in &binary_shift_queries { + zeta_eqs.push(eq_poly_non_binary(zeta, &alpha_sumcheck_lsb)); + } + debug_assert_eq!(zeta_eqs.len(), r); + let expected_batching = acc_instance.2[0] * zeta_eqs .into_iter() - .zip(xi_eq_evals) - .fold(F::zero(), |acc, (a, b)| acc + a * b) - == target_2) - .ok_or_err(VerifierError::Target)?; + .zip(&xi_eq_evals) + .fold(F::zero(), |acc, (a, b)| acc + a * *b); + (expected_batching == batching_final_claim).ok_or_err(VerifierError::Target)?; + + // 10. Accumulator consistency: new α and β. + (acc_instance.1[0] == alpha_sumcheck_lsb).ok_or_err(VerifierError::CodeEvaluationPoint)?; + + let betas = l2_taus + .into_iter() + .chain(l1_taus) + .zip(l2_xs.clone().into_iter().chain(l1_xs)) + .map(|(tau_i, x)| concat_slices(&tau_i, &x)) + .collect::>>(); + let beta = scale_and_sum(&betas, &gamma_eq_evals); + let expected_beta = concat_slices(&acc_instance.3 .0[0], &acc_instance.3 .1[0]); + (expected_beta == beta).ok_or_err(VerifierError::CircuitEvaluationPoint)?; Ok(()) } @@ -743,8 +743,8 @@ impl< let tau = &beta.0[0]; - let tau_zero_evader = Hypercube::::new(tau.len()) - .map(|(index, _point)| eq_poly(tau, index)) + let tau_zero_evader = Ascending::new(tau.len()) + .map(|p| eq_poly(tau, p.index)) .collect::>(); let mut z = beta.1[0].clone(); @@ -853,7 +853,7 @@ pub mod test { for _ in 0..l1 { let domainsep = spongefish::domain_separator!("test::warp"); - let mut prover_state = domainsep.instance(&0u32).std_prover(); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); let ((acc_x, acc_w), _pf) = hash_chain_warp .prove( (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), @@ -889,7 +889,7 @@ pub mod test { warp_config.clone(), code.clone(), r1cs.clone(), (), () ); - let mut prover_state = domainsep.instance(&0u32).std_prover(); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); let ((acc_x, acc_w), pf) = hash_chain_warp .prove( (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), @@ -903,7 +903,10 @@ pub mod test { let narg_str = prover_state.narg_string().to_vec(); let domainsep_v = spongefish::domain_separator!("test::warp"); - let mut verifier_state = domainsep_v.instance(&0u32).std_verifier(&narg_str); + let mut verifier_state = domainsep_v + .without_session() + .instance(&0u32) + .std_verifier(&narg_str); hash_chain_warp .verify( (r1cs.m, r1cs.n, r1cs.k), @@ -1001,7 +1004,7 @@ pub mod test { for _ in 0..l1 { let domainsep = spongefish::domain_separator!("test::warp"); - let mut prover_state = domainsep.instance(&0u32).std_prover(); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); let ((acc_x, acc_w), _pf) = hash_chain_warp .prove( (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), @@ -1038,7 +1041,7 @@ pub mod test { warp_config.clone(), code.clone(), r1cs.clone(), (), () ); - let mut prover_state = domainsep.instance(&0u32).std_prover(); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); let ((acc_x, acc_w), pf) = hash_chain_warp .prove( (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), @@ -1052,7 +1055,10 @@ pub mod test { let narg_str = prover_state.narg_string().to_vec(); let domainsep_v = spongefish::domain_separator!("test::warp"); - let mut verifier_state = domainsep_v.instance(&0u32).std_verifier(&narg_str); + let mut verifier_state = domainsep_v + .without_session() + .instance(&0u32) + .std_verifier(&narg_str); hash_chain_warp .verify( (r1cs.m, r1cs.n, r1cs.k), diff --git a/src/protocol/domainsep/mod.rs b/src/protocol/domainsep/mod.rs index 4bd0d1f..988d039 100644 --- a/src/protocol/domainsep/mod.rs +++ b/src/protocol/domainsep/mod.rs @@ -125,44 +125,30 @@ pub fn parse_statement< )) } -pub type DerivedRandomness = ( - ::InnerDigest, - Vec, - Vec>, - F, - Vec, - Vec, - Vec>, - ::InnerDigest, - F, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec<[F; 3]>, +/// Transcript values read before the twin-constraint sumcheck: +/// `(rt_0, l1_mus, l1_taus, omega, tau)`. +pub type PreTwinConstraint = ( + ::InnerDigest, // rt_0 + Vec, // l1_mus + Vec>, // l1_taus + F, // omega + Vec, // tau ); -pub fn derive_randomness< +pub fn derive_pre_twin_constraint< F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, MT: Config + From<[u8; 32]>>, >( verifier_state: &mut VerifierState<'_>, l1: usize, - log_n: usize, log_l: usize, - s: usize, - t: usize, #[allow(non_snake_case)] log_M: usize, -) -> VerificationResult> { - // read commitment digest +) -> VerificationResult> { let rt_0_bytes: [u8; 32] = verifier_state.prover_message()?; let rt_0: MT::InnerDigest = rt_0_bytes.into(); - // read mus let l1_mus: Vec = verifier_state.prover_messages_vec(l1)?; - // challenge taus let mut l1_taus = Vec::with_capacity(l1); for _ in 0..l1 { let tau: Vec = (0..log_M) @@ -176,37 +162,44 @@ pub fn derive_randomness< .map(|_| verifier_state.verifier_message::()) .collect(); - // e. twin constraints sumcheck - let mut gamma_sumcheck = Vec::new(); - let mut coeffs_twinc_sumcheck = Vec::new(); - for _ in 0..log_l { - let h_coeffs: Vec = - verifier_state.prover_messages_vec(2 + (log_n + 1).max(log_M + 2))?; - let c: F = verifier_state.verifier_message(); - gamma_sumcheck.push(c); - coeffs_twinc_sumcheck.push(h_coeffs); - } + Ok((rt_0, l1_mus, l1_taus, omega, tau)) +} - // read td digest +/// Transcript values read between the two sumchecks: +/// `(td, eta, nus=[nu_0, ood_answers...], ood_samples, bytes_shift_queries, xi)`. +pub type BetweenSumchecks = ( + ::InnerDigest, // td + F, // eta + Vec, // nus (nu_0 + ood answers) + Vec, // ood_samples (flat, s*log_n long) + Vec, // bytes_shift_queries + Vec, // xi +); + +pub fn derive_between_sumchecks< + F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, + MT: Config + From<[u8; 32]>>, +>( + verifier_state: &mut VerifierState<'_>, + log_n: usize, + s: usize, + t: usize, +) -> VerificationResult> { let td_bytes: [u8; 32] = verifier_state.prover_message()?; - let _td: MT::InnerDigest = td_bytes.into(); + let td: MT::InnerDigest = td_bytes.into(); - // read eta and nu_0 let eta: F = verifier_state.prover_message()?; let nu_0: F = verifier_state.prover_message()?; let mut nus = vec![nu_0]; - // g. ood samples let n_ood_samples = s * log_n; let ood_samples: Vec = (0..n_ood_samples) .map(|_| verifier_state.verifier_message::()) .collect(); - // h. ood answers let ood_answers: Vec = verifier_state.prover_messages_vec(s)?; nus.extend(ood_answers); - // i. shift queries and zero check let r = 1 + s + t; let log_r = log2(r) as usize; let n_shift_queries = (t * log_n).div_ceil(8); @@ -217,31 +210,5 @@ pub fn derive_randomness< .map(|_| verifier_state.verifier_message::()) .collect(); - // j. batching sumcheck - let mut alpha_sumcheck = Vec::new(); - let mut sums_batching_sumcheck = Vec::new(); - for _ in 0..log_n { - let sums: [F; 3] = verifier_state.prover_messages()?; - let c: F = verifier_state.verifier_message(); - alpha_sumcheck.push(c); - sums_batching_sumcheck.push(sums); - } - - Ok(( - rt_0, - l1_mus, - l1_taus, - omega, - tau, - gamma_sumcheck, - coeffs_twinc_sumcheck, - _td, - eta, - nus, - ood_samples, - bytes_shift_queries, - xi, - alpha_sumcheck, - sums_batching_sumcheck, - )) + Ok((td, eta, nus, ood_samples, bytes_shift_queries, xi)) } diff --git a/src/protocol/effsc_transcript.rs b/src/protocol/effsc_transcript.rs new file mode 100644 index 0000000..b0c7ace --- /dev/null +++ b/src/protocol/effsc_transcript.rs @@ -0,0 +1,25 @@ +//! Adapter plugging spongefish's [`VerifierState`] into effsc's +//! [`VerifierTranscript`] trait, so warp's verifier can call +//! [`effsc::verifier::sumcheck_verify`]. Prover-side is already handled by +//! a blanket impl inside effsc (on `spongefish::ProverState`), so no +//! wrapper is needed there. + +use effsc::transcript::VerifierTranscript; +use spongefish::{Decoding, Encoding, NargDeserialize, VerifierState}; + +pub struct EffscVerifierTranscript<'s, 'a>(pub &'s mut VerifierState<'a>); + +impl<'s, 'a, F> VerifierTranscript for EffscVerifierTranscript<'s, 'a> +where + F: ark_ff::Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, +{ + type Error = spongefish::VerificationError; + + fn receive(&mut self) -> Result { + self.0.prover_message::() + } + + fn challenge(&mut self) -> F { + self.0.verifier_message::() + } +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 6aa899d..5057652 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1 +1,4 @@ pub mod domainsep; +pub mod effsc_transcript; + +pub use effsc_transcript::EffscVerifierTranscript; diff --git a/src/relations/r1cs/mod.rs b/src/relations/r1cs/mod.rs index 0232a7e..72c274f 100644 --- a/src/relations/r1cs/mod.rs +++ b/src/relations/r1cs/mod.rs @@ -2,7 +2,7 @@ pub mod hashchain; use ark_ff::Field; use ark_relations::r1cs::ConstraintSystemRef; -use efficient_sumcheck::{hypercube::Hypercube, order_strategy::AscendingOrder}; +use effsc::hypercube::Ascending; use crate::error::WARPError; @@ -89,10 +89,9 @@ impl BundledPESAT for R1CS { type Constraints = R1CSConstraints; fn evaluate_bundled(&self, zero_evader_evals: &[F], z: &[F]) -> Result { - let mut cube = Hypercube::::new(self.log_m); - // TODO: multithread this - cube.try_fold(F::ZERO, |acc, (index, _point)| { + Ascending::new(self.log_m).try_fold(F::ZERO, |acc, p| { + let index = p.index; let eq_tau_i = *zero_evader_evals .get(index) .ok_or(WARPError::ZeroEvaderSize(zero_evader_evals.len(), index))?; diff --git a/src/utils/poly.rs b/src/utils/poly.rs index b477c1c..2725119 100644 --- a/src/utils/poly.rs +++ b/src/utils/poly.rs @@ -1,25 +1,23 @@ use ark_ff::Field; -use efficient_sumcheck::{ - hypercube::HypercubeMember, interpolation::LagrangePolynomial, order_strategy::AscendingOrder, -}; -pub fn eq_poly(original_tau: &[F], point: usize) -> F { - // TODO (z-tech): will fix and get rid of this function - let num_variables = original_tau.len(); - let mut tau = original_tau.to_vec(); - tau.reverse(); - let tau_hat: Vec = tau.iter().map(|t| F::ONE - *t).collect(); - LagrangePolynomial::::lag_poly( - tau, - tau_hat, - HypercubeMember::new(num_variables, point), - ) +/// `eq(τ, y)` where `y ∈ {0,1}^{τ.len()}` is the Boolean point whose bit `j` +/// is `(point >> j) & 1`. Matches the LSB-indexed formula used by +/// [`effsc::hypercube::compute_hypercube_eq_evals`], so +/// `eq_poly(τ, i) == compute_hypercube_eq_evals(τ.len(), τ)[i]`. +pub fn eq_poly(tau: &[F], point: usize) -> F { + let num_variables = tau.len(); + (0..num_variables).fold(F::one(), |acc, j| { + if (point >> j) & 1 == 1 { + acc * tau[j] + } else { + acc * (F::one() - tau[j]) + } + }) } pub fn eq_poly_non_binary(x: &[F], y: &[F]) -> F { assert_eq!(x.len(), y.len()); - let res = x.iter().zip(y).fold(F::one(), |acc, (x_i, y_i)| { + x.iter().zip(y).fold(F::one(), |acc, (x_i, y_i)| { acc * (*x_i * *y_i + (F::one() - x_i) * (F::one() - y_i)) - }); - res + }) } From 9be925b66f3824d06cfa0029294f39540df761c2 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:26:41 +0200 Subject: [PATCH 2/7] use eq eval helpers from effsc --- src/lib.rs | 36 +++++++++++++++++++----------------- src/utils/poly.rs | 30 +++++++----------------------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d90a956..ae111ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ use crypto::merkle::compute_auth_paths; use effsc::{ coefficient_sumcheck::RoundPolyEvaluator, folding::protogalaxy, - hypercube::{compute_hypercube_eq_evals, Ascending}, + hypercube::compute_hypercube_eq_evals, noop_hook, provers::{coefficient_lsb::CoefficientProverLSB, inner_product::InnerProductProver}, runner::sumcheck, @@ -32,10 +32,7 @@ use std::marker::PhantomData; use utils::binary_field_elements_to_usize; use utils::byte_to_binary_field_array; use utils::scale_and_sum; -use utils::{ - concat_slices, - poly::{eq_poly, eq_poly_non_binary}, -}; +use utils::{concat_slices, poly::eq_poly_non_binary}; use config::WARPConfig; use protocol::domainsep::{absorb_accumulated_instances, absorb_instances}; @@ -348,10 +345,10 @@ impl< let tau = prover_state.verifier_messages_vec::(log_l); // b. define [...] - // c. sumcheck protocol - let tau_eq_evals = Ascending::new(log_l) - .map(|p| eq_poly(&tau, p.index)) - .collect::>(); + // c. sumcheck protocol. `compute_hypercube_eq_evals` builds the full + // `2^log_l` table in O(2^log_l) via the incremental build-up — + // faster than the previous O(log_l · 2^log_l) per-point loop. + let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); let alpha_vecs = concat_slices(&acc_instances.1, &vec![vec![F::zero(); log_n]; l1]); @@ -402,7 +399,7 @@ impl< let beta_tau = reduced_tw[3][0].clone(); // eval the bundled r1cs - let beta_eq_evals = (0..M).map(|i| eq_poly(&beta_tau, i)).collect::>(); + let beta_eq_evals = compute_hypercube_eq_evals(log_M, &beta_tau); let eta = self .p @@ -477,13 +474,20 @@ impl< // l. sumcheck polynomials // compute evaluations for xi - let xi_eq_evals = (0..r).map(|i| eq_poly(&xis, i)).collect::>(); + // `xis` has `log_r` elements but only the first `r` entries of the + // eq-evals table are consumed (`r ≤ 2^log_r`). The incremental + // builder is still a net win: O(2^log_r) ≤ O(2r) vs the previous + // O(r · log_r). + let xi_eq_evals: Vec = compute_hypercube_eq_evals(log_r, &xis) + .into_iter() + .take(r) + .collect(); let ood_evals_vec = (0..1 + self.config.s) .map(|i| { - (0..n) - .map(|a| eq_poly(zetas[i], a) * xi_eq_evals[i]) - .collect::>() + let table = compute_hypercube_eq_evals(log_n, zetas[i]); + let scale = xi_eq_evals[i]; + table.into_iter().map(|v| v * scale).collect::>() }) .collect::>(); @@ -743,9 +747,7 @@ impl< let tau = &beta.0[0]; - let tau_zero_evader = Ascending::new(tau.len()) - .map(|p| eq_poly(tau, p.index)) - .collect::>(); + let tau_zero_evader = compute_hypercube_eq_evals(tau.len(), tau); let mut z = beta.1[0].clone(); z.extend(w[0].clone()); diff --git a/src/utils/poly.rs b/src/utils/poly.rs index 2725119..dff2c34 100644 --- a/src/utils/poly.rs +++ b/src/utils/poly.rs @@ -1,23 +1,7 @@ -use ark_ff::Field; - -/// `eq(τ, y)` where `y ∈ {0,1}^{τ.len()}` is the Boolean point whose bit `j` -/// is `(point >> j) & 1`. Matches the LSB-indexed formula used by -/// [`effsc::hypercube::compute_hypercube_eq_evals`], so -/// `eq_poly(τ, i) == compute_hypercube_eq_evals(τ.len(), τ)[i]`. -pub fn eq_poly(tau: &[F], point: usize) -> F { - let num_variables = tau.len(); - (0..num_variables).fold(F::one(), |acc, j| { - if (point >> j) & 1 == 1 { - acc * tau[j] - } else { - acc * (F::one() - tau[j]) - } - }) -} - -pub fn eq_poly_non_binary(x: &[F], y: &[F]) -> F { - assert_eq!(x.len(), y.len()); - x.iter().zip(y).fold(F::one(), |acc, (x_i, y_i)| { - acc * (*x_i * *y_i + (F::one() - x_i) * (F::one() - y_i)) - }) -} +//! Thin re-export of the eq-polynomial helpers now provided by effsc. +//! +//! Kept at this path so existing warp imports (`crate::utils::poly::eq_poly`, +//! `eq_poly_non_binary`) continue to resolve. The library version of +//! `compute_hypercube_eq_evals` is also O(2^v) now — use it instead of the +//! per-point `eq_poly` whenever the full table is needed. +pub use effsc::hypercube::{eq_poly, eq_poly_non_binary}; From cf0bde3e61b965bbc7e05e6b3580963f3cff0687 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:34:02 +0200 Subject: [PATCH 3/7] effsc main in cargo toml --- Cargo.toml | 7 +- examples/profile_phases.rs | 199 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 108 +++++++++++++------- src/profile.rs | 49 +++++++++ 4 files changed, 324 insertions(+), 39 deletions(-) create mode 100644 examples/profile_phases.rs create mode 100644 src/profile.rs diff --git a/Cargo.toml b/Cargo.toml index d75499b..b761bc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ serde_json = "1.0" spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "smallfp-support", features = [ "ark-ff", ] } -effsc = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git", branch = "z-tech/rewrite-v2" } +effsc = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git" } thiserror = "2.0.16" ark-codes = { git = "https://github.com/dmpierre/ark-codes.git" } @@ -48,6 +48,11 @@ default = ["asm"] asm = ["ark-ff/asm"] +# Opt-in per-phase wall-time instrumentation inside `prove()`. Records +# durations into a crate-global collector that the `profile_phases` example +# drains; zero-cost when the feature is off. +profile = [] + [[bench]] name = "warp_rs" harness = false diff --git a/examples/profile_phases.rs b/examples/profile_phases.rs new file mode 100644 index 0000000..e530b7a --- /dev/null +++ b/examples/profile_phases.rs @@ -0,0 +1,199 @@ +//! Per-phase wall-time profile of a Goldilocks WARP prove run. Drives the +//! `phase!` instrumentation inside `prove()` behind the `profile` feature. +//! +//! Run: +//! ```text +//! cargo run --release --features profile --example profile_phases +//! ``` +//! +//! The prover is called `WARP_PROFILE_ITERS` times (+ a warmup), per-phase +//! durations are aggregated, and the output is a markdown table suitable for +//! pasting into a PR description. Match the phase set from the +//! `constrained_code_accumulate` rollup in the PR comment shape. + +#[cfg(not(feature = "profile"))] +fn main() { + eprintln!( + "profile_phases: build with --features profile (and --release).\n\ + example: cargo run --release --features profile --example profile_phases" + ); +} + +#[cfg(feature = "profile")] +fn main() { + inner::run(); +} + +#[cfg(feature = "profile")] +mod inner { + use std::collections::BTreeMap; + use std::marker::PhantomData; + use std::time::Duration; + + use ark_codes::reed_solomon::{config::ReedSolomonConfig, ReedSolomon}; + use ark_codes::traits::LinearCode; + use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; + use ark_ff::UniformRand; + use ark_std::rand::thread_rng; + + use warp::config::WARPConfig; + use warp::crypto::merkle::blake3::Blake3MerkleTreeParams; + use warp::relations::r1cs::hashchain::{ + compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness, + }; + use warp::relations::r1cs::R1CS; + use warp::relations::{BundledPESAT, Relation, ToPolySystem}; + use warp::traits::AccumulationScheme; + use warp::utils::fields::Goldilocks; + use warp::utils::poseidon; + use warp::WARP; + + pub fn run() { + let iters = std::env::var("WARP_PROFILE_ITERS") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(5); + let hash_chain_size = std::env::var("WARP_PROFILE_HCSIZE") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(10); + let warmup = 2usize; + + let l1 = 4; + let s = 8; + let t = 7; + let mut rng = thread_rng(); + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + let code_config = + ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); + let code = ReedSolomon::new(code_config); + let n = code.code_len(); + + let (instances, witnesses): (Vec<_>, Vec<_>) = (0..l1) + .map(|_| { + let preimage = vec![Goldilocks::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness { + preimage, + _crhs_scheme: PhantomData::>, + }; + let relation = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (relation.x, relation.w) + }) + .unzip(); + + let warp_config = WARPConfig::new(l1, l1, s, t, r1cs.config(), code.code_len()); + let prover = + WARP::, _, Blake3MerkleTreeParams>::new( + warp_config, + code.clone(), + r1cs.clone(), + (), + (), + ); + + println!( + "=== warp profile (Goldilocks hashchain) — n={n}, l1={l1}, s={s}, t={t}, hashchain_size={hash_chain_size}, iters={iters} (+ {warmup} warmup)" + ); + + // Aggregated timings: phase name -> samples (one per iter), always + // taken as the TOTAL duration for that phase in that iter. + let mut series: BTreeMap<&'static str, Vec> = BTreeMap::new(); + + for i in 0..(warmup + iters) { + let ds = spongefish::domain_separator!("profile_phases"); + let mut ps = ds.without_session().instance(&0u32).std_prover(); + + warp::profile::reset(); + + prover + .prove( + (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), + &mut ps, + witnesses.clone(), + instances.clone(), + (vec![], vec![], vec![], (vec![], vec![]), vec![]), + (vec![], vec![], vec![]), + ) + .unwrap(); + + if i < warmup { + continue; + } + + // Collapse duplicate phase entries within this iter (e.g. if a + // phase fires more than once we want the sum). + let mut per_iter: BTreeMap<&'static str, Duration> = BTreeMap::new(); + for (name, dur) in warp::profile::drain() { + *per_iter.entry(name).or_default() += dur; + } + for (name, dur) in per_iter { + series.entry(name).or_default().push(dur); + } + } + + // Phases in PR-shape order + which rollups they belong to. + let phases: &[(&str, &str)] = &[ + ("rs_encode", "rs_encode (FFT)"), + ("pesat_merkle_tree", "pesat_merkle_tree"), + ("twin_constraint_sumcheck", "**twin_constraint_sumcheck**"), + ("eval_bundled_r1cs", "eval_bundled_r1cs"), + ("merkle_commit", "merkle_commit"), + ( + "eq_poly_evals_and_ood_evals_vec", + "eq_poly_evals + ood_evals_vec", + ), + ("batching_sumcheck", "batching_sumcheck (inner product)"), + ]; + let mean_ms = |samples: &[Duration]| -> Option { + if samples.is_empty() { + return None; + } + let sum_ns: u128 = samples.iter().map(|d| d.as_nanos()).sum(); + Some((sum_ns as f64 / samples.len() as f64) / 1e6) + }; + + println!(); + println!("| Phase | Mean (ms) |"); + println!("|-------|-----------|"); + for (key, label) in phases { + let samples = series.get(*key).cloned().unwrap_or_default(); + match mean_ms(&samples) { + Some(m) => println!("| {label} | {m:.2} |"), + None => println!("| {label} | n/a |"), + } + } + + // Rollups: real wall-time around `pesat_reduce` and + // `constrained_code_accumulate`, recorded as whole-block phases + // inside `prove()`. `prove_total` is the full wall time. + let cca = series + .get("constrained_code_accumulate") + .cloned() + .unwrap_or_default(); + let total = series.get("prove_total").cloned().unwrap_or_default(); + + println!(); + if let Some(m) = mean_ms(&cca) { + println!("| **Total (constrained_code_accumulate)** | **{m:.2} ms** |"); + } + if let Some(m) = mean_ms(&total) { + println!("| **Total (incl. pesat_reduce)** | **{m:.2} ms** |"); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index ae111ba..58a8d54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ pub mod config; pub mod constraints; pub mod crypto; pub mod error; +pub mod profile; pub mod protocol; pub mod relations; pub mod serialize; @@ -281,6 +282,11 @@ impl< debug_assert_eq!(witnesses.len(), instances.len()); debug_assert_eq!(acc_witnesses.0.len(), acc_instances.0.len()); + // Rollup checkpoints for the PR-shape profile table — real wall + // times, not a sum of the inner phases below. + #[cfg(feature = "profile")] + let __prove_start = std::time::Instant::now(); + let (l1, l) = (self.config.l1, self.config.l); let l2 = l - l1; debug_assert_eq!(l1 + l2, l); @@ -310,17 +316,21 @@ impl< let log_n = log2(n) as usize; // a. encode witnesses - let (codewords, leaves) = build_codeword_leaves(&self.code, &witnesses, l1); + let (codewords, leaves) = crate::phase!("rs_encode", { + build_codeword_leaves(&self.code, &witnesses, l1) + }); // b. evaluation claims let mus = codewords.iter().map(|f| f[0]).collect::>(); // c. commit to witnesses - let td_0 = MerkleTree::::new( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - leaves.chunks_exact(l1).collect::>(), - )?; + let td_0 = crate::phase!("pesat_merkle_tree", { + MerkleTree::::new( + &self.mt_leaf_hash_params, + &self.mt_two_to_one_hash_params, + leaves.chunks_exact(l1).collect::>(), + )? + }); // d. absorb commitment and code evaluations let root_bytes: [u8; 32] = td_0 @@ -336,6 +346,9 @@ impl< .map(|_| prover_state.verifier_messages_vec::(log_M)) .collect::>(); + #[cfg(feature = "profile")] + let __pesat_done = std::time::Instant::now(); + //////////////////////// // 3. Constrained Code Accumulation //////////////////////// @@ -385,7 +398,9 @@ impl< degree, }; let mut cc = CoefficientProverLSB::new(&evaluator, tablewise, pw); - let gamma = sumcheck(&mut cc, log_l, prover_state, noop_hook).challenges; + let gamma = crate::phase!("twin_constraint_sumcheck", { + sumcheck(&mut cc, log_l, prover_state, noop_hook).challenges + }); debug_assert_eq!(gamma.len(), log_l); // e. new oracle and target — after log_l rounds each group has one @@ -399,12 +414,12 @@ impl< let beta_tau = reduced_tw[3][0].clone(); // eval the bundled r1cs - let beta_eq_evals = compute_hypercube_eq_evals(log_M, &beta_tau); - - let eta = self - .p - .evaluate_bundled(&beta_eq_evals, &z) - .map_err(|_| ProverError::SpongeFish)?; + let eta = crate::phase!("eval_bundled_r1cs", { + let beta_eq_evals = compute_hypercube_eq_evals(log_M, &beta_tau); + self.p + .evaluate_bundled(&beta_eq_evals, &z) + .map_err(|_| ProverError::SpongeFish)? + }); let (x, w) = z.split_at(N - k); let beta = (vec![beta_tau], vec![x.to_vec()]); @@ -412,11 +427,13 @@ impl< let nu_0 = f_hat.fix_variables(&zeta_0)[0]; // f. new commitment - let td = MerkleTree::::new( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - f.chunks(1).collect::>(), - )?; + let td = crate::phase!("merkle_commit", { + MerkleTree::::new( + &self.mt_leaf_hash_params, + &self.mt_two_to_one_hash_params, + f.chunks(1).collect::>(), + )? + }); // g. absorb new commitment and target let td_root_bytes: [u8; 32] = td @@ -478,33 +495,37 @@ impl< // eq-evals table are consumed (`r ≤ 2^log_r`). The incremental // builder is still a net win: O(2^log_r) ≤ O(2r) vs the previous // O(r · log_r). - let xi_eq_evals: Vec = compute_hypercube_eq_evals(log_r, &xis) - .into_iter() - .take(r) - .collect(); + let batched_g = crate::phase!("eq_poly_evals_and_ood_evals_vec", { + let xi_eq_evals: Vec = compute_hypercube_eq_evals(log_r, &xis) + .into_iter() + .take(r) + .collect(); - let ood_evals_vec = (0..1 + self.config.s) - .map(|i| { - let table = compute_hypercube_eq_evals(log_n, zetas[i]); - let scale = xi_eq_evals[i]; - table.into_iter().map(|v| v * scale).collect::>() - }) - .collect::>(); + let ood_evals_vec = (0..1 + self.config.s) + .map(|i| { + let table = compute_hypercube_eq_evals(log_n, zetas[i]); + let scale = xi_eq_evals[i]; + table.into_iter().map(|v| v * scale).collect::>() + }) + .collect::>(); + + // [CBBZ23] optimization from hyperplonk + let id_non_0_eval_sums = + accumulate_sparse_evaluations(zetas, xi_eq_evals, self.config.s, r); - // [CBBZ23] optimization from hyperplonk - let id_non_0_eval_sums = - accumulate_sparse_evaluations(zetas, xi_eq_evals, self.config.s, r); + batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums) + }); // Batching inner-product sumcheck via the new `SumcheckProver` trait. // `InnerProductProver` is MSB half-split, so the challenge vector is // returned in MSB order; reverse once to arkworks' LSB-first // convention before feeding it to the MLE. - let mut ip = InnerProductProver::new( - f.clone(), - batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), - ); - let mut alpha = sumcheck(&mut ip, log_n, prover_state, noop_hook).challenges; - alpha.reverse(); + let alpha = crate::phase!("batching_sumcheck", { + let mut ip = InnerProductProver::new(f.clone(), batched_g); + let mut alpha = sumcheck(&mut ip, log_n, prover_state, noop_hook).challenges; + alpha.reverse(); + alpha + }); // m. new target let mu = f_hat.fix_variables(&alpha)[0]; @@ -534,6 +555,17 @@ impl< let acc_instance = (vec![td.root()], vec![alpha], vec![mu], beta, vec![eta]); let acc_witness = (vec![td], vec![f], vec![w.to_vec()]); + #[cfg(feature = "profile")] + { + let end = std::time::Instant::now(); + crate::profile::record("pesat_reduce", __pesat_done.duration_since(__prove_start)); + crate::profile::record( + "constrained_code_accumulate", + end.duration_since(__pesat_done), + ); + crate::profile::record("prove_total", end.duration_since(__prove_start)); + } + // 4. return Ok(( (acc_instance, acc_witness), diff --git a/src/profile.rs b/src/profile.rs new file mode 100644 index 0000000..c857906 --- /dev/null +++ b/src/profile.rs @@ -0,0 +1,49 @@ +//! Per-phase wall-time collector for the `profile` feature. +//! +//! Consumer pattern: +//! ```ignore +//! warp::profile::reset(); +//! warp.prove(...)?; +//! for (name, dur) in warp::profile::drain() { ... } +//! ``` +//! +//! With the feature off, [`phase!`] is a zero-cost block and `reset` / +//! `drain` don't exist — callers should also `#[cfg(feature = "profile")]`. + +#[cfg(feature = "profile")] +use std::sync::Mutex; +#[cfg(feature = "profile")] +use std::time::Duration; + +#[cfg(feature = "profile")] +static TIMINGS: Mutex> = Mutex::new(Vec::new()); + +#[cfg(feature = "profile")] +pub fn record(name: &'static str, dur: Duration) { + TIMINGS.lock().unwrap().push((name, dur)); +} + +#[cfg(feature = "profile")] +pub fn reset() { + TIMINGS.lock().unwrap().clear(); +} + +#[cfg(feature = "profile")] +pub fn drain() -> Vec<(&'static str, Duration)> { + std::mem::take(&mut *TIMINGS.lock().unwrap()) +} + +/// Wrap an expression block to record its wall-time under `name`. No-op +/// when `profile` is off (the block runs identically but the timing scaffold +/// is elided by the compiler). +#[macro_export] +macro_rules! phase { + ($name:expr, $body:block) => {{ + #[cfg(feature = "profile")] + let __phase_t0 = ::std::time::Instant::now(); + let __phase_res = $body; + #[cfg(feature = "profile")] + $crate::profile::record($name, __phase_t0.elapsed()); + __phase_res + }}; +} From 687ab8aec231871100fabaa1db6e2f3a0c641c31 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:08:00 +0200 Subject: [PATCH 4/7] remove profiling --- Cargo.toml | 5 - examples/profile_phases.rs | 199 ------------------------------- src/lib.rs | 165 ++++++++++++------------- src/profile.rs | 49 -------- src/protocol/domainsep/mod.rs | 89 -------------- src/protocol/effsc_transcript.rs | 25 ---- src/protocol/mod.rs | 23 +++- src/utils/mod.rs | 1 - src/utils/poly.rs | 7 -- 9 files changed, 98 insertions(+), 465 deletions(-) delete mode 100644 examples/profile_phases.rs delete mode 100644 src/profile.rs delete mode 100644 src/protocol/effsc_transcript.rs delete mode 100644 src/utils/poly.rs diff --git a/Cargo.toml b/Cargo.toml index b761bc4..dfbec31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,11 +48,6 @@ default = ["asm"] asm = ["ark-ff/asm"] -# Opt-in per-phase wall-time instrumentation inside `prove()`. Records -# durations into a crate-global collector that the `profile_phases` example -# drains; zero-cost when the feature is off. -profile = [] - [[bench]] name = "warp_rs" harness = false diff --git a/examples/profile_phases.rs b/examples/profile_phases.rs deleted file mode 100644 index e530b7a..0000000 --- a/examples/profile_phases.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! Per-phase wall-time profile of a Goldilocks WARP prove run. Drives the -//! `phase!` instrumentation inside `prove()` behind the `profile` feature. -//! -//! Run: -//! ```text -//! cargo run --release --features profile --example profile_phases -//! ``` -//! -//! The prover is called `WARP_PROFILE_ITERS` times (+ a warmup), per-phase -//! durations are aggregated, and the output is a markdown table suitable for -//! pasting into a PR description. Match the phase set from the -//! `constrained_code_accumulate` rollup in the PR comment shape. - -#[cfg(not(feature = "profile"))] -fn main() { - eprintln!( - "profile_phases: build with --features profile (and --release).\n\ - example: cargo run --release --features profile --example profile_phases" - ); -} - -#[cfg(feature = "profile")] -fn main() { - inner::run(); -} - -#[cfg(feature = "profile")] -mod inner { - use std::collections::BTreeMap; - use std::marker::PhantomData; - use std::time::Duration; - - use ark_codes::reed_solomon::{config::ReedSolomonConfig, ReedSolomon}; - use ark_codes::traits::LinearCode; - use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; - use ark_ff::UniformRand; - use ark_std::rand::thread_rng; - - use warp::config::WARPConfig; - use warp::crypto::merkle::blake3::Blake3MerkleTreeParams; - use warp::relations::r1cs::hashchain::{ - compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness, - }; - use warp::relations::r1cs::R1CS; - use warp::relations::{BundledPESAT, Relation, ToPolySystem}; - use warp::traits::AccumulationScheme; - use warp::utils::fields::Goldilocks; - use warp::utils::poseidon; - use warp::WARP; - - pub fn run() { - let iters = std::env::var("WARP_PROFILE_ITERS") - .ok() - .and_then(|s| s.parse::().ok()) - .unwrap_or(5); - let hash_chain_size = std::env::var("WARP_PROFILE_HCSIZE") - .ok() - .and_then(|s| s.parse::().ok()) - .unwrap_or(10); - let warmup = 2usize; - - let l1 = 4; - let s = 8; - let t = 7; - let mut rng = thread_rng(); - let poseidon_config = poseidon::initialize_poseidon_config::(); - let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( - poseidon_config.clone(), - hash_chain_size, - )) - .unwrap(); - let code_config = - ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); - let code = ReedSolomon::new(code_config); - let n = code.code_len(); - - let (instances, witnesses): (Vec<_>, Vec<_>) = (0..l1) - .map(|_| { - let preimage = vec![Goldilocks::rand(&mut rng)]; - let instance = HashChainInstance { - digest: compute_hash_chain::>( - &poseidon_config, - &preimage, - hash_chain_size, - ), - }; - let witness = HashChainWitness { - preimage, - _crhs_scheme: PhantomData::>, - }; - let relation = HashChainRelation::, CRHGadget<_>>::new( - instance, - witness, - (poseidon_config.clone(), hash_chain_size), - ); - (relation.x, relation.w) - }) - .unzip(); - - let warp_config = WARPConfig::new(l1, l1, s, t, r1cs.config(), code.code_len()); - let prover = - WARP::, _, Blake3MerkleTreeParams>::new( - warp_config, - code.clone(), - r1cs.clone(), - (), - (), - ); - - println!( - "=== warp profile (Goldilocks hashchain) — n={n}, l1={l1}, s={s}, t={t}, hashchain_size={hash_chain_size}, iters={iters} (+ {warmup} warmup)" - ); - - // Aggregated timings: phase name -> samples (one per iter), always - // taken as the TOTAL duration for that phase in that iter. - let mut series: BTreeMap<&'static str, Vec> = BTreeMap::new(); - - for i in 0..(warmup + iters) { - let ds = spongefish::domain_separator!("profile_phases"); - let mut ps = ds.without_session().instance(&0u32).std_prover(); - - warp::profile::reset(); - - prover - .prove( - (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), - &mut ps, - witnesses.clone(), - instances.clone(), - (vec![], vec![], vec![], (vec![], vec![]), vec![]), - (vec![], vec![], vec![]), - ) - .unwrap(); - - if i < warmup { - continue; - } - - // Collapse duplicate phase entries within this iter (e.g. if a - // phase fires more than once we want the sum). - let mut per_iter: BTreeMap<&'static str, Duration> = BTreeMap::new(); - for (name, dur) in warp::profile::drain() { - *per_iter.entry(name).or_default() += dur; - } - for (name, dur) in per_iter { - series.entry(name).or_default().push(dur); - } - } - - // Phases in PR-shape order + which rollups they belong to. - let phases: &[(&str, &str)] = &[ - ("rs_encode", "rs_encode (FFT)"), - ("pesat_merkle_tree", "pesat_merkle_tree"), - ("twin_constraint_sumcheck", "**twin_constraint_sumcheck**"), - ("eval_bundled_r1cs", "eval_bundled_r1cs"), - ("merkle_commit", "merkle_commit"), - ( - "eq_poly_evals_and_ood_evals_vec", - "eq_poly_evals + ood_evals_vec", - ), - ("batching_sumcheck", "batching_sumcheck (inner product)"), - ]; - let mean_ms = |samples: &[Duration]| -> Option { - if samples.is_empty() { - return None; - } - let sum_ns: u128 = samples.iter().map(|d| d.as_nanos()).sum(); - Some((sum_ns as f64 / samples.len() as f64) / 1e6) - }; - - println!(); - println!("| Phase | Mean (ms) |"); - println!("|-------|-----------|"); - for (key, label) in phases { - let samples = series.get(*key).cloned().unwrap_or_default(); - match mean_ms(&samples) { - Some(m) => println!("| {label} | {m:.2} |"), - None => println!("| {label} | n/a |"), - } - } - - // Rollups: real wall-time around `pesat_reduce` and - // `constrained_code_accumulate`, recorded as whole-block phases - // inside `prove()`. `prove_total` is the full wall time. - let cca = series - .get("constrained_code_accumulate") - .cloned() - .unwrap_or_default(); - let total = series.get("prove_total").cloned().unwrap_or_default(); - - println!(); - if let Some(m) = mean_ms(&cca) { - println!("| **Total (constrained_code_accumulate)** | **{m:.2} ms** |"); - } - if let Some(m) = mean_ms(&total) { - println!("| **Total (incl. pesat_reduce)** | **{m:.2} ms** |"); - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 58a8d54..f2eb43f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,13 +17,13 @@ use crypto::merkle::compute_auth_paths; use effsc::{ coefficient_sumcheck::RoundPolyEvaluator, folding::protogalaxy, - hypercube::compute_hypercube_eq_evals, + hypercube::{compute_hypercube_eq_evals, eq_poly_non_binary}, noop_hook, provers::{coefficient_lsb::CoefficientProverLSB, inner_product::InnerProductProver}, runner::sumcheck, verifier::sumcheck_verify, }; -use protocol::domainsep::{derive_between_sumchecks, derive_pre_twin_constraint, parse_statement}; +use protocol::domainsep::parse_statement; use protocol::EffscVerifierTranscript; use relations::{r1cs::R1CSConstraints, BundledPESAT}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; @@ -31,8 +31,8 @@ use std::collections::HashMap; use std::marker::PhantomData; use utils::binary_field_elements_to_usize; use utils::byte_to_binary_field_array; +use utils::concat_slices; use utils::scale_and_sum; -use utils::{concat_slices, poly::eq_poly_non_binary}; use config::WARPConfig; use protocol::domainsep::{absorb_accumulated_instances, absorb_instances}; @@ -41,7 +41,6 @@ pub mod config; pub mod constraints; pub mod crypto; pub mod error; -pub mod profile; pub mod protocol; pub mod relations; pub mod serialize; @@ -282,11 +281,6 @@ impl< debug_assert_eq!(witnesses.len(), instances.len()); debug_assert_eq!(acc_witnesses.0.len(), acc_instances.0.len()); - // Rollup checkpoints for the PR-shape profile table — real wall - // times, not a sum of the inner phases below. - #[cfg(feature = "profile")] - let __prove_start = std::time::Instant::now(); - let (l1, l) = (self.config.l1, self.config.l); let l2 = l - l1; debug_assert_eq!(l1 + l2, l); @@ -316,21 +310,17 @@ impl< let log_n = log2(n) as usize; // a. encode witnesses - let (codewords, leaves) = crate::phase!("rs_encode", { - build_codeword_leaves(&self.code, &witnesses, l1) - }); + let (codewords, leaves) = build_codeword_leaves(&self.code, &witnesses, l1); // b. evaluation claims let mus = codewords.iter().map(|f| f[0]).collect::>(); // c. commit to witnesses - let td_0 = crate::phase!("pesat_merkle_tree", { - MerkleTree::::new( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - leaves.chunks_exact(l1).collect::>(), - )? - }); + let td_0 = MerkleTree::::new( + &self.mt_leaf_hash_params, + &self.mt_two_to_one_hash_params, + leaves.chunks_exact(l1).collect::>(), + )?; // d. absorb commitment and code evaluations let root_bytes: [u8; 32] = td_0 @@ -346,9 +336,6 @@ impl< .map(|_| prover_state.verifier_messages_vec::(log_M)) .collect::>(); - #[cfg(feature = "profile")] - let __pesat_done = std::time::Instant::now(); - //////////////////////// // 3. Constrained Code Accumulation //////////////////////// @@ -398,9 +385,7 @@ impl< degree, }; let mut cc = CoefficientProverLSB::new(&evaluator, tablewise, pw); - let gamma = crate::phase!("twin_constraint_sumcheck", { - sumcheck(&mut cc, log_l, prover_state, noop_hook).challenges - }); + let gamma = sumcheck(&mut cc, log_l, prover_state, noop_hook).challenges; debug_assert_eq!(gamma.len(), log_l); // e. new oracle and target — after log_l rounds each group has one @@ -414,12 +399,11 @@ impl< let beta_tau = reduced_tw[3][0].clone(); // eval the bundled r1cs - let eta = crate::phase!("eval_bundled_r1cs", { - let beta_eq_evals = compute_hypercube_eq_evals(log_M, &beta_tau); - self.p - .evaluate_bundled(&beta_eq_evals, &z) - .map_err(|_| ProverError::SpongeFish)? - }); + let beta_eq_evals = compute_hypercube_eq_evals(log_M, &beta_tau); + let eta = self + .p + .evaluate_bundled(&beta_eq_evals, &z) + .map_err(|_| ProverError::SpongeFish)?; let (x, w) = z.split_at(N - k); let beta = (vec![beta_tau], vec![x.to_vec()]); @@ -427,13 +411,11 @@ impl< let nu_0 = f_hat.fix_variables(&zeta_0)[0]; // f. new commitment - let td = crate::phase!("merkle_commit", { - MerkleTree::::new( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - f.chunks(1).collect::>(), - )? - }); + let td = MerkleTree::::new( + &self.mt_leaf_hash_params, + &self.mt_two_to_one_hash_params, + f.chunks(1).collect::>(), + )?; // g. absorb new commitment and target let td_root_bytes: [u8; 32] = td @@ -492,40 +474,33 @@ impl< // compute evaluations for xi // `xis` has `log_r` elements but only the first `r` entries of the - // eq-evals table are consumed (`r ≤ 2^log_r`). The incremental - // builder is still a net win: O(2^log_r) ≤ O(2r) vs the previous - // O(r · log_r). - let batched_g = crate::phase!("eq_poly_evals_and_ood_evals_vec", { - let xi_eq_evals: Vec = compute_hypercube_eq_evals(log_r, &xis) - .into_iter() - .take(r) - .collect(); - - let ood_evals_vec = (0..1 + self.config.s) - .map(|i| { - let table = compute_hypercube_eq_evals(log_n, zetas[i]); - let scale = xi_eq_evals[i]; - table.into_iter().map(|v| v * scale).collect::>() - }) - .collect::>(); - - // [CBBZ23] optimization from hyperplonk - let id_non_0_eval_sums = - accumulate_sparse_evaluations(zetas, xi_eq_evals, self.config.s, r); - - batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums) - }); - - // Batching inner-product sumcheck via the new `SumcheckProver` trait. - // `InnerProductProver` is MSB half-split, so the challenge vector is - // returned in MSB order; reverse once to arkworks' LSB-first - // convention before feeding it to the MLE. - let alpha = crate::phase!("batching_sumcheck", { - let mut ip = InnerProductProver::new(f.clone(), batched_g); - let mut alpha = sumcheck(&mut ip, log_n, prover_state, noop_hook).challenges; - alpha.reverse(); - alpha - }); + // eq-evals table are consumed (`r ≤ 2^log_r`). + let xi_eq_evals: Vec = compute_hypercube_eq_evals(log_r, &xis) + .into_iter() + .take(r) + .collect(); + + let ood_evals_vec = (0..1 + self.config.s) + .map(|i| { + let table = compute_hypercube_eq_evals(log_n, zetas[i]); + let scale = xi_eq_evals[i]; + table.into_iter().map(|v| v * scale).collect::>() + }) + .collect::>(); + + // [CBBZ23] optimization from hyperplonk + let id_non_0_eval_sums = + accumulate_sparse_evaluations(zetas, xi_eq_evals, self.config.s, r); + + // Batching inner-product sumcheck. `InnerProductProver` is MSB + // half-split, so the challenge vector is returned in MSB order; + // reverse once to arkworks' LSB-first convention. + let mut ip = InnerProductProver::new( + f.clone(), + batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), + ); + let mut alpha = sumcheck(&mut ip, log_n, prover_state, noop_hook).challenges; + alpha.reverse(); // m. new target let mu = f_hat.fix_variables(&alpha)[0]; @@ -555,17 +530,6 @@ impl< let acc_instance = (vec![td.root()], vec![alpha], vec![mu], beta, vec![eta]); let acc_witness = (vec![td], vec![f], vec![w.to_vec()]); - #[cfg(feature = "profile")] - { - let end = std::time::Instant::now(); - crate::profile::record("pesat_reduce", __pesat_done.duration_since(__prove_start)); - crate::profile::record( - "constrained_code_accumulate", - end.duration_since(__pesat_done), - ); - crate::profile::record("prove_total", end.duration_since(__prove_start)); - } - // 4. return Ok(( (acc_instance, acc_witness), @@ -604,9 +568,22 @@ impl< let (l1_xs, (l2_roots, l2_alphas, l2_mus, (l2_taus, l2_xs), l2_etas)) = parse_statement::(verifier_state, l1, l2, N - k, log_n, log_M)?; - // 2. Pre-twin-constraint transcript reads. - let (rt_0, l1_mus, l1_taus, omega, tau) = - derive_pre_twin_constraint::(verifier_state, l1, log_l, log_M)?; + // 2. Pre-twin-constraint transcript reads: PESAT commit + l1 state, + // then squeeze ω and τ. + let rt_0_bytes: [u8; 32] = verifier_state.prover_message()?; + let rt_0: MT::InnerDigest = rt_0_bytes.into(); + let l1_mus: Vec = verifier_state.prover_messages_vec(l1)?; + let l1_taus: Vec> = (0..l1) + .map(|_| { + (0..log_M) + .map(|_| verifier_state.verifier_message::()) + .collect() + }) + .collect(); + let omega: F = verifier_state.verifier_message(); + let tau: Vec = (0..log_l) + .map(|_| verifier_state.verifier_message::()) + .collect(); // 3. σ₁ = Σ_i τ_eq(i) · (μ_i + ω·η_i). let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); @@ -637,8 +614,20 @@ impl< }; // 5. Between-sumchecks reads: td, η, ν₀, OOD, shift bytes, ξ. - let (_td, eta, mut nus, ood_samples, bytes_shift_queries, xi) = - derive_between_sumchecks::(verifier_state, log_n, self.config.s, self.config.t)?; + let _td: [u8; 32] = verifier_state.prover_message()?; + let eta: F = verifier_state.prover_message()?; + let nu_0: F = verifier_state.prover_message()?; + let mut nus = vec![nu_0]; + let ood_samples: Vec = (0..self.config.s * log_n) + .map(|_| verifier_state.verifier_message::()) + .collect(); + nus.extend(verifier_state.prover_messages_vec::(self.config.s)?); + let bytes_shift_queries: Vec = (0..(self.config.t * log_n).div_ceil(8)) + .map(|_| verifier_state.verifier_message::<[u8; 1]>()[0]) + .collect(); + let xi: Vec = (0..log_r) + .map(|_| verifier_state.verifier_message::()) + .collect(); // 6. Deferred twin-constraint oracle check. (eq_poly_non_binary(&tau, &gamma_sumcheck) * (nus[0] + omega * eta) == tc_final_claim) diff --git a/src/profile.rs b/src/profile.rs deleted file mode 100644 index c857906..0000000 --- a/src/profile.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Per-phase wall-time collector for the `profile` feature. -//! -//! Consumer pattern: -//! ```ignore -//! warp::profile::reset(); -//! warp.prove(...)?; -//! for (name, dur) in warp::profile::drain() { ... } -//! ``` -//! -//! With the feature off, [`phase!`] is a zero-cost block and `reset` / -//! `drain` don't exist — callers should also `#[cfg(feature = "profile")]`. - -#[cfg(feature = "profile")] -use std::sync::Mutex; -#[cfg(feature = "profile")] -use std::time::Duration; - -#[cfg(feature = "profile")] -static TIMINGS: Mutex> = Mutex::new(Vec::new()); - -#[cfg(feature = "profile")] -pub fn record(name: &'static str, dur: Duration) { - TIMINGS.lock().unwrap().push((name, dur)); -} - -#[cfg(feature = "profile")] -pub fn reset() { - TIMINGS.lock().unwrap().clear(); -} - -#[cfg(feature = "profile")] -pub fn drain() -> Vec<(&'static str, Duration)> { - std::mem::take(&mut *TIMINGS.lock().unwrap()) -} - -/// Wrap an expression block to record its wall-time under `name`. No-op -/// when `profile` is off (the block runs identically but the timing scaffold -/// is elided by the compiler). -#[macro_export] -macro_rules! phase { - ($name:expr, $body:block) => {{ - #[cfg(feature = "profile")] - let __phase_t0 = ::std::time::Instant::now(); - let __phase_res = $body; - #[cfg(feature = "profile")] - $crate::profile::record($name, __phase_t0.elapsed()); - __phase_res - }}; -} diff --git a/src/protocol/domainsep/mod.rs b/src/protocol/domainsep/mod.rs index 988d039..0084212 100644 --- a/src/protocol/domainsep/mod.rs +++ b/src/protocol/domainsep/mod.rs @@ -1,6 +1,5 @@ use ark_crypto_primitives::merkle_tree::Config; use ark_ff::Field; -use ark_std::log2; use spongefish::{ Decoding, Encoding, NargDeserialize, ProverState, VerificationResult, VerifierState, @@ -124,91 +123,3 @@ pub fn parse_statement< (l2_roots, l2_alphas, l2_mus, (l2_taus, l2_xs), l2_etas), )) } - -/// Transcript values read before the twin-constraint sumcheck: -/// `(rt_0, l1_mus, l1_taus, omega, tau)`. -pub type PreTwinConstraint = ( - ::InnerDigest, // rt_0 - Vec, // l1_mus - Vec>, // l1_taus - F, // omega - Vec, // tau -); - -pub fn derive_pre_twin_constraint< - F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, - MT: Config + From<[u8; 32]>>, ->( - verifier_state: &mut VerifierState<'_>, - l1: usize, - log_l: usize, - #[allow(non_snake_case)] log_M: usize, -) -> VerificationResult> { - let rt_0_bytes: [u8; 32] = verifier_state.prover_message()?; - let rt_0: MT::InnerDigest = rt_0_bytes.into(); - - let l1_mus: Vec = verifier_state.prover_messages_vec(l1)?; - - let mut l1_taus = Vec::with_capacity(l1); - for _ in 0..l1 { - let tau: Vec = (0..log_M) - .map(|_| verifier_state.verifier_message::()) - .collect(); - l1_taus.push(tau); - } - - let omega: F = verifier_state.verifier_message(); - let tau: Vec = (0..log_l) - .map(|_| verifier_state.verifier_message::()) - .collect(); - - Ok((rt_0, l1_mus, l1_taus, omega, tau)) -} - -/// Transcript values read between the two sumchecks: -/// `(td, eta, nus=[nu_0, ood_answers...], ood_samples, bytes_shift_queries, xi)`. -pub type BetweenSumchecks = ( - ::InnerDigest, // td - F, // eta - Vec, // nus (nu_0 + ood answers) - Vec, // ood_samples (flat, s*log_n long) - Vec, // bytes_shift_queries - Vec, // xi -); - -pub fn derive_between_sumchecks< - F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, - MT: Config + From<[u8; 32]>>, ->( - verifier_state: &mut VerifierState<'_>, - log_n: usize, - s: usize, - t: usize, -) -> VerificationResult> { - let td_bytes: [u8; 32] = verifier_state.prover_message()?; - let td: MT::InnerDigest = td_bytes.into(); - - let eta: F = verifier_state.prover_message()?; - let nu_0: F = verifier_state.prover_message()?; - let mut nus = vec![nu_0]; - - let n_ood_samples = s * log_n; - let ood_samples: Vec = (0..n_ood_samples) - .map(|_| verifier_state.verifier_message::()) - .collect(); - - let ood_answers: Vec = verifier_state.prover_messages_vec(s)?; - nus.extend(ood_answers); - - let r = 1 + s + t; - let log_r = log2(r) as usize; - let n_shift_queries = (t * log_n).div_ceil(8); - let bytes_shift_queries: Vec = (0..n_shift_queries) - .map(|_| verifier_state.verifier_message::<[u8; 1]>()[0]) - .collect(); - let xi: Vec = (0..log_r) - .map(|_| verifier_state.verifier_message::()) - .collect(); - - Ok((td, eta, nus, ood_samples, bytes_shift_queries, xi)) -} diff --git a/src/protocol/effsc_transcript.rs b/src/protocol/effsc_transcript.rs deleted file mode 100644 index b0c7ace..0000000 --- a/src/protocol/effsc_transcript.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Adapter plugging spongefish's [`VerifierState`] into effsc's -//! [`VerifierTranscript`] trait, so warp's verifier can call -//! [`effsc::verifier::sumcheck_verify`]. Prover-side is already handled by -//! a blanket impl inside effsc (on `spongefish::ProverState`), so no -//! wrapper is needed there. - -use effsc::transcript::VerifierTranscript; -use spongefish::{Decoding, Encoding, NargDeserialize, VerifierState}; - -pub struct EffscVerifierTranscript<'s, 'a>(pub &'s mut VerifierState<'a>); - -impl<'s, 'a, F> VerifierTranscript for EffscVerifierTranscript<'s, 'a> -where - F: ark_ff::Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, -{ - type Error = spongefish::VerificationError; - - fn receive(&mut self) -> Result { - self.0.prover_message::() - } - - fn challenge(&mut self) -> F { - self.0.verifier_message::() - } -} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 5057652..6a13b4e 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,4 +1,23 @@ pub mod domainsep; -pub mod effsc_transcript; -pub use effsc_transcript::EffscVerifierTranscript; +use effsc::transcript::VerifierTranscript; +use spongefish::{Decoding, Encoding, NargDeserialize, VerifierState}; + +/// Adapter plugging spongefish's [`VerifierState`] into effsc's +/// [`VerifierTranscript`] so the verifier can call +/// [`effsc::verifier::sumcheck_verify`]. Prover side is covered by a blanket +/// impl inside effsc (on `spongefish::ProverState`). +pub struct EffscVerifierTranscript<'s, 'a>(pub &'s mut VerifierState<'a>); + +impl VerifierTranscript for EffscVerifierTranscript<'_, '_> +where + F: ark_ff::Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, +{ + type Error = spongefish::VerificationError; + fn receive(&mut self) -> Result { + self.0.prover_message::() + } + fn challenge(&mut self) -> F { + self.0.verifier_message::() + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index eb85d34..d134388 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,7 +1,6 @@ use ark_ff::{Field, PrimeField}; pub mod fields; -pub mod poly; pub mod poseidon; pub const fn chunk_size_bytes(modulus_bit_size: u32) -> usize { diff --git a/src/utils/poly.rs b/src/utils/poly.rs deleted file mode 100644 index dff2c34..0000000 --- a/src/utils/poly.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Thin re-export of the eq-polynomial helpers now provided by effsc. -//! -//! Kept at this path so existing warp imports (`crate::utils::poly::eq_poly`, -//! `eq_poly_non_binary`) continue to resolve. The library version of -//! `compute_hypercube_eq_evals` is also O(2^v) now — use it instead of the -//! per-point `eq_poly` whenever the full table is needed. -pub use effsc::hypercube::{eq_poly, eq_poly_non_binary}; From 31c426656b277f83a13ef80499f06c6d56cd006e Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:33:38 +0200 Subject: [PATCH 5/7] effsc wire format --- src/lib.rs | 52 ++++++++++++++--------------------- src/protocol/domainsep/mod.rs | 47 +++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f2eb43f..2a652d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ use effsc::{ runner::sumcheck, verifier::sumcheck_verify, }; -use protocol::domainsep::parse_statement; +use protocol::domainsep::{derive_between_sumchecks, derive_pre_twin_constraint, parse_statement}; use protocol::EffscVerifierTranscript; use relations::{r1cs::R1CSConstraints, BundledPESAT}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; @@ -95,6 +95,15 @@ impl<'a, F: Field> RoundPolyEvaluator for TwinConstraintEvaluator<'a, F> { let (b_even, b_odd) = tw[3]; let (tau_even, tau_odd) = pw[0]; + // CoefficientProverLSB::final_value() invokes the evaluator on fully + // reduced singleton tables with empty odd slices. Warp ignores + // `SumcheckProof::final_value` (only `.challenges` is consumed), so + // bail out — leaves `coeffs` zero, `final_value` returns zero, no + // one reads it. + if u_odd.is_empty() { + return; + } + let f = protogalaxy::fold( a_even.iter().zip(a_odd).map(|(&l, &r)| (l, r - l)), u_even @@ -568,22 +577,9 @@ impl< let (l1_xs, (l2_roots, l2_alphas, l2_mus, (l2_taus, l2_xs), l2_etas)) = parse_statement::(verifier_state, l1, l2, N - k, log_n, log_M)?; - // 2. Pre-twin-constraint transcript reads: PESAT commit + l1 state, - // then squeeze ω and τ. - let rt_0_bytes: [u8; 32] = verifier_state.prover_message()?; - let rt_0: MT::InnerDigest = rt_0_bytes.into(); - let l1_mus: Vec = verifier_state.prover_messages_vec(l1)?; - let l1_taus: Vec> = (0..l1) - .map(|_| { - (0..log_M) - .map(|_| verifier_state.verifier_message::()) - .collect() - }) - .collect(); - let omega: F = verifier_state.verifier_message(); - let tau: Vec = (0..log_l) - .map(|_| verifier_state.verifier_message::()) - .collect(); + // 2. Pre-twin-constraint transcript reads. + let (rt_0, l1_mus, l1_taus, omega, tau) = + derive_pre_twin_constraint::(verifier_state, l1, log_l, log_M)?; // 3. σ₁ = Σ_i τ_eq(i) · (μ_i + ω·η_i). let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); @@ -614,20 +610,14 @@ impl< }; // 5. Between-sumchecks reads: td, η, ν₀, OOD, shift bytes, ξ. - let _td: [u8; 32] = verifier_state.prover_message()?; - let eta: F = verifier_state.prover_message()?; - let nu_0: F = verifier_state.prover_message()?; - let mut nus = vec![nu_0]; - let ood_samples: Vec = (0..self.config.s * log_n) - .map(|_| verifier_state.verifier_message::()) - .collect(); - nus.extend(verifier_state.prover_messages_vec::(self.config.s)?); - let bytes_shift_queries: Vec = (0..(self.config.t * log_n).div_ceil(8)) - .map(|_| verifier_state.verifier_message::<[u8; 1]>()[0]) - .collect(); - let xi: Vec = (0..log_r) - .map(|_| verifier_state.verifier_message::()) - .collect(); + let (_td, eta, mut nus, ood_samples, bytes_shift_queries, xi) = + derive_between_sumchecks::( + verifier_state, + log_n, + self.config.s, + self.config.t, + log_r, + )?; // 6. Deferred twin-constraint oracle check. (eq_poly_non_binary(&tau, &gamma_sumcheck) * (nus[0] + omega * eta) == tc_final_claim) diff --git a/src/protocol/domainsep/mod.rs b/src/protocol/domainsep/mod.rs index 0084212..32d1386 100644 --- a/src/protocol/domainsep/mod.rs +++ b/src/protocol/domainsep/mod.rs @@ -123,3 +123,50 @@ pub fn parse_statement< (l2_roots, l2_alphas, l2_mus, (l2_taus, l2_xs), l2_etas), )) } + +/// Read `rt_0 + l1_mus`, squeeze `l1_taus + ω + τ`. Runs before the +/// twin-constraint sumcheck on the verifier side. +#[allow(clippy::type_complexity)] +pub fn derive_pre_twin_constraint< + F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, + MT: Config + From<[u8; 32]>>, +>( + vs: &mut VerifierState<'_>, + l1: usize, + log_l: usize, + #[allow(non_snake_case)] log_M: usize, +) -> VerificationResult<(MT::InnerDigest, Vec, Vec>, F, Vec)> { + let rt_0: MT::InnerDigest = <[u8; 32]>::into(vs.prover_message()?); + let l1_mus = vs.prover_messages_vec(l1)?; + let l1_taus = (0..l1) + .map(|_| (0..log_M).map(|_| vs.verifier_message::()).collect()) + .collect(); + let omega = vs.verifier_message(); + let tau = (0..log_l).map(|_| vs.verifier_message::()).collect(); + Ok((rt_0, l1_mus, l1_taus, omega, tau)) +} + +/// Read `td + η + ν₀`, squeeze OOD points, read OOD answers, squeeze shift +/// query bytes and `ξ`. Runs between the two sumchecks on the verifier side. +#[allow(clippy::type_complexity)] +pub fn derive_between_sumchecks< + F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, + MT: Config + From<[u8; 32]>>, +>( + vs: &mut VerifierState<'_>, + log_n: usize, + s: usize, + t: usize, + log_r: usize, +) -> VerificationResult<(MT::InnerDigest, F, Vec, Vec, Vec, Vec)> { + let td: MT::InnerDigest = <[u8; 32]>::into(vs.prover_message()?); + let eta = vs.prover_message()?; + let mut nus = vec![vs.prover_message::()?]; + let ood_samples = (0..s * log_n).map(|_| vs.verifier_message::()).collect(); + nus.extend(vs.prover_messages_vec::(s)?); + let bytes_shift_queries = (0..(t * log_n).div_ceil(8)) + .map(|_| vs.verifier_message::<[u8; 1]>()[0]) + .collect(); + let xi = (0..log_r).map(|_| vs.verifier_message::()).collect(); + Ok((td, eta, nus, ood_samples, bytes_shift_queries, xi)) +} From c12680b10b2c943914337aff242a96a3eed21135 Mon Sep 17 00:00:00 2001 From: Andrew Zitek-Estrada <1497456+z-tech@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:48:59 +0200 Subject: [PATCH 6/7] Apply suggestion from @winderica Co-authored-by: winderica --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 2a652d5..c6c642d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -723,7 +723,7 @@ impl< let betas = l2_taus .into_iter() .chain(l1_taus) - .zip(l2_xs.clone().into_iter().chain(l1_xs)) + .zip(l2_xs.into_iter().chain(l1_xs)) .map(|(tau_i, x)| concat_slices(&tau_i, &x)) .collect::>>(); let beta = scale_and_sum(&betas, &gamma_eq_evals); From a776796fda01c8f02ee3807b389f9bad6b09fafe Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sat, 2 May 2026 12:59:33 +0200 Subject: [PATCH 7/7] bump effsc w/ spongefish 0.7.0 --- Cargo.toml | 38 +++++++-------------- src/crypto/merkle/blake3.rs | 12 +++---- src/lib.rs | 15 ++++---- src/relations/description.rs | 37 +++++++++++--------- src/relations/r1cs/hashchain/mod.rs | 2 +- src/relations/r1cs/hashchain/relation.rs | 10 +++--- src/relations/r1cs/hashchain/synthesizer.rs | 2 +- src/relations/r1cs/mod.rs | 25 +++++++++----- src/serialize.rs | 5 ++- 9 files changed, 74 insertions(+), 72 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dfbec31..c79b099 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,42 +5,30 @@ include = ["Cargo.toml", "src", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] edition = "2021" [dependencies] -ark-crypto-primitives = { version = "0.5.0", features = [ +ark-crypto-primitives = { version = "0.6.0", features = [ "merkle_tree", "crh", - "r1cs", + "constraints", "sponge", + "blake3", ] } -ark-ff = "0.5.0" -ark-poly = "0.5.0" -ark-r1cs-std = "0.5.0" -ark-relations = "0.5.0" -ark-serialize = "0.5.0" -ark-std = "0.5.0" +ark-ff = "0.6.0" +ark-poly = "0.6.0" +ark-r1cs-std = "0.6.0" +ark-relations = "0.6.0" +ark-serialize = "0.6.0" +ark-std = "0.6.0" blake3 = "1.5.0" serde = { version = "1.0.0", features = ["derive"] } serde_json = "1.0" -spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "smallfp-support", features = [ - "ark-ff", -] } +spongefish = { version = "0.7.0", features = ["ark-ff"] } effsc = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git" } thiserror = "2.0.16" -ark-codes = { git = "https://github.com/dmpierre/ark-codes.git" } - - -[patch.crates-io] -ark-crypto-primitives = { git = "https://github.com/benbencik/crypto-primitives.git", branch = "smallfp-absorb-trait" } -ark-ff = { git = "https://github.com/arkworks-rs/algebra.git" } -ark-poly = { git = "https://github.com/arkworks-rs/algebra.git" } -ark-serialize = { git = "https://github.com/arkworks-rs/algebra.git" } - -# resolve transitive pull of spongefish from efficient-sumcheck -[patch."https://github.com/arkworks-rs/spongefish"] -spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "smallfp-support" } +ark-codes = { git = "https://github.com/z-tech/ark-codes.git", branch = "z-tech/arkworks-0.6.0" } [dev-dependencies] -ark-bls12-381 = "0.5.0" -ark-bn254 = "0.5.0" +ark-bls12-381 = "0.6.0" +ark-bn254 = "0.6.0" criterion = "0.8" [features] diff --git a/src/crypto/merkle/blake3.rs b/src/crypto/merkle/blake3.rs index 963a7d4..d2ef41b 100644 --- a/src/crypto/merkle/blake3.rs +++ b/src/crypto/merkle/blake3.rs @@ -1,7 +1,6 @@ use super::parameters::MerkleTreeParams; -use ark_crypto_primitives::crh::blake3::fields::Blake3F; -use ark_crypto_primitives::crh::blake3::Blake3; -use ark_crypto_primitives::crh::blake3::GenericDigest; +use ark_crypto_primitives::crh::blake3::{Blake3CRH, Blake3TwoToOneCRH}; +use ark_crypto_primitives::crh::ByteDigest; use ark_crypto_primitives::{ crh::{CRHScheme, TwoToOneCRHScheme}, merkle_tree::{Config as MerkleConfig, IdentityDigestConverter}, @@ -15,13 +14,14 @@ pub struct Blake3MerkleConfig { _field: PhantomData, } -pub type Blake3MerkleTreeParams = MerkleTreeParams, Blake3, GenericDigest<32>>; +pub type Blake3MerkleTreeParams = + MerkleTreeParams, Blake3TwoToOneCRH, ByteDigest<32>>; impl MerkleConfig for Blake3MerkleConfig { type Leaf = [F]; type LeafDigest = ::Output; type LeafInnerDigestConverter = IdentityDigestConverter; type InnerDigest = ::Output; - type LeafHash = Blake3F; // blake3, over field elements - type TwoToOneHash = Blake3; + type LeafHash = Blake3CRH; + type TwoToOneHash = Blake3TwoToOneCRH; } diff --git a/src/lib.rs b/src/lib.rs index c6c642d..30bf5e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -694,20 +694,20 @@ impl< // `final_claim == μ · Σ eq(ζ_i, α_lsb)·ξ_eq(i)`. MSB half-split // → reverse the challenge vector once before feeding to // eq_poly_non_binary (arkworks MLE convention). - let (alpha_sumcheck_msb, batching_final_claim) = { + let (alpha_sumcheck, batching_final_claim) = { let mut wrap = EffscVerifierTranscript(verifier_state); - let res = sumcheck_verify(sigma_2, 2, log_n, &mut wrap, |_, _| Ok(()))?; + let mut res = sumcheck_verify(sigma_2, 2, log_n, &mut wrap, |_, _| Ok(()))?; + res.challenges.reverse(); (res.challenges, res.final_claim) }; - let alpha_sumcheck_lsb: Vec = alpha_sumcheck_msb.iter().rev().copied().collect(); let mut zeta_eqs = Vec::with_capacity(r); - zeta_eqs.push(eq_poly_non_binary(&zeta_0, &alpha_sumcheck_lsb)); + zeta_eqs.push(eq_poly_non_binary(&zeta_0, &alpha_sumcheck)); for chunk in ood_samples.chunks(log_n) { - zeta_eqs.push(eq_poly_non_binary(chunk, &alpha_sumcheck_lsb)); + zeta_eqs.push(eq_poly_non_binary(chunk, &alpha_sumcheck)); } for zeta in &binary_shift_queries { - zeta_eqs.push(eq_poly_non_binary(zeta, &alpha_sumcheck_lsb)); + zeta_eqs.push(eq_poly_non_binary(zeta, &alpha_sumcheck)); } debug_assert_eq!(zeta_eqs.len(), r); let expected_batching = acc_instance.2[0] @@ -718,7 +718,7 @@ impl< (expected_batching == batching_final_claim).ok_or_err(VerifierError::Target)?; // 10. Accumulator consistency: new α and β. - (acc_instance.1[0] == alpha_sumcheck_lsb).ok_or_err(VerifierError::CodeEvaluationPoint)?; + (acc_instance.1[0] == alpha_sumcheck).ok_or_err(VerifierError::CodeEvaluationPoint)?; let betas = l2_taus .into_iter() @@ -748,7 +748,6 @@ impl< )?; (rt[0] == computed_mt.root()).ok_or_err(DeciderError::MerkleRoot)?; (mt[0].root() == computed_mt.root()).ok_or_err(DeciderError::MerkleTrapDoor)?; - (mt[0].leaf_nodes == computed_mt.leaf_nodes).ok_or_err(DeciderError::MerkleRoot)?; let f_hat = DenseMultilinearExtension::from_evaluations_slice( log2(self.code.code_len()) as usize, diff --git a/src/relations/description.rs b/src/relations/description.rs index 032dfeb..ded163e 100644 --- a/src/relations/description.rs +++ b/src/relations/description.rs @@ -1,5 +1,5 @@ use ark_ff::Field; -use ark_relations::r1cs::{ConstraintMatrices, ConstraintSynthesizer, ConstraintSystem}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem, R1CS_PREDICATE_LABEL}; use serde::Serialize; #[derive(Serialize)] @@ -37,22 +37,27 @@ impl SerializableConstraintMatrices { .generate_constraints(constraint_system.clone()) .unwrap(); constraint_system.finalize(); - let matrices: ConstraintMatrices = constraint_system.to_matrices().unwrap(); - let serializable = SerializableConstraintMatrices::from(matrices); + + let num_instance_variables = constraint_system.num_instance_variables(); + let num_witness_variables = constraint_system.num_witness_variables(); + let num_constraints = constraint_system.num_constraints(); + + let mut matrices = constraint_system.to_matrices().unwrap(); + let mut r1cs = matrices.remove(R1CS_PREDICATE_LABEL).unwrap(); + let mut r1cs_iter = r1cs.drain(..); + let a = r1cs_iter.next().unwrap(); + let b = r1cs_iter.next().unwrap(); + let c = r1cs_iter.next().unwrap(); + + let serializable = SerializableConstraintMatrices { + num_instance_variables, + num_witness_variables, + num_constraints, + a: SerializableConstraintMatrices::serialize_nested_field(a), + b: SerializableConstraintMatrices::serialize_nested_field(b), + c: SerializableConstraintMatrices::serialize_nested_field(c), + }; let serialized = serde_json::to_string(&serializable).unwrap(); serialized.into_bytes() } } - -impl From> for SerializableConstraintMatrices { - fn from(m: ConstraintMatrices) -> Self { - Self { - num_instance_variables: m.num_instance_variables, - num_witness_variables: m.num_witness_variables, - num_constraints: m.num_constraints, - a: SerializableConstraintMatrices::serialize_nested_field(m.a), - b: SerializableConstraintMatrices::serialize_nested_field(m.b), - c: SerializableConstraintMatrices::serialize_nested_field(m.c), - } - } -} diff --git a/src/relations/r1cs/hashchain/mod.rs b/src/relations/r1cs/hashchain/mod.rs index 45c664c..4f34fe3 100644 --- a/src/relations/r1cs/hashchain/mod.rs +++ b/src/relations/r1cs/hashchain/mod.rs @@ -12,7 +12,7 @@ use ark_crypto_primitives::{ }; use ark_ff::PrimeField; use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; pub use config::HashChainConfig; pub use instance::HashChainInstance; pub use relation::compute_hash_chain; diff --git a/src/relations/r1cs/hashchain/relation.rs b/src/relations/r1cs/hashchain/relation.rs index 6742e0a..2bd4999 100644 --- a/src/relations/r1cs/hashchain/relation.rs +++ b/src/relations/r1cs/hashchain/relation.rs @@ -4,7 +4,7 @@ use ark_crypto_primitives::{ }; use ark_ff::{Field, PrimeField}; use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef}; use ark_serialize::CanonicalSerialize; use ark_std::marker::PhantomData; @@ -95,13 +95,15 @@ where constraint_system.finalize(); let cs = constraint_system.into_inner().unwrap(); + let x = cs.instance_assignment().unwrap().to_vec(); + let w = cs.witness_assignment().unwrap().to_vec(); Self { - constraint_system: ConstraintSystemRef::new(cs.clone()), + constraint_system: ConstraintSystemRef::new(cs), config: hash_config, instance, witness, - x: cs.instance_assignment, - w: cs.witness_assignment, + x, + w, _crhs_scheme: PhantomData, _crhs_scheme_gadget: PhantomData, } diff --git a/src/relations/r1cs/hashchain/synthesizer.rs b/src/relations/r1cs/hashchain/synthesizer.rs index beb4a90..511b1ff 100644 --- a/src/relations/r1cs/hashchain/synthesizer.rs +++ b/src/relations/r1cs/hashchain/synthesizer.rs @@ -1,7 +1,7 @@ use ark_crypto_primitives::crh::{CRHScheme, CRHSchemeGadget}; use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget, fields::fp::FpVar}; -use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; use ark_std::marker::PhantomData; use crate::relations::r1cs::hashchain::{HashChainInstance, HashChainWitness}; diff --git a/src/relations/r1cs/mod.rs b/src/relations/r1cs/mod.rs index 72c274f..b6bd5d9 100644 --- a/src/relations/r1cs/mod.rs +++ b/src/relations/r1cs/mod.rs @@ -1,7 +1,7 @@ pub mod hashchain; use ark_ff::Field; -use ark_relations::r1cs::ConstraintSystemRef; +use ark_relations::gr1cs::{ConstraintSystemRef, R1CS_PREDICATE_LABEL}; use effsc::hypercube::Ascending; use crate::error::WARPError; @@ -27,21 +27,30 @@ impl TryFrom> for R1CS { type Error = WARPError; fn try_from(cs: ConstraintSystemRef) -> Result { - let matrices = cs.to_matrices().unwrap(); + let mut matrices = cs.to_matrices().unwrap(); + let mut r1cs = matrices.remove(R1CS_PREDICATE_LABEL).unwrap(); + let mut r1cs_iter = r1cs.drain(..); + let a_mat = r1cs_iter.next().unwrap(); + let b_mat = r1cs_iter.next().unwrap(); + let c_mat = r1cs_iter.next().unwrap(); + + let num_constraints = cs.num_constraints(); + let num_instance_variables = cs.num_instance_variables(); + let num_witness_variables = cs.num_witness_variables(); // number of constraints should be to be power of 2 - let m = matrices.num_constraints.next_power_of_two(); - let n = matrices.num_instance_variables + matrices.num_witness_variables; - let k = matrices.num_witness_variables; + let m = num_constraints.next_power_of_two(); + let n = num_instance_variables + num_witness_variables; + let k = num_witness_variables; // both `unwrap()` calls below are safe since warp/lib.rs forbids compiling on platforms // with 16-bits pointers width let log_m = m.ilog2().try_into().unwrap(); let log_n = n.ilog2().try_into().unwrap(); - let mut a = matrices.a.into_iter(); - let mut b = matrices.b.into_iter(); - let mut c = matrices.c.into_iter(); + let mut a = a_mat.into_iter(); + let mut b = b_mat.into_iter(); + let mut c = c_mat.into_iter(); let mut p = vec![]; for _ in 0..m { // when there are no constraints left, we store an empty one diff --git a/src/serialize.rs b/src/serialize.rs index e03ceff..d9846ff 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -29,7 +29,7 @@ pub struct AccWitnessSerializer< F: Field + PrimeField, MT: Config + From<[u8; 32]>>, > { - pub td: Vec, + pub rt: MT::InnerDigest, pub f: Vec, pub w: Vec, } @@ -42,10 +42,9 @@ impl + Fr assert_eq!(acc_witness.1.len(), 1); assert_eq!(acc_witness.2.len(), 1); let f = acc_witness.1[0].clone(); - assert_eq!(f.len(), acc_witness.0[0].leaf_nodes.len()); let w = acc_witness.2[0].clone(); Self { - td: acc_witness.0[0].clone().leaf_nodes, + rt: acc_witness.0[0].root(), f, w, }