From 3544b1f348f8090a5319e5c4428cade9ed1e5a2b Mon Sep 17 00:00:00 2001 From: Fantoni0 Date: Wed, 11 Jun 2025 10:34:18 +0200 Subject: [PATCH 1/9] Improve compute_h and compute_h2 --- benches/mova_matrix.rs | 2 +- .../src/folding/nova/nifs/pointvsline.rs | 213 ++++++++++++------ 2 files changed, 144 insertions(+), 71 deletions(-) diff --git a/benches/mova_matrix.rs b/benches/mova_matrix.rs index f8025bab2..cbf60f49b 100644 --- a/benches/mova_matrix.rs +++ b/benches/mova_matrix.rs @@ -58,7 +58,7 @@ fn get_instances( fn bench_mova_matrix(c: &mut Criterion) { let mut group = c.benchmark_group("mova_matrix_sequential_folding"); let mut rng = ark_std::test_rng(); - let mat_dim = 4; // 4x4 matrices + let mat_dim = 8; // 4x4 matrices for count in NUM_OF_PRECONDITION_FOLDS { group diff --git a/folding-schemes/src/folding/nova/nifs/pointvsline.rs b/folding-schemes/src/folding/nova/nifs/pointvsline.rs index e79a62c42..7d2e1f788 100644 --- a/folding-schemes/src/folding/nova/nifs/pointvsline.rs +++ b/folding-schemes/src/folding/nova/nifs/pointvsline.rs @@ -10,6 +10,7 @@ use ark_poly::{DenseMultilinearExtension, DenseUVPolynomial, Polynomial}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::{log2, Zero}; use std::fmt::Debug; +use rayon::iter::{ParallelIterator, IntoParallelIterator, IntoParallelRefIterator}; /// Implements the Points vs Line as described in /// [Mova](https://eprint.iacr.org/2024/1220.pdf) and Section 4.5.2 from Thaler’s book @@ -267,33 +268,56 @@ fn compute_h( r1: &[F], r2_sub_r1: &[F], ) -> Result, Error> { - let n_vars: usize = mle.num_vars; - if r1.len() != r2_sub_r1.len() || r1.len() != n_vars { - return Err(Error::NotEqual); - } + let n_vars: usize = mle.num_vars; + if r1.len() != r2_sub_r1.len() || r1.len() != n_vars { + return Err(Error::NotEqual); + } - // Initialize the polynomial vector from the evaluations in the multilinear extension. - // Each evaluation is turned into a constant polynomial. - let mut poly: Vec> = mle - .evaluations - .iter() - .map(|&x| DensePolynomial::from_coefficients_slice(&[x])) - .collect(); - - for (i, (&r1_i, &r2_sub_r1_i)) in r1.iter().zip(r2_sub_r1.iter()).enumerate().take(n_vars) { - // Create a linear polynomial r(X) = r1_i + (r2_sub_r1_i) * X (basically l) - let r = DensePolynomial::from_coefficients_slice(&[r1_i, r2_sub_r1_i]); - let half_len = 1 << (n_vars - i - 1); - - for b in 0..half_len { - let left = &poly[b << 1]; - let right = &poly[(b << 1) + 1]; - poly[b] = left + &(&r * &(right - left)); + // Start with coefficient vectors + let mut coeffs: Vec> = mle + .evaluations + .iter() + .map(|&x| vec![x]) + .collect(); + + for (i, (&r1_i, &r2_sub_r1_i)) in r1.iter().zip(r2_sub_r1.iter()).enumerate().take(n_vars) { + let half_len = 1 << (n_vars - i - 1); + let new_coeffs: Vec> = (0..half_len) + .into_par_iter() + .map(|b| { + let left_idx = b << 1; + let right_idx = left_idx + 1; + + let left_coeffs = &coeffs[left_idx]; + let right_coeffs = &coeffs[right_idx]; + + // Compute (right - left) coefficients + let mut diff_coeffs = vec![F::zero(); right_coeffs.len()]; + for j in 0..right_coeffs.len() { + diff_coeffs[j] = right_coeffs[j] - left_coeffs[j]; + } + + // Multiply by linear polynomial + let mut result_coeffs = vec![F::zero(); diff_coeffs.len() + 1]; + + for j in 0..diff_coeffs.len() { + result_coeffs[j] += diff_coeffs[j] * r1_i; + result_coeffs[j + 1] += diff_coeffs[j] * r2_sub_r1_i; + } + + // Add left polynomial + for j in 0..left_coeffs.len() { + result_coeffs[j] += left_coeffs[j]; + } + + result_coeffs + }) + .collect(); + + coeffs = new_coeffs; } - } - // After the loop, we should be left with a single polynomial, so return it. - Ok(poly.swap_remove(0)) + Ok(DensePolynomial::from_coefficients_vec(coeffs.swap_remove(0))) } /// Implementation for computing h by not following Algorithm 1 "MLE-after-line composition" off the Mova paper @@ -312,78 +336,127 @@ fn compute_h2( match mle { MultilinearExtension::DenseMLE(mle_dense) => { - // following the paper - // Initialize poly as one constant polynomial per evaluation. - let mut poly: Vec> = mle_dense - .evaluations + // Start with evaluations as degree-0 constant polynomials, + // We'll represent polynomials as coefficient vectors instead of DensePolynomials as it's more efficient. + let mut coeffs: Vec> = mle_dense.evaluations .iter() - .map(|&eval| DensePolynomial::from_coefficients_slice(&[eval])) + .map(|&eval| vec![eval]) .collect(); - // For each variable i, fold pairs of polynomials using - // p_left + r_i * (p_right - p_left). + // For each variable, fold pairs of polynomials for (i, (&r1_i, &r2_sub_r1_i)) in r1.iter().zip(r2_sub_r1.iter()).enumerate() { let half_len = 1 << (n_vars - i - 1); - // The polynomial representing r(x) = r1_i + (r2_i - r1_i)*x - let r_poly = DensePolynomial::from_coefficients_slice(&[r1_i, r2_sub_r1_i]); + let new_coeffs: Vec> = (0..half_len) + .into_par_iter() + .map(|b| { + let left_idx = b << 1; + let right_idx = left_idx + 1; - for b in 0..half_len { - let left = &poly[b << 1]; - let right = &poly[(b << 1) + 1]; - poly[b] = left + &(&r_poly * &(right - left)); - } + let left_coeffs: &Vec = &coeffs[left_idx]; + let right_coeffs: &Vec = &coeffs[right_idx]; + + // Compute (right - left) coefficients + let mut diff_coeffs = vec![F::zero(); right_coeffs.len()]; + for j in 0..right_coeffs.len() { + diff_coeffs[j] = right_coeffs[j] - left_coeffs[j]; + } + + // Multiply by linear polynomial (r1_i + r2_sub_r1_i * x) + let mut result_coeffs = vec![F::zero(); diff_coeffs.len() + 1]; + + for j in 0..diff_coeffs.len() { + result_coeffs[j] += diff_coeffs[j] * r1_i; + result_coeffs[j + 1] += diff_coeffs[j] * r2_sub_r1_i; + } + + // Add left polynomial + for j in 0..left_coeffs.len() { + result_coeffs[j] += left_coeffs[j]; + } + + result_coeffs + }) + .collect(); - // Truncate to half the length, since we've folded pairs into single polynomials. - poly.truncate(half_len); + coeffs = new_coeffs; } - // By now, poly.len() == 1 - // Return that single polynomial as DenseOrSparsePolynomial - Ok(SparseOrDensePolynomial::from_dense(poly.remove(0))) + // Convert final coefficient vector to polynomial + Ok(SparseOrDensePolynomial::from_dense( + DensePolynomial::from_coefficients_vec(coeffs.into_iter().next().unwrap()) + )) } MultilinearExtension::SparseMLE(mle_sparse) => { // new algorithm - // If there are no evaluations, return the zero polynomial if mle_sparse.evaluations.is_empty() { return Ok(SparseOrDensePolynomial::from_sparse( SparsePolynomial::zero(), )); } - // Initialize the result polynomial as zero - let mut sum_poly = DensePolynomial::zero(); + let max_degree = n_vars + 1; - // Iterate over each non-zero evaluation - for (&index, &value) in &mle_sparse.evaluations { - // Convert index to binary vector (little-endian, least significant bit is i=0) - // This represents the variable assignments for the evaluation point, with b[i] indicating if variable i is 1 or 0. - let mut b = vec![false; n_vars]; - for (i, bit) in b.iter_mut().enumerate().take(n_vars) { - *bit = (index >> i) & 1 == 1; - } + // Pre-compute linear factors to avoid repeated computation + let linear_factors: Vec<(F, F, F, F)> = (0..n_vars) + .map(|i| ( + r1[i], // factor_1_const + r2_sub_r1[i], // factor_1_linear + F::one() - r1[i], // factor_0_const + -r2_sub_r1[i], // factor_0_linear + )) + .collect(); - // Start with the constant polynomial equal to the evaluation value - let mut contrib = DensePolynomial::from_coefficients_slice(&[value]); - - // Multiply by the linear factor for each variable - for i in 0..n_vars { - let factor = if b[i] { - // If b[i] == 1, use r1_i + r2_sub_r1_i * x - DensePolynomial::from_coefficients_slice(&[r1[i], r2_sub_r1[i]]) - } else { - // If b[i] == 0, use 1 - r1_i - r2_sub_r1_i * x - DensePolynomial::from_coefficients_slice(&[F::one() - r1[i], -r2_sub_r1[i]]) - }; - contrib = &contrib * &factor; + // Parallel version - same pattern as dense case + let contributions: Vec> = mle_sparse.evaluations + .par_iter() + .map(|(&index, &value)| { + let mut contrib_coeffs = vec![F::zero(); max_degree]; + contrib_coeffs[0] = value; + let mut current_degree = 0; + + for i in 0..n_vars { + let bit_i = (index >> i) & 1 == 1; + let (const_term, linear_term) = if bit_i { + (linear_factors[i].0, linear_factors[i].1) + } else { + (linear_factors[i].2, linear_factors[i].3) + }; + + // Multiply in-place by linear polynomial + contrib_coeffs[current_degree + 1] = contrib_coeffs[current_degree] * linear_term; + for j in (1..=current_degree).rev() { + contrib_coeffs[j] = contrib_coeffs[j] * const_term + contrib_coeffs[j-1] * linear_term; + } + contrib_coeffs[0] *= const_term; + + current_degree += 1; + } + + // Return just the needed coefficients + contrib_coeffs.truncate(current_degree + 1); + contrib_coeffs + }) + .collect(); + + // Sequential reduction - sum all contributions + let mut result_coeffs = vec![F::zero(); max_degree]; + for contrib in contributions { + for (i, &coeff) in contrib.iter().enumerate() { + result_coeffs[i] += coeff; } + } - sum_poly += &contrib; + // Remove trailing zeros + let mut result_coeffs = result_coeffs; + while result_coeffs.len() > 1 && result_coeffs.last() == Some(&F::zero()) { + result_coeffs.pop(); } - // Return the final polynomial - Ok(SparseOrDensePolynomial::from_dense(sum_poly)) + Ok(SparseOrDensePolynomial::from_dense( + DensePolynomial::from_coefficients_vec(result_coeffs) + )) } } } From dfa43dfb5effa0057e1c5328c463ee7f382125f1 Mon Sep 17 00:00:00 2001 From: Fantoni0 Date: Wed, 11 Jun 2025 12:46:49 +0200 Subject: [PATCH 2/9] Reduce allocations --- .../src/folding/nova/nifs/pointvsline.rs | 86 +++++++++---------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/folding-schemes/src/folding/nova/nifs/pointvsline.rs b/folding-schemes/src/folding/nova/nifs/pointvsline.rs index 7d2e1f788..20ecfe853 100644 --- a/folding-schemes/src/folding/nova/nifs/pointvsline.rs +++ b/folding-schemes/src/folding/nova/nifs/pointvsline.rs @@ -273,7 +273,7 @@ fn compute_h( return Err(Error::NotEqual); } - // Start with coefficient vectors + // Start with coefficient vectors. For now they are constant polynomials with a single coefficient let mut coeffs: Vec> = mle .evaluations .iter() @@ -281,6 +281,7 @@ fn compute_h( .collect(); for (i, (&r1_i, &r2_sub_r1_i)) in r1.iter().zip(r2_sub_r1.iter()).enumerate().take(n_vars) { + // Create a linear polynomial r(X) = r1_i + (r2_sub_r1_i) * X (basically l) let half_len = 1 << (n_vars - i - 1); let new_coeffs: Vec> = (0..half_len) .into_par_iter() @@ -291,23 +292,19 @@ fn compute_h( let left_coeffs = &coeffs[left_idx]; let right_coeffs = &coeffs[right_idx]; - // Compute (right - left) coefficients - let mut diff_coeffs = vec![F::zero(); right_coeffs.len()]; - for j in 0..right_coeffs.len() { - diff_coeffs[j] = right_coeffs[j] - left_coeffs[j]; - } - - // Multiply by linear polynomial - let mut result_coeffs = vec![F::zero(); diff_coeffs.len() + 1]; + // Initialize result coefficients + let mut result_coeffs = vec![F::zero(); right_coeffs.len() + 1]; - for j in 0..diff_coeffs.len() { - result_coeffs[j] += diff_coeffs[j] * r1_i; - result_coeffs[j + 1] += diff_coeffs[j] * r2_sub_r1_i; + // Add left polynomial contribution + for (j, &left_val) in left_coeffs.iter().enumerate() { + result_coeffs[j] = left_val; } - // Add left polynomial - for j in 0..left_coeffs.len() { - result_coeffs[j] += left_coeffs[j]; + // Add (right - left) * (r1_i + r2_sub_r1_i * X) contribution directly + for (j, (&right_val, &left_val)) in right_coeffs.iter().zip(left_coeffs.iter()).enumerate() { + let diff = right_val - left_val; + result_coeffs[j] += diff * r1_i; + result_coeffs[j + 1] += diff * r2_sub_r1_i; } result_coeffs @@ -322,7 +319,7 @@ fn compute_h( /// Implementation for computing h by not following Algorithm 1 "MLE-after-line composition" off the Mova paper /// This is due to the need to support sparse representation. -/// Currently this is only used for the mova_matrix.rs implementation that is configured to use Matrex +/// Currently, this is only used for the mova_matrix.rs implementation configured to use Matrex fn compute_h2( mle: &MultilinearExtension, r1: &[F], @@ -356,23 +353,19 @@ fn compute_h2( let left_coeffs: &Vec = &coeffs[left_idx]; let right_coeffs: &Vec = &coeffs[right_idx]; - // Compute (right - left) coefficients - let mut diff_coeffs = vec![F::zero(); right_coeffs.len()]; - for j in 0..right_coeffs.len() { - diff_coeffs[j] = right_coeffs[j] - left_coeffs[j]; - } - - // Multiply by linear polynomial (r1_i + r2_sub_r1_i * x) - let mut result_coeffs = vec![F::zero(); diff_coeffs.len() + 1]; + let max_degree = right_coeffs.len() + 1; + let mut result_coeffs = vec![F::zero(); max_degree]; - for j in 0..diff_coeffs.len() { - result_coeffs[j] += diff_coeffs[j] * r1_i; - result_coeffs[j + 1] += diff_coeffs[j] * r2_sub_r1_i; + // Add left polynomial first + for (j, &left_val) in left_coeffs.iter().enumerate() { + result_coeffs[j] = left_val; } - // Add left polynomial - for j in 0..left_coeffs.len() { - result_coeffs[j] += left_coeffs[j]; + // Add right polynomial contribution directly: (right - left) * (r1_i + r2_sub_r1_i * x) + for (j, (&right_val, &left_val)) in right_coeffs.iter().zip(left_coeffs.iter()).enumerate() { + let diff = right_val - left_val; + result_coeffs[j] += diff * r1_i; + result_coeffs[j + 1] += diff * r2_sub_r1_i; } result_coeffs @@ -389,15 +382,13 @@ fn compute_h2( } MultilinearExtension::SparseMLE(mle_sparse) => { - // new algorithm + // If there are no evaluations, return the zero polynomial if mle_sparse.evaluations.is_empty() { return Ok(SparseOrDensePolynomial::from_sparse( SparsePolynomial::zero(), )); } - let max_degree = n_vars + 1; - // Pre-compute linear factors to avoid repeated computation let linear_factors: Vec<(F, F, F, F)> = (0..n_vars) .map(|i| ( @@ -408,19 +399,21 @@ fn compute_h2( )) .collect(); - // Parallel version - same pattern as dense case - let contributions: Vec> = mle_sparse.evaluations + let result_coeffs = mle_sparse.evaluations .par_iter() .map(|(&index, &value)| { let mut contrib_coeffs = vec![F::zero(); max_degree]; contrib_coeffs[0] = value; let mut current_degree = 0; + // Multiply by the linear factor for each variable for i in 0..n_vars { let bit_i = (index >> i) & 1 == 1; let (const_term, linear_term) = if bit_i { + // If bit_i == 1, use r1_i + r2_sub_r1_i * x (linear_factors[i].0, linear_factors[i].1) } else { + // If bit_i == 0, use 1 - r1_i - r2_sub_r1_i * x (linear_factors[i].2, linear_factors[i].3) }; @@ -434,19 +427,22 @@ fn compute_h2( current_degree += 1; } - // Return just the needed coefficients + // Return just the required coefficients contrib_coeffs.truncate(current_degree + 1); contrib_coeffs }) - .collect(); - - // Sequential reduction - sum all contributions - let mut result_coeffs = vec![F::zero(); max_degree]; - for contrib in contributions { - for (i, &coeff) in contrib.iter().enumerate() { - result_coeffs[i] += coeff; - } - } + .reduce( + || vec![F::zero(); max_degree], + |mut acc, contrib| { + // Parallel reduction: combine two coefficient vectors + for (i, &coeff) in contrib.iter().enumerate() { + if i < acc.len() { + acc[i] += coeff; + } + } + acc + } + ); // Remove trailing zeros let mut result_coeffs = result_coeffs; From 0240f9d429d41d76167c6a892d3cd4871e3e0b75 Mon Sep 17 00:00:00 2001 From: Fantoni0 Date: Thu, 12 Jun 2025 09:27:40 +0200 Subject: [PATCH 3/9] Commit before merge --- benches/commitment_schemes.rs | 6 +- benches/mova_matrix.rs | 25 ++- examples/mova_matrix_memory.rs | 3 +- folding-schemes/src/commitment/hyrax.rs | 136 +++++++----- .../src/folding/nova/nifs/mova_matrix.rs | 5 +- .../src/folding/nova/nifs/pointvsline.rs | 210 ++++++++++++++---- 6 files changed, 267 insertions(+), 118 deletions(-) diff --git a/benches/commitment_schemes.rs b/benches/commitment_schemes.rs index 84a096c35..5dd19f848 100644 --- a/benches/commitment_schemes.rs +++ b/benches/commitment_schemes.rs @@ -1,6 +1,6 @@ use ark_ec::VariableBaseMSM; use ark_pallas::{Fr, Projective}; -use ark_std::{log2, UniformRand, Zero}; +use ark_std::{UniformRand, Zero}; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use folding_schemes::commitment::hyrax::{Hyrax, HyraxGenerators}; use folding_schemes::commitment::pedersen::Pedersen; @@ -50,7 +50,7 @@ fn bench_dense_commits(c: &mut Criterion) { // Setup parameters let pedersen_params = Pedersen::::setup_prover(&mut rng, n * n).unwrap(); - let hyrax_params = HyraxGenerators::::setup(&mut rng, log2(n * n) as usize); + let hyrax_params = HyraxGenerators::::setup(&mut rng, n * n); group.bench_with_input(BenchmarkId::new("Pedersen", n), &n, |b, _| { b.iter(|| Pedersen::::commit(&pedersen_params, data, &Fr::zero()).unwrap()); @@ -78,7 +78,7 @@ fn bench_sparse_commits(c: &mut Criterion) { // Setup parameters let pedersen_params = Pedersen::::setup_prover(&mut rng, n * n).unwrap(); - let hyrax_params = HyraxGenerators::::setup(&mut rng, log2(n * n) as usize); + let hyrax_params = HyraxGenerators::::setup(&mut rng, n * n); group.bench_with_input(BenchmarkId::new("Pedersen Sparse", n), &n, |b, _| { b.iter(|| { diff --git a/benches/mova_matrix.rs b/benches/mova_matrix.rs index cbf60f49b..3b9956dd1 100644 --- a/benches/mova_matrix.rs +++ b/benches/mova_matrix.rs @@ -7,12 +7,21 @@ use folding_schemes::commitment::hyrax::HyraxGenerators; use folding_schemes::folding::nova::nifs::mova_matrix::{RelaxedCommittedRelation, Witness, NIFS}; use folding_schemes::transcript::poseidon::poseidon_canonical_config; use folding_schemes::Curve; -use matrex::Matrix; +use matrex::{Matrix, MatrixSize, SparseMatrix}; use rand::{Rng, RngCore}; use std::time::{Duration, Instant}; const NUM_OF_PRECONDITION_FOLDS: &[usize] = &[1, 10, 20, 40]; +fn dense_to_sparse_vec(input: &[C::ScalarField]) -> Vec<(usize, C::ScalarField)> { + input + .iter() + .enumerate() + .filter(|(_, x)| **x != C::ScalarField::from(0)) + .map(|(i, x)| (i, *x)) + .collect() +} + fn random_sparse_matrix(n: usize, rng: &mut impl RngCore) -> Matrix { let elements = (0..n) .map(|row| { @@ -39,7 +48,11 @@ fn get_instances( // B matrix let b = random_sparse_matrix::(n, rng); // C = A * B matrix - let c = (&a * &b).unwrap(); + let c: Matrix = (&a * &b).unwrap(); + // Enforce sparse matrices + let c = if c.is_dense() { + Matrix::Sparse(SparseMatrix::from_vec(dense_to_sparse_vec::(c.as_dense_slice().unwrap()), c.rows(), c.cols()).unwrap()) + } else { c }; // Error matrix initialized to 0s let e = Matrix::zero(n, n); @@ -58,17 +71,15 @@ fn get_instances( fn bench_mova_matrix(c: &mut Criterion) { let mut group = c.benchmark_group("mova_matrix_sequential_folding"); let mut rng = ark_std::test_rng(); - let mat_dim = 8; // 4x4 matrices + let mat_dim = 16; // Must be a power of 2 for count in NUM_OF_PRECONDITION_FOLDS { group .measurement_time(Duration::from_secs(20 * (*count as u64))) .bench_function(&format!("{count}"), |b| { // Set up transcript and commitment scheme - let hyrax_params = HyraxGenerators::::setup( - &mut rng, - log2(mat_dim * mat_dim) as usize, - ); + let hyrax_params = + HyraxGenerators::::setup(&mut rng, mat_dim * mat_dim); let poseidon_config = poseidon_canonical_config::(); let pp_hash = Fr::rand(&mut rng); diff --git a/examples/mova_matrix_memory.rs b/examples/mova_matrix_memory.rs index 47f6204cc..503ca5abc 100644 --- a/examples/mova_matrix_memory.rs +++ b/examples/mova_matrix_memory.rs @@ -71,8 +71,7 @@ fn bench_mova_matrix() { println!("Starting with pedersen setup"); let start = Instant::now(); - let hyrax_params = - HyraxGenerators::::setup(&mut rng, log2(mat_dim * mat_dim) as usize); + let hyrax_params = HyraxGenerators::::setup(&mut rng, mat_dim * mat_dim); let hyrax_elapsed = start.elapsed(); println!("hyrax_elapsed 1 {:?}", hyrax_elapsed); diff --git a/folding-schemes/src/commitment/hyrax.rs b/folding-schemes/src/commitment/hyrax.rs index 12314dee9..c079833e1 100644 --- a/folding-schemes/src/commitment/hyrax.rs +++ b/folding-schemes/src/commitment/hyrax.rs @@ -1,23 +1,31 @@ +use crate::commitment::pedersen::{Params, Pedersen}; +use crate::commitment::{CommitmentScheme, NethermindCommitmentScheme}; +use crate::{Curve, Error}; use ark_ff::Zero; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::iterable::Iterable; +use ark_std::log2; use ark_std::rand::RngCore; use ark_std::vec::Vec; use rayon::prelude::*; use std::fmt::Debug; use std::marker::PhantomData; -use crate::commitment::pedersen::{Params, Pedersen}; -use crate::commitment::{CommitmentScheme, NethermindCommitmentScheme}; -use crate::{Curve, Error}; - -/// Taken from jolt but we assume ratio is 1 since we are dealing with square matrices +/// Taken from Jolt but we assume ratio is 1 since we are dealing with square matrices fn matrix_dimensions(num_elems: usize) -> (usize, usize) { - let left_num_vars = num_elems / 2; - let right_num_vars = num_elems - left_num_vars; - + let num_vars = if num_elems == 1 { + 1 + } else { + log2(num_elems) as usize + }; + + let mut row_size = 2_usize.pow((num_vars / 2) as u32); + row_size = row_size.next_power_of_two(); + + let right_num_vars: usize = std::cmp::min(log2(row_size) as usize, num_vars - 1); + row_size = 2_usize.pow(right_num_vars as u32); + let left_num_vars = num_vars - right_num_vars; let col_size = 2_usize.pow(left_num_vars as u32); - let row_size = 2_usize.pow(right_num_vars as u32); (col_size, row_size) } @@ -45,15 +53,8 @@ impl HyraxGenerators { impl Hyrax { pub fn commit(elems: &[C::ScalarField], gens: &HyraxGenerators) -> Result, Error> { let n = elems.len(); - let ell = { - if n.is_power_of_two() { - (1usize.leading_zeros() - n.leading_zeros()) as usize - } else { - (0usize.leading_zeros() - n.leading_zeros()) as usize - } - }; - - let (L_size, R_size) = matrix_dimensions(ell); + + let (L_size, R_size) = matrix_dimensions(n); assert_eq!(L_size * R_size, n); let row_commitments: Vec = elems @@ -76,14 +77,8 @@ impl Hyrax { ) -> Result>, Error> { let n = batch[0].len(); batch.iter().for_each(|poly| assert_eq!(poly.len(), n)); - let ell = { - if n.is_power_of_two() { - (1usize.leading_zeros() - n.leading_zeros()) as usize - } else { - (0usize.leading_zeros() - n.leading_zeros()) as usize - } - }; - let (L_size, R_size) = matrix_dimensions(ell); + + let (L_size, R_size) = matrix_dimensions(n); assert_eq!(L_size * R_size, n); let rows = batch.par_iter().flat_map(|poly| poly.par_chunks(R_size)); @@ -108,17 +103,9 @@ impl Hyrax { indices_values: &[(usize, C::ScalarField)], gens: &HyraxGenerators, ) -> Result, Error> { - // deduce the highest index to figure out n - let max_index = indices_values.iter().map(|(pos, _)| *pos).max().unwrap(); - let n = max_index + 1; // since indices are 0-based - - let ell = if n.is_power_of_two() { - (1usize.leading_zeros() - n.leading_zeros()) as usize - } else { - (0usize.leading_zeros() - n.leading_zeros()) as usize - }; - let (L_size, R_size) = matrix_dimensions(ell); - + let max_elems = + gens.pedersen_generators.generators.len() * gens.pedersen_generators.generators.len(); + let (L_size, R_size) = matrix_dimensions(max_elems); // For each row i in [0..L_size], gather all (pos, val) where row_start <= pos < row_end // and do a "sparse" Pedersen commit using the row‐local indices (pos - row_start). let row_commitments: Vec = (0..L_size) @@ -157,23 +144,14 @@ mod tests { use super::*; use ark_pallas::{Fr, Projective}; use ark_std::{test_rng, UniformRand}; - + use rand::Rng; #[test] fn test_matrix_dimensions() { - assert_eq!(matrix_dimensions(0), (1, 1)); - assert_eq!(matrix_dimensions(1), (1, 2)); - assert_eq!(matrix_dimensions(2), (2, 2)); - assert_eq!(matrix_dimensions(4), (4, 4)); - assert_eq!(matrix_dimensions(6), (8, 8)); - - let num_vars = 30; // Very large number of variables - let (cols, rows) = matrix_dimensions(num_vars); - - let expected_left = num_vars / 2; - let expected_right = num_vars - expected_left; - - assert_eq!(cols, 1 << expected_left); - assert_eq!(rows, 1 << expected_right); + // Values taken from the original Jolt implementation + assert_eq!(matrix_dimensions(1), (2, 1)); + assert_eq!(matrix_dimensions(4), (2, 2)); + assert_eq!(matrix_dimensions(8), (4, 2)); + assert_eq!(matrix_dimensions(16), (4, 4)); } #[test] @@ -186,25 +164,25 @@ mod tests { #[test] fn test_commit_success() { let mut rng = test_rng(); - let len = 4; // Must be a power of two number of bits - let elems: Vec = (0..(1 << len)).map(|_| Fr::rand(&mut rng)).collect(); + let dim = 4; // Must be a power of two number of bits + let elems: Vec = (0..(1 << dim)).map(|_| Fr::rand(&mut rng)).collect(); - let gens = HyraxGenerators::::setup(&mut rng, len); + let gens = HyraxGenerators::::setup(&mut rng, elems.len()); let commitment = Hyrax::::commit(&elems, &gens); assert!(commitment.is_ok()); let hyrax = commitment.unwrap(); - let (l_size, _) = matrix_dimensions(len); + let (l_size, _) = matrix_dimensions(elems.len()); assert_eq!(hyrax.len(), l_size); } #[test] fn test_same_commit() { let mut rng = test_rng(); - let len = 4; // Must be a power of two number of bits + let len = 4; // Must be a power of two let elems: Vec = (0..(1 << len)).map(|_| Fr::rand(&mut rng)).collect(); - let gens = HyraxGenerators::::setup(&mut rng, len); + let gens = HyraxGenerators::::setup(&mut rng, elems.len()); let commitment = Hyrax::::commit(&elems, &gens); assert!(commitment.is_ok()); @@ -217,6 +195,44 @@ mod tests { assert_eq!(hyrax, hyrax2); } + #[test] + fn test_sparse_dense() { + // Create a sparse matrix + let mut rng = test_rng(); + let dim = 8; + let max_elems = 1 << dim; + let mut elems = vec![Fr::zero(); max_elems]; + let random_idx: Vec = (0..dim).map(|_| rng.gen_range(0..dim * dim)).collect(); + for idx in random_idx { + elems[idx] = Fr::rand(&mut rng); + } + + // Compute its dense commitment + let gens = HyraxGenerators::::setup(&mut rng, elems.len()); + let commitment = Hyrax::::commit(&elems, &gens); + assert!(commitment.is_ok()); + let hyrax = commitment.unwrap(); + + // Compute its sparse commitment + let sparse_repr = elems + .into_iter() + .enumerate() + .filter_map(|(idx, elem)| { + if !elem.is_zero() { + Some((idx, elem)) + } else { + None + } + }) + .collect::>(); + let commitment2 = Hyrax::::commit_sparse_matrix(&sparse_repr, &gens); + assert!(commitment2.is_ok()); + let hyrax2 = commitment2.unwrap(); + + // Make sure they are equal + assert_eq!(hyrax, hyrax2); + } + #[test] fn test_batch_commit() { let mut rng = test_rng(); @@ -230,13 +246,13 @@ mod tests { let batch_refs: Vec<&[Fr]> = batch.iter().map(|v| v.as_slice()).collect(); - let gens = HyraxGenerators::::setup(&mut rng, len); + let gens = HyraxGenerators::::setup(&mut rng, poly_len); let result = Hyrax::::batch_commit(&batch_refs, &gens); assert!(result.is_ok()); let commitments = result.unwrap(); assert_eq!(commitments.len(), batch_size); - let (l_size, _) = matrix_dimensions(len); + let (l_size, _) = matrix_dimensions(poly_len); for c in commitments { assert_eq!(c.len(), l_size); } diff --git a/folding-schemes/src/folding/nova/nifs/mova_matrix.rs b/folding-schemes/src/folding/nova/nifs/mova_matrix.rs index 71c23a38d..3da2e2992 100644 --- a/folding-schemes/src/folding/nova/nifs/mova_matrix.rs +++ b/folding-schemes/src/folding/nova/nifs/mova_matrix.rs @@ -512,7 +512,7 @@ pub mod tests { // Set up transcript and commitment scheme let hyrax_params = - HyraxGenerators::::setup(&mut rng, log2(mat_dim * mat_dim) as usize); + HyraxGenerators::::setup(&mut rng, mat_dim * mat_dim as usize); let poseidon_config = poseidon_canonical_config::(); let mut transcript_p = PoseidonSponge::::new(&poseidon_config); let mut transcript_v = PoseidonSponge::::new(&poseidon_config); @@ -561,8 +561,7 @@ pub mod tests { let mat_dim = 16; // 16x16 matrices // Set up transcript and commitment scheme - let hyrax_params = - HyraxGenerators::::setup(&mut rng, log2(mat_dim * mat_dim) as usize); + let hyrax_params = HyraxGenerators::::setup(&mut rng, mat_dim * mat_dim); let poseidon_config = poseidon_canonical_config::(); let mut transcript_p = PoseidonSponge::::new(&poseidon_config); let mut transcript_v = PoseidonSponge::::new(&poseidon_config); diff --git a/folding-schemes/src/folding/nova/nifs/pointvsline.rs b/folding-schemes/src/folding/nova/nifs/pointvsline.rs index 20ecfe853..b43d159a6 100644 --- a/folding-schemes/src/folding/nova/nifs/pointvsline.rs +++ b/folding-schemes/src/folding/nova/nifs/pointvsline.rs @@ -481,7 +481,7 @@ mod tests { use crate::Error; use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; use ark_pallas::{Fr, Projective}; - use ark_poly::{DenseMultilinearExtension, DenseUVPolynomial, SparseMultilinearExtension}; + use ark_poly::{DenseMultilinearExtension, DenseUVPolynomial, Polynomial, SparseMultilinearExtension}; use ark_std::{log2, UniformRand}; use crate::folding::nova::nifs::mova::Witness; @@ -842,53 +842,177 @@ mod tests { #[test] fn test_compute_h2_compare() { - // Both MLEs represent the same information both in dense and sparse representation. - let vanilla_dense = DenseMultilinearExtension::from_evaluations_slice( - 3, - &[ - Fr::zero(), - Fr::zero(), - Fr::one(), - Fr::one(), - Fr::zero(), - Fr::zero(), - Fr::zero(), - Fr::one(), - ], - ); - let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); - let mle_sparse = - MultilinearExtension::SparseMLE(SparseMultilinearExtension::from_evaluations( + use ark_std::test_rng; + + // Test Case 1: Simple case with sparse pattern + { + let vanilla_dense = DenseMultilinearExtension::from_evaluations_slice( 3, - &[(2, Fr::one()), (3, Fr::one()), (7, Fr::one())], - )); + &[ + Fr::zero(), + Fr::zero(), + Fr::one(), + Fr::one(), + Fr::zero(), + Fr::zero(), + Fr::zero(), + Fr::one(), + ], + ); + let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); + let mle_sparse = + MultilinearExtension::SparseMLE(SparseMultilinearExtension::from_evaluations( + 3, + &[(2, Fr::one()), (3, Fr::one()), (7, Fr::one())], + )); - let r0 = [Fr::from(1), Fr::from(2), Fr::from(3)]; - let r1 = [Fr::from(5), Fr::from(6), Fr::from(7)]; - let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); + let r0 = [Fr::from(1), Fr::from(2), Fr::from(3)]; + let r1 = [Fr::from(5), Fr::from(6), Fr::from(7)]; + let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); - // Use original compute_h method - let result_h = compute_h(&vanilla_dense, &r0, &r1_sub_r0).unwrap(); + let result_h = compute_h(&vanilla_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); - // Use dense for compute_h2 - let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); + assert_eq!(result_h2_dense, result_h2_sparse); + assert_eq!(result_h2_dense.coeffs(), result_h.coeffs()); + assert_eq!(result_h2_sparse.coeffs(), result_h.coeffs()); + } - // Use sparse for compute_h2 - let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); + // Test Case 2: Larger size with random values (4 variables) + { + let mut rng = test_rng(); + let evaluations: Vec = (0..16).map(|_| Fr::rand(&mut rng)).collect(); + + let vanilla_dense = DenseMultilinearExtension::from_evaluations_vec(4, evaluations.clone()); + let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); + + // Create sparse version by filtering out small values + let sparse_evals: Vec<(usize, Fr)> = evaluations + .iter() + .enumerate() + .filter(|(_, &val)| !val.is_zero()) + .map(|(i, &val)| (i, val)) + .collect(); + let mle_sparse = MultilinearExtension::SparseMLE( + SparseMultilinearExtension::from_evaluations(4, &sparse_evals) + ); - assert_eq!( - result_h2_dense, result_h2_sparse, - "Sparse and dense computations for compute h2 must be equal" - ); - assert_eq!( - result_h2_dense.coeffs(), - result_h.coeffs(), - "Dense polynomial coefficients must match original h computation" - ); - assert_eq!( - result_h2_sparse.coeffs(), - result_h.coeffs(), - "Sparse polynomial coefficients must match original h computation" - ); + let r0: Vec = (0..4).map(|_| Fr::rand(&mut rng)).collect(); + let r1: Vec = (0..4).map(|_| Fr::rand(&mut rng)).collect(); + let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); + + let result_h = compute_h(&vanilla_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); + + assert_eq!(result_h2_dense, result_h2_sparse, "Random 4-var case: dense vs sparse mismatch"); + assert_eq!(result_h2_dense.coeffs(), result_h.coeffs(), "Random 4-var case: h2_dense vs h mismatch"); + } + + // Test Case 3: Edge case - all zeros except one + { + let mut evaluations = vec![Fr::zero(); 8]; + evaluations[5] = Fr::from(42); + + let vanilla_dense = DenseMultilinearExtension::from_evaluations_vec(3, evaluations); + let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); + let mle_sparse = MultilinearExtension::SparseMLE( + SparseMultilinearExtension::from_evaluations(3, &[(5, Fr::from(42))]) + ); + + let r0 = [Fr::from(7), Fr::from(11), Fr::from(13)]; + let r1 = [Fr::from(17), Fr::from(19), Fr::from(23)]; + let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); + + let result_h = compute_h(&vanilla_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); + + assert_eq!(result_h2_dense, result_h2_sparse, "Single non-zero case: dense vs sparse mismatch"); + assert_eq!(result_h2_dense.coeffs(), result_h.coeffs(), "Single non-zero case: h2_dense vs h mismatch"); + } + + // Test Case 4: Edge case - all ones (dense case) + { + let evaluations = vec![Fr::one(); 16]; + + let vanilla_dense = DenseMultilinearExtension::from_evaluations_vec(4, evaluations.clone()); + let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); + let sparse_evals: Vec<(usize, Fr)> = (0..16).map(|i| (i, Fr::one())).collect(); + let mle_sparse = MultilinearExtension::SparseMLE( + SparseMultilinearExtension::from_evaluations(4, &sparse_evals) + ); + + let r0 = [Fr::from(2), Fr::from(3), Fr::from(5), Fr::from(7)]; + let r1 = [Fr::from(11), Fr::from(13), Fr::from(17), Fr::from(19)]; + let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); + + let result_h = compute_h(&vanilla_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); + + assert_eq!(result_h2_dense, result_h2_sparse, "All ones case: dense vs sparse mismatch"); + assert_eq!(result_h2_dense.coeffs(), result_h.coeffs(), "All ones case: h2_dense vs h mismatch"); + } + + // Test Case 5: Alternating pattern + { + let evaluations: Vec = (0..32) + .map(|i| if i % 2 == 0 { Fr::from(i as u64 + 1) } else { Fr::zero() }) + .collect(); + + let vanilla_dense = DenseMultilinearExtension::from_evaluations_vec(5, evaluations.clone()); + let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); + + let sparse_evals: Vec<(usize, Fr)> = evaluations + .iter() + .enumerate() + .filter(|(_, &val)| !val.is_zero()) + .map(|(i, &val)| (i, val)) + .collect(); + let mle_sparse = MultilinearExtension::SparseMLE( + SparseMultilinearExtension::from_evaluations(5, &sparse_evals) + ); + + let r0 = [Fr::from(1), Fr::from(4), Fr::from(9), Fr::from(16), Fr::from(25)]; + let r1 = [Fr::from(36), Fr::from(49), Fr::from(64), Fr::from(81), Fr::from(100)]; + let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); + + let result_h = compute_h(&vanilla_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); + + assert_eq!(result_h2_dense, result_h2_sparse, "Alternating pattern case: dense vs sparse mismatch"); + assert_eq!(result_h2_dense.coeffs(), result_h.coeffs(), "Alternating pattern case: h2_dense vs h mismatch"); + } + + // Test Case 6: Very sparse case (only corner evaluations) + { + let mut evaluations = vec![Fr::zero(); 16]; + evaluations[0] = Fr::from(10); + evaluations[15] = Fr::from(20); + + let vanilla_dense = DenseMultilinearExtension::from_evaluations_vec(4, evaluations); + let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); + let mle_sparse = MultilinearExtension::SparseMLE( + SparseMultilinearExtension::from_evaluations(4, &[(0, Fr::from(10)), (15, Fr::from(20))]) + ); + + let r0 = [Fr::zero(), Fr::zero(), Fr::zero(), Fr::zero()]; + let r1 = [Fr::one(), Fr::one(), Fr::one(), Fr::one()]; + let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); + + let result_h = compute_h(&vanilla_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); + let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); + + assert_eq!(result_h2_dense, result_h2_sparse, "Corner values case: dense vs sparse mismatch"); + assert_eq!(result_h2_dense.coeffs(), result_h.coeffs(), "Corner values case: h2_dense vs h mismatch"); + + // Verify the interpolation property: h(0) should be MLE(r0) and h(1) should be MLE(r1) + assert_eq!(result_h.evaluate(&Fr::zero()), Fr::from(10), "h(0) should equal MLE(r0)"); + assert_eq!(result_h.evaluate(&Fr::one()), Fr::from(20), "h(1) should equal MLE(r1)"); + } } } From 4b52503ea459b87264ba8394e5c88d6f4858244d Mon Sep 17 00:00:00 2001 From: Fantoni0 Date: Thu, 12 Jun 2025 09:28:58 +0200 Subject: [PATCH 4/9] Cargo fmt --- benches/mova_matrix.rs | 13 +- .../src/folding/nova/nifs/pointvsline.rs | 262 +++++++++++------- 2 files changed, 180 insertions(+), 95 deletions(-) diff --git a/benches/mova_matrix.rs b/benches/mova_matrix.rs index 3b9956dd1..95a94aea0 100644 --- a/benches/mova_matrix.rs +++ b/benches/mova_matrix.rs @@ -51,8 +51,17 @@ fn get_instances( let c: Matrix = (&a * &b).unwrap(); // Enforce sparse matrices let c = if c.is_dense() { - Matrix::Sparse(SparseMatrix::from_vec(dense_to_sparse_vec::(c.as_dense_slice().unwrap()), c.rows(), c.cols()).unwrap()) - } else { c }; + Matrix::Sparse( + SparseMatrix::from_vec( + dense_to_sparse_vec::(c.as_dense_slice().unwrap()), + c.rows(), + c.cols(), + ) + .unwrap(), + ) + } else { + c + }; // Error matrix initialized to 0s let e = Matrix::zero(n, n); diff --git a/folding-schemes/src/folding/nova/nifs/pointvsline.rs b/folding-schemes/src/folding/nova/nifs/pointvsline.rs index b43d159a6..bfdfa08cb 100644 --- a/folding-schemes/src/folding/nova/nifs/pointvsline.rs +++ b/folding-schemes/src/folding/nova/nifs/pointvsline.rs @@ -9,8 +9,8 @@ use ark_poly::univariate::{DensePolynomial, SparsePolynomial}; use ark_poly::{DenseMultilinearExtension, DenseUVPolynomial, Polynomial}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::{log2, Zero}; +use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; use std::fmt::Debug; -use rayon::iter::{ParallelIterator, IntoParallelIterator, IntoParallelRefIterator}; /// Implements the Points vs Line as described in /// [Mova](https://eprint.iacr.org/2024/1220.pdf) and Section 4.5.2 from Thaler’s book @@ -268,53 +268,53 @@ fn compute_h( r1: &[F], r2_sub_r1: &[F], ) -> Result, Error> { - let n_vars: usize = mle.num_vars; - if r1.len() != r2_sub_r1.len() || r1.len() != n_vars { - return Err(Error::NotEqual); - } + let n_vars: usize = mle.num_vars; + if r1.len() != r2_sub_r1.len() || r1.len() != n_vars { + return Err(Error::NotEqual); + } - // Start with coefficient vectors. For now they are constant polynomials with a single coefficient - let mut coeffs: Vec> = mle - .evaluations - .iter() - .map(|&x| vec![x]) + // Start with coefficient vectors. For now they are constant polynomials with a single coefficient + let mut coeffs: Vec> = mle.evaluations.iter().map(|&x| vec![x]).collect(); + + for (i, (&r1_i, &r2_sub_r1_i)) in r1.iter().zip(r2_sub_r1.iter()).enumerate().take(n_vars) { + // Create a linear polynomial r(X) = r1_i + (r2_sub_r1_i) * X (basically l) + let half_len = 1 << (n_vars - i - 1); + let new_coeffs: Vec> = (0..half_len) + .into_par_iter() + .map(|b| { + let left_idx = b << 1; + let right_idx = left_idx + 1; + + let left_coeffs = &coeffs[left_idx]; + let right_coeffs = &coeffs[right_idx]; + + // Initialize result coefficients + let mut result_coeffs = vec![F::zero(); right_coeffs.len() + 1]; + + // Add left polynomial contribution + for (j, &left_val) in left_coeffs.iter().enumerate() { + result_coeffs[j] = left_val; + } + + // Add (right - left) * (r1_i + r2_sub_r1_i * X) contribution directly + for (j, (&right_val, &left_val)) in + right_coeffs.iter().zip(left_coeffs.iter()).enumerate() + { + let diff = right_val - left_val; + result_coeffs[j] += diff * r1_i; + result_coeffs[j + 1] += diff * r2_sub_r1_i; + } + + result_coeffs + }) .collect(); - for (i, (&r1_i, &r2_sub_r1_i)) in r1.iter().zip(r2_sub_r1.iter()).enumerate().take(n_vars) { - // Create a linear polynomial r(X) = r1_i + (r2_sub_r1_i) * X (basically l) - let half_len = 1 << (n_vars - i - 1); - let new_coeffs: Vec> = (0..half_len) - .into_par_iter() - .map(|b| { - let left_idx = b << 1; - let right_idx = left_idx + 1; - - let left_coeffs = &coeffs[left_idx]; - let right_coeffs = &coeffs[right_idx]; - - // Initialize result coefficients - let mut result_coeffs = vec![F::zero(); right_coeffs.len() + 1]; - - // Add left polynomial contribution - for (j, &left_val) in left_coeffs.iter().enumerate() { - result_coeffs[j] = left_val; - } - - // Add (right - left) * (r1_i + r2_sub_r1_i * X) contribution directly - for (j, (&right_val, &left_val)) in right_coeffs.iter().zip(left_coeffs.iter()).enumerate() { - let diff = right_val - left_val; - result_coeffs[j] += diff * r1_i; - result_coeffs[j + 1] += diff * r2_sub_r1_i; - } - - result_coeffs - }) - .collect(); - - coeffs = new_coeffs; - } + coeffs = new_coeffs; + } - Ok(DensePolynomial::from_coefficients_vec(coeffs.swap_remove(0))) + Ok(DensePolynomial::from_coefficients_vec( + coeffs.swap_remove(0), + )) } /// Implementation for computing h by not following Algorithm 1 "MLE-after-line composition" off the Mova paper @@ -335,7 +335,8 @@ fn compute_h2( MultilinearExtension::DenseMLE(mle_dense) => { // Start with evaluations as degree-0 constant polynomials, // We'll represent polynomials as coefficient vectors instead of DensePolynomials as it's more efficient. - let mut coeffs: Vec> = mle_dense.evaluations + let mut coeffs: Vec> = mle_dense + .evaluations .iter() .map(|&eval| vec![eval]) .collect(); @@ -362,7 +363,9 @@ fn compute_h2( } // Add right polynomial contribution directly: (right - left) * (r1_i + r2_sub_r1_i * x) - for (j, (&right_val, &left_val)) in right_coeffs.iter().zip(left_coeffs.iter()).enumerate() { + for (j, (&right_val, &left_val)) in + right_coeffs.iter().zip(left_coeffs.iter()).enumerate() + { let diff = right_val - left_val; result_coeffs[j] += diff * r1_i; result_coeffs[j + 1] += diff * r2_sub_r1_i; @@ -377,7 +380,7 @@ fn compute_h2( // Convert final coefficient vector to polynomial Ok(SparseOrDensePolynomial::from_dense( - DensePolynomial::from_coefficients_vec(coeffs.into_iter().next().unwrap()) + DensePolynomial::from_coefficients_vec(coeffs.into_iter().next().unwrap()), )) } @@ -391,15 +394,18 @@ fn compute_h2( let max_degree = n_vars + 1; // Pre-compute linear factors to avoid repeated computation let linear_factors: Vec<(F, F, F, F)> = (0..n_vars) - .map(|i| ( - r1[i], // factor_1_const - r2_sub_r1[i], // factor_1_linear - F::one() - r1[i], // factor_0_const - -r2_sub_r1[i], // factor_0_linear - )) + .map(|i| { + ( + r1[i], // factor_1_const + r2_sub_r1[i], // factor_1_linear + F::one() - r1[i], // factor_0_const + -r2_sub_r1[i], // factor_0_linear + ) + }) .collect(); - let result_coeffs = mle_sparse.evaluations + let result_coeffs = mle_sparse + .evaluations .par_iter() .map(|(&index, &value)| { let mut contrib_coeffs = vec![F::zero(); max_degree]; @@ -418,9 +424,11 @@ fn compute_h2( }; // Multiply in-place by linear polynomial - contrib_coeffs[current_degree + 1] = contrib_coeffs[current_degree] * linear_term; + contrib_coeffs[current_degree + 1] = + contrib_coeffs[current_degree] * linear_term; for j in (1..=current_degree).rev() { - contrib_coeffs[j] = contrib_coeffs[j] * const_term + contrib_coeffs[j-1] * linear_term; + contrib_coeffs[j] = contrib_coeffs[j] * const_term + + contrib_coeffs[j - 1] * linear_term; } contrib_coeffs[0] *= const_term; @@ -441,7 +449,7 @@ fn compute_h2( } } acc - } + }, ); // Remove trailing zeros @@ -451,7 +459,7 @@ fn compute_h2( } Ok(SparseOrDensePolynomial::from_dense( - DensePolynomial::from_coefficients_vec(result_coeffs) + DensePolynomial::from_coefficients_vec(result_coeffs), )) } } @@ -481,7 +489,9 @@ mod tests { use crate::Error; use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; use ark_pallas::{Fr, Projective}; - use ark_poly::{DenseMultilinearExtension, DenseUVPolynomial, Polynomial, SparseMultilinearExtension}; + use ark_poly::{ + DenseMultilinearExtension, DenseUVPolynomial, Polynomial, SparseMultilinearExtension, + }; use ark_std::{log2, UniformRand}; use crate::folding::nova::nifs::mova::Witness; @@ -843,7 +853,7 @@ mod tests { #[test] fn test_compute_h2_compare() { use ark_std::test_rng; - + // Test Case 1: Simple case with sparse pattern { let vanilla_dense = DenseMultilinearExtension::from_evaluations_slice( @@ -883,10 +893,11 @@ mod tests { { let mut rng = test_rng(); let evaluations: Vec = (0..16).map(|_| Fr::rand(&mut rng)).collect(); - - let vanilla_dense = DenseMultilinearExtension::from_evaluations_vec(4, evaluations.clone()); + + let vanilla_dense = + DenseMultilinearExtension::from_evaluations_vec(4, evaluations.clone()); let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); - + // Create sparse version by filtering out small values let sparse_evals: Vec<(usize, Fr)> = evaluations .iter() @@ -895,7 +906,7 @@ mod tests { .map(|(i, &val)| (i, val)) .collect(); let mle_sparse = MultilinearExtension::SparseMLE( - SparseMultilinearExtension::from_evaluations(4, &sparse_evals) + SparseMultilinearExtension::from_evaluations(4, &sparse_evals), ); let r0: Vec = (0..4).map(|_| Fr::rand(&mut rng)).collect(); @@ -906,19 +917,26 @@ mod tests { let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); - assert_eq!(result_h2_dense, result_h2_sparse, "Random 4-var case: dense vs sparse mismatch"); - assert_eq!(result_h2_dense.coeffs(), result_h.coeffs(), "Random 4-var case: h2_dense vs h mismatch"); + assert_eq!( + result_h2_dense, result_h2_sparse, + "Random 4-var case: dense vs sparse mismatch" + ); + assert_eq!( + result_h2_dense.coeffs(), + result_h.coeffs(), + "Random 4-var case: h2_dense vs h mismatch" + ); } // Test Case 3: Edge case - all zeros except one { let mut evaluations = vec![Fr::zero(); 8]; evaluations[5] = Fr::from(42); - + let vanilla_dense = DenseMultilinearExtension::from_evaluations_vec(3, evaluations); let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); let mle_sparse = MultilinearExtension::SparseMLE( - SparseMultilinearExtension::from_evaluations(3, &[(5, Fr::from(42))]) + SparseMultilinearExtension::from_evaluations(3, &[(5, Fr::from(42))]), ); let r0 = [Fr::from(7), Fr::from(11), Fr::from(13)]; @@ -929,19 +947,27 @@ mod tests { let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); - assert_eq!(result_h2_dense, result_h2_sparse, "Single non-zero case: dense vs sparse mismatch"); - assert_eq!(result_h2_dense.coeffs(), result_h.coeffs(), "Single non-zero case: h2_dense vs h mismatch"); + assert_eq!( + result_h2_dense, result_h2_sparse, + "Single non-zero case: dense vs sparse mismatch" + ); + assert_eq!( + result_h2_dense.coeffs(), + result_h.coeffs(), + "Single non-zero case: h2_dense vs h mismatch" + ); } // Test Case 4: Edge case - all ones (dense case) { let evaluations = vec![Fr::one(); 16]; - - let vanilla_dense = DenseMultilinearExtension::from_evaluations_vec(4, evaluations.clone()); + + let vanilla_dense = + DenseMultilinearExtension::from_evaluations_vec(4, evaluations.clone()); let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); let sparse_evals: Vec<(usize, Fr)> = (0..16).map(|i| (i, Fr::one())).collect(); let mle_sparse = MultilinearExtension::SparseMLE( - SparseMultilinearExtension::from_evaluations(4, &sparse_evals) + SparseMultilinearExtension::from_evaluations(4, &sparse_evals), ); let r0 = [Fr::from(2), Fr::from(3), Fr::from(5), Fr::from(7)]; @@ -952,19 +978,33 @@ mod tests { let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); - assert_eq!(result_h2_dense, result_h2_sparse, "All ones case: dense vs sparse mismatch"); - assert_eq!(result_h2_dense.coeffs(), result_h.coeffs(), "All ones case: h2_dense vs h mismatch"); + assert_eq!( + result_h2_dense, result_h2_sparse, + "All ones case: dense vs sparse mismatch" + ); + assert_eq!( + result_h2_dense.coeffs(), + result_h.coeffs(), + "All ones case: h2_dense vs h mismatch" + ); } // Test Case 5: Alternating pattern { let evaluations: Vec = (0..32) - .map(|i| if i % 2 == 0 { Fr::from(i as u64 + 1) } else { Fr::zero() }) + .map(|i| { + if i % 2 == 0 { + Fr::from(i as u64 + 1) + } else { + Fr::zero() + } + }) .collect(); - - let vanilla_dense = DenseMultilinearExtension::from_evaluations_vec(5, evaluations.clone()); + + let vanilla_dense = + DenseMultilinearExtension::from_evaluations_vec(5, evaluations.clone()); let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); - + let sparse_evals: Vec<(usize, Fr)> = evaluations .iter() .enumerate() @@ -972,19 +1012,38 @@ mod tests { .map(|(i, &val)| (i, val)) .collect(); let mle_sparse = MultilinearExtension::SparseMLE( - SparseMultilinearExtension::from_evaluations(5, &sparse_evals) + SparseMultilinearExtension::from_evaluations(5, &sparse_evals), ); - let r0 = [Fr::from(1), Fr::from(4), Fr::from(9), Fr::from(16), Fr::from(25)]; - let r1 = [Fr::from(36), Fr::from(49), Fr::from(64), Fr::from(81), Fr::from(100)]; + let r0 = [ + Fr::from(1), + Fr::from(4), + Fr::from(9), + Fr::from(16), + Fr::from(25), + ]; + let r1 = [ + Fr::from(36), + Fr::from(49), + Fr::from(64), + Fr::from(81), + Fr::from(100), + ]; let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); let result_h = compute_h(&vanilla_dense, &r0, &r1_sub_r0).unwrap(); let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); - assert_eq!(result_h2_dense, result_h2_sparse, "Alternating pattern case: dense vs sparse mismatch"); - assert_eq!(result_h2_dense.coeffs(), result_h.coeffs(), "Alternating pattern case: h2_dense vs h mismatch"); + assert_eq!( + result_h2_dense, result_h2_sparse, + "Alternating pattern case: dense vs sparse mismatch" + ); + assert_eq!( + result_h2_dense.coeffs(), + result_h.coeffs(), + "Alternating pattern case: h2_dense vs h mismatch" + ); } // Test Case 6: Very sparse case (only corner evaluations) @@ -992,12 +1051,14 @@ mod tests { let mut evaluations = vec![Fr::zero(); 16]; evaluations[0] = Fr::from(10); evaluations[15] = Fr::from(20); - + let vanilla_dense = DenseMultilinearExtension::from_evaluations_vec(4, evaluations); let mle_dense = MultilinearExtension::DenseMLE(vanilla_dense.clone()); - let mle_sparse = MultilinearExtension::SparseMLE( - SparseMultilinearExtension::from_evaluations(4, &[(0, Fr::from(10)), (15, Fr::from(20))]) - ); + let mle_sparse = + MultilinearExtension::SparseMLE(SparseMultilinearExtension::from_evaluations( + 4, + &[(0, Fr::from(10)), (15, Fr::from(20))], + )); let r0 = [Fr::zero(), Fr::zero(), Fr::zero(), Fr::zero()]; let r1 = [Fr::one(), Fr::one(), Fr::one(), Fr::one()]; @@ -1007,12 +1068,27 @@ mod tests { let result_h2_dense = compute_h2(&mle_dense, &r0, &r1_sub_r0).unwrap(); let result_h2_sparse = compute_h2(&mle_sparse, &r0, &r1_sub_r0).unwrap(); - assert_eq!(result_h2_dense, result_h2_sparse, "Corner values case: dense vs sparse mismatch"); - assert_eq!(result_h2_dense.coeffs(), result_h.coeffs(), "Corner values case: h2_dense vs h mismatch"); - + assert_eq!( + result_h2_dense, result_h2_sparse, + "Corner values case: dense vs sparse mismatch" + ); + assert_eq!( + result_h2_dense.coeffs(), + result_h.coeffs(), + "Corner values case: h2_dense vs h mismatch" + ); + // Verify the interpolation property: h(0) should be MLE(r0) and h(1) should be MLE(r1) - assert_eq!(result_h.evaluate(&Fr::zero()), Fr::from(10), "h(0) should equal MLE(r0)"); - assert_eq!(result_h.evaluate(&Fr::one()), Fr::from(20), "h(1) should equal MLE(r1)"); + assert_eq!( + result_h.evaluate(&Fr::zero()), + Fr::from(10), + "h(0) should equal MLE(r0)" + ); + assert_eq!( + result_h.evaluate(&Fr::one()), + Fr::from(20), + "h(1) should equal MLE(r1)" + ); } } } From 641e86a4dc4574c5d8387a4c735386118e0f8781 Mon Sep 17 00:00:00 2001 From: Fantoni0 Date: Thu, 12 Jun 2025 10:16:03 +0200 Subject: [PATCH 5/9] Fix clippy --- folding-schemes/src/folding/nova/nifs/pointvsline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/folding-schemes/src/folding/nova/nifs/pointvsline.rs b/folding-schemes/src/folding/nova/nifs/pointvsline.rs index bfdfa08cb..3120e3def 100644 --- a/folding-schemes/src/folding/nova/nifs/pointvsline.rs +++ b/folding-schemes/src/folding/nova/nifs/pointvsline.rs @@ -413,7 +413,7 @@ fn compute_h2( let mut current_degree = 0; // Multiply by the linear factor for each variable - for i in 0..n_vars { + for (i, _) in linear_factors.iter().enumerate().take(n_vars) { let bit_i = (index >> i) & 1 == 1; let (const_term, linear_term) = if bit_i { // If bit_i == 1, use r1_i + r2_sub_r1_i * x From 19bda58a2c0757e73151a2ce0d4172128edaf2dc Mon Sep 17 00:00:00 2001 From: Fantoni0 Date: Thu, 19 Jun 2025 09:27:49 +0200 Subject: [PATCH 6/9] Add size to benches --- benches/mova_matrix.rs | 87 ++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/benches/mova_matrix.rs b/benches/mova_matrix.rs index 95a94aea0..dc33c4177 100644 --- a/benches/mova_matrix.rs +++ b/benches/mova_matrix.rs @@ -85,52 +85,55 @@ fn bench_mova_matrix(c: &mut Criterion) { for count in NUM_OF_PRECONDITION_FOLDS { group .measurement_time(Duration::from_secs(20 * (*count as u64))) - .bench_function(&format!("{count}"), |b| { - // Set up transcript and commitment scheme - let hyrax_params = - HyraxGenerators::::setup(&mut rng, mat_dim * mat_dim); - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::rand(&mut rng); + .bench_function( + &format!("mova_matrix_sequential_folding/size={mat_dim}x{mat_dim}/n_folds={count}"), + |b| { + // Set up transcript and commitment scheme + let hyrax_params = + HyraxGenerators::::setup(&mut rng, mat_dim * mat_dim); + let poseidon_config = poseidon_canonical_config::(); + let pp_hash = Fr::rand(&mut rng); - b.iter_custom(|iters| { - let mut total_duration = Duration::ZERO; - for _ in 0..iters { - let mut instances: Vec<( - Witness, - RelaxedCommittedRelation, - )> = get_instances::( - count + 1, // we want the number of folds plus one for the acc_instance - mat_dim, - &mut rng, - &hyrax_params, - ); - let mut transcript_p = PoseidonSponge::::new(&poseidon_config); - let mut acc = instances.pop().unwrap(); + b.iter_custom(|iters| { + let mut total_duration = Duration::ZERO; + for _ in 0..iters { + let mut instances: Vec<( + Witness, + RelaxedCommittedRelation, + )> = get_instances::( + count + 1, // we want the number of folds plus one for the acc_instance + mat_dim, + &mut rng, + &hyrax_params, + ); + let mut transcript_p = PoseidonSponge::::new(&poseidon_config); + let mut acc = instances.pop().unwrap(); - for _ in 0..*count { - let mut next = instances.pop().unwrap(); - total_duration += { - let timer = Instant::now(); + for _ in 0..*count { + let mut next = instances.pop().unwrap(); + total_duration += { + let timer = Instant::now(); - let (wit_acc, inst_acc, _) = - NIFS::>::prove( - &mut transcript_p, - pp_hash, - &mut next.0, - &next.1, - &acc.0, - &acc.1, - ) - .unwrap(); - let time = timer.elapsed(); - acc = (wit_acc, inst_acc); - time - }; + let (wit_acc, inst_acc, _) = + NIFS::>::prove( + &mut transcript_p, + pp_hash, + &mut next.0, + &next.1, + &acc.0, + &acc.1, + ) + .unwrap(); + let time = timer.elapsed(); + acc = (wit_acc, inst_acc); + time + }; + } } - } - total_duration - }); - }); + total_duration + }); + }, + ); } } From 2bcbfc53a7ff69042f038b6258346fbbcdb32cc9 Mon Sep 17 00:00:00 2001 From: Fantoni0 Date: Thu, 19 Jun 2025 10:16:17 +0200 Subject: [PATCH 7/9] Update matrex dependency --- folding-schemes/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/folding-schemes/Cargo.toml b/folding-schemes/Cargo.toml index 248174918..bfeea1a6c 100644 --- a/folding-schemes/Cargo.toml +++ b/folding-schemes/Cargo.toml @@ -23,8 +23,7 @@ num-bigint = "0.4" num-integer = "0.1" sha3 = "0.10" log = "0.4" -matrex = {git = "ssh://git@github.com/NethermindEth/matrex.git", features = ["parallel"]} - +matrex = {git = "ssh://git@github.com/NethermindEth/matrex.git", branch = "main", features = ["parallel"]} [dev-dependencies] ark-pallas = {version="^0.5.0", features=["r1cs"]} From 334b26d222e9340e14685028a4ee6a0656ad5ae5 Mon Sep 17 00:00:00 2001 From: Fantoni0 Date: Wed, 2 Jul 2025 15:07:12 +0200 Subject: [PATCH 8/9] Add Merlin support --- folding-schemes/src/transcript/merlin.rs | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 folding-schemes/src/transcript/merlin.rs diff --git a/folding-schemes/src/transcript/merlin.rs b/folding-schemes/src/transcript/merlin.rs new file mode 100644 index 000000000..6f3b465a6 --- /dev/null +++ b/folding-schemes/src/transcript/merlin.rs @@ -0,0 +1,33 @@ +use ark_crypto_primitives::sponge::{ + merlin::Transcript as MerlinTranscript, Absorb, CryptographicSponge, +}; +use ark_ec::{AffineRepr, CurveGroup}; +use ark_ff::{BigInteger, PrimeField}; + +use super::{AbsorbNonNative, Transcript}; + +impl Transcript for MerlinTranscript { + fn absorb_point>(&mut self, p: &C) { + let (x, y) = p.into_affine().xy().unwrap_or_default(); + self.absorb(&x); + self.absorb(&y); + } + fn absorb_nonnative(&mut self, v: &V) { + self.absorb(&v.to_native_sponge_field_elements_as_vec::()); + } + fn get_challenge(&mut self) -> F { + let c = self.squeeze_field_elements(1); + self.absorb(&c[0]); + c[0] + } + fn get_challenge_nbits(&mut self, nbits: usize) -> Vec { + let bits = self.squeeze_bits(nbits); + self.absorb(&F::from(F::BigInt::from_bits_le(&bits))); + bits + } + fn get_challenges(&mut self, n: usize) -> Vec { + let c = self.squeeze_field_elements(n); + self.absorb(&c); + c + } +} From b47a10d5de6b0c9cec27a9f14fd11d6a26d04137 Mon Sep 17 00:00:00 2001 From: Fantoni0 Date: Wed, 2 Jul 2025 15:47:14 +0200 Subject: [PATCH 9/9] Missing export --- folding-schemes/src/transcript/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/folding-schemes/src/transcript/mod.rs b/folding-schemes/src/transcript/mod.rs index de3a05721..75dabdb48 100644 --- a/folding-schemes/src/transcript/mod.rs +++ b/folding-schemes/src/transcript/mod.rs @@ -4,6 +4,7 @@ use ark_ff::PrimeField; use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar, groups::CurveVar}; use ark_relations::r1cs::SynthesisError; +pub mod merlin; pub mod poseidon; /// An interface for objects that can be absorbed by a `Transcript`.