-
Notifications
You must be signed in to change notification settings - Fork 1
FRI arity-4 folding with BatchProof openings #531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ce9fa8c
ce6a2c3
0c8feba
6ec773b
2c94498
212649d
7bf9265
4d19460
2ce825c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,17 +8,22 @@ 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; | ||
| 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<F: IsFFTField + IsSubFieldOf<E>, E: IsField>( | ||
| number_layers: usize, | ||
| mut evals: Vec<FieldElement<E>>, | ||
|
|
@@ -27,35 +32,50 @@ pub fn commit_phase_from_evaluations<F: IsFFTField + IsSubFieldOf<E>, E: IsField | |
| domain_size: usize, | ||
| ) -> ( | ||
| FieldElement<E>, | ||
| Vec<FriLayer<E, FriLayerMerkleTreeBackend<E>>>, | ||
| Vec<FriLayer<E, FriLayerQuadMerkleTreeBackend<E>>>, | ||
| ) | ||
| where | ||
| FieldElement<F>: AsBytes + Sync + Send, | ||
| FieldElement<E>: 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<E>; 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<E>; 4]> = evals | ||
| .chunks_exact(4) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. High – silent data loss / panic for small domains
For Consider asserting the invariant explicitly: debug_assert!(
evals.len() >= 4 && evals.len() % 4 == 0,
"evals length {} is not a multiple of 4 after double fold",
evals.len()
);And add a guard at the top of the function (or at the call site) to skip FRI commit when the folded domain is too small for quad leaves. |
||
| .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<E>; 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<F: IsField>( | ||
| fri_layers: &Vec<FriLayer<F, FriLayerMerkleTreeBackend<F>>>, | ||
| fri_layers: &[FriLayer<F, FriLayerQuadMerkleTreeBackend<F>>], | ||
| iotas: &[usize], | ||
| num_double_rounds: usize, | ||
| ) -> Vec<FriDecommitment<F>> | ||
| where | ||
| FieldElement<F>: 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() | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Medium – both challenges sampled before committing: soundness note
The comment argues this is sound because the prover already fixed
f_kin the previous commitment andf_{k+2}is fully determined byf_k,zeta1,zeta2. That argument is correct for a honest prover, but it deviates from the standard Fiat-Shamir assumption used in security proofs.In standard arity-2 FRI the challenge
zeta_kis derived from the transcript state includingC_k, so the prover cannot chooseC_kafter seeingzeta_k. Here,zeta1andzeta2are both derived from the state beforeC_{k+1}is appended. A prover that has some slack in choosingf_{k+1}(e.g., from a weak binding), would learn both challenges before fixing the committed polynomial.If the security proof for this construction has been written up, it would help to reference it in the comment.