diff --git a/jolt-core/benches/grand_product.rs b/jolt-core/benches/grand_product.rs index d737bb38f..d12b437c7 100644 --- a/jolt-core/benches/grand_product.rs +++ b/jolt-core/benches/grand_product.rs @@ -153,7 +153,7 @@ fn benchmark_verify( transcript = ProofTranscript::new(b"test_transcript"); let mut verifier_accumulator: VerifierOpeningAccumulator = VerifierOpeningAccumulator::new(); - let (_, r_verifier) = QuarkGrandProduct::verify_grand_product( + let (_, r_verifier) = QuarkGrandProduct::verify_quark_grand_product( &proof, &known_products, Some(&mut verifier_accumulator), diff --git a/jolt-core/src/subprotocols/grand_product.rs b/jolt-core/src/subprotocols/grand_product.rs index 87b4028ac..059364776 100644 --- a/jolt-core/src/subprotocols/grand_product.rs +++ b/jolt-core/src/subprotocols/grand_product.rs @@ -186,6 +186,10 @@ where Self::verify_layers(&proof.gkr_layers, claim, transcript, r) } + + fn quark_poly(&self) -> Option<&[F]> { + None + } } pub trait BatchedGrandProductLayer: diff --git a/jolt-core/src/subprotocols/grand_product_quarks.rs b/jolt-core/src/subprotocols/grand_product_quarks.rs index 6096b0734..62057c917 100644 --- a/jolt-core/src/subprotocols/grand_product_quarks.rs +++ b/jolt-core/src/subprotocols/grand_product_quarks.rs @@ -8,6 +8,7 @@ use crate::poly::dense_interleaved_poly::DenseInterleavedPolynomial; use crate::poly::dense_mlpoly::DensePolynomial; use crate::poly::eq_poly::EqPolynomial; use crate::poly::opening_proof::{ProverOpeningAccumulator, VerifierOpeningAccumulator}; +use crate::subprotocols::QuarkHybridLayerDepth; use crate::utils::math::Math; use crate::utils::transcript::{AppendToTranscript, Transcript}; use ark_serialize::*; @@ -27,38 +28,16 @@ pub struct QuarkGrandProductProof< g_r_sumcheck: PCS::Field, g_r_prime: (PCS::Field, PCS::Field), v_r_prime: (PCS::Field, PCS::Field), - num_vars: usize, + pub num_vars: usize, } pub struct QuarkGrandProduct { batch_size: usize, - quark_poly: Vec, + quark_poly: Option>, base_layers: Vec>, _marker: PhantomData, } -#[derive(Clone, Copy, Debug, Default)] -pub enum QuarkHybridLayerDepth { - #[default] - Default, - Min, - Max, - Custom(usize), -} - -impl QuarkHybridLayerDepth { - /// The depth in the binary tree of the GKR grand product at which the hybrid scheme - /// will switch to using Quarks Section 5 grand product argument. - pub fn get_crossover_depth(&self) -> usize { - match self { - QuarkHybridLayerDepth::Min => 0, - QuarkHybridLayerDepth::Default => 4, - QuarkHybridLayerDepth::Max => usize::MAX, - QuarkHybridLayerDepth::Custom(depth) => *depth, - } - } -} - #[derive(Clone, Copy, Debug, Default)] pub struct QuarkGrandProductConfig { pub hybrid_layer_depth: QuarkHybridLayerDepth, @@ -114,7 +93,7 @@ where if tree_depth <= num_layers { return Self { batch_size, - quark_poly: Vec::new(), + quark_poly: None, base_layers: layers, _marker: PhantomData, }; @@ -125,37 +104,49 @@ where let quark_poly = layers.pop().unwrap().coeffs; Self { batch_size, - quark_poly, + quark_poly: Some(quark_poly), base_layers: layers, _marker: PhantomData, } } fn num_layers(&self) -> usize { - unimplemented!("Unused"); + self.base_layers.len() } /// The claimed outputs of the grand products. fn claimed_outputs(&self) -> Vec { - let chunk_size = self.quark_poly.len() / self.batch_size; - self.quark_poly - .par_chunks(chunk_size) - .map(|chunk| chunk.iter().product()) - .collect() + if let Some(quark_poly) = &self.quark_poly { + let chunk_size = quark_poly.len() / self.batch_size; + quark_poly + .par_chunks(chunk_size) + .map(|chunk| chunk.iter().product()) + .collect() + } else { + let top_layer = &self.base_layers[self.base_layers.len() - 1]; + top_layer + .par_chunks(2) + .map(|chunk| chunk[0] * chunk[1]) + .collect() + } } /// Returns an iterator over the layers of this batched grand product circuit. /// Each layer is mutable so that its polynomials can be bound over the course /// of proving. - #[allow(unreachable_code)] fn layers( &'_ mut self, ) -> impl Iterator> { - unimplemented!("We don't use the default prover and so we don't need the generic iterator"); - std::iter::empty() + self.base_layers + .iter_mut() + .map(|layer| layer as &mut dyn BatchedGrandProductLayer) + .rev() + } + + fn quark_poly(&self) -> Option<&[F]> { + self.quark_poly.as_deref() } - /// Computes a batched grand product proof, layer by layer. #[tracing::instrument(skip_all, name = "BatchedGrandProduct::prove_grand_product")] fn prove_grand_product( &mut self, @@ -163,23 +154,64 @@ where transcript: &mut ProofTranscript, setup: Option<&PCS::Setup>, ) -> (BatchedGrandProductProof, Vec) { - let mut proof_layers = Vec::with_capacity(self.base_layers.len()); + QuarkGrandProductBase::prove_quark_grand_product( + self, + opening_accumulator, + transcript, + setup, + ) + } - let outputs: Vec = - >::claimed_outputs(self); + #[tracing::instrument(skip_all, name = "BatchedGrandProduct::verify_grand_product")] + fn verify_grand_product( + proof: &BatchedGrandProductProof, + claimed_outputs: &[F], + opening_accumulator: Option<&mut VerifierOpeningAccumulator>, + transcript: &mut ProofTranscript, + _setup: Option<&PCS::Setup>, + ) -> (F, Vec) { + QuarkGrandProductBase::verify_quark_grand_product::( + proof, + claimed_outputs, + opening_accumulator, + transcript, + ) + } +} + +pub struct QuarkGrandProductBase { + _marker: PhantomData<(F, ProofTranscript)>, +} + +impl QuarkGrandProductBase +where + F: JoltField, + ProofTranscript: Transcript, +{ + /// Computes a batched grand product proof, layer by layer. + #[tracing::instrument(skip_all, name = "QuarkGrandProduct::prove_grand_product")] + pub fn prove_quark_grand_product>( + grand_product: &mut impl BatchedGrandProduct, + opening_accumulator: Option<&mut ProverOpeningAccumulator>, + transcript: &mut ProofTranscript, + setup: Option<&PCS::Setup>, + ) -> (BatchedGrandProductProof, Vec) { + let mut proof_layers = Vec::with_capacity(grand_product.num_layers()); + + let outputs: Vec = grand_product.claimed_outputs(); transcript.append_scalars(&outputs); let output_mle = DensePolynomial::new_padded(outputs); let r_outputs: Vec = transcript.challenge_vector(output_mle.get_num_vars()); let claim = output_mle.evaluate(&r_outputs); // For polynomials of size less than 16 we just use the GKR grand product - let (quark_proof, mut random, mut claim) = if !self.quark_poly.is_empty() { - // When doing the quark hybrid proof, we first prove the grand product of a layer of a polynomial which is 4 layers deep in the tree + let (quark_proof, mut random, mut claim) = if grand_product.quark_poly().is_some() { + // When doing the quark hybrid proof, we first prove the grand product of a layer of a polynomial which is N layers deep in the tree // of a standard layered sumcheck grand product, then we use the sumcheck layers to prove via GKR layers that the random point opened // by the quark proof is in fact the folded result of the base layer. let (quark, random, quark_claim) = QuarkGrandProductProof::::prove( - &self.quark_poly, + grand_product.quark_poly().unwrap(), r_outputs, claim, opening_accumulator.unwrap(), @@ -191,7 +223,7 @@ where (None, r_outputs, claim) }; - for layer in self.base_layers.iter_mut().rev() { + for layer in grand_product.layers() { proof_layers.push(layer.prove_layer(&mut claim, &mut random, transcript)); } @@ -205,16 +237,17 @@ where } /// Verifies the given grand product proof. - #[tracing::instrument(skip_all, name = "BatchedGrandProduct::verify_grand_product")] - fn verify_grand_product( + #[tracing::instrument(skip_all, name = "QuarkGrandProduct::verify_grand_product")] + pub fn verify_quark_grand_product( proof: &BatchedGrandProductProof, claimed_outputs: &[F], opening_accumulator: Option<&mut VerifierOpeningAccumulator>, transcript: &mut ProofTranscript, - _setup: Option<&PCS::Setup>, - ) -> (F, Vec) { - // Evaluate the MLE of the output layer at a random point to reduce the outputs to - // a single claim. + ) -> (F, Vec) + where + PCS: CommitmentScheme, + G: BatchedGrandProduct, + { transcript.append_scalars(claimed_outputs); let r_outputs: Vec = transcript.challenge_vector(claimed_outputs.len().next_power_of_two().log_2()); @@ -225,7 +258,6 @@ where Some(quark) => { // In this case we verify the quark which fixes the first log(n)-4 vars in the random eval point. let v_len = quark.num_vars; - // Todo (aleph_v) - bubble up errors quark .verify( r_outputs, @@ -234,7 +266,7 @@ where transcript, v_len, ) - .unwrap() + .unwrap_or_else(|e| panic!("quark verify error: {:?}", e)) } None => { // Otherwise we must check the actual claims and the preset random will be empty. @@ -242,13 +274,8 @@ where } }; - let (grand_product_claim, grand_product_r) = >::verify_layers( - &proof.gkr_layers, claim, transcript, rand - ); + let (grand_product_claim, grand_product_r) = + G::verify_layers(&proof.gkr_layers, claim, transcript, rand); (grand_product_claim, grand_product_r) } @@ -277,7 +304,7 @@ where /// Then - Constructs a g poly and preforms sumcheck proof that sum == 0 /// Finally - computes opening proofs for a random sampled during sumcheck proof and returns /// Returns a random point and evaluation to be verified by the caller (which our hybrid prover does with GKR) - fn prove( + pub fn prove( v: &[PCS::Field], r_outputs: Vec, claim: PCS::Field, @@ -288,7 +315,7 @@ where let v_length = v.len(); let v_variables = v_length.log_2(); - let v_polynomial = DensePolynomial::::new(v.to_vec()); + let v_polynomial = DensePolynomial::::new_padded(v.to_vec()); // Compute f(1, x), f(x, 0), and f(x, 1) from v(x) let (f_1x, f_x0, f_x1) = v_into_f::(&v_polynomial); @@ -443,7 +470,7 @@ where /// Verifies the given grand product proof. #[allow(clippy::type_complexity)] - fn verify( + pub fn verify( &self, r_outputs: Vec, claim: PCS::Field, @@ -681,7 +708,7 @@ mod quark_grand_product_tests { &known_products, Some(&mut verifier_accumulator), &mut verifier_transcript, - Some(&setup), + None, ); assert!(verifier_accumulator .reduce_and_verify(&setup, &batched_proof, &mut verifier_transcript) diff --git a/jolt-core/src/subprotocols/mod.rs b/jolt-core/src/subprotocols/mod.rs index 66d0b30ab..1904bec9e 100644 --- a/jolt-core/src/subprotocols/mod.rs +++ b/jolt-core/src/subprotocols/mod.rs @@ -4,3 +4,25 @@ pub mod grand_product; pub mod grand_product_quarks; pub mod sparse_grand_product; pub mod sumcheck; + +#[derive(Clone, Copy, Debug, Default)] +pub enum QuarkHybridLayerDepth { + #[default] + Default, + Min, + Max, + Custom(usize), +} + +impl QuarkHybridLayerDepth { + // The depth in the product tree of the grand product at which the + // hybrid implementation will switch to using quarks grand product proofs + pub fn get_crossover_depth(&self) -> usize { + match self { + QuarkHybridLayerDepth::Min => 0, // Always use quarks + QuarkHybridLayerDepth::Default => 4, + QuarkHybridLayerDepth::Max => usize::MAX, // Never use quarks + QuarkHybridLayerDepth::Custom(depth) => *depth, + } + } +} diff --git a/jolt-core/src/subprotocols/sparse_grand_product.rs b/jolt-core/src/subprotocols/sparse_grand_product.rs index 31e0e5f88..1e192b51d 100644 --- a/jolt-core/src/subprotocols/sparse_grand_product.rs +++ b/jolt-core/src/subprotocols/sparse_grand_product.rs @@ -1,14 +1,18 @@ use super::grand_product::{ BatchedGrandProduct, BatchedGrandProductLayer, BatchedGrandProductLayerProof, + BatchedGrandProductProof, }; use super::sumcheck::{BatchedCubicSumcheck, Bindable}; use crate::field::{JoltField, OptimizedMul}; use crate::poly::commitment::commitment_scheme::CommitmentScheme; #[cfg(test)] use crate::poly::dense_mlpoly::DensePolynomial; +use crate::poly::opening_proof::{ProverOpeningAccumulator, VerifierOpeningAccumulator}; use crate::poly::sparse_interleaved_poly::SparseInterleavedPolynomial; use crate::poly::split_eq_poly::SplitEqPolynomial; use crate::poly::unipoly::UniPoly; +use crate::subprotocols::grand_product_quarks::QuarkGrandProductBase; +use crate::subprotocols::QuarkHybridLayerDepth; use crate::utils::math::Math; use crate::utils::thread::drop_in_background_thread; use crate::utils::transcript::Transcript; @@ -383,7 +387,7 @@ impl BatchedCubicSumcheck BatchedGrandProductLayer Self { + Self { + // Quarks are not used by default + hybrid_layer_depth: QuarkHybridLayerDepth::Max, + } + } +} + pub struct ToggledBatchedGrandProduct { + batch_size: usize, toggle_layer: BatchedGrandProductToggleLayer, sparse_layers: Vec>, + quark_poly: Option>, } impl BatchedGrandProduct @@ -941,36 +961,71 @@ where ProofTranscript: Transcript, { type Leaves = (Vec>, Vec>); // (flags, fingerprints) - type Config = (); + type Config = SparseGrandProductConfig; #[tracing::instrument(skip_all, name = "ToggledBatchedGrandProduct::construct")] fn construct(leaves: Self::Leaves) -> Self { + >::construct_with_config( + leaves, + SparseGrandProductConfig::default(), + ) + } + + #[tracing::instrument(skip_all, name = "ToggledBatchedGrandProduct::construct_with_config")] + fn construct_with_config(leaves: Self::Leaves, config: Self::Config) -> Self { let (flags, fingerprints) = leaves; - let num_layers = fingerprints[0].len().log_2(); + let batch_size = fingerprints.len(); + let tree_depth = fingerprints[0].len().log_2(); + let crossover = config.hybrid_layer_depth.get_crossover_depth(); + + let uses_quarks = tree_depth - 1 > crossover; + let num_sparse_layers = if uses_quarks { + crossover + } else { + tree_depth - 1 + }; let toggle_layer = BatchedGrandProductToggleLayer::new(flags, fingerprints); - let mut layers: Vec> = Vec::with_capacity(num_layers); + let mut layers: Vec<_> = Vec::with_capacity(1 + num_sparse_layers); layers.push(toggle_layer.layer_output()); - for i in 0..num_layers - 1 { + for i in 0..num_sparse_layers { let previous_layer = &layers[i]; layers.push(previous_layer.layer_output()); } + // Set the Quark polynomial only if the number of layers exceeds the crossover depth + let quark_poly = if uses_quarks { + Some(layers.pop().unwrap().coalesce()) + } else { + None + }; + Self { + batch_size, toggle_layer, sparse_layers: layers, + quark_poly, } } fn num_layers(&self) -> usize { - self.sparse_layers.len() + 1 + self.sparse_layers.len() + 1 + self.quark_poly.is_some() as usize } fn claimed_outputs(&self) -> Vec { - let last_layer = self.sparse_layers.last().unwrap(); - let (left, right) = last_layer.uninterleave(); - left.iter().zip(right.iter()).map(|(l, r)| *l * r).collect() + // If there's a quark poly, then that's the claimed output + if let Some(quark_poly) = &self.quark_poly { + let chunk_size = quark_poly.len() / self.batch_size; + quark_poly + .par_chunks(chunk_size) + .map(|chunk| chunk.iter().product()) + .collect() + } else { + let last_layer = self.sparse_layers.last().unwrap(); + let (left, right) = last_layer.uninterleave(); + left.iter().zip(right.iter()).map(|(l, r)| *l * r).collect() + } } fn layers( @@ -986,6 +1041,43 @@ where .rev() } + fn quark_poly(&self) -> Option<&[F]> { + self.quark_poly.as_deref() + } + + /// Computes a batched grand product proof, layer by layer. + #[tracing::instrument(skip_all, name = "ToggledBatchedGrandProduct::prove_grand_product")] + fn prove_grand_product( + &mut self, + opening_accumulator: Option<&mut ProverOpeningAccumulator>, + transcript: &mut ProofTranscript, + setup: Option<&PCS::Setup>, + ) -> (BatchedGrandProductProof, Vec) { + QuarkGrandProductBase::prove_quark_grand_product( + self, + opening_accumulator, + transcript, + setup, + ) + } + + /// Verifies the given grand product proof. + #[tracing::instrument(skip_all, name = "ToggledBatchedGrandProduct::verify_grand_product")] + fn verify_grand_product( + proof: &BatchedGrandProductProof, + claimed_outputs: &[F], + opening_accumulator: Option<&mut VerifierOpeningAccumulator>, + transcript: &mut ProofTranscript, + _setup: Option<&PCS::Setup>, + ) -> (F, Vec) { + QuarkGrandProductBase::verify_quark_grand_product::( + proof, + claimed_outputs, + opening_accumulator, + transcript, + ) + } + fn verify_sumcheck_claim( layer_proofs: &[BatchedGrandProductLayerProof], layer_index: usize, @@ -1024,15 +1116,12 @@ where - layer_proof.left_claim; } } - - fn construct_with_config(leaves: Self::Leaves, _config: Self::Config) -> Self { - >::construct(leaves) - } } #[cfg(test)] mod tests { use super::*; + use crate::poly::commitment::zeromorph::ZeromorphSRS; use crate::{ poly::{ commitment::zeromorph::Zeromorph, dense_interleaved_poly::DenseInterleavedPolynomial, @@ -1042,6 +1131,7 @@ mod tests { use ark_bn254::{Bn254, Fr}; use ark_std::{rand::Rng, test_rng, One}; use itertools::Itertools; + use rand_core::SeedableRng; fn condense(sparse_layer: SparseInterleavedPolynomial) -> Vec { sparse_layer.to_dense().Z @@ -1176,72 +1266,105 @@ mod tests { } } - #[test] - fn sparse_prove_verify() { - let mut rng = test_rng(); - const NUM_VARS: [usize; 7] = [1, 2, 3, 4, 5, 6, 7]; - const DENSITY: [f64; 6] = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]; - const BATCH_SIZE: [usize; 5] = [2, 4, 6, 8, 10]; + fn run_sparse_prove_verify_test( + num_vars: usize, + density: f64, + batch_size: usize, + config: SparseGrandProductConfig, + ) { + let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(1111_u64); + let layer_size = 1 << num_vars; - for ((num_vars, density), batch_size) in NUM_VARS - .into_iter() - .cartesian_product(DENSITY.into_iter()) - .cartesian_product(BATCH_SIZE.into_iter()) - { - let layer_size = 1 << num_vars; - let fingerprints: Vec> = std::iter::repeat_with(|| { - let layer: Vec = std::iter::repeat_with(|| Fr::random(&mut rng)) - .take(layer_size) - .collect::>(); - layer - }) - .take(batch_size) + let fingerprints: Vec> = (0..batch_size) + .map(|_| (0..layer_size).map(|_| Fr::random(&mut rng)).collect()) .collect(); - let flags: Vec> = std::iter::repeat_with(|| { - let mut layer = vec![]; - for i in 0..layer_size { - if rng.gen_bool(density) { - layer.push(i); - } - } - layer - }) - .take(batch_size / 2) + let flags: Vec> = (0..batch_size / 2) + .map(|_| (0..layer_size).filter(|_| rng.gen_bool(density)).collect()) .collect(); - let mut circuit = as BatchedGrandProduct< - Fr, - Zeromorph, - KeccakTranscript, - >>::construct((flags, fingerprints)); - - let claims = as BatchedGrandProduct< - Fr, - Zeromorph, - KeccakTranscript, - >>::claimed_outputs(&circuit); + let srs = ZeromorphSRS::::setup(&mut rng, 1 << 10); + let setup = srs.trim(1 << 10); + + // Construct circuit with configuration + let mut circuit = as BatchedGrandProduct< + Fr, + Zeromorph, + KeccakTranscript, + >>::construct_with_config((flags, fingerprints), config); + + let claims = as BatchedGrandProduct< + Fr, + Zeromorph, + KeccakTranscript, + >>::claimed_outputs(&circuit); + + // Prover setup + let mut prover_transcript = KeccakTranscript::new(b"test_transcript"); + let mut prover_accumulator = ProverOpeningAccumulator::::new(); + let (proof, r_prover) = as BatchedGrandProduct< + Fr, + Zeromorph, + KeccakTranscript, + >>::prove_grand_product( + &mut circuit, + Some(&mut prover_accumulator), + &mut prover_transcript, + Some(&setup), + ); + + // Verifier setup + let mut verifier_transcript = KeccakTranscript::new(b"test_transcript"); + let mut verifier_accumulator = VerifierOpeningAccumulator::< + Fr, + Zeromorph, + KeccakTranscript, + >::new(); + verifier_transcript.compare_to(prover_transcript); + let (_, r_verifier) = ToggledBatchedGrandProduct::verify_grand_product( + &proof, + &claims, + Some(&mut verifier_accumulator), + &mut verifier_transcript, + Some(&setup), + ); + + assert_eq!( + r_prover, r_verifier, + "Prover and Verifier results do not match" + ); + } - let mut prover_transcript: KeccakTranscript = KeccakTranscript::new(b"test_transcript"); - let (proof, r_prover) = as BatchedGrandProduct< - Fr, - Zeromorph, - KeccakTranscript, - >>::prove_grand_product( - &mut circuit, None, &mut prover_transcript, None - ); + #[test] + fn sparse_prove_verify() { + const NUM_VARS: [usize; 7] = [1, 2, 3, 4, 5, 6, 7]; + const DENSITY: [f64; 6] = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]; + const BATCH_SIZE: [usize; 4] = [2, 4, 6, 8]; + + let configs = [ + SparseGrandProductConfig { + hybrid_layer_depth: QuarkHybridLayerDepth::Min, + }, + SparseGrandProductConfig { + hybrid_layer_depth: QuarkHybridLayerDepth::Default, + }, + SparseGrandProductConfig { + hybrid_layer_depth: QuarkHybridLayerDepth::Max, + }, + ]; - let mut verifier_transcript: KeccakTranscript = - KeccakTranscript::new(b"test_transcript"); - verifier_transcript.compare_to(prover_transcript); - let (_, r_verifier) = ToggledBatchedGrandProduct::verify_grand_product( - &proof, - &claims, - None, - &mut verifier_transcript, - None, - ); - assert_eq!(r_prover, r_verifier); + for ((&num_vars, &density), &batch_size) in NUM_VARS + .iter() + .cartesian_product(DENSITY.iter()) + .cartesian_product(BATCH_SIZE.iter()) + { + for config in &configs { + println!( + "Running test with num_vars = {}, density = {}, batch_size = {}, config = {:?}", + num_vars, density, batch_size, config + ); + run_sparse_prove_verify_test(num_vars, density, batch_size, config.clone()); + } } } @@ -1322,7 +1445,80 @@ mod tests { KeccakTranscript, >>::claimed_outputs(&circuit); - assert!(claimed_outputs == expected_outputs); + assert_eq!(claimed_outputs, expected_outputs); + } + } + + #[test] + fn test_construct_with_config() { + // Mock values for testing + let mut rng = test_rng(); + let dummy_flag = vec![vec![0; 4]; 32]; + let dummy_fingerprint: Vec> = vec![vec![Fr::random(&mut rng); 64]; 32]; + + let configs = vec![ + (6, 0), // tree_depth > crossover + (6, 64), // tree_depth == crossover + (6, 16), // tree_depth > crossover + ]; + + for (tree_depth, crossover) in configs { + let config = SparseGrandProductConfig { + hybrid_layer_depth: QuarkHybridLayerDepth::Custom(crossover), + }; + + // Mock leaves + let leaves = (dummy_flag.clone(), dummy_fingerprint.clone()); + + // Call construct_with_config with current config + let result = as BatchedGrandProduct< + Fr, + Zeromorph, + KeccakTranscript, + >>::construct_with_config(leaves, config); + + // Verify expectations for each configuration case + if tree_depth < crossover { + // Case where quark_poly should be None and sparse_layers populated + assert!( + result.quark_poly.is_none(), + "Expected quark_poly to be None when tree_depth < crossover" + ); + assert!( + !result.sparse_layers.is_empty(), + "Expected sparse_layers to be populated when tree_depth < crossover" + ); + } else if tree_depth == crossover { + // Case where quark_poly should be populated + assert!( + result.quark_poly.is_none(), + "Expected quark_poly to be None when tree_depth == crossover" + ); + assert!( + !result.sparse_layers.is_empty(), + "Expected sparse_layers to be populated when tree_depth == crossover" + ); + } else if crossover == 0 { + // Case where quark_poly should be populated + assert!( + result.quark_poly.is_some(), + "Expected quark_poly to be None when crossover is 0" + ); + assert!( + result.sparse_layers.is_empty(), + "Expected sparse_layers to be empty when crossover is 0" + ); + } else { + // Case where quark_poly should contain the top layer of sparse_layers + assert!( + result.quark_poly.is_some(), + "Expected quark_poly to be Some when tree_depth > crossover" + ); + assert!( + !result.sparse_layers.is_empty(), + "Expected sparse_layers to be populated when tree_depth > crossover" + ); + } } } }