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..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, FieldElementVectorBackend}, + field_element_vector::{ + FieldElementPairBackend, FieldElementQuadBackend, FieldElementVectorBackend, + }, }; // Field element backend definitions @@ -13,3 +15,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/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/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 50650e40a..7d721a2d3 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, QuadKeccak256Backend}, merkle::MerkleTree, }; @@ -19,6 +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. +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..e03ab3bc0 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,50 @@ 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. + // 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(); + + // 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 +84,56 @@ 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); - // Final fold - 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); + } - let last_value = evals.first().unwrap_or(&FieldElement::zero()).clone(); + // 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); - // >>>> Send value: pโ‚™ + let last_value = evals + .first() + .expect("FRI evals empty after folding") + .clone(); 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 +143,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..89824ee90 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}, @@ -11,16 +11,36 @@ 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. + /// 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_1: Vec>, + pub evaluations_2: Vec>, + 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, - pub proof_sym: Proof, + /// 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>, } #[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 68f50ea53..e3797a8a5 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -28,7 +28,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}; #[cfg(feature = "disk-spill")] use crate::storage_mode::StorageMode; use crate::table::Table; @@ -1192,7 +1192,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() @@ -1223,8 +1224,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::>() } @@ -1385,38 +1390,40 @@ 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, { - 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(); - 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(), + 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() + }; + + CompositionPolyOpenings { + proof: proof_01, + proof_2: proof_23, + evaluations: eval_at(index * 4), + evaluations_1: eval_at(index * 4 + 1), + evaluations_2: eval_at(index * 4 + 2), + evaluations_3: eval_at(index * 4 + 3), } } @@ -1435,14 +1442,17 @@ 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)), + 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_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)), } } @@ -1461,18 +1471,29 @@ 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(), + batch_proof: tree.get_batch_proof(&[i0, i1, i2, 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_1: lde_trace.gather_main_row_range( + 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_sym: lde_trace.gather_main_row_range( - reverse_index(index_sym, domain_size as u64), + evaluations_3: lde_trace.gather_main_row_range( + reverse_index(i3, domain_size as u64), col_start, col_end, ), @@ -1492,13 +1513,16 @@ 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)), + 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_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/tests/air_tests.rs b/crypto/stark/src/tests/air_tests.rs index 8e20f303e..ec4d13aea 100644 --- a/crypto/stark/src/tests/air_tests.rs +++ b/crypto/stark/src/tests/air_tests.rs @@ -598,6 +598,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 a1a68930a..fd68285c7 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, @@ -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< @@ -82,14 +80,42 @@ 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, 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,21 +219,11 @@ pub trait IsStarkVerifier< // <<<< Receive challenges: ๐›พโฑผ, ๐›พโฑผ' let gammas = deep_composition_coefficients; - // FRI commit phase - 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::>>(); - - // >>>> Send challenge ๐œโ‚™โ‚‹โ‚ - zetas.push(transcript.sample_field_element()); + // 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 zetas = + Self::reconstruct_fri_zetas(number_layers, &proof.fri_layers_merkle_roots, transcript); // <<<< Receive value: pโ‚™ transcript.append_field_element(&proof.fri_last_value); @@ -393,54 +409,76 @@ 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. + // 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::>>(); - FieldElement::inplace_batch_inverse(&mut evaluation_point_inverse).unwrap(); + .collect(); + FieldElement::inplace_batch_inverse(&mut eval_inv_a_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 .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) } @@ -466,28 +504,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, { - let index = iota * 2; - let index_sym = iota * 2 + 1; + // Arity-4: 4 positions per query orbit. + 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, - index, - &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, - &deep_poly_openings.main_trace_polys.evaluations_sym, - ); + let main_values = [ + deep_poly_openings.main_trace_polys.evaluations.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(), + ]; + 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 ( @@ -499,18 +541,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, - index, - &precomputed_opening.evaluations, - ); - result &= Self::verify_opening::( - &precomputed_opening.proof_sym, - precomputed_root, - index_sym, - &precomputed_opening.evaluations_sym, - ); + let precomputed_values = [ + precomputed_opening.evaluations.clone(), + precomputed_opening.evaluations_1.clone(), + precomputed_opening.evaluations_2.clone(), + precomputed_opening.evaluations_3.clone(), + ]; + result &= precomputed_opening + .batch_proof + .verify::>( + precomputed_root, + &positions, + &precomputed_values, + num_leaves, + ); } _ => {} } @@ -523,18 +567,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, - index, - &aux_trace_polys_opening.evaluations, - ); - result &= Self::verify_opening::( - &aux_trace_polys_opening.proof_sym, - &aux_root, - index_sym, - &aux_trace_polys_opening.evaluations_sym, - ); + let aux_values = [ + aux_trace_polys_opening.evaluations.clone(), + aux_trace_polys_opening.evaluations_1.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, + ); } _ => {} } @@ -553,17 +599,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: 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_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); 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 @@ -572,6 +632,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, @@ -586,205 +647,321 @@ 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 }, ) } - /// 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); + #[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); + + 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. + #[allow(clippy::type_complexity)] 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_1.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_1.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_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); + + 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_1, + )); + 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,21 +1319,10 @@ pub trait IsStarkVerifier< // <<<< Receive challenges: ๐›พโฑผ, ๐›พโฑผ' let gammas = deep_composition_coefficients; - // FRI commit phase - 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::>>(); - - // >>>> Send challenge ๐œโ‚™โ‚‹โ‚ - zetas.push(transcript.sample_field_element()); + // 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 zetas = + Self::reconstruct_fri_zetas(number_layers, &proof.fri_layers_merkle_roots, transcript); // <<<< Receive value: pโ‚™ transcript.append_field_element(&proof.fri_last_value); @@ -1202,8 +1368,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; } @@ -1271,7 +1444,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;