From ce9fa8c5556a3beefd1fe96b5b0bfae8d50a6443 Mon Sep 17 00:00:00 2001 From: Nicole Date: Mon, 20 Apr 2026 15:02:15 -0300 Subject: [PATCH 1/6] implement fri arity-4 folding --- .../backends/field_element_vector.rs | 48 ++ .../crypto/src/merkle_tree/backends/types.rs | 5 +- crypto/stark/src/config.rs | 6 +- crypto/stark/src/fri/fri_decommit.rs | 4 +- crypto/stark/src/fri/mod.rs | 146 +++-- crypto/stark/src/proof/stark.rs | 8 + crypto/stark/src/prover.rs | 120 ++-- crypto/stark/src/verifier.rs | 603 ++++++++++++------ 8 files changed, 644 insertions(+), 296 deletions(-) diff --git a/crypto/crypto/src/merkle_tree/backends/field_element_vector.rs b/crypto/crypto/src/merkle_tree/backends/field_element_vector.rs index 1023b3a03..d3c33294f 100644 --- a/crypto/crypto/src/merkle_tree/backends/field_element_vector.rs +++ b/crypto/crypto/src/merkle_tree/backends/field_element_vector.rs @@ -56,6 +56,54 @@ where } } +/// A backend for Merkle trees that uses fixed-size quads (4 elements) of field elements. +/// Used for arity-4 FRI layers, where each leaf commits to 4 consecutive evaluations. +#[derive(Clone)] +pub struct FieldElementQuadBackend { + phantom1: PhantomData, + phantom2: PhantomData, +} + +impl Default for FieldElementQuadBackend { + fn default() -> Self { + Self { + phantom1: PhantomData, + phantom2: PhantomData, + } + } +} + +impl IsMerkleTreeBackend + for FieldElementQuadBackend +where + F: IsField, + FieldElement: AsBytes, + [u8; NUM_BYTES]: From>, +{ + type Node = [u8; NUM_BYTES]; + type Data = [FieldElement; 4]; + + fn hash_data(input: &[FieldElement; 4]) -> [u8; NUM_BYTES] { + let mut hasher = D::new(); + hasher.update(input[0].as_bytes()); + hasher.update(input[1].as_bytes()); + hasher.update(input[2].as_bytes()); + hasher.update(input[3].as_bytes()); + let mut result_hash = [0_u8; NUM_BYTES]; + result_hash.copy_from_slice(&hasher.finalize()); + result_hash + } + + fn hash_new_parent(left: &[u8; NUM_BYTES], right: &[u8; NUM_BYTES]) -> [u8; NUM_BYTES] { + let mut hasher = D::new(); + hasher.update(left); + hasher.update(right); + let mut result_hash = [0_u8; NUM_BYTES]; + result_hash.copy_from_slice(&hasher.finalize()); + result_hash + } +} + #[derive(Clone)] pub struct FieldElementVectorBackend { phantom1: PhantomData, diff --git a/crypto/crypto/src/merkle_tree/backends/types.rs b/crypto/crypto/src/merkle_tree/backends/types.rs index 0c2a30422..9e0ce1c80 100644 --- a/crypto/crypto/src/merkle_tree/backends/types.rs +++ b/crypto/crypto/src/merkle_tree/backends/types.rs @@ -2,7 +2,7 @@ use sha3::Keccak256; use super::{ field_element::FieldElementBackend, - field_element_vector::{FieldElementPairBackend, FieldElementVectorBackend}, + field_element_vector::{FieldElementPairBackend, FieldElementQuadBackend, FieldElementVectorBackend}, }; // Field element backend definitions @@ -13,3 +13,6 @@ pub type BatchKeccak256Backend = FieldElementVectorBackend; // Fixed-size pair backends (more efficient for FRI layers) pub type PairKeccak256Backend = FieldElementPairBackend; + +// Fixed-size quad backends (for arity-4 FRI layers) +pub type QuadKeccak256Backend = FieldElementQuadBackend; diff --git a/crypto/stark/src/config.rs b/crypto/stark/src/config.rs index 50650e40a..2213926ec 100644 --- a/crypto/stark/src/config.rs +++ b/crypto/stark/src/config.rs @@ -1,5 +1,5 @@ use crypto::merkle_tree::{ - backends::types::{BatchKeccak256Backend, Keccak256Backend, PairKeccak256Backend}, + backends::types::{BatchKeccak256Backend, Keccak256Backend, PairKeccak256Backend, QuadKeccak256Backend}, merkle::MerkleTree, }; @@ -22,3 +22,7 @@ pub type BatchedMerkleTree = MerkleTree>; // FRI layer uses fixed-size pairs for efficiency (avoids Vec allocation per pair) pub type FriLayerMerkleTreeBackend = PairKeccak256Backend; pub type FriLayerMerkleTree = MerkleTree>; + +// Arity-4 FRI layer: each leaf commits to 4 consecutive evaluations +pub type FriLayerQuadMerkleTreeBackend = QuadKeccak256Backend; +pub type FriLayerQuadMerkleTree = MerkleTree>; diff --git a/crypto/stark/src/fri/fri_decommit.rs b/crypto/stark/src/fri/fri_decommit.rs index f398096d5..22961da36 100644 --- a/crypto/stark/src/fri/fri_decommit.rs +++ b/crypto/stark/src/fri/fri_decommit.rs @@ -8,5 +8,7 @@ use crate::config::Commitment; #[serde(bound = "")] pub struct FriDecommitment { pub layers_auth_paths: Vec>, - pub layers_evaluations_sym: Vec>, + /// For arity-4 FRI: the 3 sibling evaluations per layer at positions + /// {index^1, index^2, index^3} within the 4-element orbit. + pub layers_evaluations_siblings: Vec<[FieldElement; 3]>, } diff --git a/crypto/stark/src/fri/mod.rs b/crypto/stark/src/fri/mod.rs index 87ab66a5b..a79205fd8 100644 --- a/crypto/stark/src/fri/mod.rs +++ b/crypto/stark/src/fri/mod.rs @@ -8,7 +8,7 @@ use math::field::traits::IsSubFieldOf; use math::field::traits::{IsFFTField, IsField}; use math::traits::AsBytes; -use crate::config::{FriLayerMerkleTree, FriLayerMerkleTreeBackend}; +use crate::config::{FriLayerQuadMerkleTree, FriLayerQuadMerkleTreeBackend}; use self::fri_commitment::FriLayer; use self::fri_decommit::FriDecommitment; @@ -16,9 +16,14 @@ use self::fri_functions::{ compute_coset_twiddles_inv, fold_evaluations_in_place, update_twiddles_in_place, }; -/// FRI commit phase from pre-computed bit-reversed evaluations. -/// skipping the initial FFT. Use this when the caller already has the evaluation -/// vector (e.g. from a fused LDE pipeline). +/// FRI commit phase using arity-4 folding (2 binary folds per committed layer). +/// +/// For `number_layers` binary fold levels, this produces `(number_layers - 1) / 2` +/// committed layers (each covering 2 binary folds) plus a final single-fold to +/// produce the last value. For a 2^19 trace: 19 levels โ†’ 9 committed layers. +/// +/// Each committed layer is a quad Merkle tree (4-element leaves), halving the +/// number of Merkle commits vs binary FRI. pub fn commit_phase_from_evaluations, E: IsField>( number_layers: usize, mut evals: Vec>, @@ -27,35 +32,46 @@ pub fn commit_phase_from_evaluations, E: IsField domain_size: usize, ) -> ( FieldElement, - Vec>>, + Vec>>, ) where FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { - // Inverse twiddle factors for evaluation-form folding let mut inv_twiddles = compute_coset_twiddles_inv(coset_offset, domain_size); - let mut fri_layer_list = Vec::with_capacity(number_layers); + let mut fri_layer_list = Vec::new(); let mut current_coset_offset = coset_offset.clone(); let mut current_domain_size = domain_size; - for _ in 1..number_layers { - // <<<< Receive challenge ๐œโ‚–โ‚‹โ‚ - let zeta = transcript.sample_field_element(); + // Number of double-fold (arity-4) committed rounds from the (number_layers - 1) middle layers. + // The final fold is handled separately below. + let num_double_rounds = number_layers.saturating_sub(1) / 2; + + for _ in 0..num_double_rounds { + // Sample both fold challenges before committing (Fiat-Shamir: both betas + // depend only on transcript state before this round's commitment). + let zeta1 = transcript.sample_field_element(); + let zeta2 = transcript.sample_field_element(); + + // First binary fold: current_size โ†’ current_size / 2 current_coset_offset = current_coset_offset.square(); current_domain_size /= 2; + fold_evaluations_in_place(&mut evals, &zeta1, &inv_twiddles); + update_twiddles_in_place(&mut inv_twiddles); - // Fold evaluations in-place (no FFT needed) - fold_evaluations_in_place(&mut evals, &zeta, &inv_twiddles); + // Second binary fold: current_size / 2 โ†’ current_size / 4 + current_coset_offset = current_coset_offset.square(); + current_domain_size /= 2; + fold_evaluations_in_place(&mut evals, &zeta2, &inv_twiddles); - // Build Merkle tree from consecutive pairs - let leaves: Vec<[FieldElement; 2]> = evals - .chunks_exact(2) - .map(|chunk| [chunk[0].clone(), chunk[1].clone()]) + // Commit the doubly-folded evaluations as quad (4-element) Merkle leaves. + let leaves: Vec<[FieldElement; 4]> = evals + .chunks_exact(4) + .map(|c| [c[0].clone(), c[1].clone(), c[2].clone(), c[3].clone()]) .collect(); - let merkle_tree = FriLayerMerkleTree::build(&leaves) - .expect("FRI commit: Merkle tree construction must succeed"); + let merkle_tree = FriLayerQuadMerkleTree::build(&leaves) + .expect("FRI commit: quad Merkle tree construction must succeed"); let root = merkle_tree.root; fri_layer_list.push(FriLayer::new( &evals, @@ -64,30 +80,53 @@ where current_domain_size, )); - // >>>> Send commitment: [pโ‚–] + // Append commitment to transcript so subsequent samples depend on it. transcript.append_bytes(&root); - // Update twiddles for next level update_twiddles_in_place(&mut inv_twiddles); } - // <<<< Receive challenge: ๐œโ‚™โ‚‹โ‚ - let zeta = transcript.sample_field_element(); + // Handle the leftover single binary round when (number_layers - 1) is odd. + // For number_layers=19: (19-1)/2 = 9 double rounds, remainder 0 โ†’ skipped. + // For number_layers=20: (20-1)/2 = 9 double rounds, remainder 1 โ†’ one extra. + if number_layers.saturating_sub(1) % 2 == 1 { + let zeta = transcript.sample_field_element(); + current_coset_offset = current_coset_offset.square(); + current_domain_size /= 2; + fold_evaluations_in_place(&mut evals, &zeta, &inv_twiddles); + + // Commit remaining as quad leaves (evals.len() must be >= 4 here). + let leaves: Vec<[FieldElement; 4]> = evals + .chunks_exact(4) + .map(|c| [c[0].clone(), c[1].clone(), c[2].clone(), c[3].clone()]) + .collect(); + let merkle_tree = FriLayerQuadMerkleTree::build(&leaves) + .expect("FRI commit: quad Merkle tree construction must succeed"); + let root = merkle_tree.root; + fri_layer_list.push(FriLayer::new( + &evals, + merkle_tree, + current_coset_offset.clone().to_extension(), + current_domain_size, + )); + transcript.append_bytes(&root); + update_twiddles_in_place(&mut inv_twiddles); + } - // Final fold + // Final fold: one more binary fold to produce the last value (not committed). + let zeta = transcript.sample_field_element(); fold_evaluations_in_place(&mut evals, &zeta, &inv_twiddles); let last_value = evals.first().unwrap_or(&FieldElement::zero()).clone(); - - // >>>> Send value: pโ‚™ transcript.append_field_element(&last_value); (last_value, fri_layer_list) } pub fn query_phase( - fri_layers: &Vec>>, + fri_layers: &[FriLayer>], iotas: &[usize], + num_double_rounds: usize, ) -> Vec> where FieldElement: AsBytes + Sync + Send, @@ -97,35 +136,54 @@ where iotas .iter() .map(|iota_s| { - let mut layers_evaluations_sym = Vec::with_capacity(num_layers); - let mut layers_auth_paths_sym = Vec::with_capacity(num_layers); - - let mut index = *iota_s; - for layer in fri_layers { - // symmetric element - let evaluation_sym = layer.evaluation[index ^ 1].clone(); - let auth_path_sym = layer.merkle_tree.get_proof_by_pos(index >> 1).unwrap(); - layers_evaluations_sym.push(evaluation_sym); - layers_auth_paths_sym.push(auth_path_sym); - - index >>= 1; + let mut layers_evaluations_siblings = Vec::with_capacity(num_layers); + let mut layers_auth_paths = Vec::with_capacity(num_layers); + + // For double bootstrap (num_double_rounds >= 1): iota is already the + // index in layer[0] (which has LDE/4 elements after 2 binary folds). + // For single bootstrap (num_double_rounds == 0): layer[0] has LDE/2 + // elements, so the index is 2*iota. + let mut index = if num_double_rounds >= 1 { + *iota_s + } else { + iota_s * 2 + }; + for (i, layer) in fri_layers.iter().enumerate() { + // The 4-element orbit of `index` is {index&~3, ..., (index&~3)+3}. + // index^1, index^2, index^3 are the 3 siblings (XOR flips last 2 bits). + let s1 = layer.evaluation[index ^ 1].clone(); + let s2 = layer.evaluation[index ^ 2].clone(); + let s3 = layer.evaluation[index ^ 3].clone(); + + // Quad leaf position: each leaf holds 4 evaluations, leaf j covers + // indices {4j, 4j+1, 4j+2, 4j+3}, so the leaf index is index >> 2. + let auth_path = layer.merkle_tree.get_proof_by_pos(index >> 2).unwrap(); + + layers_evaluations_siblings.push([s1, s2, s3]); + layers_auth_paths.push(auth_path); + + // Round (i+1) is a double fold iff (i+1) < num_double_rounds, + // meaning layer[i] โ†’ layer[i+1] involves 2 binary folds (index >>= 2). + // Otherwise it is a single fold (index >>= 1). + if (i + 1) < num_double_rounds { + index >>= 2; + } else { + index >>= 1; + } } FriDecommitment { - layers_auth_paths: layers_auth_paths_sym, - layers_evaluations_sym, + layers_auth_paths, + layers_evaluations_siblings, } }) .collect() } else { - // For 0 FRI layers (small traces), return empty decommitments for each query. - // The verifier still needs one decommitment entry per query, even if the - // FRI layer data is empty. iotas .iter() .map(|_| FriDecommitment { layers_auth_paths: vec![], - layers_evaluations_sym: vec![], + layers_evaluations_siblings: vec![], }) .collect() } diff --git a/crypto/stark/src/proof/stark.rs b/crypto/stark/src/proof/stark.rs index 1751d60fe..a3319d11a 100644 --- a/crypto/stark/src/proof/stark.rs +++ b/crypto/stark/src/proof/stark.rs @@ -11,10 +11,18 @@ use crate::{ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(bound = "")] pub struct PolynomialOpenings { + /// Openings at the 4 positions of the arity-4 orbit: index, index^1, index^2, index^3. + /// For trace trees: 4 independent Merkle proofs (one per row). + /// For the composition poly tree (pair-leaf): proof and proof_sym cover {0,1} and {2,3} + /// respectively, so proof == proof for positions 0&1, proof_sym for positions 2&3. pub proof: Proof, pub proof_sym: Proof, + pub proof_2: Proof, + pub proof_3: Proof, pub evaluations: Vec>, pub evaluations_sym: Vec>, + pub evaluations_2: Vec>, + pub evaluations_3: Vec>, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 8e59807c1..5f80a7b3e 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -1116,7 +1116,8 @@ pub trait IsStarkProver< let number_of_queries = air.options().fri_number_of_queries; let iotas = Self::sample_query_indexes(number_of_queries, domain, transcript); - let query_list = fri::query_phase(&fri_layers, &iotas); + let num_double_rounds = (domain.root_order as usize).saturating_sub(1) / 2; + let query_list = fri::query_phase(&fri_layers, &iotas, num_double_rounds); let fri_layers_merkle_roots: Vec<_> = fri_layers .iter() @@ -1147,8 +1148,12 @@ pub trait IsStarkProver< transcript: &mut impl IsStarkTranscript, ) -> Vec { let domain_size = domain.lde_roots_of_unity_coset.len() as u64; + let query_domain_size = domain_size >> 2; + if query_domain_size == 0 { + return vec![]; + } (0..number_of_queries) - .map(|_| (transcript.sample_u64(domain_size >> 1)) as usize) + .map(|_| (transcript.sample_u64(query_domain_size)) as usize) .collect::>() } @@ -1280,33 +1285,34 @@ pub trait IsStarkProver< FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { - let proof = composition_poly_merkle_tree - .get_proof_by_pos(index) + // Arity-4: iota indexes into LDE/4. Composition poly tree has LDE/2 leaves, + // each leaf j covers positions {2j, 2j+1}. We need two leaves: 2*index and 2*index+1, + // covering all 4 positions {4*index, 4*index+1, 4*index+2, 4*index+3}. + let proof_01 = composition_poly_merkle_tree + .get_proof_by_pos(index * 2) + .unwrap(); + let proof_23 = composition_poly_merkle_tree + .get_proof_by_pos(index * 2 + 1) .unwrap(); - let lde_composition_poly_parts_evaluation: Vec<_> = lde_composition_poly_evaluations - .iter() - .flat_map(|part| { - vec![ - part[reverse_index(index * 2, part.len() as u64)].clone(), - part[reverse_index(index * 2 + 1, part.len() as u64)].clone(), - ] - }) - .collect(); + let num_parts = lde_composition_poly_evaluations.len(); + let part_len = lde_composition_poly_evaluations[0].len(); + + let eval_at = |raw_idx: usize| -> Vec> { + (0..num_parts) + .map(|p| lde_composition_poly_evaluations[p][reverse_index(raw_idx, part_len as u64)].clone()) + .collect() + }; PolynomialOpenings { - proof: proof.clone(), - proof_sym: proof, - evaluations: lde_composition_poly_parts_evaluation - .clone() - .into_iter() - .step_by(2) - .collect(), - evaluations_sym: lde_composition_poly_parts_evaluation - .into_iter() - .skip(1) - .step_by(2) - .collect(), + proof: proof_01.clone(), + proof_sym: proof_01, + proof_2: proof_23.clone(), + proof_3: proof_23, + evaluations: eval_at(index * 4), + evaluations_sym: eval_at(index * 4 + 1), + evaluations_2: eval_at(index * 4 + 2), + evaluations_3: eval_at(index * 4 + 3), } } @@ -1325,14 +1331,20 @@ pub trait IsStarkProver< { let domain_size = domain.lde_roots_of_unity_coset.len(); - let index = challenge * 2; - let index_sym = challenge * 2 + 1; + // Arity-4: challenge indexes into LDE/4; open all 4 positions in the orbit. + let i0 = challenge * 4; + let i1 = challenge * 4 + 1; + let i2 = challenge * 4 + 2; + let i3 = challenge * 4 + 3; PolynomialOpenings { - proof: tree.get_proof_by_pos(index).unwrap(), - proof_sym: tree.get_proof_by_pos(index_sym).unwrap(), - evaluations: lde_trace.gather_main_row(reverse_index(index, domain_size as u64)), - evaluations_sym: lde_trace - .gather_main_row(reverse_index(index_sym, domain_size as u64)), + proof: tree.get_proof_by_pos(i0).unwrap(), + proof_sym: tree.get_proof_by_pos(i1).unwrap(), + proof_2: tree.get_proof_by_pos(i2).unwrap(), + proof_3: tree.get_proof_by_pos(i3).unwrap(), + evaluations: lde_trace.gather_main_row(reverse_index(i0, domain_size as u64)), + evaluations_sym: lde_trace.gather_main_row(reverse_index(i1, domain_size as u64)), + evaluations_2: lde_trace.gather_main_row(reverse_index(i2, domain_size as u64)), + evaluations_3: lde_trace.gather_main_row(reverse_index(i3, domain_size as u64)), } } @@ -1351,18 +1363,32 @@ pub trait IsStarkProver< { let domain_size = domain.lde_roots_of_unity_coset.len(); - let index = challenge * 2; - let index_sym = challenge * 2 + 1; + let i0 = challenge * 4; + let i1 = challenge * 4 + 1; + let i2 = challenge * 4 + 2; + let i3 = challenge * 4 + 3; PolynomialOpenings { - proof: tree.get_proof_by_pos(index).unwrap(), - proof_sym: tree.get_proof_by_pos(index_sym).unwrap(), + proof: tree.get_proof_by_pos(i0).unwrap(), + proof_sym: tree.get_proof_by_pos(i1).unwrap(), + proof_2: tree.get_proof_by_pos(i2).unwrap(), + proof_3: tree.get_proof_by_pos(i3).unwrap(), evaluations: lde_trace.gather_main_row_range( - reverse_index(index, domain_size as u64), + reverse_index(i0, domain_size as u64), col_start, col_end, ), evaluations_sym: lde_trace.gather_main_row_range( - reverse_index(index_sym, domain_size as u64), + reverse_index(i1, domain_size as u64), + col_start, + col_end, + ), + evaluations_2: lde_trace.gather_main_row_range( + reverse_index(i2, domain_size as u64), + col_start, + col_end, + ), + evaluations_3: lde_trace.gather_main_row_range( + reverse_index(i3, domain_size as u64), col_start, col_end, ), @@ -1382,13 +1408,19 @@ pub trait IsStarkProver< { let domain_size = domain.lde_roots_of_unity_coset.len(); - let index = challenge * 2; - let index_sym = challenge * 2 + 1; + let i0 = challenge * 4; + let i1 = challenge * 4 + 1; + let i2 = challenge * 4 + 2; + let i3 = challenge * 4 + 3; PolynomialOpenings { - proof: tree.get_proof_by_pos(index).unwrap(), - proof_sym: tree.get_proof_by_pos(index_sym).unwrap(), - evaluations: lde_trace.gather_aux_row(reverse_index(index, domain_size as u64)), - evaluations_sym: lde_trace.gather_aux_row(reverse_index(index_sym, domain_size as u64)), + proof: tree.get_proof_by_pos(i0).unwrap(), + proof_sym: tree.get_proof_by_pos(i1).unwrap(), + proof_2: tree.get_proof_by_pos(i2).unwrap(), + proof_3: tree.get_proof_by_pos(i3).unwrap(), + evaluations: lde_trace.gather_aux_row(reverse_index(i0, domain_size as u64)), + evaluations_sym: lde_trace.gather_aux_row(reverse_index(i1, domain_size as u64)), + evaluations_2: lde_trace.gather_aux_row(reverse_index(i2, domain_size as u64)), + evaluations_3: lde_trace.gather_aux_row(reverse_index(i3, domain_size as u64)), } } diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index a1a68930a..47b5c1c6e 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -1,5 +1,5 @@ use super::{ - config::BatchedMerkleTreeBackend, + config::{BatchedMerkleTreeBackend, FriLayerQuadMerkleTreeBackend}, domain::VerifierDomain, fri::fri_decommit::FriDecommitment, grinding, @@ -88,8 +88,12 @@ pub trait IsStarkVerifier< transcript: &mut impl IsStarkTranscript, ) -> Vec { let domain_size = domain.lde_length as u64; + let query_domain_size = domain_size >> 2; + if query_domain_size == 0 { + return vec![]; + } (0..number_of_queries) - .map(|_| (transcript.sample_u64(domain_size >> 1)) as usize) + .map(|_| (transcript.sample_u64(query_domain_size)) as usize) .collect::>() } @@ -193,20 +197,25 @@ pub trait IsStarkVerifier< // <<<< Receive challenges: ๐›พโฑผ, ๐›พโฑผ' let gammas = deep_composition_coefficients; - // FRI commit phase + // FRI commit phase โ€” arity-4: 2 zetas per double-fold layer, 1 for odd-extra layer. + // number_layers = domain.root_order (log2 of the LDE domain size). + let number_layers = domain.root_order as usize; + let num_double_rounds = number_layers.saturating_sub(1) / 2; let merkle_roots = &proof.fri_layers_merkle_roots; - let mut zetas = merkle_roots - .iter() - .map(|root| { - // >>>> Send challenge ๐œโ‚– - let element = transcript.sample_field_element(); - // <<<< Receive commitment: [pโ‚–] (the first one is [pโ‚€]) - transcript.append_bytes(root); - element - }) - .collect::>>(); + let mut zetas = Vec::with_capacity(number_layers); + for (i, root) in merkle_roots.iter().enumerate() { + // >>>> Send challenges: 2 for double-fold layers, 1 for the odd-extra layer. + let z1 = transcript.sample_field_element(); + zetas.push(z1); + if i < num_double_rounds { + let z2 = transcript.sample_field_element(); + zetas.push(z2); + } + // <<<< Receive commitment: [pโ‚–] + transcript.append_bytes(root); + } - // >>>> Send challenge ๐œโ‚™โ‚‹โ‚ + // >>>> Send final challenge ๐œโ‚™โ‚‹โ‚ (for the last single fold that produces fri_last_value) zetas.push(transcript.sample_field_element()); // <<<< Receive value: pโ‚™ @@ -393,54 +402,69 @@ pub trait IsStarkVerifier< FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { - let (deep_poly_evaluations, deep_poly_evaluations_sym) = + let (evals_0, evals_1, evals_2, evals_3) = Self::reconstruct_deep_composition_poly_evaluations_for_all_queries( challenges, domain, proof, ); - // verify FRI - let mut evaluation_point_inverse = challenges + // Arity-4: each query needs eval_inv at position 4*iota (eval_inv_a) + // and at position 4*iota+2 (eval_inv_b) for the 2-step fold bootstrap. + let mut eval_inv_a_vec: Vec> = challenges .iotas .iter() .map(|iota| Self::query_challenge_to_evaluation_point(*iota, domain)) - .collect::>>(); - FieldElement::inplace_batch_inverse(&mut evaluation_point_inverse).unwrap(); + .collect(); + let mut eval_inv_b_vec: Vec> = challenges + .iotas + .iter() + .map(|iota| Self::query_challenge_to_evaluation_point_2(*iota, domain)) + .collect(); + FieldElement::inplace_batch_inverse(&mut eval_inv_a_vec).unwrap(); + FieldElement::inplace_batch_inverse(&mut eval_inv_b_vec).unwrap(); proof .query_list .iter() .zip(&challenges.iotas) - .zip(evaluation_point_inverse) + .zip(eval_inv_a_vec) + .zip(eval_inv_b_vec) .enumerate() - .fold(true, |mut result, (i, ((proof_s, iota_s), eval))| { - result &= Self::verify_query_and_sym_openings( - proof, - &challenges.zetas, - *iota_s, - proof_s, - eval, - &deep_poly_evaluations[i], - &deep_poly_evaluations_sym[i], - ); - result - }) + .fold( + true, + |mut result, (i, (((proof_s, iota_s), eval_inv_a), eval_inv_b))| { + result &= Self::verify_query_and_sym_openings( + proof, + domain, + &challenges.zetas, + *iota_s, + proof_s, + eval_inv_a, + eval_inv_b, + &evals_0[i], + &evals_1[i], + &evals_2[i], + &evals_3[i], + ); + result + }, + ) } - /// Returns the field element element of the domain `domain` corresponding to the given FRI query index challenge `iota`. + /// Returns the coset element at bit-reversed position `4*iota` (orbit base for arity-4 FRI). fn query_challenge_to_evaluation_point( iota: usize, domain: &VerifierDomain, ) -> FieldElement { - let index = reverse_index(iota * 2, domain.lde_length as u64); + let index = reverse_index(iota * 4, domain.lde_length as u64); domain.lde_coset_element(index) } - /// Returns the symmetric field element element of the domain `domain` corresponding to the given FRI query index challenge `iota`. - fn query_challenge_to_evaluation_point_sym( + /// Returns the coset element at bit-reversed position `4*iota+2` (second fold partner in arity-4 FRI). + fn query_challenge_to_evaluation_point_2( iota: usize, domain: &VerifierDomain, ) -> FieldElement { - let index = reverse_index(iota * 2 + 1, domain.lde_length as u64); + let index = reverse_index(iota * 4 + 2, domain.lde_length as u64); domain.lde_coset_element(index) } @@ -471,23 +495,38 @@ pub trait IsStarkVerifier< FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { - let index = iota * 2; - let index_sym = iota * 2 + 1; + // Arity-4: 4 positions per query orbit. + let i0 = iota * 4; + let i1 = iota * 4 + 1; + let i2 = iota * 4 + 2; + let i3 = iota * 4 + 3; let mut result = true; // Verify main trace (multiplicities for preprocessed, full trace for normal) result &= Self::verify_opening::( &deep_poly_openings.main_trace_polys.proof, &proof.lde_trace_main_merkle_root, - index, + i0, &deep_poly_openings.main_trace_polys.evaluations, ); result &= Self::verify_opening::( &deep_poly_openings.main_trace_polys.proof_sym, &proof.lde_trace_main_merkle_root, - index_sym, + i1, &deep_poly_openings.main_trace_polys.evaluations_sym, ); + result &= Self::verify_opening::( + &deep_poly_openings.main_trace_polys.proof_2, + &proof.lde_trace_main_merkle_root, + i2, + &deep_poly_openings.main_trace_polys.evaluations_2, + ); + result &= Self::verify_opening::( + &deep_poly_openings.main_trace_polys.proof_3, + &proof.lde_trace_main_merkle_root, + i3, + &deep_poly_openings.main_trace_polys.evaluations_3, + ); // Verify precomputed trace (for preprocessed tables only) match ( @@ -502,15 +541,27 @@ pub trait IsStarkVerifier< result &= Self::verify_opening::( &precomputed_opening.proof, precomputed_root, - index, + i0, &precomputed_opening.evaluations, ); result &= Self::verify_opening::( &precomputed_opening.proof_sym, precomputed_root, - index_sym, + i1, &precomputed_opening.evaluations_sym, ); + result &= Self::verify_opening::( + &precomputed_opening.proof_2, + precomputed_root, + i2, + &precomputed_opening.evaluations_2, + ); + result &= Self::verify_opening::( + &precomputed_opening.proof_3, + precomputed_root, + i3, + &precomputed_opening.evaluations_3, + ); } _ => {} } @@ -526,15 +577,27 @@ pub trait IsStarkVerifier< result &= Self::verify_opening::( &aux_trace_polys_opening.proof, &aux_root, - index, + i0, &aux_trace_polys_opening.evaluations, ); result &= Self::verify_opening::( &aux_trace_polys_opening.proof_sym, &aux_root, - index_sym, + i1, &aux_trace_polys_opening.evaluations_sym, ); + result &= Self::verify_opening::( + &aux_trace_polys_opening.proof_2, + &aux_root, + i2, + &aux_trace_polys_opening.evaluations_2, + ); + result &= Self::verify_opening::( + &aux_trace_polys_opening.proof_3, + &aux_root, + i3, + &aux_trace_polys_opening.evaluations_3, + ); } _ => {} } @@ -553,17 +616,31 @@ pub trait IsStarkVerifier< FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { - let mut value = deep_poly_openings.composition_poly.evaluations.clone(); - value.extend_from_slice(&deep_poly_openings.composition_poly.evaluations_sym); + // Arity-4: composition poly tree has pair leaves (PairKeccak256Backend). + // The 4-element orbit {4*iota, 4*iota+1, 4*iota+2, 4*iota+3} spans leaves + // {2*iota, 2*iota+1}. Verify both leaves independently. + let mut value_01 = deep_poly_openings.composition_poly.evaluations.clone(); + value_01.extend_from_slice(&deep_poly_openings.composition_poly.evaluations_sym); + + let mut value_23 = deep_poly_openings.composition_poly.evaluations_2.clone(); + value_23.extend_from_slice(&deep_poly_openings.composition_poly.evaluations_3); deep_poly_openings .composition_poly .proof .verify::>( composition_poly_merkle_root, - *iota, - &value, + iota * 2, + &value_01, ) + & deep_poly_openings + .composition_poly + .proof_2 + .verify::>( + composition_poly_merkle_root, + iota * 2 + 1, + &value_23, + ) } /// Verifies the validity of the purported values of the trace polynomials and the composition polynomial @@ -592,199 +669,306 @@ pub trait IsStarkVerifier< ) } - /// Verifies the openings of a fold polynomial of an inner layer of FRI. - fn verify_fri_layer_openings( + /// Verifies a single FRI layer opening for arity-4 (quad Merkle tree, 4-element leaves). + /// + /// `v` is the computed fold value at position `index` within the committed layer. + /// `siblings` are the prover's claimed values at `index^1`, `index^2`, `index^3`. + /// `auth_path` proves the 4-element leaf at `index >> 2`. + fn verify_fri_layer_openings_quad( merkle_root: &Commitment, - auth_path_sym: &Proof, - evaluation: &FieldElement, - evaluation_sym: &FieldElement, - iota: usize, + auth_path: &Proof, + v: &FieldElement, + siblings: &[FieldElement; 3], + index: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { - let evaluations = if iota % 2 == 1 { - vec![evaluation_sym.clone(), evaluation.clone()] - } else { - vec![evaluation.clone(), evaluation_sym.clone()] - }; - - auth_path_sym.verify::>( + // Reconstruct the 4-element leaf from v (at position k = index & 3) and siblings. + let k = index & 3; + let mut leaf: [FieldElement; 4] = [ + FieldElement::zero(), + FieldElement::zero(), + FieldElement::zero(), + FieldElement::zero(), + ]; + leaf[k] = v.clone(); + leaf[k ^ 1] = siblings[0].clone(); + leaf[k ^ 2] = siblings[1].clone(); + leaf[k ^ 3] = siblings[2].clone(); + + auth_path.verify::>( merkle_root, - iota >> 1, - &evaluations, + index >> 2, + &leaf, ) } - /// Verify a single FRI query - /// `zetas`: the vector of all challenges sent by the verifier to the prover at the commit - /// phase to fold polynomials. - /// `iota`: the index challenge of this FRI query. This index uniquely determines two elements ๐œ and -๐œ - /// of the evaluation domain of FRI layer 0. - /// `evaluation_point_inv`: precomputed value of ๐œโปยน. - /// `deep_composition_evaluation`: precomputed value of pโ‚€(๐œ), where pโ‚€ is the deep composition polynomial. - /// `deep_composition_evaluation_sym`: precomputed value of pโ‚€(-๐œ), where pโ‚€ is the deep composition polynomial. + /// Verify a single arity-4 FRI query. + /// + /// - `domain`: the verifier domain, needed to recompute twiddles at each fold level. + /// - `zetas`: all FRI folding challenges. Layout: [z0, z1] per double-fold layer, + /// [z_k] for the odd-extra layer (if any), [z_last] for the final fold. + /// - `iota`: FRI query index in [0, LDE/4). + /// - `eval_inv_a`: inverse of the coset element at bit-reversed position `4*iota`. + /// - `eval_inv_b`: inverse of the coset element at bit-reversed position `4*iota+2`. + /// - `deep_eval_{0..3}`: DEEP composition poly evaluations at the 4 orbit positions. + #[allow(clippy::too_many_arguments)] fn verify_query_and_sym_openings( proof: &StarkProof, + domain: &VerifierDomain, zetas: &[FieldElement], iota: usize, fri_decommitment: &FriDecommitment, - evaluation_point_inv: FieldElement, - deep_composition_evaluation: &FieldElement, - deep_composition_evaluation_sym: &FieldElement, + eval_inv_a: FieldElement, + eval_inv_b: FieldElement, + deep_eval_0: &FieldElement, + deep_eval_1: &FieldElement, + deep_eval_2: &FieldElement, + deep_eval_3: &FieldElement, ) -> bool where FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { let fri_layers_merkle_roots = &proof.fri_layers_merkle_roots; - let evaluation_point_vec: Vec> = - core::iter::successors(Some(evaluation_point_inv.square()), |evaluation_point| { - Some(evaluation_point.square()) - }) - .take(fri_layers_merkle_roots.len()) - .collect(); - let p0_eval = deep_composition_evaluation; - let p0_eval_sym = deep_composition_evaluation_sym; - - // Reconstruct pโ‚(๐œยฒ) - let mut v = - (p0_eval + p0_eval_sym) + evaluation_point_inv * &zetas[0] * (p0_eval - p0_eval_sym); - let mut index = iota; + // Derive bootstrap type from zeta count. + // zetas.len() = number_layers = 2*num_double_rounds + has_odd_extra + 1 + let num_double_rounds = (zetas.len() - 1) / 2; + + // Bootstrap: reconstruct layer[0] value from DEEP poly evaluations. + // Double bootstrap (num_double_rounds >= 1): two binary folds using zetas[0,1]. + // val_a = (A+B) + inv_a * z0 * (A-B) [fold pair {pos0,pos1}] + // val_b = (C+D) + inv_b * z0 * (C-D) [fold pair {pos2,pos3}] + // v = (val_a+val_b) + inv_aยฒ * z1 * (val_a-val_b) + // Single bootstrap (num_double_rounds == 0): one binary fold using zetas[0]. + // v = (A+B) + inv_a * z0 * (A-B) + // + // `folds_done` counts binary folds completed so far; used to recompute twiddles. + let (mut v, mut zeta_idx, mut index, mut layer_eval_inv_a, mut folds_done) = + if num_double_rounds >= 1 { + let eval_inv_a_sq = eval_inv_a.square(); + let val_a = (deep_eval_0 + deep_eval_1) + + &eval_inv_a * &zetas[0] * (deep_eval_0 - deep_eval_1); + let val_b = (deep_eval_2 + deep_eval_3) + + &eval_inv_b * &zetas[0] * (deep_eval_2 - deep_eval_3); + let v = (&val_a + &val_b) + &eval_inv_a_sq * &zetas[1] * (&val_a - &val_b); + ( + v, + 2usize, + iota, + eval_inv_a_sq.square(), // eval_inv_a^4: twiddle for layer[0]โ†’[1] fold + 2usize, // 2 binary folds done in bootstrap + ) + } else { + let v = (deep_eval_0 + deep_eval_1) + + &eval_inv_a * &zetas[0] * (deep_eval_0 - deep_eval_1); + ( + v, + 1usize, + iota * 2, + eval_inv_a.square(), // eval_inv_a^2: twiddle for layer[0] fold + 1usize, // 1 binary fold done in bootstrap + ) + }; - // Handle case with 0 FRI layers (trace_length <= 2) - // In this case, the fold loop below doesn't iterate, so we need to verify - // the final value directly here. if fri_layers_merkle_roots.is_empty() { return v == proof.fri_last_value; } - // For each FRI layer, starting from the layer 1: use the proof to verify the validity of values pแตข(โˆ’๐œ^(2โฑ)) (given by the prover) and - // pแตข(๐œ^(2โฑ)) (computed on the previous iteration by the verifier). Then use them to obtain pแตขโ‚Šโ‚(๐œ^(2โฑโบยน)). - // Finally, check that the final value coincides with the given by the prover. - fri_layers_merkle_roots - .iter() - .enumerate() - .zip(&fri_decommitment.layers_auth_paths) - .zip(&fri_decommitment.layers_evaluations_sym) - .zip(evaluation_point_vec) - .fold( - true, - |result, - ( - (((i, merkle_root), auth_path_sym), evaluation_sym), - evaluation_point_inv, - )| { - // Verify opening Open(pแตข(Dโ‚–), โˆ’๐œ^(2โฑ)) and Open(pแตข(Dโ‚–), ๐œ^(2โฑ)). - // `v` is pแตข(๐œ^(2โฑ)). - // `evaluation_sym` is pแตข(โˆ’๐œ^(2โฑ)). - let openings_ok = Self::verify_fri_layer_openings( - merkle_root, - auth_path_sym, - &v, - evaluation_sym, - index, - ); + let num_layers = fri_layers_merkle_roots.len(); + let mut result = true; - // Update `v` with next value pแตขโ‚Šโ‚(๐œ^(2โฑโบยน)). - v = (&v + evaluation_sym) + evaluation_point_inv * &zetas[i + 1] * (&v - evaluation_sym); + for i in 0..num_layers { + let siblings = &fri_decommitment.layers_evaluations_siblings[i]; + let auth_path = &fri_decommitment.layers_auth_paths[i]; + let merkle_root = &fri_layers_merkle_roots[i]; + + result &= Self::verify_fri_layer_openings_quad(merkle_root, auth_path, &v, siblings, index); + + let is_last = i == num_layers - 1; + + // Round (i+1) is a double fold iff it is one of the prover's committed + // double-fold rounds, i.e. (i+1) < num_double_rounds. + let fold_is_double = (i + 1) < num_double_rounds; + + if fold_is_double { + let z_a = &zetas[zeta_idx]; + let z_b = &zetas[zeta_idx + 1]; + zeta_idx += 2; + + let sib_a = &siblings[0]; // index^1 + let sib_b = &siblings[1]; // index^2 + let sib_c = &siblings[2]; // index^3 + + let inner_val_a = (&v + sib_a) + &layer_eval_inv_a * z_a * (&v - sib_a); + + // Compute the "b" pair twiddle fresh at this fold level. + // The "b" pair is {index^2, index^3}; its twiddle position is j_b = (index>>1)^1. + // twiddle_d[j] = lde_coset_element(reverse_index(2^{d+1}ยทj, N))^{-2^d} + // Sign: index^2 is lo (even) when index is even, hi (odd) when index is odd. + let j_b = (index >> 1) ^ 1; + let lde_idx_b = + reverse_index((1usize << (folds_done + 1)) * j_b, domain.lde_length as u64); + let base_b = domain.lde_coset_element(lde_idx_b); + let twiddle_b_unsigned = base_b.pow(1u64 << folds_done).inv().unwrap(); + let layer_eval_inv_b = if index & 1 == 0 { + twiddle_b_unsigned + } else { + -twiddle_b_unsigned + }; - // Update index for next iteration. The index of the squares in the next layer - // is obtained by halving the current index. This is due to the bit-reverse - // ordering of the elements in the Merkle tree. - index >>= 1; + let inner_val_b = (sib_b + sib_c) + layer_eval_inv_b * z_a * (sib_b - sib_c); - if i < fri_decommitment.layers_evaluations_sym.len() - 1 { - result & openings_ok - } else { - // Check that final value is the given by the prover - result & (v == proof.fri_last_value) & openings_ok - } - }, - ) + let layer_inv_a_sq = layer_eval_inv_a.square(); + v = (&inner_val_a + &inner_val_b) + + &layer_inv_a_sq * z_b * (&inner_val_a - &inner_val_b); + + layer_eval_inv_a = layer_inv_a_sq.square(); + index >>= 2; + folds_done += 2; + } else { + let z = &zetas[zeta_idx]; + zeta_idx += 1; + + let sib_a = &siblings[0]; + v = (&v + sib_a) + &layer_eval_inv_a * z * (&v - sib_a); + + layer_eval_inv_a = layer_eval_inv_a.square(); + index >>= 1; + folds_done += 1; + } + + if is_last { + result &= v == proof.fri_last_value; + } + } + + result } + /// Returns 4-tuple of DEEP composition polynomial evaluations for all queries. + /// Each query's 4-element orbit gives evaluations at positions + /// {4*iota, 4*iota+1, 4*iota+2, 4*iota+3} in the LDE domain. fn reconstruct_deep_composition_poly_evaluations_for_all_queries( challenges: &Challenges, domain: &VerifierDomain, proof: &StarkProof, - ) -> DeepPolynomialEvaluations { + ) -> ( + Vec>, + Vec>, + Vec>, + Vec>, + ) { let num_queries = challenges.iotas.len(); - let mut deep_poly_evaluations = Vec::with_capacity(num_queries); - let mut deep_poly_evaluations_sym = Vec::with_capacity(num_queries); + let mut evals_0 = Vec::with_capacity(num_queries); + let mut evals_1 = Vec::with_capacity(num_queries); + let mut evals_2 = Vec::with_capacity(num_queries); + let mut evals_3 = Vec::with_capacity(num_queries); + for (i, iota) in challenges.iotas.iter().enumerate() { let primitive_root = &Field::get_primitive_root_of_unity(domain.root_order as u64).unwrap(); + let opening = &proof.deep_poly_openings[i]; + + // Helper to gather trace evaluations for one orbit position. + // For preprocessed tables: precomputed columns come FIRST, then multiplicities. + let gather_trace_evals = + |main_evals: &[FieldElement], + precomp_evals: Option<&[FieldElement]>, + aux_evals: Option<&[FieldElement]>| + -> Vec> { + let mut evals: Vec> = Vec::new(); + if let Some(pe) = precomp_evals { + evals.extend(pe.iter().cloned().map(|x| x.to_extension())); + } + evals.extend(main_evals.iter().cloned().map(|x| x.to_extension())); + if let Some(ae) = aux_evals { + evals.extend_from_slice(ae); + } + evals + }; - // For preprocessed tables: precomputed columns come FIRST, then multiplicities - let mut evaluations: Vec> = Vec::new(); - if let Some(precomputed_polys) = &proof.deep_poly_openings[i].precomputed_trace_polys { - evaluations.extend( - precomputed_polys - .evaluations - .iter() - .cloned() - .map(|x| x.to_extension()), - ); - } - evaluations.extend( - proof.deep_poly_openings[i] - .main_trace_polys - .evaluations - .iter() - .cloned() - .map(|x| x.to_extension()), - ); - if let Some(aux_trace_polys) = &proof.deep_poly_openings[i].aux_trace_polys { - evaluations.extend_from_slice(&aux_trace_polys.evaluations); - } + let precomp_0 = opening + .precomputed_trace_polys + .as_ref() + .map(|p| p.evaluations.as_slice()); + let precomp_1 = opening + .precomputed_trace_polys + .as_ref() + .map(|p| p.evaluations_sym.as_slice()); + let precomp_2 = opening + .precomputed_trace_polys + .as_ref() + .map(|p| p.evaluations_2.as_slice()); + let precomp_3 = opening + .precomputed_trace_polys + .as_ref() + .map(|p| p.evaluations_3.as_slice()); + + let aux_0 = opening.aux_trace_polys.as_ref().map(|a| a.evaluations.as_slice()); + let aux_1 = opening + .aux_trace_polys + .as_ref() + .map(|a| a.evaluations_sym.as_slice()); + let aux_2 = opening.aux_trace_polys.as_ref().map(|a| a.evaluations_2.as_slice()); + let aux_3 = opening.aux_trace_polys.as_ref().map(|a| a.evaluations_3.as_slice()); + + let te0 = gather_trace_evals(&opening.main_trace_polys.evaluations, precomp_0, aux_0); + let te1 = + gather_trace_evals(&opening.main_trace_polys.evaluations_sym, precomp_1, aux_1); + let te2 = + gather_trace_evals(&opening.main_trace_polys.evaluations_2, precomp_2, aux_2); + let te3 = + gather_trace_evals(&opening.main_trace_polys.evaluations_3, precomp_3, aux_3); + + let ep0 = Self::query_challenge_to_evaluation_point(*iota, domain); + let ep1 = { + let idx = reverse_index(iota * 4 + 1, domain.lde_length as u64); + domain.lde_coset_element(idx) + }; + let ep2 = Self::query_challenge_to_evaluation_point_2(*iota, domain); + let ep3 = { + let idx = reverse_index(iota * 4 + 3, domain.lde_length as u64); + domain.lde_coset_element(idx) + }; - let evaluation_point = Self::query_challenge_to_evaluation_point(*iota, domain); - deep_poly_evaluations.push(Self::reconstruct_deep_composition_poly_evaluation( + evals_0.push(Self::reconstruct_deep_composition_poly_evaluation( proof, - &evaluation_point, + &ep0, primitive_root, challenges, - &evaluations, - &proof.deep_poly_openings[i].composition_poly.evaluations, + &te0, + &opening.composition_poly.evaluations, )); - - // For preprocessed tables: precomputed columns come FIRST, then multiplicities - let mut evaluations_sym: Vec> = Vec::new(); - if let Some(precomputed_polys) = &proof.deep_poly_openings[i].precomputed_trace_polys { - evaluations_sym.extend( - precomputed_polys - .evaluations_sym - .iter() - .cloned() - .map(|x| x.to_extension()), - ); - } - evaluations_sym.extend( - proof.deep_poly_openings[i] - .main_trace_polys - .evaluations_sym - .iter() - .cloned() - .map(|x| x.to_extension()), - ); - if let Some(aux_trace_polys) = &proof.deep_poly_openings[i].aux_trace_polys { - evaluations_sym.extend_from_slice(&aux_trace_polys.evaluations_sym); - } - - let evaluation_point = Self::query_challenge_to_evaluation_point_sym(*iota, domain); - deep_poly_evaluations_sym.push(Self::reconstruct_deep_composition_poly_evaluation( + evals_1.push(Self::reconstruct_deep_composition_poly_evaluation( + proof, + &ep1, + primitive_root, + challenges, + &te1, + &opening.composition_poly.evaluations_sym, + )); + evals_2.push(Self::reconstruct_deep_composition_poly_evaluation( proof, - &evaluation_point, + &ep2, primitive_root, challenges, - &evaluations_sym, - &proof.deep_poly_openings[i].composition_poly.evaluations_sym, + &te2, + &opening.composition_poly.evaluations_2, + )); + evals_3.push(Self::reconstruct_deep_composition_poly_evaluation( + proof, + &ep3, + primitive_root, + challenges, + &te3, + &opening.composition_poly.evaluations_3, )); } - (deep_poly_evaluations, deep_poly_evaluations_sym) + (evals_0, evals_1, evals_2, evals_3) } fn reconstruct_deep_composition_poly_evaluation( @@ -1142,20 +1326,22 @@ pub trait IsStarkVerifier< // <<<< Receive challenges: ๐›พโฑผ, ๐›พโฑผ' let gammas = deep_composition_coefficients; - // FRI commit phase + // FRI commit phase โ€” arity-4: 2 zetas per double-fold layer, 1 for odd-extra layer. + let number_layers = domain.root_order as usize; + let num_double_rounds = number_layers.saturating_sub(1) / 2; let merkle_roots = &proof.fri_layers_merkle_roots; - let mut zetas = merkle_roots - .iter() - .map(|root| { - // >>>> Send challenge ๐œโ‚– - let element = transcript.sample_field_element(); - // <<<< Receive commitment: [pโ‚–] (the first one is [pโ‚€]) - transcript.append_bytes(root); - element - }) - .collect::>>(); + let mut zetas = Vec::with_capacity(number_layers); + for (i, root) in merkle_roots.iter().enumerate() { + let z1 = transcript.sample_field_element(); + zetas.push(z1); + if i < num_double_rounds { + let z2 = transcript.sample_field_element(); + zetas.push(z2); + } + transcript.append_bytes(root); + } - // >>>> Send challenge ๐œโ‚™โ‚‹โ‚ + // >>>> Send final challenge ๐œโ‚™โ‚‹โ‚ zetas.push(transcript.sample_field_element()); // <<<< Receive value: pโ‚™ @@ -1202,8 +1388,15 @@ pub trait IsStarkVerifier< { let domain = new_verifier_domain(air, proof.trace_length); - // Verify there are enough queries - if proof.query_list.len() < air.options().fri_number_of_queries { + // Verify there are enough queries. When LDE <= 4 the query domain is empty + // and 0 queries is correct; otherwise require the full configured count. + let query_domain_size = (domain.lde_length as u64) >> 2; + let expected_queries = if query_domain_size == 0 { + 0 + } else { + air.options().fri_number_of_queries + }; + if proof.query_list.len() < expected_queries { return false; } From ce6a2c36f2e1f68c3b2e928c2a7dc4c7878c556f Mon Sep 17 00:00:00 2001 From: Nicole Date: Wed, 22 Apr 2026 12:01:49 -0300 Subject: [PATCH 2/6] Fix clippy --- .../crypto/src/merkle_tree/backends/types.rs | 4 +- crypto/stark/src/config.rs | 4 +- crypto/stark/src/prover.rs | 5 +- crypto/stark/src/verifier.rs | 55 +++++++++++-------- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/crypto/crypto/src/merkle_tree/backends/types.rs b/crypto/crypto/src/merkle_tree/backends/types.rs index 9e0ce1c80..c0cef347f 100644 --- a/crypto/crypto/src/merkle_tree/backends/types.rs +++ b/crypto/crypto/src/merkle_tree/backends/types.rs @@ -2,7 +2,9 @@ use sha3::Keccak256; use super::{ field_element::FieldElementBackend, - field_element_vector::{FieldElementPairBackend, FieldElementQuadBackend, FieldElementVectorBackend}, + field_element_vector::{ + FieldElementPairBackend, FieldElementQuadBackend, FieldElementVectorBackend, + }, }; // Field element backend definitions diff --git a/crypto/stark/src/config.rs b/crypto/stark/src/config.rs index 2213926ec..1815defd2 100644 --- a/crypto/stark/src/config.rs +++ b/crypto/stark/src/config.rs @@ -1,5 +1,7 @@ use crypto::merkle_tree::{ - backends::types::{BatchKeccak256Backend, Keccak256Backend, PairKeccak256Backend, QuadKeccak256Backend}, + backends::types::{ + BatchKeccak256Backend, Keccak256Backend, PairKeccak256Backend, QuadKeccak256Backend, + }, merkle::MerkleTree, }; diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 5f80a7b3e..541ab4863 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -1300,7 +1300,10 @@ pub trait IsStarkProver< let eval_at = |raw_idx: usize| -> Vec> { (0..num_parts) - .map(|p| lde_composition_poly_evaluations[p][reverse_index(raw_idx, part_len as u64)].clone()) + .map(|p| { + lde_composition_poly_evaluations[p][reverse_index(raw_idx, part_len as u64)] + .clone() + }) .collect() }; diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 47b5c1c6e..8c2136e77 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -781,12 +781,14 @@ pub trait IsStarkVerifier< let num_layers = fri_layers_merkle_roots.len(); let mut result = true; + #[allow(clippy::needless_range_loop)] for i in 0..num_layers { let siblings = &fri_decommitment.layers_evaluations_siblings[i]; let auth_path = &fri_decommitment.layers_auth_paths[i]; let merkle_root = &fri_layers_merkle_roots[i]; - result &= Self::verify_fri_layer_openings_quad(merkle_root, auth_path, &v, siblings, index); + result &= + Self::verify_fri_layer_openings_quad(merkle_root, auth_path, &v, siblings, index); let is_last = i == num_layers - 1; @@ -852,6 +854,7 @@ pub trait IsStarkVerifier< /// Returns 4-tuple of DEEP composition polynomial evaluations for all queries. /// Each query's 4-element orbit gives evaluations at positions /// {4*iota, 4*iota+1, 4*iota+2, 4*iota+3} in the LDE domain. + #[allow(clippy::type_complexity)] fn reconstruct_deep_composition_poly_evaluations_for_all_queries( challenges: &Challenges, domain: &VerifierDomain, @@ -875,21 +878,20 @@ pub trait IsStarkVerifier< // Helper to gather trace evaluations for one orbit position. // For preprocessed tables: precomputed columns come FIRST, then multiplicities. - let gather_trace_evals = - |main_evals: &[FieldElement], - precomp_evals: Option<&[FieldElement]>, - aux_evals: Option<&[FieldElement]>| - -> Vec> { - let mut evals: Vec> = Vec::new(); - if let Some(pe) = precomp_evals { - evals.extend(pe.iter().cloned().map(|x| x.to_extension())); - } - evals.extend(main_evals.iter().cloned().map(|x| x.to_extension())); - if let Some(ae) = aux_evals { - evals.extend_from_slice(ae); - } - evals - }; + let gather_trace_evals = |main_evals: &[FieldElement], + precomp_evals: Option<&[FieldElement]>, + aux_evals: Option<&[FieldElement]>| + -> Vec> { + let mut evals: Vec> = Vec::new(); + if let Some(pe) = precomp_evals { + evals.extend(pe.iter().cloned().map(|x| x.to_extension())); + } + evals.extend(main_evals.iter().cloned().map(|x| x.to_extension())); + if let Some(ae) = aux_evals { + evals.extend_from_slice(ae); + } + evals + }; let precomp_0 = opening .precomputed_trace_polys @@ -908,21 +910,28 @@ pub trait IsStarkVerifier< .as_ref() .map(|p| p.evaluations_3.as_slice()); - let aux_0 = opening.aux_trace_polys.as_ref().map(|a| a.evaluations.as_slice()); + let aux_0 = opening + .aux_trace_polys + .as_ref() + .map(|a| a.evaluations.as_slice()); let aux_1 = opening .aux_trace_polys .as_ref() .map(|a| a.evaluations_sym.as_slice()); - let aux_2 = opening.aux_trace_polys.as_ref().map(|a| a.evaluations_2.as_slice()); - let aux_3 = opening.aux_trace_polys.as_ref().map(|a| a.evaluations_3.as_slice()); + let aux_2 = opening + .aux_trace_polys + .as_ref() + .map(|a| a.evaluations_2.as_slice()); + let aux_3 = opening + .aux_trace_polys + .as_ref() + .map(|a| a.evaluations_3.as_slice()); let te0 = gather_trace_evals(&opening.main_trace_polys.evaluations, precomp_0, aux_0); let te1 = gather_trace_evals(&opening.main_trace_polys.evaluations_sym, precomp_1, aux_1); - let te2 = - gather_trace_evals(&opening.main_trace_polys.evaluations_2, precomp_2, aux_2); - let te3 = - gather_trace_evals(&opening.main_trace_polys.evaluations_3, precomp_3, aux_3); + let te2 = gather_trace_evals(&opening.main_trace_polys.evaluations_2, precomp_2, aux_2); + let te3 = gather_trace_evals(&opening.main_trace_polys.evaluations_3, precomp_3, aux_3); let ep0 = Self::query_challenge_to_evaluation_point(*iota, domain); let ep1 = { From 0c8febac030e11ecd5d981a98c871e54f7700c70 Mon Sep 17 00:00:00 2001 From: Nicole Date: Wed, 22 Apr 2026 14:43:41 -0300 Subject: [PATCH 3/6] deduplicate zeta reconstruction, skip unused eval_inv_b inverse and add test --- crypto/stark/src/fri/mod.rs | 13 +++-- crypto/stark/src/proof/stark.rs | 4 +- crypto/stark/src/tests/air_tests.rs | 30 +++++++++++ crypto/stark/src/verifier.rs | 79 +++++++++++++++-------------- 4 files changed, 83 insertions(+), 43 deletions(-) diff --git a/crypto/stark/src/fri/mod.rs b/crypto/stark/src/fri/mod.rs index a79205fd8..e03ab3bc0 100644 --- a/crypto/stark/src/fri/mod.rs +++ b/crypto/stark/src/fri/mod.rs @@ -49,8 +49,12 @@ where let num_double_rounds = number_layers.saturating_sub(1) / 2; for _ in 0..num_double_rounds { - // Sample both fold challenges before committing (Fiat-Shamir: both betas - // depend only on transcript state before this round's commitment). + // Sample both fold challenges before committing. + // This is sound for arity-4 FRI: the prover commits *one* combined layer + // that covers both binary folds, so it fixes its evaluations before either + // challenge is revealed. It cannot choose zeta2 adaptively after seeing + // zeta1 because both are sampled from the same transcript state, before + // the commitment is appended. let zeta1 = transcript.sample_field_element(); let zeta2 = transcript.sample_field_element(); @@ -117,7 +121,10 @@ where let zeta = transcript.sample_field_element(); fold_evaluations_in_place(&mut evals, &zeta, &inv_twiddles); - let last_value = evals.first().unwrap_or(&FieldElement::zero()).clone(); + let last_value = evals + .first() + .expect("FRI evals empty after folding") + .clone(); transcript.append_field_element(&last_value); (last_value, fri_layer_list) diff --git a/crypto/stark/src/proof/stark.rs b/crypto/stark/src/proof/stark.rs index a3319d11a..c9b132081 100644 --- a/crypto/stark/src/proof/stark.rs +++ b/crypto/stark/src/proof/stark.rs @@ -13,8 +13,8 @@ use crate::{ pub struct PolynomialOpenings { /// Openings at the 4 positions of the arity-4 orbit: index, index^1, index^2, index^3. /// For trace trees: 4 independent Merkle proofs (one per row). - /// For the composition poly tree (pair-leaf): proof and proof_sym cover {0,1} and {2,3} - /// respectively, so proof == proof for positions 0&1, proof_sym for positions 2&3. + /// For the composition poly tree (pair-leaf): proof covers {0,1} and proof_2 covers {2,3} + /// (proof_sym duplicates proof; proof_3 duplicates proof_2 โ€” both are unused by the verifier). pub proof: Proof, pub proof_sym: Proof, pub proof_2: Proof, diff --git a/crypto/stark/src/tests/air_tests.rs b/crypto/stark/src/tests/air_tests.rs index 11d356ccf..59bb10fab 100644 --- a/crypto/stark/src/tests/air_tests.rs +++ b/crypto/stark/src/tests/air_tests.rs @@ -597,6 +597,36 @@ fn test_multi_column_fibonacci_2_cols() { )); } +#[test_log::test] +fn test_prove_fib_64_rows() { + // 64-row trace โ†’ LDE 128 โ†’ number_layers=7 โ†’ num_double_rounds=3. + // Exercises the fold_is_double branch in the verifier query loop, + // which requires num_double_rounds >= 2 and is not hit by smaller traces. + let mut trace = simple_fibonacci::fibonacci_trace([Felt::from(1), Felt::from(1)], 64); + + let proof_options = ProofOptions::default_test_options(); + + let pub_inputs = FibonacciPublicInputs { + a0: Felt::one(), + a1: Felt::one(), + }; + + let air = FibonacciAIR::::new(&proof_options); + + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .unwrap(); + assert!(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )); +} + #[test] fn test_multi_column_fibonacci_4_cols() { let proof_options = ProofOptions::default_test_options(); diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 8c2136e77..0f69438ca 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -82,6 +82,30 @@ pub trait IsStarkVerifier< PI, > { + fn reconstruct_fri_zetas( + number_layers: usize, + merkle_roots: &[Commitment], + transcript: &mut impl IsStarkTranscript, + ) -> Vec> + where + FieldElement: AsBytes, + FieldElement: AsBytes, + { + let num_double_rounds = number_layers.saturating_sub(1) / 2; + let mut zetas = Vec::with_capacity(number_layers); + for (i, root) in merkle_roots.iter().enumerate() { + let z1 = transcript.sample_field_element(); + zetas.push(z1); + if i < num_double_rounds { + let z2 = transcript.sample_field_element(); + zetas.push(z2); + } + transcript.append_bytes(root); + } + zetas.push(transcript.sample_field_element()); + zetas + } + fn sample_query_indexes( number_of_queries: usize, domain: &VerifierDomain, @@ -200,23 +224,8 @@ pub trait IsStarkVerifier< // FRI commit phase โ€” arity-4: 2 zetas per double-fold layer, 1 for odd-extra layer. // number_layers = domain.root_order (log2 of the LDE domain size). let number_layers = domain.root_order as usize; - let num_double_rounds = number_layers.saturating_sub(1) / 2; - let merkle_roots = &proof.fri_layers_merkle_roots; - let mut zetas = Vec::with_capacity(number_layers); - for (i, root) in merkle_roots.iter().enumerate() { - // >>>> Send challenges: 2 for double-fold layers, 1 for the odd-extra layer. - let z1 = transcript.sample_field_element(); - zetas.push(z1); - if i < num_double_rounds { - let z2 = transcript.sample_field_element(); - zetas.push(z2); - } - // <<<< Receive commitment: [pโ‚–] - transcript.append_bytes(root); - } - - // >>>> Send final challenge ๐œโ‚™โ‚‹โ‚ (for the last single fold that produces fri_last_value) - zetas.push(transcript.sample_field_element()); + let zetas = + Self::reconstruct_fri_zetas(number_layers, &proof.fri_layers_merkle_roots, transcript); // <<<< Receive value: pโ‚™ transcript.append_field_element(&proof.fri_last_value); @@ -409,18 +418,25 @@ pub trait IsStarkVerifier< // Arity-4: each query needs eval_inv at position 4*iota (eval_inv_a) // and at position 4*iota+2 (eval_inv_b) for the 2-step fold bootstrap. + // eval_inv_b is only used when num_double_rounds >= 1. + let num_double_rounds = (challenges.zetas.len() - 1) / 2; let mut eval_inv_a_vec: Vec> = challenges .iotas .iter() .map(|iota| Self::query_challenge_to_evaluation_point(*iota, domain)) .collect(); - let mut eval_inv_b_vec: Vec> = challenges - .iotas - .iter() - .map(|iota| Self::query_challenge_to_evaluation_point_2(*iota, domain)) - .collect(); FieldElement::inplace_batch_inverse(&mut eval_inv_a_vec).unwrap(); - FieldElement::inplace_batch_inverse(&mut eval_inv_b_vec).unwrap(); + let eval_inv_b_vec: Vec> = if num_double_rounds > 0 { + let mut v: Vec> = challenges + .iotas + .iter() + .map(|iota| Self::query_challenge_to_evaluation_point_2(*iota, domain)) + .collect(); + FieldElement::inplace_batch_inverse(&mut v).unwrap(); + v + } else { + vec![FieldElement::zero(); challenges.iotas.len()] + }; proof .query_list @@ -1337,21 +1353,8 @@ pub trait IsStarkVerifier< // FRI commit phase โ€” arity-4: 2 zetas per double-fold layer, 1 for odd-extra layer. let number_layers = domain.root_order as usize; - let num_double_rounds = number_layers.saturating_sub(1) / 2; - let merkle_roots = &proof.fri_layers_merkle_roots; - let mut zetas = Vec::with_capacity(number_layers); - for (i, root) in merkle_roots.iter().enumerate() { - let z1 = transcript.sample_field_element(); - zetas.push(z1); - if i < num_double_rounds { - let z2 = transcript.sample_field_element(); - zetas.push(z2); - } - transcript.append_bytes(root); - } - - // >>>> Send final challenge ๐œโ‚™โ‚‹โ‚ - zetas.push(transcript.sample_field_element()); + let zetas = + Self::reconstruct_fri_zetas(number_layers, &proof.fri_layers_merkle_roots, transcript); // <<<< Receive value: pโ‚™ transcript.append_field_element(&proof.fri_last_value); From 6ec773b504f6253e2a0f5fd7a7bd253a370c3595 Mon Sep 17 00:00:00 2001 From: Nicole Date: Wed, 22 Apr 2026 15:27:05 -0300 Subject: [PATCH 4/6] Reduce proof size --- crypto/stark/src/proof/stark.rs | 20 +++++++++++++++++--- crypto/stark/src/prover.rs | 12 +++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/crypto/stark/src/proof/stark.rs b/crypto/stark/src/proof/stark.rs index c9b132081..1f24af317 100644 --- a/crypto/stark/src/proof/stark.rs +++ b/crypto/stark/src/proof/stark.rs @@ -13,8 +13,6 @@ use crate::{ pub struct PolynomialOpenings { /// Openings at the 4 positions of the arity-4 orbit: index, index^1, index^2, index^3. /// For trace trees: 4 independent Merkle proofs (one per row). - /// For the composition poly tree (pair-leaf): proof covers {0,1} and proof_2 covers {2,3} - /// (proof_sym duplicates proof; proof_3 duplicates proof_2 โ€” both are unused by the verifier). pub proof: Proof, pub proof_sym: Proof, pub proof_2: Proof, @@ -25,10 +23,26 @@ pub struct PolynomialOpenings { pub evaluations_3: Vec>, } +/// Openings for the composition polynomial tree (pair-leaf: leaf j covers positions {2j, 2j+1}). +/// Arity-4 opens 4 positions {4i, 4i+1, 4i+2, 4i+3} spanning 2 leaves, so only 2 Merkle proofs +/// are needed โ€” unlike the trace-tree openings where 4 distinct paths are required. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(bound = "")] +pub struct CompositionPolyOpenings { + /// Merkle proof for leaf containing positions {4i, 4i+1}. + pub proof: Proof, + /// Merkle proof for leaf containing positions {4i+2, 4i+3}. + pub proof_2: Proof, + pub evaluations: Vec>, + pub evaluations_sym: Vec>, + pub evaluations_2: Vec>, + pub evaluations_3: Vec>, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(bound = "")] pub struct DeepPolynomialOpening, E: IsField> { - pub composition_poly: PolynomialOpenings, + pub composition_poly: CompositionPolyOpenings, pub main_trace_polys: PolynomialOpenings, /// For preprocessed tables: openings for precomputed columns. /// These are verified against the hardcoded precomputed commitment. diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 541ab4863..b361cad6a 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -27,7 +27,7 @@ use crate::debug::validate_trace; use crate::domain::new_domain; use crate::fri; use crate::lookup::LOGUP_NUM_CHALLENGES; -use crate::proof::stark::{DeepPolynomialOpenings, PolynomialOpenings}; +use crate::proof::stark::{CompositionPolyOpenings, DeepPolynomialOpenings, PolynomialOpenings}; use crate::table::Table; use crate::trace::LDETraceTable; @@ -1280,7 +1280,7 @@ pub trait IsStarkProver< composition_poly_merkle_tree: &BatchedMerkleTree, lde_composition_poly_evaluations: &[Vec>], index: usize, - ) -> PolynomialOpenings + ) -> CompositionPolyOpenings where FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, @@ -1307,11 +1307,9 @@ pub trait IsStarkProver< .collect() }; - PolynomialOpenings { - proof: proof_01.clone(), - proof_sym: proof_01, - proof_2: proof_23.clone(), - proof_3: proof_23, + CompositionPolyOpenings { + proof: proof_01, + proof_2: proof_23, evaluations: eval_at(index * 4), evaluations_sym: eval_at(index * 4 + 1), evaluations_2: eval_at(index * 4 + 2), From 212649dc6b0aeb88bde9c68ac75f76407cda790b Mon Sep 17 00:00:00 2001 From: Nicole Date: Wed, 22 Apr 2026 17:27:09 -0300 Subject: [PATCH 5/6] Replace 4 independent Merkle proofs with a single BatchProof --- crypto/crypto/src/merkle_tree/proof.rs | 1 + crypto/stark/src/proof/stark.rs | 10 +- crypto/stark/src/prover.rs | 15 +-- crypto/stark/src/verifier.rs | 131 ++++++++++--------------- 4 files changed, 61 insertions(+), 96 deletions(-) diff --git a/crypto/crypto/src/merkle_tree/proof.rs b/crypto/crypto/src/merkle_tree/proof.rs index 20d5452a2..04ce08b15 100644 --- a/crypto/crypto/src/merkle_tree/proof.rs +++ b/crypto/crypto/src/merkle_tree/proof.rs @@ -82,6 +82,7 @@ where /// This ordering is critical for verification, which consumes nodes in the same order /// as they were generated by `get_batch_proof`. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BatchProof { pub path: Vec, } diff --git a/crypto/stark/src/proof/stark.rs b/crypto/stark/src/proof/stark.rs index 1f24af317..855f51a91 100644 --- a/crypto/stark/src/proof/stark.rs +++ b/crypto/stark/src/proof/stark.rs @@ -1,4 +1,4 @@ -use crypto::merkle_tree::proof::Proof; +use crypto::merkle_tree::proof::{BatchProof, Proof}; use math::field::{ element::FieldElement, traits::{IsField, IsSubFieldOf}, @@ -12,11 +12,9 @@ use crate::{ #[serde(bound = "")] pub struct PolynomialOpenings { /// Openings at the 4 positions of the arity-4 orbit: index, index^1, index^2, index^3. - /// For trace trees: 4 independent Merkle proofs (one per row). - pub proof: Proof, - pub proof_sym: Proof, - pub proof_2: Proof, - pub proof_3: Proof, + /// A single batch Merkle proof authenticates all 4 leaves, sharing the common + /// auth-path prefix โ€” smaller than 4 independent proofs. + pub batch_proof: BatchProof, pub evaluations: Vec>, pub evaluations_sym: Vec>, pub evaluations_2: Vec>, diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 1c0b9d368..3ef7ae92a 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -1338,10 +1338,7 @@ pub trait IsStarkProver< let i2 = challenge * 4 + 2; let i3 = challenge * 4 + 3; PolynomialOpenings { - proof: tree.get_proof_by_pos(i0).unwrap(), - proof_sym: tree.get_proof_by_pos(i1).unwrap(), - proof_2: tree.get_proof_by_pos(i2).unwrap(), - proof_3: tree.get_proof_by_pos(i3).unwrap(), + batch_proof: tree.get_batch_proof(&[i0, i1, i2, i3]).unwrap(), evaluations: lde_trace.gather_main_row(reverse_index(i0, domain_size as u64)), evaluations_sym: lde_trace.gather_main_row(reverse_index(i1, domain_size as u64)), evaluations_2: lde_trace.gather_main_row(reverse_index(i2, domain_size as u64)), @@ -1369,10 +1366,7 @@ pub trait IsStarkProver< let i2 = challenge * 4 + 2; let i3 = challenge * 4 + 3; PolynomialOpenings { - proof: tree.get_proof_by_pos(i0).unwrap(), - proof_sym: tree.get_proof_by_pos(i1).unwrap(), - proof_2: tree.get_proof_by_pos(i2).unwrap(), - proof_3: tree.get_proof_by_pos(i3).unwrap(), + batch_proof: tree.get_batch_proof(&[i0, i1, i2, i3]).unwrap(), evaluations: lde_trace.gather_main_row_range( reverse_index(i0, domain_size as u64), col_start, @@ -1414,10 +1408,7 @@ pub trait IsStarkProver< let i2 = challenge * 4 + 2; let i3 = challenge * 4 + 3; PolynomialOpenings { - proof: tree.get_proof_by_pos(i0).unwrap(), - proof_sym: tree.get_proof_by_pos(i1).unwrap(), - proof_2: tree.get_proof_by_pos(i2).unwrap(), - proof_3: tree.get_proof_by_pos(i3).unwrap(), + batch_proof: tree.get_batch_proof(&[i0, i1, i2, i3]).unwrap(), evaluations: lde_trace.gather_aux_row(reverse_index(i0, domain_size as u64)), evaluations_sym: lde_trace.gather_aux_row(reverse_index(i1, domain_size as u64)), evaluations_2: lde_trace.gather_aux_row(reverse_index(i2, domain_size as u64)), diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 0f69438ca..1538b69bd 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -506,43 +506,32 @@ pub trait IsStarkVerifier< proof: &StarkProof, deep_poly_openings: &DeepPolynomialOpening, iota: usize, + num_leaves: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { // Arity-4: 4 positions per query orbit. - let i0 = iota * 4; - let i1 = iota * 4 + 1; - let i2 = iota * 4 + 2; - let i3 = iota * 4 + 3; + let positions = [iota * 4, iota * 4 + 1, iota * 4 + 2, iota * 4 + 3]; let mut result = true; // Verify main trace (multiplicities for preprocessed, full trace for normal) - result &= Self::verify_opening::( - &deep_poly_openings.main_trace_polys.proof, - &proof.lde_trace_main_merkle_root, - i0, - &deep_poly_openings.main_trace_polys.evaluations, - ); - result &= Self::verify_opening::( - &deep_poly_openings.main_trace_polys.proof_sym, - &proof.lde_trace_main_merkle_root, - i1, - &deep_poly_openings.main_trace_polys.evaluations_sym, - ); - result &= Self::verify_opening::( - &deep_poly_openings.main_trace_polys.proof_2, - &proof.lde_trace_main_merkle_root, - i2, - &deep_poly_openings.main_trace_polys.evaluations_2, - ); - result &= Self::verify_opening::( - &deep_poly_openings.main_trace_polys.proof_3, - &proof.lde_trace_main_merkle_root, - i3, - &deep_poly_openings.main_trace_polys.evaluations_3, - ); + let main_values = [ + deep_poly_openings.main_trace_polys.evaluations.clone(), + deep_poly_openings.main_trace_polys.evaluations_sym.clone(), + deep_poly_openings.main_trace_polys.evaluations_2.clone(), + deep_poly_openings.main_trace_polys.evaluations_3.clone(), + ]; + result &= deep_poly_openings + .main_trace_polys + .batch_proof + .verify::>( + &proof.lde_trace_main_merkle_root, + &positions, + &main_values, + num_leaves, + ); // Verify precomputed trace (for preprocessed tables only) match ( @@ -554,30 +543,20 @@ pub trait IsStarkVerifier< (None, Some(_)) => result = false, (Some(_), None) => result = false, (Some(precomputed_root), Some(precomputed_opening)) => { - result &= Self::verify_opening::( - &precomputed_opening.proof, - precomputed_root, - i0, - &precomputed_opening.evaluations, - ); - result &= Self::verify_opening::( - &precomputed_opening.proof_sym, - precomputed_root, - i1, - &precomputed_opening.evaluations_sym, - ); - result &= Self::verify_opening::( - &precomputed_opening.proof_2, - precomputed_root, - i2, - &precomputed_opening.evaluations_2, - ); - result &= Self::verify_opening::( - &precomputed_opening.proof_3, - precomputed_root, - i3, - &precomputed_opening.evaluations_3, - ); + let precomputed_values = [ + precomputed_opening.evaluations.clone(), + precomputed_opening.evaluations_sym.clone(), + precomputed_opening.evaluations_2.clone(), + precomputed_opening.evaluations_3.clone(), + ]; + result &= precomputed_opening + .batch_proof + .verify::>( + precomputed_root, + &positions, + &precomputed_values, + num_leaves, + ); } _ => {} } @@ -590,30 +569,20 @@ pub trait IsStarkVerifier< (None, Some(_)) => result = false, (Some(_), None) => result = false, (Some(aux_root), Some(aux_trace_polys_opening)) => { - result &= Self::verify_opening::( - &aux_trace_polys_opening.proof, - &aux_root, - i0, - &aux_trace_polys_opening.evaluations, - ); - result &= Self::verify_opening::( - &aux_trace_polys_opening.proof_sym, - &aux_root, - i1, - &aux_trace_polys_opening.evaluations_sym, - ); - result &= Self::verify_opening::( - &aux_trace_polys_opening.proof_2, - &aux_root, - i2, - &aux_trace_polys_opening.evaluations_2, - ); - result &= Self::verify_opening::( - &aux_trace_polys_opening.proof_3, - &aux_root, - i3, - &aux_trace_polys_opening.evaluations_3, - ); + let aux_values = [ + aux_trace_polys_opening.evaluations.clone(), + aux_trace_polys_opening.evaluations_sym.clone(), + aux_trace_polys_opening.evaluations_2.clone(), + aux_trace_polys_opening.evaluations_3.clone(), + ]; + result &= aux_trace_polys_opening + .batch_proof + .verify::>( + &aux_root, + &positions, + &aux_values, + num_leaves, + ); } _ => {} } @@ -665,6 +634,7 @@ pub trait IsStarkVerifier< fn step_4_verify_trace_and_composition_openings( proof: &StarkProof, challenges: &Challenges, + num_leaves: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -679,7 +649,8 @@ pub trait IsStarkVerifier< iota_n, ); - result &= Self::verify_trace_openings(proof, deep_poly_opening, *iota_n); + result &= + Self::verify_trace_openings(proof, deep_poly_opening, *iota_n, num_leaves); result }, ) @@ -1476,7 +1447,11 @@ pub trait IsStarkVerifier< let timer4 = Instant::now(); #[allow(clippy::let_and_return)] - if !Self::step_4_verify_trace_and_composition_openings(proof, &challenges) { + if !Self::step_4_verify_trace_and_composition_openings( + proof, + &challenges, + domain.lde_length, + ) { #[cfg(not(feature = "test_fiat_shamir"))] error!("DEEP Composition Polynomial verification failed"); return false; From 2ce825c3a3461ca4982d61380725af9d7ad50bd4 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Thu, 21 May 2026 16:52:43 -0300 Subject: [PATCH 6/6] cleanup: remove arity-4 dead code, fix stale comment + naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review follow-ups on the arity-4 FRI change: Dead code - `config.rs`: the switch to quad FRI layers left `FriLayerMerkleTree` / `FriLayerMerkleTreeBackend` (the old pair-leaf binary-FRI aliases) referenced only by their own definitions. Removed them and the stale `PairKeccak256Backend` import. `math-cuda/tests/keccak_leaves.rs` was the only other user โ€” repointed at `crypto::merkle_tree::backends:: types::PairKeccak256Backend` directly (same type). - `verifier.rs`: `DeepPolynomialEvaluations` (a 2-tuple alias) was orphaned when `reconstruct_deep_composition_poly_evaluations_for_all_ queries` started returning a 4-tuple. Removed. Wrong comment - `verify_composition_poly_opening` claimed the composition-poly tree "has pair leaves (PairKeccak256Backend)". It is a `BatchedMerkleTree` whose leaves each hold a row-pair. Comment corrected. Naming - `PolynomialOpenings` / `CompositionPolyOpenings` had fields `evaluations`, `evaluations_sym`, `evaluations_2`, `evaluations_3`. `evaluations_sym` is a binary-FRI carryover ("sym" = the -x point); in the arity-4 orbit it is just position index^1. Renamed to `evaluations_1` for consistency with `_2` / `_3`. Behaviour-preserving: 125 stark lib tests pass; lint clean. --- crypto/math-cuda/tests/keccak_leaves.rs | 10 ++++------ crypto/stark/src/config.rs | 10 ++-------- crypto/stark/src/proof/stark.rs | 4 ++-- crypto/stark/src/prover.rs | 8 ++++---- crypto/stark/src/verifier.rs | 25 +++++++++++-------------- 5 files changed, 23 insertions(+), 34 deletions(-) diff --git a/crypto/math-cuda/tests/keccak_leaves.rs b/crypto/math-cuda/tests/keccak_leaves.rs index d614e233d..02582c89a 100644 --- a/crypto/math-cuda/tests/keccak_leaves.rs +++ b/crypto/math-cuda/tests/keccak_leaves.rs @@ -1,17 +1,17 @@ //! Parity: GPU Keccak-256 leaf hashes must match the CPU prover's leaf //! hashing helpers. `stark::prover::keccak_leaves_bit_reversed` for //! per-row commits, `keccak_leaves_row_pair_bit_reversed` for the R2 -//! composition commit, and `FriLayerMerkleTreeBackend::hash_data` for the -//! FRI commit. These are the same helpers the prover itself calls so any +//! composition commit, and `PairKeccak256Backend::hash_data` for the +//! pair-leaf commit. These are the same helpers the prover itself calls so any //! change to the CPU leaf-hash contract surfaces here. +use crypto::merkle_tree::backends::types::PairKeccak256Backend; use crypto::merkle_tree::traits::IsMerkleTreeBackend; use math::field::element::FieldElement; use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; use math::field::goldilocks::GoldilocksField; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; -use stark::config::FriLayerMerkleTreeBackend; use stark::prover::{keccak_leaves_bit_reversed, keccak_leaves_row_pair_bit_reversed}; type Fp = FieldElement; @@ -171,9 +171,7 @@ fn keccak_fri_leaves_matches_cpu() { let cpu: Vec<[u8; 32]> = evals .chunks_exact(2) .map(|c| { - FriLayerMerkleTreeBackend::::hash_data(&[ - c[0], c[1], - ]) + PairKeccak256Backend::::hash_data(&[c[0], c[1]]) }) .collect(); diff --git a/crypto/stark/src/config.rs b/crypto/stark/src/config.rs index 1815defd2..7d721a2d3 100644 --- a/crypto/stark/src/config.rs +++ b/crypto/stark/src/config.rs @@ -1,7 +1,5 @@ use crypto::merkle_tree::{ - backends::types::{ - BatchKeccak256Backend, Keccak256Backend, PairKeccak256Backend, QuadKeccak256Backend, - }, + backends::types::{BatchKeccak256Backend, Keccak256Backend, QuadKeccak256Backend}, merkle::MerkleTree, }; @@ -21,10 +19,6 @@ pub type Commitment = [u8; COMMITMENT_SIZE]; pub type BatchedMerkleTreeBackend = BatchKeccak256Backend; pub type BatchedMerkleTree = MerkleTree>; -// FRI layer uses fixed-size pairs for efficiency (avoids Vec allocation per pair) -pub type FriLayerMerkleTreeBackend = PairKeccak256Backend; -pub type FriLayerMerkleTree = MerkleTree>; - -// Arity-4 FRI layer: each leaf commits to 4 consecutive evaluations +// Arity-4 FRI layer: each leaf commits to 4 consecutive evaluations. pub type FriLayerQuadMerkleTreeBackend = QuadKeccak256Backend; pub type FriLayerQuadMerkleTree = MerkleTree>; diff --git a/crypto/stark/src/proof/stark.rs b/crypto/stark/src/proof/stark.rs index 855f51a91..89824ee90 100644 --- a/crypto/stark/src/proof/stark.rs +++ b/crypto/stark/src/proof/stark.rs @@ -16,7 +16,7 @@ pub struct PolynomialOpenings { /// auth-path prefix โ€” smaller than 4 independent proofs. pub batch_proof: BatchProof, pub evaluations: Vec>, - pub evaluations_sym: Vec>, + pub evaluations_1: Vec>, pub evaluations_2: Vec>, pub evaluations_3: Vec>, } @@ -32,7 +32,7 @@ pub struct CompositionPolyOpenings { /// Merkle proof for leaf containing positions {4i+2, 4i+3}. pub proof_2: Proof, pub evaluations: Vec>, - pub evaluations_sym: Vec>, + pub evaluations_1: Vec>, pub evaluations_2: Vec>, pub evaluations_3: Vec>, } diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 578f909ab..e3797a8a5 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -1421,7 +1421,7 @@ pub trait IsStarkProver< proof: proof_01, proof_2: proof_23, evaluations: eval_at(index * 4), - evaluations_sym: eval_at(index * 4 + 1), + evaluations_1: eval_at(index * 4 + 1), evaluations_2: eval_at(index * 4 + 2), evaluations_3: eval_at(index * 4 + 3), } @@ -1450,7 +1450,7 @@ pub trait IsStarkProver< PolynomialOpenings { batch_proof: tree.get_batch_proof(&[i0, i1, i2, i3]).unwrap(), evaluations: lde_trace.gather_main_row(reverse_index(i0, domain_size as u64)), - evaluations_sym: lde_trace.gather_main_row(reverse_index(i1, domain_size as u64)), + evaluations_1: lde_trace.gather_main_row(reverse_index(i1, domain_size as u64)), evaluations_2: lde_trace.gather_main_row(reverse_index(i2, domain_size as u64)), evaluations_3: lde_trace.gather_main_row(reverse_index(i3, domain_size as u64)), } @@ -1482,7 +1482,7 @@ pub trait IsStarkProver< col_start, col_end, ), - evaluations_sym: lde_trace.gather_main_row_range( + evaluations_1: lde_trace.gather_main_row_range( reverse_index(i1, domain_size as u64), col_start, col_end, @@ -1520,7 +1520,7 @@ pub trait IsStarkProver< PolynomialOpenings { batch_proof: tree.get_batch_proof(&[i0, i1, i2, i3]).unwrap(), evaluations: lde_trace.gather_aux_row(reverse_index(i0, domain_size as u64)), - evaluations_sym: lde_trace.gather_aux_row(reverse_index(i1, domain_size as u64)), + evaluations_1: lde_trace.gather_aux_row(reverse_index(i1, domain_size as u64)), evaluations_2: lde_trace.gather_aux_row(reverse_index(i2, domain_size as u64)), evaluations_3: lde_trace.gather_aux_row(reverse_index(i3, domain_size as u64)), } diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 1538b69bd..fd68285c7 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -72,8 +72,6 @@ where pub grinding_seed: [u8; 32], } -pub type DeepPolynomialEvaluations = (Vec>, Vec>); - /// The functionality of a STARK verifier providing methods to run the STARK Verify protocol /// https://lambdaclass.github.io/lambdaworks/starks/protocol.html pub trait IsStarkVerifier< @@ -519,7 +517,7 @@ pub trait IsStarkVerifier< // Verify main trace (multiplicities for preprocessed, full trace for normal) let main_values = [ deep_poly_openings.main_trace_polys.evaluations.clone(), - deep_poly_openings.main_trace_polys.evaluations_sym.clone(), + deep_poly_openings.main_trace_polys.evaluations_1.clone(), deep_poly_openings.main_trace_polys.evaluations_2.clone(), deep_poly_openings.main_trace_polys.evaluations_3.clone(), ]; @@ -545,7 +543,7 @@ pub trait IsStarkVerifier< (Some(precomputed_root), Some(precomputed_opening)) => { let precomputed_values = [ precomputed_opening.evaluations.clone(), - precomputed_opening.evaluations_sym.clone(), + precomputed_opening.evaluations_1.clone(), precomputed_opening.evaluations_2.clone(), precomputed_opening.evaluations_3.clone(), ]; @@ -571,7 +569,7 @@ pub trait IsStarkVerifier< (Some(aux_root), Some(aux_trace_polys_opening)) => { let aux_values = [ aux_trace_polys_opening.evaluations.clone(), - aux_trace_polys_opening.evaluations_sym.clone(), + aux_trace_polys_opening.evaluations_1.clone(), aux_trace_polys_opening.evaluations_2.clone(), aux_trace_polys_opening.evaluations_3.clone(), ]; @@ -601,11 +599,11 @@ pub trait IsStarkVerifier< FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { - // Arity-4: composition poly tree has pair leaves (PairKeccak256Backend). - // The 4-element orbit {4*iota, 4*iota+1, 4*iota+2, 4*iota+3} spans leaves - // {2*iota, 2*iota+1}. Verify both leaves independently. + // Arity-4: the composition-poly tree (a BatchedMerkleTree) commits one + // row-pair per leaf, so the 4-element orbit {4*iota .. 4*iota+3} spans + // two leaves, {2*iota, 2*iota+1}. Verify both leaves independently. let mut value_01 = deep_poly_openings.composition_poly.evaluations.clone(); - value_01.extend_from_slice(&deep_poly_openings.composition_poly.evaluations_sym); + value_01.extend_from_slice(&deep_poly_openings.composition_poly.evaluations_1); let mut value_23 = deep_poly_openings.composition_poly.evaluations_2.clone(); value_23.extend_from_slice(&deep_poly_openings.composition_poly.evaluations_3); @@ -887,7 +885,7 @@ pub trait IsStarkVerifier< let precomp_1 = opening .precomputed_trace_polys .as_ref() - .map(|p| p.evaluations_sym.as_slice()); + .map(|p| p.evaluations_1.as_slice()); let precomp_2 = opening .precomputed_trace_polys .as_ref() @@ -904,7 +902,7 @@ pub trait IsStarkVerifier< let aux_1 = opening .aux_trace_polys .as_ref() - .map(|a| a.evaluations_sym.as_slice()); + .map(|a| a.evaluations_1.as_slice()); let aux_2 = opening .aux_trace_polys .as_ref() @@ -915,8 +913,7 @@ pub trait IsStarkVerifier< .map(|a| a.evaluations_3.as_slice()); let te0 = gather_trace_evals(&opening.main_trace_polys.evaluations, precomp_0, aux_0); - let te1 = - gather_trace_evals(&opening.main_trace_polys.evaluations_sym, precomp_1, aux_1); + let te1 = gather_trace_evals(&opening.main_trace_polys.evaluations_1, precomp_1, aux_1); let te2 = gather_trace_evals(&opening.main_trace_polys.evaluations_2, precomp_2, aux_2); let te3 = gather_trace_evals(&opening.main_trace_polys.evaluations_3, precomp_3, aux_3); @@ -945,7 +942,7 @@ pub trait IsStarkVerifier< primitive_root, challenges, &te1, - &opening.composition_poly.evaluations_sym, + &opening.composition_poly.evaluations_1, )); evals_2.push(Self::reconstruct_deep_composition_poly_evaluation( proof,