From 11b00d4e5336b335cce6a9e23e00b09d94acebdf Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 21 Oct 2025 17:13:26 +0200 Subject: [PATCH 01/42] wip --- src/main.rs | 183 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 166 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index 98152abe..b76cc13f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,171 @@ #![cfg_attr(not(test), allow(unused_crate_dependencies))] -use air::examples::prove_poseidon2::{Poseidon2Config, prove_poseidon2}; -use whir_p3::{FoldingFactor, SecurityAssumption}; +use multilinear_toolkit::prelude::*; +use p3_koala_bear::{KoalaBear, QuinticExtensionFieldKB}; +use rand::{Rng, SeedableRng, rngs::StdRng}; +use std::array; +use utils::{build_prover_state, transposed_par_iter_mut}; + +type F = KoalaBear; +type EF = QuinticExtensionFieldKB; +const UNIVARIATE_SKIPS: usize = 3; + +fn multilvariate_eval>(poly: &[F], point: &[EF]) -> EF { + assert_eq!(poly.len(), 1 << (point.len() + UNIVARIATE_SKIPS - 1)); + univariate_selectors::(UNIVARIATE_SKIPS) + .iter() + .zip(poly.chunks_exact(1 << (point.len() - 1))) + .map(|(selector, chunk)| { + selector.evaluate(point[0]) * chunk.evaluate(&MultilinearPoint(point[1..].to_vec())) + }) + .sum() +} + +pub struct FullRoundComputation { + pub constants: [F; 16], + pub matrix: [[F; 16]; 16], +} + +impl, EF: ExtensionField> SumcheckComputation + for FullRoundComputation +{ + fn degree(&self) -> usize { + 3 + } + + fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { + assert_eq!(point.len(), 16); + let intermediate: [NF; 16] = array::from_fn(|j| point[j].cube() + self.constants[j]); + let mut res = EF::ZERO; + for j in 0..16 { + let mut temp = NF::ZERO; + for k in 0..16 { + temp += intermediate[k] * self.matrix[j][k]; + } + res += alpha_powers[j] * temp; + } + res + } +} + +pub struct FullRoundComputationPacked { + pub constants: [F; 16], + pub matrix: [[F; 16]; 16], +} + +impl SumcheckComputationPacked for FullRoundComputationPacked { + fn degree(&self) -> usize { + 3 + } + + fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { + assert_eq!(point.len(), 16); + let intermediate: [PFPacking; 16] = + array::from_fn(|j| point[j].cube() + self.constants[j]); + let mut res = EFPacking::::ZERO; + for j in 0..16 { + let mut temp = PFPacking::::ZERO; + for k in 0..16 { + temp += intermediate[k] * self.matrix[j][k]; + } + res += alpha_powers[j] * temp; + } + res + } + + fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { + assert_eq!(point.len(), 16); + let intermediate: [EFPacking; 16] = + array::from_fn(|j| point[j].cube() + self.constants[j]); + let mut res = EFPacking::::ZERO; + for j in 0..16 { + let mut temp = EFPacking::::ZERO; + for k in 0..16 { + temp += intermediate[k] * self.matrix[j][k]; + } + res += temp * alpha_powers[j]; + } + res + } +} fn main() { - let config = Poseidon2Config { - log_n_poseidons_16: 17, - log_n_poseidons_24: 17, - univariate_skips: 4, - folding_factor: FoldingFactor::new(7, 4), - log_inv_rate: 1, - soundness_type: SecurityAssumption::CapacityBound, - pow_bits: 16, - security_level: 128, - rs_domain_initial_reduction_factor: 5, - max_num_variables_to_send_coeffs: 7, - display_logs: true, - }; - let benchmark = prove_poseidon2(&config); - println!("\n{benchmark}"); + let mut rng = StdRng::seed_from_u64(0); + let log_n_poseidons = 13; + let n_poseidons = 1 << log_n_poseidons; + let perm_inputs = (0..n_poseidons) + .map(|_| rng.random()) + .collect::>(); + + let input_layers: [_; 16] = + array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); + + let constants: [F; 16] = rng.random(); + let matrix: [[F; 16]; 16] = rng.random(); + + let mut output_layers: [_; 16] = array::from_fn(|_| F::zero_vec(n_poseidons)); + transposed_par_iter_mut(&mut output_layers) + .enumerate() + .for_each(|(row_index, output_row)| { + let intermediate: [F; 16] = + array::from_fn(|j| input_layers[j][row_index].cube() + constants[j]); + output_row.into_iter().enumerate().for_each(|(j, output)| { + let mut res = F::ZERO; + for k in 0..16 { + res += matrix[j][k] * intermediate[k]; + } + *output = res; + }); + }); + + let claim_point = MultilinearPoint( + (0..(log_n_poseidons + 1 - UNIVARIATE_SKIPS)) + .map(|_| rng.random()) + .collect::>(), + ); + + let output_claims = output_layers + .par_iter() + .map(|output_layer| multilvariate_eval(output_layer, &claim_point)) + .collect::>(); + + let mut prover_state = build_prover_state::(); + prover_state.add_extension_scalars(&output_claims); + let batching_scalar = prover_state.sample(); + let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); + let batching_scalars_powers = batching_scalar.powers().collect_n(16); + let batched_output_layer = (0..n_poseidons) + .into_par_iter() + .map(|i| { + dot_product( + batching_scalars_powers.iter().copied(), + (0..16).map(|j| output_layers[j][i]), + ) + }) + .collect::>(); + + assert_eq!( + batched_claim, + multilvariate_eval(&batched_output_layer, &claim_point) + ); + + let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( + UNIVARIATE_SKIPS, + MleGroupRef::Base(input_layers.iter().map(Vec::as_slice).collect()), + &FullRoundComputation { constants, matrix }, + &FullRoundComputationPacked { constants, matrix }, + &batching_scalars_powers, + Some((claim_point.0.clone(), None)), + false, + &mut prover_state, + batched_claim, + None, + ); + + for i in 0..16 { + assert_eq!( + sumcheck_inner_evals[i], + multilvariate_eval(&input_layers[i], &sumcheck_point) + ); + } } From b940c3804925cca29d48585bc8321cc58a47ca39 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 21 Oct 2025 17:13:26 +0200 Subject: [PATCH 02/42] wip --- src/main.rs | 54 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index b76cc13f..cf9258ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use multilinear_toolkit::prelude::*; use p3_koala_bear::{KoalaBear, QuinticExtensionFieldKB}; use rand::{Rng, SeedableRng, rngs::StdRng}; use std::array; -use utils::{build_prover_state, transposed_par_iter_mut}; +use utils::{build_prover_state, build_verifier_state, transposed_par_iter_mut}; type F = KoalaBear; type EF = QuinticExtensionFieldKB; @@ -48,12 +48,7 @@ impl, EF: ExtensionField> SumcheckComputation } } -pub struct FullRoundComputationPacked { - pub constants: [F; 16], - pub matrix: [[F; 16]; 16], -} - -impl SumcheckComputationPacked for FullRoundComputationPacked { +impl SumcheckComputationPacked for FullRoundComputation { fn degree(&self) -> usize { 3 } @@ -91,7 +86,7 @@ impl SumcheckComputationPacked for FullRoundComputationPacked { fn main() { let mut rng = StdRng::seed_from_u64(0); - let log_n_poseidons = 13; + let log_n_poseidons = 11; let n_poseidons = 1 << log_n_poseidons; let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) @@ -149,11 +144,12 @@ fn main() { multilvariate_eval(&batched_output_layer, &claim_point) ); + let sc_computation = FullRoundComputation { constants, matrix }; let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( UNIVARIATE_SKIPS, MleGroupRef::Base(input_layers.iter().map(Vec::as_slice).collect()), - &FullRoundComputation { constants, matrix }, - &FullRoundComputationPacked { constants, matrix }, + &sc_computation, + &sc_computation, &batching_scalars_powers, Some((claim_point.0.clone(), None)), false, @@ -161,6 +157,44 @@ fn main() { batched_claim, None, ); + assert_eq!( + sc_computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) + * eq_poly_with_skip(&sumcheck_point, &claim_point, UNIVARIATE_SKIPS), + sumcheck_final_sum + ); + + prover_state.add_extension_scalars(&sumcheck_inner_evals); + + // ---------------------------------------------------- VERIFIER ---------------------------------------------------- + + let mut verifier_state = build_verifier_state(&prover_state); + + let output_claims = verifier_state.next_extension_scalars_vec(16).unwrap(); + let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); + + let batching_scalar = verifier_state.sample(); + let batching_scalars_powers = batching_scalar.powers().collect_n(16); + + let (retrieved_batched_claim, sumcheck_postponed_claim) = sumcheck_verify_with_univariate_skip( + &mut verifier_state, + 4, + log_n_poseidons, + UNIVARIATE_SKIPS, + ) + .unwrap(); + + assert_eq!(retrieved_batched_claim, batched_claim); + + let sumcheck_inner_evals = verifier_state.next_extension_scalars_vec(16).unwrap(); + assert_eq!( + sc_computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) + * eq_poly_with_skip( + &sumcheck_postponed_claim.point, + &claim_point, + UNIVARIATE_SKIPS + ), + sumcheck_postponed_claim.value + ); for i in 0..16 { assert_eq!( From 152b445a3814805254f7f6b58e8fe907f139b32b Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 21 Oct 2025 17:13:26 +0200 Subject: [PATCH 03/42] wip --- src/main.rs | 246 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 144 insertions(+), 102 deletions(-) diff --git a/src/main.rs b/src/main.rs index cf9258ca..378feaf7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,79 +10,7 @@ type F = KoalaBear; type EF = QuinticExtensionFieldKB; const UNIVARIATE_SKIPS: usize = 3; -fn multilvariate_eval>(poly: &[F], point: &[EF]) -> EF { - assert_eq!(poly.len(), 1 << (point.len() + UNIVARIATE_SKIPS - 1)); - univariate_selectors::(UNIVARIATE_SKIPS) - .iter() - .zip(poly.chunks_exact(1 << (point.len() - 1))) - .map(|(selector, chunk)| { - selector.evaluate(point[0]) * chunk.evaluate(&MultilinearPoint(point[1..].to_vec())) - }) - .sum() -} - -pub struct FullRoundComputation { - pub constants: [F; 16], - pub matrix: [[F; 16]; 16], -} - -impl, EF: ExtensionField> SumcheckComputation - for FullRoundComputation -{ - fn degree(&self) -> usize { - 3 - } - - fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { - assert_eq!(point.len(), 16); - let intermediate: [NF; 16] = array::from_fn(|j| point[j].cube() + self.constants[j]); - let mut res = EF::ZERO; - for j in 0..16 { - let mut temp = NF::ZERO; - for k in 0..16 { - temp += intermediate[k] * self.matrix[j][k]; - } - res += alpha_powers[j] * temp; - } - res - } -} - -impl SumcheckComputationPacked for FullRoundComputation { - fn degree(&self) -> usize { - 3 - } - - fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { - assert_eq!(point.len(), 16); - let intermediate: [PFPacking; 16] = - array::from_fn(|j| point[j].cube() + self.constants[j]); - let mut res = EFPacking::::ZERO; - for j in 0..16 { - let mut temp = PFPacking::::ZERO; - for k in 0..16 { - temp += intermediate[k] * self.matrix[j][k]; - } - res += alpha_powers[j] * temp; - } - res - } - - fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { - assert_eq!(point.len(), 16); - let intermediate: [EFPacking; 16] = - array::from_fn(|j| point[j].cube() + self.constants[j]); - let mut res = EFPacking::::ZERO; - for j in 0..16 { - let mut temp = EFPacking::::ZERO; - for k in 0..16 { - temp += intermediate[k] * self.matrix[j][k]; - } - res += temp * alpha_powers[j]; - } - res - } -} +const HALF_FULL_ROUNDS: usize = 2; fn main() { let mut rng = StdRng::seed_from_u64(0); @@ -95,19 +23,21 @@ fn main() { let input_layers: [_; 16] = array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); - let constants: [F; 16] = rng.random(); - let matrix: [[F; 16]; 16] = rng.random(); + let ful_round = FullRoundComputation { + constants: rng.random(), + matrix: rng.random(), + }; let mut output_layers: [_; 16] = array::from_fn(|_| F::zero_vec(n_poseidons)); transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { let intermediate: [F; 16] = - array::from_fn(|j| input_layers[j][row_index].cube() + constants[j]); + array::from_fn(|j| input_layers[j][row_index].cube() + ful_round.constants[j]); output_row.into_iter().enumerate().for_each(|(j, output)| { let mut res = F::ZERO; for k in 0..16 { - res += matrix[j][k] * intermediate[k]; + res += ful_round.matrix[j][k] * intermediate[k]; } *output = res; }); @@ -126,6 +56,48 @@ fn main() { let mut prover_state = build_prover_state::(); prover_state.add_extension_scalars(&output_claims); + + prove_full_round( + &mut prover_state, + &ful_round, + n_poseidons, + &input_layers, + &output_layers, + &claim_point, + &output_claims, + ); + + // ---------------------------------------------------- VERIFIER ---------------------------------------------------- + + let mut verifier_state = build_verifier_state(&prover_state); + + let output_claims = verifier_state.next_extension_scalars_vec(16).unwrap(); + + let (sumcheck_point, sumcheck_inner_evals) = verify_full_round( + &mut verifier_state, + &ful_round, + log_n_poseidons, + &claim_point, + &output_claims, + ); + + for i in 0..16 { + assert_eq!( + sumcheck_inner_evals[i], + multilvariate_eval(&input_layers[i], &sumcheck_point) + ); + } +} + +fn prove_full_round( + prover_state: &mut FSProver>, + ful_round: &FullRoundComputation, + n_poseidons: usize, + input_layers: &[Vec], + output_layers: &[Vec], + claim_point: &[EF], + output_claims: &[EF], +) -> (Vec, Vec) { let batching_scalar = prover_state.sample(); let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); let batching_scalars_powers = batching_scalar.powers().collect_n(16); @@ -144,50 +116,51 @@ fn main() { multilvariate_eval(&batched_output_layer, &claim_point) ); - let sc_computation = FullRoundComputation { constants, matrix }; let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( UNIVARIATE_SKIPS, MleGroupRef::Base(input_layers.iter().map(Vec::as_slice).collect()), - &sc_computation, - &sc_computation, + ful_round, + ful_round, &batching_scalars_powers, - Some((claim_point.0.clone(), None)), + Some((claim_point.to_vec(), None)), false, - &mut prover_state, + prover_state, batched_claim, None, ); + + // sanity check assert_eq!( - sc_computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) + ful_round.eval(&sumcheck_inner_evals, &batching_scalars_powers) * eq_poly_with_skip(&sumcheck_point, &claim_point, UNIVARIATE_SKIPS), sumcheck_final_sum ); prover_state.add_extension_scalars(&sumcheck_inner_evals); - // ---------------------------------------------------- VERIFIER ---------------------------------------------------- - - let mut verifier_state = build_verifier_state(&prover_state); - - let output_claims = verifier_state.next_extension_scalars_vec(16).unwrap(); - let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); + (sumcheck_point.0, sumcheck_inner_evals) +} +fn verify_full_round( + verifier_state: &mut FSVerifier>, + ful_round: &FullRoundComputation, + log_n_poseidons: usize, + claim_point: &[EF], + output_claims: &[EF], +) -> (Vec, Vec) { let batching_scalar = verifier_state.sample(); let batching_scalars_powers = batching_scalar.powers().collect_n(16); + let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); - let (retrieved_batched_claim, sumcheck_postponed_claim) = sumcheck_verify_with_univariate_skip( - &mut verifier_state, - 4, - log_n_poseidons, - UNIVARIATE_SKIPS, - ) - .unwrap(); + let (retrieved_batched_claim, sumcheck_postponed_claim) = + sumcheck_verify_with_univariate_skip(verifier_state, 4, log_n_poseidons, UNIVARIATE_SKIPS) + .unwrap(); assert_eq!(retrieved_batched_claim, batched_claim); let sumcheck_inner_evals = verifier_state.next_extension_scalars_vec(16).unwrap(); assert_eq!( - sc_computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) + ful_round.eval(&sumcheck_inner_evals, &batching_scalars_powers) * eq_poly_with_skip( &sumcheck_postponed_claim.point, &claim_point, @@ -196,10 +169,79 @@ fn main() { sumcheck_postponed_claim.value ); - for i in 0..16 { - assert_eq!( - sumcheck_inner_evals[i], - multilvariate_eval(&input_layers[i], &sumcheck_point) - ); + (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) +} + +fn multilvariate_eval>(poly: &[F], point: &[EF]) -> EF { + assert_eq!(poly.len(), 1 << (point.len() + UNIVARIATE_SKIPS - 1)); + univariate_selectors::(UNIVARIATE_SKIPS) + .iter() + .zip(poly.chunks_exact(1 << (point.len() - 1))) + .map(|(selector, chunk)| { + selector.evaluate(point[0]) * chunk.evaluate(&MultilinearPoint(point[1..].to_vec())) + }) + .sum() +} + +pub struct FullRoundComputation { + pub constants: [F; 16], + pub matrix: [[F; 16]; 16], +} + +impl, EF: ExtensionField> SumcheckComputation + for FullRoundComputation +{ + fn degree(&self) -> usize { + 3 + } + + fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { + assert_eq!(point.len(), 16); + let intermediate: [NF; 16] = array::from_fn(|j| point[j].cube() + self.constants[j]); + let mut res = EF::ZERO; + for j in 0..16 { + let mut temp = NF::ZERO; + for k in 0..16 { + temp += intermediate[k] * self.matrix[j][k]; + } + res += alpha_powers[j] * temp; + } + res + } +} + +impl SumcheckComputationPacked for FullRoundComputation { + fn degree(&self) -> usize { + 3 + } + + fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { + assert_eq!(point.len(), 16); + let intermediate: [PFPacking; 16] = + array::from_fn(|j| point[j].cube() + self.constants[j]); + let mut res = EFPacking::::ZERO; + for j in 0..16 { + let mut temp = PFPacking::::ZERO; + for k in 0..16 { + temp += intermediate[k] * self.matrix[j][k]; + } + res += alpha_powers[j] * temp; + } + res + } + + fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { + assert_eq!(point.len(), 16); + let intermediate: [EFPacking; 16] = + array::from_fn(|j| point[j].cube() + self.constants[j]); + let mut res = EFPacking::::ZERO; + for j in 0..16 { + let mut temp = EFPacking::::ZERO; + for k in 0..16 { + temp += intermediate[k] * self.matrix[j][k]; + } + res += temp * alpha_powers[j]; + } + res } } From 59f73490080dd5792462e3b713ae3fdd8d5c948e Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 21 Oct 2025 17:13:26 +0200 Subject: [PATCH 04/42] 2 full rounds --- src/main.rs | 129 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 75 insertions(+), 54 deletions(-) diff --git a/src/main.rs b/src/main.rs index 378feaf7..eebb49dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,6 @@ type F = KoalaBear; type EF = QuinticExtensionFieldKB; const UNIVARIATE_SKIPS: usize = 3; -const HALF_FULL_ROUNDS: usize = 2; - fn main() { let mut rng = StdRng::seed_from_u64(0); let log_n_poseidons = 11; @@ -19,21 +17,88 @@ fn main() { let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) .collect::>(); - let input_layers: [_; 16] = array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); - let ful_round = FullRoundComputation { + let ful_round_1 = FullRoundComputation { + constants: rng.random(), + matrix: rng.random(), + }; + let ful_round_2 = FullRoundComputation { constants: rng.random(), matrix: rng.random(), }; - let mut output_layers: [_; 16] = array::from_fn(|_| F::zero_vec(n_poseidons)); + let output_claim_point = (0..(log_n_poseidons + 1 - UNIVARIATE_SKIPS)) + .map(|_| rng.random()) + .collect::>(); + + let mut prover_state = build_prover_state::(); + + { + // ---------------------------------------------------- PROVER ---------------------------------------------------- + + let output_layers_1 = apply_full_round(&input_layers, &ful_round_1); + let output_layers_2 = apply_full_round(&output_layers_1, &ful_round_2); + + let mut output_claims = output_layers_2 + .par_iter() + .map(|output_layer| multilvariate_eval(output_layer, &output_claim_point)) + .collect::>(); + + prover_state.add_extension_scalars(&output_claims); + + let mut claim_point = output_claim_point.clone(); + for (input_layers, output_layers, full_round) in [ + (&output_layers_1, &output_layers_2, &ful_round_2), + (&input_layers, &output_layers_1, &ful_round_1), + ] { + (claim_point, output_claims) = prove_full_round( + &mut prover_state, + full_round, + n_poseidons, + input_layers, + output_layers, + &claim_point, + &output_claims, + ); + } + } + + { + // ---------------------------------------------------- VERIFIER ---------------------------------------------------- + + let mut verifier_state = build_verifier_state(&prover_state); + + let mut output_claims = verifier_state.next_extension_scalars_vec(16).unwrap(); + + let mut claim_point = output_claim_point.clone(); + for full_round in [&ful_round_2, &ful_round_1] { + (claim_point, output_claims) = verify_full_round( + &mut verifier_state, + full_round, + log_n_poseidons, + &claim_point, + &output_claims, + ); + } + + for i in 0..16 { + assert_eq!( + output_claims[i], + multilvariate_eval(&input_layers[i], &claim_point) + ); + } + } +} + +fn apply_full_round(input_layers: &[Vec], ful_round: &FullRoundComputation) -> [Vec; 16] { + let mut output_layers: [_; 16] = array::from_fn(|_| F::zero_vec(input_layers[0].len())); transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { let intermediate: [F; 16] = - array::from_fn(|j| input_layers[j][row_index].cube() + ful_round.constants[j]); + array::from_fn(|j| (input_layers[j][row_index] + ful_round.constants[j]).cube()); output_row.into_iter().enumerate().for_each(|(j, output)| { let mut res = F::ZERO; for k in 0..16 { @@ -42,51 +107,7 @@ fn main() { *output = res; }); }); - - let claim_point = MultilinearPoint( - (0..(log_n_poseidons + 1 - UNIVARIATE_SKIPS)) - .map(|_| rng.random()) - .collect::>(), - ); - - let output_claims = output_layers - .par_iter() - .map(|output_layer| multilvariate_eval(output_layer, &claim_point)) - .collect::>(); - - let mut prover_state = build_prover_state::(); - prover_state.add_extension_scalars(&output_claims); - - prove_full_round( - &mut prover_state, - &ful_round, - n_poseidons, - &input_layers, - &output_layers, - &claim_point, - &output_claims, - ); - - // ---------------------------------------------------- VERIFIER ---------------------------------------------------- - - let mut verifier_state = build_verifier_state(&prover_state); - - let output_claims = verifier_state.next_extension_scalars_vec(16).unwrap(); - - let (sumcheck_point, sumcheck_inner_evals) = verify_full_round( - &mut verifier_state, - &ful_round, - log_n_poseidons, - &claim_point, - &output_claims, - ); - - for i in 0..16 { - assert_eq!( - sumcheck_inner_evals[i], - multilvariate_eval(&input_layers[i], &sumcheck_point) - ); - } + output_layers } fn prove_full_round( @@ -197,7 +218,7 @@ impl, EF: ExtensionField> SumcheckComputation fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { assert_eq!(point.len(), 16); - let intermediate: [NF; 16] = array::from_fn(|j| point[j].cube() + self.constants[j]); + let intermediate: [NF; 16] = array::from_fn(|j| (point[j] + self.constants[j]).cube()); let mut res = EF::ZERO; for j in 0..16 { let mut temp = NF::ZERO; @@ -218,7 +239,7 @@ impl SumcheckComputationPacked for FullRoundComputation { fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { assert_eq!(point.len(), 16); let intermediate: [PFPacking; 16] = - array::from_fn(|j| point[j].cube() + self.constants[j]); + array::from_fn(|j| (point[j] + self.constants[j]).cube()); let mut res = EFPacking::::ZERO; for j in 0..16 { let mut temp = PFPacking::::ZERO; @@ -233,7 +254,7 @@ impl SumcheckComputationPacked for FullRoundComputation { fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { assert_eq!(point.len(), 16); let intermediate: [EFPacking; 16] = - array::from_fn(|j| point[j].cube() + self.constants[j]); + array::from_fn(|j| (point[j] + self.constants[j]).cube()); let mut res = EFPacking::::ZERO; for j in 0..16 { let mut temp = EFPacking::::ZERO; From 20c968720b9bae2dae12c035da2bc9097d58b328 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 21 Oct 2025 17:13:26 +0200 Subject: [PATCH 05/42] partial rounds --- src/main.rs | 168 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 137 insertions(+), 31 deletions(-) diff --git a/src/main.rs b/src/main.rs index eebb49dd..ebaeb695 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,10 @@ fn main() { let input_layers: [_; 16] = array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); + let partial_round_1 = PartialRoundComputation { + constant: rng.random(), + diag: rng.random(), + }; let ful_round_1 = FullRoundComputation { constants: rng.random(), matrix: rng.random(), @@ -38,10 +42,11 @@ fn main() { { // ---------------------------------------------------- PROVER ---------------------------------------------------- - let output_layers_1 = apply_full_round(&input_layers, &ful_round_1); - let output_layers_2 = apply_full_round(&output_layers_1, &ful_round_2); + let partial_output_layer_1 = apply_partial_round(&input_layers, &partial_round_1); + let full_output_layers_1 = apply_full_round(&partial_output_layer_1, &ful_round_1); + let full_output_layers_2 = apply_full_round(&full_output_layers_1, &ful_round_2); - let mut output_claims = output_layers_2 + let mut output_claims = full_output_layers_2 .par_iter() .map(|output_layer| multilvariate_eval(output_layer, &output_claim_point)) .collect::>(); @@ -50,19 +55,27 @@ fn main() { let mut claim_point = output_claim_point.clone(); for (input_layers, output_layers, full_round) in [ - (&output_layers_1, &output_layers_2, &ful_round_2), - (&input_layers, &output_layers_1, &ful_round_1), + (&full_output_layers_1, &full_output_layers_2, &ful_round_2), + (&partial_output_layer_1, &full_output_layers_1, &ful_round_1), ] { - (claim_point, output_claims) = prove_full_round( + (claim_point, output_claims) = prove_gkr_round( &mut prover_state, full_round, - n_poseidons, input_layers, output_layers, &claim_point, &output_claims, ); } + + prove_gkr_round( + &mut prover_state, + &partial_round_1, + &input_layers, + &partial_output_layer_1, + &claim_point, + &output_claims, + ); } { @@ -74,7 +87,7 @@ fn main() { let mut claim_point = output_claim_point.clone(); for full_round in [&ful_round_2, &ful_round_1] { - (claim_point, output_claims) = verify_full_round( + (claim_point, output_claims) = verify_gkr_round( &mut verifier_state, full_round, log_n_poseidons, @@ -83,6 +96,14 @@ fn main() { ); } + (claim_point, output_claims) = verify_gkr_round( + &mut verifier_state, + &partial_round_1, + log_n_poseidons, + &claim_point, + &output_claims, + ); + for i in 0..16 { assert_eq!( output_claims[i], @@ -110,10 +131,37 @@ fn apply_full_round(input_layers: &[Vec], ful_round: &FullRoundComputation) - output_layers } -fn prove_full_round( +fn apply_partial_round( + input_layers: &[Vec], + partial_round: &PartialRoundComputation, +) -> [Vec; 16] { + let mut output_layers: [_; 16] = array::from_fn(|_| F::zero_vec(input_layers[0].len())); + transposed_par_iter_mut(&mut output_layers) + .enumerate() + .for_each(|(row_index, output_row)| { + let first_cubed = (input_layers[0][row_index] + partial_round.constant).cube(); + let sum: F = first_cubed + + input_layers + .iter() + .skip(1) + .map(|layer| layer[row_index]) + .sum::(); + *output_row[0] = sum + first_cubed * partial_round.diag[0]; + for j in 1..16 { + *output_row[j] = sum + input_layers[j][row_index] * partial_round.diag[j]; + } + }); + output_layers +} + +fn prove_gkr_round< + SC: SumcheckComputation + + SumcheckComputation + + SumcheckComputationPacked + + 'static, +>( prover_state: &mut FSProver>, - ful_round: &FullRoundComputation, - n_poseidons: usize, + computation: &SC, input_layers: &[Vec], output_layers: &[Vec], claim_point: &[EF], @@ -122,17 +170,9 @@ fn prove_full_round( let batching_scalar = prover_state.sample(); let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); let batching_scalars_powers = batching_scalar.powers().collect_n(16); - let batched_output_layer = (0..n_poseidons) - .into_par_iter() - .map(|i| { - dot_product( - batching_scalars_powers.iter().copied(), - (0..16).map(|j| output_layers[j][i]), - ) - }) - .collect::>(); + let batched_output_layer = batch_layer(output_layers, &batching_scalars_powers); - assert_eq!( + debug_assert_eq!( batched_claim, multilvariate_eval(&batched_output_layer, &claim_point) ); @@ -140,8 +180,8 @@ fn prove_full_round( let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( UNIVARIATE_SKIPS, MleGroupRef::Base(input_layers.iter().map(Vec::as_slice).collect()), - ful_round, - ful_round, + computation, + computation, &batching_scalars_powers, Some((claim_point.to_vec(), None)), false, @@ -151,8 +191,8 @@ fn prove_full_round( ); // sanity check - assert_eq!( - ful_round.eval(&sumcheck_inner_evals, &batching_scalars_powers) + debug_assert_eq!( + computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) * eq_poly_with_skip(&sumcheck_point, &claim_point, UNIVARIATE_SKIPS), sumcheck_final_sum ); @@ -162,9 +202,9 @@ fn prove_full_round( (sumcheck_point.0, sumcheck_inner_evals) } -fn verify_full_round( +fn verify_gkr_round>( verifier_state: &mut FSVerifier>, - ful_round: &FullRoundComputation, + computation: &SC, log_n_poseidons: usize, claim_point: &[EF], output_claims: &[EF], @@ -181,7 +221,7 @@ fn verify_full_round( let sumcheck_inner_evals = verifier_state.next_extension_scalars_vec(16).unwrap(); assert_eq!( - ful_round.eval(&sumcheck_inner_evals, &batching_scalars_powers) + computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) * eq_poly_with_skip( &sumcheck_postponed_claim.point, &claim_point, @@ -204,6 +244,20 @@ fn multilvariate_eval>(poly: &[F], point: &[EF]) .sum() } +fn batch_layer(layers: &[Vec], batching_scalars_powers: &[EF]) -> Vec { + let n_layers = layers.len(); + let height = layers[0].len(); + (0..height) + .into_par_iter() + .map(|i| { + dot_product( + batching_scalars_powers.iter().copied(), + (0..n_layers).map(|j| layers[j][i]), + ) + }) + .collect::>() +} + pub struct FullRoundComputation { pub constants: [F; 16], pub matrix: [[F; 16]; 16], @@ -217,7 +271,7 @@ impl, EF: ExtensionField> SumcheckComputation } fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { - assert_eq!(point.len(), 16); + debug_assert_eq!(point.len(), 16); let intermediate: [NF; 16] = array::from_fn(|j| (point[j] + self.constants[j]).cube()); let mut res = EF::ZERO; for j in 0..16 { @@ -237,7 +291,7 @@ impl SumcheckComputationPacked for FullRoundComputation { } fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { - assert_eq!(point.len(), 16); + debug_assert_eq!(point.len(), 16); let intermediate: [PFPacking; 16] = array::from_fn(|j| (point[j] + self.constants[j]).cube()); let mut res = EFPacking::::ZERO; @@ -252,7 +306,7 @@ impl SumcheckComputationPacked for FullRoundComputation { } fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { - assert_eq!(point.len(), 16); + debug_assert_eq!(point.len(), 16); let intermediate: [EFPacking; 16] = array::from_fn(|j| (point[j] + self.constants[j]).cube()); let mut res = EFPacking::::ZERO; @@ -266,3 +320,55 @@ impl SumcheckComputationPacked for FullRoundComputation { res } } + +pub struct PartialRoundComputation { + pub constant: F, + pub diag: [F; 16], +} + +impl, EF: ExtensionField> SumcheckComputation + for PartialRoundComputation +{ + fn degree(&self) -> usize { + 3 + } + + fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { + debug_assert_eq!(point.len(), 16); + let first_cubed = (point[0] + self.constant).cube(); + let sum = first_cubed + point[1..].iter().copied().sum::(); + let mut res = alpha_powers[0] * (sum + first_cubed * self.diag[0]); + for j in 1..16 { + res += alpha_powers[j] * (sum + point[j] * self.diag[j]); + } + res + } +} + +impl SumcheckComputationPacked for PartialRoundComputation { + fn degree(&self) -> usize { + 3 + } + + fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { + debug_assert_eq!(point.len(), 16); + let first_cubed = (point[0] + self.constant).cube(); + let sum = first_cubed + point[1..].iter().copied().sum::>(); + let mut res = EFPacking::::from(alpha_powers[0]) * (sum + first_cubed * self.diag[0]); + for j in 1..16 { + res += EFPacking::::from(alpha_powers[j]) * (sum + point[j] * self.diag[j]); + } + res + } + + fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { + debug_assert_eq!(point.len(), 16); + let first_cubed = (point[0] + self.constant).cube(); + let sum = first_cubed + point[1..].iter().copied().sum::>(); + let mut res = EFPacking::::from(alpha_powers[0]) * (sum + first_cubed * self.diag[0]); + for j in 1..16 { + res += EFPacking::::from(alpha_powers[j]) * (sum + point[j] * self.diag[j]); + } + res + } +} From c3198b1d79c90ffdb2a33742d362241d494d20c5 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 21 Oct 2025 17:21:00 +0200 Subject: [PATCH 06/42] wip --- src/main.rs | 100 ++++++++++++++++++++++++++-------------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/src/main.rs b/src/main.rs index ebaeb695..16db7a16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ #![cfg_attr(not(test), allow(unused_crate_dependencies))] use multilinear_toolkit::prelude::*; -use p3_koala_bear::{KoalaBear, QuinticExtensionFieldKB}; +use p3_koala_bear::{GenericPoseidon2LinearLayersKoalaBear, KoalaBear, QuinticExtensionFieldKB}; +use p3_poseidon2::GenericPoseidon2LinearLayers; use rand::{Rng, SeedableRng, rngs::StdRng}; use std::array; use utils::{build_prover_state, build_verifier_state, transposed_par_iter_mut}; @@ -22,15 +23,12 @@ fn main() { let partial_round_1 = PartialRoundComputation { constant: rng.random(), - diag: rng.random(), }; let ful_round_1 = FullRoundComputation { constants: rng.random(), - matrix: rng.random(), }; let ful_round_2 = FullRoundComputation { constants: rng.random(), - matrix: rng.random(), }; let output_claim_point = (0..(log_n_poseidons + 1 - UNIVARIATE_SKIPS)) @@ -111,6 +109,8 @@ fn main() { ); } } + + println!("GKR proof for Poseidon2 permutation successful!"); } fn apply_full_round(input_layers: &[Vec], ful_round: &FullRoundComputation) -> [Vec; 16] { @@ -118,15 +118,12 @@ fn apply_full_round(input_layers: &[Vec], ful_round: &FullRoundComputation) - transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { - let intermediate: [F; 16] = + let mut intermediate: [F; 16] = array::from_fn(|j| (input_layers[j][row_index] + ful_round.constants[j]).cube()); - output_row.into_iter().enumerate().for_each(|(j, output)| { - let mut res = F::ZERO; - for k in 0..16 { - res += ful_round.matrix[j][k] * intermediate[k]; - } - *output = res; - }); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + for j in 0..16 { + *output_row[j] = intermediate[j]; + } }); output_layers } @@ -140,15 +137,14 @@ fn apply_partial_round( .enumerate() .for_each(|(row_index, output_row)| { let first_cubed = (input_layers[0][row_index] + partial_round.constant).cube(); - let sum: F = first_cubed - + input_layers - .iter() - .skip(1) - .map(|layer| layer[row_index]) - .sum::(); - *output_row[0] = sum + first_cubed * partial_round.diag[0]; + let mut intermediate = [F::ZERO; 16]; + intermediate[0] = first_cubed; for j in 1..16 { - *output_row[j] = sum + input_layers[j][row_index] * partial_round.diag[j]; + intermediate[j] = input_layers[j][row_index]; + } + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + for j in 0..16 { + *output_row[j] = intermediate[j]; } }); output_layers @@ -260,7 +256,6 @@ fn batch_layer(layers: &[Vec], batching_scalars_powers: &[EF]) -> Vec { pub struct FullRoundComputation { pub constants: [F; 16], - pub matrix: [[F; 16]; 16], } impl, EF: ExtensionField> SumcheckComputation @@ -272,14 +267,11 @@ impl, EF: ExtensionField> SumcheckComputation fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { debug_assert_eq!(point.len(), 16); - let intermediate: [NF; 16] = array::from_fn(|j| (point[j] + self.constants[j]).cube()); + let mut intermediate: [NF; 16] = array::from_fn(|j| (point[j] + self.constants[j]).cube()); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); let mut res = EF::ZERO; - for j in 0..16 { - let mut temp = NF::ZERO; - for k in 0..16 { - temp += intermediate[k] * self.matrix[j][k]; - } - res += alpha_powers[j] * temp; + for i in 0..16 { + res += alpha_powers[i] * intermediate[i]; } res } @@ -292,30 +284,24 @@ impl SumcheckComputationPacked for FullRoundComputation { fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let intermediate: [PFPacking; 16] = + let mut intermediate: [PFPacking; 16] = array::from_fn(|j| (point[j] + self.constants[j]).cube()); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { - let mut temp = PFPacking::::ZERO; - for k in 0..16 { - temp += intermediate[k] * self.matrix[j][k]; - } - res += alpha_powers[j] * temp; + res += EFPacking::::from(alpha_powers[j]) * intermediate[j]; } res } fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let intermediate: [EFPacking; 16] = + let mut intermediate: [EFPacking; 16] = array::from_fn(|j| (point[j] + self.constants[j]).cube()); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { - let mut temp = EFPacking::::ZERO; - for k in 0..16 { - temp += intermediate[k] * self.matrix[j][k]; - } - res += temp * alpha_powers[j]; + res += intermediate[j] * alpha_powers[j]; } res } @@ -323,7 +309,6 @@ impl SumcheckComputationPacked for FullRoundComputation { pub struct PartialRoundComputation { pub constant: F, - pub diag: [F; 16], } impl, EF: ExtensionField> SumcheckComputation @@ -336,10 +321,15 @@ impl, EF: ExtensionField> SumcheckComputation fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { debug_assert_eq!(point.len(), 16); let first_cubed = (point[0] + self.constant).cube(); - let sum = first_cubed + point[1..].iter().copied().sum::(); - let mut res = alpha_powers[0] * (sum + first_cubed * self.diag[0]); + let mut intermediate = [NF::ZERO; 16]; + intermediate[0] = first_cubed; for j in 1..16 { - res += alpha_powers[j] * (sum + point[j] * self.diag[j]); + intermediate[j] = point[j]; + } + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + let mut res = EF::ZERO; + for i in 0..16 { + res += alpha_powers[i] * intermediate[i]; } res } @@ -353,10 +343,15 @@ impl SumcheckComputationPacked for PartialRoundComputation { fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); let first_cubed = (point[0] + self.constant).cube(); - let sum = first_cubed + point[1..].iter().copied().sum::>(); - let mut res = EFPacking::::from(alpha_powers[0]) * (sum + first_cubed * self.diag[0]); + let mut intermediate = [PFPacking::::ZERO; 16]; + intermediate[0] = first_cubed; for j in 1..16 { - res += EFPacking::::from(alpha_powers[j]) * (sum + point[j] * self.diag[j]); + intermediate[j] = point[j]; + } + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + let mut res = EFPacking::::ZERO; + for j in 0..16 { + res += EFPacking::::from(alpha_powers[j]) * intermediate[j]; } res } @@ -364,10 +359,15 @@ impl SumcheckComputationPacked for PartialRoundComputation { fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); let first_cubed = (point[0] + self.constant).cube(); - let sum = first_cubed + point[1..].iter().copied().sum::>(); - let mut res = EFPacking::::from(alpha_powers[0]) * (sum + first_cubed * self.diag[0]); + let mut intermediate = [EFPacking::::ZERO; 16]; + intermediate[0] = first_cubed; for j in 1..16 { - res += EFPacking::::from(alpha_powers[j]) * (sum + point[j] * self.diag[j]); + intermediate[j] = point[j]; + } + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + let mut res = EFPacking::::ZERO; + for j in 0..16 { + res += intermediate[j] * alpha_powers[j]; } res } From 583fd7294d28d189b60ac8c5f40fadd43a0cf24c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 21 Oct 2025 17:37:52 +0200 Subject: [PATCH 07/42] real constants --- src/main.rs | 129 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 35 deletions(-) diff --git a/src/main.rs b/src/main.rs index 16db7a16..0b15182e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,10 @@ #![cfg_attr(not(test), allow(unused_crate_dependencies))] use multilinear_toolkit::prelude::*; -use p3_koala_bear::{GenericPoseidon2LinearLayersKoalaBear, KoalaBear, QuinticExtensionFieldKB}; +use p3_koala_bear::{ + GenericPoseidon2LinearLayersKoalaBear, KOALABEAR_RC16_EXTERNAL_FINAL, + KOALABEAR_RC16_EXTERNAL_INITIAL, KOALABEAR_RC16_INTERNAL, KoalaBear, QuinticExtensionFieldKB, +}; use p3_poseidon2::GenericPoseidon2LinearLayers; use rand::{Rng, SeedableRng, rngs::StdRng}; use std::array; @@ -21,15 +24,22 @@ fn main() { let input_layers: [_; 16] = array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); - let partial_round_1 = PartialRoundComputation { - constant: rng.random(), - }; - let ful_round_1 = FullRoundComputation { - constants: rng.random(), - }; - let ful_round_2 = FullRoundComputation { - constants: rng.random(), - }; + let initial_full_rounds = KOALABEAR_RC16_EXTERNAL_INITIAL + .into_iter() + .map(|constants| FullRoundComputation { constants }) + .collect::>(); + let mid_partial_rounds = KOALABEAR_RC16_INTERNAL + .into_iter() + .map(|constant| PartialRoundComputation { constant }) + .collect::>(); + let final_full_rounds = KOALABEAR_RC16_EXTERNAL_FINAL + .into_iter() + .map(|constants| FullRoundComputation { constants }) + .collect::>(); + + let n_initial_full_rounds = initial_full_rounds.len(); + let n_mid_partial_rounds = mid_partial_rounds.len(); + let n_final_full_rounds = final_full_rounds.len(); let output_claim_point = (0..(log_n_poseidons + 1 - UNIVARIATE_SKIPS)) .map(|_| rng.random()) @@ -40,11 +50,24 @@ fn main() { { // ---------------------------------------------------- PROVER ---------------------------------------------------- - let partial_output_layer_1 = apply_partial_round(&input_layers, &partial_round_1); - let full_output_layers_1 = apply_full_round(&partial_output_layer_1, &ful_round_1); - let full_output_layers_2 = apply_full_round(&full_output_layers_1, &ful_round_2); - - let mut output_claims = full_output_layers_2 + let mut all_layers = vec![input_layers.clone()]; + for round in &initial_full_rounds { + all_layers.push(apply_full_round(all_layers.last().unwrap(), round)); + } + for round in &mid_partial_rounds { + all_layers.push(apply_partial_round(all_layers.last().unwrap(), round)); + } + for round in &final_full_rounds { + all_layers.push(apply_full_round(all_layers.last().unwrap(), round)); + } + let initial_full_layers = &all_layers[..n_initial_full_rounds + 1]; + let mid_partial_layers = + &all_layers[n_initial_full_rounds..n_initial_full_rounds + n_mid_partial_rounds + 1]; + let final_full_layers = &all_layers[all_layers.len() - n_final_full_rounds - 1..]; + + let mut output_claims = all_layers + .last() + .unwrap() .par_iter() .map(|output_layer| multilvariate_eval(output_layer, &output_claim_point)) .collect::>(); @@ -52,10 +75,11 @@ fn main() { prover_state.add_extension_scalars(&output_claims); let mut claim_point = output_claim_point.clone(); - for (input_layers, output_layers, full_round) in [ - (&full_output_layers_1, &full_output_layers_2, &ful_round_2), - (&partial_output_layer_1, &full_output_layers_1, &ful_round_1), - ] { + for (input_and_output_layers, full_round) in + final_full_layers.windows(2).zip(&final_full_rounds).rev() + { + let (input_layers, output_layers) = + (&input_and_output_layers[0], &input_and_output_layers[1]); (claim_point, output_claims) = prove_gkr_round( &mut prover_state, full_round, @@ -66,14 +90,37 @@ fn main() { ); } - prove_gkr_round( - &mut prover_state, - &partial_round_1, - &input_layers, - &partial_output_layer_1, - &claim_point, - &output_claims, - ); + for (input_and_output_layers, partial_round) in + mid_partial_layers.windows(2).zip(&mid_partial_rounds).rev() + { + let (input_layers, output_layers) = + (&input_and_output_layers[0], &input_and_output_layers[1]); + (claim_point, output_claims) = prove_gkr_round( + &mut prover_state, + partial_round, + input_layers, + output_layers, + &claim_point, + &output_claims, + ); + } + + for (input_and_output_layers, full_round) in initial_full_layers + .windows(2) + .zip(&initial_full_rounds) + .rev() + { + let (input_layers, output_layers) = + (&input_and_output_layers[0], &input_and_output_layers[1]); + (claim_point, output_claims) = prove_gkr_round( + &mut prover_state, + full_round, + input_layers, + output_layers, + &claim_point, + &output_claims, + ); + } } { @@ -84,7 +131,7 @@ fn main() { let mut output_claims = verifier_state.next_extension_scalars_vec(16).unwrap(); let mut claim_point = output_claim_point.clone(); - for full_round in [&ful_round_2, &ful_round_1] { + for full_round in final_full_rounds.iter().rev() { (claim_point, output_claims) = verify_gkr_round( &mut verifier_state, full_round, @@ -94,13 +141,25 @@ fn main() { ); } - (claim_point, output_claims) = verify_gkr_round( - &mut verifier_state, - &partial_round_1, - log_n_poseidons, - &claim_point, - &output_claims, - ); + for partial_round in mid_partial_rounds.iter().rev() { + (claim_point, output_claims) = verify_gkr_round( + &mut verifier_state, + partial_round, + log_n_poseidons, + &claim_point, + &output_claims, + ); + } + + for full_round in initial_full_rounds.iter().rev() { + (claim_point, output_claims) = verify_gkr_round( + &mut verifier_state, + full_round, + log_n_poseidons, + &claim_point, + &output_claims, + ); + } for i in 0..16 { assert_eq!( From 4baa7a810962ce62eca4ab52e448f43e075ff415 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 21 Oct 2025 17:47:42 +0200 Subject: [PATCH 08/42] stats --- Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 27 +++++++++++++++++++++------ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10bd28f6..7b2a1e3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,6 +383,7 @@ dependencies = [ "packed_pcs", "rand", "rec_aggregation", + "tracing", "utils", "whir-p3", ] diff --git a/Cargo.toml b/Cargo.toml index f5785a71..4d646992 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ p3-uni-stark.workspace = true utils.workspace = true p3-util.workspace = true packed_pcs.workspace = true +tracing.workspace = true p3-air.workspace = true multilinear-toolkit.workspace = true diff --git a/src/main.rs b/src/main.rs index 0b15182e..b14281f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,16 +7,19 @@ use p3_koala_bear::{ }; use p3_poseidon2::GenericPoseidon2LinearLayers; use rand::{Rng, SeedableRng, rngs::StdRng}; -use std::array; -use utils::{build_prover_state, build_verifier_state, transposed_par_iter_mut}; +use std::{array, time::Instant}; +use tracing::instrument; +use utils::{build_prover_state, build_verifier_state, init_tracing, transposed_par_iter_mut}; type F = KoalaBear; type EF = QuinticExtensionFieldKB; const UNIVARIATE_SKIPS: usize = 3; fn main() { + init_tracing(); + let mut rng = StdRng::seed_from_u64(0); - let log_n_poseidons = 11; + let log_n_poseidons = 17; let n_poseidons = 1 << log_n_poseidons; let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) @@ -47,6 +50,7 @@ fn main() { let mut prover_state = build_prover_state::(); + let prover_time = Instant::now(); { // ---------------------------------------------------- PROVER ---------------------------------------------------- @@ -122,7 +126,9 @@ fn main() { ); } } + let prover_duration = prover_time.elapsed(); + let verifier_time = Instant::now(); { // ---------------------------------------------------- VERIFIER ---------------------------------------------------- @@ -168,10 +174,18 @@ fn main() { ); } } + let verifier_duration = verifier_time.elapsed(); - println!("GKR proof for Poseidon2 permutation successful!"); + println!("GKR proof for {} Poseidon2:", n_poseidons); + println!( + "Prover time: {:?} ({:.1} Poseidons / s)", + prover_duration, + n_poseidons as f64 / prover_duration.as_secs_f64() + ); + println!("Verifier time: {:?}", verifier_duration); } +#[instrument(skip_all)] fn apply_full_round(input_layers: &[Vec], ful_round: &FullRoundComputation) -> [Vec; 16] { let mut output_layers: [_; 16] = array::from_fn(|_| F::zero_vec(input_layers[0].len())); transposed_par_iter_mut(&mut output_layers) @@ -187,6 +201,7 @@ fn apply_full_round(input_layers: &[Vec], ful_round: &FullRoundComputation) - output_layers } +#[instrument(skip_all)] fn apply_partial_round( input_layers: &[Vec], partial_round: &PartialRoundComputation, @@ -356,7 +371,7 @@ impl SumcheckComputationPacked for FullRoundComputation { fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); let mut intermediate: [EFPacking; 16] = - array::from_fn(|j| (point[j] + self.constants[j]).cube()); + array::from_fn(|j| (point[j] + PFPacking::::from(self.constants[j])).cube()); GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { @@ -417,7 +432,7 @@ impl SumcheckComputationPacked for PartialRoundComputation { fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let first_cubed = (point[0] + self.constant).cube(); + let first_cubed = (point[0] + PFPacking::::from(self.constant)).cube(); let mut intermediate = [EFPacking::::ZERO; 16]; intermediate[0] = first_cubed; for j in 1..16 { From bdd2f93f59fefaa992ca465790ef24bf45e67449 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 21 Oct 2025 18:05:51 +0200 Subject: [PATCH 09/42] 590K poseidons/s --- src/main.rs | 88 ++++++++++++++++++++--------------------------------- 1 file changed, 33 insertions(+), 55 deletions(-) diff --git a/src/main.rs b/src/main.rs index b14281f6..480dd8e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,9 +17,9 @@ const UNIVARIATE_SKIPS: usize = 3; fn main() { init_tracing(); - + let mut rng = StdRng::seed_from_u64(0); - let log_n_poseidons = 17; + let log_n_poseidons = 19; let n_poseidons = 1 << log_n_poseidons; let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) @@ -54,7 +54,9 @@ fn main() { { // ---------------------------------------------------- PROVER ---------------------------------------------------- - let mut all_layers = vec![input_layers.clone()]; + let input_layers_packed: [_; 16] = + array::from_fn(|i| PFPacking::::pack_slice(&input_layers[i]).to_vec()); + let mut all_layers = vec![input_layers_packed]; for round in &initial_full_rounds { all_layers.push(apply_full_round(all_layers.last().unwrap(), round)); } @@ -64,63 +66,55 @@ fn main() { for round in &final_full_rounds { all_layers.push(apply_full_round(all_layers.last().unwrap(), round)); } - let initial_full_layers = &all_layers[..n_initial_full_rounds + 1]; + let initial_full_layers = &all_layers[..n_initial_full_rounds]; let mid_partial_layers = - &all_layers[n_initial_full_rounds..n_initial_full_rounds + n_mid_partial_rounds + 1]; - let final_full_layers = &all_layers[all_layers.len() - n_final_full_rounds - 1..]; + &all_layers[n_initial_full_rounds..n_initial_full_rounds + n_mid_partial_rounds]; + let final_full_layers = &all_layers[n_initial_full_rounds + n_mid_partial_rounds + ..n_initial_full_rounds + n_mid_partial_rounds + n_final_full_rounds]; let mut output_claims = all_layers .last() .unwrap() .par_iter() - .map(|output_layer| multilvariate_eval(output_layer, &output_claim_point)) + .map(|output_layer| { + multilvariate_eval( + PFPacking::::unpack_slice(&output_layer), + &output_claim_point, + ) + }) .collect::>(); prover_state.add_extension_scalars(&output_claims); let mut claim_point = output_claim_point.clone(); - for (input_and_output_layers, full_round) in - final_full_layers.windows(2).zip(&final_full_rounds).rev() - { - let (input_layers, output_layers) = - (&input_and_output_layers[0], &input_and_output_layers[1]); + for (input_layers, full_round) in final_full_layers.iter().zip(&final_full_rounds).rev() { (claim_point, output_claims) = prove_gkr_round( &mut prover_state, full_round, input_layers, - output_layers, &claim_point, &output_claims, ); } - for (input_and_output_layers, partial_round) in - mid_partial_layers.windows(2).zip(&mid_partial_rounds).rev() + for (input_layers, partial_round) in + mid_partial_layers.iter().zip(&mid_partial_rounds).rev() { - let (input_layers, output_layers) = - (&input_and_output_layers[0], &input_and_output_layers[1]); (claim_point, output_claims) = prove_gkr_round( &mut prover_state, partial_round, input_layers, - output_layers, &claim_point, &output_claims, ); } - for (input_and_output_layers, full_round) in initial_full_layers - .windows(2) - .zip(&initial_full_rounds) - .rev() + for (input_layers, full_round) in initial_full_layers.iter().zip(&initial_full_rounds).rev() { - let (input_layers, output_layers) = - (&input_and_output_layers[0], &input_and_output_layers[1]); (claim_point, output_claims) = prove_gkr_round( &mut prover_state, full_round, input_layers, - output_layers, &claim_point, &output_claims, ); @@ -186,12 +180,16 @@ fn main() { } #[instrument(skip_all)] -fn apply_full_round(input_layers: &[Vec], ful_round: &FullRoundComputation) -> [Vec; 16] { - let mut output_layers: [_; 16] = array::from_fn(|_| F::zero_vec(input_layers[0].len())); +fn apply_full_round( + input_layers: &[Vec>], + ful_round: &FullRoundComputation, +) -> [Vec>; 16] { + let mut output_layers: [_; 16] = + array::from_fn(|_| PFPacking::::zero_vec(input_layers[0].len())); transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { - let mut intermediate: [F; 16] = + let mut intermediate: [PFPacking; 16] = array::from_fn(|j| (input_layers[j][row_index] + ful_round.constants[j]).cube()); GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); for j in 0..16 { @@ -203,15 +201,16 @@ fn apply_full_round(input_layers: &[Vec], ful_round: &FullRoundComputation) - #[instrument(skip_all)] fn apply_partial_round( - input_layers: &[Vec], + input_layers: &[Vec>], partial_round: &PartialRoundComputation, -) -> [Vec; 16] { - let mut output_layers: [_; 16] = array::from_fn(|_| F::zero_vec(input_layers[0].len())); +) -> [Vec>; 16] { + let mut output_layers: [_; 16] = + array::from_fn(|_| PFPacking::::zero_vec(input_layers[0].len())); transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { let first_cubed = (input_layers[0][row_index] + partial_round.constant).cube(); - let mut intermediate = [F::ZERO; 16]; + let mut intermediate = [PFPacking::::ZERO; 16]; intermediate[0] = first_cubed; for j in 1..16 { intermediate[j] = input_layers[j][row_index]; @@ -232,24 +231,17 @@ fn prove_gkr_round< >( prover_state: &mut FSProver>, computation: &SC, - input_layers: &[Vec], - output_layers: &[Vec], + input_layers: &[Vec>], claim_point: &[EF], output_claims: &[EF], ) -> (Vec, Vec) { let batching_scalar = prover_state.sample(); let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); let batching_scalars_powers = batching_scalar.powers().collect_n(16); - let batched_output_layer = batch_layer(output_layers, &batching_scalars_powers); - - debug_assert_eq!( - batched_claim, - multilvariate_eval(&batched_output_layer, &claim_point) - ); let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( UNIVARIATE_SKIPS, - MleGroupRef::Base(input_layers.iter().map(Vec::as_slice).collect()), + MleGroupRef::BasePacked(input_layers.iter().map(Vec::as_slice).collect()), computation, computation, &batching_scalars_powers, @@ -314,20 +306,6 @@ fn multilvariate_eval>(poly: &[F], point: &[EF]) .sum() } -fn batch_layer(layers: &[Vec], batching_scalars_powers: &[EF]) -> Vec { - let n_layers = layers.len(); - let height = layers[0].len(); - (0..height) - .into_par_iter() - .map(|i| { - dot_product( - batching_scalars_powers.iter().copied(), - (0..n_layers).map(|j| layers[j][i]), - ) - }) - .collect::>() -} - pub struct FullRoundComputation { pub constants: [F; 16], } From a55ac64f536b874336778f610da1cbf63a721f4f Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 21 Oct 2025 23:44:10 +0200 Subject: [PATCH 10/42] fix (still wip) --- src/main.rs | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 480dd8e3..0cd498d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,10 @@ use p3_poseidon2::GenericPoseidon2LinearLayers; use rand::{Rng, SeedableRng, rngs::StdRng}; use std::{array, time::Instant}; use tracing::instrument; -use utils::{build_prover_state, build_verifier_state, init_tracing, transposed_par_iter_mut}; +use utils::{ + build_prover_state, build_verifier_state, init_tracing, poseidon16_permute, + transposed_par_iter_mut, +}; type F = KoalaBear; type EF = QuinticExtensionFieldKB; @@ -24,6 +27,10 @@ fn main() { let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) .collect::>(); + let perm_outputs = perm_inputs + .par_iter() + .map(|input| poseidon16_permute(*input)) + .collect::>(); let input_layers: [_; 16] = array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); @@ -54,6 +61,7 @@ fn main() { { // ---------------------------------------------------- PROVER ---------------------------------------------------- + let input_layers_packed: [_; 16] = array::from_fn(|i| PFPacking::::pack_slice(&input_layers[i]).to_vec()); let mut all_layers = vec![input_layers_packed]; @@ -72,6 +80,17 @@ fn main() { let final_full_layers = &all_layers[n_initial_full_rounds + n_mid_partial_rounds ..n_initial_full_rounds + n_mid_partial_rounds + n_final_full_rounds]; + { + // sanity check + // let last_layers: [_; 16] = + // array::from_fn(|i| PFPacking::::unpack_slice(&all_layers.last().unwrap()[i])); + // (0..n_poseidons).into_par_iter().for_each(|row| { + // for i in 0..16 { + // assert_eq!(perm_outputs[row][i], last_layers[i][row]); + // } + // }); + } + let mut output_claims = all_layers .last() .unwrap() @@ -191,7 +210,7 @@ fn apply_full_round( .for_each(|(row_index, output_row)| { let mut intermediate: [PFPacking; 16] = array::from_fn(|j| (input_layers[j][row_index] + ful_round.constants[j]).cube()); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); for j in 0..16 { *output_row[j] = intermediate[j]; } @@ -215,7 +234,7 @@ fn apply_partial_round( for j in 1..16 { intermediate[j] = input_layers[j][row_index]; } - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); for j in 0..16 { *output_row[j] = intermediate[j]; } @@ -320,7 +339,7 @@ impl, EF: ExtensionField> SumcheckComputation fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { debug_assert_eq!(point.len(), 16); let mut intermediate: [NF; 16] = array::from_fn(|j| (point[j] + self.constants[j]).cube()); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); let mut res = EF::ZERO; for i in 0..16 { res += alpha_powers[i] * intermediate[i]; @@ -338,7 +357,7 @@ impl SumcheckComputationPacked for FullRoundComputation { debug_assert_eq!(point.len(), 16); let mut intermediate: [PFPacking; 16] = array::from_fn(|j| (point[j] + self.constants[j]).cube()); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { res += EFPacking::::from(alpha_powers[j]) * intermediate[j]; @@ -350,7 +369,7 @@ impl SumcheckComputationPacked for FullRoundComputation { debug_assert_eq!(point.len(), 16); let mut intermediate: [EFPacking; 16] = array::from_fn(|j| (point[j] + PFPacking::::from(self.constants[j])).cube()); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { res += intermediate[j] * alpha_powers[j]; @@ -378,7 +397,7 @@ impl, EF: ExtensionField> SumcheckComputation for j in 1..16 { intermediate[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); let mut res = EF::ZERO; for i in 0..16 { res += alpha_powers[i] * intermediate[i]; @@ -400,7 +419,7 @@ impl SumcheckComputationPacked for PartialRoundComputation { for j in 1..16 { intermediate[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { res += EFPacking::::from(alpha_powers[j]) * intermediate[j]; @@ -416,7 +435,7 @@ impl SumcheckComputationPacked for PartialRoundComputation { for j in 1..16 { intermediate[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { res += intermediate[j] * alpha_powers[j]; From 664b94b05ac66fe670b6c2e93af1113751f83f9d Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 21 Oct 2025 23:56:45 +0200 Subject: [PATCH 11/42] works!! --- src/main.rs | 84 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0cd498d6..c716ae22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,8 @@ type F = KoalaBear; type EF = QuinticExtensionFieldKB; const UNIVARIATE_SKIPS: usize = 3; +const SANITY_CHECK: bool = false; + fn main() { init_tracing(); @@ -27,16 +29,16 @@ fn main() { let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) .collect::>(); - let perm_outputs = perm_inputs - .par_iter() - .map(|input| poseidon16_permute(*input)) - .collect::>(); let input_layers: [_; 16] = array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); let initial_full_rounds = KOALABEAR_RC16_EXTERNAL_INITIAL .into_iter() - .map(|constants| FullRoundComputation { constants }) + .enumerate() + .map(|(i, constants)| FullRoundComputation { + constants, + first_full_round: i == 0, + }) .collect::>(); let mid_partial_rounds = KOALABEAR_RC16_INTERNAL .into_iter() @@ -44,7 +46,10 @@ fn main() { .collect::>(); let final_full_rounds = KOALABEAR_RC16_EXTERNAL_FINAL .into_iter() - .map(|constants| FullRoundComputation { constants }) + .map(|constants| FullRoundComputation { + constants, + first_full_round: false, + }) .collect::>(); let n_initial_full_rounds = initial_full_rounds.len(); @@ -61,18 +66,17 @@ fn main() { { // ---------------------------------------------------- PROVER ---------------------------------------------------- - let input_layers_packed: [_; 16] = array::from_fn(|i| PFPacking::::pack_slice(&input_layers[i]).to_vec()); let mut all_layers = vec![input_layers_packed]; - for round in &initial_full_rounds { - all_layers.push(apply_full_round(all_layers.last().unwrap(), round)); + for (i, round) in initial_full_rounds.iter().enumerate() { + all_layers.push(apply_full_round(all_layers.last().unwrap(), round, i == 0)); } for round in &mid_partial_rounds { all_layers.push(apply_partial_round(all_layers.last().unwrap(), round)); } for round in &final_full_rounds { - all_layers.push(apply_full_round(all_layers.last().unwrap(), round)); + all_layers.push(apply_full_round(all_layers.last().unwrap(), round, false)); } let initial_full_layers = &all_layers[..n_initial_full_rounds]; let mid_partial_layers = @@ -80,15 +84,18 @@ fn main() { let final_full_layers = &all_layers[n_initial_full_rounds + n_mid_partial_rounds ..n_initial_full_rounds + n_mid_partial_rounds + n_final_full_rounds]; - { - // sanity check - // let last_layers: [_; 16] = - // array::from_fn(|i| PFPacking::::unpack_slice(&all_layers.last().unwrap()[i])); - // (0..n_poseidons).into_par_iter().for_each(|row| { - // for i in 0..16 { - // assert_eq!(perm_outputs[row][i], last_layers[i][row]); - // } - // }); + if SANITY_CHECK { + let perm_outputs = perm_inputs + .par_iter() + .map(|input| poseidon16_permute(*input)) + .collect::>(); + let last_layers: [_; 16] = + array::from_fn(|i| PFPacking::::unpack_slice(&all_layers.last().unwrap()[i])); + (0..n_poseidons).into_par_iter().for_each(|row| { + for i in 0..16 { + assert_eq!(perm_outputs[row][i], last_layers[i][row]); + } + }); } let mut output_claims = all_layers @@ -202,6 +209,7 @@ fn main() { fn apply_full_round( input_layers: &[Vec>], ful_round: &FullRoundComputation, + first_full_round: bool, ) -> [Vec>; 16] { let mut output_layers: [_; 16] = array::from_fn(|_| PFPacking::::zero_vec(input_layers[0].len())); @@ -209,7 +217,16 @@ fn apply_full_round( .enumerate() .for_each(|(row_index, output_row)| { let mut intermediate: [PFPacking; 16] = - array::from_fn(|j| (input_layers[j][row_index] + ful_round.constants[j]).cube()); + array::from_fn(|j| input_layers[j][row_index]); + if first_full_round { + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + } + intermediate + .par_iter_mut() + .enumerate() + .for_each(|(j, val)| { + *val = (*val + ful_round.constants[j]).cube(); + }); GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); for j in 0..16 { *output_row[j] = intermediate[j]; @@ -327,6 +344,7 @@ fn multilvariate_eval>(poly: &[F], point: &[EF]) pub struct FullRoundComputation { pub constants: [F; 16], + pub first_full_round: bool, } impl, EF: ExtensionField> SumcheckComputation @@ -338,7 +356,13 @@ impl, EF: ExtensionField> SumcheckComputation fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { debug_assert_eq!(point.len(), 16); - let mut intermediate: [NF; 16] = array::from_fn(|j| (point[j] + self.constants[j]).cube()); + let mut intermediate: [NF; 16] = array::from_fn(|j| point[j]); + if self.first_full_round { + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + } + intermediate.iter_mut().enumerate().for_each(|(j, val)| { + *val = (*val + self.constants[j]).cube(); + }); GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); let mut res = EF::ZERO; for i in 0..16 { @@ -355,8 +379,13 @@ impl SumcheckComputationPacked for FullRoundComputation { fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let mut intermediate: [PFPacking; 16] = - array::from_fn(|j| (point[j] + self.constants[j]).cube()); + let mut intermediate: [PFPacking; 16] = array::from_fn(|j| point[j]); + if self.first_full_round { + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + } + intermediate.iter_mut().enumerate().for_each(|(j, val)| { + *val = (*val + self.constants[j]).cube(); + }); GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { @@ -367,8 +396,13 @@ impl SumcheckComputationPacked for FullRoundComputation { fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let mut intermediate: [EFPacking; 16] = - array::from_fn(|j| (point[j] + PFPacking::::from(self.constants[j])).cube()); + let mut intermediate: [EFPacking; 16] = array::from_fn(|j| point[j]); + if self.first_full_round { + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + } + intermediate.iter_mut().enumerate().for_each(|(j, val)| { + *val = (*val + PFPacking::::from(self.constants[j])).cube(); + }); GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { From 50ab9f5a70c82b84e0026a8a2184841709c5d76a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 22 Oct 2025 08:00:30 +0200 Subject: [PATCH 12/42] evaluate_sequential --- Cargo.lock | 8 ++++---- src/main.rs | 41 ++++++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b2a1e3b..e5d9c80f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,7 +59,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backend" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#a711129973dca1840157d7f24df144dca4ea9f75" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#a9627e485439bb04491b11b4e4206f2eb871299d" dependencies = [ "fiat-shamir", "itertools 0.14.0", @@ -155,7 +155,7 @@ dependencies = [ [[package]] name = "constraints-folder" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#a711129973dca1840157d7f24df144dca4ea9f75" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#a9627e485439bb04491b11b4e4206f2eb871299d" dependencies = [ "fiat-shamir", "p3-air", @@ -523,7 +523,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "multilinear-toolkit" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#a711129973dca1840157d7f24df144dca4ea9f75" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#a9627e485439bb04491b11b4e4206f2eb871299d" dependencies = [ "backend", "constraints-folder", @@ -1126,7 +1126,7 @@ checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" [[package]] name = "sumcheck" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#a711129973dca1840157d7f24df144dca4ea9f75" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#a9627e485439bb04491b11b4e4206f2eb871299d" dependencies = [ "backend", "constraints-folder", diff --git a/src/main.rs b/src/main.rs index c716ae22..eb230d47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use p3_koala_bear::{ use p3_poseidon2::GenericPoseidon2LinearLayers; use rand::{Rng, SeedableRng, rngs::StdRng}; use std::{array, time::Instant}; -use tracing::instrument; +use tracing::{info_span, instrument}; use utils::{ build_prover_state, build_verifier_state, init_tracing, poseidon16_permute, transposed_par_iter_mut, @@ -78,6 +78,7 @@ fn main() { for round in &final_full_rounds { all_layers.push(apply_full_round(all_layers.last().unwrap(), round, false)); } + let initial_full_layers = &all_layers[..n_initial_full_rounds]; let mid_partial_layers = &all_layers[n_initial_full_rounds..n_initial_full_rounds + n_mid_partial_rounds]; @@ -98,17 +99,19 @@ fn main() { }); } - let mut output_claims = all_layers - .last() - .unwrap() - .par_iter() - .map(|output_layer| { - multilvariate_eval( - PFPacking::::unpack_slice(&output_layer), - &output_claim_point, - ) - }) - .collect::>(); + let mut output_claims = info_span!("computing output claims").in_scope(|| { + all_layers + .last() + .unwrap() + .par_iter() + .map(|output_layer| { + multivariate_eval::<_, _, false>( + PFPacking::::unpack_slice(&output_layer), + &output_claim_point, + ) + }) + .collect::>() + }); prover_state.add_extension_scalars(&output_claims); @@ -190,7 +193,7 @@ fn main() { for i in 0..16 { assert_eq!( output_claims[i], - multilvariate_eval(&input_layers[i], &claim_point) + multivariate_eval::<_, _, true>(&input_layers[i], &claim_point) ); } } @@ -331,13 +334,21 @@ fn verify_gkr_round>( (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) } -fn multilvariate_eval>(poly: &[F], point: &[EF]) -> EF { +fn multivariate_eval, const PARALLEL: bool>( + poly: &[F], + point: &[EF], +) -> EF { assert_eq!(poly.len(), 1 << (point.len() + UNIVARIATE_SKIPS - 1)); univariate_selectors::(UNIVARIATE_SKIPS) .iter() .zip(poly.chunks_exact(1 << (point.len() - 1))) .map(|(selector, chunk)| { - selector.evaluate(point[0]) * chunk.evaluate(&MultilinearPoint(point[1..].to_vec())) + selector.evaluate(point[0]) + * if PARALLEL { + chunk.evaluate(&MultilinearPoint(point[1..].to_vec())) + } else { + chunk.evaluate_sequential(&MultilinearPoint(point[1..].to_vec())) + } }) .sum() } From 7757d6ea1581b2dc822e08992ed8340edc7b7e83 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 22 Oct 2025 08:55:03 +0200 Subject: [PATCH 13/42] typo --- src/main.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index eb230d47..ff166ee2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -224,12 +224,9 @@ fn apply_full_round( if first_full_round { GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); } - intermediate - .par_iter_mut() - .enumerate() - .for_each(|(j, val)| { - *val = (*val + ful_round.constants[j]).cube(); - }); + intermediate.iter_mut().enumerate().for_each(|(j, val)| { + *val = (*val + ful_round.constants[j]).cube(); + }); GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); for j in 0..16 { *output_row[j] = intermediate[j]; From 5777fca77e2982a08605f13744178083d1686d1f Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Oct 2025 00:24:00 +0400 Subject: [PATCH 14/42] fix deadlock --- src/main.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index ff166ee2..94a01c1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ fn main() { let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) .collect::>(); + let selectors = univariate_selectors::(UNIVARIATE_SKIPS); let input_layers: [_; 16] = array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); @@ -108,6 +109,7 @@ fn main() { multivariate_eval::<_, _, false>( PFPacking::::unpack_slice(&output_layer), &output_claim_point, + &selectors, ) }) .collect::>() @@ -193,7 +195,7 @@ fn main() { for i in 0..16 { assert_eq!( output_claims[i], - multivariate_eval::<_, _, true>(&input_layers[i], &claim_point) + multivariate_eval::<_, _, true>(&input_layers[i], &claim_point, &selectors), ); } } @@ -334,9 +336,10 @@ fn verify_gkr_round>( fn multivariate_eval, const PARALLEL: bool>( poly: &[F], point: &[EF], + selectors: &[DensePolynomial], ) -> EF { assert_eq!(poly.len(), 1 << (point.len() + UNIVARIATE_SKIPS - 1)); - univariate_selectors::(UNIVARIATE_SKIPS) + selectors .iter() .zip(poly.chunks_exact(1 << (point.len() - 1))) .map(|(selector, chunk)| { From cff0334bbb09e1afed77d8008f6596e85af88949 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Oct 2025 00:52:58 +0400 Subject: [PATCH 15/42] experiment-double-internal-layers --- src/main.rs | 138 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 79 insertions(+), 59 deletions(-) diff --git a/src/main.rs b/src/main.rs index 94a01c1b..46032f90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,9 +41,13 @@ fn main() { first_full_round: i == 0, }) .collect::>(); + assert!(KOALABEAR_RC16_INTERNAL.len().is_multiple_of(2)); let mid_partial_rounds = KOALABEAR_RC16_INTERNAL - .into_iter() - .map(|constant| PartialRoundComputation { constant }) + .chunks_exact(2) + .map(|constants| DoublePartialRoundComputation { + constant_1: constants[0], + constant_2: constants[1], + }) .collect::>(); let final_full_rounds = KOALABEAR_RC16_EXTERNAL_FINAL .into_iter() @@ -74,7 +78,10 @@ fn main() { all_layers.push(apply_full_round(all_layers.last().unwrap(), round, i == 0)); } for round in &mid_partial_rounds { - all_layers.push(apply_partial_round(all_layers.last().unwrap(), round)); + all_layers.push(apply_double_partial_round( + all_layers.last().unwrap(), + round, + )); } for round in &final_full_rounds { all_layers.push(apply_full_round(all_layers.last().unwrap(), round, false)); @@ -221,46 +228,48 @@ fn apply_full_round( transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { - let mut intermediate: [PFPacking; 16] = - array::from_fn(|j| input_layers[j][row_index]); + let mut buff: [PFPacking; 16] = array::from_fn(|j| input_layers[j][row_index]); if first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); } - intermediate.iter_mut().enumerate().for_each(|(j, val)| { + buff.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + ful_round.constants[j]).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); for j in 0..16 { - *output_row[j] = intermediate[j]; + *output_row[j] = buff[j]; } }); output_layers } #[instrument(skip_all)] -fn apply_partial_round( +fn apply_double_partial_round( input_layers: &[Vec>], - partial_round: &PartialRoundComputation, + partial_round: &DoublePartialRoundComputation, ) -> [Vec>; 16] { let mut output_layers: [_; 16] = array::from_fn(|_| PFPacking::::zero_vec(input_layers[0].len())); transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { - let first_cubed = (input_layers[0][row_index] + partial_round.constant).cube(); - let mut intermediate = [PFPacking::::ZERO; 16]; - intermediate[0] = first_cubed; + let first_cubed = (input_layers[0][row_index] + partial_round.constant_1).cube(); + let mut buff = [PFPacking::::ZERO; 16]; + buff[0] = first_cubed; for j in 1..16 { - intermediate[j] = input_layers[j][row_index]; + buff[j] = input_layers[j][row_index]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + buff[0] = (buff[0] + partial_round.constant_2).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); for j in 0..16 { - *output_row[j] = intermediate[j]; + *output_row[j] = buff[j]; } }); output_layers } +#[instrument(skip_all)] fn prove_gkr_round< SC: SumcheckComputation + SumcheckComputation @@ -313,9 +322,13 @@ fn verify_gkr_round>( let batching_scalars_powers = batching_scalar.powers().collect_n(16); let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); - let (retrieved_batched_claim, sumcheck_postponed_claim) = - sumcheck_verify_with_univariate_skip(verifier_state, 4, log_n_poseidons, UNIVARIATE_SKIPS) - .unwrap(); + let (retrieved_batched_claim, sumcheck_postponed_claim) = sumcheck_verify_with_univariate_skip( + verifier_state, + computation.degree() + 1, + log_n_poseidons, + UNIVARIATE_SKIPS, + ) + .unwrap(); assert_eq!(retrieved_batched_claim, batched_claim); @@ -367,17 +380,17 @@ impl, EF: ExtensionField> SumcheckComputation fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { debug_assert_eq!(point.len(), 16); - let mut intermediate: [NF; 16] = array::from_fn(|j| point[j]); + let mut buff: [NF; 16] = array::from_fn(|j| point[j]); if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); } - intermediate.iter_mut().enumerate().for_each(|(j, val)| { + buff.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + self.constants[j]).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); let mut res = EF::ZERO; for i in 0..16 { - res += alpha_powers[i] * intermediate[i]; + res += alpha_powers[i] * buff[i]; } res } @@ -390,100 +403,107 @@ impl SumcheckComputationPacked for FullRoundComputation { fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let mut intermediate: [PFPacking; 16] = array::from_fn(|j| point[j]); + let mut buff: [PFPacking; 16] = array::from_fn(|j| point[j]); if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); } - intermediate.iter_mut().enumerate().for_each(|(j, val)| { + buff.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + self.constants[j]).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += EFPacking::::from(alpha_powers[j]) * intermediate[j]; + res += EFPacking::::from(alpha_powers[j]) * buff[j]; } res } fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let mut intermediate: [EFPacking; 16] = array::from_fn(|j| point[j]); + let mut buff: [EFPacking; 16] = array::from_fn(|j| point[j]); if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); } - intermediate.iter_mut().enumerate().for_each(|(j, val)| { + buff.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + PFPacking::::from(self.constants[j])).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += intermediate[j] * alpha_powers[j]; + res += buff[j] * alpha_powers[j]; } res } } -pub struct PartialRoundComputation { - pub constant: F, +pub struct DoublePartialRoundComputation { + pub constant_1: F, + pub constant_2: F, } impl, EF: ExtensionField> SumcheckComputation - for PartialRoundComputation + for DoublePartialRoundComputation { fn degree(&self) -> usize { - 3 + 9 } fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { debug_assert_eq!(point.len(), 16); - let first_cubed = (point[0] + self.constant).cube(); - let mut intermediate = [NF::ZERO; 16]; - intermediate[0] = first_cubed; + let first_cubed = (point[0] + self.constant_1).cube(); + let mut buff = [NF::ZERO; 16]; + buff[0] = first_cubed; for j in 1..16 { - intermediate[j] = point[j]; + buff[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + buff[0] = (buff[0] + self.constant_2).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); let mut res = EF::ZERO; for i in 0..16 { - res += alpha_powers[i] * intermediate[i]; + res += alpha_powers[i] * buff[i]; } res } } -impl SumcheckComputationPacked for PartialRoundComputation { +impl SumcheckComputationPacked for DoublePartialRoundComputation { fn degree(&self) -> usize { - 3 + 9 } fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let first_cubed = (point[0] + self.constant).cube(); - let mut intermediate = [PFPacking::::ZERO; 16]; - intermediate[0] = first_cubed; + let first_cubed = (point[0] + self.constant_1).cube(); + let mut buff = [PFPacking::::ZERO; 16]; + buff[0] = first_cubed; for j in 1..16 { - intermediate[j] = point[j]; + buff[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + buff[0] = (buff[0] + PFPacking::::from(self.constant_2)).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += EFPacking::::from(alpha_powers[j]) * intermediate[j]; + res += EFPacking::::from(alpha_powers[j]) * buff[j]; } res } fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let first_cubed = (point[0] + PFPacking::::from(self.constant)).cube(); - let mut intermediate = [EFPacking::::ZERO; 16]; - intermediate[0] = first_cubed; + let first_cubed = (point[0] + PFPacking::::from(self.constant_1)).cube(); + let mut buff = [EFPacking::::ZERO; 16]; + buff[0] = first_cubed; for j in 1..16 { - intermediate[j] = point[j]; + buff[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + buff[0] = (buff[0] + PFPacking::::from(self.constant_2)).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += intermediate[j] * alpha_powers[j]; + res += buff[j] * alpha_powers[j]; } res } From d4d5c6275f83be632d26cdd6354fdac09e67f68f Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Oct 2025 00:53:10 +0400 Subject: [PATCH 16/42] Revert "experiment-double-internal-layers" This reverts commit cff0334bbb09e1afed77d8008f6596e85af88949. --- src/main.rs | 138 ++++++++++++++++++++++------------------------------ 1 file changed, 59 insertions(+), 79 deletions(-) diff --git a/src/main.rs b/src/main.rs index 46032f90..94a01c1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,13 +41,9 @@ fn main() { first_full_round: i == 0, }) .collect::>(); - assert!(KOALABEAR_RC16_INTERNAL.len().is_multiple_of(2)); let mid_partial_rounds = KOALABEAR_RC16_INTERNAL - .chunks_exact(2) - .map(|constants| DoublePartialRoundComputation { - constant_1: constants[0], - constant_2: constants[1], - }) + .into_iter() + .map(|constant| PartialRoundComputation { constant }) .collect::>(); let final_full_rounds = KOALABEAR_RC16_EXTERNAL_FINAL .into_iter() @@ -78,10 +74,7 @@ fn main() { all_layers.push(apply_full_round(all_layers.last().unwrap(), round, i == 0)); } for round in &mid_partial_rounds { - all_layers.push(apply_double_partial_round( - all_layers.last().unwrap(), - round, - )); + all_layers.push(apply_partial_round(all_layers.last().unwrap(), round)); } for round in &final_full_rounds { all_layers.push(apply_full_round(all_layers.last().unwrap(), round, false)); @@ -228,48 +221,46 @@ fn apply_full_round( transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { - let mut buff: [PFPacking; 16] = array::from_fn(|j| input_layers[j][row_index]); + let mut intermediate: [PFPacking; 16] = + array::from_fn(|j| input_layers[j][row_index]); if first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); } - buff.iter_mut().enumerate().for_each(|(j, val)| { + intermediate.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + ful_round.constants[j]).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); for j in 0..16 { - *output_row[j] = buff[j]; + *output_row[j] = intermediate[j]; } }); output_layers } #[instrument(skip_all)] -fn apply_double_partial_round( +fn apply_partial_round( input_layers: &[Vec>], - partial_round: &DoublePartialRoundComputation, + partial_round: &PartialRoundComputation, ) -> [Vec>; 16] { let mut output_layers: [_; 16] = array::from_fn(|_| PFPacking::::zero_vec(input_layers[0].len())); transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { - let first_cubed = (input_layers[0][row_index] + partial_round.constant_1).cube(); - let mut buff = [PFPacking::::ZERO; 16]; - buff[0] = first_cubed; + let first_cubed = (input_layers[0][row_index] + partial_round.constant).cube(); + let mut intermediate = [PFPacking::::ZERO; 16]; + intermediate[0] = first_cubed; for j in 1..16 { - buff[j] = input_layers[j][row_index]; + intermediate[j] = input_layers[j][row_index]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - buff[0] = (buff[0] + partial_round.constant_2).cube(); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); for j in 0..16 { - *output_row[j] = buff[j]; + *output_row[j] = intermediate[j]; } }); output_layers } -#[instrument(skip_all)] fn prove_gkr_round< SC: SumcheckComputation + SumcheckComputation @@ -322,13 +313,9 @@ fn verify_gkr_round>( let batching_scalars_powers = batching_scalar.powers().collect_n(16); let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); - let (retrieved_batched_claim, sumcheck_postponed_claim) = sumcheck_verify_with_univariate_skip( - verifier_state, - computation.degree() + 1, - log_n_poseidons, - UNIVARIATE_SKIPS, - ) - .unwrap(); + let (retrieved_batched_claim, sumcheck_postponed_claim) = + sumcheck_verify_with_univariate_skip(verifier_state, 4, log_n_poseidons, UNIVARIATE_SKIPS) + .unwrap(); assert_eq!(retrieved_batched_claim, batched_claim); @@ -380,17 +367,17 @@ impl, EF: ExtensionField> SumcheckComputation fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { debug_assert_eq!(point.len(), 16); - let mut buff: [NF; 16] = array::from_fn(|j| point[j]); + let mut intermediate: [NF; 16] = array::from_fn(|j| point[j]); if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); } - buff.iter_mut().enumerate().for_each(|(j, val)| { + intermediate.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + self.constants[j]).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); let mut res = EF::ZERO; for i in 0..16 { - res += alpha_powers[i] * buff[i]; + res += alpha_powers[i] * intermediate[i]; } res } @@ -403,107 +390,100 @@ impl SumcheckComputationPacked for FullRoundComputation { fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let mut buff: [PFPacking; 16] = array::from_fn(|j| point[j]); + let mut intermediate: [PFPacking; 16] = array::from_fn(|j| point[j]); if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); } - buff.iter_mut().enumerate().for_each(|(j, val)| { + intermediate.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + self.constants[j]).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += EFPacking::::from(alpha_powers[j]) * buff[j]; + res += EFPacking::::from(alpha_powers[j]) * intermediate[j]; } res } fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let mut buff: [EFPacking; 16] = array::from_fn(|j| point[j]); + let mut intermediate: [EFPacking; 16] = array::from_fn(|j| point[j]); if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); } - buff.iter_mut().enumerate().for_each(|(j, val)| { + intermediate.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + PFPacking::::from(self.constants[j])).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += buff[j] * alpha_powers[j]; + res += intermediate[j] * alpha_powers[j]; } res } } -pub struct DoublePartialRoundComputation { - pub constant_1: F, - pub constant_2: F, +pub struct PartialRoundComputation { + pub constant: F, } impl, EF: ExtensionField> SumcheckComputation - for DoublePartialRoundComputation + for PartialRoundComputation { fn degree(&self) -> usize { - 9 + 3 } fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { debug_assert_eq!(point.len(), 16); - let first_cubed = (point[0] + self.constant_1).cube(); - let mut buff = [NF::ZERO; 16]; - buff[0] = first_cubed; + let first_cubed = (point[0] + self.constant).cube(); + let mut intermediate = [NF::ZERO; 16]; + intermediate[0] = first_cubed; for j in 1..16 { - buff[j] = point[j]; + intermediate[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - buff[0] = (buff[0] + self.constant_2).cube(); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); let mut res = EF::ZERO; for i in 0..16 { - res += alpha_powers[i] * buff[i]; + res += alpha_powers[i] * intermediate[i]; } res } } -impl SumcheckComputationPacked for DoublePartialRoundComputation { +impl SumcheckComputationPacked for PartialRoundComputation { fn degree(&self) -> usize { - 9 + 3 } fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let first_cubed = (point[0] + self.constant_1).cube(); - let mut buff = [PFPacking::::ZERO; 16]; - buff[0] = first_cubed; + let first_cubed = (point[0] + self.constant).cube(); + let mut intermediate = [PFPacking::::ZERO; 16]; + intermediate[0] = first_cubed; for j in 1..16 { - buff[j] = point[j]; + intermediate[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - buff[0] = (buff[0] + PFPacking::::from(self.constant_2)).cube(); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += EFPacking::::from(alpha_powers[j]) * buff[j]; + res += EFPacking::::from(alpha_powers[j]) * intermediate[j]; } res } fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let first_cubed = (point[0] + PFPacking::::from(self.constant_1)).cube(); - let mut buff = [EFPacking::::ZERO; 16]; - buff[0] = first_cubed; + let first_cubed = (point[0] + PFPacking::::from(self.constant)).cube(); + let mut intermediate = [EFPacking::::ZERO; 16]; + intermediate[0] = first_cubed; for j in 1..16 { - buff[j] = point[j]; + intermediate[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - buff[0] = (buff[0] + PFPacking::::from(self.constant_2)).cube(); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += buff[j] * alpha_powers[j]; + res += intermediate[j] * alpha_powers[j]; } res } From e9fc3aeef7a4cf3768fda470a51bdcf6bf85f563 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Oct 2025 01:42:09 +0400 Subject: [PATCH 17/42] naming --- src/main.rs | 94 ++++++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/src/main.rs b/src/main.rs index 94a01c1b..8b2ab57b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ fn main() { init_tracing(); let mut rng = StdRng::seed_from_u64(0); - let log_n_poseidons = 19; + let log_n_poseidons = 20; let n_poseidons = 1 << log_n_poseidons; let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) @@ -221,17 +221,16 @@ fn apply_full_round( transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { - let mut intermediate: [PFPacking; 16] = - array::from_fn(|j| input_layers[j][row_index]); + let mut buff: [PFPacking; 16] = array::from_fn(|j| input_layers[j][row_index]); if first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); } - intermediate.iter_mut().enumerate().for_each(|(j, val)| { + buff.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + ful_round.constants[j]).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); for j in 0..16 { - *output_row[j] = intermediate[j]; + *output_row[j] = buff[j]; } }); output_layers @@ -248,19 +247,20 @@ fn apply_partial_round( .enumerate() .for_each(|(row_index, output_row)| { let first_cubed = (input_layers[0][row_index] + partial_round.constant).cube(); - let mut intermediate = [PFPacking::::ZERO; 16]; - intermediate[0] = first_cubed; + let mut buff = [PFPacking::::ZERO; 16]; + buff[0] = first_cubed; for j in 1..16 { - intermediate[j] = input_layers[j][row_index]; + buff[j] = input_layers[j][row_index]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); for j in 0..16 { - *output_row[j] = intermediate[j]; + *output_row[j] = buff[j]; } }); output_layers } +#[instrument(skip_all)] fn prove_gkr_round< SC: SumcheckComputation + SumcheckComputation @@ -313,9 +313,13 @@ fn verify_gkr_round>( let batching_scalars_powers = batching_scalar.powers().collect_n(16); let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); - let (retrieved_batched_claim, sumcheck_postponed_claim) = - sumcheck_verify_with_univariate_skip(verifier_state, 4, log_n_poseidons, UNIVARIATE_SKIPS) - .unwrap(); + let (retrieved_batched_claim, sumcheck_postponed_claim) = sumcheck_verify_with_univariate_skip( + verifier_state, + computation.degree() + 1, + log_n_poseidons, + UNIVARIATE_SKIPS, + ) + .unwrap(); assert_eq!(retrieved_batched_claim, batched_claim); @@ -367,17 +371,17 @@ impl, EF: ExtensionField> SumcheckComputation fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { debug_assert_eq!(point.len(), 16); - let mut intermediate: [NF; 16] = array::from_fn(|j| point[j]); + let mut buff: [NF; 16] = array::from_fn(|j| point[j]); if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); } - intermediate.iter_mut().enumerate().for_each(|(j, val)| { + buff.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + self.constants[j]).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); let mut res = EF::ZERO; for i in 0..16 { - res += alpha_powers[i] * intermediate[i]; + res += alpha_powers[i] * buff[i]; } res } @@ -390,34 +394,34 @@ impl SumcheckComputationPacked for FullRoundComputation { fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let mut intermediate: [PFPacking; 16] = array::from_fn(|j| point[j]); + let mut buff: [PFPacking; 16] = array::from_fn(|j| point[j]); if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); } - intermediate.iter_mut().enumerate().for_each(|(j, val)| { + buff.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + self.constants[j]).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += EFPacking::::from(alpha_powers[j]) * intermediate[j]; + res += EFPacking::::from(alpha_powers[j]) * buff[j]; } res } fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); - let mut intermediate: [EFPacking; 16] = array::from_fn(|j| point[j]); + let mut buff: [EFPacking; 16] = array::from_fn(|j| point[j]); if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); } - intermediate.iter_mut().enumerate().for_each(|(j, val)| { + buff.iter_mut().enumerate().for_each(|(j, val)| { *val = (*val + PFPacking::::from(self.constants[j])).cube(); }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += intermediate[j] * alpha_powers[j]; + res += buff[j] * alpha_powers[j]; } res } @@ -437,15 +441,15 @@ impl, EF: ExtensionField> SumcheckComputation fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { debug_assert_eq!(point.len(), 16); let first_cubed = (point[0] + self.constant).cube(); - let mut intermediate = [NF::ZERO; 16]; - intermediate[0] = first_cubed; + let mut buff = [NF::ZERO; 16]; + buff[0] = first_cubed; for j in 1..16 { - intermediate[j] = point[j]; + buff[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); let mut res = EF::ZERO; for i in 0..16 { - res += alpha_powers[i] * intermediate[i]; + res += alpha_powers[i] * buff[i]; } res } @@ -459,15 +463,15 @@ impl SumcheckComputationPacked for PartialRoundComputation { fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); let first_cubed = (point[0] + self.constant).cube(); - let mut intermediate = [PFPacking::::ZERO; 16]; - intermediate[0] = first_cubed; + let mut buff = [PFPacking::::ZERO; 16]; + buff[0] = first_cubed; for j in 1..16 { - intermediate[j] = point[j]; + buff[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += EFPacking::::from(alpha_powers[j]) * intermediate[j]; + res += EFPacking::::from(alpha_powers[j]) * buff[j]; } res } @@ -475,15 +479,15 @@ impl SumcheckComputationPacked for PartialRoundComputation { fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { debug_assert_eq!(point.len(), 16); let first_cubed = (point[0] + PFPacking::::from(self.constant)).cube(); - let mut intermediate = [EFPacking::::ZERO; 16]; - intermediate[0] = first_cubed; + let mut buff = [EFPacking::::ZERO; 16]; + buff[0] = first_cubed; for j in 1..16 { - intermediate[j] = point[j]; + buff[j] = point[j]; } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut intermediate); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; for j in 0..16 { - res += intermediate[j] * alpha_powers[j]; + res += buff[j] * alpha_powers[j]; } res } From f4a8bec976d585fc3b40356601a4229ff480def2 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Oct 2025 12:52:07 +0400 Subject: [PATCH 18/42] WOAwwwwwww --- src/main.rs | 361 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 324 insertions(+), 37 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8b2ab57b..52b804a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,9 +18,15 @@ type F = KoalaBear; type EF = QuinticExtensionFieldKB; const UNIVARIATE_SKIPS: usize = 3; -const SANITY_CHECK: bool = false; +const SANITY_CHECK: bool = true; +const N_COMMITED_CUBES: usize = 16; // power of 2 to increase PCS efficiency + +// const N_INITIAL_ROUNDS: usize = KOALABEAR_RC16_EXTERNAL_INITIAL.len(); +const N_INTERNAL_ROUNDS: usize = KOALABEAR_RC16_INTERNAL.len(); +// const N_FINAL_ROUNDS: usize = KOALABEAR_RC16_EXTERNAL_FINAL.len(); fn main() { + assert!(N_COMMITED_CUBES <= N_INTERNAL_ROUNDS); init_tracing(); let mut rng = StdRng::seed_from_u64(0); @@ -41,8 +47,15 @@ fn main() { first_full_round: i == 0, }) .collect::>(); - let mid_partial_rounds = KOALABEAR_RC16_INTERNAL - .into_iter() + let partial_rounds_with_committed_cubes = PartialRoundsWithCommittedCubes { + constants: KOALABEAR_RC16_INTERNAL[..N_COMMITED_CUBES] + .try_into() + .unwrap(), + last_constant: KOALABEAR_RC16_INTERNAL[N_COMMITED_CUBES], + }; + let partial_rounds_remaining = KOALABEAR_RC16_INTERNAL[N_COMMITED_CUBES + 1..] + .iter() + .copied() .map(|constant| PartialRoundComputation { constant }) .collect::>(); let final_full_rounds = KOALABEAR_RC16_EXTERNAL_FINAL @@ -53,46 +66,62 @@ fn main() { }) .collect::>(); - let n_initial_full_rounds = initial_full_rounds.len(); - let n_mid_partial_rounds = mid_partial_rounds.len(); - let n_final_full_rounds = final_full_rounds.len(); - - let output_claim_point = (0..(log_n_poseidons + 1 - UNIVARIATE_SKIPS)) - .map(|_| rng.random()) - .collect::>(); - - let mut prover_state = build_prover_state::(); - let prover_time = Instant::now(); - { + + let mut verifier_state = { // ---------------------------------------------------- PROVER ---------------------------------------------------- - let input_layers_packed: [_; 16] = + let mut prover_state = build_prover_state::(); + + let initial_full_layer_inputs: [_; 16] = array::from_fn(|i| PFPacking::::pack_slice(&input_layers[i]).to_vec()); - let mut all_layers = vec![input_layers_packed]; + let mut all_initial_full_layers = vec![initial_full_layer_inputs]; for (i, round) in initial_full_rounds.iter().enumerate() { - all_layers.push(apply_full_round(all_layers.last().unwrap(), round, i == 0)); + all_initial_full_layers.push(apply_full_round( + all_initial_full_layers.last().unwrap(), + round, + i == 0, + )); } - for round in &mid_partial_rounds { - all_layers.push(apply_partial_round(all_layers.last().unwrap(), round)); + + let internal_partial_layer_with_committed_cubes_inputs = + all_initial_full_layers.pop().unwrap(); + let (next_layer, committed_cubes) = apply_partial_round_for_commit_cubes( + &internal_partial_layer_with_committed_cubes_inputs, + &partial_rounds_with_committed_cubes, + ); + + let mut internal_partial_layer_remaining_inputs = vec![next_layer]; + for round in &partial_rounds_remaining { + internal_partial_layer_remaining_inputs.push(apply_partial_round( + internal_partial_layer_remaining_inputs.last().unwrap(), + round, + )); } + + let mut all_final_full_layers = + vec![internal_partial_layer_remaining_inputs.pop().unwrap()]; for round in &final_full_rounds { - all_layers.push(apply_full_round(all_layers.last().unwrap(), round, false)); + all_final_full_layers.push(apply_full_round( + all_final_full_layers.last().unwrap(), + round, + false, + )); } - let initial_full_layers = &all_layers[..n_initial_full_rounds]; - let mid_partial_layers = - &all_layers[n_initial_full_rounds..n_initial_full_rounds + n_mid_partial_rounds]; - let final_full_layers = &all_layers[n_initial_full_rounds + n_mid_partial_rounds - ..n_initial_full_rounds + n_mid_partial_rounds + n_final_full_rounds]; + for committed_cube in &committed_cubes { + // TODO use PCS + prover_state.hint_base_scalars(PFPacking::::unpack_slice(committed_cube)); + } if SANITY_CHECK { let perm_outputs = perm_inputs .par_iter() .map(|input| poseidon16_permute(*input)) .collect::>(); - let last_layers: [_; 16] = - array::from_fn(|i| PFPacking::::unpack_slice(&all_layers.last().unwrap()[i])); + let last_layers: [_; 16] = array::from_fn(|i| { + PFPacking::::unpack_slice(&all_final_full_layers.last().unwrap()[i]) + }); (0..n_poseidons).into_par_iter().for_each(|row| { for i in 0..16 { assert_eq!(perm_outputs[row][i], last_layers[i][row]); @@ -100,8 +129,10 @@ fn main() { }); } + let output_claim_point = prover_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); + let mut output_claims = info_span!("computing output claims").in_scope(|| { - all_layers + all_final_full_layers .last() .unwrap() .par_iter() @@ -118,7 +149,9 @@ fn main() { prover_state.add_extension_scalars(&output_claims); let mut claim_point = output_claim_point.clone(); - for (input_layers, full_round) in final_full_layers.iter().zip(&final_full_rounds).rev() { + + for (input_layers, full_round) in all_final_full_layers.iter().zip(&final_full_rounds).rev() + { (claim_point, output_claims) = prove_gkr_round( &mut prover_state, full_round, @@ -128,8 +161,10 @@ fn main() { ); } - for (input_layers, partial_round) in - mid_partial_layers.iter().zip(&mid_partial_rounds).rev() + for (input_layers, partial_round) in internal_partial_layer_remaining_inputs + .iter() + .zip(&partial_rounds_remaining) + .rev() { (claim_point, output_claims) = prove_gkr_round( &mut prover_state, @@ -140,7 +175,38 @@ fn main() { ); } - for (input_layers, full_round) in initial_full_layers.iter().zip(&initial_full_rounds).rev() + (claim_point, output_claims) = prove_internal_rounds_with_committed_cube( + &mut prover_state, + &internal_partial_layer_with_committed_cubes_inputs, + &committed_cubes, + &partial_rounds_with_committed_cubes, + &claim_point, + &output_claims, + &selectors, + ); + + { + // PCS open for committed cubes (claim_points[16..]) + if SANITY_CHECK { + for i in 0..N_COMMITED_CUBES { + assert_eq!( + multivariate_eval::<_, _, true>( + &PFPacking::::unpack_slice(&committed_cubes[i]), + &claim_point, + &selectors + ), + output_claims[16 + i] + ); + } + } + } + + output_claims = output_claims[..16].to_vec(); + + for (input_layers, full_round) in all_initial_full_layers + .iter() + .zip(&initial_full_rounds) + .rev() { (claim_point, output_claims) = prove_gkr_round( &mut prover_state, @@ -150,14 +216,25 @@ fn main() { &output_claims, ); } - } + + build_verifier_state(&prover_state) + }; let prover_duration = prover_time.elapsed(); let verifier_time = Instant::now(); { // ---------------------------------------------------- VERIFIER ---------------------------------------------------- - let mut verifier_state = build_verifier_state(&prover_state); + // TODO use PCS + let committed_cubes = (0..N_COMMITED_CUBES) + .map(|_| { + verifier_state + .receive_hint_base_scalars(n_poseidons) + .unwrap() + }) + .collect::>(); + + let output_claim_point = verifier_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); let mut output_claims = verifier_state.next_extension_scalars_vec(16).unwrap(); @@ -172,7 +249,7 @@ fn main() { ); } - for partial_round in mid_partial_rounds.iter().rev() { + for partial_round in partial_rounds_remaining.iter().rev() { (claim_point, output_claims) = verify_gkr_round( &mut verifier_state, partial_round, @@ -181,6 +258,33 @@ fn main() { &output_claims, ); } + let claimed_cubes_evals = verifier_state + .next_extension_scalars_vec(N_COMMITED_CUBES) + .unwrap(); + + (claim_point, output_claims) = verify_gkr_round( + &mut verifier_state, + &partial_rounds_with_committed_cubes, + log_n_poseidons, + &claim_point, + &[output_claims, claimed_cubes_evals.clone()].concat(), + ); + + { + // PCS open for committed cubes (claim_points[16..]) + // TODO + for i in 0..N_COMMITED_CUBES { + assert_eq!( + multivariate_eval::<_, _, true>( + &committed_cubes[i], + &claim_point, + &selectors + ), + output_claims[16 + i] + ); + } + } + output_claims = output_claims[..16].to_vec(); for full_round in initial_full_rounds.iter().rev() { (claim_point, output_claims) = verify_gkr_round( @@ -260,6 +364,37 @@ fn apply_partial_round( output_layers } +#[instrument(skip_all)] +fn apply_partial_round_for_commit_cubes( + input_layers: &[Vec>], + rounds: &PartialRoundsWithCommittedCubes, +) -> ( + [Vec>; 16], + [Vec>; N_COMMITED_CUBES], +) { + let mut output_layers: [_; 16] = + array::from_fn(|_| PFPacking::::zero_vec(input_layers[0].len())); + let mut cubes: [_; N_COMMITED_CUBES] = + array::from_fn(|_| PFPacking::::zero_vec(input_layers[0].len())); + transposed_par_iter_mut(&mut output_layers) + .zip(transposed_par_iter_mut(&mut cubes)) + .enumerate() + .for_each(|(row_index, (output_row, cubes))| { + let mut buff: [PFPacking; 16] = array::from_fn(|j| input_layers[j][row_index]); + for (i, &constant) in rounds.constants.iter().enumerate() { + *cubes[i] = (buff[0] + constant).cube(); + buff[0] = *cubes[i]; + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + } + buff[0] = (buff[0] + rounds.last_constant).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + for j in 0..16 { + *output_row[j] = buff[j]; + } + }); + (output_layers, cubes) +} + #[instrument(skip_all)] fn prove_gkr_round< SC: SumcheckComputation @@ -302,6 +437,72 @@ fn prove_gkr_round< (sumcheck_point.0, sumcheck_inner_evals) } +#[instrument(skip_all)] +fn prove_internal_rounds_with_committed_cube( + prover_state: &mut FSProver>, + input_layers: &[Vec>], + committed_cubes: &[Vec>], + computation: &PartialRoundsWithCommittedCubes, + claim_point: &[EF], + output_claims: &[EF], + selectors: &[DensePolynomial], +) -> (Vec, Vec) { + assert_eq!(input_layers.len(), 16); + assert_eq!(committed_cubes.len(), N_COMMITED_CUBES); + + let cubes_evals = info_span!("computing cube evals").in_scope(|| { + committed_cubes + .par_iter() + .map(|layer| { + multivariate_eval::<_, _, false>( + PFPacking::::unpack_slice(&layer), + &claim_point, + selectors, + ) + }) + .collect::>() + }); + + prover_state.add_extension_scalars(&cubes_evals); + + let batching_scalar = prover_state.sample(); + let batched_claim: EF = dot_product( + output_claims.iter().chain(&cubes_evals).copied(), + batching_scalar.powers(), + ); + let batching_scalars_powers = batching_scalar.powers().collect_n(16 + N_COMMITED_CUBES); + + let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( + UNIVARIATE_SKIPS, + MleGroupRef::BasePacked( + input_layers + .iter() + .chain(committed_cubes.iter()) + .map(Vec::as_slice) + .collect(), + ), + computation, + computation, + &batching_scalars_powers, + Some((claim_point.to_vec(), None)), + false, + prover_state, + batched_claim, + None, + ); + + // sanity check + debug_assert_eq!( + computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) + * eq_poly_with_skip(&sumcheck_point, &claim_point, UNIVARIATE_SKIPS), + sumcheck_final_sum + ); + + prover_state.add_extension_scalars(&sumcheck_inner_evals); + + (sumcheck_point.0, sumcheck_inner_evals) +} + fn verify_gkr_round>( verifier_state: &mut FSVerifier>, computation: &SC, @@ -310,7 +511,7 @@ fn verify_gkr_round>( output_claims: &[EF], ) -> (Vec, Vec) { let batching_scalar = verifier_state.sample(); - let batching_scalars_powers = batching_scalar.powers().collect_n(16); + let batching_scalars_powers = batching_scalar.powers().collect_n(output_claims.len()); let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); let (retrieved_batched_claim, sumcheck_postponed_claim) = sumcheck_verify_with_univariate_skip( @@ -323,7 +524,9 @@ fn verify_gkr_round>( assert_eq!(retrieved_batched_claim, batched_claim); - let sumcheck_inner_evals = verifier_state.next_extension_scalars_vec(16).unwrap(); + let sumcheck_inner_evals = verifier_state + .next_extension_scalars_vec(output_claims.len()) + .unwrap(); assert_eq!( computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) * eq_poly_with_skip( @@ -492,3 +695,87 @@ impl SumcheckComputationPacked for PartialRoundComputation { res } } + +pub struct PartialRoundsWithCommittedCubes { + pub constants: [F; N_COMMITED_CUBES], + pub last_constant: F, +} + +impl, EF: ExtensionField> SumcheckComputation + for PartialRoundsWithCommittedCubes +{ + fn degree(&self) -> usize { + 3 + } + + fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { + // points = 16 inputs, then N_COMMITED_CUBES cubes + + debug_assert_eq!(point.len(), 16 + N_COMMITED_CUBES); + debug_assert_eq!(alpha_powers.len(), 16 + N_COMMITED_CUBES); + + let mut res = EF::ZERO; + let mut buff: [NF; 16] = array::from_fn(|j| point[j]); + for (i, &constant) in self.constants.iter().enumerate() { + let computed_cube = (buff[0] + constant).cube(); + res += alpha_powers[16 + i] * computed_cube; + buff[0] = point[16 + i]; // commited cube + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + } + + buff[0] = (buff[0] + self.last_constant).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + for i in 0..16 { + res += alpha_powers[i] * buff[i]; + } + res + } +} + +impl SumcheckComputationPacked for PartialRoundsWithCommittedCubes { + fn degree(&self) -> usize { + 3 + } + + fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { + debug_assert_eq!(point.len(), 16 + N_COMMITED_CUBES); + debug_assert_eq!(alpha_powers.len(), 16 + N_COMMITED_CUBES); + + let mut res = EFPacking::::ZERO; + let mut buff: [PFPacking; 16] = array::from_fn(|j| point[j]); + for (i, &constant) in self.constants.iter().enumerate() { + let computed_cube = (buff[0] + constant).cube(); + res += EFPacking::::from(alpha_powers[16 + i]) * computed_cube; + buff[0] = point[16 + i]; // commited cube + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + } + + buff[0] = (buff[0] + self.last_constant).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + for i in 0..16 { + res += EFPacking::::from(alpha_powers[i]) * buff[i]; + } + res + } + + fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { + debug_assert_eq!(point.len(), 16 + N_COMMITED_CUBES); + debug_assert_eq!(alpha_powers.len(), 16 + N_COMMITED_CUBES); + + let mut res = EFPacking::::ZERO; + let mut buff: [EFPacking; 16] = array::from_fn(|j| point[j]); + for (i, &constant) in self.constants.iter().enumerate() { + let computed_cube = (buff[0] + PFPacking::::from(constant)).cube(); + res += computed_cube * alpha_powers[16 + i]; + buff[0] = point[16 + i]; // commited cube + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + } + + buff[0] = (buff[0] + PFPacking::::from(self.last_constant)).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + for i in 0..16 { + res += buff[i] * alpha_powers[i]; + } + res + } +} From ff3fdde346eeae66fa8fc0fad0918dfc1b1f7073 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Oct 2025 14:38:49 +0400 Subject: [PATCH 19/42] working for real with the pcs --- src/main.rs | 285 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 231 insertions(+), 54 deletions(-) diff --git a/src/main.rs b/src/main.rs index 52b804a0..c22880a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,12 +13,13 @@ use utils::{ build_prover_state, build_verifier_state, init_tracing, poseidon16_permute, transposed_par_iter_mut, }; +use whir_p3::{FoldingFactor, SecurityAssumption, WhirConfig, WhirConfigBuilder}; type F = KoalaBear; type EF = QuinticExtensionFieldKB; const UNIVARIATE_SKIPS: usize = 3; -const SANITY_CHECK: bool = true; +const SANITY_CHECK: bool = false; const N_COMMITED_CUBES: usize = 16; // power of 2 to increase PCS efficiency // const N_INITIAL_ROUNDS: usize = KOALABEAR_RC16_EXTERNAL_INITIAL.len(); @@ -29,8 +30,21 @@ fn main() { assert!(N_COMMITED_CUBES <= N_INTERNAL_ROUNDS); init_tracing(); - let mut rng = StdRng::seed_from_u64(0); let log_n_poseidons = 20; + + let whir_config_builder = WhirConfigBuilder { + folding_factor: FoldingFactor::new(7, 4), + soundness_type: SecurityAssumption::CapacityBound, + pow_bits: 16, + max_num_variables_to_send_coeffs: 6, + rs_domain_initial_reduction_factor: 5, + security_level: 128, + starting_log_inv_rate: 1, + }; + let whir_n_vars = log_n_poseidons + log2_strict_usize(16 + N_COMMITED_CUBES); + let whir_config = WhirConfig::new(whir_config_builder, whir_n_vars); + + let mut rng = StdRng::seed_from_u64(0); let n_poseidons = 1 << log_n_poseidons; let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) @@ -71,8 +85,6 @@ fn main() { let mut verifier_state = { // ---------------------------------------------------- PROVER ---------------------------------------------------- - let mut prover_state = build_prover_state::(); - let initial_full_layer_inputs: [_; 16] = array::from_fn(|i| PFPacking::::pack_slice(&input_layers[i]).to_vec()); let mut all_initial_full_layers = vec![initial_full_layer_inputs]; @@ -109,10 +121,32 @@ fn main() { )); } - for committed_cube in &committed_cubes { - // TODO use PCS - prover_state.hint_base_scalars(PFPacking::::unpack_slice(committed_cube)); - } + let mut prover_state = build_prover_state::(); + let mut global_poly_commited: Vec = unsafe { uninitialized_vec(1 << whir_n_vars) }; + let mut chunks = split_at_mut_many( + &mut global_poly_commited, + (0..16 + N_COMMITED_CUBES - 1) + .map(|i| (i + 1) << log_n_poseidons) + .collect::>() + .as_slice(), + ); + chunks[..16] + .par_iter_mut() + .enumerate() + .for_each(|(i, chunk)| { + chunk.copy_from_slice(&input_layers[i]); + }); + chunks[16..] + .par_iter_mut() + .enumerate() + .for_each(|(i, chunk)| { + chunk.copy_from_slice(PFPacking::::unpack_slice(&committed_cubes[i])); + }); + + let global_poly_commited = MleOwned::Base(global_poly_commited); + let pcs_witness = whir_config.commit(&mut prover_state, &global_poly_commited); + let global_poly_commited_packed = + PFPacking::::pack_slice(global_poly_commited.as_base().unwrap()); if SANITY_CHECK { let perm_outputs = perm_inputs @@ -137,7 +171,7 @@ fn main() { .unwrap() .par_iter() .map(|output_layer| { - multivariate_eval::<_, _, false>( + multivariate_eval::<_, _, _, false>( PFPacking::::unpack_slice(&output_layer), &output_claim_point, &selectors, @@ -185,21 +219,8 @@ fn main() { &selectors, ); - { - // PCS open for committed cubes (claim_points[16..]) - if SANITY_CHECK { - for i in 0..N_COMMITED_CUBES { - assert_eq!( - multivariate_eval::<_, _, true>( - &PFPacking::::unpack_slice(&committed_cubes[i]), - &claim_point, - &selectors - ), - output_claims[16 + i] - ); - } - } - } + let pcs_point_for_cubes = claim_point.clone(); + let pcs_evals_for_cubes = output_claims[16..].to_vec(); output_claims = output_claims[..16].to_vec(); @@ -217,22 +238,116 @@ fn main() { ); } + let pcs_point_for_inputs = claim_point.clone(); + let pcs_evals_for_inputs = output_claims.to_vec(); + + // PCS opening + let mut pcs_statements = vec![]; + + let eq_mle_inputs = eval_eq_packed(&pcs_point_for_inputs[1..]); + let inner_evals_inputs = global_poly_commited_packed + [..global_poly_commited_packed.len() / 2] + .par_chunks_exact(eq_mle_inputs.len()) + .map(|chunk| { + let ef_sum = dot_product::, _, _>( + eq_mle_inputs.iter().copied(), + chunk.iter().copied(), + ); + as PackedFieldExtension>::to_ext_iter([ef_sum]).sum::() + }) + .collect::>(); + prover_state.add_extension_scalars(&inner_evals_inputs); + let pcs_batching_scalars_inputs = prover_state.sample_vec(4 + UNIVARIATE_SKIPS); // 4 = log2(16) + pcs_statements.push(Evaluation { + point: MultilinearPoint( + [ + vec![EF::ZERO], + pcs_batching_scalars_inputs.clone(), + pcs_point_for_inputs[1..].to_vec(), + ] + .concat(), + ), + value: inner_evals_inputs.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs)), + }); + { + // sanity check + for (&eval, inner_evals) in pcs_evals_for_inputs + .iter() + .zip(inner_evals_inputs.chunks_exact(1 << UNIVARIATE_SKIPS)) + { + assert_eq!( + eval, + multivariate_eval::<_, _, _, false>( + inner_evals, + &pcs_point_for_inputs[..1], + &selectors + ) + ); + } + } + + let eq_mle_cubes = eval_eq_packed(&pcs_point_for_cubes[1..]); + let inner_evals_cubes = global_poly_commited_packed + [global_poly_commited_packed.len() / 2..] + .par_chunks_exact(eq_mle_cubes.len()) + .map(|chunk| { + let ef_sum = dot_product::, _, _>( + eq_mle_cubes.iter().copied(), + chunk.iter().copied(), + ); + as PackedFieldExtension>::to_ext_iter([ef_sum]).sum::() + }) + .collect::>(); + prover_state.add_extension_scalars(&inner_evals_cubes); + let pcs_batching_scalars_cubes = + prover_state.sample_vec(log2_strict_usize(N_COMMITED_CUBES) + UNIVARIATE_SKIPS); + pcs_statements.push(Evaluation { + point: MultilinearPoint( + [ + vec![EF::ONE], + pcs_batching_scalars_cubes.clone(), + pcs_point_for_cubes[1..].to_vec(), + ] + .concat(), + ), + value: inner_evals_cubes.evaluate(&MultilinearPoint(pcs_batching_scalars_cubes)), + }); + { + // sanity check + for (&eval, inner_evals) in pcs_evals_for_cubes + .iter() + .zip(inner_evals_cubes.chunks_exact(1 << UNIVARIATE_SKIPS)) + { + assert_eq!( + eval, + multivariate_eval::<_, _, _, false>( + inner_evals, + &pcs_point_for_cubes[..1], + &selectors + ) + ); + } + } + + whir_config.prove( + &mut prover_state, + pcs_statements, + pcs_witness, + &global_poly_commited.by_ref(), + ); + build_verifier_state(&prover_state) }; + let prover_duration = prover_time.elapsed(); let verifier_time = Instant::now(); { // ---------------------------------------------------- VERIFIER ---------------------------------------------------- - // TODO use PCS - let committed_cubes = (0..N_COMMITED_CUBES) - .map(|_| { - verifier_state - .receive_hint_base_scalars(n_poseidons) - .unwrap() - }) - .collect::>(); + let parsed_pcs_commitment = whir_config + .parse_commitment::(&mut verifier_state) + .unwrap(); let output_claim_point = verifier_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); @@ -270,20 +385,9 @@ fn main() { &[output_claims, claimed_cubes_evals.clone()].concat(), ); - { - // PCS open for committed cubes (claim_points[16..]) - // TODO - for i in 0..N_COMMITED_CUBES { - assert_eq!( - multivariate_eval::<_, _, true>( - &committed_cubes[i], - &claim_point, - &selectors - ), - output_claims[16 + i] - ); - } - } + let pcs_point_for_cubes = claim_point.clone(); + let pcs_evals_for_cubes = output_claims[16..].to_vec(); + output_claims = output_claims[..16].to_vec(); for full_round in initial_full_rounds.iter().rev() { @@ -296,12 +400,80 @@ fn main() { ); } - for i in 0..16 { - assert_eq!( - output_claims[i], - multivariate_eval::<_, _, true>(&input_layers[i], &claim_point, &selectors), - ); + let pcs_point_for_inputs = claim_point.clone(); + let pcs_evals_for_inputs = output_claims.to_vec(); + + // PCS verification + + let mut pcs_statements = vec![]; + + let inner_evals_inputs = verifier_state + .next_extension_scalars_vec(16 << UNIVARIATE_SKIPS) + .unwrap(); + let pcs_batching_scalars_inputs = verifier_state.sample_vec(4 + UNIVARIATE_SKIPS); // 4 = log2(16) + pcs_statements.push(Evaluation { + point: MultilinearPoint( + [ + vec![EF::ZERO], + pcs_batching_scalars_inputs.clone(), + pcs_point_for_inputs[1..].to_vec(), + ] + .concat(), + ), + value: inner_evals_inputs.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs)), + }); + { + for (&eval, inner_evals) in pcs_evals_for_inputs + .iter() + .zip(inner_evals_inputs.chunks_exact(1 << UNIVARIATE_SKIPS)) + { + assert_eq!( + eval, + multivariate_eval::<_, _, _, false>( + inner_evals, + &pcs_point_for_inputs[..1], + &selectors + ) + ); + } + } + + let inner_evals_cubes = verifier_state + .next_extension_scalars_vec(N_COMMITED_CUBES << UNIVARIATE_SKIPS) + .unwrap(); + let pcs_batching_scalars_cubes = + verifier_state.sample_vec(log2_strict_usize(N_COMMITED_CUBES) + UNIVARIATE_SKIPS); + pcs_statements.push(Evaluation { + point: MultilinearPoint( + [ + vec![EF::ONE], + pcs_batching_scalars_cubes.clone(), + pcs_point_for_cubes[1..].to_vec(), + ] + .concat(), + ), + value: inner_evals_cubes.evaluate(&MultilinearPoint(pcs_batching_scalars_cubes)), + }); + { + // sanity check + for (&eval, inner_evals) in pcs_evals_for_cubes + .iter() + .zip(inner_evals_cubes.chunks_exact(1 << UNIVARIATE_SKIPS)) + { + assert_eq!( + eval, + multivariate_eval::<_, _, _, false>( + inner_evals, + &pcs_point_for_cubes[..1], + &selectors + ) + ); + } } + + whir_config + .verify(&mut verifier_state, &parsed_pcs_commitment, pcs_statements) + .unwrap(); } let verifier_duration = verifier_time.elapsed(); @@ -454,7 +626,7 @@ fn prove_internal_rounds_with_committed_cube( committed_cubes .par_iter() .map(|layer| { - multivariate_eval::<_, _, false>( + multivariate_eval::<_, _, _, false>( PFPacking::::unpack_slice(&layer), &claim_point, selectors, @@ -540,8 +712,13 @@ fn verify_gkr_round>( (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) } -fn multivariate_eval, const PARALLEL: bool>( - poly: &[F], +fn multivariate_eval< + F: Field, + NF: ExtensionField, + EF: ExtensionField + ExtensionField, + const PARALLEL: bool, +>( + poly: &[NF], point: &[EF], selectors: &[DensePolynomial], ) -> EF { From 3286ee64b1ecf9c97b5f09e8f1fccbc4ddcce834 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Oct 2025 18:13:48 +0400 Subject: [PATCH 20/42] overhead versus plaintext --- crates/utils/src/poseidon2.rs | 5 +++++ src/main.rs | 42 +++++++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/crates/utils/src/poseidon2.rs b/crates/utils/src/poseidon2.rs index d2946005..ba7c2795 100644 --- a/crates/utils/src/poseidon2.rs +++ b/crates/utils/src/poseidon2.rs @@ -102,6 +102,11 @@ pub fn poseidon16_permute(input: [KoalaBear; 16]) -> [KoalaBear; 16] { get_poseidon16().permute(input) } +#[inline(always)] +pub fn poseidon16_permute_mut(input: &mut [KoalaBear; 16]) { + get_poseidon16().permute_mut(input); +} + static POSEIDON24_INSTANCE: OnceLock = OnceLock::new(); #[inline(always)] diff --git a/src/main.rs b/src/main.rs index c22880a3..6d7e8da8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use std::{array, time::Instant}; use tracing::{info_span, instrument}; use utils::{ build_prover_state, build_verifier_state, init_tracing, poseidon16_permute, - transposed_par_iter_mut, + poseidon16_permute_mut, transposed_par_iter_mut, }; use whir_p3::{FoldingFactor, SecurityAssumption, WhirConfig, WhirConfigBuilder}; @@ -52,6 +52,8 @@ fn main() { let selectors = univariate_selectors::(UNIVARIATE_SKIPS); let input_layers: [_; 16] = array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); + let input_layers_packed: [_; 16] = + array::from_fn(|i| PFPacking::::pack_slice(&input_layers[i]).to_vec()); let initial_full_rounds = KOALABEAR_RC16_EXTERNAL_INITIAL .into_iter() @@ -82,12 +84,10 @@ fn main() { let prover_time = Instant::now(); - let mut verifier_state = { + let (mut verifier_state, proof_size) = { // ---------------------------------------------------- PROVER ---------------------------------------------------- - let initial_full_layer_inputs: [_; 16] = - array::from_fn(|i| PFPacking::::pack_slice(&input_layers[i]).to_vec()); - let mut all_initial_full_layers = vec![initial_full_layer_inputs]; + let mut all_initial_full_layers = vec![input_layers_packed]; for (i, round) in initial_full_rounds.iter().enumerate() { all_initial_full_layers.push(apply_full_round( all_initial_full_layers.last().unwrap(), @@ -336,7 +336,10 @@ fn main() { &global_poly_commited.by_ref(), ); - build_verifier_state(&prover_state) + ( + build_verifier_state(&prover_state), + prover_state.proof_size(), + ) }; let prover_duration = prover_time.elapsed(); @@ -477,11 +480,32 @@ fn main() { } let verifier_duration = verifier_time.elapsed(); - println!("GKR proof for {} Poseidon2:", n_poseidons); + let mut data_to_hash = input_layers.clone(); + let plaintext_time = Instant::now(); + transposed_par_iter_mut(&mut data_to_hash).for_each(|row| { + let mut buff = array::from_fn(|j| *row[j]); + poseidon16_permute_mut(&mut buff); + for j in 0..16 { + *row[j] = buff[j]; + } + }); + let plaintext_duration = plaintext_time.elapsed(); + + println!("{} Poseidon2", n_poseidons); println!( - "Prover time: {:?} ({:.1} Poseidons / s)", + "Plaintext (no proof) time: {:?} ({:.1} Poseidons / s)", + plaintext_duration, + n_poseidons as f64 / plaintext_duration.as_secs_f64() + ); + println!( + "Prover time: {:?} ({:.1} Poseidons / s, {:.1}x slower than plaintext)", prover_duration, - n_poseidons as f64 / prover_duration.as_secs_f64() + n_poseidons as f64 / prover_duration.as_secs_f64(), + prover_duration.as_secs_f64() / plaintext_duration.as_secs_f64() + ); + println!( + "Proof size: {} KiB", + (proof_size * F::bits()) as f64 / (8.0 * 1024.0) ); println!("Verifier time: {:?}", verifier_duration); } From 68738926e2b9e520963c5f3d6f250641f8460db7 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Oct 2025 18:26:05 +0400 Subject: [PATCH 21/42] pretty text --- src/main.rs | 54 ++++++++++++++++++++++------------------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6d7e8da8..baab6954 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,8 @@ use rand::{Rng, SeedableRng, rngs::StdRng}; use std::{array, time::Instant}; use tracing::{info_span, instrument}; use utils::{ - build_prover_state, build_verifier_state, init_tracing, poseidon16_permute, - poseidon16_permute_mut, transposed_par_iter_mut, + build_prover_state, build_verifier_state, init_tracing, poseidon16_permute_mut, + transposed_par_iter_mut, }; use whir_p3::{FoldingFactor, SecurityAssumption, WhirConfig, WhirConfigBuilder}; @@ -19,7 +19,6 @@ type F = KoalaBear; type EF = QuinticExtensionFieldKB; const UNIVARIATE_SKIPS: usize = 3; -const SANITY_CHECK: bool = false; const N_COMMITED_CUBES: usize = 16; // power of 2 to increase PCS efficiency // const N_INITIAL_ROUNDS: usize = KOALABEAR_RC16_EXTERNAL_INITIAL.len(); @@ -82,11 +81,10 @@ fn main() { }) .collect::>(); - let prover_time = Instant::now(); - - let (mut verifier_state, proof_size) = { + let (mut verifier_state, proof_size, output_layers, prover_duration) = { // ---------------------------------------------------- PROVER ---------------------------------------------------- + let prover_time = Instant::now(); let mut all_initial_full_layers = vec![input_layers_packed]; for (i, round) in initial_full_rounds.iter().enumerate() { all_initial_full_layers.push(apply_full_round( @@ -148,21 +146,6 @@ fn main() { let global_poly_commited_packed = PFPacking::::pack_slice(global_poly_commited.as_base().unwrap()); - if SANITY_CHECK { - let perm_outputs = perm_inputs - .par_iter() - .map(|input| poseidon16_permute(*input)) - .collect::>(); - let last_layers: [_; 16] = array::from_fn(|i| { - PFPacking::::unpack_slice(&all_final_full_layers.last().unwrap()[i]) - }); - (0..n_poseidons).into_par_iter().for_each(|row| { - for i in 0..16 { - assert_eq!(perm_outputs[row][i], last_layers[i][row]); - } - }); - } - let output_claim_point = prover_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); let mut output_claims = info_span!("computing output claims").in_scope(|| { @@ -336,14 +319,16 @@ fn main() { &global_poly_commited.by_ref(), ); + let prover_duration = prover_time.elapsed(); + ( build_verifier_state(&prover_state), prover_state.proof_size(), + all_final_full_layers.last().unwrap().clone(), + prover_duration, ) }; - let prover_duration = prover_time.elapsed(); - let verifier_time = Instant::now(); { // ---------------------------------------------------- VERIFIER ---------------------------------------------------- @@ -491,23 +476,28 @@ fn main() { }); let plaintext_duration = plaintext_time.elapsed(); - println!("{} Poseidon2", n_poseidons); + // sanity check: ensure the plaintext poseidons matches the last GKR layer: + output_layers.iter().enumerate().for_each(|(i, layer)| { + assert_eq!(PFPacking::::unpack_slice(&layer), data_to_hash[i]); + }); + + println!("2^{} Poseidon2", log_n_poseidons); println!( - "Plaintext (no proof) time: {:?} ({:.1} Poseidons / s)", - plaintext_duration, - n_poseidons as f64 / plaintext_duration.as_secs_f64() + "Plaintext (no proof) time: {:.3}s ({:.2}M Poseidons / s)", + plaintext_duration.as_secs_f64(), + n_poseidons as f64 / (plaintext_duration.as_secs_f64() * 1e6) ); println!( - "Prover time: {:?} ({:.1} Poseidons / s, {:.1}x slower than plaintext)", - prover_duration, - n_poseidons as f64 / prover_duration.as_secs_f64(), + "Prover time: {:.3}s ({:.2}M Poseidons / s, {:.1}x slower than plaintext)", + prover_duration.as_secs_f64(), + n_poseidons as f64 / (prover_duration.as_secs_f64() * 1e6), prover_duration.as_secs_f64() / plaintext_duration.as_secs_f64() ); println!( - "Proof size: {} KiB", + "Proof size: {:.1} KiB (without merkle pruning)", (proof_size * F::bits()) as f64 / (8.0 * 1024.0) ); - println!("Verifier time: {:?}", verifier_duration); + println!("Verifier time: {}ms", verifier_duration.as_millis()); } #[instrument(skip_all)] From 3494c334067d89593f989601ec0d1a661b8e3c9a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 24 Oct 2025 15:06:03 +0400 Subject: [PATCH 22/42] evaluate_univariate_multilinear --- Cargo.lock | 12 +++--- src/main.rs | 115 +++++++++++++--------------------------------------- 2 files changed, 34 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7467db52..5d7b11d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,7 +59,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backend" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#1d5b5259518a3d74a4cb7d13a9caf65316893a6a" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#abff289607aa2ff52f3b989468a25cc952127030" dependencies = [ "fiat-shamir", "itertools 0.14.0", @@ -155,7 +155,7 @@ dependencies = [ [[package]] name = "constraints-folder" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#1d5b5259518a3d74a4cb7d13a9caf65316893a6a" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#abff289607aa2ff52f3b989468a25cc952127030" dependencies = [ "fiat-shamir", "p3-air", @@ -294,7 +294,7 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "fiat-shamir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/fiat-shamir.git#40d6492b109af9e22a9370a6e674df6f7e4db84e" +source = "git+https://github.com/leanEthereum/fiat-shamir.git#211b12c35c9742c3d2ec0477381954208f97986c" dependencies = [ "p3-challenger", "p3-field", @@ -523,7 +523,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "multilinear-toolkit" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#1d5b5259518a3d74a4cb7d13a9caf65316893a6a" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#abff289607aa2ff52f3b989468a25cc952127030" dependencies = [ "backend", "constraints-folder", @@ -1126,7 +1126,7 @@ checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" [[package]] name = "sumcheck" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#1d5b5259518a3d74a4cb7d13a9caf65316893a6a" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#abff289607aa2ff52f3b989468a25cc952127030" dependencies = [ "backend", "constraints-folder", @@ -1387,7 +1387,7 @@ dependencies = [ [[package]] name = "whir-p3" version = "0.1.0" -source = "git+https://github.com/TomWambsgans/whir-p3?branch=lean-multisig#b2812e409a6465491129db457e3693b0c0127800" +source = "git+https://github.com/TomWambsgans/whir-p3?branch=lean-multisig#61c58fc683faa20bf3e4ac6d5611705f9db3806d" dependencies = [ "itertools 0.14.0", "multilinear-toolkit", diff --git a/src/main.rs b/src/main.rs index baab6954..cb6abe47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,9 @@ use utils::{ build_prover_state, build_verifier_state, init_tracing, poseidon16_permute_mut, transposed_par_iter_mut, }; -use whir_p3::{FoldingFactor, SecurityAssumption, WhirConfig, WhirConfigBuilder}; +use whir_p3::{ + FoldingFactor, SecurityAssumption, WhirConfig, WhirConfigBuilder, precompute_dft_twiddles, +}; type F = KoalaBear; type EF = QuinticExtensionFieldKB; @@ -28,6 +30,7 @@ const N_INTERNAL_ROUNDS: usize = KOALABEAR_RC16_INTERNAL.len(); fn main() { assert!(N_COMMITED_CUBES <= N_INTERNAL_ROUNDS); init_tracing(); + precompute_dft_twiddles::(1 << 24); let log_n_poseidons = 20; @@ -149,18 +152,16 @@ fn main() { let output_claim_point = prover_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); let mut output_claims = info_span!("computing output claims").in_scope(|| { - all_final_full_layers - .last() - .unwrap() - .par_iter() - .map(|output_layer| { - multivariate_eval::<_, _, _, false>( - PFPacking::::unpack_slice(&output_layer), - &output_claim_point, - &selectors, - ) - }) - .collect::>() + batch_evaluate_univariate_multilinear( + &all_final_full_layers + .last() + .unwrap() + .iter() + .map(|l| PFPacking::::unpack_slice(l)) + .collect::>(), + &output_claim_point, + &selectors, + ) }); prover_state.add_extension_scalars(&output_claims); @@ -203,7 +204,6 @@ fn main() { ); let pcs_point_for_cubes = claim_point.clone(); - let pcs_evals_for_cubes = output_claims[16..].to_vec(); output_claims = output_claims[..16].to_vec(); @@ -222,7 +222,6 @@ fn main() { } let pcs_point_for_inputs = claim_point.clone(); - let pcs_evals_for_inputs = output_claims.to_vec(); // PCS opening let mut pcs_statements = vec![]; @@ -252,22 +251,6 @@ fn main() { ), value: inner_evals_inputs.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs)), }); - { - // sanity check - for (&eval, inner_evals) in pcs_evals_for_inputs - .iter() - .zip(inner_evals_inputs.chunks_exact(1 << UNIVARIATE_SKIPS)) - { - assert_eq!( - eval, - multivariate_eval::<_, _, _, false>( - inner_evals, - &pcs_point_for_inputs[..1], - &selectors - ) - ); - } - } let eq_mle_cubes = eval_eq_packed(&pcs_point_for_cubes[1..]); let inner_evals_cubes = global_poly_commited_packed @@ -295,22 +278,6 @@ fn main() { ), value: inner_evals_cubes.evaluate(&MultilinearPoint(pcs_batching_scalars_cubes)), }); - { - // sanity check - for (&eval, inner_evals) in pcs_evals_for_cubes - .iter() - .zip(inner_evals_cubes.chunks_exact(1 << UNIVARIATE_SKIPS)) - { - assert_eq!( - eval, - multivariate_eval::<_, _, _, false>( - inner_evals, - &pcs_point_for_cubes[..1], - &selectors - ) - ); - } - } whir_config.prove( &mut prover_state, @@ -417,10 +384,11 @@ fn main() { { assert_eq!( eval, - multivariate_eval::<_, _, _, false>( + evaluate_univariate_multilinear::<_, _, _, false>( inner_evals, &pcs_point_for_inputs[..1], - &selectors + &selectors, + None ) ); } @@ -443,17 +411,17 @@ fn main() { value: inner_evals_cubes.evaluate(&MultilinearPoint(pcs_batching_scalars_cubes)), }); { - // sanity check for (&eval, inner_evals) in pcs_evals_for_cubes .iter() .zip(inner_evals_cubes.chunks_exact(1 << UNIVARIATE_SKIPS)) { assert_eq!( eval, - multivariate_eval::<_, _, _, false>( + evaluate_univariate_multilinear::<_, _, _, false>( inner_evals, &pcs_point_for_cubes[..1], - &selectors + &selectors, + None ) ); } @@ -637,16 +605,14 @@ fn prove_internal_rounds_with_committed_cube( assert_eq!(committed_cubes.len(), N_COMMITED_CUBES); let cubes_evals = info_span!("computing cube evals").in_scope(|| { - committed_cubes - .par_iter() - .map(|layer| { - multivariate_eval::<_, _, _, false>( - PFPacking::::unpack_slice(&layer), - &claim_point, - selectors, - ) - }) - .collect::>() + batch_evaluate_univariate_multilinear( + &committed_cubes + .iter() + .map(|l| PFPacking::::unpack_slice(l)) + .collect::>(), + &claim_point, + selectors, + ) }); prover_state.add_extension_scalars(&cubes_evals); @@ -726,31 +692,6 @@ fn verify_gkr_round>( (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) } -fn multivariate_eval< - F: Field, - NF: ExtensionField, - EF: ExtensionField + ExtensionField, - const PARALLEL: bool, ->( - poly: &[NF], - point: &[EF], - selectors: &[DensePolynomial], -) -> EF { - assert_eq!(poly.len(), 1 << (point.len() + UNIVARIATE_SKIPS - 1)); - selectors - .iter() - .zip(poly.chunks_exact(1 << (point.len() - 1))) - .map(|(selector, chunk)| { - selector.evaluate(point[0]) - * if PARALLEL { - chunk.evaluate(&MultilinearPoint(point[1..].to_vec())) - } else { - chunk.evaluate_sequential(&MultilinearPoint(point[1..].to_vec())) - } - }) - .sum() -} - pub struct FullRoundComputation { pub constants: [F; 16], pub first_full_round: bool, From a10ebe064d61cce5e2360dbc1d17d6aac2c0b5a9 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 24 Oct 2025 16:24:20 +0400 Subject: [PATCH 23/42] create a dedicated "poseidon_circuit" crate --- Cargo.lock | 20 +- Cargo.toml | 8 +- benches/poseidon2.rs | 42 - crates/air/Cargo.toml | 7 +- crates/air/src/examples/mod.rs | 4 - crates/air/src/examples/prove_poseidon2.rs | 358 ------- crates/air/src/lib.rs | 3 +- .../src/{examples/simple_air.rs => tests.rs} | 0 crates/lean_prover/tests/hash_chain.rs | 22 - crates/poseidon_circuit/Cargo.toml | 22 + .../src/gkr_layers/batch_partial_rounds.rs | 101 ++ .../src/gkr_layers/full_round.rs | 87 ++ crates/poseidon_circuit/src/gkr_layers/mod.rs | 87 ++ .../src/gkr_layers/partial_round.rs | 82 ++ crates/poseidon_circuit/src/lib.rs | 20 + crates/poseidon_circuit/src/prove.rs | 0 crates/poseidon_circuit/src/tests.rs | 569 +++++++++++ crates/poseidon_circuit/src/verify.rs | 0 crates/poseidon_circuit/src/witness_gen.rs | 167 ++++ src/main.rs | 914 +----------------- 20 files changed, 1161 insertions(+), 1352 deletions(-) delete mode 100644 benches/poseidon2.rs delete mode 100644 crates/air/src/examples/mod.rs delete mode 100644 crates/air/src/examples/prove_poseidon2.rs rename crates/air/src/{examples/simple_air.rs => tests.rs} (100%) create mode 100644 crates/poseidon_circuit/Cargo.toml create mode 100644 crates/poseidon_circuit/src/gkr_layers/batch_partial_rounds.rs create mode 100644 crates/poseidon_circuit/src/gkr_layers/full_round.rs create mode 100644 crates/poseidon_circuit/src/gkr_layers/mod.rs create mode 100644 crates/poseidon_circuit/src/gkr_layers/partial_round.rs create mode 100644 crates/poseidon_circuit/src/lib.rs create mode 100644 crates/poseidon_circuit/src/prove.rs create mode 100644 crates/poseidon_circuit/src/tests.rs create mode 100644 crates/poseidon_circuit/src/verify.rs create mode 100644 crates/poseidon_circuit/src/witness_gen.rs diff --git a/Cargo.lock b/Cargo.lock index 5d7b11d4..916d4225 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,11 +22,9 @@ dependencies = [ "p3-matrix", "p3-uni-stark", "p3-util", - "packed_pcs", "rand", "tracing", "utils", - "whir-p3", ] [[package]] @@ -381,6 +379,7 @@ dependencies = [ "p3-uni-stark", "p3-util", "packed_pcs", + "poseidon_circuit", "rand", "rec_aggregation", "tracing", @@ -893,6 +892,23 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "poseidon_circuit" +version = "0.1.0" +dependencies = [ + "multilinear-toolkit", + "p3-field", + "p3-koala-bear", + "p3-matrix", + "p3-monty-31", + "p3-poseidon2", + "p3-util", + "rand", + "tracing", + "utils", + "whir-p3", +] + [[package]] name = "ppv-lite86" version = "0.2.21" diff --git a/Cargo.toml b/Cargo.toml index 4d646992..ee980111 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ lean_prover = { path = "crates/lean_prover" } rec_aggregation = { path = "crates/rec_aggregation" } witness_generation = { path = "crates/lean_prover/witness_generation" } vm_air = { path = "crates/lean_prover/vm_air" } +poseidon_circuit = { path = "crates/poseidon_circuit" } # External thiserror = "2.0" @@ -78,12 +79,14 @@ p3-poseidon2-air = { git = "https://github.com/TomWambsgans/Plonky3.git", branch p3-goldilocks = { git = "https://github.com/TomWambsgans/Plonky3.git", branch = "lean-multisig" } p3-challenger = { git = "https://github.com/TomWambsgans/Plonky3.git", branch = "lean-multisig" } p3-util = { git = "https://github.com/TomWambsgans/Plonky3.git", branch = "lean-multisig" } +p3-monty-31 = { git = "https://github.com/TomWambsgans/Plonky3.git", branch = "lean-multisig" } whir-p3 = { git = "https://github.com/TomWambsgans/whir-p3", branch = "lean-multisig" } multilinear-toolkit = { git = "https://github.com/leanEthereum/multilinear-toolkit.git" } [dependencies] air.workspace = true +poseidon_circuit.workspace = true p3-field.workspace = true p3-koala-bear.workspace = true p3-poseidon2.workspace = true @@ -111,6 +114,7 @@ multilinear-toolkit.workspace = true # p3-poseidon2-air = { path = "../zk/Plonky3/poseidon2-air" } # p3-dft = { path = "../zk/Plonky3/dft" } # p3-challenger = { path = "../zk/Plonky3/challenger" } +# p3-monty-31 = { path = "../zk/Plonky3/monty-31" } # [patch."https://github.com/TomWambsgans/whir-p3.git"] # whir-p3 = { path = "../zk/whir/fork-whir-p3" } @@ -125,10 +129,6 @@ rec_aggregation.workspace = true [profile.release] lto = "thin" -[[bench]] -name = "poseidon2" -harness = false - [[bench]] name = "recursion" harness = false diff --git a/benches/poseidon2.rs b/benches/poseidon2.rs deleted file mode 100644 index 7d2d9e55..00000000 --- a/benches/poseidon2.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::{hint::black_box, time::Duration}; - -use air::examples::prove_poseidon2::{Poseidon2Config, prove_poseidon2}; -use criterion::{Criterion, Throughput, criterion_group, criterion_main}; -use whir_p3::{FoldingFactor, SecurityAssumption}; - -fn bench_poseidon2(c: &mut Criterion) { - const L16: usize = 17; - const L24: usize = 17; - - let mut group = c.benchmark_group("poseidon2"); - group.sample_size(10); - group.measurement_time(Duration::from_secs(60)); - group.warm_up_time(Duration::from_secs(10)); - group.throughput(Throughput::Elements((1u64 << L16) + (1u64 << L24))); - - let config = Poseidon2Config { - log_n_poseidons_16: L16, - log_n_poseidons_24: L24, - univariate_skips: 4, - folding_factor: FoldingFactor::new(7, 4), - log_inv_rate: 1, - soundness_type: SecurityAssumption::CapacityBound, - pow_bits: 16, - security_level: 128, - rs_domain_initial_reduction_factor: 5, - max_num_variables_to_send_coeffs: 3, - display_logs: false, - }; - - group.bench_function("poseidon2", |b| { - b.iter(|| { - let result = prove_poseidon2(black_box(&config)); - black_box(result.prover_time); - }); - }); - - group.finish(); -} - -criterion_group!(benches, bench_poseidon2); -criterion_main!(benches); diff --git a/crates/air/Cargo.toml b/crates/air/Cargo.toml index 82cc89d5..04d287b5 100644 --- a/crates/air/Cargo.toml +++ b/crates/air/Cargo.toml @@ -15,13 +15,8 @@ p3-uni-stark.workspace = true p3-matrix.workspace = true p3-util.workspace = true multilinear-toolkit.workspace = true -p3-koala-bear.workspace = true -rand.workspace = true -whir-p3.workspace = true -packed_pcs.workspace = true - [dev-dependencies] p3-koala-bear.workspace = true p3-matrix.workspace = true -rand.workspace = true +rand.workspace = true \ No newline at end of file diff --git a/crates/air/src/examples/mod.rs b/crates/air/src/examples/mod.rs deleted file mode 100644 index fb5c0672..00000000 --- a/crates/air/src/examples/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[cfg(test)] -mod simple_air; - -pub mod prove_poseidon2; diff --git a/crates/air/src/examples/prove_poseidon2.rs b/crates/air/src/examples/prove_poseidon2.rs deleted file mode 100644 index 18ef7c6f..00000000 --- a/crates/air/src/examples/prove_poseidon2.rs +++ /dev/null @@ -1,358 +0,0 @@ -use crate::table::AirTable; -use multilinear_toolkit::prelude::*; -use p3_air::BaseAir; -use p3_field::PrimeField64; -use p3_koala_bear::{KoalaBear, QuinticExtensionFieldKB}; -use packed_pcs::{ - ColDims, packed_pcs_commit, packed_pcs_global_statements_for_prover, - packed_pcs_global_statements_for_verifier, packed_pcs_parse_commitment, -}; -use rand::{Rng, SeedableRng, rngs::StdRng}; -use std::collections::BTreeMap; -use std::fmt; -use std::time::{Duration, Instant}; -use utils::{ - MyChallenger, Poseidon16Air, Poseidon24Air, build_poseidon_16_air, - build_poseidon_16_air_packed, build_poseidon_24_air, build_poseidon_24_air_packed, - build_prover_state, build_verifier_state, generate_trace_poseidon_16, - generate_trace_poseidon_24, init_tracing, poseidon16_permute, poseidon24_permute, -}; -use whir_p3::{ - FoldingFactor, SecurityAssumption, WhirConfig, WhirConfigBuilder, precompute_dft_twiddles, -}; - -type F = KoalaBear; -type EF = QuinticExtensionFieldKB; - -#[derive(Clone, Debug)] -pub struct Poseidon2Benchmark { - pub log_n_poseidons_16: usize, - pub log_n_poseidons_24: usize, - pub prover_time: Duration, - pub verifier_time: Duration, - pub proof_size: f64, // in bytes -} - -impl fmt::Display for Poseidon2Benchmark { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!( - f, - "Proved {} poseidon2-16 and {} poseidon2-24 in {:.3} s ({} / s)", - 1 << self.log_n_poseidons_16, - 1 << self.log_n_poseidons_24, - self.prover_time.as_millis() as f64 / 1000.0, - (f64::from((1 << self.log_n_poseidons_16) + (1 << self.log_n_poseidons_24)) - / self.prover_time.as_secs_f64()) - .round() as usize - )?; - writeln!( - f, - "Proof size: {:.1} KiB (not optimized)", - self.proof_size / 1024.0 - )?; - writeln!(f, "Verification: {} ms", self.verifier_time.as_millis()) - } -} - -#[derive(Clone, Debug)] -pub struct Poseidon2Config { - pub log_n_poseidons_16: usize, - pub log_n_poseidons_24: usize, - pub univariate_skips: usize, - pub folding_factor: FoldingFactor, - pub log_inv_rate: usize, - pub soundness_type: SecurityAssumption, - pub pow_bits: usize, - pub security_level: usize, - pub rs_domain_initial_reduction_factor: usize, - pub max_num_variables_to_send_coeffs: usize, - pub display_logs: bool, -} - -struct PoseidonSetup { - n_columns_16: usize, - n_columns_24: usize, - witness_columns_16: Vec>, - witness_columns_24: Vec>, - table_16: AirTable, Poseidon16Air>>, - table_24: AirTable, Poseidon24Air>>, -} - -struct ProverArtifacts { - prover_time: Duration, - whir_config_builder: WhirConfigBuilder, - whir_config: WhirConfig, - dims: Vec>, -} - -fn prepare_poseidon(config: &Poseidon2Config) -> PoseidonSetup { - let n_poseidons_16 = 1 << config.log_n_poseidons_16; - let n_poseidons_24 = 1 << config.log_n_poseidons_24; - - let poseidon_air_16 = build_poseidon_16_air(); - let poseidon_air_16_packed = build_poseidon_16_air_packed(); - let poseidon_air_24 = build_poseidon_24_air(); - let poseidon_air_24_packed = build_poseidon_24_air_packed(); - - let n_columns_16 = poseidon_air_16.width(); - let n_columns_24 = poseidon_air_24.width(); - - let mut rng = StdRng::seed_from_u64(0); - let inputs_16: Vec<[F; 16]> = (0..n_poseidons_16).map(|_| Default::default()).collect(); - let inputs_24: Vec<[F; 24]> = (0..n_poseidons_24) - .map(|_| std::array::from_fn(|_| rng.random())) - .collect(); - let compress = vec![true; n_poseidons_16]; - let index_res: Vec = vec![0; n_poseidons_16]; // unused - - let witness_matrix_16 = generate_trace_poseidon_16(&inputs_16, &compress, &index_res); - let witness_matrix_24 = generate_trace_poseidon_24(&inputs_24); - - assert_eq!( - &witness_matrix_16.values[n_columns_16 - 16..n_columns_16 - 8], - &poseidon16_permute(witness_matrix_16.values[0..16].try_into().unwrap())[..8] - ); - assert_eq!( - &witness_matrix_24.values[n_columns_24 - 8..n_columns_24], - &poseidon24_permute(witness_matrix_24.values[0..24].try_into().unwrap())[16..] - ); - - let witness_matrix_16_transposed = witness_matrix_16.transpose(); - let witness_matrix_24_transposed = witness_matrix_24.transpose(); - - let witness_columns_16 = (0..n_columns_16) - .map(|row| { - witness_matrix_16_transposed.values[row * n_poseidons_16..(row + 1) * n_poseidons_16] - .to_vec() - }) - .collect::>(); - let witness_columns_24 = (0..n_columns_24) - .map(|row| { - witness_matrix_24_transposed.values[row * n_poseidons_24..(row + 1) * n_poseidons_24] - .to_vec() - }) - .collect::>(); - - let table_16: AirTable, Poseidon16Air>> = - AirTable::new(poseidon_air_16, poseidon_air_16_packed); - let table_24: AirTable, Poseidon24Air>> = - AirTable::new(poseidon_air_24, poseidon_air_24_packed); - - PoseidonSetup { - n_columns_16, - n_columns_24, - witness_columns_16, - witness_columns_24, - table_16, - table_24, - } -} - -fn run_prover_phase( - config: &Poseidon2Config, - setup: &PoseidonSetup, - witness_16: &[&[F]], - witness_24: &[&[F]], - prover_state: &mut FSProver, -) -> ProverArtifacts { - let start = Instant::now(); - - let whir_config_builder = WhirConfigBuilder { - folding_factor: config.folding_factor, - soundness_type: config.soundness_type, - pow_bits: config.pow_bits, - max_num_variables_to_send_coeffs: config.max_num_variables_to_send_coeffs, - rs_domain_initial_reduction_factor: config.rs_domain_initial_reduction_factor, - security_level: config.security_level, - starting_log_inv_rate: config.log_inv_rate, - }; - - precompute_dft_twiddles::(1 << 24); - - let dims = [ - vec![ColDims::full(config.log_n_poseidons_16); setup.n_columns_16], - vec![ColDims::full(config.log_n_poseidons_24); setup.n_columns_24], - ] - .concat(); - let log_smallest_decomposition_chunk = 0; - let commited_slices = setup - .witness_columns_16 - .iter() - .chain(setup.witness_columns_24.iter()) - .map(Vec::as_slice) - .collect::>(); - - let commitment_witness = packed_pcs_commit( - &whir_config_builder, - &commited_slices, - &dims, - prover_state, - log_smallest_decomposition_chunk, - ); - - let (p16_point, evaluations_remaining_to_prove_16) = - setup - .table_16 - .prove_base(prover_state, config.univariate_skips, witness_16); - let (p24_point, evaluations_remaining_to_prove_24) = - setup - .table_24 - .prove_base(prover_state, config.univariate_skips, witness_24); - - let global_statements_to_prove = packed_pcs_global_statements_for_prover( - &commited_slices, - &dims, - log_smallest_decomposition_chunk, - &evaluations_remaining_to_prove_16 - .into_iter() - .map(|v| vec![Evaluation::new(p16_point.clone(), v)]) - .chain( - evaluations_remaining_to_prove_24 - .into_iter() - .map(|v| vec![Evaluation::new(p24_point.clone(), v)]), - ) - .collect::>(), - prover_state, - ); - let whir_config = WhirConfig::new( - whir_config_builder.clone(), - commitment_witness.packed_polynomial.by_ref().n_vars(), - ); - whir_config.prove( - prover_state, - global_statements_to_prove, - commitment_witness.inner_witness, - &commitment_witness.packed_polynomial.by_ref(), - ); - - ProverArtifacts { - prover_time: start.elapsed(), - whir_config_builder, - whir_config, - dims, - } -} - -fn run_verifier_phase( - config: &Poseidon2Config, - setup: &PoseidonSetup, - artifacts: &ProverArtifacts, - prover_state: &FSProver, -) -> Duration { - let start = Instant::now(); - let mut verifier_state = build_verifier_state(prover_state); - let log_smallest_decomposition_chunk = 0; // unused (everything is power of two) - - let packed_parsed_commitment = packed_pcs_parse_commitment( - &artifacts.whir_config_builder, - &mut verifier_state, - &artifacts.dims, - log_smallest_decomposition_chunk, - ) - .unwrap(); - - let (p16_point, evaluations_remaining_to_verify_16) = setup - .table_16 - .verify( - &mut verifier_state, - config.univariate_skips, - config.log_n_poseidons_16, - ) - .unwrap(); - let (p24_point, evaluations_remaining_to_verify_24) = setup - .table_24 - .verify( - &mut verifier_state, - config.univariate_skips, - config.log_n_poseidons_24, - ) - .unwrap(); - - let global_statements_to_verify = packed_pcs_global_statements_for_verifier( - &artifacts.dims, - log_smallest_decomposition_chunk, - &evaluations_remaining_to_verify_16 - .into_iter() - .map(|v| vec![Evaluation::new(p16_point.clone(), v)]) - .chain( - evaluations_remaining_to_verify_24 - .into_iter() - .map(|v| vec![Evaluation::new(p24_point.clone(), v)]), - ) - .collect::>(), - &mut verifier_state, - &BTreeMap::default(), - ) - .unwrap(); - artifacts - .whir_config - .verify( - &mut verifier_state, - &packed_parsed_commitment, - global_statements_to_verify, - ) - .unwrap(); - - start.elapsed() -} - -pub fn prove_poseidon2(config: &Poseidon2Config) -> Poseidon2Benchmark { - if config.display_logs { - init_tracing(); - } - - let setup = prepare_poseidon(config); - - let mut prover_state = build_prover_state(); - let artifacts = run_prover_phase( - config, - &setup, - &setup - .witness_columns_16 - .iter() - .map(Vec::as_slice) - .collect::>(), - &setup - .witness_columns_24 - .iter() - .map(Vec::as_slice) - .collect::>(), - &mut prover_state, - ); - let verifier_time = run_verifier_phase(config, &setup, &artifacts, &prover_state); - - let proof_size = prover_state.proof_data().len() as f64 * (F::ORDER_U64 as f64).log2() / 8.0; - - Poseidon2Benchmark { - log_n_poseidons_16: config.log_n_poseidons_16, - log_n_poseidons_24: config.log_n_poseidons_24, - prover_time: artifacts.prover_time, - verifier_time, - proof_size, - } -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn test_prove_poseidon2() { - let config = Poseidon2Config { - log_n_poseidons_16: 13, - log_n_poseidons_24: 12, - univariate_skips: 4, - folding_factor: FoldingFactor::new(5, 3), - log_inv_rate: 2, - soundness_type: SecurityAssumption::CapacityBound, - pow_bits: 13, - security_level: 128, - rs_domain_initial_reduction_factor: 1, - max_num_variables_to_send_coeffs: 7, - display_logs: false, - }; - let benchmark = prove_poseidon2(&config); - println!("\n{benchmark}"); - } -} diff --git a/crates/air/src/lib.rs b/crates/air/src/lib.rs index 191d066f..387e8cc7 100644 --- a/crates/air/src/lib.rs +++ b/crates/air/src/lib.rs @@ -12,7 +12,8 @@ mod uni_skip_utils; mod utils; mod verify; -pub mod examples; +#[cfg(test)] +pub mod tests; pub trait NormalAir>>: Air>> diff --git a/crates/air/src/examples/simple_air.rs b/crates/air/src/tests.rs similarity index 100% rename from crates/air/src/examples/simple_air.rs rename to crates/air/src/tests.rs diff --git a/crates/lean_prover/tests/hash_chain.rs b/crates/lean_prover/tests/hash_chain.rs index 06afb886..75992ee6 100644 --- a/crates/lean_prover/tests/hash_chain.rs +++ b/crates/lean_prover/tests/hash_chain.rs @@ -1,13 +1,11 @@ use std::time::Instant; -use air::examples::prove_poseidon2::{Poseidon2Config, prove_poseidon2}; use lean_compiler::*; use lean_prover::{ prove_execution::prove_execution, verify_execution::verify_execution, whir_config_builder, }; use lean_vm::{F, execute_bytecode}; use p3_field::PrimeCharacteristicRing; -use whir_p3::{FoldingFactor, SecurityAssumption}; use xmss::iterate_hash; #[test] @@ -87,25 +85,5 @@ fn benchmark_poseidon_chain() { let vm_time = time.elapsed(); verify_execution(&bytecode, &public_input, proof_data, whir_config_builder()).unwrap(); - let raw_proof = prove_poseidon2(&Poseidon2Config { - log_n_poseidons_16: LOG_CHAIN_LENGTH, - log_n_poseidons_24: 10, // (almost invisible cost) - univariate_skips: 4, - folding_factor: FoldingFactor::new(7, 4), - log_inv_rate: 1, - soundness_type: SecurityAssumption::CapacityBound, - pow_bits: 16, - security_level: 128, - rs_domain_initial_reduction_factor: 5, - max_num_variables_to_send_coeffs: 7, - display_logs: true, - }); - println!("VM proof time: {vm_time:?}"); - println!("Raw Poseidon proof time: {:?}", raw_proof.prover_time); - - println!( - "VM overhead: {:.2}x", - vm_time.as_secs_f64() / raw_proof.prover_time.as_secs_f64() - ); } diff --git a/crates/poseidon_circuit/Cargo.toml b/crates/poseidon_circuit/Cargo.toml new file mode 100644 index 00000000..6726708b --- /dev/null +++ b/crates/poseidon_circuit/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "poseidon_circuit" +version.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +p3-field.workspace = true +tracing.workspace = true +utils.workspace = true +p3-util.workspace = true +multilinear-toolkit.workspace = true +p3-koala-bear.workspace = true +p3-poseidon2.workspace = true +p3-monty-31.workspace = true + +[dev-dependencies] +p3-matrix.workspace = true +rand.workspace = true +whir-p3.workspace = true \ No newline at end of file diff --git a/crates/poseidon_circuit/src/gkr_layers/batch_partial_rounds.rs b/crates/poseidon_circuit/src/gkr_layers/batch_partial_rounds.rs new file mode 100644 index 00000000..f806eed9 --- /dev/null +++ b/crates/poseidon_circuit/src/gkr_layers/batch_partial_rounds.rs @@ -0,0 +1,101 @@ +use std::array; + +use multilinear_toolkit::prelude::*; +use p3_field::ExtensionField; +use p3_koala_bear::{ + GenericPoseidon2LinearLayersKoalaBear, KoalaBearInternalLayerParameters, KoalaBearParameters, +}; +use p3_monty_31::InternalLayerBaseParameters; +use p3_poseidon2::GenericPoseidon2LinearLayers; + +use crate::{EF, F}; + +#[derive(Debug)] +pub struct BatchPartialRounds { + pub constants: [F; N_COMMITED_CUBES], + pub last_constant: F, +} + +impl, const WIDTH: usize, const N_COMMITED_CUBES: usize> + SumcheckComputation for BatchPartialRounds +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, + EF: ExtensionField, +{ + fn degree(&self) -> usize { + 3 + } + + fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { + debug_assert_eq!(point.len(), WIDTH + N_COMMITED_CUBES); + debug_assert_eq!(alpha_powers.len(), WIDTH + N_COMMITED_CUBES); + + let mut res = EF::ZERO; + let mut buff: [NF; WIDTH] = array::from_fn(|j| point[j]); + for (i, &constant) in self.constants.iter().enumerate() { + let computed_cube = (buff[0] + constant).cube(); + res += alpha_powers[WIDTH + i] * computed_cube; + buff[0] = point[WIDTH + i]; // commited cube + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + } + + buff[0] = (buff[0] + self.last_constant).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + for i in 0..WIDTH { + res += alpha_powers[i] * buff[i]; + } + res + } +} + +impl SumcheckComputationPacked + for BatchPartialRounds +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + fn degree(&self) -> usize { + 3 + } + + fn eval_packed_base(&self, point: &[FPacking], alpha_powers: &[EF]) -> EFPacking { + debug_assert_eq!(point.len(), WIDTH + N_COMMITED_CUBES); + debug_assert_eq!(alpha_powers.len(), WIDTH + N_COMMITED_CUBES); + + let mut res = EFPacking::::ZERO; + let mut buff: [FPacking; WIDTH] = array::from_fn(|j| point[j]); + for (i, &constant) in self.constants.iter().enumerate() { + let computed_cube = (buff[0] + constant).cube(); + res += EFPacking::::from(alpha_powers[WIDTH + i]) * computed_cube; + buff[0] = point[WIDTH + i]; // commited cube + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + } + + buff[0] = (buff[0] + self.last_constant).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + for i in 0..WIDTH { + res += EFPacking::::from(alpha_powers[i]) * buff[i]; + } + res + } + + fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { + debug_assert_eq!(point.len(), WIDTH + N_COMMITED_CUBES); + debug_assert_eq!(alpha_powers.len(), WIDTH + N_COMMITED_CUBES); + + let mut res = EFPacking::::ZERO; + let mut buff: [EFPacking; WIDTH] = array::from_fn(|j| point[j]); + for (i, &constant) in self.constants.iter().enumerate() { + let computed_cube = (buff[0] + PFPacking::::from(constant)).cube(); + res += computed_cube * alpha_powers[WIDTH + i]; + buff[0] = point[WIDTH + i]; // commited cube + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + } + + buff[0] = (buff[0] + PFPacking::::from(self.last_constant)).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + for i in 0..WIDTH { + res += buff[i] * alpha_powers[i]; + } + res + } +} diff --git a/crates/poseidon_circuit/src/gkr_layers/full_round.rs b/crates/poseidon_circuit/src/gkr_layers/full_round.rs new file mode 100644 index 00000000..f48e1a21 --- /dev/null +++ b/crates/poseidon_circuit/src/gkr_layers/full_round.rs @@ -0,0 +1,87 @@ +use std::array; + +use multilinear_toolkit::prelude::*; +use p3_field::ExtensionField; +use p3_koala_bear::{ + GenericPoseidon2LinearLayersKoalaBear, KoalaBearInternalLayerParameters, KoalaBearParameters, +}; +use p3_monty_31::InternalLayerBaseParameters; +use p3_poseidon2::GenericPoseidon2LinearLayers; + +use crate::{EF, F}; + +#[derive(Debug)] +pub struct FullRoundComputation { + pub constants: [F; WIDTH], +} + +impl, const WIDTH: usize, const FIRST: bool> SumcheckComputation + for FullRoundComputation +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, + EF: ExtensionField, +{ + fn degree(&self) -> usize { + 3 + } + + fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { + debug_assert_eq!(point.len(), WIDTH); + let mut buff: [NF; WIDTH] = array::from_fn(|j| point[j]); + if FIRST { + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + } + buff.iter_mut().enumerate().for_each(|(j, val)| { + *val = (*val + self.constants[j]).cube(); + }); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + let mut res = EF::ZERO; + for i in 0..WIDTH { + res += alpha_powers[i] * buff[i]; + } + res + } +} + +impl SumcheckComputationPacked for FullRoundComputation +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + fn degree(&self) -> usize { + 3 + } + + fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { + debug_assert_eq!(point.len(), WIDTH); + let mut buff: [PFPacking; WIDTH] = array::from_fn(|j| point[j]); + if FIRST { + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + } + buff.iter_mut().enumerate().for_each(|(j, val)| { + *val = (*val + self.constants[j]).cube(); + }); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + let mut res = EFPacking::::ZERO; + for j in 0..WIDTH { + res += EFPacking::::from(alpha_powers[j]) * buff[j]; + } + res + } + + fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { + debug_assert_eq!(point.len(), WIDTH); + let mut buff: [EFPacking; WIDTH] = array::from_fn(|j| point[j]); + if FIRST { + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + } + buff.iter_mut().enumerate().for_each(|(j, val)| { + *val = (*val + PFPacking::::from(self.constants[j])).cube(); + }); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + let mut res = EFPacking::::ZERO; + for j in 0..WIDTH { + res += buff[j] * alpha_powers[j]; + } + res + } +} diff --git a/crates/poseidon_circuit/src/gkr_layers/mod.rs b/crates/poseidon_circuit/src/gkr_layers/mod.rs new file mode 100644 index 00000000..4b81f806 --- /dev/null +++ b/crates/poseidon_circuit/src/gkr_layers/mod.rs @@ -0,0 +1,87 @@ +mod full_round; +pub use full_round::*; + +mod partial_round; +pub use partial_round::*; + +mod batch_partial_rounds; +pub use batch_partial_rounds::*; + +use p3_koala_bear::{ + KOALABEAR_RC16_EXTERNAL_FINAL, KOALABEAR_RC16_EXTERNAL_INITIAL, KOALABEAR_RC16_INTERNAL, + KOALABEAR_RC24_EXTERNAL_FINAL, KOALABEAR_RC24_EXTERNAL_INITIAL, KOALABEAR_RC24_INTERNAL, +}; + +use crate::F; + +#[derive(Debug)] +pub struct PoseidonGKRLayers { + pub initial_full_round: FullRoundComputation, + pub initial_full_rounds_remaining: Vec>, + pub batch_partial_rounds: BatchPartialRounds, + pub partial_rounds_remaining: Vec>, + pub final_full_rounds: Vec>, +} + +impl PoseidonGKRLayers { + pub fn build() -> Self { + match WIDTH { + 16 => { + unsafe { + Self::build_generic( + &*(&KOALABEAR_RC16_EXTERNAL_INITIAL as *const [[F; 16]] + as *const [[F; WIDTH]]), + &KOALABEAR_RC16_INTERNAL, + &*(&KOALABEAR_RC16_EXTERNAL_FINAL as *const [[F; 16]] + as *const [[F; WIDTH]]), + ) + } + } + 24 => { + unsafe { + Self::build_generic( + &*(&KOALABEAR_RC24_EXTERNAL_INITIAL as *const [[F; 24]] + as *const [[F; WIDTH]]), + &KOALABEAR_RC24_INTERNAL, + &*(&KOALABEAR_RC24_EXTERNAL_FINAL as *const [[F; 24]] + as *const [[F; WIDTH]]), + ) + } + } + _ => panic!("Only Poseidon 16 and 24 are supported currently"), + } + } + + fn build_generic( + initial_constants: &[[F; WIDTH]], + internal_constants: &[F], + final_constants: &[[F; WIDTH]], + ) -> Self { + let initial_full_round = FullRoundComputation { + constants: initial_constants[0], + }; + let initial_full_rounds_remaining = initial_constants[1..] + .iter() + .map(|&constants| FullRoundComputation { constants }) + .collect::>(); + let batch_partial_rounds = BatchPartialRounds { + constants: internal_constants[..N_COMMITED_CUBES].try_into().unwrap(), + last_constant: internal_constants[N_COMMITED_CUBES], + }; + let partial_rounds_remaining = internal_constants[N_COMMITED_CUBES + 1..] + .iter() + .map(|&constant| PartialRoundComputation { constant }) + .collect::>(); + let final_full_rounds = final_constants + .iter() + .map(|&constants| FullRoundComputation { constants }) + .collect::>(); + Self { + initial_full_round, + initial_full_rounds_remaining, + batch_partial_rounds, + partial_rounds_remaining, + final_full_rounds, + } + } +} diff --git a/crates/poseidon_circuit/src/gkr_layers/partial_round.rs b/crates/poseidon_circuit/src/gkr_layers/partial_round.rs new file mode 100644 index 00000000..7cdd6a55 --- /dev/null +++ b/crates/poseidon_circuit/src/gkr_layers/partial_round.rs @@ -0,0 +1,82 @@ +use multilinear_toolkit::prelude::*; +use p3_field::ExtensionField; +use p3_koala_bear::{ + GenericPoseidon2LinearLayersKoalaBear, KoalaBearInternalLayerParameters, KoalaBearParameters, +}; +use p3_monty_31::InternalLayerBaseParameters; +use p3_poseidon2::GenericPoseidon2LinearLayers; + +use crate::{EF, F}; + +#[derive(Debug)] +pub struct PartialRoundComputation { + pub constant: F, +} + +impl, const WIDTH: usize> SumcheckComputation + for PartialRoundComputation +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, + EF: ExtensionField, +{ + fn degree(&self) -> usize { + 3 + } + + fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { + debug_assert_eq!(point.len(), WIDTH); + let first_cubed = (point[0] + self.constant).cube(); + let mut buff = [NF::ZERO; WIDTH]; + buff[0] = first_cubed; + for j in 1..WIDTH { + buff[j] = point[j]; + } + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + let mut res = EF::ZERO; + for i in 0..WIDTH { + res += alpha_powers[i] * buff[i]; + } + res + } +} + +impl SumcheckComputationPacked for PartialRoundComputation +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + fn degree(&self) -> usize { + 3 + } + + fn eval_packed_base(&self, point: &[FPacking], alpha_powers: &[EF]) -> EFPacking { + debug_assert_eq!(point.len(), WIDTH); + let first_cubed = (point[0] + self.constant).cube(); + let mut buff = [PFPacking::::ZERO; WIDTH]; + buff[0] = first_cubed; + for j in 1..WIDTH { + buff[j] = point[j]; + } + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + let mut res = EFPacking::::ZERO; + for j in 0..WIDTH { + res += EFPacking::::from(alpha_powers[j]) * buff[j]; + } + res + } + + fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { + debug_assert_eq!(point.len(), WIDTH); + let first_cubed = (point[0] + PFPacking::::from(self.constant)).cube(); + let mut buff = [EFPacking::::ZERO; WIDTH]; + buff[0] = first_cubed; + for j in 1..WIDTH { + buff[j] = point[j]; + } + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + let mut res = EFPacking::::ZERO; + for j in 0..WIDTH { + res += buff[j] * alpha_powers[j]; + } + res + } +} diff --git a/crates/poseidon_circuit/src/lib.rs b/crates/poseidon_circuit/src/lib.rs new file mode 100644 index 00000000..25134829 --- /dev/null +++ b/crates/poseidon_circuit/src/lib.rs @@ -0,0 +1,20 @@ +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +use p3_koala_bear::{KoalaBear, QuinticExtensionFieldKB}; + +mod prove; +pub use prove::*; + +mod verify; +pub use verify::*; + +mod witness_gen; +pub use witness_gen::*; + +#[cfg(test)] +mod tests; + +pub(crate) mod gkr_layers; + +pub(crate) type F = KoalaBear; +pub(crate) type EF = QuinticExtensionFieldKB; diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs new file mode 100644 index 00000000..71bd02ba --- /dev/null +++ b/crates/poseidon_circuit/src/tests.rs @@ -0,0 +1,569 @@ +#![cfg_attr(not(test), allow(unused_crate_dependencies))] + +use multilinear_toolkit::prelude::*; +use p3_koala_bear::{KoalaBear, QuinticExtensionFieldKB}; +use rand::{Rng, SeedableRng, rngs::StdRng}; +use std::{array, time::Instant}; +use tracing::{info_span, instrument}; +use utils::{ + build_prover_state, build_verifier_state, init_tracing, poseidon16_permute_mut, + transposed_par_iter_mut, +}; +use whir_p3::{ + FoldingFactor, SecurityAssumption, WhirConfig, WhirConfigBuilder, precompute_dft_twiddles, +}; + +use crate::{generate_poseidon_witness, gkr_layers::{BatchPartialRounds, PoseidonGKRLayers}}; + +type F = KoalaBear; +type EF = QuinticExtensionFieldKB; +const UNIVARIATE_SKIPS: usize = 3; + +const WIDTH: usize = 16; +const N_COMMITED_CUBES: usize = 16; // power of 2 to increase PCS efficiency + +#[test] +fn test_prove_poseidons() { + init_tracing(); + precompute_dft_twiddles::(1 << 24); + + let log_n_poseidons = 20; + + let whir_config_builder = WhirConfigBuilder { + folding_factor: FoldingFactor::new(7, 4), + soundness_type: SecurityAssumption::CapacityBound, + pow_bits: WIDTH, + max_num_variables_to_send_coeffs: 6, + rs_domain_initial_reduction_factor: 5, + security_level: 128, + starting_log_inv_rate: 1, + }; + let whir_n_vars = log_n_poseidons + log2_strict_usize(WIDTH + N_COMMITED_CUBES); + let whir_config = WhirConfig::new(whir_config_builder, whir_n_vars); + + let mut rng = StdRng::seed_from_u64(0); + let n_poseidons = 1 << log_n_poseidons; + let perm_inputs = (0..n_poseidons) + .map(|_| rng.random()) + .collect::>(); + let selectors = univariate_selectors::(UNIVARIATE_SKIPS); + let input_layers: [_; WIDTH] = + array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); + let input_layers_packed: [_; WIDTH] = + array::from_fn(|i| PFPacking::::pack_slice(&input_layers[i]).to_vec()); + + let layers = PoseidonGKRLayers::::build(); + + let (mut verifier_state, proof_size, output_layer, prover_duration) = { + // ---------------------------------------------------- PROVER ---------------------------------------------------- + + let prover_time = Instant::now(); + + let witness = + generate_poseidon_witness::(input_layers_packed, &layers); + + let mut prover_state = build_prover_state::(); + let mut global_poly_commited: Vec = unsafe { uninitialized_vec(1 << whir_n_vars) }; + let mut chunks = split_at_mut_many( + &mut global_poly_commited, + (0..WIDTH + N_COMMITED_CUBES - 1) + .map(|i| (i + 1) << log_n_poseidons) + .collect::>() + .as_slice(), + ); + chunks[..WIDTH] + .par_iter_mut() + .enumerate() + .for_each(|(i, chunk)| { + chunk.copy_from_slice(&input_layers[i]); + }); + chunks[WIDTH..] + .par_iter_mut() + .enumerate() + .for_each(|(i, chunk)| { + chunk.copy_from_slice(PFPacking::::unpack_slice(&witness.committed_cubes[i])); + }); + + let global_poly_commited = MleOwned::Base(global_poly_commited); + let pcs_witness = whir_config.commit(&mut prover_state, &global_poly_commited); + let global_poly_commited_packed = + PFPacking::::pack_slice(global_poly_commited.as_base().unwrap()); + + let mut claim_point = prover_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); + + let mut output_claims = info_span!("computing output claims").in_scope(|| { + batch_evaluate_univariate_multilinear( + &witness + .output_layer + .iter() + .map(|l| PFPacking::::unpack_slice(l)) + .collect::>(), + &claim_point, + &selectors, + ) + }); + + prover_state.add_extension_scalars(&output_claims); + + for (input_layers, full_round) in witness + .final_full_round_inputs + .iter() + .zip(&layers.final_full_rounds) + .rev() + { + (claim_point, output_claims) = prove_gkr_round( + &mut prover_state, + full_round, + input_layers, + &claim_point, + &output_claims, + ); + } + + for (input_layers, partial_round) in witness + .remaining_partial_round_inputs + .iter() + .zip(&layers.partial_rounds_remaining) + .rev() + { + (claim_point, output_claims) = prove_gkr_round( + &mut prover_state, + partial_round, + input_layers, + &claim_point, + &output_claims, + ); + } + + (claim_point, output_claims) = prove_batch_internal_rounds( + &mut prover_state, + &witness.batch_partial_round_input, + &witness.committed_cubes, + &layers.batch_partial_rounds, + &claim_point, + &output_claims, + &selectors, + ); + + let pcs_point_for_cubes = claim_point.clone(); + + output_claims = output_claims[..WIDTH].to_vec(); + + for (input_layers, full_round) in witness + .remaining_initial_full_round_inputs + .iter() + .zip(&layers.initial_full_rounds_remaining) + .rev() + { + (claim_point, output_claims) = prove_gkr_round( + &mut prover_state, + full_round, + input_layers, + &claim_point, + &output_claims, + ); + } + (claim_point, _) = prove_gkr_round( + &mut prover_state, + &layers.initial_full_round, + &witness.input_layer, + &claim_point, + &output_claims, + ); + + let pcs_point_for_inputs = claim_point.clone(); + + // PCS opening + let mut pcs_statements = vec![]; + + let eq_mle_inputs = eval_eq_packed(&pcs_point_for_inputs[1..]); + let inner_evals_inputs = global_poly_commited_packed + [..global_poly_commited_packed.len() / 2] + .par_chunks_exact(eq_mle_inputs.len()) + .map(|chunk| { + let ef_sum = dot_product::, _, _>( + eq_mle_inputs.iter().copied(), + chunk.iter().copied(), + ); + as PackedFieldExtension>::to_ext_iter([ef_sum]).sum::() + }) + .collect::>(); + prover_state.add_extension_scalars(&inner_evals_inputs); + let pcs_batching_scalars_inputs = prover_state.sample_vec(4 + UNIVARIATE_SKIPS); // 4 = log2(WIDTH) + pcs_statements.push(Evaluation { + point: MultilinearPoint( + [ + vec![EF::ZERO], + pcs_batching_scalars_inputs.clone(), + pcs_point_for_inputs[1..].to_vec(), + ] + .concat(), + ), + value: inner_evals_inputs.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs)), + }); + + let eq_mle_cubes = eval_eq_packed(&pcs_point_for_cubes[1..]); + let inner_evals_cubes = global_poly_commited_packed + [global_poly_commited_packed.len() / 2..] + .par_chunks_exact(eq_mle_cubes.len()) + .map(|chunk| { + let ef_sum = dot_product::, _, _>( + eq_mle_cubes.iter().copied(), + chunk.iter().copied(), + ); + as PackedFieldExtension>::to_ext_iter([ef_sum]).sum::() + }) + .collect::>(); + prover_state.add_extension_scalars(&inner_evals_cubes); + let pcs_batching_scalars_cubes = + prover_state.sample_vec(log2_strict_usize(N_COMMITED_CUBES) + UNIVARIATE_SKIPS); + pcs_statements.push(Evaluation { + point: MultilinearPoint( + [ + vec![EF::ONE], + pcs_batching_scalars_cubes.clone(), + pcs_point_for_cubes[1..].to_vec(), + ] + .concat(), + ), + value: inner_evals_cubes.evaluate(&MultilinearPoint(pcs_batching_scalars_cubes)), + }); + + whir_config.prove( + &mut prover_state, + pcs_statements, + pcs_witness, + &global_poly_commited.by_ref(), + ); + + let prover_duration = prover_time.elapsed(); + + ( + build_verifier_state(&prover_state), + prover_state.proof_size(), + witness.output_layer, + prover_duration, + ) + }; + + let verifier_time = Instant::now(); + { + // ---------------------------------------------------- VERIFIER ---------------------------------------------------- + + let parsed_pcs_commitment = whir_config + .parse_commitment::(&mut verifier_state) + .unwrap(); + + let output_claim_point = verifier_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); + + let mut output_claims = verifier_state.next_extension_scalars_vec(WIDTH).unwrap(); + + let mut claim_point = output_claim_point.clone(); + for full_round in layers.final_full_rounds.iter().rev() { + (claim_point, output_claims) = verify_gkr_round( + &mut verifier_state, + full_round, + log_n_poseidons, + &claim_point, + &output_claims, + ); + } + + for partial_round in layers.partial_rounds_remaining.iter().rev() { + (claim_point, output_claims) = verify_gkr_round( + &mut verifier_state, + partial_round, + log_n_poseidons, + &claim_point, + &output_claims, + ); + } + let claimed_cubes_evals = verifier_state + .next_extension_scalars_vec(N_COMMITED_CUBES) + .unwrap(); + + (claim_point, output_claims) = verify_gkr_round( + &mut verifier_state, + &layers.batch_partial_rounds, + log_n_poseidons, + &claim_point, + &[output_claims, claimed_cubes_evals.clone()].concat(), + ); + + let pcs_point_for_cubes = claim_point.clone(); + let pcs_evals_for_cubes = output_claims[WIDTH..].to_vec(); + + output_claims = output_claims[..WIDTH].to_vec(); + + for full_round in layers.initial_full_rounds_remaining.iter().rev() { + (claim_point, output_claims) = verify_gkr_round( + &mut verifier_state, + full_round, + log_n_poseidons, + &claim_point, + &output_claims, + ); + } + (claim_point, output_claims) = verify_gkr_round( + &mut verifier_state, + &layers.initial_full_round, + log_n_poseidons, + &claim_point, + &output_claims, + ); + + let pcs_point_for_inputs = claim_point.clone(); + let pcs_evals_for_inputs = output_claims.to_vec(); + + // PCS verification + + let mut pcs_statements = vec![]; + + let inner_evals_inputs = verifier_state + .next_extension_scalars_vec(WIDTH << UNIVARIATE_SKIPS) + .unwrap(); + let pcs_batching_scalars_inputs = verifier_state.sample_vec(4 + UNIVARIATE_SKIPS); // 4 = log2(WIDTH) + pcs_statements.push(Evaluation { + point: MultilinearPoint( + [ + vec![EF::ZERO], + pcs_batching_scalars_inputs.clone(), + pcs_point_for_inputs[1..].to_vec(), + ] + .concat(), + ), + value: inner_evals_inputs.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs)), + }); + { + for (&eval, inner_evals) in pcs_evals_for_inputs + .iter() + .zip(inner_evals_inputs.chunks_exact(1 << UNIVARIATE_SKIPS)) + { + assert_eq!( + eval, + evaluate_univariate_multilinear::<_, _, _, false>( + inner_evals, + &pcs_point_for_inputs[..1], + &selectors, + None + ) + ); + } + } + + let inner_evals_cubes = verifier_state + .next_extension_scalars_vec(N_COMMITED_CUBES << UNIVARIATE_SKIPS) + .unwrap(); + let pcs_batching_scalars_cubes = + verifier_state.sample_vec(log2_strict_usize(N_COMMITED_CUBES) + UNIVARIATE_SKIPS); + pcs_statements.push(Evaluation { + point: MultilinearPoint( + [ + vec![EF::ONE], + pcs_batching_scalars_cubes.clone(), + pcs_point_for_cubes[1..].to_vec(), + ] + .concat(), + ), + value: inner_evals_cubes.evaluate(&MultilinearPoint(pcs_batching_scalars_cubes)), + }); + { + for (&eval, inner_evals) in pcs_evals_for_cubes + .iter() + .zip(inner_evals_cubes.chunks_exact(1 << UNIVARIATE_SKIPS)) + { + assert_eq!( + eval, + evaluate_univariate_multilinear::<_, _, _, false>( + inner_evals, + &pcs_point_for_cubes[..1], + &selectors, + None + ) + ); + } + } + + whir_config + .verify(&mut verifier_state, &parsed_pcs_commitment, pcs_statements) + .unwrap(); + } + let verifier_duration = verifier_time.elapsed(); + + let mut data_to_hash = input_layers.clone(); + let plaintext_time = Instant::now(); + transposed_par_iter_mut(&mut data_to_hash).for_each(|row| { + let mut buff = array::from_fn(|j| *row[j]); + poseidon16_permute_mut(&mut buff); + for j in 0..WIDTH { + *row[j] = buff[j]; + } + }); + let plaintext_duration = plaintext_time.elapsed(); + + // sanity check: ensure the plaintext poseidons matches the last GKR layer: + output_layer.iter().enumerate().for_each(|(i, layer)| { + assert_eq!(PFPacking::::unpack_slice(&layer), data_to_hash[i]); + }); + + println!("2^{} Poseidon2", log_n_poseidons); + println!( + "Plaintext (no proof) time: {:.3}s ({:.2}M Poseidons / s)", + plaintext_duration.as_secs_f64(), + n_poseidons as f64 / (plaintext_duration.as_secs_f64() * 1e6) + ); + println!( + "Prover time: {:.3}s ({:.2}M Poseidons / s, {:.1}x slower than plaintext)", + prover_duration.as_secs_f64(), + n_poseidons as f64 / (prover_duration.as_secs_f64() * 1e6), + prover_duration.as_secs_f64() / plaintext_duration.as_secs_f64() + ); + println!( + "Proof size: {:.1} KiB (without merkle pruning)", + (proof_size * F::bits()) as f64 / (8.0 * 1024.0) + ); + println!("Verifier time: {}ms", verifier_duration.as_millis()); +} + +#[instrument(skip_all)] +fn prove_gkr_round< + SC: SumcheckComputation + + SumcheckComputation + + SumcheckComputationPacked + + 'static, +>( + prover_state: &mut FSProver>, + computation: &SC, + input_layers: &[impl AsRef>>], + claim_point: &[EF], + output_claims: &[EF], +) -> (Vec, Vec) { + let batching_scalar = prover_state.sample(); + let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); + let batching_scalars_powers = batching_scalar.powers().collect_n(WIDTH); + + let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( + UNIVARIATE_SKIPS, + MleGroupRef::BasePacked(input_layers.iter().map(|l| l.as_ref().as_slice()).collect()), + computation, + computation, + &batching_scalars_powers, + Some((claim_point.to_vec(), None)), + false, + prover_state, + batched_claim, + None, + ); + + // sanity check + debug_assert_eq!( + computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) + * eq_poly_with_skip(&sumcheck_point, &claim_point, UNIVARIATE_SKIPS), + sumcheck_final_sum + ); + + prover_state.add_extension_scalars(&sumcheck_inner_evals); + + (sumcheck_point.0, sumcheck_inner_evals) +} + +#[instrument(skip_all)] +fn prove_batch_internal_rounds( + prover_state: &mut FSProver>, + input_layers: &[Vec>], + committed_cubes: &[Vec>], + computation: &BatchPartialRounds, + claim_point: &[EF], + output_claims: &[EF], + selectors: &[DensePolynomial], +) -> (Vec, Vec) { + assert_eq!(input_layers.len(), WIDTH); + assert_eq!(committed_cubes.len(), N_COMMITED_CUBES); + + let cubes_evals = info_span!("computing cube evals").in_scope(|| { + batch_evaluate_univariate_multilinear( + &committed_cubes + .iter() + .map(|l| PFPacking::::unpack_slice(l)) + .collect::>(), + &claim_point, + selectors, + ) + }); + + prover_state.add_extension_scalars(&cubes_evals); + + let batching_scalar = prover_state.sample(); + let batched_claim: EF = dot_product( + output_claims.iter().chain(&cubes_evals).copied(), + batching_scalar.powers(), + ); + let batching_scalars_powers = batching_scalar.powers().collect_n(WIDTH + N_COMMITED_CUBES); + + let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( + UNIVARIATE_SKIPS, + MleGroupRef::BasePacked( + input_layers + .iter() + .chain(committed_cubes.iter()) + .map(Vec::as_slice) + .collect(), + ), + computation, + computation, + &batching_scalars_powers, + Some((claim_point.to_vec(), None)), + false, + prover_state, + batched_claim, + None, + ); + + // sanity check + debug_assert_eq!( + computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) + * eq_poly_with_skip(&sumcheck_point, &claim_point, UNIVARIATE_SKIPS), + sumcheck_final_sum + ); + + prover_state.add_extension_scalars(&sumcheck_inner_evals); + + (sumcheck_point.0, sumcheck_inner_evals) +} + +fn verify_gkr_round>( + verifier_state: &mut FSVerifier>, + computation: &SC, + log_n_poseidons: usize, + claim_point: &[EF], + output_claims: &[EF], +) -> (Vec, Vec) { + let batching_scalar = verifier_state.sample(); + let batching_scalars_powers = batching_scalar.powers().collect_n(output_claims.len()); + let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); + + let (retrieved_batched_claim, sumcheck_postponed_claim) = sumcheck_verify_with_univariate_skip( + verifier_state, + computation.degree() + 1, + log_n_poseidons, + UNIVARIATE_SKIPS, + ) + .unwrap(); + + assert_eq!(retrieved_batched_claim, batched_claim); + + let sumcheck_inner_evals = verifier_state + .next_extension_scalars_vec(output_claims.len()) + .unwrap(); + assert_eq!( + computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) + * eq_poly_with_skip( + &sumcheck_postponed_claim.point, + &claim_point, + UNIVARIATE_SKIPS + ), + sumcheck_postponed_claim.value + ); + + (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) +} diff --git a/crates/poseidon_circuit/src/verify.rs b/crates/poseidon_circuit/src/verify.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/poseidon_circuit/src/witness_gen.rs b/crates/poseidon_circuit/src/witness_gen.rs new file mode 100644 index 00000000..906dae6e --- /dev/null +++ b/crates/poseidon_circuit/src/witness_gen.rs @@ -0,0 +1,167 @@ +use std::array; + +use multilinear_toolkit::prelude::*; +use p3_field::PrimeCharacteristicRing; +use p3_koala_bear::GenericPoseidon2LinearLayersKoalaBear; +use p3_koala_bear::KoalaBearInternalLayerParameters; +use p3_koala_bear::KoalaBearParameters; +use p3_monty_31::InternalLayerBaseParameters; +use p3_poseidon2::GenericPoseidon2LinearLayers; +use tracing::instrument; +use utils::transposed_par_iter_mut; + +use crate::gkr_layers::BatchPartialRounds; +use crate::gkr_layers::PartialRoundComputation; +use crate::gkr_layers::PoseidonGKRLayers; +use crate::{F, gkr_layers::FullRoundComputation}; + +#[derive(Debug)] +pub struct PoseidonWitness { + pub input_layer: [Vec>; WIDTH], // input of the permutation + pub remaining_initial_full_round_inputs: Vec<[Vec>; WIDTH]>, // the remaining input of each initial full round + pub batch_partial_round_input: [Vec>; WIDTH], // again, the input of the batch (partial) round + pub committed_cubes: [Vec>; N_COMMITED_CUBES], // the cubes commited in the batch (partial) rounds + pub remaining_partial_round_inputs: Vec<[Vec>; WIDTH]>, // the input of each remaining partial round + pub final_full_round_inputs: Vec<[Vec>; WIDTH]>, // the input of each final full round + pub output_layer: [Vec>; WIDTH], // output of the permutation +} + +pub fn generate_poseidon_witness( + input_layer: [Vec>; WIDTH], + layers: &PoseidonGKRLayers +) -> PoseidonWitness +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + let mut remaining_initial_full_layers = vec![apply_full_round::<_, true>( + &input_layer, + &layers.initial_full_round, + )]; + for round in &layers.initial_full_rounds_remaining { + remaining_initial_full_layers.push(apply_full_round::<_, false>( + remaining_initial_full_layers.last().unwrap(), + round, + )); + } + + let batch_partial_round_layer = remaining_initial_full_layers.pop().unwrap(); + let (next_layer, committed_cubes) = + apply_batch_partial_rounds(&batch_partial_round_layer, &layers.batch_partial_rounds); + + let mut remaining_partial_inputs = vec![next_layer]; + for constant in &layers.partial_rounds_remaining { + remaining_partial_inputs.push(apply_partial_round( + remaining_partial_inputs.last().unwrap(), + constant, + )); + } + + let mut final_full_layer_inputs = vec![remaining_partial_inputs.pop().unwrap()]; + for round in &layers.final_full_rounds { + final_full_layer_inputs.push(apply_full_round::<_, false>( + final_full_layer_inputs.last().unwrap(), + round, + )); + } + + let output_layer = final_full_layer_inputs.pop().unwrap(); + + PoseidonWitness { + input_layer, + remaining_initial_full_round_inputs: remaining_initial_full_layers, + batch_partial_round_input: batch_partial_round_layer, + committed_cubes, + remaining_partial_round_inputs: remaining_partial_inputs, + final_full_round_inputs: final_full_layer_inputs, + output_layer, + } +} + +#[instrument(skip_all)] +fn apply_full_round( + input_layers: &[Vec>; WIDTH], + full_round: &FullRoundComputation, +) -> [Vec>; WIDTH] +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + let mut output_layers: [_; WIDTH] = + array::from_fn(|_| FPacking::::zero_vec(input_layers[0].len())); + transposed_par_iter_mut(&mut output_layers) + .enumerate() + .for_each(|(row_index, output_row)| { + let mut buff: [FPacking; WIDTH] = array::from_fn(|j| input_layers[j][row_index]); + if FIRST { + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + } + buff.iter_mut().enumerate().for_each(|(j, val)| { + *val = (*val + full_round.constants[j]).cube(); + }); + GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); + for j in 0..WIDTH { + *output_row[j] = buff[j]; + } + }); + output_layers +} + +#[instrument(skip_all)] +fn apply_partial_round( + input_layers: &[Vec>], + partial_round: &PartialRoundComputation, +) -> [Vec>; WIDTH] +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + let mut output_layers: [_; WIDTH] = + array::from_fn(|_| FPacking::::zero_vec(input_layers[0].len())); + transposed_par_iter_mut(&mut output_layers) + .enumerate() + .for_each(|(row_index, output_row)| { + let first_cubed = (input_layers[0][row_index] + partial_round.constant).cube(); + let mut buff = [FPacking::::ZERO; WIDTH]; + buff[0] = first_cubed; + for j in 1..WIDTH { + buff[j] = input_layers[j][row_index]; + } + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + for j in 0..WIDTH { + *output_row[j] = buff[j]; + } + }); + output_layers +} + +#[instrument(skip_all)] +fn apply_batch_partial_rounds( + input_layers: &[Vec>], + rounds: &BatchPartialRounds, +) -> ( + [Vec>; WIDTH], + [Vec>; N_COMMITED_CUBES], +) +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + let mut output_layers: [_; WIDTH] = + array::from_fn(|_| FPacking::::zero_vec(input_layers[0].len())); + let mut cubes: [_; N_COMMITED_CUBES] = + array::from_fn(|_| FPacking::::zero_vec(input_layers[0].len())); + transposed_par_iter_mut(&mut output_layers) + .zip(transposed_par_iter_mut(&mut cubes)) + .enumerate() + .for_each(|(row_index, (output_row, cubes))| { + let mut buff: [FPacking; WIDTH] = array::from_fn(|j| input_layers[j][row_index]); + for (i, &constant) in rounds.constants.iter().enumerate() { + *cubes[i] = (buff[0] + constant).cube(); + buff[0] = *cubes[i]; + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + } + buff[0] = (buff[0] + rounds.last_constant).cube(); + GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); + for j in 0..WIDTH { + *output_row[j] = buff[j]; + } + }); + (output_layers, cubes) +} diff --git a/src/main.rs b/src/main.rs index cb6abe47..f328e4d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,913 +1 @@ -#![cfg_attr(not(test), allow(unused_crate_dependencies))] - -use multilinear_toolkit::prelude::*; -use p3_koala_bear::{ - GenericPoseidon2LinearLayersKoalaBear, KOALABEAR_RC16_EXTERNAL_FINAL, - KOALABEAR_RC16_EXTERNAL_INITIAL, KOALABEAR_RC16_INTERNAL, KoalaBear, QuinticExtensionFieldKB, -}; -use p3_poseidon2::GenericPoseidon2LinearLayers; -use rand::{Rng, SeedableRng, rngs::StdRng}; -use std::{array, time::Instant}; -use tracing::{info_span, instrument}; -use utils::{ - build_prover_state, build_verifier_state, init_tracing, poseidon16_permute_mut, - transposed_par_iter_mut, -}; -use whir_p3::{ - FoldingFactor, SecurityAssumption, WhirConfig, WhirConfigBuilder, precompute_dft_twiddles, -}; - -type F = KoalaBear; -type EF = QuinticExtensionFieldKB; -const UNIVARIATE_SKIPS: usize = 3; - -const N_COMMITED_CUBES: usize = 16; // power of 2 to increase PCS efficiency - -// const N_INITIAL_ROUNDS: usize = KOALABEAR_RC16_EXTERNAL_INITIAL.len(); -const N_INTERNAL_ROUNDS: usize = KOALABEAR_RC16_INTERNAL.len(); -// const N_FINAL_ROUNDS: usize = KOALABEAR_RC16_EXTERNAL_FINAL.len(); - -fn main() { - assert!(N_COMMITED_CUBES <= N_INTERNAL_ROUNDS); - init_tracing(); - precompute_dft_twiddles::(1 << 24); - - let log_n_poseidons = 20; - - let whir_config_builder = WhirConfigBuilder { - folding_factor: FoldingFactor::new(7, 4), - soundness_type: SecurityAssumption::CapacityBound, - pow_bits: 16, - max_num_variables_to_send_coeffs: 6, - rs_domain_initial_reduction_factor: 5, - security_level: 128, - starting_log_inv_rate: 1, - }; - let whir_n_vars = log_n_poseidons + log2_strict_usize(16 + N_COMMITED_CUBES); - let whir_config = WhirConfig::new(whir_config_builder, whir_n_vars); - - let mut rng = StdRng::seed_from_u64(0); - let n_poseidons = 1 << log_n_poseidons; - let perm_inputs = (0..n_poseidons) - .map(|_| rng.random()) - .collect::>(); - let selectors = univariate_selectors::(UNIVARIATE_SKIPS); - let input_layers: [_; 16] = - array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); - let input_layers_packed: [_; 16] = - array::from_fn(|i| PFPacking::::pack_slice(&input_layers[i]).to_vec()); - - let initial_full_rounds = KOALABEAR_RC16_EXTERNAL_INITIAL - .into_iter() - .enumerate() - .map(|(i, constants)| FullRoundComputation { - constants, - first_full_round: i == 0, - }) - .collect::>(); - let partial_rounds_with_committed_cubes = PartialRoundsWithCommittedCubes { - constants: KOALABEAR_RC16_INTERNAL[..N_COMMITED_CUBES] - .try_into() - .unwrap(), - last_constant: KOALABEAR_RC16_INTERNAL[N_COMMITED_CUBES], - }; - let partial_rounds_remaining = KOALABEAR_RC16_INTERNAL[N_COMMITED_CUBES + 1..] - .iter() - .copied() - .map(|constant| PartialRoundComputation { constant }) - .collect::>(); - let final_full_rounds = KOALABEAR_RC16_EXTERNAL_FINAL - .into_iter() - .map(|constants| FullRoundComputation { - constants, - first_full_round: false, - }) - .collect::>(); - - let (mut verifier_state, proof_size, output_layers, prover_duration) = { - // ---------------------------------------------------- PROVER ---------------------------------------------------- - - let prover_time = Instant::now(); - let mut all_initial_full_layers = vec![input_layers_packed]; - for (i, round) in initial_full_rounds.iter().enumerate() { - all_initial_full_layers.push(apply_full_round( - all_initial_full_layers.last().unwrap(), - round, - i == 0, - )); - } - - let internal_partial_layer_with_committed_cubes_inputs = - all_initial_full_layers.pop().unwrap(); - let (next_layer, committed_cubes) = apply_partial_round_for_commit_cubes( - &internal_partial_layer_with_committed_cubes_inputs, - &partial_rounds_with_committed_cubes, - ); - - let mut internal_partial_layer_remaining_inputs = vec![next_layer]; - for round in &partial_rounds_remaining { - internal_partial_layer_remaining_inputs.push(apply_partial_round( - internal_partial_layer_remaining_inputs.last().unwrap(), - round, - )); - } - - let mut all_final_full_layers = - vec![internal_partial_layer_remaining_inputs.pop().unwrap()]; - for round in &final_full_rounds { - all_final_full_layers.push(apply_full_round( - all_final_full_layers.last().unwrap(), - round, - false, - )); - } - - let mut prover_state = build_prover_state::(); - let mut global_poly_commited: Vec = unsafe { uninitialized_vec(1 << whir_n_vars) }; - let mut chunks = split_at_mut_many( - &mut global_poly_commited, - (0..16 + N_COMMITED_CUBES - 1) - .map(|i| (i + 1) << log_n_poseidons) - .collect::>() - .as_slice(), - ); - chunks[..16] - .par_iter_mut() - .enumerate() - .for_each(|(i, chunk)| { - chunk.copy_from_slice(&input_layers[i]); - }); - chunks[16..] - .par_iter_mut() - .enumerate() - .for_each(|(i, chunk)| { - chunk.copy_from_slice(PFPacking::::unpack_slice(&committed_cubes[i])); - }); - - let global_poly_commited = MleOwned::Base(global_poly_commited); - let pcs_witness = whir_config.commit(&mut prover_state, &global_poly_commited); - let global_poly_commited_packed = - PFPacking::::pack_slice(global_poly_commited.as_base().unwrap()); - - let output_claim_point = prover_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); - - let mut output_claims = info_span!("computing output claims").in_scope(|| { - batch_evaluate_univariate_multilinear( - &all_final_full_layers - .last() - .unwrap() - .iter() - .map(|l| PFPacking::::unpack_slice(l)) - .collect::>(), - &output_claim_point, - &selectors, - ) - }); - - prover_state.add_extension_scalars(&output_claims); - - let mut claim_point = output_claim_point.clone(); - - for (input_layers, full_round) in all_final_full_layers.iter().zip(&final_full_rounds).rev() - { - (claim_point, output_claims) = prove_gkr_round( - &mut prover_state, - full_round, - input_layers, - &claim_point, - &output_claims, - ); - } - - for (input_layers, partial_round) in internal_partial_layer_remaining_inputs - .iter() - .zip(&partial_rounds_remaining) - .rev() - { - (claim_point, output_claims) = prove_gkr_round( - &mut prover_state, - partial_round, - input_layers, - &claim_point, - &output_claims, - ); - } - - (claim_point, output_claims) = prove_internal_rounds_with_committed_cube( - &mut prover_state, - &internal_partial_layer_with_committed_cubes_inputs, - &committed_cubes, - &partial_rounds_with_committed_cubes, - &claim_point, - &output_claims, - &selectors, - ); - - let pcs_point_for_cubes = claim_point.clone(); - - output_claims = output_claims[..16].to_vec(); - - for (input_layers, full_round) in all_initial_full_layers - .iter() - .zip(&initial_full_rounds) - .rev() - { - (claim_point, output_claims) = prove_gkr_round( - &mut prover_state, - full_round, - input_layers, - &claim_point, - &output_claims, - ); - } - - let pcs_point_for_inputs = claim_point.clone(); - - // PCS opening - let mut pcs_statements = vec![]; - - let eq_mle_inputs = eval_eq_packed(&pcs_point_for_inputs[1..]); - let inner_evals_inputs = global_poly_commited_packed - [..global_poly_commited_packed.len() / 2] - .par_chunks_exact(eq_mle_inputs.len()) - .map(|chunk| { - let ef_sum = dot_product::, _, _>( - eq_mle_inputs.iter().copied(), - chunk.iter().copied(), - ); - as PackedFieldExtension>::to_ext_iter([ef_sum]).sum::() - }) - .collect::>(); - prover_state.add_extension_scalars(&inner_evals_inputs); - let pcs_batching_scalars_inputs = prover_state.sample_vec(4 + UNIVARIATE_SKIPS); // 4 = log2(16) - pcs_statements.push(Evaluation { - point: MultilinearPoint( - [ - vec![EF::ZERO], - pcs_batching_scalars_inputs.clone(), - pcs_point_for_inputs[1..].to_vec(), - ] - .concat(), - ), - value: inner_evals_inputs.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs)), - }); - - let eq_mle_cubes = eval_eq_packed(&pcs_point_for_cubes[1..]); - let inner_evals_cubes = global_poly_commited_packed - [global_poly_commited_packed.len() / 2..] - .par_chunks_exact(eq_mle_cubes.len()) - .map(|chunk| { - let ef_sum = dot_product::, _, _>( - eq_mle_cubes.iter().copied(), - chunk.iter().copied(), - ); - as PackedFieldExtension>::to_ext_iter([ef_sum]).sum::() - }) - .collect::>(); - prover_state.add_extension_scalars(&inner_evals_cubes); - let pcs_batching_scalars_cubes = - prover_state.sample_vec(log2_strict_usize(N_COMMITED_CUBES) + UNIVARIATE_SKIPS); - pcs_statements.push(Evaluation { - point: MultilinearPoint( - [ - vec![EF::ONE], - pcs_batching_scalars_cubes.clone(), - pcs_point_for_cubes[1..].to_vec(), - ] - .concat(), - ), - value: inner_evals_cubes.evaluate(&MultilinearPoint(pcs_batching_scalars_cubes)), - }); - - whir_config.prove( - &mut prover_state, - pcs_statements, - pcs_witness, - &global_poly_commited.by_ref(), - ); - - let prover_duration = prover_time.elapsed(); - - ( - build_verifier_state(&prover_state), - prover_state.proof_size(), - all_final_full_layers.last().unwrap().clone(), - prover_duration, - ) - }; - - let verifier_time = Instant::now(); - { - // ---------------------------------------------------- VERIFIER ---------------------------------------------------- - - let parsed_pcs_commitment = whir_config - .parse_commitment::(&mut verifier_state) - .unwrap(); - - let output_claim_point = verifier_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); - - let mut output_claims = verifier_state.next_extension_scalars_vec(16).unwrap(); - - let mut claim_point = output_claim_point.clone(); - for full_round in final_full_rounds.iter().rev() { - (claim_point, output_claims) = verify_gkr_round( - &mut verifier_state, - full_round, - log_n_poseidons, - &claim_point, - &output_claims, - ); - } - - for partial_round in partial_rounds_remaining.iter().rev() { - (claim_point, output_claims) = verify_gkr_round( - &mut verifier_state, - partial_round, - log_n_poseidons, - &claim_point, - &output_claims, - ); - } - let claimed_cubes_evals = verifier_state - .next_extension_scalars_vec(N_COMMITED_CUBES) - .unwrap(); - - (claim_point, output_claims) = verify_gkr_round( - &mut verifier_state, - &partial_rounds_with_committed_cubes, - log_n_poseidons, - &claim_point, - &[output_claims, claimed_cubes_evals.clone()].concat(), - ); - - let pcs_point_for_cubes = claim_point.clone(); - let pcs_evals_for_cubes = output_claims[16..].to_vec(); - - output_claims = output_claims[..16].to_vec(); - - for full_round in initial_full_rounds.iter().rev() { - (claim_point, output_claims) = verify_gkr_round( - &mut verifier_state, - full_round, - log_n_poseidons, - &claim_point, - &output_claims, - ); - } - - let pcs_point_for_inputs = claim_point.clone(); - let pcs_evals_for_inputs = output_claims.to_vec(); - - // PCS verification - - let mut pcs_statements = vec![]; - - let inner_evals_inputs = verifier_state - .next_extension_scalars_vec(16 << UNIVARIATE_SKIPS) - .unwrap(); - let pcs_batching_scalars_inputs = verifier_state.sample_vec(4 + UNIVARIATE_SKIPS); // 4 = log2(16) - pcs_statements.push(Evaluation { - point: MultilinearPoint( - [ - vec![EF::ZERO], - pcs_batching_scalars_inputs.clone(), - pcs_point_for_inputs[1..].to_vec(), - ] - .concat(), - ), - value: inner_evals_inputs.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs)), - }); - { - for (&eval, inner_evals) in pcs_evals_for_inputs - .iter() - .zip(inner_evals_inputs.chunks_exact(1 << UNIVARIATE_SKIPS)) - { - assert_eq!( - eval, - evaluate_univariate_multilinear::<_, _, _, false>( - inner_evals, - &pcs_point_for_inputs[..1], - &selectors, - None - ) - ); - } - } - - let inner_evals_cubes = verifier_state - .next_extension_scalars_vec(N_COMMITED_CUBES << UNIVARIATE_SKIPS) - .unwrap(); - let pcs_batching_scalars_cubes = - verifier_state.sample_vec(log2_strict_usize(N_COMMITED_CUBES) + UNIVARIATE_SKIPS); - pcs_statements.push(Evaluation { - point: MultilinearPoint( - [ - vec![EF::ONE], - pcs_batching_scalars_cubes.clone(), - pcs_point_for_cubes[1..].to_vec(), - ] - .concat(), - ), - value: inner_evals_cubes.evaluate(&MultilinearPoint(pcs_batching_scalars_cubes)), - }); - { - for (&eval, inner_evals) in pcs_evals_for_cubes - .iter() - .zip(inner_evals_cubes.chunks_exact(1 << UNIVARIATE_SKIPS)) - { - assert_eq!( - eval, - evaluate_univariate_multilinear::<_, _, _, false>( - inner_evals, - &pcs_point_for_cubes[..1], - &selectors, - None - ) - ); - } - } - - whir_config - .verify(&mut verifier_state, &parsed_pcs_commitment, pcs_statements) - .unwrap(); - } - let verifier_duration = verifier_time.elapsed(); - - let mut data_to_hash = input_layers.clone(); - let plaintext_time = Instant::now(); - transposed_par_iter_mut(&mut data_to_hash).for_each(|row| { - let mut buff = array::from_fn(|j| *row[j]); - poseidon16_permute_mut(&mut buff); - for j in 0..16 { - *row[j] = buff[j]; - } - }); - let plaintext_duration = plaintext_time.elapsed(); - - // sanity check: ensure the plaintext poseidons matches the last GKR layer: - output_layers.iter().enumerate().for_each(|(i, layer)| { - assert_eq!(PFPacking::::unpack_slice(&layer), data_to_hash[i]); - }); - - println!("2^{} Poseidon2", log_n_poseidons); - println!( - "Plaintext (no proof) time: {:.3}s ({:.2}M Poseidons / s)", - plaintext_duration.as_secs_f64(), - n_poseidons as f64 / (plaintext_duration.as_secs_f64() * 1e6) - ); - println!( - "Prover time: {:.3}s ({:.2}M Poseidons / s, {:.1}x slower than plaintext)", - prover_duration.as_secs_f64(), - n_poseidons as f64 / (prover_duration.as_secs_f64() * 1e6), - prover_duration.as_secs_f64() / plaintext_duration.as_secs_f64() - ); - println!( - "Proof size: {:.1} KiB (without merkle pruning)", - (proof_size * F::bits()) as f64 / (8.0 * 1024.0) - ); - println!("Verifier time: {}ms", verifier_duration.as_millis()); -} - -#[instrument(skip_all)] -fn apply_full_round( - input_layers: &[Vec>], - ful_round: &FullRoundComputation, - first_full_round: bool, -) -> [Vec>; 16] { - let mut output_layers: [_; 16] = - array::from_fn(|_| PFPacking::::zero_vec(input_layers[0].len())); - transposed_par_iter_mut(&mut output_layers) - .enumerate() - .for_each(|(row_index, output_row)| { - let mut buff: [PFPacking; 16] = array::from_fn(|j| input_layers[j][row_index]); - if first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); - } - buff.iter_mut().enumerate().for_each(|(j, val)| { - *val = (*val + ful_round.constants[j]).cube(); - }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); - for j in 0..16 { - *output_row[j] = buff[j]; - } - }); - output_layers -} - -#[instrument(skip_all)] -fn apply_partial_round( - input_layers: &[Vec>], - partial_round: &PartialRoundComputation, -) -> [Vec>; 16] { - let mut output_layers: [_; 16] = - array::from_fn(|_| PFPacking::::zero_vec(input_layers[0].len())); - transposed_par_iter_mut(&mut output_layers) - .enumerate() - .for_each(|(row_index, output_row)| { - let first_cubed = (input_layers[0][row_index] + partial_round.constant).cube(); - let mut buff = [PFPacking::::ZERO; 16]; - buff[0] = first_cubed; - for j in 1..16 { - buff[j] = input_layers[j][row_index]; - } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - for j in 0..16 { - *output_row[j] = buff[j]; - } - }); - output_layers -} - -#[instrument(skip_all)] -fn apply_partial_round_for_commit_cubes( - input_layers: &[Vec>], - rounds: &PartialRoundsWithCommittedCubes, -) -> ( - [Vec>; 16], - [Vec>; N_COMMITED_CUBES], -) { - let mut output_layers: [_; 16] = - array::from_fn(|_| PFPacking::::zero_vec(input_layers[0].len())); - let mut cubes: [_; N_COMMITED_CUBES] = - array::from_fn(|_| PFPacking::::zero_vec(input_layers[0].len())); - transposed_par_iter_mut(&mut output_layers) - .zip(transposed_par_iter_mut(&mut cubes)) - .enumerate() - .for_each(|(row_index, (output_row, cubes))| { - let mut buff: [PFPacking; 16] = array::from_fn(|j| input_layers[j][row_index]); - for (i, &constant) in rounds.constants.iter().enumerate() { - *cubes[i] = (buff[0] + constant).cube(); - buff[0] = *cubes[i]; - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - } - buff[0] = (buff[0] + rounds.last_constant).cube(); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - for j in 0..16 { - *output_row[j] = buff[j]; - } - }); - (output_layers, cubes) -} - -#[instrument(skip_all)] -fn prove_gkr_round< - SC: SumcheckComputation - + SumcheckComputation - + SumcheckComputationPacked - + 'static, ->( - prover_state: &mut FSProver>, - computation: &SC, - input_layers: &[Vec>], - claim_point: &[EF], - output_claims: &[EF], -) -> (Vec, Vec) { - let batching_scalar = prover_state.sample(); - let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); - let batching_scalars_powers = batching_scalar.powers().collect_n(16); - - let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( - UNIVARIATE_SKIPS, - MleGroupRef::BasePacked(input_layers.iter().map(Vec::as_slice).collect()), - computation, - computation, - &batching_scalars_powers, - Some((claim_point.to_vec(), None)), - false, - prover_state, - batched_claim, - None, - ); - - // sanity check - debug_assert_eq!( - computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) - * eq_poly_with_skip(&sumcheck_point, &claim_point, UNIVARIATE_SKIPS), - sumcheck_final_sum - ); - - prover_state.add_extension_scalars(&sumcheck_inner_evals); - - (sumcheck_point.0, sumcheck_inner_evals) -} - -#[instrument(skip_all)] -fn prove_internal_rounds_with_committed_cube( - prover_state: &mut FSProver>, - input_layers: &[Vec>], - committed_cubes: &[Vec>], - computation: &PartialRoundsWithCommittedCubes, - claim_point: &[EF], - output_claims: &[EF], - selectors: &[DensePolynomial], -) -> (Vec, Vec) { - assert_eq!(input_layers.len(), 16); - assert_eq!(committed_cubes.len(), N_COMMITED_CUBES); - - let cubes_evals = info_span!("computing cube evals").in_scope(|| { - batch_evaluate_univariate_multilinear( - &committed_cubes - .iter() - .map(|l| PFPacking::::unpack_slice(l)) - .collect::>(), - &claim_point, - selectors, - ) - }); - - prover_state.add_extension_scalars(&cubes_evals); - - let batching_scalar = prover_state.sample(); - let batched_claim: EF = dot_product( - output_claims.iter().chain(&cubes_evals).copied(), - batching_scalar.powers(), - ); - let batching_scalars_powers = batching_scalar.powers().collect_n(16 + N_COMMITED_CUBES); - - let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( - UNIVARIATE_SKIPS, - MleGroupRef::BasePacked( - input_layers - .iter() - .chain(committed_cubes.iter()) - .map(Vec::as_slice) - .collect(), - ), - computation, - computation, - &batching_scalars_powers, - Some((claim_point.to_vec(), None)), - false, - prover_state, - batched_claim, - None, - ); - - // sanity check - debug_assert_eq!( - computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) - * eq_poly_with_skip(&sumcheck_point, &claim_point, UNIVARIATE_SKIPS), - sumcheck_final_sum - ); - - prover_state.add_extension_scalars(&sumcheck_inner_evals); - - (sumcheck_point.0, sumcheck_inner_evals) -} - -fn verify_gkr_round>( - verifier_state: &mut FSVerifier>, - computation: &SC, - log_n_poseidons: usize, - claim_point: &[EF], - output_claims: &[EF], -) -> (Vec, Vec) { - let batching_scalar = verifier_state.sample(); - let batching_scalars_powers = batching_scalar.powers().collect_n(output_claims.len()); - let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); - - let (retrieved_batched_claim, sumcheck_postponed_claim) = sumcheck_verify_with_univariate_skip( - verifier_state, - computation.degree() + 1, - log_n_poseidons, - UNIVARIATE_SKIPS, - ) - .unwrap(); - - assert_eq!(retrieved_batched_claim, batched_claim); - - let sumcheck_inner_evals = verifier_state - .next_extension_scalars_vec(output_claims.len()) - .unwrap(); - assert_eq!( - computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) - * eq_poly_with_skip( - &sumcheck_postponed_claim.point, - &claim_point, - UNIVARIATE_SKIPS - ), - sumcheck_postponed_claim.value - ); - - (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) -} - -pub struct FullRoundComputation { - pub constants: [F; 16], - pub first_full_round: bool, -} - -impl, EF: ExtensionField> SumcheckComputation - for FullRoundComputation -{ - fn degree(&self) -> usize { - 3 - } - - fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { - debug_assert_eq!(point.len(), 16); - let mut buff: [NF; 16] = array::from_fn(|j| point[j]); - if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); - } - buff.iter_mut().enumerate().for_each(|(j, val)| { - *val = (*val + self.constants[j]).cube(); - }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); - let mut res = EF::ZERO; - for i in 0..16 { - res += alpha_powers[i] * buff[i]; - } - res - } -} - -impl SumcheckComputationPacked for FullRoundComputation { - fn degree(&self) -> usize { - 3 - } - - fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { - debug_assert_eq!(point.len(), 16); - let mut buff: [PFPacking; 16] = array::from_fn(|j| point[j]); - if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); - } - buff.iter_mut().enumerate().for_each(|(j, val)| { - *val = (*val + self.constants[j]).cube(); - }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); - let mut res = EFPacking::::ZERO; - for j in 0..16 { - res += EFPacking::::from(alpha_powers[j]) * buff[j]; - } - res - } - - fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { - debug_assert_eq!(point.len(), 16); - let mut buff: [EFPacking; 16] = array::from_fn(|j| point[j]); - if self.first_full_round { - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); - } - buff.iter_mut().enumerate().for_each(|(j, val)| { - *val = (*val + PFPacking::::from(self.constants[j])).cube(); - }); - GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); - let mut res = EFPacking::::ZERO; - for j in 0..16 { - res += buff[j] * alpha_powers[j]; - } - res - } -} - -pub struct PartialRoundComputation { - pub constant: F, -} - -impl, EF: ExtensionField> SumcheckComputation - for PartialRoundComputation -{ - fn degree(&self) -> usize { - 3 - } - - fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { - debug_assert_eq!(point.len(), 16); - let first_cubed = (point[0] + self.constant).cube(); - let mut buff = [NF::ZERO; 16]; - buff[0] = first_cubed; - for j in 1..16 { - buff[j] = point[j]; - } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - let mut res = EF::ZERO; - for i in 0..16 { - res += alpha_powers[i] * buff[i]; - } - res - } -} - -impl SumcheckComputationPacked for PartialRoundComputation { - fn degree(&self) -> usize { - 3 - } - - fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { - debug_assert_eq!(point.len(), 16); - let first_cubed = (point[0] + self.constant).cube(); - let mut buff = [PFPacking::::ZERO; 16]; - buff[0] = first_cubed; - for j in 1..16 { - buff[j] = point[j]; - } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - let mut res = EFPacking::::ZERO; - for j in 0..16 { - res += EFPacking::::from(alpha_powers[j]) * buff[j]; - } - res - } - - fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { - debug_assert_eq!(point.len(), 16); - let first_cubed = (point[0] + PFPacking::::from(self.constant)).cube(); - let mut buff = [EFPacking::::ZERO; 16]; - buff[0] = first_cubed; - for j in 1..16 { - buff[j] = point[j]; - } - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - let mut res = EFPacking::::ZERO; - for j in 0..16 { - res += buff[j] * alpha_powers[j]; - } - res - } -} - -pub struct PartialRoundsWithCommittedCubes { - pub constants: [F; N_COMMITED_CUBES], - pub last_constant: F, -} - -impl, EF: ExtensionField> SumcheckComputation - for PartialRoundsWithCommittedCubes -{ - fn degree(&self) -> usize { - 3 - } - - fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { - // points = 16 inputs, then N_COMMITED_CUBES cubes - - debug_assert_eq!(point.len(), 16 + N_COMMITED_CUBES); - debug_assert_eq!(alpha_powers.len(), 16 + N_COMMITED_CUBES); - - let mut res = EF::ZERO; - let mut buff: [NF; 16] = array::from_fn(|j| point[j]); - for (i, &constant) in self.constants.iter().enumerate() { - let computed_cube = (buff[0] + constant).cube(); - res += alpha_powers[16 + i] * computed_cube; - buff[0] = point[16 + i]; // commited cube - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - } - - buff[0] = (buff[0] + self.last_constant).cube(); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - for i in 0..16 { - res += alpha_powers[i] * buff[i]; - } - res - } -} - -impl SumcheckComputationPacked for PartialRoundsWithCommittedCubes { - fn degree(&self) -> usize { - 3 - } - - fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { - debug_assert_eq!(point.len(), 16 + N_COMMITED_CUBES); - debug_assert_eq!(alpha_powers.len(), 16 + N_COMMITED_CUBES); - - let mut res = EFPacking::::ZERO; - let mut buff: [PFPacking; 16] = array::from_fn(|j| point[j]); - for (i, &constant) in self.constants.iter().enumerate() { - let computed_cube = (buff[0] + constant).cube(); - res += EFPacking::::from(alpha_powers[16 + i]) * computed_cube; - buff[0] = point[16 + i]; // commited cube - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - } - - buff[0] = (buff[0] + self.last_constant).cube(); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - for i in 0..16 { - res += EFPacking::::from(alpha_powers[i]) * buff[i]; - } - res - } - - fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { - debug_assert_eq!(point.len(), 16 + N_COMMITED_CUBES); - debug_assert_eq!(alpha_powers.len(), 16 + N_COMMITED_CUBES); - - let mut res = EFPacking::::ZERO; - let mut buff: [EFPacking; 16] = array::from_fn(|j| point[j]); - for (i, &constant) in self.constants.iter().enumerate() { - let computed_cube = (buff[0] + PFPacking::::from(constant)).cube(); - res += computed_cube * alpha_powers[16 + i]; - buff[0] = point[16 + i]; // commited cube - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - } - - buff[0] = (buff[0] + PFPacking::::from(self.last_constant)).cube(); - GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); - for i in 0..16 { - res += buff[i] * alpha_powers[i]; - } - res - } -} +fn main() {} From 3ebe3179ec20e556b16440d5414918449e51dc6d Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 24 Oct 2025 18:37:47 +0400 Subject: [PATCH 24/42] wip --- README.md | 2 +- crates/poseidon_circuit/src/prove.rs | 222 +++++++++++++++++++ crates/poseidon_circuit/src/tests.rs | 297 ++------------------------ crates/poseidon_circuit/src/verify.rs | 123 +++++++++++ 4 files changed, 363 insertions(+), 281 deletions(-) diff --git a/README.md b/README.md index a6831e46..247f64d5 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ target ≈ 128 bits of security, currently using conjecture: 4.12 of [WHIR](http ### Poseidon2 ```console -RUSTFLAGS='-C target-cpu=native' cargo run --release +RUSTFLAGS='-C target-cpu=native' cargo test --release --package poseidon_circuit --lib -- tests::test_prove_poseidons --nocapture ``` 50 % over 16 field elements, 50 % over 24 field elements. rate = 1/2 diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index e69de29b..9cda2b2b 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -0,0 +1,222 @@ +use crate::{ + EF, F, PoseidonWitness, + gkr_layers::{BatchPartialRounds, PoseidonGKRLayers}, +}; +use multilinear_toolkit::prelude::*; +use p3_koala_bear::{KoalaBearInternalLayerParameters, KoalaBearParameters}; +use p3_monty_31::InternalLayerBaseParameters; +use tracing::{info_span, instrument}; + +pub fn prove_poseidon_gkr( + prover_state: &mut FSProver>, + witness: &PoseidonWitness, + mut claim_point: Vec, + univariate_skips: usize, + layers: &PoseidonGKRLayers, +) -> (Vec, Vec) +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + let selectors = univariate_selectors::(univariate_skips); + + let mut output_claims = info_span!("computing output claims").in_scope(|| { + batch_evaluate_univariate_multilinear( + &witness + .output_layer + .iter() + .map(|l| PFPacking::::unpack_slice(l)) + .collect::>(), + &claim_point, + &selectors, + ) + }); + + prover_state.add_extension_scalars(&output_claims); + + for (input_layers, full_round) in witness + .final_full_round_inputs + .iter() + .zip(&layers.final_full_rounds) + .rev() + { + (claim_point, output_claims) = prove_gkr_round( + prover_state, + full_round, + input_layers, + &claim_point, + &output_claims, + univariate_skips, + ); + } + + for (input_layers, partial_round) in witness + .remaining_partial_round_inputs + .iter() + .zip(&layers.partial_rounds_remaining) + .rev() + { + (claim_point, output_claims) = prove_gkr_round( + prover_state, + partial_round, + input_layers, + &claim_point, + &output_claims, + univariate_skips, + ); + } + + (claim_point, output_claims) = prove_batch_internal_round( + prover_state, + &witness.batch_partial_round_input, + &witness.committed_cubes, + &layers.batch_partial_rounds, + &claim_point, + &output_claims, + &selectors, + ); + + let pcs_point_for_cubes = claim_point.clone(); + + output_claims = output_claims[..WIDTH].to_vec(); + + for (input_layers, full_round) in witness + .remaining_initial_full_round_inputs + .iter() + .zip(&layers.initial_full_rounds_remaining) + .rev() + { + (claim_point, output_claims) = prove_gkr_round( + prover_state, + full_round, + input_layers, + &claim_point, + &output_claims, + univariate_skips, + ); + } + (claim_point, _) = prove_gkr_round( + prover_state, + &layers.initial_full_round, + &witness.input_layer, + &claim_point, + &output_claims, + univariate_skips, + ); + let pcs_point_for_inputs = claim_point.clone(); + + (pcs_point_for_inputs, pcs_point_for_cubes) +} + +#[instrument(skip_all)] +fn prove_gkr_round< + SC: SumcheckComputation + + SumcheckComputation + + SumcheckComputationPacked + + 'static, +>( + prover_state: &mut FSProver>, + computation: &SC, + input_layers: &[impl AsRef>>], + claim_point: &[EF], + output_claims: &[EF], + univariate_skips: usize, +) -> (Vec, Vec) { + let batching_scalar = prover_state.sample(); + let batching_scalars_powers = batching_scalar.powers().collect_n(output_claims.len()); + let batched_claim: EF = dot_product( + output_claims.iter().copied(), + batching_scalars_powers.iter().copied(), + ); + + let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( + univariate_skips, + MleGroupRef::BasePacked(input_layers.iter().map(|l| l.as_ref().as_slice()).collect()), + computation, + computation, + &batching_scalars_powers, + Some((claim_point.to_vec(), None)), + false, + prover_state, + batched_claim, + None, + ); + + // sanity check + debug_assert_eq!( + computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) + * eq_poly_with_skip(&sumcheck_point, &claim_point, univariate_skips), + sumcheck_final_sum + ); + + prover_state.add_extension_scalars(&sumcheck_inner_evals); + + (sumcheck_point.0, sumcheck_inner_evals) +} + +#[instrument(skip_all)] +fn prove_batch_internal_round( + prover_state: &mut FSProver>, + input_layers: &[Vec>], + committed_cubes: &[Vec>], + computation: &BatchPartialRounds, + claim_point: &[EF], + output_claims: &[EF], + selectors: &[DensePolynomial], +) -> (Vec, Vec) +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + assert_eq!(input_layers.len(), WIDTH); + assert_eq!(committed_cubes.len(), N_COMMITED_CUBES); + let univariate_skips = log2_strict_usize(selectors.len()); + + let cubes_evals = info_span!("computing cube evals").in_scope(|| { + batch_evaluate_univariate_multilinear( + &committed_cubes + .iter() + .map(|l| PFPacking::::unpack_slice(l)) + .collect::>(), + &claim_point, + selectors, + ) + }); + + prover_state.add_extension_scalars(&cubes_evals); + + let batching_scalar = prover_state.sample(); + let batched_claim: EF = dot_product( + output_claims.iter().chain(&cubes_evals).copied(), + batching_scalar.powers(), + ); + let batching_scalars_powers = batching_scalar.powers().collect_n(WIDTH + N_COMMITED_CUBES); + + let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( + univariate_skips, + MleGroupRef::BasePacked( + input_layers + .iter() + .chain(committed_cubes.iter()) + .map(Vec::as_slice) + .collect(), + ), + computation, + computation, + &batching_scalars_powers, + Some((claim_point.to_vec(), None)), + false, + prover_state, + batched_claim, + None, + ); + + // sanity check + debug_assert_eq!( + computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) + * eq_poly_with_skip(&sumcheck_point, &claim_point, univariate_skips), + sumcheck_final_sum + ); + + prover_state.add_extension_scalars(&sumcheck_inner_evals); + + (sumcheck_point.0, sumcheck_inner_evals) +} diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index 71bd02ba..78004c95 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -4,7 +4,6 @@ use multilinear_toolkit::prelude::*; use p3_koala_bear::{KoalaBear, QuinticExtensionFieldKB}; use rand::{Rng, SeedableRng, rngs::StdRng}; use std::{array, time::Instant}; -use tracing::{info_span, instrument}; use utils::{ build_prover_state, build_verifier_state, init_tracing, poseidon16_permute_mut, transposed_par_iter_mut, @@ -13,7 +12,10 @@ use whir_p3::{ FoldingFactor, SecurityAssumption, WhirConfig, WhirConfigBuilder, precompute_dft_twiddles, }; -use crate::{generate_poseidon_witness, gkr_layers::{BatchPartialRounds, PoseidonGKRLayers}}; +use crate::{ + generate_poseidon_witness, gkr_layers::PoseidonGKRLayers, prove_poseidon_gkr, + verify_poseidon_gkr, +}; type F = KoalaBear; type EF = QuinticExtensionFieldKB; @@ -89,90 +91,16 @@ fn test_prove_poseidons() { let global_poly_commited_packed = PFPacking::::pack_slice(global_poly_commited.as_base().unwrap()); - let mut claim_point = prover_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); - - let mut output_claims = info_span!("computing output claims").in_scope(|| { - batch_evaluate_univariate_multilinear( - &witness - .output_layer - .iter() - .map(|l| PFPacking::::unpack_slice(l)) - .collect::>(), - &claim_point, - &selectors, - ) - }); - - prover_state.add_extension_scalars(&output_claims); - - for (input_layers, full_round) in witness - .final_full_round_inputs - .iter() - .zip(&layers.final_full_rounds) - .rev() - { - (claim_point, output_claims) = prove_gkr_round( - &mut prover_state, - full_round, - input_layers, - &claim_point, - &output_claims, - ); - } + let claim_point = prover_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); - for (input_layers, partial_round) in witness - .remaining_partial_round_inputs - .iter() - .zip(&layers.partial_rounds_remaining) - .rev() - { - (claim_point, output_claims) = prove_gkr_round( - &mut prover_state, - partial_round, - input_layers, - &claim_point, - &output_claims, - ); - } - - (claim_point, output_claims) = prove_batch_internal_rounds( + let (pcs_point_for_inputs, pcs_point_for_cubes) = prove_poseidon_gkr( &mut prover_state, - &witness.batch_partial_round_input, - &witness.committed_cubes, - &layers.batch_partial_rounds, - &claim_point, - &output_claims, - &selectors, + &witness, + claim_point, + UNIVARIATE_SKIPS, + &layers, ); - let pcs_point_for_cubes = claim_point.clone(); - - output_claims = output_claims[..WIDTH].to_vec(); - - for (input_layers, full_round) in witness - .remaining_initial_full_round_inputs - .iter() - .zip(&layers.initial_full_rounds_remaining) - .rev() - { - (claim_point, output_claims) = prove_gkr_round( - &mut prover_state, - full_round, - input_layers, - &claim_point, - &output_claims, - ); - } - (claim_point, _) = prove_gkr_round( - &mut prover_state, - &layers.initial_full_round, - &witness.input_layer, - &claim_point, - &output_claims, - ); - - let pcs_point_for_inputs = claim_point.clone(); - // PCS opening let mut pcs_statements = vec![]; @@ -256,65 +184,17 @@ fn test_prove_poseidons() { let output_claim_point = verifier_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); - let mut output_claims = verifier_state.next_extension_scalars_vec(WIDTH).unwrap(); - - let mut claim_point = output_claim_point.clone(); - for full_round in layers.final_full_rounds.iter().rev() { - (claim_point, output_claims) = verify_gkr_round( - &mut verifier_state, - full_round, - log_n_poseidons, - &claim_point, - &output_claims, - ); - } - - for partial_round in layers.partial_rounds_remaining.iter().rev() { - (claim_point, output_claims) = verify_gkr_round( - &mut verifier_state, - partial_round, - log_n_poseidons, - &claim_point, - &output_claims, - ); - } - let claimed_cubes_evals = verifier_state - .next_extension_scalars_vec(N_COMMITED_CUBES) - .unwrap(); - - (claim_point, output_claims) = verify_gkr_round( - &mut verifier_state, - &layers.batch_partial_rounds, - log_n_poseidons, - &claim_point, - &[output_claims, claimed_cubes_evals.clone()].concat(), - ); - - let pcs_point_for_cubes = claim_point.clone(); - let pcs_evals_for_cubes = output_claims[WIDTH..].to_vec(); - - output_claims = output_claims[..WIDTH].to_vec(); - - for full_round in layers.initial_full_rounds_remaining.iter().rev() { - (claim_point, output_claims) = verify_gkr_round( - &mut verifier_state, - full_round, - log_n_poseidons, - &claim_point, - &output_claims, - ); - } - (claim_point, output_claims) = verify_gkr_round( + let ( + (pcs_point_for_inputs, pcs_evals_for_inputs), + (pcs_point_for_cubes, pcs_evals_for_cubes), + ) = verify_poseidon_gkr( &mut verifier_state, - &layers.initial_full_round, log_n_poseidons, - &claim_point, - &output_claims, + &output_claim_point, + &layers, + UNIVARIATE_SKIPS, ); - let pcs_point_for_inputs = claim_point.clone(); - let pcs_evals_for_inputs = output_claims.to_vec(); - // PCS verification let mut pcs_statements = vec![]; @@ -424,146 +304,3 @@ fn test_prove_poseidons() { ); println!("Verifier time: {}ms", verifier_duration.as_millis()); } - -#[instrument(skip_all)] -fn prove_gkr_round< - SC: SumcheckComputation - + SumcheckComputation - + SumcheckComputationPacked - + 'static, ->( - prover_state: &mut FSProver>, - computation: &SC, - input_layers: &[impl AsRef>>], - claim_point: &[EF], - output_claims: &[EF], -) -> (Vec, Vec) { - let batching_scalar = prover_state.sample(); - let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); - let batching_scalars_powers = batching_scalar.powers().collect_n(WIDTH); - - let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( - UNIVARIATE_SKIPS, - MleGroupRef::BasePacked(input_layers.iter().map(|l| l.as_ref().as_slice()).collect()), - computation, - computation, - &batching_scalars_powers, - Some((claim_point.to_vec(), None)), - false, - prover_state, - batched_claim, - None, - ); - - // sanity check - debug_assert_eq!( - computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) - * eq_poly_with_skip(&sumcheck_point, &claim_point, UNIVARIATE_SKIPS), - sumcheck_final_sum - ); - - prover_state.add_extension_scalars(&sumcheck_inner_evals); - - (sumcheck_point.0, sumcheck_inner_evals) -} - -#[instrument(skip_all)] -fn prove_batch_internal_rounds( - prover_state: &mut FSProver>, - input_layers: &[Vec>], - committed_cubes: &[Vec>], - computation: &BatchPartialRounds, - claim_point: &[EF], - output_claims: &[EF], - selectors: &[DensePolynomial], -) -> (Vec, Vec) { - assert_eq!(input_layers.len(), WIDTH); - assert_eq!(committed_cubes.len(), N_COMMITED_CUBES); - - let cubes_evals = info_span!("computing cube evals").in_scope(|| { - batch_evaluate_univariate_multilinear( - &committed_cubes - .iter() - .map(|l| PFPacking::::unpack_slice(l)) - .collect::>(), - &claim_point, - selectors, - ) - }); - - prover_state.add_extension_scalars(&cubes_evals); - - let batching_scalar = prover_state.sample(); - let batched_claim: EF = dot_product( - output_claims.iter().chain(&cubes_evals).copied(), - batching_scalar.powers(), - ); - let batching_scalars_powers = batching_scalar.powers().collect_n(WIDTH + N_COMMITED_CUBES); - - let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( - UNIVARIATE_SKIPS, - MleGroupRef::BasePacked( - input_layers - .iter() - .chain(committed_cubes.iter()) - .map(Vec::as_slice) - .collect(), - ), - computation, - computation, - &batching_scalars_powers, - Some((claim_point.to_vec(), None)), - false, - prover_state, - batched_claim, - None, - ); - - // sanity check - debug_assert_eq!( - computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) - * eq_poly_with_skip(&sumcheck_point, &claim_point, UNIVARIATE_SKIPS), - sumcheck_final_sum - ); - - prover_state.add_extension_scalars(&sumcheck_inner_evals); - - (sumcheck_point.0, sumcheck_inner_evals) -} - -fn verify_gkr_round>( - verifier_state: &mut FSVerifier>, - computation: &SC, - log_n_poseidons: usize, - claim_point: &[EF], - output_claims: &[EF], -) -> (Vec, Vec) { - let batching_scalar = verifier_state.sample(); - let batching_scalars_powers = batching_scalar.powers().collect_n(output_claims.len()); - let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); - - let (retrieved_batched_claim, sumcheck_postponed_claim) = sumcheck_verify_with_univariate_skip( - verifier_state, - computation.degree() + 1, - log_n_poseidons, - UNIVARIATE_SKIPS, - ) - .unwrap(); - - assert_eq!(retrieved_batched_claim, batched_claim); - - let sumcheck_inner_evals = verifier_state - .next_extension_scalars_vec(output_claims.len()) - .unwrap(); - assert_eq!( - computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) - * eq_poly_with_skip( - &sumcheck_postponed_claim.point, - &claim_point, - UNIVARIATE_SKIPS - ), - sumcheck_postponed_claim.value - ); - - (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) -} diff --git a/crates/poseidon_circuit/src/verify.rs b/crates/poseidon_circuit/src/verify.rs index e69de29b..f2c057b3 100644 --- a/crates/poseidon_circuit/src/verify.rs +++ b/crates/poseidon_circuit/src/verify.rs @@ -0,0 +1,123 @@ +use multilinear_toolkit::prelude::*; +use p3_koala_bear::{KoalaBearInternalLayerParameters, KoalaBearParameters}; +use p3_monty_31::InternalLayerBaseParameters; + +use crate::{EF, gkr_layers::PoseidonGKRLayers}; + +pub fn verify_poseidon_gkr( + verifier_state: &mut FSVerifier>, + log_n_poseidons: usize, + output_claim_point: &[EF], + layers: &PoseidonGKRLayers, + univariate_skips: usize, +) -> ((Vec, Vec), (Vec, Vec)) +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + let mut output_claims = verifier_state.next_extension_scalars_vec(WIDTH).unwrap(); + + let mut claim_point = output_claim_point.to_vec(); + for full_round in layers.final_full_rounds.iter().rev() { + (claim_point, output_claims) = verify_gkr_round( + verifier_state, + full_round, + log_n_poseidons, + &claim_point, + &output_claims, + univariate_skips, + ); + } + + for partial_round in layers.partial_rounds_remaining.iter().rev() { + (claim_point, output_claims) = verify_gkr_round( + verifier_state, + partial_round, + log_n_poseidons, + &claim_point, + &output_claims, + univariate_skips, + ); + } + let claimed_cubes_evals = verifier_state + .next_extension_scalars_vec(N_COMMITED_CUBES) + .unwrap(); + + (claim_point, output_claims) = verify_gkr_round( + verifier_state, + &layers.batch_partial_rounds, + log_n_poseidons, + &claim_point, + &[output_claims, claimed_cubes_evals.clone()].concat(), + univariate_skips, + ); + + let pcs_point_for_cubes = claim_point.clone(); + let pcs_evals_for_cubes = output_claims[WIDTH..].to_vec(); + + output_claims = output_claims[..WIDTH].to_vec(); + + for full_round in layers.initial_full_rounds_remaining.iter().rev() { + (claim_point, output_claims) = verify_gkr_round( + verifier_state, + full_round, + log_n_poseidons, + &claim_point, + &output_claims, + univariate_skips, + ); + } + (claim_point, output_claims) = verify_gkr_round( + verifier_state, + &layers.initial_full_round, + log_n_poseidons, + &claim_point, + &output_claims, + univariate_skips, + ); + + let pcs_point_for_inputs = claim_point.clone(); + let pcs_evals_for_inputs = output_claims.to_vec(); + + ( + (pcs_point_for_inputs, pcs_evals_for_inputs), + (pcs_point_for_cubes, pcs_evals_for_cubes), + ) +} + +fn verify_gkr_round>( + verifier_state: &mut FSVerifier>, + computation: &SC, + log_n_poseidons: usize, + claim_point: &[EF], + output_claims: &[EF], + univariate_skips: usize, +) -> (Vec, Vec) { + let batching_scalar = verifier_state.sample(); + let batching_scalars_powers = batching_scalar.powers().collect_n(output_claims.len()); + let batched_claim: EF = dot_product(output_claims.iter().copied(), batching_scalar.powers()); + + let (retrieved_batched_claim, sumcheck_postponed_claim) = sumcheck_verify_with_univariate_skip( + verifier_state, + computation.degree() + 1, + log_n_poseidons, + univariate_skips, + ) + .unwrap(); + + assert_eq!(retrieved_batched_claim, batched_claim); + + let sumcheck_inner_evals = verifier_state + .next_extension_scalars_vec(output_claims.len()) + .unwrap(); + assert_eq!( + computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) + * eq_poly_with_skip( + &sumcheck_postponed_claim.point, + &claim_point, + univariate_skips + ), + sumcheck_postponed_claim.value + ); + + (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) +} From e18dbc246634d080a414b9786093379fec7c586d Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 24 Oct 2025 20:31:28 +0400 Subject: [PATCH 25/42] using packed pcs --- Cargo.lock | 1 + crates/poseidon_circuit/Cargo.toml | 1 + crates/poseidon_circuit/src/prove.rs | 2 +- crates/poseidon_circuit/src/tests.rs | 320 ++++++++++++--------- crates/poseidon_circuit/src/witness_gen.rs | 93 +++--- 5 files changed, 242 insertions(+), 175 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 916d4225..d47d7c80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -903,6 +903,7 @@ dependencies = [ "p3-monty-31", "p3-poseidon2", "p3-util", + "packed_pcs", "rand", "tracing", "utils", diff --git a/crates/poseidon_circuit/Cargo.toml b/crates/poseidon_circuit/Cargo.toml index 6726708b..4e211f77 100644 --- a/crates/poseidon_circuit/Cargo.toml +++ b/crates/poseidon_circuit/Cargo.toml @@ -14,6 +14,7 @@ p3-util.workspace = true multilinear-toolkit.workspace = true p3-koala-bear.workspace = true p3-poseidon2.workspace = true +packed_pcs.workspace = true p3-monty-31.workspace = true [dev-dependencies] diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index 9cda2b2b..1b38f2d5 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -9,7 +9,7 @@ use tracing::{info_span, instrument}; pub fn prove_poseidon_gkr( prover_state: &mut FSProver>, - witness: &PoseidonWitness, + witness: &PoseidonWitness, WIDTH, N_COMMITED_CUBES>, mut claim_point: Vec, univariate_skips: usize, layers: &PoseidonGKRLayers, diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index 78004c95..f94a87a9 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -2,6 +2,10 @@ use multilinear_toolkit::prelude::*; use p3_koala_bear::{KoalaBear, QuinticExtensionFieldKB}; +use packed_pcs::{ + ColDims, packed_pcs_commit, packed_pcs_global_statements_for_prover, + packed_pcs_global_statements_for_verifier, packed_pcs_parse_commitment, +}; use rand::{Rng, SeedableRng, rngs::StdRng}; use std::{array, time::Instant}; use utils::{ @@ -13,8 +17,8 @@ use whir_p3::{ }; use crate::{ - generate_poseidon_witness, gkr_layers::PoseidonGKRLayers, prove_poseidon_gkr, - verify_poseidon_gkr, + default_cube_layers, generate_poseidon_witness, gkr_layers::PoseidonGKRLayers, + prove_poseidon_gkr, verify_poseidon_gkr, }; type F = KoalaBear; @@ -41,7 +45,7 @@ fn test_prove_poseidons() { starting_log_inv_rate: 1, }; let whir_n_vars = log_n_poseidons + log2_strict_usize(WIDTH + N_COMMITED_CUBES); - let whir_config = WhirConfig::new(whir_config_builder, whir_n_vars); + let whir_config = WhirConfig::new(whir_config_builder.clone(), whir_n_vars); let mut rng = StdRng::seed_from_u64(0); let n_poseidons = 1 << log_n_poseidons; @@ -56,40 +60,42 @@ fn test_prove_poseidons() { let layers = PoseidonGKRLayers::::build(); + let default_cubes = default_cube_layers::(&layers); + let input_col_dims = vec![ColDims::padded(n_poseidons, F::ZERO); WIDTH]; + let cubes_col_dims = default_cubes + .iter() + .map(|&v| ColDims::padded(n_poseidons, v)) + .collect::>(); + let committed_col_dims = [input_col_dims, cubes_col_dims].concat(); + + let log_smallest_decomposition_chunk = 10; // unused because everything is a power of 2 + let (mut verifier_state, proof_size, output_layer, prover_duration) = { // ---------------------------------------------------- PROVER ---------------------------------------------------- let prover_time = Instant::now(); - let witness = - generate_poseidon_witness::(input_layers_packed, &layers); + let witness = generate_poseidon_witness::, WIDTH, N_COMMITED_CUBES>( + input_layers_packed, + &layers, + ); let mut prover_state = build_prover_state::(); - let mut global_poly_commited: Vec = unsafe { uninitialized_vec(1 << whir_n_vars) }; - let mut chunks = split_at_mut_many( - &mut global_poly_commited, - (0..WIDTH + N_COMMITED_CUBES - 1) - .map(|i| (i + 1) << log_n_poseidons) - .collect::>() - .as_slice(), + + let committed_polys = witness + .input_layer + .iter() + .chain(&witness.committed_cubes) + .map(|s| PFPacking::::unpack_slice(s)) + .collect::>(); + + let pcs_commitment_witness = packed_pcs_commit( + &whir_config_builder, + &committed_polys, + &committed_col_dims, + &mut prover_state, + log_smallest_decomposition_chunk, ); - chunks[..WIDTH] - .par_iter_mut() - .enumerate() - .for_each(|(i, chunk)| { - chunk.copy_from_slice(&input_layers[i]); - }); - chunks[WIDTH..] - .par_iter_mut() - .enumerate() - .for_each(|(i, chunk)| { - chunk.copy_from_slice(PFPacking::::unpack_slice(&witness.committed_cubes[i])); - }); - - let global_poly_commited = MleOwned::Base(global_poly_commited); - let pcs_witness = whir_config.commit(&mut prover_state, &global_poly_commited); - let global_poly_commited_packed = - PFPacking::::pack_slice(global_poly_commited.as_base().unwrap()); let claim_point = prover_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); @@ -102,66 +108,98 @@ fn test_prove_poseidons() { ); // PCS opening - let mut pcs_statements = vec![]; let eq_mle_inputs = eval_eq_packed(&pcs_point_for_inputs[1..]); - let inner_evals_inputs = global_poly_commited_packed - [..global_poly_commited_packed.len() / 2] - .par_chunks_exact(eq_mle_inputs.len()) - .map(|chunk| { - let ef_sum = dot_product::, _, _>( - eq_mle_inputs.iter().copied(), - chunk.iter().copied(), - ); - as PackedFieldExtension>::to_ext_iter([ef_sum]).sum::() + let inner_evals_inputs = witness + .input_layer + .par_iter() + .map(|col| { + col.chunks_exact(eq_mle_inputs.len()) + .map(|chunk| { + let ef_sum = dot_product::, _, _>( + eq_mle_inputs.iter().copied(), + chunk.iter().copied(), + ); + as PackedFieldExtension>::to_ext_iter([ef_sum]) + .sum::() + }) + .collect::>() }) + .flatten() .collect::>(); + assert_eq!(inner_evals_inputs.len(), WIDTH << UNIVARIATE_SKIPS); prover_state.add_extension_scalars(&inner_evals_inputs); - let pcs_batching_scalars_inputs = prover_state.sample_vec(4 + UNIVARIATE_SKIPS); // 4 = log2(WIDTH) - pcs_statements.push(Evaluation { - point: MultilinearPoint( - [ - vec![EF::ZERO], - pcs_batching_scalars_inputs.clone(), - pcs_point_for_inputs[1..].to_vec(), - ] - .concat(), - ), - value: inner_evals_inputs.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs)), - }); + let mut input_pcs_statements = vec![]; + let pcs_batching_scalars_inputs = prover_state.sample_vec(UNIVARIATE_SKIPS); // 4 = log2(WIDTH) + for col_inner_evals in inner_evals_inputs.chunks_exact(1 << UNIVARIATE_SKIPS) { + input_pcs_statements.push(vec![Evaluation { + point: MultilinearPoint( + [ + pcs_batching_scalars_inputs.clone(), + pcs_point_for_inputs[1..].to_vec(), + ] + .concat(), + ), + value: col_inner_evals + .evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone())), + }]); + } let eq_mle_cubes = eval_eq_packed(&pcs_point_for_cubes[1..]); - let inner_evals_cubes = global_poly_commited_packed - [global_poly_commited_packed.len() / 2..] - .par_chunks_exact(eq_mle_cubes.len()) - .map(|chunk| { - let ef_sum = dot_product::, _, _>( - eq_mle_cubes.iter().copied(), - chunk.iter().copied(), - ); - as PackedFieldExtension>::to_ext_iter([ef_sum]).sum::() + let inner_evals_cubes = witness + .committed_cubes + .par_iter() + .map(|col| { + col.chunks_exact(eq_mle_cubes.len()) + .map(|chunk| { + let ef_sum = dot_product::, _, _>( + eq_mle_cubes.iter().copied(), + chunk.iter().copied(), + ); + as PackedFieldExtension>::to_ext_iter([ef_sum]) + .sum::() + }) + .collect::>() }) + .flatten() .collect::>(); + assert_eq!( + inner_evals_cubes.len(), + N_COMMITED_CUBES << UNIVARIATE_SKIPS + ); prover_state.add_extension_scalars(&inner_evals_cubes); - let pcs_batching_scalars_cubes = - prover_state.sample_vec(log2_strict_usize(N_COMMITED_CUBES) + UNIVARIATE_SKIPS); - pcs_statements.push(Evaluation { - point: MultilinearPoint( - [ - vec![EF::ONE], - pcs_batching_scalars_cubes.clone(), - pcs_point_for_cubes[1..].to_vec(), - ] - .concat(), - ), - value: inner_evals_cubes.evaluate(&MultilinearPoint(pcs_batching_scalars_cubes)), - }); + let mut cubes_pcs_statements = vec![]; + let pcs_batching_scalars_cubes = prover_state.sample_vec(UNIVARIATE_SKIPS); + for col_inner_evals in inner_evals_cubes.chunks_exact(1 << UNIVARIATE_SKIPS) { + cubes_pcs_statements.push(vec![Evaluation { + point: MultilinearPoint( + [ + pcs_batching_scalars_cubes.clone(), + pcs_point_for_cubes[1..].to_vec(), + ] + .concat(), + ), + value: col_inner_evals + .evaluate(&MultilinearPoint(pcs_batching_scalars_cubes.clone())), + }]); + } + + assert_eq!(committed_polys.len(), WIDTH + N_COMMITED_CUBES); + assert_eq!(input_pcs_statements.len(), WIDTH); + assert_eq!(cubes_pcs_statements.len(), N_COMMITED_CUBES); + let global_statements = packed_pcs_global_statements_for_prover( + &committed_polys, + &committed_col_dims, + log_smallest_decomposition_chunk, + &[input_pcs_statements, cubes_pcs_statements].concat(), + &mut prover_state, + ); whir_config.prove( &mut prover_state, - pcs_statements, - pcs_witness, - &global_poly_commited.by_ref(), + global_statements, + pcs_commitment_witness.inner_witness, + &pcs_commitment_witness.packed_polynomial.by_ref(), ); let prover_duration = prover_time.elapsed(); @@ -178,9 +216,13 @@ fn test_prove_poseidons() { { // ---------------------------------------------------- VERIFIER ---------------------------------------------------- - let parsed_pcs_commitment = whir_config - .parse_commitment::(&mut verifier_state) - .unwrap(); + let parsed_pcs_commitment = packed_pcs_parse_commitment( + &whir_config_builder, + &mut verifier_state, + &committed_col_dims, + log_smallest_decomposition_chunk, + ) + .unwrap(); let output_claim_point = verifier_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); @@ -197,75 +239,83 @@ fn test_prove_poseidons() { // PCS verification - let mut pcs_statements = vec![]; - let inner_evals_inputs = verifier_state .next_extension_scalars_vec(WIDTH << UNIVARIATE_SKIPS) .unwrap(); - let pcs_batching_scalars_inputs = verifier_state.sample_vec(4 + UNIVARIATE_SKIPS); // 4 = log2(WIDTH) - pcs_statements.push(Evaluation { - point: MultilinearPoint( - [ - vec![EF::ZERO], - pcs_batching_scalars_inputs.clone(), - pcs_point_for_inputs[1..].to_vec(), - ] - .concat(), - ), - value: inner_evals_inputs.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs)), - }); + let pcs_batching_scalars_inputs = verifier_state.sample_vec(UNIVARIATE_SKIPS); + let mut input_pcs_statements = vec![]; + for (&eval, col_inner_evals) in pcs_evals_for_inputs + .iter() + .zip(inner_evals_inputs.chunks_exact(1 << UNIVARIATE_SKIPS)) { - for (&eval, inner_evals) in pcs_evals_for_inputs - .iter() - .zip(inner_evals_inputs.chunks_exact(1 << UNIVARIATE_SKIPS)) - { - assert_eq!( - eval, - evaluate_univariate_multilinear::<_, _, _, false>( - inner_evals, - &pcs_point_for_inputs[..1], - &selectors, - None - ) - ); - } + assert_eq!( + eval, + evaluate_univariate_multilinear::<_, _, _, false>( + col_inner_evals, + &pcs_point_for_inputs[..1], + &selectors, + None + ) + ); + input_pcs_statements.push(vec![Evaluation { + point: MultilinearPoint( + [ + pcs_batching_scalars_inputs.clone(), + pcs_point_for_inputs[1..].to_vec(), + ] + .concat(), + ), + value: col_inner_evals + .evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone())), + }]); } let inner_evals_cubes = verifier_state .next_extension_scalars_vec(N_COMMITED_CUBES << UNIVARIATE_SKIPS) .unwrap(); - let pcs_batching_scalars_cubes = - verifier_state.sample_vec(log2_strict_usize(N_COMMITED_CUBES) + UNIVARIATE_SKIPS); - pcs_statements.push(Evaluation { - point: MultilinearPoint( - [ - vec![EF::ONE], - pcs_batching_scalars_cubes.clone(), - pcs_point_for_cubes[1..].to_vec(), - ] - .concat(), - ), - value: inner_evals_cubes.evaluate(&MultilinearPoint(pcs_batching_scalars_cubes)), - }); + let pcs_batching_scalars_cubes = verifier_state.sample_vec(UNIVARIATE_SKIPS); + let mut cubes_pcs_statements = vec![]; + for (&eval, col_inner_evals) in pcs_evals_for_cubes + .iter() + .zip(inner_evals_cubes.chunks_exact(1 << UNIVARIATE_SKIPS)) { - for (&eval, inner_evals) in pcs_evals_for_cubes - .iter() - .zip(inner_evals_cubes.chunks_exact(1 << UNIVARIATE_SKIPS)) - { - assert_eq!( - eval, - evaluate_univariate_multilinear::<_, _, _, false>( - inner_evals, - &pcs_point_for_cubes[..1], - &selectors, - None - ) - ); - } + assert_eq!( + eval, + evaluate_univariate_multilinear::<_, _, _, false>( + col_inner_evals, + &pcs_point_for_cubes[..1], + &selectors, + None + ) + ); + cubes_pcs_statements.push(vec![Evaluation { + point: MultilinearPoint( + [ + pcs_batching_scalars_cubes.clone(), + pcs_point_for_cubes[1..].to_vec(), + ] + .concat(), + ), + value: col_inner_evals + .evaluate(&MultilinearPoint(pcs_batching_scalars_cubes.clone())), + }]); } + let global_statements = packed_pcs_global_statements_for_verifier( + &committed_col_dims, + log_smallest_decomposition_chunk, + &[input_pcs_statements, cubes_pcs_statements].concat(), + &mut verifier_state, + &Default::default(), + ) + .unwrap(); + whir_config - .verify(&mut verifier_state, &parsed_pcs_commitment, pcs_statements) + .verify::( + &mut verifier_state, + &parsed_pcs_commitment, + global_statements, + ) .unwrap(); } let verifier_duration = verifier_time.elapsed(); diff --git a/crates/poseidon_circuit/src/witness_gen.rs b/crates/poseidon_circuit/src/witness_gen.rs index 906dae6e..ef7e2879 100644 --- a/crates/poseidon_circuit/src/witness_gen.rs +++ b/crates/poseidon_circuit/src/witness_gen.rs @@ -1,7 +1,6 @@ use std::array; use multilinear_toolkit::prelude::*; -use p3_field::PrimeCharacteristicRing; use p3_koala_bear::GenericPoseidon2LinearLayersKoalaBear; use p3_koala_bear::KoalaBearInternalLayerParameters; use p3_koala_bear::KoalaBearParameters; @@ -16,29 +15,30 @@ use crate::gkr_layers::PoseidonGKRLayers; use crate::{F, gkr_layers::FullRoundComputation}; #[derive(Debug)] -pub struct PoseidonWitness { - pub input_layer: [Vec>; WIDTH], // input of the permutation - pub remaining_initial_full_round_inputs: Vec<[Vec>; WIDTH]>, // the remaining input of each initial full round - pub batch_partial_round_input: [Vec>; WIDTH], // again, the input of the batch (partial) round - pub committed_cubes: [Vec>; N_COMMITED_CUBES], // the cubes commited in the batch (partial) rounds - pub remaining_partial_round_inputs: Vec<[Vec>; WIDTH]>, // the input of each remaining partial round - pub final_full_round_inputs: Vec<[Vec>; WIDTH]>, // the input of each final full round - pub output_layer: [Vec>; WIDTH], // output of the permutation +pub struct PoseidonWitness { + pub input_layer: [Vec; WIDTH], // input of the permutation + pub remaining_initial_full_round_inputs: Vec<[Vec; WIDTH]>, // the remaining input of each initial full round + pub batch_partial_round_input: [Vec; WIDTH], // again, the input of the batch (partial) round + pub committed_cubes: [Vec; N_COMMITED_CUBES], // the cubes commited in the batch (partial) rounds + pub remaining_partial_round_inputs: Vec<[Vec; WIDTH]>, // the input of each remaining partial round + pub final_full_round_inputs: Vec<[Vec; WIDTH]>, // the input of each final full round + pub output_layer: [Vec; WIDTH], // output of the permutation } -pub fn generate_poseidon_witness( - input_layer: [Vec>; WIDTH], - layers: &PoseidonGKRLayers -) -> PoseidonWitness +pub fn generate_poseidon_witness( + input_layer: [Vec; WIDTH], + layers: &PoseidonGKRLayers, +) -> PoseidonWitness where + A: Algebra + Copy + Send + Sync, KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { - let mut remaining_initial_full_layers = vec![apply_full_round::<_, true>( + let mut remaining_initial_full_layers = vec![apply_full_round::<_, _, true>( &input_layer, &layers.initial_full_round, )]; for round in &layers.initial_full_rounds_remaining { - remaining_initial_full_layers.push(apply_full_round::<_, false>( + remaining_initial_full_layers.push(apply_full_round::<_, _, false>( remaining_initial_full_layers.last().unwrap(), round, )); @@ -58,7 +58,7 @@ where let mut final_full_layer_inputs = vec![remaining_partial_inputs.pop().unwrap()]; for round in &layers.final_full_rounds { - final_full_layer_inputs.push(apply_full_round::<_, false>( + final_full_layer_inputs.push(apply_full_round::<_, _, false>( final_full_layer_inputs.last().unwrap(), round, )); @@ -78,19 +78,19 @@ where } #[instrument(skip_all)] -fn apply_full_round( - input_layers: &[Vec>; WIDTH], +fn apply_full_round( + input_layers: &[Vec; WIDTH], full_round: &FullRoundComputation, -) -> [Vec>; WIDTH] +) -> [Vec; WIDTH] where + A: Algebra + Copy + Send + Sync, KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { - let mut output_layers: [_; WIDTH] = - array::from_fn(|_| FPacking::::zero_vec(input_layers[0].len())); + let mut output_layers: [_; WIDTH] = array::from_fn(|_| A::zero_vec(input_layers[0].len())); transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { - let mut buff: [FPacking; WIDTH] = array::from_fn(|j| input_layers[j][row_index]); + let mut buff: [A; WIDTH] = array::from_fn(|j| input_layers[j][row_index]); if FIRST { GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); } @@ -106,20 +106,20 @@ where } #[instrument(skip_all)] -fn apply_partial_round( - input_layers: &[Vec>], +fn apply_partial_round( + input_layers: &[Vec], partial_round: &PartialRoundComputation, -) -> [Vec>; WIDTH] +) -> [Vec; WIDTH] where + A: Algebra + Copy + Send + Sync, KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { - let mut output_layers: [_; WIDTH] = - array::from_fn(|_| FPacking::::zero_vec(input_layers[0].len())); + let mut output_layers: [_; WIDTH] = array::from_fn(|_| A::zero_vec(input_layers[0].len())); transposed_par_iter_mut(&mut output_layers) .enumerate() .for_each(|(row_index, output_row)| { let first_cubed = (input_layers[0][row_index] + partial_round.constant).cube(); - let mut buff = [FPacking::::ZERO; WIDTH]; + let mut buff = [A::ZERO; WIDTH]; buff[0] = first_cubed; for j in 1..WIDTH { buff[j] = input_layers[j][row_index]; @@ -133,25 +133,21 @@ where } #[instrument(skip_all)] -fn apply_batch_partial_rounds( - input_layers: &[Vec>], +fn apply_batch_partial_rounds( + input_layers: &[Vec], rounds: &BatchPartialRounds, -) -> ( - [Vec>; WIDTH], - [Vec>; N_COMMITED_CUBES], -) +) -> ([Vec; WIDTH], [Vec; N_COMMITED_CUBES]) where + A: Algebra + Copy + Send + Sync, KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { - let mut output_layers: [_; WIDTH] = - array::from_fn(|_| FPacking::::zero_vec(input_layers[0].len())); - let mut cubes: [_; N_COMMITED_CUBES] = - array::from_fn(|_| FPacking::::zero_vec(input_layers[0].len())); + let mut output_layers: [_; WIDTH] = array::from_fn(|_| A::zero_vec(input_layers[0].len())); + let mut cubes: [_; N_COMMITED_CUBES] = array::from_fn(|_| A::zero_vec(input_layers[0].len())); transposed_par_iter_mut(&mut output_layers) .zip(transposed_par_iter_mut(&mut cubes)) .enumerate() .for_each(|(row_index, (output_row, cubes))| { - let mut buff: [FPacking; WIDTH] = array::from_fn(|j| input_layers[j][row_index]); + let mut buff: [A; WIDTH] = array::from_fn(|j| input_layers[j][row_index]); for (i, &constant) in rounds.constants.iter().enumerate() { *cubes[i] = (buff[0] + constant).cube(); buff[0] = *cubes[i]; @@ -165,3 +161,22 @@ where }); (output_layers, cubes) } + +pub fn default_cube_layers( + layers: &PoseidonGKRLayers, +) -> [A; N_COMMITED_CUBES] +where + A: Algebra + Copy + Send + Sync, + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + generate_poseidon_witness::( + array::from_fn(|_| vec![A::ZERO]), + layers, + ) + .committed_cubes + .iter() + .map(|v| v[0]) + .collect::>() + .try_into() + .unwrap() +} From 419a1b093e21b0975ca5a7e96c0f0387a0ff02b7 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 24 Oct 2025 21:07:26 +0400 Subject: [PATCH 26/42] simplify --- crates/poseidon_circuit/src/prove.rs | 41 ++++++ crates/poseidon_circuit/src/tests.rs | 188 ++++++-------------------- crates/poseidon_circuit/src/verify.rs | 41 +++++- crates/utils/src/poseidon2.rs | 15 +- 4 files changed, 133 insertions(+), 152 deletions(-) diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index 1b38f2d5..98b774af 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -220,3 +220,44 @@ where (sumcheck_point.0, sumcheck_inner_evals) } + +pub(crate) fn inner_evals_on_commited_columns( + prover_state: &mut FSProver>, + point: &[EF], + univariate_skips: usize, + columns: &[Vec>], +) -> Vec>> { + let eq_mle = eval_eq_packed(&point[1..]); + let inner_evals = columns + .par_iter() + .map(|col| { + col.chunks_exact(eq_mle.len()) + .map(|chunk| { + let ef_sum = dot_product::, _, _>( + eq_mle.iter().copied(), + chunk.iter().copied(), + ); + as PackedFieldExtension>::to_ext_iter([ef_sum]) + .sum::() + }) + .collect::>() + }) + .flatten() + .collect::>(); + prover_state.add_extension_scalars(&inner_evals); + let mut pcs_statements = vec![]; + let pcs_batching_scalars_inputs = prover_state.sample_vec(univariate_skips); + for col_inner_evals in inner_evals.chunks_exact(1 << univariate_skips) { + pcs_statements.push(vec![Evaluation { + point: MultilinearPoint( + [ + pcs_batching_scalars_inputs.clone(), + point[1..].to_vec(), + ] + .concat(), + ), + value: col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone())), + }]); + } + pcs_statements +} diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index f94a87a9..0600b2ce 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -10,7 +10,7 @@ use rand::{Rng, SeedableRng, rngs::StdRng}; use std::{array, time::Instant}; use utils::{ build_prover_state, build_verifier_state, init_tracing, poseidon16_permute_mut, - transposed_par_iter_mut, + poseidon24_permute_mut, transposed_par_iter_mut, }; use whir_p3::{ FoldingFactor, SecurityAssumption, WhirConfig, WhirConfigBuilder, precompute_dft_twiddles, @@ -18,7 +18,8 @@ use whir_p3::{ use crate::{ default_cube_layers, generate_poseidon_witness, gkr_layers::PoseidonGKRLayers, - prove_poseidon_gkr, verify_poseidon_gkr, + inner_evals_on_commited_columns, prove_poseidon_gkr, verify_inner_evals_on_commited_columns, + verify_poseidon_gkr, }; type F = KoalaBear; @@ -26,7 +27,7 @@ type EF = QuinticExtensionFieldKB; const UNIVARIATE_SKIPS: usize = 3; const WIDTH: usize = 16; -const N_COMMITED_CUBES: usize = 16; // power of 2 to increase PCS efficiency +const N_COMMITED_CUBES: usize = 16; #[test] fn test_prove_poseidons() { @@ -44,7 +45,7 @@ fn test_prove_poseidons() { security_level: 128, starting_log_inv_rate: 1, }; - let whir_n_vars = log_n_poseidons + log2_strict_usize(WIDTH + N_COMMITED_CUBES); + let whir_n_vars = log_n_poseidons + log2_ceil_usize(WIDTH + N_COMMITED_CUBES); let whir_config = WhirConfig::new(whir_config_builder.clone(), whir_n_vars); let mut rng = StdRng::seed_from_u64(0); @@ -109,85 +110,18 @@ fn test_prove_poseidons() { // PCS opening - let eq_mle_inputs = eval_eq_packed(&pcs_point_for_inputs[1..]); - let inner_evals_inputs = witness - .input_layer - .par_iter() - .map(|col| { - col.chunks_exact(eq_mle_inputs.len()) - .map(|chunk| { - let ef_sum = dot_product::, _, _>( - eq_mle_inputs.iter().copied(), - chunk.iter().copied(), - ); - as PackedFieldExtension>::to_ext_iter([ef_sum]) - .sum::() - }) - .collect::>() - }) - .flatten() - .collect::>(); - assert_eq!(inner_evals_inputs.len(), WIDTH << UNIVARIATE_SKIPS); - prover_state.add_extension_scalars(&inner_evals_inputs); - let mut input_pcs_statements = vec![]; - let pcs_batching_scalars_inputs = prover_state.sample_vec(UNIVARIATE_SKIPS); // 4 = log2(WIDTH) - for col_inner_evals in inner_evals_inputs.chunks_exact(1 << UNIVARIATE_SKIPS) { - input_pcs_statements.push(vec![Evaluation { - point: MultilinearPoint( - [ - pcs_batching_scalars_inputs.clone(), - pcs_point_for_inputs[1..].to_vec(), - ] - .concat(), - ), - value: col_inner_evals - .evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone())), - }]); - } - - let eq_mle_cubes = eval_eq_packed(&pcs_point_for_cubes[1..]); - let inner_evals_cubes = witness - .committed_cubes - .par_iter() - .map(|col| { - col.chunks_exact(eq_mle_cubes.len()) - .map(|chunk| { - let ef_sum = dot_product::, _, _>( - eq_mle_cubes.iter().copied(), - chunk.iter().copied(), - ); - as PackedFieldExtension>::to_ext_iter([ef_sum]) - .sum::() - }) - .collect::>() - }) - .flatten() - .collect::>(); - assert_eq!( - inner_evals_cubes.len(), - N_COMMITED_CUBES << UNIVARIATE_SKIPS + let input_pcs_statements = inner_evals_on_commited_columns( + &mut prover_state, + &pcs_point_for_inputs, + UNIVARIATE_SKIPS, + &witness.input_layer, + ); + let cubes_pcs_statements = inner_evals_on_commited_columns( + &mut prover_state, + &pcs_point_for_cubes, + UNIVARIATE_SKIPS, + &witness.committed_cubes, ); - prover_state.add_extension_scalars(&inner_evals_cubes); - let mut cubes_pcs_statements = vec![]; - let pcs_batching_scalars_cubes = prover_state.sample_vec(UNIVARIATE_SKIPS); - for col_inner_evals in inner_evals_cubes.chunks_exact(1 << UNIVARIATE_SKIPS) { - cubes_pcs_statements.push(vec![Evaluation { - point: MultilinearPoint( - [ - pcs_batching_scalars_cubes.clone(), - pcs_point_for_cubes[1..].to_vec(), - ] - .concat(), - ), - value: col_inner_evals - .evaluate(&MultilinearPoint(pcs_batching_scalars_cubes.clone())), - }]); - } - - assert_eq!(committed_polys.len(), WIDTH + N_COMMITED_CUBES); - assert_eq!(input_pcs_statements.len(), WIDTH); - assert_eq!(cubes_pcs_statements.len(), N_COMMITED_CUBES); - let global_statements = packed_pcs_global_statements_for_prover( &committed_polys, &committed_col_dims, @@ -239,67 +173,19 @@ fn test_prove_poseidons() { // PCS verification - let inner_evals_inputs = verifier_state - .next_extension_scalars_vec(WIDTH << UNIVARIATE_SKIPS) - .unwrap(); - let pcs_batching_scalars_inputs = verifier_state.sample_vec(UNIVARIATE_SKIPS); - let mut input_pcs_statements = vec![]; - for (&eval, col_inner_evals) in pcs_evals_for_inputs - .iter() - .zip(inner_evals_inputs.chunks_exact(1 << UNIVARIATE_SKIPS)) - { - assert_eq!( - eval, - evaluate_univariate_multilinear::<_, _, _, false>( - col_inner_evals, - &pcs_point_for_inputs[..1], - &selectors, - None - ) - ); - input_pcs_statements.push(vec![Evaluation { - point: MultilinearPoint( - [ - pcs_batching_scalars_inputs.clone(), - pcs_point_for_inputs[1..].to_vec(), - ] - .concat(), - ), - value: col_inner_evals - .evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone())), - }]); - } + let input_pcs_statements = verify_inner_evals_on_commited_columns( + &mut verifier_state, + &pcs_point_for_inputs, + &pcs_evals_for_inputs, + &selectors, + ); - let inner_evals_cubes = verifier_state - .next_extension_scalars_vec(N_COMMITED_CUBES << UNIVARIATE_SKIPS) - .unwrap(); - let pcs_batching_scalars_cubes = verifier_state.sample_vec(UNIVARIATE_SKIPS); - let mut cubes_pcs_statements = vec![]; - for (&eval, col_inner_evals) in pcs_evals_for_cubes - .iter() - .zip(inner_evals_cubes.chunks_exact(1 << UNIVARIATE_SKIPS)) - { - assert_eq!( - eval, - evaluate_univariate_multilinear::<_, _, _, false>( - col_inner_evals, - &pcs_point_for_cubes[..1], - &selectors, - None - ) - ); - cubes_pcs_statements.push(vec![Evaluation { - point: MultilinearPoint( - [ - pcs_batching_scalars_cubes.clone(), - pcs_point_for_cubes[1..].to_vec(), - ] - .concat(), - ), - value: col_inner_evals - .evaluate(&MultilinearPoint(pcs_batching_scalars_cubes.clone())), - }]); - } + let cubes_pcs_statements = verify_inner_evals_on_commited_columns( + &mut verifier_state, + &pcs_point_for_cubes, + &pcs_evals_for_cubes, + &selectors, + ); let global_statements = packed_pcs_global_statements_for_verifier( &committed_col_dims, @@ -323,10 +209,20 @@ fn test_prove_poseidons() { let mut data_to_hash = input_layers.clone(); let plaintext_time = Instant::now(); transposed_par_iter_mut(&mut data_to_hash).for_each(|row| { - let mut buff = array::from_fn(|j| *row[j]); - poseidon16_permute_mut(&mut buff); - for j in 0..WIDTH { - *row[j] = buff[j]; + if WIDTH == 16 { + let mut buff = array::from_fn(|j| *row[j]); + poseidon16_permute_mut(&mut buff); + for j in 0..WIDTH { + *row[j] = buff[j]; + } + } else if WIDTH == 24 { + let mut buff = array::from_fn(|j| *row[j]); + poseidon24_permute_mut(&mut buff); + for j in 0..WIDTH { + *row[j] = buff[j]; + } + } else { + panic!("Unsupported WIDTH"); } }); let plaintext_duration = plaintext_time.elapsed(); diff --git a/crates/poseidon_circuit/src/verify.rs b/crates/poseidon_circuit/src/verify.rs index f2c057b3..e74f2a3e 100644 --- a/crates/poseidon_circuit/src/verify.rs +++ b/crates/poseidon_circuit/src/verify.rs @@ -2,7 +2,7 @@ use multilinear_toolkit::prelude::*; use p3_koala_bear::{KoalaBearInternalLayerParameters, KoalaBearParameters}; use p3_monty_31::InternalLayerBaseParameters; -use crate::{EF, gkr_layers::PoseidonGKRLayers}; +use crate::{gkr_layers::PoseidonGKRLayers, EF, F}; pub fn verify_poseidon_gkr( verifier_state: &mut FSVerifier>, @@ -121,3 +121,42 @@ fn verify_gkr_round>( (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) } + +pub(crate) fn verify_inner_evals_on_commited_columns( + verifier_state: &mut FSVerifier>, + point: &[EF], + claimed_evals: &[EF], + selectors: &[DensePolynomial], +) -> Vec>> { + let univariate_skips = log2_strict_usize(selectors.len()); + let inner_evals_inputs = verifier_state + .next_extension_scalars_vec(claimed_evals.len() << univariate_skips) + .unwrap(); + let pcs_batching_scalars_inputs = verifier_state.sample_vec(univariate_skips); + let mut pcs_statements = vec![]; + for (&eval, col_inner_evals) in claimed_evals + .iter() + .zip(inner_evals_inputs.chunks_exact(1 << univariate_skips)) + { + assert_eq!( + eval, + evaluate_univariate_multilinear::<_, _, _, false>( + col_inner_evals, + &point[..1], + &selectors, + None + ) + ); + pcs_statements.push(vec![Evaluation { + point: MultilinearPoint( + [ + pcs_batching_scalars_inputs.clone(), + point[1..].to_vec(), + ] + .concat(), + ), + value: col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone())), + }]); + } + pcs_statements +} diff --git a/crates/utils/src/poseidon2.rs b/crates/utils/src/poseidon2.rs index ba7c2795..b494bd73 100644 --- a/crates/utils/src/poseidon2.rs +++ b/crates/utils/src/poseidon2.rs @@ -107,6 +107,16 @@ pub fn poseidon16_permute_mut(input: &mut [KoalaBear; 16]) { get_poseidon16().permute_mut(input); } +#[inline(always)] +pub fn poseidon24_permute(input: [KoalaBear; 24]) -> [KoalaBear; 24] { + get_poseidon24().permute(input) +} + +#[inline(always)] +pub fn poseidon24_permute_mut(input: &mut [KoalaBear; 24]) { + get_poseidon24().permute_mut(input); +} + static POSEIDON24_INSTANCE: OnceLock = OnceLock::new(); #[inline(always)] @@ -124,11 +134,6 @@ pub(crate) fn get_poseidon24() -> &'static Poseidon24 { }) } -#[inline(always)] -pub fn poseidon24_permute(input: [KoalaBear; 24]) -> [KoalaBear; 24] { - get_poseidon24().permute(input) -} - pub fn build_poseidon_16_air() -> Poseidon16Air { Poseidon16Air::new(build_poseidon16_constants()) } From 1e5529aa27b58c5391f5377e6ac8d7b6241481df Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 24 Oct 2025 22:53:21 +0400 Subject: [PATCH 27/42] simplify --- crates/poseidon_circuit/src/prove.rs | 28 ++++++++---- crates/poseidon_circuit/src/tests.rs | 51 +++++----------------- crates/poseidon_circuit/src/verify.rs | 32 +++++++++----- crates/poseidon_circuit/src/witness_gen.rs | 6 +-- 4 files changed, 53 insertions(+), 64 deletions(-) diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index 98b774af..a066c45f 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -13,7 +13,10 @@ pub fn prove_poseidon_gkr( mut claim_point: Vec, univariate_skips: usize, layers: &PoseidonGKRLayers, -) -> (Vec, Vec) +) -> ( + Vec>>, + Vec>>, +) where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { @@ -104,7 +107,20 @@ where ); let pcs_point_for_inputs = claim_point.clone(); - (pcs_point_for_inputs, pcs_point_for_cubes) + let input_pcs_statements = inner_evals_on_commited_columns( + prover_state, + &pcs_point_for_inputs, + univariate_skips, + &witness.input_layer, + ); + let cubes_pcs_statements = inner_evals_on_commited_columns( + prover_state, + &pcs_point_for_cubes, + univariate_skips, + &witness.committed_cubes, + ); + + (input_pcs_statements, cubes_pcs_statements) } #[instrument(skip_all)] @@ -221,7 +237,7 @@ where (sumcheck_point.0, sumcheck_inner_evals) } -pub(crate) fn inner_evals_on_commited_columns( +fn inner_evals_on_commited_columns( prover_state: &mut FSProver>, point: &[EF], univariate_skips: usize, @@ -250,11 +266,7 @@ pub(crate) fn inner_evals_on_commited_columns( for col_inner_evals in inner_evals.chunks_exact(1 << univariate_skips) { pcs_statements.push(vec![Evaluation { point: MultilinearPoint( - [ - pcs_batching_scalars_inputs.clone(), - point[1..].to_vec(), - ] - .concat(), + [pcs_batching_scalars_inputs.clone(), point[1..].to_vec()].concat(), ), value: col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone())), }]); diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index 0600b2ce..aaaf7787 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -18,15 +18,14 @@ use whir_p3::{ use crate::{ default_cube_layers, generate_poseidon_witness, gkr_layers::PoseidonGKRLayers, - inner_evals_on_commited_columns, prove_poseidon_gkr, verify_inner_evals_on_commited_columns, - verify_poseidon_gkr, + prove_poseidon_gkr, verify_poseidon_gkr, }; type F = KoalaBear; type EF = QuinticExtensionFieldKB; -const UNIVARIATE_SKIPS: usize = 3; const WIDTH: usize = 16; +const UNIVARIATE_SKIPS: usize = 3; const N_COMMITED_CUBES: usize = 16; #[test] @@ -53,11 +52,10 @@ fn test_prove_poseidons() { let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) .collect::>(); - let selectors = univariate_selectors::(UNIVARIATE_SKIPS); - let input_layers: [_; WIDTH] = + let input: [_; WIDTH] = array::from_fn(|i| perm_inputs.par_iter().map(|x| x[i]).collect::>()); - let input_layers_packed: [_; WIDTH] = - array::from_fn(|i| PFPacking::::pack_slice(&input_layers[i]).to_vec()); + let input_packed: [_; WIDTH] = + array::from_fn(|i| PFPacking::::pack_slice(&input[i]).to_vec()); let layers = PoseidonGKRLayers::::build(); @@ -69,7 +67,7 @@ fn test_prove_poseidons() { .collect::>(); let committed_col_dims = [input_col_dims, cubes_col_dims].concat(); - let log_smallest_decomposition_chunk = 10; // unused because everything is a power of 2 + let log_smallest_decomposition_chunk = 0; // unused because everything is a power of 2 let (mut verifier_state, proof_size, output_layer, prover_duration) = { // ---------------------------------------------------- PROVER ---------------------------------------------------- @@ -77,7 +75,7 @@ fn test_prove_poseidons() { let prover_time = Instant::now(); let witness = generate_poseidon_witness::, WIDTH, N_COMMITED_CUBES>( - input_layers_packed, + input_packed, &layers, ); @@ -100,7 +98,7 @@ fn test_prove_poseidons() { let claim_point = prover_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); - let (pcs_point_for_inputs, pcs_point_for_cubes) = prove_poseidon_gkr( + let (input_pcs_statements, cubes_pcs_statements) = prove_poseidon_gkr( &mut prover_state, &witness, claim_point, @@ -110,18 +108,6 @@ fn test_prove_poseidons() { // PCS opening - let input_pcs_statements = inner_evals_on_commited_columns( - &mut prover_state, - &pcs_point_for_inputs, - UNIVARIATE_SKIPS, - &witness.input_layer, - ); - let cubes_pcs_statements = inner_evals_on_commited_columns( - &mut prover_state, - &pcs_point_for_cubes, - UNIVARIATE_SKIPS, - &witness.committed_cubes, - ); let global_statements = packed_pcs_global_statements_for_prover( &committed_polys, &committed_col_dims, @@ -160,10 +146,7 @@ fn test_prove_poseidons() { let output_claim_point = verifier_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); - let ( - (pcs_point_for_inputs, pcs_evals_for_inputs), - (pcs_point_for_cubes, pcs_evals_for_cubes), - ) = verify_poseidon_gkr( + let (input_pcs_statements, cubes_pcs_statements) = verify_poseidon_gkr( &mut verifier_state, log_n_poseidons, &output_claim_point, @@ -173,20 +156,6 @@ fn test_prove_poseidons() { // PCS verification - let input_pcs_statements = verify_inner_evals_on_commited_columns( - &mut verifier_state, - &pcs_point_for_inputs, - &pcs_evals_for_inputs, - &selectors, - ); - - let cubes_pcs_statements = verify_inner_evals_on_commited_columns( - &mut verifier_state, - &pcs_point_for_cubes, - &pcs_evals_for_cubes, - &selectors, - ); - let global_statements = packed_pcs_global_statements_for_verifier( &committed_col_dims, log_smallest_decomposition_chunk, @@ -206,7 +175,7 @@ fn test_prove_poseidons() { } let verifier_duration = verifier_time.elapsed(); - let mut data_to_hash = input_layers.clone(); + let mut data_to_hash = input.clone(); let plaintext_time = Instant::now(); transposed_par_iter_mut(&mut data_to_hash).for_each(|row| { if WIDTH == 16 { diff --git a/crates/poseidon_circuit/src/verify.rs b/crates/poseidon_circuit/src/verify.rs index e74f2a3e..c8c27dec 100644 --- a/crates/poseidon_circuit/src/verify.rs +++ b/crates/poseidon_circuit/src/verify.rs @@ -2,7 +2,7 @@ use multilinear_toolkit::prelude::*; use p3_koala_bear::{KoalaBearInternalLayerParameters, KoalaBearParameters}; use p3_monty_31::InternalLayerBaseParameters; -use crate::{gkr_layers::PoseidonGKRLayers, EF, F}; +use crate::{EF, F, gkr_layers::PoseidonGKRLayers}; pub fn verify_poseidon_gkr( verifier_state: &mut FSVerifier>, @@ -10,7 +10,7 @@ pub fn verify_poseidon_gkr( output_claim_point: &[EF], layers: &PoseidonGKRLayers, univariate_skips: usize, -) -> ((Vec, Vec), (Vec, Vec)) +) -> (Vec>>, Vec>>) where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { @@ -78,10 +78,22 @@ where let pcs_point_for_inputs = claim_point.clone(); let pcs_evals_for_inputs = output_claims.to_vec(); - ( - (pcs_point_for_inputs, pcs_evals_for_inputs), - (pcs_point_for_cubes, pcs_evals_for_cubes), - ) + let selectors = univariate_selectors::(univariate_skips); + let input_pcs_statements = verify_inner_evals_on_commited_columns( + verifier_state, + &pcs_point_for_inputs, + &pcs_evals_for_inputs, + &selectors, + ); + + let cubes_pcs_statements = verify_inner_evals_on_commited_columns( + verifier_state, + &pcs_point_for_cubes, + &pcs_evals_for_cubes, + &selectors, + ); + + (input_pcs_statements, cubes_pcs_statements) } fn verify_gkr_round>( @@ -122,7 +134,7 @@ fn verify_gkr_round>( (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) } -pub(crate) fn verify_inner_evals_on_commited_columns( +fn verify_inner_evals_on_commited_columns( verifier_state: &mut FSVerifier>, point: &[EF], claimed_evals: &[EF], @@ -149,11 +161,7 @@ pub(crate) fn verify_inner_evals_on_commited_columns( ); pcs_statements.push(vec![Evaluation { point: MultilinearPoint( - [ - pcs_batching_scalars_inputs.clone(), - point[1..].to_vec(), - ] - .concat(), + [pcs_batching_scalars_inputs.clone(), point[1..].to_vec()].concat(), ), value: col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone())), }]); diff --git a/crates/poseidon_circuit/src/witness_gen.rs b/crates/poseidon_circuit/src/witness_gen.rs index ef7e2879..2d47c06f 100644 --- a/crates/poseidon_circuit/src/witness_gen.rs +++ b/crates/poseidon_circuit/src/witness_gen.rs @@ -26,7 +26,7 @@ pub struct PoseidonWitness } pub fn generate_poseidon_witness( - input_layer: [Vec; WIDTH], + input: [Vec; WIDTH], layers: &PoseidonGKRLayers, ) -> PoseidonWitness where @@ -34,7 +34,7 @@ where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { let mut remaining_initial_full_layers = vec![apply_full_round::<_, _, true>( - &input_layer, + &input, &layers.initial_full_round, )]; for round in &layers.initial_full_rounds_remaining { @@ -67,7 +67,7 @@ where let output_layer = final_full_layer_inputs.pop().unwrap(); PoseidonWitness { - input_layer, + input_layer: input, remaining_initial_full_round_inputs: remaining_initial_full_layers, batch_partial_round_input: batch_partial_round_layer, committed_cubes, From 3077f3376a6cf73e46f7a27408e671e36a139f38 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Fri, 24 Oct 2025 23:03:58 +0400 Subject: [PATCH 28/42] w --- crates/poseidon_circuit/src/prove.rs | 31 ++++++++++++-------- crates/poseidon_circuit/src/tests.rs | 9 +++--- crates/poseidon_circuit/src/verify.rs | 42 +++++++++++++++++---------- 3 files changed, 50 insertions(+), 32 deletions(-) diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index a066c45f..59a53168 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -14,6 +14,7 @@ pub fn prove_poseidon_gkr( univariate_skips: usize, layers: &PoseidonGKRLayers, ) -> ( + [EF; WIDTH], Vec>>, Vec>>, ) @@ -22,7 +23,7 @@ where { let selectors = univariate_selectors::(univariate_skips); - let mut output_claims = info_span!("computing output claims").in_scope(|| { + let output_claims = info_span!("computing output claims").in_scope(|| { batch_evaluate_univariate_multilinear( &witness .output_layer @@ -36,18 +37,20 @@ where prover_state.add_extension_scalars(&output_claims); + let mut claims = output_claims.to_vec(); + for (input_layers, full_round) in witness .final_full_round_inputs .iter() .zip(&layers.final_full_rounds) .rev() { - (claim_point, output_claims) = prove_gkr_round( + (claim_point, claims) = prove_gkr_round( prover_state, full_round, input_layers, &claim_point, - &output_claims, + &claims, univariate_skips, ); } @@ -58,29 +61,29 @@ where .zip(&layers.partial_rounds_remaining) .rev() { - (claim_point, output_claims) = prove_gkr_round( + (claim_point, claims) = prove_gkr_round( prover_state, partial_round, input_layers, &claim_point, - &output_claims, + &claims, univariate_skips, ); } - (claim_point, output_claims) = prove_batch_internal_round( + (claim_point, claims) = prove_batch_internal_round( prover_state, &witness.batch_partial_round_input, &witness.committed_cubes, &layers.batch_partial_rounds, &claim_point, - &output_claims, + &claims, &selectors, ); let pcs_point_for_cubes = claim_point.clone(); - output_claims = output_claims[..WIDTH].to_vec(); + claims = claims[..WIDTH].to_vec(); for (input_layers, full_round) in witness .remaining_initial_full_round_inputs @@ -88,12 +91,12 @@ where .zip(&layers.initial_full_rounds_remaining) .rev() { - (claim_point, output_claims) = prove_gkr_round( + (claim_point, claims) = prove_gkr_round( prover_state, full_round, input_layers, &claim_point, - &output_claims, + &claims, univariate_skips, ); } @@ -102,7 +105,7 @@ where &layers.initial_full_round, &witness.input_layer, &claim_point, - &output_claims, + &claims, univariate_skips, ); let pcs_point_for_inputs = claim_point.clone(); @@ -120,7 +123,11 @@ where &witness.committed_cubes, ); - (input_pcs_statements, cubes_pcs_statements) + ( + output_claims.try_into().unwrap(), + input_pcs_statements, + cubes_pcs_statements, + ) } #[instrument(skip_all)] diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index aaaf7787..12becd0a 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -24,7 +24,8 @@ use crate::{ type F = KoalaBear; type EF = QuinticExtensionFieldKB; -const WIDTH: usize = 16; +const WIDTH: usize = 16; + const UNIVARIATE_SKIPS: usize = 3; const N_COMMITED_CUBES: usize = 16; @@ -38,7 +39,7 @@ fn test_prove_poseidons() { let whir_config_builder = WhirConfigBuilder { folding_factor: FoldingFactor::new(7, 4), soundness_type: SecurityAssumption::CapacityBound, - pow_bits: WIDTH, + pow_bits: 16, max_num_variables_to_send_coeffs: 6, rs_domain_initial_reduction_factor: 5, security_level: 128, @@ -98,7 +99,7 @@ fn test_prove_poseidons() { let claim_point = prover_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); - let (input_pcs_statements, cubes_pcs_statements) = prove_poseidon_gkr( + let (_output_values, input_pcs_statements, cubes_pcs_statements) = prove_poseidon_gkr( &mut prover_state, &witness, claim_point, @@ -146,7 +147,7 @@ fn test_prove_poseidons() { let output_claim_point = verifier_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); - let (input_pcs_statements, cubes_pcs_statements) = verify_poseidon_gkr( + let (_output_values, input_pcs_statements, cubes_pcs_statements) = verify_poseidon_gkr( &mut verifier_state, log_n_poseidons, &output_claim_point, diff --git a/crates/poseidon_circuit/src/verify.rs b/crates/poseidon_circuit/src/verify.rs index c8c27dec..682b55d3 100644 --- a/crates/poseidon_circuit/src/verify.rs +++ b/crates/poseidon_circuit/src/verify.rs @@ -10,31 +10,37 @@ pub fn verify_poseidon_gkr( output_claim_point: &[EF], layers: &PoseidonGKRLayers, univariate_skips: usize, -) -> (Vec>>, Vec>>) +) -> ( + [EF; WIDTH], + Vec>>, + Vec>>, +) where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { - let mut output_claims = verifier_state.next_extension_scalars_vec(WIDTH).unwrap(); + let output_claims = verifier_state.next_extension_scalars_vec(WIDTH).unwrap(); + + let mut claims = output_claims.clone(); let mut claim_point = output_claim_point.to_vec(); for full_round in layers.final_full_rounds.iter().rev() { - (claim_point, output_claims) = verify_gkr_round( + (claim_point, claims) = verify_gkr_round( verifier_state, full_round, log_n_poseidons, &claim_point, - &output_claims, + &claims, univariate_skips, ); } for partial_round in layers.partial_rounds_remaining.iter().rev() { - (claim_point, output_claims) = verify_gkr_round( + (claim_point, claims) = verify_gkr_round( verifier_state, partial_round, log_n_poseidons, &claim_point, - &output_claims, + &claims, univariate_skips, ); } @@ -42,41 +48,41 @@ where .next_extension_scalars_vec(N_COMMITED_CUBES) .unwrap(); - (claim_point, output_claims) = verify_gkr_round( + (claim_point, claims) = verify_gkr_round( verifier_state, &layers.batch_partial_rounds, log_n_poseidons, &claim_point, - &[output_claims, claimed_cubes_evals.clone()].concat(), + &[claims, claimed_cubes_evals.clone()].concat(), univariate_skips, ); let pcs_point_for_cubes = claim_point.clone(); - let pcs_evals_for_cubes = output_claims[WIDTH..].to_vec(); + let pcs_evals_for_cubes = claims[WIDTH..].to_vec(); - output_claims = output_claims[..WIDTH].to_vec(); + claims = claims[..WIDTH].to_vec(); for full_round in layers.initial_full_rounds_remaining.iter().rev() { - (claim_point, output_claims) = verify_gkr_round( + (claim_point, claims) = verify_gkr_round( verifier_state, full_round, log_n_poseidons, &claim_point, - &output_claims, + &claims, univariate_skips, ); } - (claim_point, output_claims) = verify_gkr_round( + (claim_point, claims) = verify_gkr_round( verifier_state, &layers.initial_full_round, log_n_poseidons, &claim_point, - &output_claims, + &claims, univariate_skips, ); let pcs_point_for_inputs = claim_point.clone(); - let pcs_evals_for_inputs = output_claims.to_vec(); + let pcs_evals_for_inputs = claims.to_vec(); let selectors = univariate_selectors::(univariate_skips); let input_pcs_statements = verify_inner_evals_on_commited_columns( @@ -93,7 +99,11 @@ where &selectors, ); - (input_pcs_statements, cubes_pcs_statements) + ( + output_claims.try_into().unwrap(), + input_pcs_statements, + cubes_pcs_statements, + ) } fn verify_gkr_round>( From 8cc7409c22112acc7fd36ac23e0cce970cc58ca2 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sat, 25 Oct 2025 12:56:59 +0400 Subject: [PATCH 29/42] compress --- Cargo.lock | 45 +++++----- crates/poseidon_circuit/Cargo.toml | 6 +- .../src/gkr_layers/full_round.rs | 82 ++++++++++++++++--- crates/poseidon_circuit/src/gkr_layers/mod.rs | 59 +++++++------ crates/poseidon_circuit/src/lib.rs | 1 + crates/poseidon_circuit/src/prove.rs | 23 +++++- crates/poseidon_circuit/src/tests.rs | 43 ++++++++-- crates/poseidon_circuit/src/verify.rs | 42 ++++++++-- crates/poseidon_circuit/src/witness_gen.rs | 46 ++++++++++- 9 files changed, 267 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d47d7c80..6839bee3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,7 +57,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backend" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#abff289607aa2ff52f3b989468a25cc952127030" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#61c0491b6fe52a55ba93d643aa92c9f0b2cf52ae" dependencies = [ "fiat-shamir", "itertools 0.14.0", @@ -153,7 +153,7 @@ dependencies = [ [[package]] name = "constraints-folder" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#abff289607aa2ff52f3b989468a25cc952127030" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#61c0491b6fe52a55ba93d643aa92c9f0b2cf52ae" dependencies = [ "fiat-shamir", "p3-air", @@ -522,7 +522,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "multilinear-toolkit" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#abff289607aa2ff52f3b989468a25cc952127030" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#61c0491b6fe52a55ba93d643aa92c9f0b2cf52ae" dependencies = [ "backend", "constraints-folder", @@ -585,7 +585,7 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "p3-air" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "p3-field", "p3-matrix", @@ -594,7 +594,7 @@ dependencies = [ [[package]] name = "p3-baby-bear" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "p3-field", "p3-mds", @@ -607,7 +607,7 @@ dependencies = [ [[package]] name = "p3-challenger" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "p3-field", "p3-maybe-rayon", @@ -619,7 +619,7 @@ dependencies = [ [[package]] name = "p3-commit" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "itertools 0.14.0", "p3-challenger", @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "p3-dft" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "itertools 0.14.0", "p3-field", @@ -646,7 +646,7 @@ dependencies = [ [[package]] name = "p3-field" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "itertools 0.14.0", "num-bigint", @@ -661,7 +661,7 @@ dependencies = [ [[package]] name = "p3-interpolation" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "p3-field", "p3-matrix", @@ -672,7 +672,7 @@ dependencies = [ [[package]] name = "p3-koala-bear" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "itertools 0.14.0", "num-bigint", @@ -688,7 +688,7 @@ dependencies = [ [[package]] name = "p3-matrix" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "itertools 0.14.0", "p3-field", @@ -703,7 +703,7 @@ dependencies = [ [[package]] name = "p3-maybe-rayon" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "rayon", ] @@ -711,7 +711,7 @@ dependencies = [ [[package]] name = "p3-mds" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "p3-dft", "p3-field", @@ -723,7 +723,7 @@ dependencies = [ [[package]] name = "p3-merkle-tree" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "itertools 0.14.0", "p3-commit", @@ -740,7 +740,7 @@ dependencies = [ [[package]] name = "p3-monty-31" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "itertools 0.14.0", "num-bigint", @@ -762,7 +762,7 @@ dependencies = [ [[package]] name = "p3-poseidon2" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "p3-field", "p3-mds", @@ -774,7 +774,7 @@ dependencies = [ [[package]] name = "p3-poseidon2-air" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "p3-air", "p3-field", @@ -788,7 +788,7 @@ dependencies = [ [[package]] name = "p3-symmetric" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "itertools 0.14.0", "p3-field", @@ -798,7 +798,7 @@ dependencies = [ [[package]] name = "p3-uni-stark" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "itertools 0.14.0", "p3-air", @@ -816,7 +816,7 @@ dependencies = [ [[package]] name = "p3-util" version = "0.3.0" -source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1b3305de665b9d13a9cedfb88ab8dca94ebad116" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ "rayon", "serde", @@ -902,7 +902,6 @@ dependencies = [ "p3-matrix", "p3-monty-31", "p3-poseidon2", - "p3-util", "packed_pcs", "rand", "tracing", @@ -1143,7 +1142,7 @@ checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" [[package]] name = "sumcheck" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#abff289607aa2ff52f3b989468a25cc952127030" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#61c0491b6fe52a55ba93d643aa92c9f0b2cf52ae" dependencies = [ "backend", "constraints-folder", diff --git a/crates/poseidon_circuit/Cargo.toml b/crates/poseidon_circuit/Cargo.toml index 4e211f77..2d7eb9ad 100644 --- a/crates/poseidon_circuit/Cargo.toml +++ b/crates/poseidon_circuit/Cargo.toml @@ -10,14 +10,14 @@ workspace = true p3-field.workspace = true tracing.workspace = true utils.workspace = true -p3-util.workspace = true +# p3-util.workspace = true multilinear-toolkit.workspace = true p3-koala-bear.workspace = true p3-poseidon2.workspace = true -packed_pcs.workspace = true p3-monty-31.workspace = true [dev-dependencies] p3-matrix.workspace = true rand.workspace = true -whir-p3.workspace = true \ No newline at end of file +whir-p3.workspace = true +packed_pcs.workspace = true diff --git a/crates/poseidon_circuit/src/gkr_layers/full_round.rs b/crates/poseidon_circuit/src/gkr_layers/full_round.rs index f48e1a21..fd1214b3 100644 --- a/crates/poseidon_circuit/src/gkr_layers/full_round.rs +++ b/crates/poseidon_circuit/src/gkr_layers/full_round.rs @@ -13,6 +13,7 @@ use crate::{EF, F}; #[derive(Debug)] pub struct FullRoundComputation { pub constants: [F; WIDTH], + pub compressed_output: Option, } impl, const WIDTH: usize, const FIRST: bool> SumcheckComputation @@ -22,11 +23,19 @@ where EF: ExtensionField, { fn degree(&self) -> usize { - 3 + 3 + self.compressed_output.is_some() as usize } fn eval(&self, point: &[NF], alpha_powers: &[EF]) -> EF { - debug_assert_eq!(point.len(), WIDTH); + debug_assert_eq!( + point.len(), + WIDTH + + if self.compressed_output.is_some() { + 1 + } else { + 0 + } + ); let mut buff: [NF; WIDTH] = array::from_fn(|j| point[j]); if FIRST { GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); @@ -36,23 +45,42 @@ where }); GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); let mut res = EF::ZERO; - for i in 0..WIDTH { - res += alpha_powers[i] * buff[i]; + if let Some(compression_output_width) = self.compressed_output { + let compressed = point[WIDTH]; + for i in 0..compression_output_width { + res += alpha_powers[i] * buff[i]; + } + for i in compression_output_width..WIDTH { + res += alpha_powers[i] * buff[i] * (EF::ONE - compressed); + } + } else { + for i in 0..WIDTH { + res += alpha_powers[i] * buff[i]; + } } res } } -impl SumcheckComputationPacked for FullRoundComputation +impl SumcheckComputationPacked + for FullRoundComputation where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { fn degree(&self) -> usize { - 3 + 3 + self.compressed_output.is_some() as usize } fn eval_packed_base(&self, point: &[PFPacking], alpha_powers: &[EF]) -> EFPacking { - debug_assert_eq!(point.len(), WIDTH); + debug_assert_eq!( + point.len(), + WIDTH + + if self.compressed_output.is_some() { + 1 + } else { + 0 + } + ); let mut buff: [PFPacking; WIDTH] = array::from_fn(|j| point[j]); if FIRST { GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); @@ -62,14 +90,34 @@ where }); GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; - for j in 0..WIDTH { - res += EFPacking::::from(alpha_powers[j]) * buff[j]; + if let Some(compression_output_width) = self.compressed_output { + let compressed = point[WIDTH]; + for i in 0..compression_output_width { + res += EFPacking::::from(alpha_powers[i]) * buff[i]; + } + for i in compression_output_width..WIDTH { + res += EFPacking::::from(alpha_powers[i]) + * buff[i] + * (PFPacking::::ONE - compressed); + } + } else { + for j in 0..WIDTH { + res += EFPacking::::from(alpha_powers[j]) * buff[j]; + } } res } fn eval_packed_extension(&self, point: &[EFPacking], alpha_powers: &[EF]) -> EFPacking { - debug_assert_eq!(point.len(), WIDTH); + debug_assert_eq!( + point.len(), + WIDTH + + if self.compressed_output.is_some() { + 1 + } else { + 0 + } + ); let mut buff: [EFPacking; WIDTH] = array::from_fn(|j| point[j]); if FIRST { GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); @@ -79,8 +127,18 @@ where }); GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; - for j in 0..WIDTH { - res += buff[j] * alpha_powers[j]; + if let Some(compression_output_width) = self.compressed_output { + let compressed = point[WIDTH]; + for i in 0..compression_output_width { + res += buff[i] * alpha_powers[i]; + } + for i in compression_output_width..WIDTH { + res += buff[i] * (EFPacking::::ONE - compressed) * alpha_powers[i]; + } + } else { + for j in 0..WIDTH { + res += buff[j] * alpha_powers[j]; + } } res } diff --git a/crates/poseidon_circuit/src/gkr_layers/mod.rs b/crates/poseidon_circuit/src/gkr_layers/mod.rs index 4b81f806..a3e453b6 100644 --- a/crates/poseidon_circuit/src/gkr_layers/mod.rs +++ b/crates/poseidon_circuit/src/gkr_layers/mod.rs @@ -24,30 +24,26 @@ pub struct PoseidonGKRLayers } impl PoseidonGKRLayers { - pub fn build() -> Self { + pub fn build(compressed_output: Option) -> Self { match WIDTH { - 16 => { - unsafe { - Self::build_generic( - &*(&KOALABEAR_RC16_EXTERNAL_INITIAL as *const [[F; 16]] - as *const [[F; WIDTH]]), - &KOALABEAR_RC16_INTERNAL, - &*(&KOALABEAR_RC16_EXTERNAL_FINAL as *const [[F; 16]] - as *const [[F; WIDTH]]), - ) - } - } - 24 => { - unsafe { - Self::build_generic( - &*(&KOALABEAR_RC24_EXTERNAL_INITIAL as *const [[F; 24]] - as *const [[F; WIDTH]]), - &KOALABEAR_RC24_INTERNAL, - &*(&KOALABEAR_RC24_EXTERNAL_FINAL as *const [[F; 24]] - as *const [[F; WIDTH]]), - ) - } - } + 16 => unsafe { + Self::build_generic( + &*(&KOALABEAR_RC16_EXTERNAL_INITIAL as *const [[F; 16]] + as *const [[F; WIDTH]]), + &KOALABEAR_RC16_INTERNAL, + &*(&KOALABEAR_RC16_EXTERNAL_FINAL as *const [[F; 16]] as *const [[F; WIDTH]]), + compressed_output, + ) + }, + 24 => unsafe { + Self::build_generic( + &*(&KOALABEAR_RC24_EXTERNAL_INITIAL as *const [[F; 24]] + as *const [[F; WIDTH]]), + &KOALABEAR_RC24_INTERNAL, + &*(&KOALABEAR_RC24_EXTERNAL_FINAL as *const [[F; 24]] as *const [[F; WIDTH]]), + compressed_output, + ) + }, _ => panic!("Only Poseidon 16 and 24 are supported currently"), } } @@ -56,13 +52,18 @@ impl PoseidonGKRLayers, ) -> Self { let initial_full_round = FullRoundComputation { constants: initial_constants[0], + compressed_output: None, }; let initial_full_rounds_remaining = initial_constants[1..] .iter() - .map(|&constants| FullRoundComputation { constants }) + .map(|&constants| FullRoundComputation { + constants, + compressed_output: None, + }) .collect::>(); let batch_partial_rounds = BatchPartialRounds { constants: internal_constants[..N_COMMITED_CUBES].try_into().unwrap(), @@ -74,7 +75,15 @@ impl PoseidonGKRLayers>(); let final_full_rounds = final_constants .iter() - .map(|&constants| FullRoundComputation { constants }) + .enumerate() + .map(|(i, &constants)| FullRoundComputation { + constants, + compressed_output: if i == final_constants.len() - 1 { + compressed_output + } else { + None + }, + }) .collect::>(); Self { initial_full_round, diff --git a/crates/poseidon_circuit/src/lib.rs b/crates/poseidon_circuit/src/lib.rs index 25134829..ac6556a6 100644 --- a/crates/poseidon_circuit/src/lib.rs +++ b/crates/poseidon_circuit/src/lib.rs @@ -18,3 +18,4 @@ pub(crate) mod gkr_layers; pub(crate) type F = KoalaBear; pub(crate) type EF = QuinticExtensionFieldKB; + diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index 59a53168..00207a61 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -35,24 +35,39 @@ where ) }); + if let Some((n_compressions, _)) = &witness.compression { + prover_state.add_base_scalars(&[F::from_usize(*n_compressions)]); + } + prover_state.add_extension_scalars(&output_claims); let mut claims = output_claims.to_vec(); - for (input_layers, full_round) in witness + for (i, (input_layers, full_round)) in witness .final_full_round_inputs .iter() .zip(&layers.final_full_rounds) .rev() + .enumerate() { + let mut input_layers = input_layers.iter().map(Vec::as_slice).collect::>(); + if i == 0 + && let Some((_, compression_indicator)) = &witness.compression + { + input_layers.push(compression_indicator); + } (claim_point, claims) = prove_gkr_round( prover_state, full_round, - input_layers, + &input_layers, &claim_point, &claims, univariate_skips, ); + + if i == 0 && witness.compression.is_some() { + let _ = claims.pop().unwrap(); // the claim on the compression indicator columns can be evaluated by the verifier directly + } } for (input_layers, partial_round) in witness @@ -139,7 +154,7 @@ fn prove_gkr_round< >( prover_state: &mut FSProver>, computation: &SC, - input_layers: &[impl AsRef>>], + input_layers: &[impl AsRef<[PFPacking]>], claim_point: &[EF], output_claims: &[EF], univariate_skips: usize, @@ -153,7 +168,7 @@ fn prove_gkr_round< let (sumcheck_point, sumcheck_inner_evals, sumcheck_final_sum) = sumcheck_prove( univariate_skips, - MleGroupRef::BasePacked(input_layers.iter().map(|l| l.as_ref().as_slice()).collect()), + MleGroupRef::BasePacked(input_layers.iter().map(|l| l.as_ref()).collect()), computation, computation, &batching_scalars_powers, diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index 12becd0a..333609b7 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -24,10 +24,11 @@ use crate::{ type F = KoalaBear; type EF = QuinticExtensionFieldKB; -const WIDTH: usize = 16; +const WIDTH: usize = 16; const UNIVARIATE_SKIPS: usize = 3; const N_COMMITED_CUBES: usize = 16; +const COMPRESSION_OUTPUT_WIDTH: usize = 8; #[test] fn test_prove_poseidons() { @@ -50,6 +51,8 @@ fn test_prove_poseidons() { let mut rng = StdRng::seed_from_u64(0); let n_poseidons = 1 << log_n_poseidons; + let n_compressions = n_poseidons / 10; + let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) .collect::>(); @@ -58,7 +61,8 @@ fn test_prove_poseidons() { let input_packed: [_; WIDTH] = array::from_fn(|i| PFPacking::::pack_slice(&input[i]).to_vec()); - let layers = PoseidonGKRLayers::::build(); + let layers = + PoseidonGKRLayers::::build(Some(COMPRESSION_OUTPUT_WIDTH)); let default_cubes = default_cube_layers::(&layers); let input_col_dims = vec![ColDims::padded(n_poseidons, F::ZERO); WIDTH]; @@ -74,10 +78,19 @@ fn test_prove_poseidons() { // ---------------------------------------------------- PROVER ---------------------------------------------------- let prover_time = Instant::now(); + let compression_indicator = [ + vec![F::ZERO; n_poseidons - n_compressions], + vec![F::ONE; n_compressions], + ] + .concat(); let witness = generate_poseidon_witness::, WIDTH, N_COMMITED_CUBES>( input_packed, &layers, + Some(( + n_compressions, + PFPacking::::pack_slice(&compression_indicator).to_vec(), // TODO avoid cloning + )), ); let mut prover_state = build_prover_state::(); @@ -153,6 +166,7 @@ fn test_prove_poseidons() { &output_claim_point, &layers, UNIVARIATE_SKIPS, + true, ); // PCS verification @@ -198,9 +212,28 @@ fn test_prove_poseidons() { let plaintext_duration = plaintext_time.elapsed(); // sanity check: ensure the plaintext poseidons matches the last GKR layer: - output_layer.iter().enumerate().for_each(|(i, layer)| { - assert_eq!(PFPacking::::unpack_slice(&layer), data_to_hash[i]); - }); + output_layer + .iter() + .enumerate() + .take(COMPRESSION_OUTPUT_WIDTH) + .for_each(|(i, layer)| { + assert_eq!(PFPacking::::unpack_slice(&layer), data_to_hash[i]); + }); + output_layer + .iter() + .enumerate() + .skip(COMPRESSION_OUTPUT_WIDTH) + .for_each(|(i, layer)| { + assert_eq!( + &PFPacking::::unpack_slice(&layer)[..n_poseidons - n_compressions], + &data_to_hash[i][..n_poseidons - n_compressions] + ); + assert!( + PFPacking::::unpack_slice(&layer)[n_poseidons - n_compressions..] + .iter() + .all(|&x| x.is_zero()) + ); + }); println!("2^{} Poseidon2", log_n_poseidons); println!( diff --git a/crates/poseidon_circuit/src/verify.rs b/crates/poseidon_circuit/src/verify.rs index 682b55d3..efa7bdc6 100644 --- a/crates/poseidon_circuit/src/verify.rs +++ b/crates/poseidon_circuit/src/verify.rs @@ -1,6 +1,7 @@ use multilinear_toolkit::prelude::*; use p3_koala_bear::{KoalaBearInternalLayerParameters, KoalaBearParameters}; use p3_monty_31::InternalLayerBaseParameters; +use utils::ToUsize; use crate::{EF, F, gkr_layers::PoseidonGKRLayers}; @@ -10,6 +11,7 @@ pub fn verify_poseidon_gkr( output_claim_point: &[EF], layers: &PoseidonGKRLayers, univariate_skips: usize, + has_compressions: bool, ) -> ( [EF; WIDTH], Vec>>, @@ -18,12 +20,24 @@ pub fn verify_poseidon_gkr( where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { + let selectors = univariate_selectors::(univariate_skips); + + let n_compressions = + has_compressions.then(|| verifier_state.next_base_scalars_const::<1>().unwrap()[0]); + let output_claims = verifier_state.next_extension_scalars_vec(WIDTH).unwrap(); let mut claims = output_claims.clone(); let mut claim_point = output_claim_point.to_vec(); - for full_round in layers.final_full_rounds.iter().rev() { + for (i, full_round) in layers.final_full_rounds.iter().rev().enumerate() { + let n_inputs = if i == 0 + && let Some(_) = n_compressions + { + WIDTH + 1 + } else { + WIDTH + }; (claim_point, claims) = verify_gkr_round( verifier_state, full_round, @@ -31,7 +45,23 @@ where &claim_point, &claims, univariate_skips, + n_inputs, ); + if i == 0 + && let Some(n_compressions) = n_compressions + { + let n_compressions = n_compressions.to_usize(); + assert!(n_compressions <= 1 << log_n_poseidons); + let compression_eval = claims.pop().unwrap(); + assert_eq!( + compression_eval, + skipped_mle_of_zeros_then_ones( + (1 << log_n_poseidons) - n_compressions, + &claim_point, + &selectors + ) + ); + } } for partial_round in layers.partial_rounds_remaining.iter().rev() { @@ -42,6 +72,7 @@ where &claim_point, &claims, univariate_skips, + WIDTH, ); } let claimed_cubes_evals = verifier_state @@ -55,6 +86,7 @@ where &claim_point, &[claims, claimed_cubes_evals.clone()].concat(), univariate_skips, + WIDTH + N_COMMITED_CUBES, ); let pcs_point_for_cubes = claim_point.clone(); @@ -70,6 +102,7 @@ where &claim_point, &claims, univariate_skips, + WIDTH, ); } (claim_point, claims) = verify_gkr_round( @@ -79,12 +112,12 @@ where &claim_point, &claims, univariate_skips, + WIDTH, ); let pcs_point_for_inputs = claim_point.clone(); let pcs_evals_for_inputs = claims.to_vec(); - let selectors = univariate_selectors::(univariate_skips); let input_pcs_statements = verify_inner_evals_on_commited_columns( verifier_state, &pcs_point_for_inputs, @@ -113,6 +146,7 @@ fn verify_gkr_round>( claim_point: &[EF], output_claims: &[EF], univariate_skips: usize, + n_inputs: usize, ) -> (Vec, Vec) { let batching_scalar = verifier_state.sample(); let batching_scalars_powers = batching_scalar.powers().collect_n(output_claims.len()); @@ -128,9 +162,7 @@ fn verify_gkr_round>( assert_eq!(retrieved_batched_claim, batched_claim); - let sumcheck_inner_evals = verifier_state - .next_extension_scalars_vec(output_claims.len()) - .unwrap(); + let sumcheck_inner_evals = verifier_state.next_extension_scalars_vec(n_inputs).unwrap(); assert_eq!( computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) * eq_poly_with_skip( diff --git a/crates/poseidon_circuit/src/witness_gen.rs b/crates/poseidon_circuit/src/witness_gen.rs index 2d47c06f..256de33d 100644 --- a/crates/poseidon_circuit/src/witness_gen.rs +++ b/crates/poseidon_circuit/src/witness_gen.rs @@ -23,11 +23,21 @@ pub struct PoseidonWitness pub remaining_partial_round_inputs: Vec<[Vec; WIDTH]>, // the input of each remaining partial round pub final_full_round_inputs: Vec<[Vec; WIDTH]>, // the input of each final full round pub output_layer: [Vec; WIDTH], // output of the permutation + pub compression: Option<(usize, Vec)>, // num compressions, compression indicator column +} + +impl + PoseidonWitness, WIDTH, N_COMMITED_CUBES> +{ + pub fn n_poseidons(&self) -> usize { + self.input_layer[0].len() * packing_width::() + } } pub fn generate_poseidon_witness( input: [Vec; WIDTH], layers: &PoseidonGKRLayers, + compression: Option<(usize, Vec)>, ) -> PoseidonWitness where A: Algebra + Copy + Send + Sync, @@ -36,11 +46,13 @@ where let mut remaining_initial_full_layers = vec![apply_full_round::<_, _, true>( &input, &layers.initial_full_round, + None, )]; for round in &layers.initial_full_rounds_remaining { remaining_initial_full_layers.push(apply_full_round::<_, _, false>( remaining_initial_full_layers.last().unwrap(), round, + None, )); } @@ -57,10 +69,15 @@ where } let mut final_full_layer_inputs = vec![remaining_partial_inputs.pop().unwrap()]; - for round in &layers.final_full_rounds { + for (i, round) in layers.final_full_rounds.iter().enumerate() { final_full_layer_inputs.push(apply_full_round::<_, _, false>( final_full_layer_inputs.last().unwrap(), round, + if i == layers.final_full_rounds.len() - 1 { + compression.as_ref().map(|(_, v)| v.as_slice()) + } else { + None + }, )); } @@ -74,6 +91,7 @@ where remaining_partial_round_inputs: remaining_partial_inputs, final_full_round_inputs: final_full_layer_inputs, output_layer, + compression, } } @@ -81,6 +99,7 @@ where fn apply_full_round( input_layers: &[Vec; WIDTH], full_round: &FullRoundComputation, + compression_indicator: Option<&[A]>, ) -> [Vec; WIDTH] where A: Algebra + Copy + Send + Sync, @@ -98,8 +117,18 @@ where *val = (*val + full_round.constants[j]).cube(); }); GenericPoseidon2LinearLayersKoalaBear::external_linear_layer(&mut buff); - for j in 0..WIDTH { - *output_row[j] = buff[j]; + if let Some(compression_output_width) = full_round.compressed_output { + let compressed = compression_indicator.unwrap()[row_index]; + for i in 0..compression_output_width { + *output_row[i] = buff[i]; + } + for i in compression_output_width..WIDTH { + *output_row[i] = buff[i] * (A::ONE - compressed); + } + } else { + for j in 0..WIDTH { + *output_row[j] = buff[j]; + } } }); output_layers @@ -172,6 +201,17 @@ where generate_poseidon_witness::( array::from_fn(|_| vec![A::ZERO]), layers, + if layers + .final_full_rounds + .last() + .unwrap() + .compressed_output + .is_some() + { + Some((0, vec![A::ZERO])) + } else { + None + }, ) .committed_cubes .iter() From 4e04c702963f992eefb8fd87caa81e407e7ae658 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sat, 25 Oct 2025 13:31:43 +0400 Subject: [PATCH 30/42] optional compress --- crates/poseidon_circuit/src/tests.rs | 80 ++++++++++++++++------------ 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index 333609b7..3dc61432 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -28,6 +28,7 @@ const WIDTH: usize = 16; const UNIVARIATE_SKIPS: usize = 3; const N_COMMITED_CUBES: usize = 16; +const COMPRESS: bool = false; const COMPRESSION_OUTPUT_WIDTH: usize = 8; #[test] @@ -51,7 +52,7 @@ fn test_prove_poseidons() { let mut rng = StdRng::seed_from_u64(0); let n_poseidons = 1 << log_n_poseidons; - let n_compressions = n_poseidons / 10; + let n_compressions = if COMPRESS { n_poseidons / 3 } else { 0 }; let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) @@ -61,8 +62,9 @@ fn test_prove_poseidons() { let input_packed: [_; WIDTH] = array::from_fn(|i| PFPacking::::pack_slice(&input[i]).to_vec()); - let layers = - PoseidonGKRLayers::::build(Some(COMPRESSION_OUTPUT_WIDTH)); + let layers = PoseidonGKRLayers::::build( + COMPRESS.then(|| COMPRESSION_OUTPUT_WIDTH), + ); let default_cubes = default_cube_layers::(&layers); let input_col_dims = vec![ColDims::padded(n_poseidons, F::ZERO); WIDTH]; @@ -78,19 +80,25 @@ fn test_prove_poseidons() { // ---------------------------------------------------- PROVER ---------------------------------------------------- let prover_time = Instant::now(); - let compression_indicator = [ - vec![F::ZERO; n_poseidons - n_compressions], - vec![F::ONE; n_compressions], - ] - .concat(); let witness = generate_poseidon_witness::, WIDTH, N_COMMITED_CUBES>( input_packed, &layers, - Some(( - n_compressions, - PFPacking::::pack_slice(&compression_indicator).to_vec(), // TODO avoid cloning - )), + if COMPRESS { + Some(( + n_compressions, + PFPacking::::pack_slice( + &[ + vec![F::ZERO; n_poseidons - n_compressions], + vec![F::ONE; n_compressions], + ] + .concat(), + ) + .to_vec(), + )) + } else { + None + }, ); let mut prover_state = build_prover_state::(); @@ -166,7 +174,7 @@ fn test_prove_poseidons() { &output_claim_point, &layers, UNIVARIATE_SKIPS, - true, + COMPRESS, ); // PCS verification @@ -212,28 +220,34 @@ fn test_prove_poseidons() { let plaintext_duration = plaintext_time.elapsed(); // sanity check: ensure the plaintext poseidons matches the last GKR layer: - output_layer - .iter() - .enumerate() - .take(COMPRESSION_OUTPUT_WIDTH) - .for_each(|(i, layer)| { + if COMPRESS { + output_layer + .iter() + .enumerate() + .take(COMPRESSION_OUTPUT_WIDTH) + .for_each(|(i, layer)| { + assert_eq!(PFPacking::::unpack_slice(&layer), data_to_hash[i]); + }); + output_layer + .iter() + .enumerate() + .skip(COMPRESSION_OUTPUT_WIDTH) + .for_each(|(i, layer)| { + assert_eq!( + &PFPacking::::unpack_slice(&layer)[..n_poseidons - n_compressions], + &data_to_hash[i][..n_poseidons - n_compressions] + ); + assert!( + PFPacking::::unpack_slice(&layer)[n_poseidons - n_compressions..] + .iter() + .all(|&x| x.is_zero()) + ); + }); + } else { + output_layer.iter().enumerate().for_each(|(i, layer)| { assert_eq!(PFPacking::::unpack_slice(&layer), data_to_hash[i]); }); - output_layer - .iter() - .enumerate() - .skip(COMPRESSION_OUTPUT_WIDTH) - .for_each(|(i, layer)| { - assert_eq!( - &PFPacking::::unpack_slice(&layer)[..n_poseidons - n_compressions], - &data_to_hash[i][..n_poseidons - n_compressions] - ); - assert!( - PFPacking::::unpack_slice(&layer)[n_poseidons - n_compressions..] - .iter() - .all(|&x| x.is_zero()) - ); - }); + } println!("2^{} Poseidon2", log_n_poseidons); println!( From 4816d155ecb71f52501db014efa78b69de696900 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sat, 25 Oct 2025 23:26:15 +0400 Subject: [PATCH 31/42] GKR integration in leanVM, wip --- Cargo.lock | 3 + crates/lean_prover/Cargo.toml | 1 + crates/lean_prover/src/common.rs | 77 ++++---- crates/lean_prover/src/prove_execution.rs | 180 +++++++++++------- crates/lean_prover/src/verify_execution.rs | 125 ++++++++---- .../lean_prover/witness_generation/Cargo.toml | 2 + .../witness_generation/src/execution_trace.rs | 22 +++ .../witness_generation/src/poseidon_tables.rs | 115 ++++++----- crates/lean_vm/src/witness/mod.rs | 6 + crates/lean_vm/src/witness/poseidon16.rs | 9 +- crates/lean_vm/src/witness/poseidon24.rs | 9 +- crates/poseidon_circuit/src/lib.rs | 3 +- crates/poseidon_circuit/src/prove.rs | 24 +-- crates/poseidon_circuit/src/tests.rs | 28 ++- crates/poseidon_circuit/src/verify.rs | 29 ++- crates/utils/src/poseidon2.rs | 132 ------------- 16 files changed, 386 insertions(+), 379 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6839bee3..abb58acd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,6 +436,7 @@ dependencies = [ "packed_pcs", "pest", "pest_derive", + "poseidon_circuit", "rand", "rayon", "tracing", @@ -1567,6 +1568,7 @@ dependencies = [ "p3-field", "p3-koala-bear", "p3-matrix", + "p3-monty-31", "p3-poseidon2", "p3-poseidon2-air", "p3-symmetric", @@ -1574,6 +1576,7 @@ dependencies = [ "packed_pcs", "pest", "pest_derive", + "poseidon_circuit", "rand", "rayon", "tracing", diff --git a/crates/lean_prover/Cargo.toml b/crates/lean_prover/Cargo.toml index 88554996..95ddc5e6 100644 --- a/crates/lean_prover/Cargo.toml +++ b/crates/lean_prover/Cargo.toml @@ -32,6 +32,7 @@ lean_compiler.workspace = true witness_generation.workspace = true vm_air.workspace = true multilinear-toolkit.workspace = true +poseidon_circuit.workspace = true [dev-dependencies] xmss.workspace = true \ No newline at end of file diff --git a/crates/lean_prover/src/common.rs b/crates/lean_prover/src/common.rs index 59ba303e..fcdf793f 100644 --- a/crates/lean_prover/src/common.rs +++ b/crates/lean_prover/src/common.rs @@ -1,12 +1,17 @@ use multilinear_toolkit::prelude::*; use p3_field::{Algebra, BasedVectorSpace}; use p3_field::{ExtensionField, PrimeCharacteristicRing}; +use p3_koala_bear::{KOALABEAR_RC16_INTERNAL, KOALABEAR_RC24_INTERNAL}; use p3_util::log2_ceil_usize; use packed_pcs::ColDims; +use poseidon_circuit::{PoseidonGKRLayers, default_cube_layers}; use crate::*; use lean_vm::*; +pub(crate) const N_COMMITED_CUBES_P16: usize = KOALABEAR_RC16_INTERNAL.len() - 1; +pub(crate) const N_COMMITED_CUBES_P24: usize = KOALABEAR_RC24_INTERNAL.len() - 1; + pub fn get_base_dims( n_cycles: usize, log_public_memory: usize, @@ -14,11 +19,12 @@ pub fn get_base_dims( bytecode_ending_pc: usize, poseidon_counts: (usize, usize), n_rows_table_dot_products: usize, + p16_gkr_layers: &PoseidonGKRLayers<16, N_COMMITED_CUBES_P16>, + p24_gkr_layers: &PoseidonGKRLayers<24, N_COMMITED_CUBES_P24>, ) -> Vec> { let (n_poseidons_16, n_poseidons_24) = poseidon_counts; - let default_p16_row = - default_poseidon16_air_row(POSEIDON_16_DEFAULT_COMPRESSION, POSEIDON_16_NULL_HASH_PTR); - let default_p24_row = default_poseidon24_air_row(); + let p16_default_cubes = default_cube_layers::(&p16_gkr_layers); + let p24_default_cubes = default_cube_layers::(&p24_gkr_layers); [ vec![ @@ -30,16 +36,19 @@ pub fn get_base_dims( ColDims::padded(n_cycles, F::ZERO), // mem_addr_c ColDims::padded(n_poseidons_16, F::from_usize(ZERO_VEC_PTR)), // poseidon16 index a ColDims::padded(n_poseidons_16, F::from_usize(ZERO_VEC_PTR)), // poseidon16 index b + ColDims::padded(n_poseidons_16, F::from_usize(POSEIDON_16_NULL_HASH_PTR)), // poseidon16 index res ColDims::padded(n_poseidons_24, F::from_usize(ZERO_VEC_PTR)), // poseidon24 index a ColDims::padded(n_poseidons_24, F::from_usize(ZERO_VEC_PTR)), // poseidon24 index b ColDims::padded(n_poseidons_24, F::from_usize(POSEIDON_24_NULL_HASH_PTR)), // poseidon24 index res ], - (0..POSEIDON16_AIR_N_COLS - 16 * 2) - .map(|i| ColDims::padded(n_poseidons_16, default_p16_row[16 + i])) - .collect::>(), // rest of poseidon16 table - (0..POSEIDON24_AIR_N_COLS - 24 - 8) - .map(|i| ColDims::padded(n_poseidons_24, default_p24_row[24 + i])) - .collect::>(), // rest of poseidon24 table + p16_default_cubes + .iter() + .map(|&c| ColDims::padded(n_poseidons_16, c)) + .collect::>(), // commited cubes for poseidon16 + p24_default_cubes + .iter() + .map(|&c| ColDims::padded(n_poseidons_24, c)) + .collect::>(), // commited cubes for poseidon24 vec![ ColDims::padded(n_rows_table_dot_products, F::ONE), // dot product: (start) flag ColDims::padded(n_rows_table_dot_products, F::ONE), // dot product: length @@ -296,24 +305,22 @@ impl SumcheckComputationPacked for DotProductFootprint { } pub fn get_poseidon_lookup_statements( - (p16_air_width, p24_air_width): (usize, usize), (log_n_p16, log_n_p24): (usize, usize), - (p16_evals, p24_evals): (&[EF], &[EF]), - (p16_air_point, p24_air_point): (&MultilinearPoint, &MultilinearPoint), + (p16_input_point, p16_input_evals): &(MultilinearPoint, Vec), + (p16_output_point, p16_output_evals): &(MultilinearPoint, Vec), + (p24_input_point, p24_input_evals): &(MultilinearPoint, Vec), + (p24_output_point, p24_output_evals): &(MultilinearPoint, Vec), memory_folding_challenges: &MultilinearPoint, ) -> Vec> { - let p16_folded_eval_addr_a = (&p16_evals[..8]).evaluate(memory_folding_challenges); - let p16_folded_eval_addr_b = (&p16_evals[8..16]).evaluate(memory_folding_challenges); - let p16_folded_eval_addr_res_a = - (&p16_evals[p16_air_width - 16..p16_air_width - 8]).evaluate(memory_folding_challenges); - let p16_folded_eval_addr_res_b = - (&p16_evals[p16_air_width - 8..]).evaluate(memory_folding_challenges); + let p16_folded_eval_addr_a = (&p16_input_evals[..8]).evaluate(memory_folding_challenges); + let p16_folded_eval_addr_b = (&p16_input_evals[8..16]).evaluate(memory_folding_challenges); + let p16_folded_eval_addr_res_a = (&p16_output_evals[..8]).evaluate(memory_folding_challenges); + let p16_folded_eval_addr_res_b = (&p16_output_evals[8..16]).evaluate(memory_folding_challenges); - let p24_folded_eval_addr_a = (&p24_evals[..8]).evaluate(memory_folding_challenges); - let p24_folded_eval_addr_b = (&p24_evals[8..16]).evaluate(memory_folding_challenges); - let p24_folded_eval_addr_c = (&p24_evals[16..24]).evaluate(memory_folding_challenges); - let p24_folded_eval_addr_res = - (&p24_evals[p24_air_width - 8..]).evaluate(memory_folding_challenges); + let p24_folded_eval_addr_a = (&p24_input_evals[..8]).evaluate(memory_folding_challenges); + let p24_folded_eval_addr_b = (&p24_input_evals[8..16]).evaluate(memory_folding_challenges); + let p24_folded_eval_addr_c = (&p24_input_evals[16..24]).evaluate(memory_folding_challenges); + let p24_folded_eval_addr_res = p24_output_evals.evaluate(memory_folding_challenges); let padding_p16 = EF::zero_vec(log_n_p16.max(log_n_p24) - log_n_p16); let padding_p24 = EF::zero_vec(log_n_p16.max(log_n_p24) - log_n_p24); @@ -323,7 +330,7 @@ pub fn get_poseidon_lookup_statements( [ vec![EF::ZERO; 3], padding_p16.clone(), - p16_air_point.0.clone(), + p16_input_point.0.clone(), ] .concat(), p16_folded_eval_addr_a, @@ -332,7 +339,7 @@ pub fn get_poseidon_lookup_statements( [ vec![EF::ZERO, EF::ZERO, EF::ONE], padding_p16.clone(), - p16_air_point.0.clone(), + p16_input_point.0.clone(), ] .concat(), p16_folded_eval_addr_b, @@ -341,7 +348,7 @@ pub fn get_poseidon_lookup_statements( [ vec![EF::ZERO, EF::ONE, EF::ZERO], padding_p16.clone(), - p16_air_point.0.clone(), + p16_output_point.0.clone(), ] .concat(), p16_folded_eval_addr_res_a, @@ -350,7 +357,7 @@ pub fn get_poseidon_lookup_statements( [ vec![EF::ZERO, EF::ONE, EF::ONE], padding_p16.clone(), - p16_air_point.0.clone(), + p16_output_point.0.clone(), ] .concat(), p16_folded_eval_addr_res_b, @@ -359,7 +366,7 @@ pub fn get_poseidon_lookup_statements( [ vec![EF::ONE, EF::ZERO, EF::ZERO], padding_p24.clone(), - p24_air_point.0.clone(), + p24_input_point.0.clone(), ] .concat(), p24_folded_eval_addr_a, @@ -368,7 +375,7 @@ pub fn get_poseidon_lookup_statements( [ vec![EF::ONE, EF::ZERO, EF::ONE], padding_p24.clone(), - p24_air_point.0.clone(), + p24_input_point.0.clone(), ] .concat(), p24_folded_eval_addr_b, @@ -377,7 +384,7 @@ pub fn get_poseidon_lookup_statements( [ vec![EF::ONE, EF::ONE, EF::ZERO], padding_p24.clone(), - p24_air_point.0.clone(), + p24_input_point.0.clone(), ] .concat(), p24_folded_eval_addr_c, @@ -386,7 +393,7 @@ pub fn get_poseidon_lookup_statements( [ vec![EF::ONE, EF::ONE, EF::ONE], padding_p24.clone(), - p24_air_point.0.clone(), + p24_output_point.0.clone(), ] .concat(), p24_folded_eval_addr_res, @@ -415,10 +422,10 @@ pub fn add_poseidon_lookup_statements_on_indexes( log_n_p24: usize, index_lookup_point: &MultilinearPoint, inner_values: &[EF], - p16_index_statements: [&mut Vec>; 4], // a, b, res_1, res_2 - p24_index_statements: [&mut Vec>; 3], // a, b, res + p16_index_statements: [&mut Vec>; 3], // input_a, input_b, res_a + p24_index_statements: [&mut Vec>; 3], // input_a, input_b, res ) { - assert_eq!(inner_values.len(), 7); + assert_eq!(inner_values.len(), 6); let mut idx_point_right_p16 = MultilinearPoint(index_lookup_point[3..].to_vec()); let mut idx_point_right_p24 = MultilinearPoint(index_lookup_point[3 + log_n_p16.abs_diff(log_n_p24)..].to_vec()); @@ -434,7 +441,7 @@ pub fn add_poseidon_lookup_statements_on_indexes( for (i, stmt) in p24_index_statements.into_iter().enumerate() { stmt.push(Evaluation::new( idx_point_right_p24.clone(), - inner_values[i + 4], + inner_values[i + 3], )); } } diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index c92f418d..7729654e 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -5,20 +5,17 @@ use lean_vm::*; use lookup::prove_gkr_product; use lookup::{compute_pushforward, prove_logup_star}; use multilinear_toolkit::prelude::*; -use p3_air::BaseAir; use p3_field::ExtensionField; use p3_field::Field; use p3_field::PrimeCharacteristicRing; use p3_util::{log2_ceil_usize, log2_strict_usize}; use packed_pcs::*; +use poseidon_circuit::{PoseidonGKRLayers, prove_poseidon_gkr}; use tracing::info_span; use utils::ToUsize; use utils::dot_product_with_base; use utils::field_slice_as_base; -use utils::{ - build_poseidon_16_air, build_poseidon_24_air, build_prover_state, - padd_with_zero_to_next_power_of_two, -}; +use utils::{build_prover_state, padd_with_zero_to_next_power_of_two}; use vm_air::*; use whir_p3::{ WhirConfig, WhirConfigBuilder, precompute_dft_twiddles, second_batched_whir_config_builder, @@ -37,8 +34,9 @@ pub fn prove_execution( full_trace, n_poseidons_16, n_poseidons_24, - poseidons_16, // padded with empty poseidons - poseidons_24, // padded with empty poseidons + poseidons_16, // padded with empty poseidons + poseidons_24, // padded with empty poseidons + n_compressions_16, // included the padding (that are compressions of zeros) dot_products, multilinear_evals: vm_multilinear_evals, public_memory_size, @@ -86,23 +84,14 @@ pub fn prove_execution( let _validity_proof_span = info_span!("Validity proof generation").entered(); - let p16_air = build_poseidon_16_air(); - let p24_air = build_poseidon_24_air(); - let p16_air_packed = build_poseidon_16_air_packed(); - let p24_air_packed = build_poseidon_24_air_packed(); - let p16_table = AirTable::::new(p16_air.clone(), p16_air_packed); - let p24_table = AirTable::::new(p24_air.clone(), p24_air_packed); + let p16_gkr_layers = PoseidonGKRLayers::<16, N_COMMITED_CUBES_P16>::build(Some(VECTOR_LEN)); + let p24_gkr_layers = PoseidonGKRLayers::<24, N_COMMITED_CUBES_P24>::build(None); - let dot_product_table = AirTable::::new(DotProductAir, DotProductAir); + let p16_witness = + generate_poseidon_witness_helper(&p16_gkr_layers, &poseidons_16, Some(n_compressions_16)); + let p24_witness = generate_poseidon_witness_helper(&p24_gkr_layers, &poseidons_24, None); - let p16_columns = build_poseidon_16_columns( - &poseidons_16[..n_poseidons_16], - poseidons_16.len() - n_poseidons_16, - ); - let p24_columns = build_poseidon_24_columns( - &poseidons_24[..n_poseidons_24], - poseidons_24.len() - n_poseidons_24, - ); + let dot_product_table = AirTable::::new(DotProductAir, DotProductAir); let (dot_product_columns, dot_product_padding_len) = build_dot_product_columns(&dot_products); @@ -121,6 +110,7 @@ pub fn prove_execution( &[ log_n_cycles, n_poseidons_16, + n_compressions_16, n_poseidons_24, dot_products.len(), n_rows_table_dot_products, @@ -154,11 +144,7 @@ pub fn prove_execution( ) .unwrap(); } - let p16_indexes_input = all_poseidon_16_indexes_input(&poseidons_16); - // 0..16: input, 16: compress, 17: res_index_1, 18: res_index_2 - let p16_compression_col = &p16_columns[16]; - let p16_index_out_1_col = &p16_columns[17]; - + let p16_indexes = all_poseidon_16_indexes(&poseidons_16); let p24_indexes = all_poseidon_24_indexes(&poseidons_24); let base_dims = get_base_dims( @@ -168,6 +154,8 @@ pub fn prove_execution( bytecode.ending_pc, (n_poseidons_16, n_poseidons_24), n_rows_table_dot_products, + &p16_gkr_layers, + &p24_gkr_layers, ); let dot_product_col_index_a = field_slice_as_base(&dot_product_columns[2]).unwrap(); @@ -183,18 +171,17 @@ pub fn prove_execution( full_trace[COL_INDEX_MEM_ADDRESS_B].as_slice(), full_trace[COL_INDEX_MEM_ADDRESS_C].as_slice(), ], - p16_indexes_input - .iter() - .map(Vec::as_slice) - .collect::>(), + p16_indexes.iter().map(Vec::as_slice).collect::>(), p24_indexes.iter().map(Vec::as_slice).collect::>(), - p16_columns[16..p16_air.width() - 16] + p16_witness + .committed_cubes .iter() - .map(Vec::as_slice) + .map(|s| FPacking::::unpack_slice(s)) .collect::>(), - p24_columns[24..p24_air.width() - 8] + p24_witness + .committed_cubes .iter() - .map(Vec::as_slice) + .map(|s| FPacking::::unpack_slice(s)) .collect::>(), vec![ dot_product_flags.as_slice(), @@ -362,18 +349,16 @@ pub fn prove_execution( ); let p16_grand_product_evals_on_indexes_a = - p16_indexes_input[0].evaluate(&grand_product_p16_statement.point); + p16_indexes[0].evaluate(&grand_product_p16_statement.point); let p16_grand_product_evals_on_indexes_b = - p16_indexes_input[1].evaluate(&grand_product_p16_statement.point); + p16_indexes[1].evaluate(&grand_product_p16_statement.point); let p16_grand_product_evals_on_indexes_res = - p16_index_out_1_col.evaluate(&grand_product_p16_statement.point); - let p16_grand_product_evals_on_compression = - p16_compression_col.evaluate(&grand_product_p16_statement.point); + p16_indexes[2].evaluate(&grand_product_p16_statement.point); + prover_state.add_extension_scalars(&[ p16_grand_product_evals_on_indexes_a, p16_grand_product_evals_on_indexes_b, p16_grand_product_evals_on_indexes_res, - p16_grand_product_evals_on_compression, ]); let mut p16_indexes_a_statements = vec![Evaluation::new( @@ -384,6 +369,10 @@ pub fn prove_execution( grand_product_p16_statement.point.clone(), p16_grand_product_evals_on_indexes_b, )]; + let mut p16_indexes_res_statements = vec![Evaluation::new( + grand_product_p16_statement.point.clone(), + p16_grand_product_evals_on_indexes_res, + )]; let p24_grand_product_evals_on_indexes_a = p24_indexes[0].evaluate(&grand_product_p24_statement.point); @@ -539,40 +528,54 @@ pub fn prove_execution( dot_product_table.prove_extension(&mut prover_state, 1, &dot_product_columns_ref) }); - let p16_columns_ref = p16_columns.iter().map(Vec::as_slice).collect::>(); - let (p16_air_point, p16_evals_to_prove) = info_span!("Poseidon-16 AIR proof") - .in_scope(|| p16_table.prove_base(&mut prover_state, UNIVARIATE_SKIPS, &p16_columns_ref)); - let mut p16_statements = p16_evals_to_prove[16..p16_air.width() - 16] + let random_point_p16 = MultilinearPoint(prover_state.sample_vec(log_n_p16)); + let (p16_output_values, p16_input_statements, p16_cubes_statements) = prove_poseidon_gkr( + &mut prover_state, + &p16_witness, + random_point_p16.0.clone(), + UNIVARIATE_SKIPS, + &p16_gkr_layers, + ); + let p16_cubes_statements = p16_cubes_statements + .1 .iter() - .map(|&e| vec![Evaluation::new(p16_air_point.clone(), e)]) + .map(|&e| { + vec![Evaluation { + point: p16_cubes_statements.0.clone(), + value: e, + }] + }) .collect::>(); - p16_statements[0].push(Evaluation::new( - grand_product_p16_statement.point.clone(), - p16_grand_product_evals_on_compression, - )); - p16_statements[1].push(Evaluation::new( - grand_product_p16_statement.point.clone(), - p16_grand_product_evals_on_indexes_res, - )); - - let p24_columns_ref = p24_columns.iter().map(Vec::as_slice).collect::>(); - let (p24_air_point, p24_evals_to_prove) = info_span!("Poseidon-24 AIR proof") - .in_scope(|| p24_table.prove_base(&mut prover_state, UNIVARIATE_SKIPS, &p24_columns_ref)); - let p24_statements = p24_evals_to_prove[24..p24_air.width() - 8] + let random_point_p24 = MultilinearPoint(prover_state.sample_vec(log_n_p24)); + let (p24_output_values, p24_input_statements, p24_cubes_statements) = prove_poseidon_gkr( + &mut prover_state, + &p24_witness, + random_point_p24.0.clone(), + UNIVARIATE_SKIPS, + &p24_gkr_layers, + ); + let p24_cubes_statements = p24_cubes_statements + .1 .iter() - .map(|&e| vec![Evaluation::new(p24_air_point.clone(), e)]) - .collect(); + .map(|&e| { + vec![Evaluation { + point: p24_cubes_statements.0.clone(), + value: e, + }] + }) + .collect::>(); // Poseidons 16/24 memory addresses lookup let poseidon_logup_star_alpha = prover_state.sample(); let memory_folding_challenges = MultilinearPoint(prover_state.sample_vec(LOG_VECTOR_LEN)); let poseidon_lookup_statements = get_poseidon_lookup_statements( - (p16_air.width(), p24_air.width()), (log_n_p16, log_n_p24), - (&p16_evals_to_prove, &p24_evals_to_prove), - (&p16_air_point, &p24_air_point), + &p16_input_statements, + &(random_point_p16.clone(), p16_output_values.to_vec()), + &p24_input_statements, + &(random_point_p24.clone(), p24_output_values.to_vec()), &memory_folding_challenges, ); @@ -906,22 +909,51 @@ pub fn prove_execution( &all_poseidon_indexes, &MultilinearPoint(poseidon_logup_star_statements.on_indexes.point[3..].to_vec()), ); + let inner_values = [ poseidon_index_evals[0] / correcting_factor_p16, poseidon_index_evals[1] / correcting_factor_p16, poseidon_index_evals[2] / correcting_factor_p16, - poseidon_index_evals[3] / correcting_factor_p16, + // skip 3 (16_output_b, proved via sumcheck) poseidon_index_evals[4] / correcting_factor_p24, - // skip 5 + // skip 5 (24_input_b) poseidon_index_evals[6] / correcting_factor_p24, poseidon_index_evals[7] / correcting_factor_p24, ]; - prover_state.add_extension_scalars(&inner_values); - let (left, right) = p16_statements.split_at_mut(2); - let p16_statements_res_1 = &mut left[1]; - let p16_statements_res_2 = &mut right[0]; + let p16_value_index_res_b = poseidon_index_evals[3] / correcting_factor_p16; + // prove this value via sumcheck: index_res_b = (index_res_a + 1) * (1 - compression) + let p16_one_minus_compression = p16_witness + .compression + .as_ref() + .unwrap() + .1 + .par_iter() + .map(|c| FPacking::::ONE - *c) + .collect::>(); + let p16_index_res_a_plus_one = FPacking::::pack_slice(&p16_indexes[2]) + .par_iter() + .map(|c| *c + F::ONE) + .collect::>(); + + let (sc_point, sc_values, _) = sumcheck_prove( + UNIVARIATE_SKIPS, + MleGroupRef::BasePacked(vec![&p16_one_minus_compression, &p16_index_res_a_plus_one]), + &ProductComputation, + &ProductComputation, + &[], + Some(( + poseidon_logup_star_statements.on_indexes.point[3..].to_vec(), + None, + )), + false, + &mut prover_state, + p16_value_index_res_b, + None, + ); + prover_state.add_extension_scalar(sc_values[1]); + p16_indexes_res_statements.push(Evaluation::new(sc_point, sc_values[1] - EF::ONE)); add_poseidon_lookup_statements_on_indexes( log_n_p16, @@ -931,8 +963,7 @@ pub fn prove_execution( [ &mut p16_indexes_a_statements, &mut p16_indexes_b_statements, - p16_statements_res_1, - p16_statements_res_2, + &mut p16_indexes_res_statements, ], [ &mut p24_indexes_a_statements, @@ -1060,12 +1091,13 @@ pub fn prove_execution( ], // exec memory address C p16_indexes_a_statements, p16_indexes_b_statements, + p16_indexes_res_statements, p24_indexes_a_statements, p24_indexes_b_statements, p24_indexes_res_statements, ], - p16_statements, - p24_statements, + p16_cubes_statements, + p24_cubes_statements, vec![ vec![ dot_product_air_statement(0), diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index e92e6ba3..27eddc0d 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -5,13 +5,14 @@ use lean_vm::*; use lookup::verify_gkr_product; use lookup::verify_logup_star; use multilinear_toolkit::prelude::*; -use p3_air::BaseAir; use p3_field::PrimeCharacteristicRing; use p3_field::dot_product; use p3_util::{log2_ceil_usize, log2_strict_usize}; use packed_pcs::*; +use poseidon_circuit::PoseidonGKRLayers; +use poseidon_circuit::verify_poseidon_gkr; +use utils::ToUsize; use utils::dot_product_with_base; -use utils::{ToUsize, build_poseidon_16_air, build_poseidon_24_air}; use utils::{build_challenger, padd_with_zero_to_next_power_of_two}; use vm_air::*; use whir_p3::WhirConfig; @@ -27,24 +28,22 @@ pub fn verify_execution( let mut verifier_state = VerifierState::new(proof_data, build_challenger()); let exec_table = AirTable::::new(VMAir, VMAir); - let p16_air = build_poseidon_16_air(); - let p24_air = build_poseidon_24_air(); - let p16_air_packed = build_poseidon_16_air_packed(); - let p24_air_packed = build_poseidon_24_air_packed(); - let p16_table = AirTable::::new(p16_air.clone(), p16_air_packed); - let p24_table = AirTable::::new(p24_air.clone(), p24_air_packed); + let p16_gkr_layers = PoseidonGKRLayers::<16, N_COMMITED_CUBES_P16>::build(Some(VECTOR_LEN)); + let p24_gkr_layers = PoseidonGKRLayers::<24, N_COMMITED_CUBES_P24>::build(None); + let dot_product_table = AirTable::::new(DotProductAir, DotProductAir); let [ log_n_cycles, n_poseidons_16, + n_compressions_16, n_poseidons_24, n_dot_products, n_rows_table_dot_products, private_memory_len, n_vm_multilinear_evals, ] = verifier_state - .next_base_scalars_const::<7>()? + .next_base_scalars_const::<8>()? .into_iter() .map(|x| x.to_usize()) .collect::>() @@ -53,6 +52,7 @@ pub fn verify_execution( if log_n_cycles > 32 || n_poseidons_16 > 1 << 32 + || n_compressions_16 > n_poseidons_16 || n_poseidons_24 > 1 << 32 || n_dot_products > 1 << 32 || n_rows_table_dot_products > 1 << 32 @@ -112,6 +112,8 @@ pub fn verify_execution( bytecode.ending_pc, (n_poseidons_16, n_poseidons_24), n_rows_table_dot_products, + &p16_gkr_layers, + &p24_gkr_layers, ); let parsed_commitment_base = packed_pcs_parse_commitment( @@ -189,8 +191,10 @@ pub fn verify_execution( p16_grand_product_evals_on_indexes_a, p16_grand_product_evals_on_indexes_b, p16_grand_product_evals_on_indexes_res, - p16_grand_product_evals_on_compression, ] = verifier_state.next_extension_scalars_const()?; + let p16_grand_product_evals_on_compression = + mle_of_zeros_then_ones(n_compressions_16, &grand_product_p16_statement.point); + if grand_product_challenge_global + finger_print( &[ @@ -214,6 +218,10 @@ pub fn verify_execution( grand_product_p16_statement.point.clone(), p16_grand_product_evals_on_indexes_b, )]; + let mut p16_indexes_res_statements = vec![Evaluation::new( + grand_product_p16_statement.point.clone(), + p16_grand_product_evals_on_indexes_res, + )]; let [ p24_grand_product_evals_on_indexes_a, @@ -340,29 +348,45 @@ pub fn verify_execution( let (dot_product_air_point, dot_product_evals_to_verify) = dot_product_table.verify(&mut verifier_state, 1, table_dot_products_log_n_rows)?; - let (p16_air_point, p16_evals_to_verify) = - p16_table.verify(&mut verifier_state, UNIVARIATE_SKIPS, log_n_p16)?; - let (p24_air_point, p24_evals_to_verify) = - p24_table.verify(&mut verifier_state, UNIVARIATE_SKIPS, log_n_p24)?; - - let mut p16_statements = p16_evals_to_verify[16..p16_air.width() - 16] + let random_point_p16 = MultilinearPoint(verifier_state.sample_vec(log_n_p16)); + let (p16_output_values, p16_input_statements, p16_cubes_statements) = verify_poseidon_gkr( + &mut verifier_state, + log_n_p16, + &random_point_p16, + &p16_gkr_layers, + UNIVARIATE_SKIPS, + Some(n_compressions_16), + ); + let p16_cubes_statements = p16_cubes_statements + .1 .iter() - .map(|&e| vec![Evaluation::new(p16_air_point.clone(), e)]) + .map(|&e| { + vec![Evaluation { + point: p16_cubes_statements.0.clone(), + value: e, + }] + }) .collect::>(); - p16_statements[0].push(Evaluation::new( - grand_product_p16_statement.point.clone(), - p16_grand_product_evals_on_compression, - )); - p16_statements[1].push(Evaluation::new( - grand_product_p16_statement.point.clone(), - p16_grand_product_evals_on_indexes_res, - )); - - let p24_statements = p24_evals_to_verify[24..p24_air.width() - 8] + let random_point_p24 = MultilinearPoint(verifier_state.sample_vec(log_n_p24)); + let (p24_output_values, p24_input_statements, p24_cubes_statements) = verify_poseidon_gkr( + &mut verifier_state, + log_n_p24, + &random_point_p24, + &p24_gkr_layers, + UNIVARIATE_SKIPS, + None, + ); + let p24_cubes_statements = p24_cubes_statements + .1 .iter() - .map(|&e| vec![Evaluation::new(p24_air_point.clone(), e)]) - .collect(); + .map(|&e| { + vec![Evaluation { + point: p24_cubes_statements.0.clone(), + value: e, + }] + }) + .collect::>(); let poseidon_logup_star_alpha = verifier_state.sample(); let memory_folding_challenges = MultilinearPoint(verifier_state.sample_vec(LOG_VECTOR_LEN)); @@ -512,10 +536,11 @@ pub fn verify_execution( .unwrap(); let poseidon_lookup_statements = get_poseidon_lookup_statements( - (p16_air.width(), p24_air.width()), (log_n_p16, log_n_p24), - (&p16_evals_to_verify, &p24_evals_to_verify), - (&p16_air_point, &p24_air_point), + &p16_input_statements, + &(random_point_p16.clone(), p16_output_values.to_vec()), + &p24_input_statements, + &(random_point_p24.clone(), p24_output_values.to_vec()), &memory_folding_challenges, ); @@ -567,11 +592,7 @@ pub fn verify_execution( &poseidon_logup_star_statements.on_indexes.point, ); - let mut inner_values = verifier_state.next_extension_scalars_vec(7)?; - - let (left, right) = p16_statements.split_at_mut(2); - let p16_statements_res_1 = &mut left[1]; - let p16_statements_res_2 = &mut right[0]; + let mut inner_values = verifier_state.next_extension_scalars_vec(6)?; add_poseidon_lookup_statements_on_indexes( log_n_p16, @@ -581,8 +602,7 @@ pub fn verify_execution( [ &mut p16_indexes_a_statements, &mut p16_indexes_b_statements, - p16_statements_res_1, - p16_statements_res_2, + &mut p16_indexes_res_statements, ], [ &mut p24_indexes_a_statements, @@ -591,6 +611,28 @@ pub fn verify_execution( ], ); + let (p16_value_index_res_b, sc_eval) = sumcheck_verify_with_univariate_skip( + &mut verifier_state, + 3, + log_n_p16, + UNIVARIATE_SKIPS, + )?; + let sc_res_index_value = verifier_state.next_extension_scalar()?; + p16_indexes_res_statements + .push(Evaluation::new(sc_eval.point.clone(), sc_res_index_value - EF::ONE)); + + if sc_res_index_value + * (EF::ONE + - mle_of_zeros_then_ones((1 << log_n_p16) - n_compressions_16, &sc_eval.point)) + * sc_eval.point.eq_poly_outside(&MultilinearPoint( + poseidon_logup_star_statements.on_indexes.point[3..].to_vec(), + )) + != sc_eval.value + { + return Err(ProofError::InvalidProof); + } + + inner_values.insert(3, p16_value_index_res_b); inner_values.insert(5, inner_values[4] + EF::ONE); for v in &mut inner_values[..4] { @@ -760,12 +802,13 @@ pub fn verify_execution( ], // exec memory address C p16_indexes_a_statements, p16_indexes_b_statements, + p16_indexes_res_statements, p24_indexes_a_statements, p24_indexes_b_statements, p24_indexes_res_statements, ], - p16_statements, - p24_statements, + p16_cubes_statements, + p24_cubes_statements, vec![ vec![ dot_product_air_statement(0), diff --git a/crates/lean_prover/witness_generation/Cargo.toml b/crates/lean_prover/witness_generation/Cargo.toml index 63186fe6..b57d4dca 100644 --- a/crates/lean_prover/witness_generation/Cargo.toml +++ b/crates/lean_prover/witness_generation/Cargo.toml @@ -31,3 +31,5 @@ lean_vm.workspace = true lean_compiler.workspace = true derive_more.workspace = true multilinear-toolkit.workspace = true +poseidon_circuit.workspace = true +p3-monty-31.workspace = true diff --git a/crates/lean_prover/witness_generation/src/execution_trace.rs b/crates/lean_prover/witness_generation/src/execution_trace.rs index 2125a642..0a7f3134 100644 --- a/crates/lean_prover/witness_generation/src/execution_trace.rs +++ b/crates/lean_prover/witness_generation/src/execution_trace.rs @@ -17,6 +17,7 @@ pub struct ExecutionTrace { pub full_trace: [Vec; N_TOTAL_COLUMNS], pub n_poseidons_16: usize, pub n_poseidons_24: usize, + pub n_compressions_16: usize, pub poseidons_16: Vec, // padded with empty poseidons pub poseidons_24: Vec, // padded with empty poseidons pub dot_products: Vec, @@ -116,10 +117,14 @@ pub fn get_execution_trace( n_poseidons_24.next_power_of_two() - n_poseidons_24 ]); + let n_compressions_16; + (poseidons_16, n_compressions_16) = put_poseidon16_compressions_at_the_end(&poseidons_16); // TODO avoid reallocation + ExecutionTrace { full_trace: trace, n_poseidons_16, n_poseidons_24, + n_compressions_16, poseidons_16, poseidons_24, dot_products, @@ -129,3 +134,20 @@ pub fn get_execution_trace( memory: memory_padded, } } + +fn put_poseidon16_compressions_at_the_end( + poseidons_16: &[WitnessPoseidon16], +) -> (Vec, usize) { + let no_compression = poseidons_16 + .par_iter() + .filter(|p| !p.is_compression) + .cloned() + .collect::>(); + let compression = poseidons_16 + .par_iter() + .filter(|p| p.is_compression) + .cloned() + .collect::>(); + let n_compressions = compression.len(); + ([no_compression, compression].concat(), n_compressions) +} diff --git a/crates/lean_prover/witness_generation/src/poseidon_tables.rs b/crates/lean_prover/witness_generation/src/poseidon_tables.rs index 48513cb2..5bcac822 100644 --- a/crates/lean_prover/witness_generation/src/poseidon_tables.rs +++ b/crates/lean_prover/witness_generation/src/poseidon_tables.rs @@ -1,63 +1,14 @@ -use lean_vm::{ - F, POSEIDON_16_DEFAULT_COMPRESSION, POSEIDON_16_NULL_HASH_PTR, WitnessPoseidon16, - WitnessPoseidon24, -}; -use p3_field::PrimeCharacteristicRing; -use rayon::prelude::*; -use tracing::instrument; -use utils::{ - POSEIDON16_AIR_N_COLS, POSEIDON24_AIR_N_COLS, default_poseidon16_air_row, - default_poseidon24_air_row, generate_trace_poseidon_16, generate_trace_poseidon_24, - padd_with_zero_to_next_power_of_two, -}; - -#[instrument(skip_all)] -pub fn build_poseidon_24_columns( - poseidons_24: &[WitnessPoseidon24], - padding: usize, -) -> Vec> { - let inputs = poseidons_24.par_iter().map(|w| w.input).collect::>(); - let matrix = generate_trace_poseidon_24(&inputs); - let mut res = utils::transpose(&matrix.values, POSEIDON24_AIR_N_COLS, padding); - let default_p24_row = default_poseidon24_air_row(); - assert_eq!(default_p24_row.len(), res.len()); - res.par_iter_mut() - .zip(default_p24_row.par_iter()) - .for_each(|(col, default_value)| { - col.resize(col.len() + padding, *default_value); - }); - res -} - -#[instrument(skip_all)] -pub fn build_poseidon_16_columns( - poseidons_16: &[WitnessPoseidon16], - padding: usize, -) -> Vec> { - let inputs = poseidons_16.par_iter().map(|w| w.input).collect::>(); - let compresses = poseidons_16 - .par_iter() - .map(|w| w.is_compression) - .collect::>(); - let index_res = poseidons_16 - .par_iter() - .map(|w| w.addr_output) - .collect::>(); - let matrix = generate_trace_poseidon_16(&inputs, &compresses, &index_res); - let mut res = utils::transpose(&matrix.values, POSEIDON16_AIR_N_COLS, padding); +use std::array; - let default_p16_row = - default_poseidon16_air_row(POSEIDON_16_DEFAULT_COMPRESSION, POSEIDON_16_NULL_HASH_PTR); - assert_eq!(default_p16_row.len(), res.len()); - res.par_iter_mut() - .zip(default_p16_row.par_iter()) - .for_each(|(col, default_value)| { - col.resize(col.len() + padding, *default_value); - }); - res -} +use lean_vm::{F, PoseidonWitnessTrait, WitnessPoseidon16, WitnessPoseidon24}; +use multilinear_toolkit::prelude::*; +use p3_field::PrimeCharacteristicRing; +use p3_koala_bear::{KoalaBearInternalLayerParameters, KoalaBearParameters}; +use p3_monty_31::InternalLayerBaseParameters; +use poseidon_circuit::{PoseidonGKRLayers, PoseidonWitness, generate_poseidon_witness}; +use utils::{padd_with_zero_to_next_power_of_two, transposed_par_iter_mut}; -pub fn all_poseidon_16_indexes_input(poseidons_16: &[WitnessPoseidon16]) -> [Vec; 2] { +pub fn all_poseidon_16_indexes(poseidons_16: &[WitnessPoseidon16]) -> [Vec; 3] { [ poseidons_16 .par_iter() @@ -67,6 +18,10 @@ pub fn all_poseidon_16_indexes_input(poseidons_16: &[WitnessPoseidon16]) -> [Vec .par_iter() .map(|p| F::from_usize(p.addr_input_b)) .collect::>(), + poseidons_16 + .par_iter() + .map(|p| F::from_usize(p.addr_output)) + .collect::>(), ] } @@ -128,3 +83,47 @@ pub fn full_poseidon_indexes_poly( all_poseidon_indexes } + +pub fn generate_poseidon_witness_helper< + const WIDTH: usize, + const N_COMMITED_CUBES: usize, + W: PoseidonWitnessTrait + Send + Sync, +>( + layers: &PoseidonGKRLayers, + inputs: &[W], + n_compressions: Option, +) -> PoseidonWitness, WIDTH, N_COMMITED_CUBES> +where + KoalaBearInternalLayerParameters: InternalLayerBaseParameters, +{ + let n_poseidons = inputs.len(); + assert!(n_poseidons.is_power_of_two()); + let mut inputs_transposed: [_; WIDTH] = + array::from_fn(|_| unsafe { uninitialized_vec(n_poseidons) }); + transposed_par_iter_mut(&mut inputs_transposed) + .enumerate() + .for_each(|(i, row)| { + for (j, p) in row.into_iter().enumerate() { + *p = inputs[i].inputs()[j]; + } + }); + let inputs_transposed_packed: [_; WIDTH] = + array::from_fn(|i| PFPacking::::pack_slice(&inputs_transposed[i]).to_vec()); // TODO avoid cloning + generate_poseidon_witness::, WIDTH, N_COMMITED_CUBES>( + inputs_transposed_packed, + &layers, + n_compressions.map(|n_compressions| { + ( + n_compressions, + PFPacking::::pack_slice( + &[ + vec![F::ZERO; n_poseidons - n_compressions], + vec![F::ONE; n_compressions], + ] + .concat(), + ) + .to_vec(), + ) + }), + ) +} diff --git a/crates/lean_vm/src/witness/mod.rs b/crates/lean_vm/src/witness/mod.rs index 9af192ec..d0d9d694 100644 --- a/crates/lean_vm/src/witness/mod.rs +++ b/crates/lean_vm/src/witness/mod.rs @@ -9,3 +9,9 @@ pub use dot_product::*; pub use multilinear_eval::*; pub use poseidon16::*; pub use poseidon24::*; + +use crate::F; + +pub trait PoseidonWitnessTrait { + fn inputs(&self) -> &[F; WIDTH]; +} diff --git a/crates/lean_vm/src/witness/poseidon16.rs b/crates/lean_vm/src/witness/poseidon16.rs index 62fe9d34..37f75eb2 100644 --- a/crates/lean_vm/src/witness/poseidon16.rs +++ b/crates/lean_vm/src/witness/poseidon16.rs @@ -1,6 +1,6 @@ //! Poseidon2 hash witness for 16-element input -use crate::core::{F, POSEIDON_16_NULL_HASH_PTR, ZERO_VEC_PTR}; +use crate::{core::{F, POSEIDON_16_NULL_HASH_PTR, ZERO_VEC_PTR}, PoseidonWitnessTrait}; use p3_field::PrimeCharacteristicRing; pub const POSEIDON_16_DEFAULT_COMPRESSION: bool = true; @@ -22,6 +22,13 @@ pub struct WitnessPoseidon16 { pub is_compression: bool, } +impl PoseidonWitnessTrait<16> for WitnessPoseidon16 { + #[inline(always)] + fn inputs(&self) -> &[F; 16] { + &self.input + } +} + impl WitnessPoseidon16 { /// Create a precomputed Poseidon16 witness for all-zero input /// diff --git a/crates/lean_vm/src/witness/poseidon24.rs b/crates/lean_vm/src/witness/poseidon24.rs index d77c8a2b..4fa06e3a 100644 --- a/crates/lean_vm/src/witness/poseidon24.rs +++ b/crates/lean_vm/src/witness/poseidon24.rs @@ -1,6 +1,6 @@ //! Poseidon2 hash witness for 24-element input -use crate::core::{F, POSEIDON_24_NULL_HASH_PTR, ZERO_VEC_PTR}; +use crate::{core::{F, POSEIDON_24_NULL_HASH_PTR, ZERO_VEC_PTR}, PoseidonWitnessTrait}; use p3_field::PrimeCharacteristicRing; /// Witness data for Poseidon2 over 24 field elements @@ -18,6 +18,13 @@ pub struct WitnessPoseidon24 { pub input: [F; 24], } +impl PoseidonWitnessTrait<24> for WitnessPoseidon24 { + #[inline(always)] + fn inputs(&self) -> &[F; 24] { + &self.input + } +} + impl WitnessPoseidon24 { /// Create a new Poseidon24 witness with all hash data pub const fn new( diff --git a/crates/poseidon_circuit/src/lib.rs b/crates/poseidon_circuit/src/lib.rs index ac6556a6..8c116be7 100644 --- a/crates/poseidon_circuit/src/lib.rs +++ b/crates/poseidon_circuit/src/lib.rs @@ -14,7 +14,8 @@ pub use witness_gen::*; #[cfg(test)] mod tests; -pub(crate) mod gkr_layers; +pub mod gkr_layers; +pub use gkr_layers::*; pub(crate) type F = KoalaBear; pub(crate) type EF = QuinticExtensionFieldKB; diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index 00207a61..1a963894 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -15,8 +15,8 @@ pub fn prove_poseidon_gkr( layers: &PoseidonGKRLayers, ) -> ( [EF; WIDTH], - Vec>>, - Vec>>, + (MultilinearPoint, Vec), + (MultilinearPoint, Vec), ) where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, @@ -35,10 +35,6 @@ where ) }); - if let Some((n_compressions, _)) = &witness.compression { - prover_state.add_base_scalars(&[F::from_usize(*n_compressions)]); - } - prover_state.add_extension_scalars(&output_claims); let mut claims = output_claims.to_vec(); @@ -264,7 +260,7 @@ fn inner_evals_on_commited_columns( point: &[EF], univariate_skips: usize, columns: &[Vec>], -) -> Vec>> { +) -> (MultilinearPoint, Vec) { let eq_mle = eval_eq_packed(&point[1..]); let inner_evals = columns .par_iter() @@ -283,15 +279,13 @@ fn inner_evals_on_commited_columns( .flatten() .collect::>(); prover_state.add_extension_scalars(&inner_evals); - let mut pcs_statements = vec![]; + let mut values_to_prove = vec![]; let pcs_batching_scalars_inputs = prover_state.sample_vec(univariate_skips); + let point_to_prove = + MultilinearPoint([pcs_batching_scalars_inputs.clone(), point[1..].to_vec()].concat()); for col_inner_evals in inner_evals.chunks_exact(1 << univariate_skips) { - pcs_statements.push(vec![Evaluation { - point: MultilinearPoint( - [pcs_batching_scalars_inputs.clone(), point[1..].to_vec()].concat(), - ), - value: col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone())), - }]); + values_to_prove + .push(col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone()))); } - pcs_statements + (point_to_prove, values_to_prove) } diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index 3dc61432..c8915306 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -129,12 +129,21 @@ fn test_prove_poseidons() { ); // PCS opening + let mut pcs_statements = vec![]; + for (point_to_prove, evals_to_prove) in [input_pcs_statements, cubes_pcs_statements] { + for v in evals_to_prove { + pcs_statements.push(vec![Evaluation { + point: point_to_prove.clone(), + value: v, + }]); + } + } let global_statements = packed_pcs_global_statements_for_prover( &committed_polys, &committed_col_dims, log_smallest_decomposition_chunk, - &[input_pcs_statements, cubes_pcs_statements].concat(), + &pcs_statements, &mut prover_state, ); whir_config.prove( @@ -174,15 +183,28 @@ fn test_prove_poseidons() { &output_claim_point, &layers, UNIVARIATE_SKIPS, - COMPRESS, + if COMPRESS { + Some(n_compressions) + } else { + None + }, ); // PCS verification + let mut pcs_statements = vec![]; + for (point_to_verif, evals_to_verif) in [input_pcs_statements, cubes_pcs_statements] { + for v in evals_to_verif { + pcs_statements.push(vec![Evaluation { + point: point_to_verif.clone(), + value: v, + }]); + } + } let global_statements = packed_pcs_global_statements_for_verifier( &committed_col_dims, log_smallest_decomposition_chunk, - &[input_pcs_statements, cubes_pcs_statements].concat(), + &pcs_statements, &mut verifier_state, &Default::default(), ) diff --git a/crates/poseidon_circuit/src/verify.rs b/crates/poseidon_circuit/src/verify.rs index efa7bdc6..3f83e072 100644 --- a/crates/poseidon_circuit/src/verify.rs +++ b/crates/poseidon_circuit/src/verify.rs @@ -1,7 +1,6 @@ use multilinear_toolkit::prelude::*; use p3_koala_bear::{KoalaBearInternalLayerParameters, KoalaBearParameters}; use p3_monty_31::InternalLayerBaseParameters; -use utils::ToUsize; use crate::{EF, F, gkr_layers::PoseidonGKRLayers}; @@ -11,20 +10,17 @@ pub fn verify_poseidon_gkr( output_claim_point: &[EF], layers: &PoseidonGKRLayers, univariate_skips: usize, - has_compressions: bool, + n_compressions: Option, ) -> ( [EF; WIDTH], - Vec>>, - Vec>>, + (MultilinearPoint, Vec), + (MultilinearPoint, Vec), ) where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { let selectors = univariate_selectors::(univariate_skips); - let n_compressions = - has_compressions.then(|| verifier_state.next_base_scalars_const::<1>().unwrap()[0]); - let output_claims = verifier_state.next_extension_scalars_vec(WIDTH).unwrap(); let mut claims = output_claims.clone(); @@ -32,7 +28,7 @@ where let mut claim_point = output_claim_point.to_vec(); for (i, full_round) in layers.final_full_rounds.iter().rev().enumerate() { let n_inputs = if i == 0 - && let Some(_) = n_compressions + && n_compressions.is_some() { WIDTH + 1 } else { @@ -50,7 +46,6 @@ where if i == 0 && let Some(n_compressions) = n_compressions { - let n_compressions = n_compressions.to_usize(); assert!(n_compressions <= 1 << log_n_poseidons); let compression_eval = claims.pop().unwrap(); assert_eq!( @@ -181,13 +176,15 @@ fn verify_inner_evals_on_commited_columns( point: &[EF], claimed_evals: &[EF], selectors: &[DensePolynomial], -) -> Vec>> { +) -> (MultilinearPoint, Vec) { let univariate_skips = log2_strict_usize(selectors.len()); let inner_evals_inputs = verifier_state .next_extension_scalars_vec(claimed_evals.len() << univariate_skips) .unwrap(); let pcs_batching_scalars_inputs = verifier_state.sample_vec(univariate_skips); - let mut pcs_statements = vec![]; + let mut values_to_verif = vec![]; + let point_to_verif = + MultilinearPoint([pcs_batching_scalars_inputs.clone(), point[1..].to_vec()].concat()); for (&eval, col_inner_evals) in claimed_evals .iter() .zip(inner_evals_inputs.chunks_exact(1 << univariate_skips)) @@ -201,12 +198,8 @@ fn verify_inner_evals_on_commited_columns( None ) ); - pcs_statements.push(vec![Evaluation { - point: MultilinearPoint( - [pcs_batching_scalars_inputs.clone(), point[1..].to_vec()].concat(), - ), - value: col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone())), - }]); + values_to_verif + .push(col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone()))); } - pcs_statements + (point_to_verif, values_to_verif) } diff --git a/crates/utils/src/poseidon2.rs b/crates/utils/src/poseidon2.rs index b494bd73..dc6b6512 100644 --- a/crates/utils/src/poseidon2.rs +++ b/crates/utils/src/poseidon2.rs @@ -1,7 +1,5 @@ use std::sync::OnceLock; -use multilinear_toolkit::prelude::*; -use p3_koala_bear::GenericPoseidon2LinearLayersKoalaBear; use p3_koala_bear::KOALABEAR_RC16_EXTERNAL_FINAL; use p3_koala_bear::KOALABEAR_RC16_EXTERNAL_INITIAL; use p3_koala_bear::KOALABEAR_RC16_INTERNAL; @@ -10,15 +8,9 @@ use p3_koala_bear::KOALABEAR_RC24_EXTERNAL_INITIAL; use p3_koala_bear::KOALABEAR_RC24_INTERNAL; use p3_koala_bear::KoalaBear; use p3_koala_bear::Poseidon2KoalaBear; -use p3_matrix::dense::RowMajorMatrix; use p3_poseidon2::ExternalLayerConstants; -use p3_poseidon2_air::p16::Poseidon2Air16; use p3_poseidon2_air::p16::RoundConstants16; -use p3_poseidon2_air::p16::generate_trace_rows_16; -use p3_poseidon2_air::p24::Poseidon2Air24; use p3_poseidon2_air::p24::RoundConstants24; -use p3_poseidon2_air::p24::generate_trace_rows_24; -use p3_poseidon2_air::{p16, p24}; use p3_symmetric::Permutation; pub type Poseidon16 = Poseidon2KoalaBear<16>; @@ -32,51 +24,6 @@ pub const QUARTER_FULL_ROUNDS_24: usize = 2; pub const HALF_FULL_ROUNDS_24: usize = 4; pub const PARTIAL_ROUNDS_24: usize = 23; -pub const SBOX_DEGREE: u64 = 3; -pub const SBOX_REGISTERS: usize = 0; - -pub const POSEIDON16_AIR_N_COLS: usize = p16::num_cols::< - 16, - SBOX_DEGREE, - SBOX_REGISTERS, - QUARTER_FULL_ROUNDS_16, - HALF_FULL_ROUNDS_16, - PARTIAL_ROUNDS_16, ->(); - -pub const POSEIDON24_AIR_N_COLS: usize = p24::num_cols::< - 24, - SBOX_DEGREE, - SBOX_REGISTERS, - QUARTER_FULL_ROUNDS_24, - HALF_FULL_ROUNDS_24, - PARTIAL_ROUNDS_24, ->(); - -pub type MyLinearLayers = GenericPoseidon2LinearLayersKoalaBear; - -pub type Poseidon16Air = Poseidon2Air16< - F, - MyLinearLayers, - 16, - SBOX_DEGREE, - SBOX_REGISTERS, - QUARTER_FULL_ROUNDS_16, - HALF_FULL_ROUNDS_16, - PARTIAL_ROUNDS_16, ->; - -pub type Poseidon24Air = Poseidon2Air24< - F, - MyLinearLayers, - 24, - SBOX_DEGREE, - SBOX_REGISTERS, - QUARTER_FULL_ROUNDS_24, - HALF_FULL_ROUNDS_24, - PARTIAL_ROUNDS_24, ->; - pub type MyRoundConstants16 = RoundConstants16; pub type MyRoundConstants24 = RoundConstants24; @@ -134,22 +81,6 @@ pub(crate) fn get_poseidon24() -> &'static Poseidon24 { }) } -pub fn build_poseidon_16_air() -> Poseidon16Air { - Poseidon16Air::new(build_poseidon16_constants()) -} - -pub fn build_poseidon_24_air() -> Poseidon24Air { - Poseidon24Air::new(build_poseidon24_constants()) -} - -pub fn build_poseidon_16_air_packed() -> Poseidon16Air> { - Poseidon16Air::new(build_poseidon16_constants_packed()) -} - -pub fn build_poseidon_24_air_packed() -> Poseidon24Air> { - Poseidon24Air::new(build_poseidon24_constants_packed()) -} - pub fn build_poseidon16_constants() -> MyRoundConstants16 { RoundConstants16 { beginning_full_round_constants: KOALABEAR_RC16_EXTERNAL_INITIAL, @@ -166,66 +97,3 @@ pub fn build_poseidon24_constants() -> MyRoundConstants24 { } } -fn build_poseidon16_constants_packed() -> MyRoundConstants16> { - let normal_constants = build_poseidon16_constants(); - RoundConstants16 { - beginning_full_round_constants: normal_constants - .beginning_full_round_constants - .map(|arr| arr.map(Into::into)), - partial_round_constants: normal_constants.partial_round_constants.map(Into::into), - ending_full_round_constants: normal_constants - .ending_full_round_constants - .map(|arr| arr.map(Into::into)), - } -} - -fn build_poseidon24_constants_packed() -> MyRoundConstants24> { - let normal_constants = build_poseidon24_constants(); - MyRoundConstants24 { - beginning_full_round_constants: normal_constants - .beginning_full_round_constants - .map(|arr| arr.map(Into::into)), - partial_round_constants: normal_constants.partial_round_constants.map(Into::into), - ending_full_round_constants: normal_constants - .ending_full_round_constants - .map(|arr| arr.map(Into::into)), - } -} - -pub fn generate_trace_poseidon_16( - inputs: &[[KoalaBear; 16]], - compress: &[bool], - index_res: &[usize], -) -> RowMajorMatrix { - generate_trace_rows_16::< - KoalaBear, - MyLinearLayers, - 16, - SBOX_DEGREE, - SBOX_REGISTERS, - QUARTER_FULL_ROUNDS_16, - HALF_FULL_ROUNDS_16, - PARTIAL_ROUNDS_16, - >(inputs, compress, index_res, &build_poseidon16_constants()) -} - -pub fn generate_trace_poseidon_24(inputs: &[[KoalaBear; 24]]) -> RowMajorMatrix { - generate_trace_rows_24::< - KoalaBear, - MyLinearLayers, - 24, - SBOX_DEGREE, - SBOX_REGISTERS, - QUARTER_FULL_ROUNDS_24, - HALF_FULL_ROUNDS_24, - PARTIAL_ROUNDS_24, - >(inputs, &build_poseidon24_constants()) -} - -pub fn default_poseidon16_air_row(compress: bool, index_res: usize) -> Vec { - generate_trace_poseidon_16(&[Default::default()], &[compress], &[index_res]).values -} - -pub fn default_poseidon24_air_row() -> Vec { - generate_trace_poseidon_24(&[Default::default()]).values -} From 2e926c8378fbba7e4e0719d7a794d9c0aaa37c13 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 26 Oct 2025 09:10:06 +0400 Subject: [PATCH 32/42] abstract away the univariate skip from Poseidon GKR api --- crates/poseidon_circuit/src/prove.rs | 47 +++++++++++++++++++-------- crates/poseidon_circuit/src/tests.rs | 4 +-- crates/poseidon_circuit/src/verify.rs | 35 ++++++++++++++++---- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index 1a963894..52fcd650 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -23,21 +23,42 @@ where { let selectors = univariate_selectors::(univariate_skips); - let output_claims = info_span!("computing output claims").in_scope(|| { - batch_evaluate_univariate_multilinear( - &witness - .output_layer - .iter() - .map(|l| PFPacking::::unpack_slice(l)) - .collect::>(), - &claim_point, - &selectors, - ) - }); + assert_eq!(claim_point.len(), log2_strict_usize(witness.n_poseidons())); - prover_state.add_extension_scalars(&output_claims); + let (output_claims, mut claims) = info_span!("computing output claims").in_scope(|| { + let eq_poly = eval_eq(&claim_point[univariate_skips..]); + let inner_evals = witness + .output_layer + .par_iter() + .map(|poly| { + FPacking::::unpack_slice(poly) + .chunks_exact(eq_poly.len()) + .map(|chunk| dot_product(eq_poly.iter().copied(), chunk.iter().copied())) + .collect::>() + }) + .collect::>(); + for evals in &inner_evals { + prover_state.add_extension_scalars(evals); + } + let alpha = prover_state.sample(); + claim_point = [vec![alpha], claim_point[univariate_skips..].to_vec()].concat(); + let selectors_at_alpha = selectors + .iter() + .map(|selector| selector.evaluate(alpha)) + .collect::>(); - let mut claims = output_claims.to_vec(); + let mut output_claims = vec![]; + let mut claims = vec![]; + for evals in inner_evals { + output_claims + .push(evals.evaluate(&MultilinearPoint(claim_point[..univariate_skips].to_vec()))); + claims.push(dot_product( + selectors_at_alpha.iter().copied(), + evals.into_iter(), + )) + } + (output_claims, claims) + }); for (i, (input_layers, full_round)) in witness .final_full_round_inputs diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index c8915306..2cd653fd 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -118,7 +118,7 @@ fn test_prove_poseidons() { log_smallest_decomposition_chunk, ); - let claim_point = prover_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); + let claim_point = prover_state.sample_vec(log_n_poseidons); let (_output_values, input_pcs_statements, cubes_pcs_statements) = prove_poseidon_gkr( &mut prover_state, @@ -175,7 +175,7 @@ fn test_prove_poseidons() { ) .unwrap(); - let output_claim_point = verifier_state.sample_vec(log_n_poseidons + 1 - UNIVARIATE_SKIPS); + let output_claim_point = verifier_state.sample_vec(log_n_poseidons); let (_output_values, input_pcs_statements, cubes_pcs_statements) = verify_poseidon_gkr( &mut verifier_state, diff --git a/crates/poseidon_circuit/src/verify.rs b/crates/poseidon_circuit/src/verify.rs index 3f83e072..bbb1fece 100644 --- a/crates/poseidon_circuit/src/verify.rs +++ b/crates/poseidon_circuit/src/verify.rs @@ -21,15 +21,36 @@ where { let selectors = univariate_selectors::(univariate_skips); - let output_claims = verifier_state.next_extension_scalars_vec(WIDTH).unwrap(); - - let mut claims = output_claims.clone(); + let mut output_claims = vec![]; + let mut claims = vec![]; + + let mut claim_point = { + let inner_evals = (0..WIDTH) + .map(|_| { + verifier_state + .next_extension_scalars_vec(1 << univariate_skips) + .unwrap() + }) + .collect::>(); + let alpha = verifier_state.sample(); + let selectors_at_alpha = selectors + .iter() + .map(|selector| selector.evaluate(alpha)) + .collect::>(); + for evals in inner_evals { + output_claims.push(evals.evaluate(&MultilinearPoint( + output_claim_point[..univariate_skips].to_vec(), + ))); + claims.push(dot_product( + selectors_at_alpha.iter().copied(), + evals.into_iter(), + )) + } + [vec![alpha], output_claim_point[univariate_skips..].to_vec()].concat() + }; - let mut claim_point = output_claim_point.to_vec(); for (i, full_round) in layers.final_full_rounds.iter().rev().enumerate() { - let n_inputs = if i == 0 - && n_compressions.is_some() - { + let n_inputs = if i == 0 && n_compressions.is_some() { WIDTH + 1 } else { WIDTH From fe9b4b0aa0e685284bc7ca376907ed57f1f343e9 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 26 Oct 2025 10:23:58 +0400 Subject: [PATCH 33/42] fix --- crates/poseidon_circuit/src/prove.rs | 2 +- crates/poseidon_circuit/src/tests.rs | 38 ++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index 52fcd650..bf360133 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -41,7 +41,6 @@ where prover_state.add_extension_scalars(evals); } let alpha = prover_state.sample(); - claim_point = [vec![alpha], claim_point[univariate_skips..].to_vec()].concat(); let selectors_at_alpha = selectors .iter() .map(|selector| selector.evaluate(alpha)) @@ -57,6 +56,7 @@ where evals.into_iter(), )) } + claim_point = [vec![alpha], claim_point[univariate_skips..].to_vec()].concat(); (output_claims, claims) }); diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index 2cd653fd..bff1365c 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -76,7 +76,14 @@ fn test_prove_poseidons() { let log_smallest_decomposition_chunk = 0; // unused because everything is a power of 2 - let (mut verifier_state, proof_size, output_layer, prover_duration) = { + let ( + mut verifier_state, + proof_size, + output_layer, + prover_duration, + output_values_prover, + claim_point, + ) = { // ---------------------------------------------------- PROVER ---------------------------------------------------- let prover_time = Instant::now(); @@ -120,10 +127,10 @@ fn test_prove_poseidons() { let claim_point = prover_state.sample_vec(log_n_poseidons); - let (_output_values, input_pcs_statements, cubes_pcs_statements) = prove_poseidon_gkr( + let (output_values, input_pcs_statements, cubes_pcs_statements) = prove_poseidon_gkr( &mut prover_state, &witness, - claim_point, + claim_point.clone(), UNIVARIATE_SKIPS, &layers, ); @@ -160,11 +167,14 @@ fn test_prove_poseidons() { prover_state.proof_size(), witness.output_layer, prover_duration, + output_values, + claim_point, ) }; let verifier_time = Instant::now(); - { + + let output_values_verifier = { // ---------------------------------------------------- VERIFIER ---------------------------------------------------- let parsed_pcs_commitment = packed_pcs_parse_commitment( @@ -177,17 +187,13 @@ fn test_prove_poseidons() { let output_claim_point = verifier_state.sample_vec(log_n_poseidons); - let (_output_values, input_pcs_statements, cubes_pcs_statements) = verify_poseidon_gkr( + let (output_values, input_pcs_statements, cubes_pcs_statements) = verify_poseidon_gkr( &mut verifier_state, log_n_poseidons, &output_claim_point, &layers, UNIVARIATE_SKIPS, - if COMPRESS { - Some(n_compressions) - } else { - None - }, + if COMPRESS { Some(n_compressions) } else { None }, ); // PCS verification @@ -217,7 +223,8 @@ fn test_prove_poseidons() { global_statements, ) .unwrap(); - } + output_values + }; let verifier_duration = verifier_time.elapsed(); let mut data_to_hash = input.clone(); @@ -270,6 +277,15 @@ fn test_prove_poseidons() { assert_eq!(PFPacking::::unpack_slice(&layer), data_to_hash[i]); }); } + assert_eq!(output_values_verifier, output_values_prover); + assert_eq!( + output_values_verifier.as_slice(), + &output_layer + .iter() + .map(|layer| PFPacking::::unpack_slice(&layer) + .evaluate(&MultilinearPoint(claim_point.clone()))) + .collect::>() + ); println!("2^{} Poseidon2", log_n_poseidons); println!( From b80c1106805110c0b2893fe5dd26fc9cd7af0c49 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 26 Oct 2025 10:31:51 +0400 Subject: [PATCH 34/42] fix --- crates/lean_prover/src/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lean_prover/src/common.rs b/crates/lean_prover/src/common.rs index fcdf793f..dbc95adc 100644 --- a/crates/lean_prover/src/common.rs +++ b/crates/lean_prover/src/common.rs @@ -320,7 +320,7 @@ pub fn get_poseidon_lookup_statements( let p24_folded_eval_addr_a = (&p24_input_evals[..8]).evaluate(memory_folding_challenges); let p24_folded_eval_addr_b = (&p24_input_evals[8..16]).evaluate(memory_folding_challenges); let p24_folded_eval_addr_c = (&p24_input_evals[16..24]).evaluate(memory_folding_challenges); - let p24_folded_eval_addr_res = p24_output_evals.evaluate(memory_folding_challenges); + let p24_folded_eval_addr_res = (&p24_output_evals[16..24]).evaluate(memory_folding_challenges); let padding_p16 = EF::zero_vec(log_n_p16.max(log_n_p24) - log_n_p16); let padding_p24 = EF::zero_vec(log_n_p16.max(log_n_p24) - log_n_p24); From 07b5550aee01be2f54d39de2ea007132dce19bba Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 26 Oct 2025 10:42:28 +0400 Subject: [PATCH 35/42] wip --- Cargo.lock | 2 +- crates/lean_prover/src/prove_execution.rs | 3 ++- crates/lean_prover/src/verify_execution.rs | 8 +++++--- crates/poseidon_circuit/src/prove.rs | 5 +++-- crates/poseidon_circuit/src/witness_gen.rs | 7 +++---- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abb58acd..fb8ff292 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,7 +60,7 @@ version = "0.3.0" source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#61c0491b6fe52a55ba93d643aa92c9f0b2cf52ae" dependencies = [ "fiat-shamir", - "itertools 0.14.0", + "itertools 0.13.0", "p3-field", "p3-util", "rand", diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index 7729654e..3a30ebaf 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -937,8 +937,9 @@ pub fn prove_execution( .map(|c| *c + F::ONE) .collect::>(); + // TODO there is a big inneficiency in impl SumcheckComputationPacked for ProductComputation let (sc_point, sc_values, _) = sumcheck_prove( - UNIVARIATE_SKIPS, + 1, // TODO univariate skip MleGroupRef::BasePacked(vec![&p16_one_minus_compression, &p16_index_res_a_plus_one]), &ProductComputation, &ProductComputation, diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index 27eddc0d..9cc4203a 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -615,11 +615,13 @@ pub fn verify_execution( &mut verifier_state, 3, log_n_p16, - UNIVARIATE_SKIPS, + 1, // TODO univariate skip )?; let sc_res_index_value = verifier_state.next_extension_scalar()?; - p16_indexes_res_statements - .push(Evaluation::new(sc_eval.point.clone(), sc_res_index_value - EF::ONE)); + p16_indexes_res_statements.push(Evaluation::new( + sc_eval.point.clone(), + sc_res_index_value - EF::ONE, + )); if sc_res_index_value * (EF::ONE diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index bf360133..f485ee59 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -7,6 +7,7 @@ use p3_koala_bear::{KoalaBearInternalLayerParameters, KoalaBearParameters}; use p3_monty_31::InternalLayerBaseParameters; use tracing::{info_span, instrument}; +#[instrument(skip_all)] pub fn prove_poseidon_gkr( prover_state: &mut FSProver>, witness: &PoseidonWitness, WIDTH, N_COMMITED_CUBES>, @@ -162,7 +163,7 @@ where ) } -#[instrument(skip_all)] +// #[instrument(skip_all)] fn prove_gkr_round< SC: SumcheckComputation + SumcheckComputation @@ -208,7 +209,7 @@ fn prove_gkr_round< (sumcheck_point.0, sumcheck_inner_evals) } -#[instrument(skip_all)] +// #[instrument(skip_all)] fn prove_batch_internal_round( prover_state: &mut FSProver>, input_layers: &[Vec>], diff --git a/crates/poseidon_circuit/src/witness_gen.rs b/crates/poseidon_circuit/src/witness_gen.rs index 256de33d..fa090e53 100644 --- a/crates/poseidon_circuit/src/witness_gen.rs +++ b/crates/poseidon_circuit/src/witness_gen.rs @@ -6,7 +6,6 @@ use p3_koala_bear::KoalaBearInternalLayerParameters; use p3_koala_bear::KoalaBearParameters; use p3_monty_31::InternalLayerBaseParameters; use p3_poseidon2::GenericPoseidon2LinearLayers; -use tracing::instrument; use utils::transposed_par_iter_mut; use crate::gkr_layers::BatchPartialRounds; @@ -95,7 +94,7 @@ where } } -#[instrument(skip_all)] +// #[instrument(skip_all)] fn apply_full_round( input_layers: &[Vec; WIDTH], full_round: &FullRoundComputation, @@ -134,7 +133,7 @@ where output_layers } -#[instrument(skip_all)] +// #[instrument(skip_all)] fn apply_partial_round( input_layers: &[Vec], partial_round: &PartialRoundComputation, @@ -161,7 +160,7 @@ where output_layers } -#[instrument(skip_all)] +// #[instrument(skip_all)] fn apply_batch_partial_rounds( input_layers: &[Vec], rounds: &BatchPartialRounds, From c22a616e4bbee4543fcc52ffb09e44e2b0bbc5eb Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 26 Oct 2025 10:52:00 +0400 Subject: [PATCH 36/42] fix --- crates/lean_prover/src/verify_execution.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index 9cc4203a..fed457ab 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -52,7 +52,7 @@ pub fn verify_execution( if log_n_cycles > 32 || n_poseidons_16 > 1 << 32 - || n_compressions_16 > n_poseidons_16 + || n_compressions_16 > n_poseidons_16.next_power_of_two() || n_poseidons_24 > 1 << 32 || n_dot_products > 1 << 32 || n_rows_table_dot_products > 1 << 32 @@ -191,9 +191,11 @@ pub fn verify_execution( p16_grand_product_evals_on_indexes_a, p16_grand_product_evals_on_indexes_b, p16_grand_product_evals_on_indexes_res, - ] = verifier_state.next_extension_scalars_const()?; - let p16_grand_product_evals_on_compression = - mle_of_zeros_then_ones(n_compressions_16, &grand_product_p16_statement.point); + ] = verifier_state.next_extension_scalars_const().unwrap(); + let p16_grand_product_evals_on_compression = mle_of_zeros_then_ones( + n_poseidons_16.next_power_of_two() - n_compressions_16, + &grand_product_p16_statement.point, + ); if grand_product_challenge_global + finger_print( From 30a6e4dedf7d98e1f65fae055da06108e1dc7e4e Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 26 Oct 2025 11:04:22 +0400 Subject: [PATCH 37/42] gud --- Cargo.lock | 10 +++---- crates/lean_prover/src/verify_execution.rs | 34 +++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb8ff292..5ba72c50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,10 +57,10 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backend" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#61c0491b6fe52a55ba93d643aa92c9f0b2cf52ae" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#2d6e327f04349d481167746659cffe1bb2b2ec85" dependencies = [ "fiat-shamir", - "itertools 0.13.0", + "itertools 0.14.0", "p3-field", "p3-util", "rand", @@ -153,7 +153,7 @@ dependencies = [ [[package]] name = "constraints-folder" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#61c0491b6fe52a55ba93d643aa92c9f0b2cf52ae" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#2d6e327f04349d481167746659cffe1bb2b2ec85" dependencies = [ "fiat-shamir", "p3-air", @@ -523,7 +523,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "multilinear-toolkit" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#61c0491b6fe52a55ba93d643aa92c9f0b2cf52ae" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#2d6e327f04349d481167746659cffe1bb2b2ec85" dependencies = [ "backend", "constraints-folder", @@ -1143,7 +1143,7 @@ checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" [[package]] name = "sumcheck" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#61c0491b6fe52a55ba93d643aa92c9f0b2cf52ae" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#2d6e327f04349d481167746659cffe1bb2b2ec85" dependencies = [ "backend", "constraints-folder", diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index fed457ab..7ee76013 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -596,23 +596,6 @@ pub fn verify_execution( let mut inner_values = verifier_state.next_extension_scalars_vec(6)?; - add_poseidon_lookup_statements_on_indexes( - log_n_p16, - log_n_p24, - &poseidon_logup_star_statements.on_indexes.point, - &inner_values, - [ - &mut p16_indexes_a_statements, - &mut p16_indexes_b_statements, - &mut p16_indexes_res_statements, - ], - [ - &mut p24_indexes_a_statements, - &mut p24_indexes_b_statements, - &mut p24_indexes_res_statements, - ], - ); - let (p16_value_index_res_b, sc_eval) = sumcheck_verify_with_univariate_skip( &mut verifier_state, 3, @@ -636,6 +619,23 @@ pub fn verify_execution( return Err(ProofError::InvalidProof); } + add_poseidon_lookup_statements_on_indexes( + log_n_p16, + log_n_p24, + &poseidon_logup_star_statements.on_indexes.point, + &inner_values, + [ + &mut p16_indexes_a_statements, + &mut p16_indexes_b_statements, + &mut p16_indexes_res_statements, + ], + [ + &mut p24_indexes_a_statements, + &mut p24_indexes_b_statements, + &mut p24_indexes_res_statements, + ], + ); + inner_values.insert(3, p16_value_index_res_b); inner_values.insert(5, inner_values[4] + EF::ONE); From 81026e84009612ba59da34297f2ae50cc1567c6a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 26 Oct 2025 16:02:50 +0400 Subject: [PATCH 38/42] remove dead code --- Cargo.lock | 8 +- crates/air/src/lib.rs | 20 +-- crates/air/src/prove.rs | 73 ++--------- crates/air/src/table.rs | 10 +- crates/air/src/tests.rs | 118 +----------------- crates/air/src/verify.rs | 14 ++- crates/lean_prover/src/common.rs | 18 +-- crates/lean_prover/src/prove_execution.rs | 32 +++-- crates/lean_prover/src/verify_execution.rs | 29 ++--- .../witness_generation/src/poseidon_tables.rs | 2 +- crates/lean_vm/src/witness/poseidon16.rs | 5 +- crates/lean_vm/src/witness/poseidon24.rs | 5 +- crates/lookup/src/product_gkr.rs | 3 +- crates/lookup/src/quotient_gkr.rs | 9 +- .../src/gkr_layers/partial_round.rs | 12 +- crates/poseidon_circuit/src/lib.rs | 7 ++ crates/poseidon_circuit/src/prove.rs | 37 +++--- crates/poseidon_circuit/src/tests.rs | 32 +++-- crates/poseidon_circuit/src/verify.rs | 38 +++--- crates/utils/src/poseidon2.rs | 1 - src/main.rs | 4 +- 21 files changed, 150 insertions(+), 327 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ba72c50..8258273f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,7 +57,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backend" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#2d6e327f04349d481167746659cffe1bb2b2ec85" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#ed923559dcb49da4c83cfd6ccd5e00ffb8119aa8" dependencies = [ "fiat-shamir", "itertools 0.14.0", @@ -153,7 +153,7 @@ dependencies = [ [[package]] name = "constraints-folder" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#2d6e327f04349d481167746659cffe1bb2b2ec85" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#ed923559dcb49da4c83cfd6ccd5e00ffb8119aa8" dependencies = [ "fiat-shamir", "p3-air", @@ -523,7 +523,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "multilinear-toolkit" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#2d6e327f04349d481167746659cffe1bb2b2ec85" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#ed923559dcb49da4c83cfd6ccd5e00ffb8119aa8" dependencies = [ "backend", "constraints-folder", @@ -1143,7 +1143,7 @@ checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" [[package]] name = "sumcheck" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#2d6e327f04349d481167746659cffe1bb2b2ec85" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#ed923559dcb49da4c83cfd6ccd5e00ffb8119aa8" dependencies = [ "backend", "constraints-folder", diff --git a/crates/air/src/lib.rs b/crates/air/src/lib.rs index 387e8cc7..fb02e7cd 100644 --- a/crates/air/src/lib.rs +++ b/crates/air/src/lib.rs @@ -15,36 +15,26 @@ mod verify; #[cfg(test)] pub mod tests; -pub trait NormalAir>>: +pub trait MyAir>>: Air>> + for<'a> Air, EF>> + for<'a> Air> + for<'a> Air, EF>> + for<'a> Air> -{ -} - -pub trait PackedAir>>: - for<'a> Air> + + for<'a> Air> + for<'a> Air> { } -impl NormalAir for A +impl MyAir for A where EF: ExtensionField>, A: Air>> + for<'a> Air, EF>> + for<'a> Air> + for<'a> Air, EF>> - + for<'a> Air>, -{ -} - -impl PackedAir for A -where - EF: ExtensionField>, - A: for<'a> Air> + + for<'a> Air> + + for<'a> Air> + for<'a> Air>, { } diff --git a/crates/air/src/prove.rs b/crates/air/src/prove.rs index b0650ea8..db1f18c3 100644 --- a/crates/air/src/prove.rs +++ b/crates/air/src/prove.rs @@ -9,7 +9,7 @@ use utils::{ FSProver, add_multilinears_inplace, fold_multilinear_chunks, multilinears_linear_combination, }; -use crate::{NormalAir, PackedAir}; +use crate::MyAir; use crate::{ uni_skip_utils::{matrix_down_folded, matrix_up_folded}, utils::{column_down, column_up, columns_up_and_down}, @@ -28,12 +28,11 @@ fn prove_air< 'a, WF: ExtensionField>, // witness field EF: ExtensionField> + ExtensionField, - A: NormalAir + 'static, - AP: PackedAir, + A: MyAir + 'static, >( prover_state: &mut FSProver>, univariate_skips: usize, - table: &AirTable, + table: &AirTable, witness: &[&'a [WF]], ) -> (MultilinearPoint, Vec) { let n_rows = witness[0].len(); @@ -76,11 +75,10 @@ fn prove_air< let columns_for_zero_check_packed = columns_for_zero_check.by_ref().pack(); let (outer_sumcheck_challenge, inner_sums, _) = info_span!("zerocheck").in_scope(|| { - sumcheck_prove::<_, _, _, _>( + sumcheck_prove( univariate_skips, columns_for_zero_check_packed, &table.air, - &table.air_packed, &constraints_batching_scalars, Some((zerocheck_challenges, None)), true, @@ -100,18 +98,11 @@ fn prove_air< &outer_sumcheck_challenge, ) } else { - open_unstructured_columns( - prover_state, - univariate_skips, - witness, - &outer_sumcheck_challenge, - ) + unreachable!() } } -impl>, A: NormalAir + 'static, AP: PackedAir> - AirTable -{ +impl>, A: MyAir + 'static> AirTable { #[instrument(name = "air: prove in base", skip_all)] pub fn prove_base( &self, @@ -119,7 +110,7 @@ impl>, A: NormalAir + 'static, AP: PackedAir> univariate_skips: usize, witness: &[&[PF]], ) -> (MultilinearPoint, Vec) { - prove_air::, EF, A, AP>(prover_state, univariate_skips, self, witness) + prove_air::, EF, A>(prover_state, univariate_skips, self, witness) } #[instrument(name = "air: prove in extension", skip_all)] @@ -129,55 +120,10 @@ impl>, A: NormalAir + 'static, AP: PackedAir> univariate_skips: usize, witness: &[&[EF]], ) -> (MultilinearPoint, Vec) { - prove_air::(prover_state, univariate_skips, self, witness) + prove_air::(prover_state, univariate_skips, self, witness) } } -#[instrument(skip_all)] -fn open_unstructured_columns< - WF: ExtensionField>, - EF: ExtensionField> + ExtensionField, ->( - prover_state: &mut FSProver>, - univariate_skips: usize, - witness: &[&[WF]], - outer_sumcheck_challenge: &[EF], -) -> (MultilinearPoint, Vec) { - let log_n_rows = log2_strict_usize(witness[0].len()); - - let columns_batching_scalars = prover_state.sample_vec(log2_ceil_usize(witness.len())); - - let batched_column = multilinears_linear_combination( - witness, - &eval_eq(&columns_batching_scalars)[..witness.len()], - ); - - // TODO opti - let sub_evals = fold_multilinear_chunks( - &batched_column, - &MultilinearPoint(outer_sumcheck_challenge[1..log_n_rows - univariate_skips + 1].to_vec()), - ); - - prover_state.add_extension_scalars(&sub_evals); - - let epsilons = MultilinearPoint(prover_state.sample_vec(univariate_skips)); - let common_point = MultilinearPoint( - [ - epsilons.0.clone(), - outer_sumcheck_challenge[1..log_n_rows - univariate_skips + 1].to_vec(), - ] - .concat(), - ); - - let evaluations_remaining_to_prove = witness - .iter() - .map(|col| col.evaluate(&common_point)) - .collect::>(); - prover_state.add_extension_scalars(&evaluations_remaining_to_prove); - - (common_point, evaluations_remaining_to_prove) -} - #[instrument(skip_all)] fn open_structured_columns> + ExtensionField, IF: Field>( prover_state: &mut FSProver>, @@ -238,11 +184,10 @@ fn open_structured_columns> + ExtensionField, IF: }); let (inner_challenges, _, _) = info_span!("structured columns sumcheck").in_scope(|| { - sumcheck_prove::( + sumcheck_prove::( 1, inner_mle, &ProductComputation, - &ProductComputation, &[], None, false, diff --git a/crates/air/src/table.rs b/crates/air/src/table.rs index c62b7da9..ed706929 100644 --- a/crates/air/src/table.rs +++ b/crates/air/src/table.rs @@ -9,19 +9,18 @@ use p3_uni_stark::get_symbolic_constraints; use tracing::instrument; use utils::ConstraintChecker; -use crate::{NormalAir, PackedAir}; +use crate::MyAir; #[derive(Debug)] -pub struct AirTable { +pub struct AirTable { pub air: A, - pub air_packed: AP, pub n_constraints: usize, _phantom: std::marker::PhantomData, } -impl>, A: NormalAir, AP: PackedAir> AirTable { - pub fn new(air: A, air_packed: AP) -> Self { +impl>, A: MyAir> AirTable { + pub fn new(air: A) -> Self { let symbolic_constraints = get_symbolic_constraints(&air, 0, 0); let n_constraints = symbolic_constraints.len(); let constraint_degree = @@ -29,7 +28,6 @@ impl>, A: NormalAir, AP: PackedAir> AirTable>>::degree(&air)); Self { air, - air_packed, n_constraints, _phantom: std::marker::PhantomData, } diff --git a/crates/air/src/tests.rs b/crates/air/src/tests.rs index d6cec491..0d931270 100644 --- a/crates/air/src/tests.rs +++ b/crates/air/src/tests.rs @@ -57,44 +57,6 @@ impl; - -impl BaseAir - for ExampleUnstructuredAir -{ - fn width(&self) -> usize { - N_COLUMNS - } - fn structured(&self) -> bool { - false - } - fn degree(&self) -> usize { - N_PREPROCESSED_COLUMNS - } -} - -impl Air - for ExampleUnstructuredAir -{ - #[inline] - fn eval(&self, builder: &mut AB) { - let main = builder.main(); - let up = main.row_slice(0).expect("The matrix is empty?"); - let up: &[AB::Var] = (*up).borrow(); - assert_eq!(up.len(), N_COLUMNS); - - for j in N_PREPROCESSED_COLUMNS..N_COLUMNS { - builder.assert_eq( - up[j].clone(), - (0..N_PREPROCESSED_COLUMNS) - .map(|k| AB::Expr::from(up[k].clone())) - .product::() - + AB::F::from_usize(j), - ); - } - } -} - fn generate_structured_trace( log_length: usize, ) -> Vec> { @@ -125,47 +87,6 @@ fn generate_structured_trace( - log_length: usize, -) -> Vec> { - let n_rows = 1 << log_length; - let mut trace = vec![]; - let mut rng = StdRng::seed_from_u64(0); - for _ in 0..N_PREPROCESSED_COLUMNS { - trace.push((0..n_rows).map(|_| rng.random()).collect::>()); - } - let mut witness_cols = vec![vec![]; N_COLUMNS - N_PREPROCESSED_COLUMNS]; - let mut column_iters = trace[..N_PREPROCESSED_COLUMNS] - .iter() - .map(|col| col.iter()) - .collect::>(); - if column_iters.is_empty() { - trace.extend(witness_cols); - return trace; - } - loop { - let mut row_product = F::ONE; - let mut progressed = true; - for iter in &mut column_iters { - match iter.next() { - Some(value) => row_product *= *value, - None => { - progressed = false; - break; - } - } - } - if !progressed { - break; - } - for (j, witness_col) in witness_cols.iter_mut().enumerate() { - witness_col.push(F::from_usize(j + N_PREPROCESSED_COLUMNS) + row_product); - } - } - trace.extend(witness_cols); - trace -} - #[test] fn test_structured_air() { const N_COLUMNS: usize = 17; @@ -176,44 +97,7 @@ fn test_structured_air() { let columns = generate_structured_trace::(log_n_rows); let columns_ref = columns.iter().map(|col| col.as_slice()).collect::>(); - let table = AirTable::::new( - ExampleStructuredAir::, - ExampleStructuredAir::, - ); - table.check_trace_validity(&columns_ref).unwrap(); - let (point_prover, evaluations_remaining_to_prove) = - table.prove_base(&mut prover_state, UNIVARIATE_SKIPS, &columns_ref); - let mut verifier_state = build_verifier_state(&prover_state); - let (point_verifier, evaluations_remaining_to_verify) = table - .verify(&mut verifier_state, UNIVARIATE_SKIPS, log_n_rows) - .unwrap(); - assert_eq!(point_prover, point_verifier); - assert_eq!( - &evaluations_remaining_to_prove, - &evaluations_remaining_to_verify - ); - for i in 0..N_COLUMNS { - assert_eq!( - columns[i].evaluate(&point_prover), - evaluations_remaining_to_verify[i] - ); - } -} - -#[test] -fn test_unstructured_air() { - const N_COLUMNS: usize = 18; - const N_PREPROCESSED_COLUMNS: usize = 5; - let log_n_rows = 12; - let mut prover_state = build_prover_state::(); - - let columns = generate_unstructured_trace::(log_n_rows); - let columns_ref = columns.iter().map(|col| col.as_slice()).collect::>(); - - let table = AirTable::::new( - ExampleUnstructuredAir::, - ExampleUnstructuredAir::, - ); + let table = AirTable::::new(ExampleStructuredAir::); table.check_trace_validity(&columns_ref).unwrap(); let (point_prover, evaluations_remaining_to_prove) = table.prove_base(&mut prover_state, UNIVARIATE_SKIPS, &columns_ref); diff --git a/crates/air/src/verify.rs b/crates/air/src/verify.rs index 054d9654..5b2b2ef0 100644 --- a/crates/air/src/verify.rs +++ b/crates/air/src/verify.rs @@ -3,14 +3,16 @@ use p3_air::BaseAir; use p3_field::{ExtensionField, cyclic_subgroup_known_order, dot_product}; use p3_util::log2_ceil_usize; -use crate::utils::{matrix_down_lde, matrix_up_lde}; -use crate::{NormalAir, PackedAir}; +use crate::{ + MyAir, + utils::{matrix_down_lde, matrix_up_lde}, +}; use super::table::AirTable; -fn verify_air>, A: NormalAir, AP: PackedAir>( +fn verify_air>, A: MyAir>( verifier_state: &mut FSVerifier>, - table: &AirTable, + table: &AirTable, univariate_skips: usize, log_n_rows: usize, ) -> Result<(MultilinearPoint, Vec), ProofError> { @@ -85,14 +87,14 @@ fn verify_air>, A: NormalAir, AP: PackedAir>( } } -impl>, A: NormalAir, AP: PackedAir> AirTable { +impl>, A: MyAir> AirTable { pub fn verify( &self, verifier_state: &mut FSVerifier>, univariate_skips: usize, log_n_rows: usize, ) -> Result<(MultilinearPoint, Vec), ProofError> { - verify_air::(verifier_state, self, univariate_skips, log_n_rows) + verify_air::(verifier_state, self, univariate_skips, log_n_rows) } } diff --git a/crates/lean_prover/src/common.rs b/crates/lean_prover/src/common.rs index dbc95adc..875441c9 100644 --- a/crates/lean_prover/src/common.rs +++ b/crates/lean_prover/src/common.rs @@ -19,12 +19,14 @@ pub fn get_base_dims( bytecode_ending_pc: usize, poseidon_counts: (usize, usize), n_rows_table_dot_products: usize, - p16_gkr_layers: &PoseidonGKRLayers<16, N_COMMITED_CUBES_P16>, - p24_gkr_layers: &PoseidonGKRLayers<24, N_COMMITED_CUBES_P24>, + (p16_gkr_layers, p24_gkr_layers): ( + &PoseidonGKRLayers<16, N_COMMITED_CUBES_P16>, + &PoseidonGKRLayers<24, N_COMMITED_CUBES_P24>, + ), ) -> Vec> { let (n_poseidons_16, n_poseidons_24) = poseidon_counts; - let p16_default_cubes = default_cube_layers::(&p16_gkr_layers); - let p24_default_cubes = default_cube_layers::(&p24_gkr_layers); + let p16_default_cubes = default_cube_layers::(p16_gkr_layers); + let p24_default_cubes = default_cube_layers::(p24_gkr_layers); [ vec![ @@ -306,10 +308,10 @@ impl SumcheckComputationPacked for DotProductFootprint { pub fn get_poseidon_lookup_statements( (log_n_p16, log_n_p24): (usize, usize), - (p16_input_point, p16_input_evals): &(MultilinearPoint, Vec), - (p16_output_point, p16_output_evals): &(MultilinearPoint, Vec), - (p24_input_point, p24_input_evals): &(MultilinearPoint, Vec), - (p24_output_point, p24_output_evals): &(MultilinearPoint, Vec), + (p16_input_point, p16_input_evals): &(MultilinearPoint, [EF; 16]), + (p16_output_point, p16_output_evals): &(MultilinearPoint, [EF; 16]), + (p24_input_point, p24_input_evals): &(MultilinearPoint, [EF; 24]), + (p24_output_point, p24_output_evals): &(MultilinearPoint, [EF; 24]), memory_folding_challenges: &MultilinearPoint, ) -> Vec> { let p16_folded_eval_addr_a = (&p16_input_evals[..8]).evaluate(memory_folding_challenges); diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index 3a30ebaf..43700333 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -80,7 +80,7 @@ pub fn prove_execution( .map(Vec::as_slice) .collect::>(), ); - let exec_table = AirTable::::new(VMAir, VMAir); + let exec_table = AirTable::::new(VMAir); let _validity_proof_span = info_span!("Validity proof generation").entered(); @@ -91,7 +91,7 @@ pub fn prove_execution( generate_poseidon_witness_helper(&p16_gkr_layers, &poseidons_16, Some(n_compressions_16)); let p24_witness = generate_poseidon_witness_helper(&p24_gkr_layers, &poseidons_24, None); - let dot_product_table = AirTable::::new(DotProductAir, DotProductAir); + let dot_product_table = AirTable::::new(DotProductAir); let (dot_product_columns, dot_product_padding_len) = build_dot_product_columns(&dot_products); @@ -154,8 +154,7 @@ pub fn prove_execution( bytecode.ending_pc, (n_poseidons_16, n_poseidons_24), n_rows_table_dot_products, - &p16_gkr_layers, - &p24_gkr_layers, + (&p16_gkr_layers, &p24_gkr_layers), ); let dot_product_col_index_a = field_slice_as_base(&dot_product_columns[2]).unwrap(); @@ -418,7 +417,6 @@ pub fn prove_execution( .collect::>(), ), // we do not use packing here because it's slower in practice (this sumcheck is small) &dot_product_footprint_computation, - &dot_product_footprint_computation, &[], Some((grand_product_dot_product_statement.point.0.clone(), None)), false, @@ -475,7 +473,6 @@ pub fn prove_execution( ) .pack(), &precompile_foot_print_computation, - &precompile_foot_print_computation, &[], Some((grand_product_exec_statement.point.0.clone(), None)), false, @@ -529,38 +526,40 @@ pub fn prove_execution( }); let random_point_p16 = MultilinearPoint(prover_state.sample_vec(log_n_p16)); - let (p16_output_values, p16_input_statements, p16_cubes_statements) = prove_poseidon_gkr( + let p16_gkr = prove_poseidon_gkr( &mut prover_state, &p16_witness, random_point_p16.0.clone(), UNIVARIATE_SKIPS, &p16_gkr_layers, ); - let p16_cubes_statements = p16_cubes_statements + let p16_cubes_statements = p16_gkr + .cubes_statements .1 .iter() .map(|&e| { vec![Evaluation { - point: p16_cubes_statements.0.clone(), + point: p16_gkr.cubes_statements.0.clone(), value: e, }] }) .collect::>(); let random_point_p24 = MultilinearPoint(prover_state.sample_vec(log_n_p24)); - let (p24_output_values, p24_input_statements, p24_cubes_statements) = prove_poseidon_gkr( + let p24_gkr = prove_poseidon_gkr( &mut prover_state, &p24_witness, random_point_p24.0.clone(), UNIVARIATE_SKIPS, &p24_gkr_layers, ); - let p24_cubes_statements = p24_cubes_statements + let p24_cubes_statements = p24_gkr + .cubes_statements .1 .iter() .map(|&e| { vec![Evaluation { - point: p24_cubes_statements.0.clone(), + point: p24_gkr.cubes_statements.0.clone(), value: e, }] }) @@ -572,10 +571,10 @@ pub fn prove_execution( let poseidon_lookup_statements = get_poseidon_lookup_statements( (log_n_p16, log_n_p24), - &p16_input_statements, - &(random_point_p16.clone(), p16_output_values.to_vec()), - &p24_input_statements, - &(random_point_p24.clone(), p24_output_values.to_vec()), + &p16_gkr.input_statements, + &(random_point_p16.clone(), p16_gkr.output_values), + &p24_gkr.input_statements, + &(random_point_p24.clone(), p24_gkr.output_values), &memory_folding_challenges, ); @@ -942,7 +941,6 @@ pub fn prove_execution( 1, // TODO univariate skip MleGroupRef::BasePacked(vec![&p16_one_minus_compression, &p16_index_res_a_plus_one]), &ProductComputation, - &ProductComputation, &[], Some(( poseidon_logup_star_statements.on_indexes.point[3..].to_vec(), diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index 7ee76013..53668a7f 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -27,11 +27,11 @@ pub fn verify_execution( ) -> Result<(), ProofError> { let mut verifier_state = VerifierState::new(proof_data, build_challenger()); - let exec_table = AirTable::::new(VMAir, VMAir); + let exec_table = AirTable::::new(VMAir); let p16_gkr_layers = PoseidonGKRLayers::<16, N_COMMITED_CUBES_P16>::build(Some(VECTOR_LEN)); let p24_gkr_layers = PoseidonGKRLayers::<24, N_COMMITED_CUBES_P24>::build(None); - let dot_product_table = AirTable::::new(DotProductAir, DotProductAir); + let dot_product_table = AirTable::::new(DotProductAir); let [ log_n_cycles, @@ -112,8 +112,7 @@ pub fn verify_execution( bytecode.ending_pc, (n_poseidons_16, n_poseidons_24), n_rows_table_dot_products, - &p16_gkr_layers, - &p24_gkr_layers, + (&p16_gkr_layers, &p24_gkr_layers), ); let parsed_commitment_base = packed_pcs_parse_commitment( @@ -351,7 +350,7 @@ pub fn verify_execution( dot_product_table.verify(&mut verifier_state, 1, table_dot_products_log_n_rows)?; let random_point_p16 = MultilinearPoint(verifier_state.sample_vec(log_n_p16)); - let (p16_output_values, p16_input_statements, p16_cubes_statements) = verify_poseidon_gkr( + let gkr_16 = verify_poseidon_gkr( &mut verifier_state, log_n_p16, &random_point_p16, @@ -359,19 +358,20 @@ pub fn verify_execution( UNIVARIATE_SKIPS, Some(n_compressions_16), ); - let p16_cubes_statements = p16_cubes_statements + let p16_cubes_statements = gkr_16 + .cubes_statements .1 .iter() .map(|&e| { vec![Evaluation { - point: p16_cubes_statements.0.clone(), + point: gkr_16.cubes_statements.0.clone(), value: e, }] }) .collect::>(); let random_point_p24 = MultilinearPoint(verifier_state.sample_vec(log_n_p24)); - let (p24_output_values, p24_input_statements, p24_cubes_statements) = verify_poseidon_gkr( + let gkr_24 = verify_poseidon_gkr( &mut verifier_state, log_n_p24, &random_point_p24, @@ -379,12 +379,13 @@ pub fn verify_execution( UNIVARIATE_SKIPS, None, ); - let p24_cubes_statements = p24_cubes_statements + let p24_cubes_statements = gkr_24 + .cubes_statements .1 .iter() .map(|&e| { vec![Evaluation { - point: p24_cubes_statements.0.clone(), + point: gkr_24.cubes_statements.0.clone(), value: e, }] }) @@ -539,10 +540,10 @@ pub fn verify_execution( let poseidon_lookup_statements = get_poseidon_lookup_statements( (log_n_p16, log_n_p24), - &p16_input_statements, - &(random_point_p16.clone(), p16_output_values.to_vec()), - &p24_input_statements, - &(random_point_p24.clone(), p24_output_values.to_vec()), + &gkr_16.input_statements, + &(random_point_p16.clone(), gkr_16.output_values), + &gkr_24.input_statements, + &(random_point_p24.clone(), gkr_24.output_values), &memory_folding_challenges, ); diff --git a/crates/lean_prover/witness_generation/src/poseidon_tables.rs b/crates/lean_prover/witness_generation/src/poseidon_tables.rs index 5bcac822..245ee343 100644 --- a/crates/lean_prover/witness_generation/src/poseidon_tables.rs +++ b/crates/lean_prover/witness_generation/src/poseidon_tables.rs @@ -111,7 +111,7 @@ where array::from_fn(|i| PFPacking::::pack_slice(&inputs_transposed[i]).to_vec()); // TODO avoid cloning generate_poseidon_witness::, WIDTH, N_COMMITED_CUBES>( inputs_transposed_packed, - &layers, + layers, n_compressions.map(|n_compressions| { ( n_compressions, diff --git a/crates/lean_vm/src/witness/poseidon16.rs b/crates/lean_vm/src/witness/poseidon16.rs index 37f75eb2..d223995d 100644 --- a/crates/lean_vm/src/witness/poseidon16.rs +++ b/crates/lean_vm/src/witness/poseidon16.rs @@ -1,6 +1,9 @@ //! Poseidon2 hash witness for 16-element input -use crate::{core::{F, POSEIDON_16_NULL_HASH_PTR, ZERO_VEC_PTR}, PoseidonWitnessTrait}; +use crate::{ + PoseidonWitnessTrait, + core::{F, POSEIDON_16_NULL_HASH_PTR, ZERO_VEC_PTR}, +}; use p3_field::PrimeCharacteristicRing; pub const POSEIDON_16_DEFAULT_COMPRESSION: bool = true; diff --git a/crates/lean_vm/src/witness/poseidon24.rs b/crates/lean_vm/src/witness/poseidon24.rs index 4fa06e3a..4912f311 100644 --- a/crates/lean_vm/src/witness/poseidon24.rs +++ b/crates/lean_vm/src/witness/poseidon24.rs @@ -1,6 +1,9 @@ //! Poseidon2 hash witness for 24-element input -use crate::{core::{F, POSEIDON_24_NULL_HASH_PTR, ZERO_VEC_PTR}, PoseidonWitnessTrait}; +use crate::{ + PoseidonWitnessTrait, + core::{F, POSEIDON_24_NULL_HASH_PTR, ZERO_VEC_PTR}, +}; use p3_field::PrimeCharacteristicRing; /// Witness data for Poseidon2 over 24 field elements diff --git a/crates/lookup/src/product_gkr.rs b/crates/lookup/src/product_gkr.rs index d31960df..04294797 100644 --- a/crates/lookup/src/product_gkr.rs +++ b/crates/lookup/src/product_gkr.rs @@ -119,11 +119,10 @@ where EF: ExtensionField>, PF: PrimeField64, { - let (sc_point, inner_evals, _) = sumcheck_prove::( + let (sc_point, inner_evals, _) = sumcheck_prove::( 1, up_layer, &ProductComputation, - &ProductComputation, &[], Some((claim.point.0.clone(), None)), false, diff --git a/crates/lookup/src/quotient_gkr.rs b/crates/lookup/src/quotient_gkr.rs index 83fe6008..6979150c 100644 --- a/crates/lookup/src/quotient_gkr.rs +++ b/crates/lookup/src/quotient_gkr.rs @@ -166,11 +166,10 @@ where vec![u0_folded[0], u1_folded[0], u2_folded[0], u3_folded[0]], ) } else { - let (mut sc_point, inner_evals, _) = sumcheck_prove::( + let (mut sc_point, inner_evals, _) = sumcheck_prove::( 1, MleGroupRef::Extension(vec![u0_folded, u1_folded, u2_folded, u3_folded]), &GKRQuotientComputation { u4_const, u5_const }, - &GKRQuotientComputation { u4_const, u5_const }, &[], Some((claim.point.0[1..].to_vec(), None)), false, @@ -409,7 +408,7 @@ where .map(|(&l, &r)| c_packed - l + sumcheck_challenge_2_packed * (l - r)) .collect(); - let (mut sc_point, quarter_evals, _) = sumcheck_fold_and_prove::( + let (mut sc_point, quarter_evals, _) = sumcheck_fold_and_prove::( 1, MleGroupOwned::ExtensionPacked(vec![ u0_folded_packed, @@ -419,7 +418,6 @@ where ]), None, &GKRQuotientComputation { u4_const, u5_const }, - &GKRQuotientComputation { u4_const, u5_const }, &[], Some(( claim.point.0[2..].to_vec(), @@ -620,7 +618,7 @@ where + claim.point[1] * sumcheck_challenge_2) / (EF::ONE - claim.point.get(2).copied().unwrap_or_default()); - let (mut sc_point, quarter_evals, _) = sumcheck_fold_and_prove::( + let (mut sc_point, quarter_evals, _) = sumcheck_fold_and_prove::( 1, MleGroupRef::ExtensionPacked(vec![ u0_folded_packed, @@ -630,7 +628,6 @@ where ]), Some(vec![EF::ONE - sumcheck_challenge_2, sumcheck_challenge_2]), &GKRQuotientComputation { u4_const, u5_const }, - &GKRQuotientComputation { u4_const, u5_const }, &[], Some(( claim.point.0[2..].to_vec(), diff --git a/crates/poseidon_circuit/src/gkr_layers/partial_round.rs b/crates/poseidon_circuit/src/gkr_layers/partial_round.rs index 7cdd6a55..aed313af 100644 --- a/crates/poseidon_circuit/src/gkr_layers/partial_round.rs +++ b/crates/poseidon_circuit/src/gkr_layers/partial_round.rs @@ -28,9 +28,7 @@ where let first_cubed = (point[0] + self.constant).cube(); let mut buff = [NF::ZERO; WIDTH]; buff[0] = first_cubed; - for j in 1..WIDTH { - buff[j] = point[j]; - } + buff[1..WIDTH].copy_from_slice(&point[1..WIDTH]); GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); let mut res = EF::ZERO; for i in 0..WIDTH { @@ -53,9 +51,7 @@ where let first_cubed = (point[0] + self.constant).cube(); let mut buff = [PFPacking::::ZERO; WIDTH]; buff[0] = first_cubed; - for j in 1..WIDTH { - buff[j] = point[j]; - } + buff[1..WIDTH].copy_from_slice(&point[1..WIDTH]); GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; for j in 0..WIDTH { @@ -69,9 +65,7 @@ where let first_cubed = (point[0] + PFPacking::::from(self.constant)).cube(); let mut buff = [EFPacking::::ZERO; WIDTH]; buff[0] = first_cubed; - for j in 1..WIDTH { - buff[j] = point[j]; - } + buff[1..WIDTH].copy_from_slice(&point[1..WIDTH]); GenericPoseidon2LinearLayersKoalaBear::internal_linear_layer(&mut buff); let mut res = EFPacking::::ZERO; for j in 0..WIDTH { diff --git a/crates/poseidon_circuit/src/lib.rs b/crates/poseidon_circuit/src/lib.rs index 8c116be7..dc0d36f0 100644 --- a/crates/poseidon_circuit/src/lib.rs +++ b/crates/poseidon_circuit/src/lib.rs @@ -1,5 +1,6 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] +use multilinear_toolkit::prelude::MultilinearPoint; use p3_koala_bear::{KoalaBear, QuinticExtensionFieldKB}; mod prove; @@ -20,3 +21,9 @@ pub use gkr_layers::*; pub(crate) type F = KoalaBear; pub(crate) type EF = QuinticExtensionFieldKB; +#[derive(Debug, Clone)] +pub struct GKRPoseidonResult { + pub output_values: [EF; WIDTH], + pub input_statements: (MultilinearPoint, [EF; WIDTH]), // remain to be proven + pub cubes_statements: (MultilinearPoint, [EF; N_COMMITED_CUBES]), // remain to be proven +} diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index f485ee59..4b716208 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -1,3 +1,4 @@ +use crate::GKRPoseidonResult; use crate::{ EF, F, PoseidonWitness, gkr_layers::{BatchPartialRounds, PoseidonGKRLayers}, @@ -14,11 +15,7 @@ pub fn prove_poseidon_gkr( mut claim_point: Vec, univariate_skips: usize, layers: &PoseidonGKRLayers, -) -> ( - [EF; WIDTH], - (MultilinearPoint, Vec), - (MultilinearPoint, Vec), -) +) -> GKRPoseidonResult where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { @@ -143,24 +140,24 @@ where ); let pcs_point_for_inputs = claim_point.clone(); - let input_pcs_statements = inner_evals_on_commited_columns( + let input_statements = inner_evals_on_commited_columns( prover_state, &pcs_point_for_inputs, univariate_skips, &witness.input_layer, ); - let cubes_pcs_statements = inner_evals_on_commited_columns( + let cubes_statements = inner_evals_on_commited_columns( prover_state, &pcs_point_for_cubes, univariate_skips, &witness.committed_cubes, ); - ( - output_claims.try_into().unwrap(), - input_pcs_statements, - cubes_pcs_statements, - ) + GKRPoseidonResult { + output_values: output_claims.try_into().unwrap(), + input_statements, + cubes_statements, + } } // #[instrument(skip_all)] @@ -188,7 +185,6 @@ fn prove_gkr_round< univariate_skips, MleGroupRef::BasePacked(input_layers.iter().map(|l| l.as_ref()).collect()), computation, - computation, &batching_scalars_powers, Some((claim_point.to_vec(), None)), false, @@ -200,7 +196,7 @@ fn prove_gkr_round< // sanity check debug_assert_eq!( computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) - * eq_poly_with_skip(&sumcheck_point, &claim_point, univariate_skips), + * eq_poly_with_skip(&sumcheck_point, claim_point, univariate_skips), sumcheck_final_sum ); @@ -232,7 +228,7 @@ where .iter() .map(|l| PFPacking::::unpack_slice(l)) .collect::>(), - &claim_point, + claim_point, selectors, ) }); @@ -256,7 +252,6 @@ where .collect(), ), computation, - computation, &batching_scalars_powers, Some((claim_point.to_vec(), None)), false, @@ -268,7 +263,7 @@ where // sanity check debug_assert_eq!( computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) - * eq_poly_with_skip(&sumcheck_point, &claim_point, univariate_skips), + * eq_poly_with_skip(&sumcheck_point, claim_point, univariate_skips), sumcheck_final_sum ); @@ -277,12 +272,12 @@ where (sumcheck_point.0, sumcheck_inner_evals) } -fn inner_evals_on_commited_columns( +fn inner_evals_on_commited_columns( prover_state: &mut FSProver>, point: &[EF], univariate_skips: usize, - columns: &[Vec>], -) -> (MultilinearPoint, Vec) { + columns: &[Vec>; N], +) -> (MultilinearPoint, [EF; N]) { let eq_mle = eval_eq_packed(&point[1..]); let inner_evals = columns .par_iter() @@ -309,5 +304,5 @@ fn inner_evals_on_commited_columns( values_to_prove .push(col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone()))); } - (point_to_prove, values_to_prove) + (point_to_prove, values_to_prove.try_into().unwrap()) } diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index bff1365c..8da292e4 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -17,8 +17,8 @@ use whir_p3::{ }; use crate::{ - default_cube_layers, generate_poseidon_witness, gkr_layers::PoseidonGKRLayers, - prove_poseidon_gkr, verify_poseidon_gkr, + GKRPoseidonResult, default_cube_layers, generate_poseidon_witness, + gkr_layers::PoseidonGKRLayers, prove_poseidon_gkr, verify_poseidon_gkr, }; type F = KoalaBear; @@ -63,7 +63,7 @@ fn test_prove_poseidons() { array::from_fn(|i| PFPacking::::pack_slice(&input[i]).to_vec()); let layers = PoseidonGKRLayers::::build( - COMPRESS.then(|| COMPRESSION_OUTPUT_WIDTH), + COMPRESS.then_some(COMPRESSION_OUTPUT_WIDTH), ); let default_cubes = default_cube_layers::(&layers); @@ -127,7 +127,11 @@ fn test_prove_poseidons() { let claim_point = prover_state.sample_vec(log_n_poseidons); - let (output_values, input_pcs_statements, cubes_pcs_statements) = prove_poseidon_gkr( + let GKRPoseidonResult { + output_values, + input_statements, + cubes_statements, + } = prove_poseidon_gkr( &mut prover_state, &witness, claim_point.clone(), @@ -137,7 +141,7 @@ fn test_prove_poseidons() { // PCS opening let mut pcs_statements = vec![]; - for (point_to_prove, evals_to_prove) in [input_pcs_statements, cubes_pcs_statements] { + for (point_to_prove, evals_to_prove) in [input_statements, cubes_statements] { for v in evals_to_prove { pcs_statements.push(vec![Evaluation { point: point_to_prove.clone(), @@ -187,7 +191,11 @@ fn test_prove_poseidons() { let output_claim_point = verifier_state.sample_vec(log_n_poseidons); - let (output_values, input_pcs_statements, cubes_pcs_statements) = verify_poseidon_gkr( + let GKRPoseidonResult { + output_values, + input_statements, + cubes_statements, + } = verify_poseidon_gkr( &mut verifier_state, log_n_poseidons, &output_claim_point, @@ -198,7 +206,7 @@ fn test_prove_poseidons() { // PCS verification let mut pcs_statements = vec![]; - for (point_to_verif, evals_to_verif) in [input_pcs_statements, cubes_pcs_statements] { + for (point_to_verif, evals_to_verif) in [input_statements, cubes_statements] { for v in evals_to_verif { pcs_statements.push(vec![Evaluation { point: point_to_verif.clone(), @@ -255,7 +263,7 @@ fn test_prove_poseidons() { .enumerate() .take(COMPRESSION_OUTPUT_WIDTH) .for_each(|(i, layer)| { - assert_eq!(PFPacking::::unpack_slice(&layer), data_to_hash[i]); + assert_eq!(PFPacking::::unpack_slice(layer), data_to_hash[i]); }); output_layer .iter() @@ -263,18 +271,18 @@ fn test_prove_poseidons() { .skip(COMPRESSION_OUTPUT_WIDTH) .for_each(|(i, layer)| { assert_eq!( - &PFPacking::::unpack_slice(&layer)[..n_poseidons - n_compressions], + &PFPacking::::unpack_slice(layer)[..n_poseidons - n_compressions], &data_to_hash[i][..n_poseidons - n_compressions] ); assert!( - PFPacking::::unpack_slice(&layer)[n_poseidons - n_compressions..] + PFPacking::::unpack_slice(layer)[n_poseidons - n_compressions..] .iter() .all(|&x| x.is_zero()) ); }); } else { output_layer.iter().enumerate().for_each(|(i, layer)| { - assert_eq!(PFPacking::::unpack_slice(&layer), data_to_hash[i]); + assert_eq!(PFPacking::::unpack_slice(layer), data_to_hash[i]); }); } assert_eq!(output_values_verifier, output_values_prover); @@ -282,7 +290,7 @@ fn test_prove_poseidons() { output_values_verifier.as_slice(), &output_layer .iter() - .map(|layer| PFPacking::::unpack_slice(&layer) + .map(|layer| PFPacking::::unpack_slice(layer) .evaluate(&MultilinearPoint(claim_point.clone()))) .collect::>() ); diff --git a/crates/poseidon_circuit/src/verify.rs b/crates/poseidon_circuit/src/verify.rs index bbb1fece..0946a64a 100644 --- a/crates/poseidon_circuit/src/verify.rs +++ b/crates/poseidon_circuit/src/verify.rs @@ -2,7 +2,7 @@ use multilinear_toolkit::prelude::*; use p3_koala_bear::{KoalaBearInternalLayerParameters, KoalaBearParameters}; use p3_monty_31::InternalLayerBaseParameters; -use crate::{EF, F, gkr_layers::PoseidonGKRLayers}; +use crate::{EF, F, GKRPoseidonResult, gkr_layers::PoseidonGKRLayers}; pub fn verify_poseidon_gkr( verifier_state: &mut FSVerifier>, @@ -11,11 +11,7 @@ pub fn verify_poseidon_gkr( layers: &PoseidonGKRLayers, univariate_skips: usize, n_compressions: Option, -) -> ( - [EF; WIDTH], - (MultilinearPoint, Vec), - (MultilinearPoint, Vec), -) +) -> GKRPoseidonResult where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { @@ -106,7 +102,7 @@ where ); let pcs_point_for_cubes = claim_point.clone(); - let pcs_evals_for_cubes = claims[WIDTH..].to_vec(); + let pcs_evals_for_cubes = claims[WIDTH..].try_into().unwrap(); claims = claims[..WIDTH].to_vec(); @@ -132,27 +128,27 @@ where ); let pcs_point_for_inputs = claim_point.clone(); - let pcs_evals_for_inputs = claims.to_vec(); + let pcs_evals_for_inputs = claims.try_into().unwrap(); - let input_pcs_statements = verify_inner_evals_on_commited_columns( + let input_statements = verify_inner_evals_on_commited_columns( verifier_state, &pcs_point_for_inputs, &pcs_evals_for_inputs, &selectors, ); - let cubes_pcs_statements = verify_inner_evals_on_commited_columns( + let cubes_statements = verify_inner_evals_on_commited_columns( verifier_state, &pcs_point_for_cubes, &pcs_evals_for_cubes, &selectors, ); - ( - output_claims.try_into().unwrap(), - input_pcs_statements, - cubes_pcs_statements, - ) + GKRPoseidonResult { + output_values: output_claims.try_into().unwrap(), + input_statements, + cubes_statements, + } } fn verify_gkr_round>( @@ -183,7 +179,7 @@ fn verify_gkr_round>( computation.eval(&sumcheck_inner_evals, &batching_scalars_powers) * eq_poly_with_skip( &sumcheck_postponed_claim.point, - &claim_point, + claim_point, univariate_skips ), sumcheck_postponed_claim.value @@ -192,12 +188,12 @@ fn verify_gkr_round>( (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) } -fn verify_inner_evals_on_commited_columns( +fn verify_inner_evals_on_commited_columns( verifier_state: &mut FSVerifier>, point: &[EF], - claimed_evals: &[EF], + claimed_evals: &[EF; N], selectors: &[DensePolynomial], -) -> (MultilinearPoint, Vec) { +) -> (MultilinearPoint, [EF; N]) { let univariate_skips = log2_strict_usize(selectors.len()); let inner_evals_inputs = verifier_state .next_extension_scalars_vec(claimed_evals.len() << univariate_skips) @@ -215,12 +211,12 @@ fn verify_inner_evals_on_commited_columns( evaluate_univariate_multilinear::<_, _, _, false>( col_inner_evals, &point[..1], - &selectors, + selectors, None ) ); values_to_verif .push(col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone()))); } - (point_to_verif, values_to_verif) + (point_to_verif, values_to_verif.try_into().unwrap()) } diff --git a/crates/utils/src/poseidon2.rs b/crates/utils/src/poseidon2.rs index dc6b6512..4214c8e8 100644 --- a/crates/utils/src/poseidon2.rs +++ b/crates/utils/src/poseidon2.rs @@ -96,4 +96,3 @@ pub fn build_poseidon24_constants() -> MyRoundConstants24 { ending_full_round_constants: KOALABEAR_RC24_EXTERNAL_FINAL, } } - diff --git a/src/main.rs b/src/main.rs index f328e4d9..e7a11a96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1 +1,3 @@ -fn main() {} +fn main() { + println!("Hello, world!"); +} From d035127cff30911f5e6a77eb7ea8a698034163d4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 27 Oct 2025 13:02:34 +0400 Subject: [PATCH 39/42] fix whir --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 8258273f..d9271cec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1404,7 +1404,7 @@ dependencies = [ [[package]] name = "whir-p3" version = "0.1.0" -source = "git+https://github.com/TomWambsgans/whir-p3?branch=lean-multisig#61c58fc683faa20bf3e4ac6d5611705f9db3806d" +source = "git+https://github.com/TomWambsgans/whir-p3?branch=lean-multisig#57c8d12015260a6aca706a021364225e5ebdd636" dependencies = [ "itertools 0.14.0", "multilinear-toolkit", From ed2df458475d388b8c317d10a88adc842a362a6c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 27 Oct 2025 16:11:58 +0400 Subject: [PATCH 40/42] Proving_Poseidons_with_GKR.pdf + reset benchmarks + cleanup --- Cargo.lock | 362 ++-- Cargo.toml | 31 +- README.md | 39 +- benches/recursion.rs | 23 - benches/xmss.rs | 26 - crates/lean_compiler/src/lib.rs | 8 +- crates/lean_prover/src/common.rs | 8 +- crates/lean_prover/src/prove_execution.rs | 9 +- crates/lean_prover/tests/hash_chain.rs | 2 +- crates/lean_vm/src/diagnostics/error.rs | 1 + crates/lean_vm/src/diagnostics/profiler.rs | 5 +- crates/lean_vm/src/execution/runner.rs | 169 +- crates/lean_vm/tests/test_lean_vm.rs | 3 +- crates/poseidon_circuit/Cargo.toml | 4 +- crates/poseidon_circuit/src/lib.rs | 11 +- crates/poseidon_circuit/src/prove.rs | 16 +- crates/poseidon_circuit/src/tests.rs | 22 +- crates/poseidon_circuit/src/verify.rs | 16 +- crates/rec_aggregation/src/lib.rs | 3 - crates/rec_aggregation/src/recursion.rs | 34 +- crates/rec_aggregation/src/xmss_aggregate.rs | 44 +- docs/Proving_Poseidons_with_GKR.pdf | Bin 0 -> 158309 bytes docs/Sumcheck_with_low_memory_throughput.pdf | Bin 144954 -> 0 bytes Whirlaway.pdf => docs/Whirlaway.pdf | Bin .../xmss => docs}/XMSS_trivial_encoding.pdf | Bin .../benchmark_graphs/graphs/raw_poseidons.svg | 1032 +++++------ .../graphs/recursive_whir_opening.svg | 884 +++++----- ...ggregated_time.svg => xmss_aggregated.svg} | 1531 ++++++++--------- .../graphs/xmss_aggregated_overhead.svg | 1334 -------------- docs/benchmark_graphs/main.py | 163 +- .../lookup => docs}/cost_of_logup_star.pdf | Bin docs/whirlaway_pdf/bibliography.bib | 41 - docs/whirlaway_pdf/main.tex | 729 -------- src/main.rs | 40 +- 34 files changed, 2172 insertions(+), 4418 deletions(-) delete mode 100644 benches/recursion.rs delete mode 100644 benches/xmss.rs create mode 100644 docs/Proving_Poseidons_with_GKR.pdf delete mode 100644 docs/Sumcheck_with_low_memory_throughput.pdf rename Whirlaway.pdf => docs/Whirlaway.pdf (100%) rename {crates/xmss => docs}/XMSS_trivial_encoding.pdf (100%) rename docs/benchmark_graphs/graphs/{xmss_aggregated_time.svg => xmss_aggregated.svg} (51%) delete mode 100644 docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg rename {crates/lookup => docs}/cost_of_logup_star.pdf (100%) delete mode 100644 docs/whirlaway_pdf/bibliography.bib delete mode 100644 docs/whirlaway_pdf/main.tex diff --git a/Cargo.lock b/Cargo.lock index d9271cec..3511ba66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,12 +27,6 @@ dependencies = [ "utils", ] -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "ansi_term" version = "0.12.1" @@ -42,12 +36,56 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -60,7 +98,7 @@ version = "0.3.0" source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#ed923559dcb49da4c83cfd6ccd5e00ffb8119aa8" dependencies = [ "fiat-shamir", - "itertools 0.14.0", + "itertools", "p3-field", "p3-util", "rand", @@ -77,45 +115,12 @@ dependencies = [ "generic-array", ] -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "clap" version = "4.5.50" @@ -123,6 +128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -131,8 +137,22 @@ version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -141,6 +161,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "colored" version = "3.0.0" @@ -179,37 +205,6 @@ dependencies = [ "libc", ] -[[package]] -name = "criterion" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools 0.13.0", - "num-traits", - "oorandom", - "regex", - "serde", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" -dependencies = [ - "cast", - "itertools 0.13.0", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -235,12 +230,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.1.6" @@ -322,24 +311,16 @@ dependencies = [ ] [[package]] -name = "half" -version = "2.7.1" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "itertools" -version = "0.13.0" +name = "is_terminal_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -366,25 +347,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" name = "lean-multisig" version = "0.1.0" dependencies = [ - "air", - "criterion", - "multilinear-toolkit", - "p3-air", - "p3-challenger", - "p3-field", - "p3-koala-bear", - "p3-matrix", - "p3-poseidon2", - "p3-poseidon2-air", - "p3-uni-stark", - "p3-util", - "packed_pcs", + "clap", "poseidon_circuit", - "rand", "rec_aggregation", - "tracing", - "utils", - "whir-p3", ] [[package]] @@ -578,10 +543,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "oorandom" -version = "11.1.5" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "p3-air" @@ -622,7 +587,7 @@ name = "p3-commit" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ - "itertools 0.14.0", + "itertools", "p3-challenger", "p3-dft", "p3-field", @@ -636,7 +601,7 @@ name = "p3-dft" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ - "itertools 0.14.0", + "itertools", "p3-field", "p3-matrix", "p3-maybe-rayon", @@ -649,7 +614,7 @@ name = "p3-field" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ - "itertools 0.14.0", + "itertools", "num-bigint", "p3-maybe-rayon", "p3-util", @@ -675,7 +640,7 @@ name = "p3-koala-bear" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ - "itertools 0.14.0", + "itertools", "num-bigint", "p3-field", "p3-monty-31", @@ -691,7 +656,7 @@ name = "p3-matrix" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ - "itertools 0.14.0", + "itertools", "p3-field", "p3-maybe-rayon", "p3-util", @@ -726,7 +691,7 @@ name = "p3-merkle-tree" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ - "itertools 0.14.0", + "itertools", "p3-commit", "p3-field", "p3-matrix", @@ -743,7 +708,7 @@ name = "p3-monty-31" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ - "itertools 0.14.0", + "itertools", "num-bigint", "p3-dft", "p3-field", @@ -791,7 +756,7 @@ name = "p3-symmetric" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ - "itertools 0.14.0", + "itertools", "p3-field", "serde", ] @@ -801,7 +766,7 @@ name = "p3-uni-stark" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#59a799ce00907553aeef3d7cf38f616023e9ad5f" dependencies = [ - "itertools 0.14.0", + "itertools", "p3-air", "p3-challenger", "p3-commit", @@ -900,7 +865,6 @@ dependencies = [ "multilinear-toolkit", "p3-field", "p3-koala-bear", - "p3-matrix", "p3-monty-31", "p3-poseidon2", "packed_pcs", @@ -1021,18 +985,6 @@ dependencies = [ "xmss", ] -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - [[package]] name = "regex-automata" version = "0.4.13" @@ -1056,15 +1008,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "serde" version = "1.0.228" @@ -1140,6 +1083,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "sumcheck" version = "0.3.0" @@ -1195,16 +1144,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "tracing" version = "0.1.41" @@ -1319,6 +1258,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "utils" version = "0.1.0" @@ -1382,16 +1327,6 @@ dependencies = [ "xmss", ] -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -1406,7 +1341,7 @@ name = "whir-p3" version = "0.1.0" source = "git+https://github.com/TomWambsgans/whir-p3?branch=lean-multisig#57c8d12015260a6aca706a021364225e5ebdd636" dependencies = [ - "itertools 0.14.0", + "itertools", "multilinear-toolkit", "p3-baby-bear", "p3-challenger", @@ -1444,15 +1379,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1471,7 +1397,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -1489,14 +1424,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -1505,48 +1457,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index ee980111..eae85faa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ poseidon_circuit = { path = "crates/poseidon_circuit" } # External thiserror = "2.0" +clap = { version = "4.3.10", features = ["derive"] } rand = "0.9.2" sha3 = "0.10.8" rayon = "1.5.1" @@ -85,23 +86,9 @@ whir-p3 = { git = "https://github.com/TomWambsgans/whir-p3", branch = "lean-mult multilinear-toolkit = { git = "https://github.com/leanEthereum/multilinear-toolkit.git" } [dependencies] -air.workspace = true +clap.workspace = true +rec_aggregation.workspace = true poseidon_circuit.workspace = true -p3-field.workspace = true -p3-koala-bear.workspace = true -p3-poseidon2.workspace = true -rand.workspace = true -p3-poseidon2-air.workspace = true -p3-matrix.workspace = true -p3-challenger.workspace = true -whir-p3.workspace = true -p3-uni-stark.workspace = true -utils.workspace = true -p3-util.workspace = true -packed_pcs.workspace = true -tracing.workspace = true -p3-air.workspace = true -multilinear-toolkit.workspace = true # [patch."https://github.com/TomWambsgans/Plonky3.git"] # p3-koala-bear = { path = "../zk/Plonky3/koala-bear" } @@ -122,17 +109,5 @@ multilinear-toolkit.workspace = true # [patch."https://github.com/leanEthereum/multilinear-toolkit.git"] # multilinear-toolkit = { path = "../zk/multilinear-toolkit" } -[dev-dependencies] -criterion = { version = "0.7", default-features = false, features = ["cargo_bench_support"] } -rec_aggregation.workspace = true - [profile.release] lto = "thin" - -[[bench]] -name = "recursion" -harness = false - -[[bench]] -name = "xmss" -harness = false diff --git a/README.md b/README.md index 247f64d5..e2c8ccac 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ XMSS + minimal [zkVM](minimal_zkVM.pdf) = lightweight PQ signatures, with unboun ## Proving System -- AIR tables committed via multilinear polynomial, using [WHIR](https://eprint.iacr.org/2024/1586.pdf) +- [WHIR](https://eprint.iacr.org/2024/1586.pdf) - [SuperSpartan](https://eprint.iacr.org/2023/552.pdf), with AIR-specific optimizations developed by W. Borgeaud in [A simple multivariate AIR argument inspired by SuperSpartan](https://solvable.group/posts/super-air/#fnref:1) - [Univariate Skip](https://eprint.iacr.org/2024/108.pdf) - [Logup*](https://eprint.iacr.org/2025/946.pdf) @@ -14,49 +14,46 @@ XMSS + minimal [zkVM](minimal_zkVM.pdf) = lightweight PQ signatures, with unboun The VM design is inspired by the famous [Cairo paper](https://eprint.iacr.org/2021/1063.pdf). -Details on how to prove AIR constraints in the multilinear settings are described in [Whirlaway.pdf](Whirlaway.pdf). - -[Deep-wiki](https://deepwiki.com/leanEthereum/leanMultisig/1-overview) (thanks [adust09](https://github.com/adust09)) - - ## Benchmarks -cpu: i9-12900H, ram: 32 gb +Benchmarks are performed on 2 laptops: +- i9-12900H, 32 gb of RAM +- mac m4 max -> TLDR: Slow, **but there is hope** (cf [TODO](TODO.md)) - -target ≈ 128 bits of security, currently using conjecture: 4.12 of [WHIR](https://eprint.iacr.org/2024/1586.pdf), "up to capacity" (TODO: a version without any conjecture, requires an extension of koala-bear of degree > 5) +target ≈ 128 bits of security, currently using conjecture: 4.12 of [WHIR](https://eprint.iacr.org/2024/1586.pdf), "up to capacity" (TODO: provable security) ### Poseidon2 +Poseidon2 over 16 KoalaBear field elements. + ```console -RUSTFLAGS='-C target-cpu=native' cargo test --release --package poseidon_circuit --lib -- tests::test_prove_poseidons --nocapture +RUSTFLAGS='-C target-cpu=native' cargo run --release -- poseidon --log-n-perms 20 ``` -50 % over 16 field elements, 50 % over 24 field elements. rate = 1/2 - ![Alt text](docs/benchmark_graphs/graphs/raw_poseidons.svg) ### Recursion -```console -RUSTFLAGS='-C target-cpu=native' cargo test --release --package rec_aggregation --lib -- recursion::test_whir_recursion --nocapture -``` The full recursion program is not finished yet. Instead, we prove validity of a WHIR opening, with 25 variables, and rate = 1/4. +```console +RUSTFLAGS='-C target-cpu=native' cargo run --release -- recursion +``` + ![Alt text](docs/benchmark_graphs/graphs/recursive_whir_opening.svg) ### XMSS aggregation ```console -RUSTFLAGS='-C target-cpu=native' NUM_XMSS_AGGREGATED='500' cargo test --release --package rec_aggregation --lib -- xmss_aggregate::test_xmss_aggregate --nocapture +RUSTFLAGS='-C target-cpu=native' cargo run --release -- xmss --n-signatures 800 ``` -500 XMSS aggregated. "Trivial encoding" (for now). +[Trivial encoding](docs/XMSS_trivial_encoding.pdf) (for now). + +Overhead versus raw Poseidons: ≈ 10x -![Alt text](docs/benchmark_graphs/graphs/xmss_aggregated_time.svg) -![Alt text](docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg) +![Alt text](docs/benchmark_graphs/graphs/xmss_aggregated.svg) ### Proof size @@ -65,7 +62,7 @@ With conjecture "up to capacity", current proofs with rate = 1/2 are about ≈ 4 - The remaining 100 - 200 KiB will be significantly reduced in the future (this part has not been optimized at all). - WHIR proof size will also be reduced, thanks to merkle pruning (TODO). -Reasonable target: 256 KiB for fast proof, 128 KiB for slower proofs (rate = 1/4 or 1/8). +Target: 256 KiB for fast proof, 128 KiB for slower proofs (rate = 1/4 or 1/8). ## Credits diff --git a/benches/recursion.rs b/benches/recursion.rs deleted file mode 100644 index ff086a25..00000000 --- a/benches/recursion.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::{hint::black_box, time::Duration}; - -use criterion::{Criterion, criterion_group, criterion_main}; -use rec_aggregation::bench_recursion; - -fn bench_recursion_benchmark(c: &mut Criterion) { - let mut group = c.benchmark_group("recursion"); - group.sample_size(10); - group.measurement_time(Duration::from_secs(60)); - group.warm_up_time(Duration::from_secs(10)); - - group.bench_function("recursion", |b| { - b.iter(|| { - let duration = bench_recursion(); - black_box(duration); - }); - }); - - group.finish(); -} - -criterion_group!(benches, bench_recursion_benchmark); -criterion_main!(benches); diff --git a/benches/xmss.rs b/benches/xmss.rs deleted file mode 100644 index 611a23d9..00000000 --- a/benches/xmss.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::{hint::black_box, time::Duration}; - -use criterion::{Criterion, Throughput, criterion_group, criterion_main}; -use rec_aggregation::run_xmss_benchmark; - -fn bench_xmss_benchmark(c: &mut Criterion) { - const N: usize = 500; - - let mut group = c.benchmark_group("xmss"); - group.sample_size(10); - group.measurement_time(Duration::from_secs(60)); - group.warm_up_time(Duration::from_secs(10)); - group.throughput(Throughput::Elements(N as u64)); - - group.bench_function("xmss", |b| { - b.iter(|| { - let duration = run_xmss_benchmark(N).proving_time; - black_box(duration); - }); - }); - - group.finish(); -} - -criterion_group!(benches, bench_xmss_benchmark); -criterion_main!(benches); diff --git a/crates/lean_compiler/src/lib.rs b/crates/lean_compiler/src/lib.rs index d2dd2584..8f85618b 100644 --- a/crates/lean_compiler/src/lib.rs +++ b/crates/lean_compiler/src/lib.rs @@ -37,13 +37,15 @@ pub fn compile_and_run( profiler: bool, ) { let bytecode = compile_program(program); - execute_bytecode( + let summary = execute_bytecode( &bytecode, (public_input, private_input), no_vec_runtime_memory, - (profiler, true), + profiler, (&vec![], &vec![]), - ); + ) + .summary; + println!("{summary}"); } #[derive(Debug, Clone, Default)] diff --git a/crates/lean_prover/src/common.rs b/crates/lean_prover/src/common.rs index 875441c9..26141ccf 100644 --- a/crates/lean_prover/src/common.rs +++ b/crates/lean_prover/src/common.rs @@ -308,10 +308,10 @@ impl SumcheckComputationPacked for DotProductFootprint { pub fn get_poseidon_lookup_statements( (log_n_p16, log_n_p24): (usize, usize), - (p16_input_point, p16_input_evals): &(MultilinearPoint, [EF; 16]), - (p16_output_point, p16_output_evals): &(MultilinearPoint, [EF; 16]), - (p24_input_point, p24_input_evals): &(MultilinearPoint, [EF; 24]), - (p24_output_point, p24_output_evals): &(MultilinearPoint, [EF; 24]), + (p16_input_point, p16_input_evals): &(MultilinearPoint, Vec), + (p16_output_point, p16_output_evals): &(MultilinearPoint, Vec), + (p24_input_point, p24_input_evals): &(MultilinearPoint, Vec), + (p24_output_point, p24_output_evals): &(MultilinearPoint, Vec), memory_folding_challenges: &MultilinearPoint, ) -> Vec> { let p16_folded_eval_addr_a = (&p16_input_evals[..8]).evaluate(memory_folding_challenges); diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index 26d4fe6a..54582e6a 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -29,7 +29,8 @@ pub fn prove_execution( no_vec_runtime_memory: usize, // size of the "non-vectorized" runtime memory vm_profiler: bool, (poseidons_16_precomputed, poseidons_24_precomputed): (&Poseidon16History, &Poseidon24History), -) -> (Vec>, usize) { +) -> (Vec>, usize, String) { + let mut exec_summary = String::new(); let ExecutionTrace { full_trace, n_poseidons_16, @@ -43,15 +44,16 @@ pub fn prove_execution( non_zero_memory_size, memory, // padded with zeros to next power of two } = info_span!("Witness generation").in_scope(|| { - let execution_result = info_span!("Executing bytecode").in_scope(|| { + let mut execution_result = info_span!("Executing bytecode").in_scope(|| { execute_bytecode( bytecode, (public_input, private_input), no_vec_runtime_memory, - (vm_profiler, true), + vm_profiler, (poseidons_16_precomputed, poseidons_24_precomputed), ) }); + exec_summary = std::mem::take(&mut execution_result.summary); info_span!("Building execution trace") .in_scope(|| get_execution_trace(bytecode, execution_result)) }); @@ -1163,5 +1165,6 @@ pub fn prove_execution( ( prover_state.proof_data().to_vec(), prover_state.proof_size(), + exec_summary, ) } diff --git a/crates/lean_prover/tests/hash_chain.rs b/crates/lean_prover/tests/hash_chain.rs index b2092280..d4a9f874 100644 --- a/crates/lean_prover/tests/hash_chain.rs +++ b/crates/lean_prover/tests/hash_chain.rs @@ -67,7 +67,7 @@ fn benchmark_poseidon_chain() { &bytecode, (&public_input, &private_input), 1 << (3 + LOG_CHAIN_LENGTH), - (false, false), + false, (&vec![], &vec![]), ) .no_vec_runtime_memory; diff --git a/crates/lean_vm/src/diagnostics/error.rs b/crates/lean_vm/src/diagnostics/error.rs index 2ad9fac1..0a225733 100644 --- a/crates/lean_vm/src/diagnostics/error.rs +++ b/crates/lean_vm/src/diagnostics/error.rs @@ -45,4 +45,5 @@ pub struct ExecutionResult { pub poseidons_24: Vec, pub dot_products: Vec, pub multilinear_evals: Vec, + pub summary: String, } diff --git a/crates/lean_vm/src/diagnostics/profiler.rs b/crates/lean_vm/src/diagnostics/profiler.rs index 6fe43005..182ee016 100644 --- a/crates/lean_vm/src/diagnostics/profiler.rs +++ b/crates/lean_vm/src/diagnostics/profiler.rs @@ -57,8 +57,9 @@ pub(crate) fn profiling_report( let mut report = String::new(); - report - .push_str("╔═════════════════════════════════════════════════════════════════════════╗\n"); + report.push_str( + "\n╔═════════════════════════════════════════════════════════════════════════╗\n", + ); report .push_str("║ PROFILING REPORT ║\n"); report.push_str( diff --git a/crates/lean_vm/src/execution/runner.rs b/crates/lean_vm/src/execution/runner.rs index ea6b03c6..df7da46a 100644 --- a/crates/lean_vm/src/execution/runner.rs +++ b/crates/lean_vm/src/execution/runner.rs @@ -65,7 +65,7 @@ pub fn execute_bytecode( bytecode: &Bytecode, (public_input, private_input): (&[F], &[F]), no_vec_runtime_memory: usize, // size of the "non-vectorized" runtime memory - (profiler, display_std_out_and_stats): (bool, bool), + profiler: bool, (poseidons_16_precomputed, poseidons_24_precomputed): (&Poseidon16History, &Poseidon24History), ) -> ExecutionResult { let mut std_out = String::new(); @@ -76,7 +76,7 @@ pub fn execute_bytecode( &mut std_out, &mut instruction_history, no_vec_runtime_memory, - (profiler, display_std_out_and_stats), + profiler, (poseidons_16_precomputed, poseidons_24_precomputed), ) .unwrap_or_else(|err| { @@ -151,7 +151,7 @@ fn execute_bytecode_helper( std_out: &mut String, instruction_history: &mut ExecutionHistory, no_vec_runtime_memory: usize, - (profiler, display_std_out_and_stats): (bool, bool), + profiler: bool, (poseidons_16_precomputed, poseidons_24_precomputed): (&Poseidon16History, &Poseidon24History), ) -> Result { // set public memory @@ -284,92 +284,110 @@ fn execute_bytecode_helper( let no_vec_runtime_memory = ap - initial_ap; + let mut summary = String::new(); + if profiler { let report = crate::diagnostics::profiling_report(instruction_history, &bytecode.function_locations); - println!("\n{report}"); + summary.push_str(&report); } - if display_std_out_and_stats { - if !std_out.is_empty() { - println!("╔═════════════════════════════════════════════════════════════════════════╗"); - println!("║ STD-OUT ║"); - println!("╚═════════════════════════════════════════════════════════════════════════╝"); - print!("\n{std_out}"); - println!( - "──────────────────────────────────────────────────────────────────────────\n" - ); - } - - println!("╔═════════════════════════════════════════════════════════════════════════╗"); - println!("║ STATS ║"); - println!("╚═════════════════════════════════════════════════════════════════════════╝\n"); - - println!("CYCLES: {}", pretty_integer(cpu_cycles)); - println!("MEMORY: {}", pretty_integer(memory.0.len())); - println!(); - let runtime_memory_size = memory.0.len() - (PUBLIC_INPUT_START + public_input.len()); - println!( - "Bytecode size: {}", - pretty_integer(bytecode.instructions.len()) + if !std_out.is_empty() { + summary.push_str( + "╔═════════════════════════════════════════════════════════════════════════╗\n", ); - println!("Public input size: {}", pretty_integer(public_input.len())); - println!( - "Private input size: {}", - pretty_integer(private_input.len()) + summary.push_str( + "║ STD-OUT ║\n", ); - println!( - "Runtime memory: {} ({:.2}% vec) (no vec mem: {})", - pretty_integer(runtime_memory_size), - (VECTOR_LEN * (ap_vec - initial_ap_vec)) as f64 / runtime_memory_size as f64 * 100.0, - no_vec_runtime_memory + summary.push_str( + "╚═════════════════════════════════════════════════════════════════════════╝\n", ); - let used_memory_cells = memory - .0 - .iter() - .skip(PUBLIC_INPUT_START + public_input.len()) - .filter(|&&x| x.is_some()) - .count(); - println!( - "Memory usage: {:.1}%", - used_memory_cells as f64 / runtime_memory_size as f64 * 100.0 + summary.push_str(&format!("\n{std_out}")); + summary.push_str( + "──────────────────────────────────────────────────────────────────────────\n\n", ); + } - println!(); - - if poseidons_16.len() + poseidons_24.len() > 0 { - println!( - "Poseidon2_16 calls: {}, Poseidon2_24 calls: {}, (1 poseidon per {} instructions)", - pretty_integer(poseidons_16.len()), - pretty_integer(poseidons_24.len()), - cpu_cycles / (poseidons_16.len() + poseidons_24.len()) - ); - } - if !dot_products.is_empty() { - println!("DotProduct calls: {}", pretty_integer(dot_products.len())); - } - if !multilinear_evals.is_empty() { - println!( - "MultilinearEval calls: {}", - pretty_integer(multilinear_evals.len()) - ); - } + summary + .push_str("╔═════════════════════════════════════════════════════════════════════════╗\n"); + summary + .push_str("║ STATS ║\n"); + summary.push_str( + "╚═════════════════════════════════════════════════════════════════════════╝\n\n", + ); - if false { - println!("Low level instruction counts:"); - println!( - "COMPUTE: {} ({} ADD, {} MUL)", - add_counts + mul_counts, - add_counts, - mul_counts - ); - println!("DEREF: {deref_counts}"); - println!("JUMP: {jump_counts}"); - } + summary.push_str(&format!("CYCLES: {}\n", pretty_integer(cpu_cycles))); + summary.push_str(&format!("MEMORY: {}\n", pretty_integer(memory.0.len()))); + summary.push('\n'); + + let runtime_memory_size = memory.0.len() - (PUBLIC_INPUT_START + public_input.len()); + summary.push_str(&format!( + "Bytecode size: {}\n", + pretty_integer(bytecode.instructions.len()) + )); + summary.push_str(&format!( + "Public input size: {}\n", + pretty_integer(public_input.len()) + )); + summary.push_str(&format!( + "Private input size: {}\n", + pretty_integer(private_input.len()) + )); + summary.push_str(&format!( + "Runtime memory: {} ({:.2}% vec) (no vec mem: {})\n", + pretty_integer(runtime_memory_size), + (VECTOR_LEN * (ap_vec - initial_ap_vec)) as f64 / runtime_memory_size as f64 * 100.0, + no_vec_runtime_memory + )); + let used_memory_cells = memory + .0 + .iter() + .skip(PUBLIC_INPUT_START + public_input.len()) + .filter(|&&x| x.is_some()) + .count(); + summary.push_str(&format!( + "Memory usage: {:.1}%\n", + used_memory_cells as f64 / runtime_memory_size as f64 * 100.0 + )); + + summary.push('\n'); + + if poseidons_16.len() + poseidons_24.len() > 0 { + summary.push_str(&format!( + "Poseidon2_16 calls: {}, Poseidon2_24 calls: {}, (1 poseidon per {} instructions)\n", + pretty_integer(poseidons_16.len()), + pretty_integer(poseidons_24.len()), + cpu_cycles / (poseidons_16.len() + poseidons_24.len()) + )); + } + if !dot_products.is_empty() { + summary.push_str(&format!( + "DotProduct calls: {}\n", + pretty_integer(dot_products.len()) + )); + } + if !multilinear_evals.is_empty() { + summary.push_str(&format!( + "MultilinearEval calls: {}\n", + pretty_integer(multilinear_evals.len()) + )); + } - println!("──────────────────────────────────────────────────────────────────────────\n"); + if false { + summary.push_str("Low level instruction counts:\n"); + summary.push_str(&format!( + "COMPUTE: {} ({} ADD, {} MUL)\n", + add_counts + mul_counts, + add_counts, + mul_counts + )); + summary.push_str(&format!("DEREF: {deref_counts}\n")); + summary.push_str(&format!("JUMP: {jump_counts}\n")); } + summary + .push_str("──────────────────────────────────────────────────────────────────────────\n"); + Ok(ExecutionResult { no_vec_runtime_memory, public_memory_size, @@ -380,5 +398,6 @@ fn execute_bytecode_helper( poseidons_24, dot_products, multilinear_evals, + summary, }) } diff --git a/crates/lean_vm/tests/test_lean_vm.rs b/crates/lean_vm/tests/test_lean_vm.rs index 26428023..a6bc6e54 100644 --- a/crates/lean_vm/tests/test_lean_vm.rs +++ b/crates/lean_vm/tests/test_lean_vm.rs @@ -208,9 +208,10 @@ fn run_program() -> (Bytecode, ExecutionResult) { &bytecode, (&public_input, &[]), 1 << 20, - (false, true), + false, (&vec![], &vec![]), ); + println!("{}", result.summary); (bytecode, result) } diff --git a/crates/poseidon_circuit/Cargo.toml b/crates/poseidon_circuit/Cargo.toml index 2d7eb9ad..a995a9d9 100644 --- a/crates/poseidon_circuit/Cargo.toml +++ b/crates/poseidon_circuit/Cargo.toml @@ -15,9 +15,7 @@ multilinear-toolkit.workspace = true p3-koala-bear.workspace = true p3-poseidon2.workspace = true p3-monty-31.workspace = true - -[dev-dependencies] -p3-matrix.workspace = true rand.workspace = true whir-p3.workspace = true packed_pcs.workspace = true + diff --git a/crates/poseidon_circuit/src/lib.rs b/crates/poseidon_circuit/src/lib.rs index dc0d36f0..91eff4a2 100644 --- a/crates/poseidon_circuit/src/lib.rs +++ b/crates/poseidon_circuit/src/lib.rs @@ -12,8 +12,7 @@ pub use verify::*; mod witness_gen; pub use witness_gen::*; -#[cfg(test)] -mod tests; +pub mod tests; pub mod gkr_layers; pub use gkr_layers::*; @@ -22,8 +21,8 @@ pub(crate) type F = KoalaBear; pub(crate) type EF = QuinticExtensionFieldKB; #[derive(Debug, Clone)] -pub struct GKRPoseidonResult { - pub output_values: [EF; WIDTH], - pub input_statements: (MultilinearPoint, [EF; WIDTH]), // remain to be proven - pub cubes_statements: (MultilinearPoint, [EF; N_COMMITED_CUBES]), // remain to be proven +pub struct GKRPoseidonResult { + pub output_values: Vec, // of length width + pub input_statements: (MultilinearPoint, Vec), // of length width, remain to be proven + pub cubes_statements: (MultilinearPoint, Vec), // of length n_committed_cubes, remain to be proven } diff --git a/crates/poseidon_circuit/src/prove.rs b/crates/poseidon_circuit/src/prove.rs index 4b716208..b17cbbdd 100644 --- a/crates/poseidon_circuit/src/prove.rs +++ b/crates/poseidon_circuit/src/prove.rs @@ -15,7 +15,7 @@ pub fn prove_poseidon_gkr( mut claim_point: Vec, univariate_skips: usize, layers: &PoseidonGKRLayers, -) -> GKRPoseidonResult +) -> GKRPoseidonResult where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { @@ -154,13 +154,13 @@ where ); GKRPoseidonResult { - output_values: output_claims.try_into().unwrap(), + output_values: output_claims, input_statements, cubes_statements, } } -// #[instrument(skip_all)] +#[instrument(skip_all)] fn prove_gkr_round< SC: SumcheckComputation + SumcheckComputation @@ -205,7 +205,7 @@ fn prove_gkr_round< (sumcheck_point.0, sumcheck_inner_evals) } -// #[instrument(skip_all)] +#[instrument(skip_all)] fn prove_batch_internal_round( prover_state: &mut FSProver>, input_layers: &[Vec>], @@ -272,12 +272,12 @@ where (sumcheck_point.0, sumcheck_inner_evals) } -fn inner_evals_on_commited_columns( +fn inner_evals_on_commited_columns( prover_state: &mut FSProver>, point: &[EF], univariate_skips: usize, - columns: &[Vec>; N], -) -> (MultilinearPoint, [EF; N]) { + columns: &[Vec>], +) -> (MultilinearPoint, Vec) { let eq_mle = eval_eq_packed(&point[1..]); let inner_evals = columns .par_iter() @@ -304,5 +304,5 @@ fn inner_evals_on_commited_columns( values_to_prove .push(col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone()))); } - (point_to_prove, values_to_prove.try_into().unwrap()) + (point_to_prove, values_to_prove) } diff --git a/crates/poseidon_circuit/src/tests.rs b/crates/poseidon_circuit/src/tests.rs index 8da292e4..8f9ddc42 100644 --- a/crates/poseidon_circuit/src/tests.rs +++ b/crates/poseidon_circuit/src/tests.rs @@ -1,5 +1,3 @@ -#![cfg_attr(not(test), allow(unused_crate_dependencies))] - use multilinear_toolkit::prelude::*; use p3_koala_bear::{KoalaBear, QuinticExtensionFieldKB}; use packed_pcs::{ @@ -28,16 +26,18 @@ const WIDTH: usize = 16; const UNIVARIATE_SKIPS: usize = 3; const N_COMMITED_CUBES: usize = 16; -const COMPRESS: bool = false; const COMPRESSION_OUTPUT_WIDTH: usize = 8; #[test] -fn test_prove_poseidons() { +fn test_poseidon_benchmark() { + run_poseidon_benchmark(15, true); + run_poseidon_benchmark(15, false); +} + +pub fn run_poseidon_benchmark(log_n_poseidons: usize, compress: bool) { init_tracing(); precompute_dft_twiddles::(1 << 24); - let log_n_poseidons = 20; - let whir_config_builder = WhirConfigBuilder { folding_factor: FoldingFactor::new(7, 4), soundness_type: SecurityAssumption::CapacityBound, @@ -52,7 +52,7 @@ fn test_prove_poseidons() { let mut rng = StdRng::seed_from_u64(0); let n_poseidons = 1 << log_n_poseidons; - let n_compressions = if COMPRESS { n_poseidons / 3 } else { 0 }; + let n_compressions = if compress { n_poseidons / 3 } else { 0 }; let perm_inputs = (0..n_poseidons) .map(|_| rng.random()) @@ -63,7 +63,7 @@ fn test_prove_poseidons() { array::from_fn(|i| PFPacking::::pack_slice(&input[i]).to_vec()); let layers = PoseidonGKRLayers::::build( - COMPRESS.then_some(COMPRESSION_OUTPUT_WIDTH), + compress.then_some(COMPRESSION_OUTPUT_WIDTH), ); let default_cubes = default_cube_layers::(&layers); @@ -91,7 +91,7 @@ fn test_prove_poseidons() { let witness = generate_poseidon_witness::, WIDTH, N_COMMITED_CUBES>( input_packed, &layers, - if COMPRESS { + if compress { Some(( n_compressions, PFPacking::::pack_slice( @@ -201,7 +201,7 @@ fn test_prove_poseidons() { &output_claim_point, &layers, UNIVARIATE_SKIPS, - if COMPRESS { Some(n_compressions) } else { None }, + if compress { Some(n_compressions) } else { None }, ); // PCS verification @@ -257,7 +257,7 @@ fn test_prove_poseidons() { let plaintext_duration = plaintext_time.elapsed(); // sanity check: ensure the plaintext poseidons matches the last GKR layer: - if COMPRESS { + if compress { output_layer .iter() .enumerate() diff --git a/crates/poseidon_circuit/src/verify.rs b/crates/poseidon_circuit/src/verify.rs index 0946a64a..f21a45a3 100644 --- a/crates/poseidon_circuit/src/verify.rs +++ b/crates/poseidon_circuit/src/verify.rs @@ -11,7 +11,7 @@ pub fn verify_poseidon_gkr( layers: &PoseidonGKRLayers, univariate_skips: usize, n_compressions: Option, -) -> GKRPoseidonResult +) -> GKRPoseidonResult where KoalaBearInternalLayerParameters: InternalLayerBaseParameters, { @@ -102,7 +102,7 @@ where ); let pcs_point_for_cubes = claim_point.clone(); - let pcs_evals_for_cubes = claims[WIDTH..].try_into().unwrap(); + let pcs_evals_for_cubes = claims[WIDTH..].to_vec(); claims = claims[..WIDTH].to_vec(); @@ -128,7 +128,7 @@ where ); let pcs_point_for_inputs = claim_point.clone(); - let pcs_evals_for_inputs = claims.try_into().unwrap(); + let pcs_evals_for_inputs = claims; let input_statements = verify_inner_evals_on_commited_columns( verifier_state, @@ -145,7 +145,7 @@ where ); GKRPoseidonResult { - output_values: output_claims.try_into().unwrap(), + output_values: output_claims, input_statements, cubes_statements, } @@ -188,12 +188,12 @@ fn verify_gkr_round>( (sumcheck_postponed_claim.point.0, sumcheck_inner_evals) } -fn verify_inner_evals_on_commited_columns( +fn verify_inner_evals_on_commited_columns( verifier_state: &mut FSVerifier>, point: &[EF], - claimed_evals: &[EF; N], + claimed_evals: &[EF], selectors: &[DensePolynomial], -) -> (MultilinearPoint, [EF; N]) { +) -> (MultilinearPoint, Vec) { let univariate_skips = log2_strict_usize(selectors.len()); let inner_evals_inputs = verifier_state .next_extension_scalars_vec(claimed_evals.len() << univariate_skips) @@ -218,5 +218,5 @@ fn verify_inner_evals_on_commited_columns( values_to_verif .push(col_inner_evals.evaluate(&MultilinearPoint(pcs_batching_scalars_inputs.clone()))); } - (point_to_verif, values_to_verif.try_into().unwrap()) + (point_to_verif, values_to_verif) } diff --git a/crates/rec_aggregation/src/lib.rs b/crates/rec_aggregation/src/lib.rs index 58fff34e..1f3d901e 100644 --- a/crates/rec_aggregation/src/lib.rs +++ b/crates/rec_aggregation/src/lib.rs @@ -2,6 +2,3 @@ pub mod recursion; pub mod xmss_aggregate; - -pub use recursion::bench_recursion; -pub use xmss_aggregate::run_xmss_benchmark; diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index 7a609cb4..9066e467 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -1,5 +1,5 @@ use std::path::Path; -use std::time::{Duration, Instant}; +use std::time::Instant; use lean_compiler::compile_program; use lean_prover::prove_execution::prove_execution; @@ -20,12 +20,7 @@ use whir_p3::{ const NUM_VARIABLES: usize = 25; -struct RecursionBenchStats { - proving_time: Duration, - proof_size: usize, -} - -fn run_recursion_benchmark() -> RecursionBenchStats { +pub fn run_whir_recursion_benchmark() { let src_file = Path::new(env!("CARGO_MANIFEST_DIR")).join("recursion_program.lean_lang"); let mut program_str = std::fs::read_to_string(src_file).unwrap(); let recursion_config_builder = WhirConfigBuilder { @@ -189,14 +184,14 @@ fn run_recursion_benchmark() -> RecursionBenchStats { &bytecode, (&public_input, &[]), 1 << 20, - (false, false), + false, (&vec![], &vec![]), ) .no_vec_runtime_memory; let time = Instant::now(); - let (proof_data, proof_size) = prove_execution( + let (proof_data, proof_size, summary) = prove_execution( &bytecode, (&public_input, &[]), whir_config_builder(), @@ -206,23 +201,16 @@ fn run_recursion_benchmark() -> RecursionBenchStats { ); let proving_time = time.elapsed(); verify_execution(&bytecode, &public_input, proof_data, whir_config_builder()).unwrap(); - RecursionBenchStats { - proving_time, - proof_size, - } -} -pub fn bench_recursion() -> Duration { - run_recursion_benchmark().proving_time + println!("{}", summary); + println!( + "WHIR recursion, proving time: {} ms, proof size: {} KiB (not optimized)", + proving_time.as_millis(), + proof_size * F::bits() / (8 * 1024) + ); } #[test] fn test_whir_recursion() { - use p3_field::Field; - let stats = run_recursion_benchmark(); - println!( - "\nWHIR recursion, proving time: {:?}, proof size: {} KiB (not optimized)", - stats.proving_time, - stats.proof_size * F::bits() / (8 * 1024) - ); + run_whir_recursion_benchmark(); } diff --git a/crates/rec_aggregation/src/xmss_aggregate.rs b/crates/rec_aggregation/src/xmss_aggregate.rs index 15752a0a..c017c04c 100644 --- a/crates/rec_aggregation/src/xmss_aggregate.rs +++ b/crates/rec_aggregation/src/xmss_aggregate.rs @@ -1,9 +1,10 @@ -use std::time::{Duration, Instant}; +use std::time::Instant; use lean_compiler::*; use lean_prover::whir_config_builder; use lean_prover::{prove_execution::prove_execution, verify_execution::verify_execution}; use lean_vm::*; +use p3_field::Field; use p3_field::PrimeCharacteristicRing; use rand::{Rng, SeedableRng, rngs::StdRng}; use rayon::prelude::*; @@ -14,14 +15,7 @@ use xmss::{ const LOG_LIFETIME: usize = 32; -#[derive(Default, Debug)] -pub struct XmssBenchStats { - pub proving_time: Duration, - pub proof_size: usize, - pub n_xmss: usize, -} - -pub fn run_xmss_benchmark(n_xmss: usize) -> XmssBenchStats { +pub fn run_xmss_benchmark(n_xmss: usize) { // Public input: message_hash | all_public_keys | bitield // Private input: signatures = (randomness | chain_tips | merkle_path) let mut program_str = r#" @@ -267,7 +261,7 @@ pub fn run_xmss_benchmark(n_xmss: usize) -> XmssBenchStats { &bytecode, (&public_input, &private_input), 1 << 21, - (false, false), + false, (&vec![], &vec![]), ) .no_vec_runtime_memory; @@ -278,7 +272,7 @@ pub fn run_xmss_benchmark(n_xmss: usize) -> XmssBenchStats { let (poseidons_16_precomputed, poseidons_24_precomputed) = precompute_poseidons(&all_public_keys, &all_signatures, &message_hash); - let (proof_data, proof_size) = prove_execution( + let (proof_data, proof_size, summary) = prove_execution( &bytecode, (&public_input, &private_input), whir_config_builder(), @@ -288,11 +282,14 @@ pub fn run_xmss_benchmark(n_xmss: usize) -> XmssBenchStats { ); let proving_time = time.elapsed(); verify_execution(&bytecode, &public_input, proof_data, whir_config_builder()).unwrap(); - XmssBenchStats { - proving_time, - proof_size, - n_xmss, - } + + println!("{}", summary); + println!( + "XMSS aggregation, proving time: {:.3} s ({:.1} XMSS/s), proof size: {} KiB (not optimized)", + proving_time.as_secs_f64(), + n_xmss as f64 / proving_time.as_secs_f64(), + proof_size * F::bits() / (8 * 1024) + ); } #[instrument(skip_all)] @@ -319,20 +316,9 @@ fn precompute_poseidons( #[test] fn test_xmss_aggregate() { - use p3_field::Field; - let n_public_keys: usize = std::env::var("NUM_XMSS_AGGREGATED") + let n_xmss: usize = std::env::var("NUM_XMSS_AGGREGATED") .unwrap_or("100".to_string()) .parse() .unwrap(); - let stats = run_xmss_benchmark(n_public_keys); - println!( - "\nXMSS aggregation (n_signatures = {}, lifetime = 2^{})", - stats.n_xmss, 32 - ); - println!( - "Proving time: {:?} ({:.1} XMSS/s), proof size: {} KiB (not optimized)", - stats.proving_time, - stats.n_xmss as f64 / stats.proving_time.as_secs_f64(), - stats.proof_size * F::bits() / (8 * 1024) - ); + run_xmss_benchmark(n_xmss); } diff --git a/docs/Proving_Poseidons_with_GKR.pdf b/docs/Proving_Poseidons_with_GKR.pdf new file mode 100644 index 0000000000000000000000000000000000000000..442e021bd16cca4b4e93e5f14190e1dfa9d0b76a GIT binary patch literal 158309 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�&bNMO%cng*hI_!M`#XSfQ>S$s21jL1U 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%0D)=!_h?BcjB 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 I@;kKu1u&qO{r~^~ diff --git a/Whirlaway.pdf b/docs/Whirlaway.pdf similarity index 100% rename from Whirlaway.pdf rename to docs/Whirlaway.pdf diff --git a/crates/xmss/XMSS_trivial_encoding.pdf b/docs/XMSS_trivial_encoding.pdf similarity index 100% rename from crates/xmss/XMSS_trivial_encoding.pdf rename to docs/XMSS_trivial_encoding.pdf diff --git a/docs/benchmark_graphs/graphs/raw_poseidons.svg b/docs/benchmark_graphs/graphs/raw_poseidons.svg index cd7714ca..4fda3fdc 100644 --- a/docs/benchmark_graphs/graphs/raw_poseidons.svg +++ b/docs/benchmark_graphs/graphs/raw_poseidons.svg @@ -1,12 +1,12 @@ - + - 2025-10-18T16:27:08.901962 + 2025-10-27T16:10:57.164038 image/svg+xml @@ -21,43 +21,57 @@ - - - + - - + - - + + + + + - - - - - + + - + - + - + - - + + - - - + + - + - + - + - - - - + + + + - - + + - + - + - - - - - - - + + + + - + - + - + - - + + - - - + + - + - + + + - + + + + - + - - + + + + + - - - - + + - + - + - + - - - - - - - - - - + + + + + - + - + - + - - - - - - - - - - + + + + + - + - + - + - - - - - - - + + + + + + + + - - - + - + - - - - + - - + + + + - + - + - + - - - - - - - - - - + + + + + - + - + - + - - + + - - - - - + + - + - + - + - - + + - - - - - + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - + - + + + + + + + + + - - + + - - - - - - - - - - - + + + + + + + + + + + + + - - + + - - - - - + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - + + diff --git a/docs/benchmark_graphs/graphs/recursive_whir_opening.svg b/docs/benchmark_graphs/graphs/recursive_whir_opening.svg index 6f39171c..16168c50 100644 --- a/docs/benchmark_graphs/graphs/recursive_whir_opening.svg +++ b/docs/benchmark_graphs/graphs/recursive_whir_opening.svg @@ -1,12 +1,12 @@ - + - 2025-10-18T16:27:08.978098 + 2025-10-27T16:10:57.245462 image/svg+xml @@ -21,43 +21,57 @@ - - - + - - + - - + + + + + - - - - - + + - + - + - + - - + + - - - + + - + - + - + - - - - + + + + - - + + - + - + - - - - - - - + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - + + - + - - + - + - + - - + + - + - + - - - - - - + + + - + - - + + - + - + - - - - + + + + - + - - + + - + - + - - - - + + + + + + + - + - - - - - - - - - - - - - - - - - - - - + + - + - + - - - - + + + + - + - + - + - + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - + + - - - - - + - + - - - + + + + + - - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1244,19 +1162,17 @@ z - - - - - + + + - - + + diff --git a/docs/benchmark_graphs/graphs/xmss_aggregated_time.svg b/docs/benchmark_graphs/graphs/xmss_aggregated.svg similarity index 51% rename from docs/benchmark_graphs/graphs/xmss_aggregated_time.svg rename to docs/benchmark_graphs/graphs/xmss_aggregated.svg index 019440aa..cae23cbe 100644 --- a/docs/benchmark_graphs/graphs/xmss_aggregated_time.svg +++ b/docs/benchmark_graphs/graphs/xmss_aggregated.svg @@ -1,12 +1,12 @@ - + - 2025-10-18T16:27:09.055074 + 2025-10-27T16:10:57.320500 image/svg+xml @@ -21,43 +21,57 @@ - - - + - - + - - + + + + + - - - - - + + - + - + - + - - + + - - - + + - + - + - + - - - - + + + + - - + + - + - + - - - - - - - + + + + - + - + - + - - + + - - - + + - + - + + + - + + + + - + - - + + - - - - - + - + - + - - - - - - + + + - - - + - + - + - + - - - - - - + + + - - - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - + + - + - + - - - + + + + + - - + + - + - + - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - + @@ -1271,20 +1178,26 @@ z - - - - - - + + + + + + + + + + + + - - + + diff --git a/docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg b/docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg deleted file mode 100644 index ce5a048b..00000000 --- a/docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg +++ /dev/null @@ -1,1334 +0,0 @@ - - - - - - - - 2025-10-18T16:27:09.134509 - image/svg+xml - - - Matplotlib v3.10.5, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/benchmark_graphs/main.py b/docs/benchmark_graphs/main.py index ac4d23ac..035149f5 100644 --- a/docs/benchmark_graphs/main.py +++ b/docs/benchmark_graphs/main.py @@ -4,21 +4,51 @@ # uv run python main.py -N_DAYS_SHOWN = 60 +N_DAYS_SHOWN = 30 + +plt.rcParams.update({ + 'font.size': 12, # Base font size + 'axes.titlesize': 16, # Title font size + 'axes.labelsize': 12, # X and Y label font size + 'xtick.labelsize': 12, # X tick label font size + 'ytick.labelsize': 12, # Y tick label font size + 'legend.fontsize': 12, # Legend font size +}) -def create_duration_graph(data, target, target_label, title, y_legend, file): - dates = [] - values = [] - for day, duration in data: - dates.append(datetime.strptime(day, '%Y-%m-%d')) - values.append(duration) +def create_duration_graph(data, target=None, target_label=None, title="", y_legend="", file="", label1="Series 1", label2=None): + dates = [] + values1 = [] + values2 = [] + + # Check if data contains triplets or pairs + has_second_curve = len(data[0]) == 3 if data else False + + for item in data: + if has_second_curve: + day, perf1, perf2 = item + dates.append(datetime.strptime(day, '%Y-%m-%d')) + values1.append(perf1) + values2.append(perf2) + else: + day, perf1 = item + dates.append(datetime.strptime(day, '%Y-%m-%d')) + values1.append(perf1) color = '#2E86AB' + color2 = '#A23B72' # Different color for second curve + + _, ax = plt.subplots(figsize=(8, 4.8)) + ax.plot(dates, values1, marker='o', linewidth=2, + markersize=7, color=color, label=label1) - _, ax = plt.subplots(figsize=(10, 6)) - ax.plot(dates, values, marker='o', linewidth=2, - markersize=7, color=color) + # Plot second curve if it exists + if has_second_curve and label2 is not None: + ax.plot(dates, values2, marker='s', linewidth=2, + markersize=7, color=color2, label=label2) + all_values = values1 + values2 + else: + all_values = values1 min_date = min(dates) max_date = max(dates) @@ -32,15 +62,20 @@ def create_duration_graph(data, target, target_label, title, y_legend, file): plt.setp(ax.xaxis.get_majorticklabels(), rotation=50, ha='right') - ax.axhline(y=target, color=color, linestyle='--', - linewidth=2, label=target_label) + if target is not None and target_label is not None: + ax.axhline(y=target, color=color, linestyle='--', + linewidth=2, label=target_label) - ax.set_ylabel(y_legend, fontsize=12) - ax.set_title(title, fontsize=16, pad=15) + ax.set_ylabel(y_legend) + ax.set_title(title, pad=15) ax.grid(True, alpha=0.3) ax.legend() - ax.set_ylim(0, max(max(values), target) * 1.1) + # Adjust y-limit to accommodate both curves + max_value = max(all_values) + if target is not None: + max_value = max(max_value, target) + ax.set_ylim(0, max_value * 1.1) plt.tight_layout() plt.savefig(f'graphs/{file}.svg', format='svg', bbox_inches='tight') @@ -48,60 +83,44 @@ def create_duration_graph(data, target, target_label, title, y_legend, file): if __name__ == "__main__": - create_duration_graph(data=[ - ('2025-08-27', 85000), - ('2025-08-30', 95000), - ('2025-09-09', 108000), - ('2025-09-14', 108000), - ('2025-09-28', 125000), - ('2025-10-01', 185000), - ('2025-10-12', 195000), - ('2025-10-13', 205000), - ('2025-10-18', 210000), - ], target=300_000, target_label="Target (300.000 Poseidon2 / s)", title="Raw Poseidon2", y_legend="Poseidons proven / s", file="raw_poseidons") - - create_duration_graph(data=[ - ('2025-08-27', 2.7), - ('2025-09-07', 1.4), - ('2025-09-09', 1.32), - ('2025-09-10', 0.970), - ('2025-09-14', 0.825), - ('2025-09-28', 0.725), - ('2025-10-01', 0.685), - ('2025-10-03', 0.647), - ('2025-10-12', 0.569), - ('2025-10-13', 0.521), - ('2025-10-18', 0.411), - ], target=0.125, target_label="Target (0.125 s)", title="Recursive WHIR opening", y_legend="Proving time (s)", file="recursive_whir_opening") - - create_duration_graph(data=[ - ('2025-08-27', 14.2), - ('2025-09-02', 13.5), - ('2025-09-03', 9.4), - ('2025-09-09', 8.02), - ('2025-09-10', 6.53), - ('2025-09-14', 4.65), - ('2025-09-28', 3.63), - ('2025-10-01', 2.9), - ('2025-10-03', 2.81), - ('2025-10-07', 2.59), - ('2025-10-12', 2.33), - ('2025-10-13', 2.13), - ('2025-10-18', 1.96), - ], target=0.5, target_label="Target (0.5 s)", title="500 XMSS aggregated: proving time", y_legend="Proving time (s)", file="xmss_aggregated_time") - - create_duration_graph(data=[ - ('2025-08-27', 14.2 / 0.92), - ('2025-09-02', 13.5 / 0.82), - ('2025-09-03', 9.4 / 0.82), - ('2025-09-09', 8.02 / 0.72), - ('2025-09-10', 6.53 / 0.72), - ('2025-09-14', 4.65 / 0.72), - ('2025-09-28', 3.63 / 0.63), - ('2025-10-01', 2.9 / 0.42), - ('2025-10-03', 2.81 / 0.42), - ('2025-10-07', 2.59 / 0.42), - ('2025-10-12', 2.33 / 0.40), - ('2025-10-13', 2.13 / 0.38), - ('2025-10-18', 1.96 / 0.37), - ], target=2.0, target_label="Target (2x)", title="500 XMSS aggregated: zkVM overhead vs raw Poseidons", y_legend="", file="xmss_aggregated_overhead") + create_duration_graph( + data=[ + ('2025-10-26', 230_000, 620_000), + ('2025-10-27', 610_000, 1_250_000), + ], + target=1_500_000, + target_label="Target (1.5M Poseidon2 / s)", + title="Raw Poseidon2", + y_legend="Poseidons proven / s", + file="raw_poseidons", + label1="i9-12900H", + label2="mac m4 max" + ) + + create_duration_graph( + data=[ + ('2025-10-26', 0.411, 0.320), + ('2025-10-27', 0.425, 0.330), + ], + target=0.1, + target_label="Target (0.1 s)", + title="Recursive WHIR opening", + y_legend="Proving time (s)", + file="recursive_whir_opening", + label1="i9-12900H", + label2="mac m4 max" + ) + + create_duration_graph( + data=[ + ('2025-10-26', 255, 465), + ('2025-10-27', 314, 555), + ], + target=1000, + target_label="Target (1000 XMSS/s)", + title="number of XMSS aggregated / s", + y_legend="", + file="xmss_aggregated", + label1="i9-12900H", + label2="mac m4 max" + ) diff --git a/crates/lookup/cost_of_logup_star.pdf b/docs/cost_of_logup_star.pdf similarity index 100% rename from crates/lookup/cost_of_logup_star.pdf rename to docs/cost_of_logup_star.pdf diff --git a/docs/whirlaway_pdf/bibliography.bib b/docs/whirlaway_pdf/bibliography.bib deleted file mode 100644 index 555b434b..00000000 --- a/docs/whirlaway_pdf/bibliography.bib +++ /dev/null @@ -1,41 +0,0 @@ -@article{whir, - author = {Gal Arnon and Alessandro Chiesa and Giacomo Fenzi and Eylon Yogev}, - title = {{WHIR}: Reed–Solomon Proximity Testing with Super-Fast Verification}, - howpublished = {Cryptology {ePrint} Archive, Paper 2024/1586}, - year = {2024}, - url = {https://eprint.iacr.org/2024/1586} -} -@article{fri_binius, - author = {Benjamin E. Diamond and Jim Posen}, - title = {Polylogarithmic Proofs for EvaluationsLists over Binary Towers}, - howpublished = {Cryptology {ePrint} Archive, Paper 2024/504}, - year = {2024}, - url = {https://eprint.iacr.org/2024/504} -} -@article{ccs, - author = {Srinath Setty and Justin Thaler and Riad Wahby}, - title = {Customizable constraint systems for succinct arguments}, - howpublished = {Cryptology {ePrint} Archive, Paper 2023/552}, - year = {2023}, - url = {https://eprint.iacr.org/2023/552} -} -@article{simple_multivariate_AIR, - author = {William Borgeaud}, - title = {A simple multivariate AIR argument inspired by SuperSpartan}, - year = {2023}, - url = {https://solvable.group/posts/super-air/} -} -@article{hyperplonk, - author = {Binyi Chen and Benedikt Bünz and Dan Boneh and Zhenfei Zhang}, - title = {{HyperPlonk}: Plonk with Linear-Time Prover and High-Degree Custom Gates}, - howpublished = {Cryptology {ePrint} Archive, Paper 2022/1355}, - year = {2022}, - url = {https://eprint.iacr.org/2022/1355} -} -@article{univariate_skip, - author = {Angus Gruen}, - title = {Some Improvements for the {PIOP} for {ZeroCheck}}, - howpublished = {Cryptology {ePrint} Archive, Paper 2024/108}, - year = {2024}, - url = {https://eprint.iacr.org/2024/108} -} \ No newline at end of file diff --git a/docs/whirlaway_pdf/main.tex b/docs/whirlaway_pdf/main.tex deleted file mode 100644 index ce1f30b5..00000000 --- a/docs/whirlaway_pdf/main.tex +++ /dev/null @@ -1,729 +0,0 @@ -\documentclass{article} - -\usepackage[english]{babel} - -\usepackage[letterpaper,top=2cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry} - -% Useful packages -\usepackage{amsmath} -\usepackage{amssymb} -\usepackage{graphicx} -\usepackage{mathtools} -\usepackage{tikz} -\usepackage[colorlinks=true, allcolors=blue]{hyperref} -\usepackage{xcolor} -\usepackage{colortbl} -\usepackage{booktabs} -\usepackage{tikz} -\usepackage{tcolorbox} -\usetikzlibrary{positioning, arrows.meta} - -\newcommand{\Fp}{\mathbb F_p} -\newcommand{\Fq}{\mathbb F_q} -\newcommand{\Pol}{T} - -\title{Whirlaway: multilinear PIOP for AIR} -\author{Thomas Coratger, Tom Wambsgans} -\date{} -\begin{document} -\maketitle - -\section{Introduction} - -AIR (Algebraic Intermediate Representation) is a common arithmetization standard is the context of (ZK)-SNARK (Succint Non-Interactive Argument of Knowledge, potentially Zero-Knowledge). Traditionally, AIR constraints were proven using a univariate PIOP (Polynomial Interactive Oracle Proof), as explained in the EthStark paper \cite{eth_stark}. -Whirlaway is a simple multilinear PIOP, proving AIR constraints, focusing on lightweight proofs. - -The multilinear framework has two main advantages compared to classical approaches. First, it enables encoding and committing the entire AIR execution trace as a single multilinear polynomial, instead of encoding each table column as a separate low-degree univariate polynomial. In many modern proof systems, the correctness of a computation is expressed through multiple tables, each capturing specific CPU instructions (in the context of zkVMs) or constraints. To enable efficient verification, the prover encodes the columns of these tables as low-degree polynomials and commits to them using a polynomial commitment scheme. However, this design introduces a caveat: the prover must commit to and provide evaluation proofs for each polynomial separately, and the verifier must process and check each of these commitments individually. As the number of columns grows, the verifier’s workload and the overall proof size scale linearly, which can significantly impact efficiency. - -Second, the multilinear approach allows proving the validity of AIR constraints using the sumcheck protocol, which removes the need for a quotient polynomial. In the classical univariate approach, AIR constraints are bundled into a single vanishing condition, and the prover must construct and commit to a quotient polynomial that encodes the division of the constraint polynomial by the vanishing polynomial of the domain. The opening proof for the committed quotient polynomial again increases the proof size. From the prover perspective, using a quotient polynomial coupled with a FRI based PCS (superlinear time) may also be slower than the sumcheck (linear time) algorithm - - -Paired with the recent WHIR Polynomial Commitment Scheme \cite{whir}, we obtain a hash-based SNARK, plausibly post quantum, with small proof size (on the order of 128 KiB), and promising proving speed (an experimental implementation reached one million Poseidon2 permutations proven per second, on the KoalaBear field, with 128 bits of security, on a RTX 4090). - -An initial implementation can be found \href{https://github.com/TomWambsgans/Whirlaway}{here}. - -\section{Definitions and notations} - -\subsection{Multilinear framework} - -\subsubsection{Multilinear polynomial} - -A multilinear polynomial over variables $X_1, \dots, X_k$ is a multivariate polynomial where each variable appears with degree at most one. Formally, it can be written as: -$$ - P(X_1, \dots, X_k) = \sum_{S \subseteq [k]} c_S \prod_{i \in S} X_i, -$$ -where each $c_S \in \Fp$ is a coefficient, and the sum ranges over all subsets $S$ of ${1, \dots, k}$. For example, over two variables $X, Y$, the polynomial $3 + 2X + 5Y + 7XY$ is multilinear, but $X^2$ or $Y^3$ are not. - -A multilinear polynomial is uniquely identified by its evaluation on the boolean hypercube: - -$$ - P(X_1, \dots, X_k) = \sum_{b \in \{0, 1\}^k} eq((X_1, \dots, X_k), b) \cdot P(b) -$$ - - -\subsubsection{Multilinear extension} - - - -Given a function $f: \{0,1\}^n \to \Fp$, its multilinear extension $\widehat{f}: \Fp^n \to \Fp$ is the unique multilinear polynomial over $n$ variables that agrees with $f$ on all Boolean inputs. Explicitly, it can be written as: - -$$ -\widehat{f}(x) = \sum_{b \in \{0,1\}^n} f(b) \cdot eq(b, x) -$$ - -This extension allows evaluating the function not just at Boolean points, but at any point $x \in \Fp^n$. - - -\subsection{Notations} - -\begin{itemize} - \item $log$ is always in base 2 - \item $[i]_2$: big-endian bit decomposition of an integer $i$ - \item $eq(x, y) := \prod_{i = 1}^{n} (x_i y_i + (1 - x_i) (1 - y_i))$, for $x$ and $y$ in $\mathbb F^n$. This "equality multilinear polynomial" verifies: $eq(x, y) = 1$ if $x = y$, $0$ otherwise, for $x$ and $y$ both sampled on the hypercube $\{0, 1\}^n$. - \item $\Fp$: base field, typically KoalaBear ($p = 2^{31} - 2^{24} + 1$), or BabyBear ($p = 2^{31} - 2^{27} + 1$) - \item $\Fq$: extension field ($q = p^\kappa$) - \item $M$ $(\text{resp. } M')$: number of columns (resp. non-preprocessed columns) in the AIR table - \item $m$ $(\text{resp. } m')$: smallest integer such that $2^m \geq M$ (resp. $2^{m'} \geq M'$) - \item $N = 2^n$: number of rows in the AIR table - \item $h_1, \dots, h_u$: transition constraints - \item $H$: batched constraint ($H := \sum_{i=0}^{u-1} h_i \alpha^i $) - \item $\Pol$: multilinear polynomial in $\Fp$ encoding all the (non-preprocessed) columns, with $n + m'$ variables -\end{itemize} - -\section{AIR Arithmetization} - -\subsection{Description} - -In the AIR arithmetization, the witness consists of a list of $M$ columns $c_0, \dots, c_{M-1}$ (forming the AIR table). Each column contains $N = 2^n$ elements in $\Fp$ (without loss of generality, we use a power-of-two domain). The goal of the prover is to convince the verifier that the table respects a set of $u$ transition constraints $h_0, \dots, h_{u-1}$. Each constraint $h$ is a polynomial in $2 M$ variables, which is respected if for all rows $r \in \{0, \dots, N-2\}$: - -$$h(c_0[r], \dots, c_{M-1}[r], c_0[r+1], \dots, c_{M-1}[r+1]) = 0$$ - -\subsection{Preprocessed columns} - -Traditional AIR systems allow the verifier to fix certain cells in the table (see "boundary conditions" \href{https://aszepieniec.github.io/stark-anatomy/stark}{here}). For technical reasons, we use a slightly different approach: we allow the verifier to fix certain columns, potentially sparse (called "preprocessed columns"). The work of the verifier associated to each preprocessed column is proportional to its number of nonzero rows. We denote by $c_0, \dots, c_{M'-1}$ the non-preprocessed columns and $c_{M'}, \dots, c_{M-1}$ the preprocessed ones. - -\subsection{Example: Fibonacci sequence} - - -In general, each constraint $h_i$ is a polynomial relation over the current and next rows of the table. We use the notation $X_j^{\text{up}}$ to refer to the value of column $c_j$ at row $r$ (the “upper” row), and $X_j^{\text{down}}$ to refer to its value at row $r + 1$ (the “lower” row). These variables appear as inputs to the $h_i$ constraints, which together enforce the correct behavior across the table. Each constraint has the form: -$$ -h_i(\underbrace{X_0^{\text{up}}, \dots, X_{M-1}^{\text{up}}}_{\text{Upper row columns}}, \underbrace{X_0^{\text{down}}, \dots, X_{M-1}^{\text{down}}}_{\text{Lower row columns}}) = \text{some constraint} -$$ -where: -\begin{itemize} - \item $X_j^{\text{up}} = c_j[r]$ is the value of column $c_j$ at row $r$ - \item $X_j^{\text{down}} = c_j[r + 1]$ is the value of column $c_j$ at row $r + 1$ -\end{itemize} - -Let's say the prover wants to convince the verifier that the $N$-th values of the Fibonacci sequence equals $F_N$. We use $M = 4$ columns, as illustrated in Figure~\ref{fig:fibonacci_air}: - -\begin{itemize} - \item The first $M' = 2$ columns $c_0$ and $c_1$ contain the values of the Fibonacci sequence, which is guaranteed by the constraints: - -\begin{itemize} - \item $h_0$ constraint ensures that the next $c_1$ value equals the sum of the current $c_0$ and $c_1$ values, i.e., $F_{r+2} = F_r + F_{r+1}$: - - $$h_0(X_0^{\text{up}}, X_1^{\text{up}}, -, -, -, X_1^{\text{down}}, -, -) = X_1^{\text{down}} - (X_0^{\text{up}} + X_1^{\text{up}})$$ - \item $h_1$ constraint shifts the sequence forward, ensuring that the next $c_0$ value equals the current $c_1$ value: - - $$h_1(-, X_1^{\text{up}}, -, -, X_0^{\text{down}}, -, -, -) = X_0^{\text{down}} - X_1^{\text{up}}$$ -\end{itemize} - - \item The last two columns $c_2$ and $c_3$ are "preprocessed": their content is enforced by the verifier. In our case we set $c_2 = [1, 0, \dots, 0]$ to act as a selector for the initial row and $c_3 = [0, \dots, 0, 1]$ as a selector for the final row. We finally use the following constraints, to ensure that the 2 initial values of the sequence are correct ($0$ and $1$), and that the final value equals $F_N$: - - \begin{itemize} - \item $h_2$ ensures that $c_0=0$ when $c_2 \neq 0$ (which only occurs at the first row): - $$h_2(X_0^{\text{up}}, -, X_2^{\text{up}}, -, -, -, -, -) = X_2^{\text{up}} \cdot X_0^{\text{up}}$$ - - \item $h_3$ ensures $c_1 = 1$ when $c_2 \neq 0$ (only at the first row). - - $$h_3(-, X_1^{\text{up}}, X_2^{\text{up}}, -, -, -, -, -) = X_2^{\text{up}} \cdot (X_1^{\text{up}} - 1) $$ - -% (When the selector $c_2 \neq 0$ (which turns out to be the case at the initial row), we necessarily have $c_0 = 0$ and $c_1 = 1$) - - \item $h_4$ ensures $c_0 = F_N$ when $c_3 \neq 0$ (only at the last row). - - $$h_4(X_0^{\text{up}}, -, -, X_3^{\text{up}}, -, -, -, -) = X_3^{\text{up}} \cdot (X_0^{\text{up}} - F_n)$$ - -% (When the selector $c_3 \neq 0$ (which turns out to be the case at the final row), we necessarily have $c_0 = F_n$) - -\end{itemize} - -Note that $c_2$ and $c_3$ are sparse, both contain only one non-zero index. As a consequence, they have a negligible impact on the verification time. - - \end{itemize} - - -\begin{figure}[h!] -\centering -\begin{tabular}{ccccc} -\toprule -Row & $c_0$ & $c_1$ & $c_2$ (preproc.) & $c_3$ (preproc.) \\ -\midrule -0 & \cellcolor{blue!10}0 & \cellcolor{blue!10}1 & \cellcolor{orange!10}1 & \cellcolor{orange!10}0 \\ -1 & \cellcolor{blue!10}1 & \cellcolor{blue!10}1 & \cellcolor{orange!10}0 & \cellcolor{orange!10}0 \\ -2 & \cellcolor{blue!10}1 & \cellcolor{blue!10}2 & \cellcolor{orange!10}0 & \cellcolor{orange!10}0 \\ -$\vdots$ & $\vdots$ & $\vdots$ & $\vdots$ & $\vdots$ \\ -N-1 & \cellcolor{blue!10}$F_N$ & \cellcolor{blue!10}$F_{N+1}$ & \cellcolor{orange!10}0 & \cellcolor{orange!10}1 \\ -\bottomrule -\end{tabular} - -\vspace{1em} -\caption{Fibonacci sequence AIR table layout with preprocessed selectors} -\label{fig:fibonacci_air} -\end{figure} - -\section{Proving system} - -\subsection{{Commitment}} - -Contrary to most of the STARK systems, which use a univariate Polynomial Commitment Scheme (PCS), like FRI or KZG, we use instead a multilinear one. - - - -The entire AIR table is encoded and committed as a single multilinear polynomial $\Pol$ (except for the preprocessed columns, which are not committed). $\Pol$ has $n + m'$ variables, where $n = \log N = \log \text{(number of rows)}$ and $m' = \left\lceil \log M' \right\rceil = \left\lceil \log \text{(number of non-preprocessed columns)} \right\rceil$. $\Pol$ is defined by its evaluations over the boolean hypercube: for every (non-preprocessed) column index $i$ ($0 \leq i < M'$) and for every row index $r$ ($0 \leq r < N$): - -$$\Pol([i]_2 [r]_2) := c_{i}[r],$$ - -where: -\begin{itemize} - \item $[i]_2$ is the big-endian bit decomposition of $i$ into $m'$ bits (where $m' = \lceil \log M' \rceil$) - \item $[r]_2$ is the big-endian bit decomposition of $r$ into $n$ bits (where $n = \log N$) - \item $[i]_2 [r]_2$ represents concatenated $m' + n$ bits -\end{itemize} - -For example, if $M' = 20$, $N = 128$, $i = 3$, and $r = 33$, we have: -$$ -[i]_2 = 00011, \quad [r]_2 = 0100001, \quad [i]_2 [r]_2 = 00011 \, | \, 0100001. -$$ - -This means that $\Pol$ evaluates at the point corresponding to these bits to give: -$$ -\Pol(00011 \, | \, 0100001) = c_3[33] -$$ - -% Where $[i]_2$ and $[r]_2$ are the corresponding bit decomposition (big-endian) of $i$ and $r$ (e.g. $M' = 20, N = 128, i = 3, r = 33, [i]_2[r]_2 = (00011 | 0100001)$). - -The undefined evaluations (for indices $M' \leq i < 2^{m'}$) are irrelevant and can be set to zero. - -% Note that the coefficients of $\Pol$ are in the base field $\Fp$. The random evaluation point at which $\Pol$ will be queried later by the verifier is in the extension field $\Fq$ (for soundness). - -Note that the coefficients of $\Pol$ are in the base field $\Fp$, while the verifier will later query $\Pol$ at a random evaluation point drawn from the extension field $\Fq$. Querying over the larger extension field is important for soundness because it reduces the prover's chance of successfully cheating: the Schwartz-Zippel lemma guarantees that a nonzero polynomial will evaluate to zero at a random point with probability at most $\deg / |\Fq|$, and using a larger field strengthens this bound. - -One solution is to embed $\Pol$ into the extension field $\Fq$ before committing to it, but this create overhead. -Section~\ref{embedding_overhead} describes a simple and efficient approach to avoid this "embedding overhead" with WHIR. - -\subsection{Batching the constraints} - -After receiving the commitment to $\Pol$, the verifier sends a random scalar $\alpha \in \Fq$ to the prover. Up to a small soundness error, we can replace the $u$ transition constraints by a single one: - -$$H := \sum_{i=0}^{u-1} h_i \alpha^i $$ - -The batched constraint $H$ is a single combined polynomial that aggregates all the individual transition constraints $h_0, \dots, h_{u-1}$. Instead of checking each $h_i$ separately, we check $H$ once, which compresses the verification work while preserving soundness. - -\subsection{Zerocheck} \label{zerocheck} - -The main argument comes from \cite{ccs} (see also \cite{simple_multivariate_AIR}). For each column $c$, we define two multilinear polynomials over $n$ variables: -\begin{itemize} - \item The shifted copy $c^{\text{up}}$, - \item The forward-shifted copy $c^{\text{down}}$. -\end{itemize} - -Specifically, as illustrated in Figure~\ref{fig:zerocheck_mappings_n4}: - -\begin{equation} -\begin{aligned} -c^{\text{up}}([r]_2) &= -\begin{cases} -c[r] & \text{if } r \in \{0, \dots, N - 2\} \\ -c[N - 2] & \text{if } r = N - 1 -\end{cases} \\ -c^{\text{down}}([r]_2) &= -\begin{cases} -c[r + 1] & \text{if } r \in \{0, \dots, N - 2\} \\ -c[N - 1] & \text{if } r = N - 1 -\end{cases} -\end{aligned} -\end{equation} - - -\begin{figure}[h!] -\centering -\begin{tikzpicture}[ - node distance=0.2cm and 1.6cm, % Further reduced y-distance to 0.2cm - cnode/.style={draw, rectangle, minimum height=0.6cm, minimum width=1.3cm, anchor=center, thick}, % Reduced height - rnode/.style={minimum height=0.6cm, anchor=center, font=\ttfamily}, % Reduced height - arrowstyle/.style={-{Stealth[length=2.5mm, width=2mm]}, thick}, - labelstyle/.style={font=\small\itshape} -] - -\node[cnode] (c0) {$c[0]$}; -\node[cnode] (c1) [below=of c0] {$c[1]$}; -\node[cnode] (c2) [below=of c1] {$c[2]$}; -\node[cnode] (c3) [below=of c2] {$c[3]$}; -\node[labelstyle, above=0.15cm of c0] {Column $c$}; % Slightly reduced space - -% Inputs r for c^{up} (left) -\node[rnode] (r0_up) [left=of c0] {$0$}; -\node[rnode] (r1_up) [left=of c1] {$1$}; -\node[rnode] (r2_up) [left=of c2] {$2$}; -\node[rnode] (r3_up) [left=of c3] {$3$}; -\node[labelstyle, text width=2cm, align=center, above=0.15cm of r0_up] {$c^{\text{up}}$}; % Slightly reduced space - -\draw[arrowstyle] (r0_up.east) -- (c0.west); -\draw[arrowstyle] (r1_up.east) -- (c1.west); -\draw[arrowstyle] (r2_up.east) -- (c2.west); -\draw[arrowstyle] (r3_up.east) -- (c2.west); - -% Inputs r for c^{down} (right) -\node[rnode] (r0_down) [right=of c0] {$0$}; -\node[rnode] (r1_down) [right=of c1] {$1$}; -\node[rnode] (r2_down) [right=of c2] {$2$}; -\node[rnode] (r3_down) [right=of c3] {$3$}; -\node[labelstyle, text width=2cm, align=center, above=0.15cm of r0_down] {$c^{\text{down}}$}; % Slightly reduced space - -\draw[arrowstyle] (r0_down.west) -- (c1.east); -\draw[arrowstyle] (r1_down.west) -- (c2.east); -\draw[arrowstyle] (r2_down.west) -- (c3.east); -\draw[arrowstyle] (r3_down.west) -- (c3.east); - -\end{tikzpicture} -\caption{Visualization of $c^{\text{up}}$ and $c^{\text{down}}$ for $N=4$.} -\label{fig:zerocheck_mappings_n4} -\end{figure} - -The batched constraint $H$ is respected on the table if and only if: - -$$\begin{gathered} -\forall r \in \{0, \dots, N-2\}, \hspace{2mm} H(c_0[r], \dots, c_{M-1}[r], c_0[r+1], \dots, c_{M-1}[r+1]) = 0 \\ -\Leftrightarrow\\ -\forall r \in \{0, \dots, N-1\}, \hspace{2mm} H(c_0^{\text{up}}([r]_2), \dots, c_{M-1}^{\text{up}}([r]_2), c_0^{\text{down}}([r]_2), \dots, c_{M-1}^{\text{down}}([r]_2)) = 0 -\end{gathered}$$ - -The last equality can be proven using a zerocheck (see \cite{hyperplonk}), assuming the verifier has oracle access to $c_0^{\text{up}}, \dots, c_{M-1}^{\text{up}}$ and $ c_0^{\text{down}}, \dots, c_{M-1}^{\text{down}}$, which will be addressed in Section~\ref{shifted_mle}. The zerocheck is performed as follows: - -\begin{enumerate} - \item The verifier sends a random vector $r \in \Fq^n$, - \item Prover and verifier run the sumcheck protocol to prove that: - $$ \sum_{b \in \{0, 1\}^n} eq(b, r) \cdot H(c_0^{\text{up}}(b), \dots, c_{M-1}^{\text{up}}(b), c_0^{\text{down}}(b), \dots, c_{M-1}^{\text{down}}(b)) = 0 $$ - \item At each round $i$ of the sumcheck, the verifier sends a random challenge $\beta_i \in \Fq$. At the end of the final round, the verifier needs to evaluate the expression inside the sum above for $b \xleftarrow{} \beta = (\beta_1, \dots, \beta_n)$. - \begin{itemize} - \item The factor $eq(\beta, r)$ can be computed directly by the verifier. - \item For the remaining part, the prover provides the claimed evaluations (how the verifier checks the correctness of these values will be detailed in Section~\ref{shifted_mle}.): - \begin{equation*} - c_0^{\text{up}}(\beta), \dots, c_{M-1}^{\text{up}}(\beta), \quad c_0^{\text{down}}(\beta), \dots, c_{M-1}^{\text{down}}(\beta) - \end{equation*} - Given these $2M$ values, the verifier can finally evaluate $H$, which concludes the zerocheck. - \end{itemize} -\end{enumerate} - -\subsection{Oracle access to \texorpdfstring{$\textbf{\textit{c}}^{\text{up}}$}{} and \texorpdfstring{$\textbf{\textit{c}}^{\text{down}}$}{}}\label{shifted_mle} - -In Section~\ref{zerocheck}, for each column $c_i$, the prover has sent two values: $\text{claim}^\text{up}_i$ and $\text{claim}^\text{down}_i$ respectively equal to $c_i^{\text{up}}(\beta)$ and $c_i^{\text{down}}(\beta)$ in the honest case. It is now time to prove the correctness of these $2M$ evaluations. - -First, the verifier sends a random challenge $\gamma \in \Fq$. Except with small soundness error, the $2M$ claims can be reduced to the following: - -\begin{equation}\label{eq1} - \sum_{i = 0}^{M-1} (\gamma^i \cdot \text{claim}^\text{up}_i + \gamma^{i+M} \cdot \text{claim}^\text{down}_i) \stackrel{?}{=} \sum_{i = 0}^{M-1} (\gamma^i \cdot c_i^{\text{up}}(\beta) + \gamma^{i+M} \cdot c_i^{\text{down}}(\beta)) -\end{equation} - -\begin{itemize} - \item Left-hand side: the verifier can compute it directly using the claimed values. - \item Right-hand side: we need explicit formulas for $c_i^{\text{up}}$ and $c_i^{\text{down}}$ in terms of the multilinear extension of the the corresponding column $c_i$: -\end{itemize} - - -\subsubsection{\texorpdfstring{Expression of $\textbf{\textit{c}}^{\text{up}}$}{}} \label{shifted_mle_up} - -For every column $c$, for every $r \in \Fq^n$, we have: -\begin{equation*} - \begin{aligned} - c^{\text{up}}(r) &= \sum_{b \in \{0, 1\}^n} \text{shift}^{\text{up}}(r, b) \cdot \widehat{c}(b) \\ - &= \sum_{b \in \{0, 1\}^n} \left[\overbrace{eq(b, r) \cdot (1 - eq(r, (\underbrace{1, \dots, 1}_{n \text{ times}})))}^{\text{picks $c[r]$ when $r \neq N_1$ (i.e. before the last row)}} + \overbrace{eq((r, b), (\underbrace{1, \dots, 1}_{2n - 1 \text{ times}}, 0))}^{\text{picks $c[N - 2]$ when $r=N-1$}}\right] \cdot \widehat{c}(b), - \end{aligned} -\end{equation*} - -where: -\begin{itemize} - \item $\widehat{c}$ represents the multilinear extension (MLE) of $c$, - \item $\text{shift}^{\text{up}}(r, b)$ is a selector polynomial that determines how each $b$ contributes to the sum. -\end{itemize} - - -\subsubsection{\texorpdfstring{Expression of $\textbf{\textit{c}}^{\text{down}}$}{}} - - -For any column $c$, we define its “down-shifted” version $c^{\text{down}}$. For any point $r \in \Fq^n$, the expression is: -\begin{equation*} -\begin{aligned} -c^{\text{down}}(r) &= \sum_{b \in {0, 1}^n} \text{shift}^{\text{down}}(r, b) \cdot \widehat{c}(b) \\ -&= \sum_{b \in {0, 1}^n} \left[ \overbrace{\text{next}(r, b)}^{\text{picks $c[r + 1]$ when $r \neq N - 1$}} + \overbrace{eq((r, b), (\underbrace{1, \dots, 1}_{2n \text{ times}}))}^{\text{picks $c[N - 1]$ when $r = N - 1$ }} \right] \cdot \widehat{c}(b), -\end{aligned} -\end{equation*} - - - -where: -\begin{itemize} - \item $\widehat{c}$ represents the multilinear extension (MLE) of $c$, - \item $\text{shift}^{\text{down}}(r, b)$ is a selector polynomial that determines how each $b$ contributes to the sum, - \item "next" is the multilinear polynomial in $2n$ variables defined on the hypercube by: - $$\text{next}([x]_2 [y]_2) = \begin{cases} - 1 & \text{if } y = x +1\\ - 0 & \text{otherwise} - \end{cases} \text{ for every pair of n-bit integers } (x, y)$$ - - See section 5.1 of \cite{ccs} for more details. -\end{itemize} - - - - -\subsubsection{Final sumcheck} - -The right side of (\ref{eq1}) can thus be expressed as: - -$$\sum_{b \in \{0, 1\}^n} \underbrace{\sum_{i = 0}^{M-1} [\gamma^i \cdot \text{shift}^{\text{up}}(\beta, b) + \gamma^{i+M} \cdot \text{shift}^{\text{down}}(\beta, b) ] \cdot \widehat{c}_i(b)}_{\text{expr}(\beta, b)}$$ - -A second sumcheck (with respect to $b$) is used to compute this sum. Let $\delta \in \Fq^n$ be the corresponding vector of challenges. The verifier must finally evaluate $\text{expr}(\beta, \delta)$. Both $\text{shift}^{\text{up}}$ and $\text{shift}^{\text{down}}$ can be succinctly computed. It remains $(\widehat{c}_i(\delta))_{0 \leq i < M}$, that is, the values of the columns’ multilinear extensions at the common point $\delta$. - -\subsection{PCS opening} - -At the end of the protocol, the verifier needs to check that the prover’s claimed evaluations of the multilinear extensions $\widehat{c}_i$ at the point $\delta$ are correct. This is done as follows: - -\begin{enumerate} -\item For the preprocessed columns ($M' \leq i < M$), the verifier can directly compute the values $\widehat{c}_i(\delta)$, since the verifier already knows these columns. - -\item For the non-preprocessed columns ($0 \leq i < M'$), the prover sends the claimed values $v_i$, which should equal $\widehat{c}_i(\delta)$ if the prover is honest. - -\item To efficiently verify all these claims in one go, the verifier samples a random vector $z \in \Fq^{m'}$, where $m' = \lceil \log M' \rceil$. This vector selects a random linear combination over the non-preprocessed columns. - -% The verifier samples a random vector \( z \in \Fq^{m'} \), where \( m' = \lceil \log M' \rceil \). - -\item Using this $z$, the verifier computes the combined evaluation: -$$ -\sum_{i \in \{0, 1\}^{m'}} eq(i, z) \cdot v_i, -$$ -which collapses all the prover’s claims into a single value. Here, $eq(i, z)$ acts as a selector that matches the random point $z$. -% and requests a PCS opening at the point \( \Pol((z, \delta)) \). - -% \item Finally, the verifier checks that the two evaluations match. - -\item Finally, the verifier requests the prover to open the committed multilinear polynomial $\Pol$ at the combined point $(z, \delta)$ and checks that the opening matches the combined evaluation computed above. -\end{enumerate} - -This process reduces many separate checks into just one, saving both prover and verifier time, while preserving soundness through the use of random linear combinations. - - - -\section{Univariate skip} - -\subsection{The traditional sumcheck protocol} - -Let's consider the case of the zerocheck (see Section~\ref{zerocheck}), which is the most sumcheck-intensive part of the protocol. At this stage, the prover wants to convince the verifier that: - -$$ \sum_{b \in \{0, 1\}^n} eq(b, r) \cdot H(c_0^{\text{up}}(b), \dots, c_{M-1}^{\text{up}}(b), c_0^{\text{down}}(b), \dots, c_{M-1}^{\text{down}}(b)) = 0$$ - -The traditional sumcheck protocol operates across $n$ rounds, one per variable: - -\begin{itemize} - \item \textbf{Round 1:} - - The prover starts by sending $P_1(X)$, supposedly equal to: - - $$ \sum_{b \in \{0, 1\}^{n-1}} eq((X, b), r) \cdot H(c_0^{\text{up}}(X, b), \dots, c_{M-1}^{\text{down}}(X, b)), $$ - - where $ H(c_0^{\text{up}}(X, b), \dots, c_{M-1}^{\text{down}}(X, b)) = H(c_0^{\text{up}}(b), \dots, c_{M-1}^{\text{up}}(b), c_0^{\text{down}}(b), \dots, c_{M-1}^{\text{down}}(b))$ for the sake of simplicity. After receiving $P_1$, the verifier checks that $P_1(0) + P_1(1) = 0$ and responds with a random challenge $\beta_1 \in \Fq$. - \item \textbf{Round 2:} - - The prover then sends $P_2(X)$, supposedly equal to - - $$ \sum_{b \in \{0, 1\}^{n-2}} eq((\beta_1, X, b), r) \cdot H(c_0^{\text{up}}(\beta_1, X, b), \dots, c_{M-1}^{\text{down}}(\beta_1, X, b)) $$ - - After receiving $P_2$, the verifier checks that $P_1(\beta_1) = P_2(0) + P_2(1)$ and responds with a random challenge $\beta_2 \in \Fp$. - \item Continue for $n$ rounds, until all variables are fixed to random points. - \item At the end of the protocol the verifier must check (with $\beta = (\beta_1, \dots, \beta_n)$): - - $$P_n(\beta_n) = eq(\beta, r) \cdot H(c_0^{\text{up}}(\beta), \dots, c_{M-1}^{\text{down}}(\beta))$$ - -\end{itemize} - -\subsection{Where the overhead comes from} - -For soudness reason, if the base field $\Fp$ is too small, the random challenges $\beta$ must be sampled in an extension field $\Fq$. While the first round of the sumcheck happens entirely inside the cheaper base field $\Fp$, the moment the verifier sends $\beta_1 \in \Fq$, the prover is forced to fold the multilinear polynomials over $\Fq$ —meaning every subsequent round involves more expensive operations in the extension field. - - -Indeed, after receiving the first challenge $\beta_1 \in \Fq$, the prover must compute the "folded" multilinear polynomials $c_0^{\text{up}}(\beta_1, \cdot), \dots, c_{M-1}^{\text{down}}(\beta_1, \cdot)$, whose coefficients are now in the extension field $\Fq$. -This may significantly slow down the consecutive rounds, compared to the first one. - -In summary, from the prover perspective, the first round is computationally less expensive than the next few ones. The \emph{univariate skip} optimization\cite{univariate_skip} leverages this asymmetry: by reorganizing the domain, we can perform the first $k$ rounds of sumcheck all at once, entirely within the base field $\Fp$. This leads to major efficiency improvements. - - -\subsection{Changing the evaluation domain} - -The univariate skip optimization (see \cite{univariate_skip}) improves prover efficiency by cleverly restructuring the evaluation domain. Traditionally, the sumcheck protocol works over the Boolean hypercube ${0,1}^n$, handling one variable per round. To skip $k$ variables all at once, we reshape the domain as: -\begin{equation} - D \times \{0,1\}^{n - k} -\end{equation} -where $D \subset \Fp$ is an arbitrary subset of size $2^k$— for example, $D = {0, \dots, 2^k - 1}$. This effectively replaces the first $k$ boolean variables with a single univariate variable over $D$, while keeping the remaining $n - k$ variables in the usual boolean space. - -\vspace{\baselineskip} - -With this new perspective, we define, for $i \in \{0, \dots, M -1 \}$, $\widetilde{c_i}^{\text{up}}(X_1, \dots, X_{n + 1 - k})$ as the polynomial, with degree less than $ 2^k$ in $X_1$, and multilinear in the remaining variables, such that: - -$$\widetilde{c_i}^{\text{up}}(x, b_1, \dots , b_{n - k}) = c_i^{\text{up}}([x]_2, b_1, \dots , b_{n - k}),$$ -for all $x \in D$ and $(b_1, \dots, b_{n - k}) \in \{0, 1\}^{n - k}$, where $[x]_2$ represents the big-endian decomposition of $x$ in $k$ bits. - -\vspace{\baselineskip} - -We define $\widetilde{c_i}^{\text{down}}$ similarly. - - -\vspace{\baselineskip} - -Finally, we define $\widetilde{eq}(X_1, \dots, X_{n + 1 - k}, Y_1, \dots, Y_{n + 1 - k})$ as the polynomial, with degree $< 2^k$ in $X_1$ and $Y_1$, and multilinear in the remaining variables, such that: -$$ -\widetilde{eq}(x, y) = -\begin{cases} -1 & \text{if } x = y \\ -0 & \text{otherwise} -\end{cases} -$$ - -for all $(x, y) \in (D \times {0,1}^{n - k})^2$. - - -\subsection{Zerocheck in the new domain} - -To apply the univariate skip, we need to slightly adapt the zerocheck protocol. Previously, the random challenge $r$ for the zerocheck was drawn from $\Fq^n$, matching the $n$ variables of the hypercube $\{0,1\}^n$. Now, after restructuring the domain to $D \times \{0,1\}^{n - k}$, the challenge must be drawn from the new space: -$$ -r \in \Fq^{n + 1 - k}, -$$ -where the first coordinate corresponds to the univariate component $x \in D$, and the remaining coordinates correspond to the $n - k$ boolean variables. With this setup, the zerocheck equation becomes: -$$ -\sum_{x \in D,\, b \in \{0,1\}^{n - k}} \widetilde{eq}((x, b), r) \cdot H\big(\widetilde{c_0}^{\text{up}}(x, b), \dots, \widetilde{c}_{M-1}^{\text{up}}(x, b),\, \widetilde{c_0}^{\text{down}}(x, b), \dots, \widetilde{c}_{M-1}^{\text{down}}(x, b)\big) \stackrel{?}{=} 0, -$$ - -where: -\begin{itemize} - \item $\widetilde{eq}((x, b), r)$ is the extended equality polynomial matching the combined domain. - \item $H$ is the batched constraint polynomial, evaluating the sum of all constraints at the selected point. -\end{itemize} - - -There is a key difference in the sumcheck protocol. In the traditional hypercube case, after the prover sends the first-round polynomial $P_1$, the verifier checks: -$$ -P_1(0) + P_1(1) = 0, -$$ -but in the new setting, the verifier instead checks: -$$ -\sum_{x \in D} P_1(x) = 0, -$$ -where $D$ has size $2^k$. This adjustment reflects the fact that, thanks to univariate skip, we are treating the first $k$ Boolean variables as one combined univariate block. - -Importantly, when $k = 1$, this construction reduces back to the traditional sumcheck on the hypercube —meaning the univariate skip is a strict generalization of the classic protocol. - - -In summary, by reorganizing the domain and adapting the sumcheck’s first step, the univariate skip allows the prover to efficiently collapse multiple rounds into a single, larger but cheaper univariate sum, all while staying in the base field. - -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -% We need to adapt the protocol: the random zerocheck challenge $r$ is now sampled in $\Fq^{n + 1 - k}$ (instead of $\Fq^n)$. The zerocheck becomes: - -% $$ \sum_{x \in D, b \in \{0, 1\}^{n - k}} \widetilde{eq}((x, b), r) \cdot H(\widetilde{c_0}^{\text{up}}(x, b), \dots, \widetilde{c}_{M-1}^{\text{ up}}(x, b), \widetilde{c_0}^{\text{down}}(x, b), \dots, \widetilde{c}_{M-1}^{\text{ down}}(x, b)) \stackrel{?}{=} 0$$ - -% This equality can be proven using the sumcheck protocol, as before. Only difference: after receiving $P_1$, the verifier checks $\sum_{x \in D} P_1(x) = 0$ (instead of $P_1(0) + P_1(1) = 0$). - -% This is no more, no less, the univariate skip. - -% Note that $k = 1$ corresponds to the traditional sumcheck on the hypercube. - -\subsection{Cost analysis} - -Let’s break down the cost for the prover when using univariate skip. First, consider the degree of the first-round polynomial $P_1$. Because we collapse $k$ variables at once over the set $D$ (with $|D| = 2^k$), -the degree bound becomes: - -$$\text{deg}(P_1) = (\text{deg}(H) + 1) (2^k - 1),$$ -where $\deg(H)$ is the degree of the batched constraint polynomial. - - -% Note that $\forall x \in D, P_1(x) = 0 $, which gives $2^k$ "free" evaluations to the prover when interpolating $P_1$. - - -An important observation is that for all $x \in D$, we already know that $P_1(x) = 0$. These points correspond to the valid domain, so they provide $2^k$ "free" evaluations that the prover doesn’t need to compute explicitly when interpolating $P_1$. - -However, to fully interpolate $P_1$, the prover still needs to compute the remaining evaluations. For each such point, the prover sums over $2^{n - k}$ combinations (from the $n - k$ residual variables). Thus, the total number of prover operations to interpolate $P_1$ is roughly: - -$$[(\text{deg}(H) + 1) (2^k - 1) - 2^k + 1] \cdot 2^{n - k} = \text{deg}(H) (2^k - 1) \cdot 2^{n - k}$$ - -% This turns out to be equal to $\sum_{i = 1}^k {\text{deg}(H) \cdot 2^{n - i}}$, the number of corresponding operations to perform the first $k$ rounds of the traditional sumcheck, but in which case, the rounds $2, 3, \dots, k$ operate over the extension field, and are thus slower. - - -Interestingly, this matches the cost of performing the first $k$ rounds of the traditional sumcheck: -$$ -\sum_{i = 1}^k \deg(H) \cdot 2^{n - i}. -$$ - -But with one key difference. In the traditional protocol, rounds $2, 3, \dots, k$ involve folded polynomials -with coefficients in the extension field $\Fq$, which makes them significantly more expensive to compute. In contrast, univariate skip pushes all this work into the first round, but keeps it entirely within the base field $\Fp$, delivering major efficiency gains in practice. - -\subsection{Rest of the protocol} - -After finishing the zerocheck, the verifier still needs to verify the claimed values of the shifted polynomials. Specifically, he wants to check the evaluations of: - -$$ -\widetilde{c_0}^{\text{up}}, \, \widetilde{c_1}^{\text{up}}, \, \dots, \, \widetilde{c}_{M-1}^{\text{up}}, \quad \widetilde{c_0}^{\text{down}}, \, \widetilde{c_1}^{\text{down}}, \, \dots, \, \widetilde{c}_{M-1}^{\text{down}}, -$$ - -at a random point: - -$$ -\beta = (\beta_1, \beta_2, \dots, \beta_{n + 1 - k}) \in \Fq^{n + 1 - k}. -$$ - -Let’s break down how these are computed. For each column $c$, we have: -$$ -\widetilde{c}^{\text{up}}(\beta) = \sum_{x \in D} \widetilde{eq}(x, \beta_1) \cdot c^{\text{up}}([x]_2, \beta_2, \dots, \beta_{n + 1 - k}). -$$ - -This expression essentially says: -\begin{itemize} - \item For the first coordinate $\beta_1$, we match it against $x \in D$ using the equality polynomial $\widetilde{eq}$. - \item For each $x$, we map it back to its $k$-bit decomposition $[x]_2$ and evaluate the original $c^{\text{up}}$. -\end{itemize} - -We can further rewrite it as: - -$$ -\widetilde{c}^{\text{up}}(\beta) = \sum_{b \in \{0,1\}^k} \Gamma(b, \beta_1) \cdot c^{\text{up}}(b, \beta_2, \dots, \beta_{n + 1 - k}), -$$ -where: -$$ -\Gamma(b, \beta_1) := \sum_{x \in D} eq(b, [x]_2) \cdot \widetilde{eq}(x, \beta_1). -$$ - -The key point is that $\Gamma$ can be computed efficiently by the verifier. To prove the correctness of these evaluations, we could: -\begin{itemize} - \item Either run another sumcheck to reduce the claims about $\widetilde{c}^{\text{up}}$ back to the original $c^{\text{up}}$ (handled as described in Section~\ref{shifted_mle}), - \item Or more directly, expand $c^{\text{up}}$ itself (see Section~\ref{shifted_mle_up}) in terms of the base column $c$, allowing us to run a single sumcheck over $n + k$ variables. -\end{itemize} - -The same approach applies symmetrically for all the $\widetilde{c}^{\text{down}}$ terms. Finally, all these evaluation checks (for the up- and down-shifted polynomials) can be batched together using a random challenge, so that the prover and verifier only need to perform one combined sumcheck over $n + k$ variables. - - - -% Every column $\widetilde{c}^{\text{ up}}$ is defined by: - -% \begin{align} -% \widetilde{c}^{\text{up}}(\beta) &= \sum_{x \in D} \widetilde{eq}(x, \beta_1) \cdot c^{\text{up}}([x]_2, \beta_2, \dots, \beta_{n + 1 - k}) \\ -% &= \sum_{b \in \{0,1\}^k} \underbrace{\sum_{x \in D} eq(b, [x]_2) \cdot \widetilde{eq}(x, \beta_1)}_{\Gamma(b, \alpha_1)} c^{\text{up}}(b, \beta_2, \dots, \beta_{n + 1 - k}) -% \end{align} - -% $\Gamma$ can be efficiently computed by the verifier. - -% As a consequence, we could, again, use the sumcheck protocol to reduce an evaluation claim about $\widetilde{c}^{\text{ up}}$ to one about $c^{\text{up}}$, which is handled by \ref{shifted_mle}. In a more direct manner, we prefer to expand the expression of $c^{\text{up}}$ (see \ref{shifted_mle_up}) in terms of $c$, to get a single sumcheck on $n + k$ variables. - -% The same naturally applies for all the $\widetilde{c}^{\text{ down}}$. - -% And all these sumcheck above can be batched (using a random challenge) into a single one, in $n + k$ variables. - -\section{Avoiding the "embedding overhead" in WHIR}\label{embedding_overhead} - -In its simplest form, a polynomial commitment scheme (PCS) operates over a single finite field: the prover commits to a polynomial with coefficients in that field and later opens it at a chosen evaluation point, all within the same arithmetic setting. However, in many modern constructions, the situation is more nuanced. It’s common for the committed polynomial to have coefficients in a small base field $\Fp$, while the verifier requests openings at points lying in a larger extension field $\Fq$. This shift introduces what’s known as the embedding overhead — the prover must effectively “lift” or embed the polynomial’s coefficients into the larger field before opening, which can add significant computational cost. - -To address this, prior work introduced the ring-switching protocol (see Section 3 of \cite{fri_binius}), which provides an elegant way to sidestep the embedding cost by switching the representation of the polynomial directly into the extension field. While this technique is especially well-suited for binary tower fields, it comes with two main limitations: -\begin{enumerate} - \item The extension degree $[\Fq:\Fp]$ must be a power of two, which restricts field choices. - \item It adds complexity to the protocol, both conceptually and in terms of implementation effort. -\end{enumerate} - -In this section, we propose a simpler alternative to avoid the embedding overhead specifically for the WHIR PCS construction introduced in \cite{whir}. Our approach achieves the same efficiency benefits as ring-switching, but without its rigidity or added protocol complexity — offering a more streamlined and flexible design. - - -\subsection{Ring-Switching costs} - -% Let's first analyze the prover costs when using "ring-switching" on a multilinear polynomial $P$ with $v$ variables in the base field $\Fp$. -% $P$ will be reinterpreted as a multilinear polynomial $P'$ with $v-d$ variablesin $\Fq$, with $[\Fq:\Fp] = 2^d$. - - -To understand the efficiency tradeoffs, let’s first break down the prover’s costs when using the ring-switching approach on a multilinear polynomial $P$ with $v$ variables over the base field $\Fp$. Under ring-switching, the polynomial $P$ is reinterpreted as a multilinear polynomial $P'$ over the larger extension field $\Fq$, where the number of variables is reduced to $v - d$ (with the extension degree $[\Fq:\Fp] = 2^d$). - - -We’ll also introduce: -\begin{itemize} - \item $\rho = 1 / 2^r$, the initial code rate of the Reed–Solomon (RS) code, - \item $f_1, f_2, \dots$, the folding factors applied at each round (these match the $k$ values defined in Section 2.1.3 of \cite{whir}). -\end{itemize} - -% Let's denote by $\rho = 1 / 2^r$ the initial rate of the Reed Solomon (RS) code, and by $f_1, f_2, \dots$ the folding factors of each round (corresponding to values of $k$ in section 2.1.3 of \cite{whir}). - - -In the first round of the protocol, the prover’s dominant cost comes from performing $2^{f_1}$ Fast Fourier Transforms (FFTs), each over a domain of size: -$$ -2^{v - d + r - f_1} -$$ -in the extension field $\Fq$. - -In the second round, the RS domain size is effectively halved. The prover’s work is now dominated by $2^{f_2}$ FFTs, each over a (smaller) domain of size: -$$ -2^{v - d + r - 1 - f_2} -$$ -again over $\Fq$. - - -This pattern continues across subsequent rounds: as the folding progresses, the RS domain keeps shrinking, and the prover’s cost scales with both the number of FFTs and the size of the domains on which they operate. - - - -% During the first round, the prover cost is dominated by $2^{f_1}$ FFTs (Fast Fourrier Transforms), each over a domain of size $2^{v - d + r - f_1}$ (over $\Fq$). For the second round, the size of the RS domain is halved, and the prover costs are dominated by $2^{f_2}$ FFTs, each over a domain of size $2^{v - d + r - 1 - f_2}$. And so on \dots. - -\subsection{Embedding costs} - -Without applying the ring-switching protocol, the prover’s workload in the first round remains quite similar to the ring-switching case. Specifically, the prover must perform $2^{f_1}$ FFTs, each over a domain of size $2^{v + r - f_1}$, but crucially, these computations take place over the smaller base field $\Fp$. While the FFT domain is $2^d$ times larger than in the ring-switching setup, the field itself is $2^d$ times smaller, making the overall computational effort comparable in this initial round. - -However, the situation changes significantly in the subsequent rounds. After the first round, the polynomial has been folded, and the prover is now forced to operate over the larger extension field $\Fq$. In the second round, for example, the prover must carry out $2^{f_2}$ FFTs, each over a domain of size $2^{v + r - 1 - f_2}$. Importantly, the Reed–Solomon domain in this setting is $2^d$ times larger compared to what it would be under ring-switching, since the base-to-extension embedding was avoided. - -As a result, although the first-round costs are effectively balanced, the subsequent rounds become notably more expensive. This slowdown arises from having to perform FFTs over significantly larger domains in the extension field, eroding the overall efficiency that ring-switching was designed to protect. Over multiple rounds, this cumulative cost becomes a meaningful disadvantage, motivating the search for alternative strategies to bypass the embedding overhead. - - -% Without the use of the "ring-switching" protocol, the prover costs for the first round are fairly similar: $2^{f_1}$ FFTs, each over a domain of size $2^{v + r - f_1}$ (over $\Fp$). The domain of the FFTs is $2^d$ times larger, but the field is $2^d$ times smaller. Unfortunately, the next rounds are slower. The second one requires $2^{f_2}$ FFTs, each over a domain of size $2^{v + r - 1 - f_2}$, but now over the extension field $\Fq$ (the polynomial has been folded in the first round). The Reed Solomon domain size is $2^d$ larger compared to "ring-switching". As a result, all except the first rounds are slower, due to FFTs over a larger domain, on the same extension field. - -\subsection{A simple solution} - -% To tackle this overhead, we suggest the following approach: -% \begin{enumerate} -% \item In the first round, increase the folding factor to $f_1' = f_1 + d$. The consecutive folding factors are unaffected: $f_2' = f_2, \dots$ -% \item When transitioning from the first to the second round, instead of dividing by 2 the size of the Reed Solomon domain, divide it by $2^{d + 1}$. -% \end{enumerate} - -% The prover cost for the first round are similar compared to "ring-switching": $2^d$ times more FFTs, but on a field $2^d$ times smaller. -% Starting from the second round, costs become identical: same number of FFTs, over the same domain, with the same field - -% So from the prover perspective, this approach is as efficient as using ring-switching. - -% It turns out it is also the case in terms of verification costs, and proof size: - -% \begin{itemize} -% \item The code rate at each round is the same: Initially $1/2^r$, then $1/2^{r + f_1 - 1}$, $1/2^{r + f_1 + f_2 - 2}$ \dots. -% \item The size of each Merkle tree leaf is the same. The main difference is at the first round, it consists in $2^{f_1}$ elements of $\Fq$ with ring-switching, while it is $2^{f_1 + d}$ elements of $\Fp$ with our approach (both types require the same of bytes to be represented). -% \end{itemize} - -To address the overhead introduced by avoiding ring-switching, we propose a straightforward and effective adjustment to the protocol. Specifically, we suggest increasing the folding factor in the first round to $f_1' = f_1 + d$, where $d$ is the extension degree (i.e., $[\Fq : \Fp] = 2^d$). The folding factors for all subsequent rounds remain unchanged, so $f_2' = f_2$, $f_3' = f_3$, and so on. - -Additionally, when transitioning from the first to the second round, we propose adjusting the Reed–Solomon domain reduction: rather than simply halving the domain size as in the traditional approach, the domain should be divided by $2^{d + 1}$. This adjustment compensates for the lack of ring-switching and aligns the domain size with that of the equivalent protocol using ring-switching. - -With this modification, the prover’s costs in the first round remain comparable to those under ring-switching. While the prover now performs $2^d$ times more FFTs, these are carried out over a field that is $2^d$ times smaller, maintaining a balanced computational load. -$$ -\text{Prover cost (first round)} \sim 2^d \times \text{FFTs over } \Fp \quad \text{vs.} \quad \text{FFTs over } \Fq. -$$ - -From the second round onward, the prover’s workload becomes essentially identical to that of the ring-switching setup: the number of FFTs, the domain sizes, and the field arithmetic all align. - -Importantly, this adjustment not only preserves prover efficiency but also ensures that verification costs and proof size remain on par with the ring-switching protocol. At each round, the code rate progression is preserved: it begins at $1/2^r$, then advances to $1/2^{r + f_1 - 1}$, then to $1/2^{r + f_1 + f_2 - 2}$, and so forth. Furthermore, the size of each Merkle tree leaf remains equivalent. The only notable difference is that, in the first round, ring-switching commits to $2^{f_1}$ elements in the extension field $\Fq$, while our adjusted approach commits to $2^{f_1 + d}$ elements in the base field $\Fp$. Crucially, both representations occupy the same number of bytes, preserving the compactness of the proof. - - - -\bibliographystyle{IEEEtran} -\bibliography{bibliography} - -\end{document} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e7a11a96..a0c1db73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,41 @@ +use clap::Parser; +use poseidon_circuit::tests::run_poseidon_benchmark; +use rec_aggregation::{ + recursion::run_whir_recursion_benchmark, xmss_aggregate::run_xmss_benchmark, +}; + +#[derive(Parser)] +enum Cli { + #[command(about = "Aggregate XMSS signature")] + Xmss { + #[arg(long)] + n_signatures: usize, + }, + #[command(about = "Run 1 WHIR recursive proof")] + Recursion, + #[command(about = "Prove validity of Poseidon2 permutations over 16 field elements")] + Poseidon { + #[arg(long, help = "log2(number of Poseidons)")] + log_n_perms: usize, + }, +} + fn main() { - println!("Hello, world!"); + let cli = Cli::parse(); + + match cli { + Cli::Xmss { + n_signatures: count, + } => { + run_xmss_benchmark(count); + } + Cli::Recursion => { + run_whir_recursion_benchmark(); + } + Cli::Poseidon { + log_n_perms: log_count, + } => { + run_poseidon_benchmark(log_count, false); + } + } } From cb16068baecb8386e4f3009b1fa9cdb625feac50 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 27 Oct 2025 16:53:21 +0400 Subject: [PATCH 41/42] revert reseting benchmarks --- README.md | 3 +- .../benchmark_graphs/graphs/raw_poseidons.svg | 856 ++++++---- .../graphs/recursive_whir_opening.svg | 840 +++++---- .../graphs/xmss_aggregated.svg | 785 +++++---- .../graphs/xmss_aggregated_overhead.svg | 1517 +++++++++++++++++ docs/benchmark_graphs/main.py | 98 +- 6 files changed, 3136 insertions(+), 963 deletions(-) create mode 100644 docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg diff --git a/README.md b/README.md index e2c8ccac..9980faeb 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,11 @@ RUSTFLAGS='-C target-cpu=native' cargo run --release -- xmss --n-signatures 800 [Trivial encoding](docs/XMSS_trivial_encoding.pdf) (for now). -Overhead versus raw Poseidons: ≈ 10x ![Alt text](docs/benchmark_graphs/graphs/xmss_aggregated.svg) +![Alt text](docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg) + ### Proof size With conjecture "up to capacity", current proofs with rate = 1/2 are about ≈ 400 - 500 KiB, of which ≈ 300 KiB comes from WHIR. diff --git a/docs/benchmark_graphs/graphs/raw_poseidons.svg b/docs/benchmark_graphs/graphs/raw_poseidons.svg index 4fda3fdc..85ddd9fd 100644 --- a/docs/benchmark_graphs/graphs/raw_poseidons.svg +++ b/docs/benchmark_graphs/graphs/raw_poseidons.svg @@ -1,12 +1,12 @@ - + - 2025-10-27T16:10:57.164038 + 2025-10-27T16:50:33.815256 image/svg+xml @@ -21,18 +21,18 @@ - - @@ -40,38 +40,24 @@ z - + - - + - - + + - - - + + + - - + + - + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - - - - + + + - - + + - - + + - + - - + - + - + - - + + - + - + - + - + @@ -365,19 +580,19 @@ L 555.9375 255.18462 - - + + - + - + - + - + @@ -385,51 +600,19 @@ L 555.9375 224.725707 - - + + - + - + - + - - - - + @@ -437,19 +620,19 @@ z - - + + - + - + - + - + @@ -457,19 +640,19 @@ L 555.9375 163.80788 - - + + - + - + - + - + @@ -477,19 +660,19 @@ L 555.9375 133.348967 - - + + - + - + - + - + @@ -497,19 +680,19 @@ L 555.9375 102.890054 - - + + - + - + - + - + @@ -517,28 +700,28 @@ L 555.9375 72.431141 - - + + - + - + - + - + - + - + - + @@ -782,12 +965,20 @@ z - - + + - - - - + + + + + + + + + + + - - + + - - - - + + + - - + + - - - - + - + - - - + - + - + - + - - - + - + - + - + - - + - + - + + - + + diff --git a/docs/benchmark_graphs/graphs/recursive_whir_opening.svg b/docs/benchmark_graphs/graphs/recursive_whir_opening.svg index 16168c50..d2fae0fa 100644 --- a/docs/benchmark_graphs/graphs/recursive_whir_opening.svg +++ b/docs/benchmark_graphs/graphs/recursive_whir_opening.svg @@ -1,12 +1,12 @@ - + - 2025-10-27T16:10:57.245462 + 2025-10-27T16:50:33.913644 image/svg+xml @@ -21,18 +21,18 @@ - - @@ -40,38 +40,24 @@ z - + - - + - - + + - - - + + + - - + + - + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - - - - + + + - - + + - - + + - + - - + - + - + - - + + - + - + - - - + + + + + + - + - - + + - + - + - - - - + + + + - + - - + + - + - + - - - - - - - + + + + - + - - + + - + - + - - - - + + + + - + - + + + + + + + + + + + + + + + + + + + - + - + + - - - - + + + + + + + + + + + + + - - + + - - - - + + + - - + + - - - - + - + - - - + - + - + - + - - - + - + - + - + - - + - + - + - + + diff --git a/docs/benchmark_graphs/graphs/xmss_aggregated.svg b/docs/benchmark_graphs/graphs/xmss_aggregated.svg index cae23cbe..807229ae 100644 --- a/docs/benchmark_graphs/graphs/xmss_aggregated.svg +++ b/docs/benchmark_graphs/graphs/xmss_aggregated.svg @@ -1,12 +1,12 @@ - + - 2025-10-27T16:10:57.320500 + 2025-10-27T16:50:34.008564 image/svg+xml @@ -21,18 +21,18 @@ - - @@ -40,38 +40,24 @@ z - + - - + - - + + - - - + + + - - + + - + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - - - - + + + - - + + - - + + - + - - + - + - + - - + + - + - + - + - + @@ -354,19 +569,19 @@ L 557.49 239.955163 - - + + - + - + - + - + @@ -374,51 +589,19 @@ L 557.49 194.266793 - - + + - + - + - + - - - - + @@ -426,19 +609,19 @@ z - - + + - + - + - + - + @@ -446,19 +629,19 @@ L 557.49 102.890054 - - + + - + - + - + - + @@ -467,12 +650,24 @@ L 557.49 57.201685 - - + + - - - - + + + + + + + + + + + + + + + - - + + - - - - + + + - - + + - - - - + - + - - - + - + - + - + - - - + - + - + - + - - + - + - + - + + diff --git a/docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg b/docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg new file mode 100644 index 00000000..1d2950e3 --- /dev/null +++ b/docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg @@ -0,0 +1,1517 @@ + + + + + + + + 2025-10-27T16:50:34.106920 + image/svg+xml + + + Matplotlib v3.10.5, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/benchmark_graphs/main.py b/docs/benchmark_graphs/main.py index 035149f5..8521f8ca 100644 --- a/docs/benchmark_graphs/main.py +++ b/docs/benchmark_graphs/main.py @@ -4,11 +4,11 @@ # uv run python main.py -N_DAYS_SHOWN = 30 +N_DAYS_SHOWN = 70 plt.rcParams.update({ 'font.size': 12, # Base font size - 'axes.titlesize': 16, # Title font size + 'axes.titlesize': 14, # Title font size 'axes.labelsize': 12, # X and Y label font size 'xtick.labelsize': 12, # X tick label font size 'ytick.labelsize': 12, # Y tick label font size @@ -38,17 +38,26 @@ def create_duration_graph(data, target=None, target_label=None, title="", y_lege color = '#2E86AB' color2 = '#A23B72' # Different color for second curve - _, ax = plt.subplots(figsize=(8, 4.8)) - ax.plot(dates, values1, marker='o', linewidth=2, + _, ax = plt.subplots(figsize=(10, 6)) + + # Filter out None values for first curve + dates1_filtered = [d for d, v in zip(dates, values1) if v is not None] + values1_filtered = [v for v in values1 if v is not None] + + ax.plot(dates1_filtered, values1_filtered, marker='o', linewidth=2, markersize=7, color=color, label=label1) # Plot second curve if it exists if has_second_curve and label2 is not None: - ax.plot(dates, values2, marker='s', linewidth=2, + # Filter out None values for second curve + dates2_filtered = [d for d, v in zip(dates, values2) if v is not None] + values2_filtered = [v for v in values2 if v is not None] + + ax.plot(dates2_filtered, values2_filtered, marker='s', linewidth=2, markersize=7, color=color2, label=label2) - all_values = values1 + values2 + all_values = values1_filtered + values2_filtered else: - all_values = values1 + all_values = values1_filtered min_date = min(dates) max_date = max(dates) @@ -66,16 +75,17 @@ def create_duration_graph(data, target=None, target_label=None, title="", y_lege ax.axhline(y=target, color=color, linestyle='--', linewidth=2, label=target_label) - ax.set_ylabel(y_legend) - ax.set_title(title, pad=15) + ax.set_ylabel(y_legend, fontsize=12) + ax.set_title(title, fontsize=16, pad=15) ax.grid(True, alpha=0.3) ax.legend() # Adjust y-limit to accommodate both curves - max_value = max(all_values) - if target is not None: - max_value = max(max_value, target) - ax.set_ylim(0, max_value * 1.1) + if all_values: + max_value = max(all_values) + if target is not None: + max_value = max(max_value, target) + ax.set_ylim(0, max_value * 1.1) plt.tight_layout() plt.savefig(f'graphs/{file}.svg', format='svg', bbox_inches='tight') @@ -85,7 +95,15 @@ def create_duration_graph(data, target=None, target_label=None, title="", y_lege create_duration_graph( data=[ - ('2025-10-26', 230_000, 620_000), + ('2025-08-27', 85000, None), + ('2025-08-30', 95000, None), + ('2025-09-09', 108000, None), + ('2025-09-14', 108000, None), + ('2025-09-28', 125000, None), + ('2025-10-01', 185000, None), + ('2025-10-12', 195000, None), + ('2025-10-13', 205000, None), + ('2025-10-18', 210000, 620_000), ('2025-10-27', 610_000, 1_250_000), ], target=1_500_000, @@ -99,7 +117,17 @@ def create_duration_graph(data, target=None, target_label=None, title="", y_lege create_duration_graph( data=[ - ('2025-10-26', 0.411, 0.320), + ('2025-08-27', 2.7, None), + ('2025-09-07', 1.4, None), + ('2025-09-09', 1.32, None), + ('2025-09-10', 0.970, None), + ('2025-09-14', 0.825, None), + ('2025-09-28', 0.725, None), + ('2025-10-01', 0.685, None), + ('2025-10-03', 0.647, None), + ('2025-10-12', 0.569, None), + ('2025-10-13', 0.521, None), + ('2025-10-18', 0.411, 0.320), ('2025-10-27', 0.425, 0.330), ], target=0.1, @@ -113,7 +141,19 @@ def create_duration_graph(data, target=None, target_label=None, title="", y_lege create_duration_graph( data=[ - ('2025-10-26', 255, 465), + ('2025-08-27', 35, None), + ('2025-09-02', 37, None), + ('2025-09-03', 53, None), + ('2025-09-09', 62, None), + ('2025-09-10', 76, None), + ('2025-09-14', 107, None), + ('2025-09-28', 137, None), + ('2025-10-01', 172, None), + ('2025-10-03', 177, None), + ('2025-10-07', 193, None), + ('2025-10-12', 214, None), + ('2025-10-13', 234, None), + ('2025-10-18', 255, 465), ('2025-10-27', 314, 555), ], target=1000, @@ -124,3 +164,29 @@ def create_duration_graph(data, target=None, target_label=None, title="", y_lege label1="i9-12900H", label2="mac m4 max" ) + + create_duration_graph( + data=[ + ('2025-08-27', 14.2 / 0.92, None), + ('2025-09-02', 13.5 / 0.82, None), + ('2025-09-03', 9.4 / 0.82, None), + ('2025-09-09', 8.02 / 0.72, None), + ('2025-09-10', 6.53 / 0.72, None), + ('2025-09-14', 4.65 / 0.72, None), + ('2025-09-28', 3.63 / 0.63, None), + ('2025-10-01', 2.9 / 0.42, None), + ('2025-10-03', 2.81 / 0.42, None), + ('2025-10-07', 2.59 / 0.42, None), + ('2025-10-12', 2.33 / 0.40, None), + ('2025-10-13', 2.13 / 0.38, None), + ('2025-10-18', 1.96 / 0.37, 1.07 / 0.12), + ('2025-10-27', (610_000 / 157) / 314, (1_250_000 / 157) / 555), + ], + target=2, + target_label="Target (2x)", + title="XMSS aggregated: zkVM overhead vs raw Poseidons", + y_legend="", + file="xmss_aggregated_overhead", + label1="i9-12900H", + label2="mac m4 max" + ) From 987c88575ae80f8f7f93107cb79a16b95f719c9c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 27 Oct 2025 17:02:20 +0400 Subject: [PATCH 42/42] log scale for WHIR recursion graph --- .../benchmark_graphs/graphs/raw_poseidons.svg | 130 ++-- .../graphs/recursive_whir_opening.svg | 642 +++++++++++------- .../graphs/xmss_aggregated.svg | 126 ++-- .../graphs/xmss_aggregated_overhead.svg | 134 ++-- docs/benchmark_graphs/main.py | 44 +- 5 files changed, 631 insertions(+), 445 deletions(-) diff --git a/docs/benchmark_graphs/graphs/raw_poseidons.svg b/docs/benchmark_graphs/graphs/raw_poseidons.svg index 85ddd9fd..20e44048 100644 --- a/docs/benchmark_graphs/graphs/raw_poseidons.svg +++ b/docs/benchmark_graphs/graphs/raw_poseidons.svg @@ -6,7 +6,7 @@ - 2025-10-27T16:50:33.815256 + 2025-10-27T17:01:34.195704 image/svg+xml @@ -42,16 +42,16 @@ z +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -192,11 +192,11 @@ z +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -246,11 +246,11 @@ z +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -268,11 +268,11 @@ L 175.488611 34.3575 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -306,11 +306,11 @@ z +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -362,11 +362,11 @@ z +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -384,11 +384,11 @@ L 365.375278 34.3575 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -418,11 +418,11 @@ z +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -461,11 +461,11 @@ z +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -483,11 +483,11 @@ L 555.261944 34.3575 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -505,11 +505,11 @@ L 618.5575 34.3575 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -529,16 +529,16 @@ L 681.853056 34.3575 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -563,11 +563,11 @@ z +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -583,11 +583,11 @@ L 699.9375 331.172479 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -603,11 +603,11 @@ L 699.9375 290.232482 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -623,11 +623,11 @@ L 699.9375 249.292485 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -643,11 +643,11 @@ L 699.9375 208.352488 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -663,11 +663,11 @@ L 699.9375 167.412491 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -683,11 +683,11 @@ L 699.9375 126.472494 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -703,11 +703,11 @@ L 699.9375 85.532496 +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -976,9 +976,9 @@ L 473.881944 332.195979 L 482.924167 330.14898 L 528.135278 329.12548 L 609.515278 247.245485 -" clip-path="url(#p99c71b6d26)" style="fill: none; stroke: #2e86ab; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #2e86ab; stroke-width: 2; stroke-linecap: square"/> - - - - - - - - - - - - + + + + + + + + + + + +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke: #a23b72; stroke-width: 2; stroke-linecap: square"/> - - - - + + + +" clip-path="url(#p7e61cc75bf)" style="fill: none; stroke-dasharray: 7.4,3.2; stroke-dashoffset: 0; stroke: #2e86ab; stroke-width: 2"/> - + @@ -1209,7 +1209,7 @@ L 71.6975 202.346238 L 83.6975 202.346238 " style="fill: none; stroke: #a23b72; stroke-width: 2; stroke-linecap: square"/> - + @@ -1472,7 +1472,7 @@ z - + diff --git a/docs/benchmark_graphs/graphs/recursive_whir_opening.svg b/docs/benchmark_graphs/graphs/recursive_whir_opening.svg index d2fae0fa..17ad03cc 100644 --- a/docs/benchmark_graphs/graphs/recursive_whir_opening.svg +++ b/docs/benchmark_graphs/graphs/recursive_whir_opening.svg @@ -1,12 +1,12 @@ - + - 2025-10-27T16:50:33.913644 + 2025-10-27T17:01:34.332924 image/svg+xml @@ -22,41 +22,41 @@ - - + - - + - + - + - + - + - + - + - + @@ -266,18 +266,18 @@ L 175.506111 34.3575 - + - + - + - + - + - + - + - + - + @@ -382,18 +382,18 @@ L 365.419028 34.3575 - + - + - + - + - + - + - + - + - + @@ -481,18 +481,18 @@ L 555.331944 34.3575 - + - + - + @@ -503,18 +503,18 @@ L 618.63625 34.3575 - + - + - + @@ -527,23 +527,23 @@ L 681.940556 34.3575 - + - - + - - + + - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + - + - + - - - - + + + + - + - - - + + + - + - + - - - - + + + + - + - - - + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + + + - + - + - - - - + + + + - + - + @@ -979,22 +1126,22 @@ z - - + + - - - - - - - - - - - - - - + + + + + + + + + + + + + - - + + - - - - + + + - - + + - - - - - - - + + + + + @@ -1225,34 +1412,46 @@ z + + + + + + + + + + + + - - - + - + - + - + - - + - + - + - + - - - + - + - + - + + diff --git a/docs/benchmark_graphs/graphs/xmss_aggregated.svg b/docs/benchmark_graphs/graphs/xmss_aggregated.svg index 807229ae..eb247f2f 100644 --- a/docs/benchmark_graphs/graphs/xmss_aggregated.svg +++ b/docs/benchmark_graphs/graphs/xmss_aggregated.svg @@ -6,7 +6,7 @@ - 2025-10-27T16:50:34.008564 + 2025-10-27T17:01:34.445473 image/svg+xml @@ -42,16 +42,16 @@ z +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -192,11 +192,11 @@ z +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -246,11 +246,11 @@ z +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -268,11 +268,11 @@ L 172.441389 34.3575 +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -306,11 +306,11 @@ z +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -362,11 +362,11 @@ z +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -384,11 +384,11 @@ L 363.993472 34.3575 +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -418,11 +418,11 @@ z +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -461,11 +461,11 @@ z +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -483,11 +483,11 @@ L 555.545556 34.3575 +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -505,11 +505,11 @@ L 619.39625 34.3575 +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -529,16 +529,16 @@ L 683.246944 34.3575 +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -552,11 +552,11 @@ L -3.5 0 +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -572,11 +572,11 @@ L 701.49 310.702481 +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -592,11 +592,11 @@ L 701.49 249.292485 +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -612,11 +612,11 @@ L 701.49 187.882489 +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -632,11 +632,11 @@ L 701.49 126.472494 +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -665,9 +665,9 @@ L 473.451806 306.403781 L 482.573333 300.262782 L 528.180972 293.814732 L 610.274722 275.698783 -" clip-path="url(#p6ea2ed4c22)" style="fill: none; stroke: #2e86ab; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #2e86ab; stroke-width: 2; stroke-linecap: square"/> - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke: #a23b72; stroke-width: 2; stroke-linecap: square"/> - - - - + + + +" clip-path="url(#p8d98a6b6c3)" style="fill: none; stroke-dasharray: 7.4,3.2; stroke-dashoffset: 0; stroke: #2e86ab; stroke-width: 2"/> - + @@ -1243,7 +1243,7 @@ L 540.3225 337.003102 L 552.3225 337.003102 " style="fill: none; stroke: #a23b72; stroke-width: 2; stroke-linecap: square"/> - + @@ -1373,7 +1373,7 @@ z - + diff --git a/docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg b/docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg index 1d2950e3..35758e13 100644 --- a/docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg +++ b/docs/benchmark_graphs/graphs/xmss_aggregated_overhead.svg @@ -6,7 +6,7 @@ - 2025-10-27T16:50:34.106920 + 2025-10-27T17:01:34.555931 image/svg+xml @@ -42,16 +42,16 @@ z +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -192,11 +192,11 @@ z +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -246,11 +246,11 @@ z +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -268,11 +268,11 @@ L 169.337639 34.3575 +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -306,11 +306,11 @@ z +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -362,11 +362,11 @@ z +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -384,11 +384,11 @@ L 361.965972 34.3575 +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -418,11 +418,11 @@ z +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -461,11 +461,11 @@ z +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -483,11 +483,11 @@ L 554.594306 34.3575 +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -505,11 +505,11 @@ L 618.80375 34.3575 +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -529,16 +529,16 @@ L 683.013194 34.3575 +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -563,11 +563,11 @@ z +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -610,11 +610,11 @@ z +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -630,11 +630,11 @@ L 701.35875 278.860261 +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -650,11 +650,11 @@ L 701.35875 232.234153 +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -671,11 +671,11 @@ L 701.35875 185.608045 +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -692,11 +692,11 @@ L 701.35875 138.981937 +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -713,11 +713,11 @@ L 701.35875 92.355829 +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.3; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -746,9 +746,9 @@ L 472.039306 263.473645 L 481.212083 267.571835 L 527.075972 273.315535 L 609.630972 141.336982 -" clip-path="url(#p180a040bc8)" style="fill: none; stroke: #2e86ab; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #2e86ab; stroke-width: 2; stroke-linecap: square"/> - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke: #a23b72; stroke-width: 2; stroke-linecap: square"/> - - - - + + + +" clip-path="url(#pd6f5031cb3)" style="fill: none; stroke-dasharray: 7.4,3.2; stroke-dashoffset: 0; stroke: #2e86ab; stroke-width: 2"/> - + @@ -1359,7 +1359,7 @@ L 590.221875 67.689375 L 602.221875 67.689375 " style="fill: none; stroke: #a23b72; stroke-width: 2; stroke-linecap: square"/> - + @@ -1510,7 +1510,7 @@ z - + diff --git a/docs/benchmark_graphs/main.py b/docs/benchmark_graphs/main.py index 8521f8ca..b5e51645 100644 --- a/docs/benchmark_graphs/main.py +++ b/docs/benchmark_graphs/main.py @@ -1,5 +1,6 @@ import matplotlib.pyplot as plt import matplotlib.dates as mdates +from matplotlib.ticker import ScalarFormatter, LogLocator from datetime import datetime, timedelta # uv run python main.py @@ -16,7 +17,7 @@ }) -def create_duration_graph(data, target=None, target_label=None, title="", y_legend="", file="", label1="Series 1", label2=None): +def create_duration_graph(data, target=None, target_label=None, title="", y_legend="", file="", label1="Series 1", label2=None, log_scale=False): dates = [] values1 = [] values2 = [] @@ -77,15 +78,32 @@ def create_duration_graph(data, target=None, target_label=None, title="", y_lege ax.set_ylabel(y_legend, fontsize=12) ax.set_title(title, fontsize=16, pad=15) - ax.grid(True, alpha=0.3) + ax.grid(True, alpha=0.3, which='both') # Grid for both major and minor ticks ax.legend() - # Adjust y-limit to accommodate both curves - if all_values: - max_value = max(all_values) - if target is not None: - max_value = max(max_value, target) - ax.set_ylim(0, max_value * 1.1) + # Set log scale if requested + if log_scale: + ax.set_yscale('log') + + # Set locators for major and minor ticks + ax.yaxis.set_major_locator(LogLocator(base=10.0, numticks=15)) + ax.yaxis.set_minor_locator(LogLocator(base=10.0, subs=range(1, 10), numticks=100)) + + # Format labels + ax.yaxis.set_major_formatter(ScalarFormatter()) + ax.yaxis.set_minor_formatter(ScalarFormatter()) + ax.yaxis.get_major_formatter().set_scientific(False) + ax.yaxis.get_minor_formatter().set_scientific(False) + + # Show minor tick labels + ax.tick_params(axis='y', which='minor', labelsize=10) + else: + # Adjust y-limit to accommodate both curves (only for linear scale) + if all_values: + max_value = max(all_values) + if target is not None: + max_value = max(max_value, target) + ax.set_ylim(0, max_value * 1.1) plt.tight_layout() plt.savefig(f'graphs/{file}.svg', format='svg', bbox_inches='tight') @@ -112,7 +130,8 @@ def create_duration_graph(data, target=None, target_label=None, title="", y_lege y_legend="Poseidons proven / s", file="raw_poseidons", label1="i9-12900H", - label2="mac m4 max" + label2="mac m4 max", + log_scale=False ) create_duration_graph( @@ -132,11 +151,12 @@ def create_duration_graph(data, target=None, target_label=None, title="", y_lege ], target=0.1, target_label="Target (0.1 s)", - title="Recursive WHIR opening", + title="Recursive WHIR opening (log scale)", y_legend="Proving time (s)", file="recursive_whir_opening", label1="i9-12900H", - label2="mac m4 max" + label2="mac m4 max", + log_scale=True ) create_duration_graph( @@ -189,4 +209,4 @@ def create_duration_graph(data, target=None, target_label=None, title="", y_lege file="xmss_aggregated_overhead", label1="i9-12900H", label2="mac m4 max" - ) + ) \ No newline at end of file