From 9211406da2ab883a103e2368271b3d2778857fee Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 10:02:56 +0400 Subject: [PATCH 01/47] remove logup* and second whir commitment --- crates/lean_prover/src/lib.rs | 29 +- crates/lean_prover/src/prove_execution.rs | 62 +--- crates/lean_prover/src/test_zkvm.rs | 8 +- crates/lean_prover/src/verify_execution.rs | 59 +--- crates/lean_vm/src/tables/table_trait.rs | 27 +- crates/rec_aggregation/src/recursion.rs | 41 ++- crates/rec_aggregation/src/xmss_aggregate.rs | 6 +- crates/sub_protocols/src/generic_logup.rs | 4 +- crates/sub_protocols/src/lib.rs | 3 - crates/sub_protocols/src/logup_star.rs | 285 ------------------- crates/sub_protocols/src/packed_pcs.rs | 4 +- 11 files changed, 45 insertions(+), 483 deletions(-) delete mode 100644 crates/sub_protocols/src/logup_star.rs diff --git a/crates/lean_prover/src/lib.rs b/crates/lean_prover/src/lib.rs index 92339a1c..d9cde5dc 100644 --- a/crates/lean_prover/src/lib.rs +++ b/crates/lean_prover/src/lib.rs @@ -26,35 +26,14 @@ pub const GRINDING_BITS: usize = 16; pub const STARTING_LOG_INV_RATE_BASE: usize = 2; -pub const STARTING_LOG_INV_RATE_EXTENSION: usize = 3; - -#[derive(Debug)] -pub struct SnarkParams { - pub first_whir: WhirConfigBuilder, - pub second_whir: WhirConfigBuilder, -} - -impl Default for SnarkParams { - fn default() -> Self { - Self { - first_whir: whir_config_builder(STARTING_LOG_INV_RATE_BASE, 7, 5), - second_whir: whir_config_builder(STARTING_LOG_INV_RATE_EXTENSION, 4, 1), - } - } -} - -pub fn whir_config_builder( - starting_log_inv_rate: usize, - first_folding_factor: usize, - rs_domain_initial_reduction_factor: usize, -) -> WhirConfigBuilder { +pub fn default_whir_config() -> WhirConfigBuilder { WhirConfigBuilder { - folding_factor: FoldingFactor::new(first_folding_factor, 4), + folding_factor: FoldingFactor::new(7, 4), soundness_type: SECURITY_REGIME, pow_bits: GRINDING_BITS, max_num_variables_to_send_coeffs: 6, - rs_domain_initial_reduction_factor, + rs_domain_initial_reduction_factor: 5, security_level: SECURITY_BITS, - starting_log_inv_rate, + starting_log_inv_rate: STARTING_LOG_INV_RATE_BASE, } } diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index 5ef4f698..ec6ddbf1 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -1,14 +1,12 @@ use std::collections::BTreeMap; -use crate::common::*; use crate::*; use air::prove_air; use lean_vm::*; -use p3_util::log2_ceil_usize; use sub_protocols::*; use tracing::info_span; -use utils::{build_prover_state, padd_with_zero_to_next_power_of_two}; +use utils::build_prover_state; use xmss::Poseidon16History; #[derive(Debug)] @@ -23,7 +21,7 @@ pub fn prove_execution( bytecode: &Bytecode, (public_input, private_input): (&[F], &[F]), poseidons_16_precomputed: &Poseidon16History, - params: &SnarkParams, + whir_config: &WhirConfigBuilder, vm_profiler: bool, ) -> ExecutionProof { let mut exec_summary = String::new(); @@ -83,7 +81,7 @@ pub fn prove_execution( }); // 1st Commitment - let packed_pcs_witness_base = packed_pcs_commit(&mut prover_state, ¶ms.first_whir, &memory, &acc, &traces); + let packed_pcs_witness_base = packed_pcs_commit(&mut prover_state, &whir_config, &memory, &acc, &traces); let first_whir_n_vars = packed_pcs_witness_base.packed_polynomial.by_ref().n_vars(); // logup (GKR) @@ -125,48 +123,6 @@ pub fn prove_execution( committed_statements.get_mut(table).unwrap().extend(this_air_claims); } - let bytecode_compression_challenges = - MultilinearPoint(prover_state.sample_vec(log2_ceil_usize(N_INSTRUCTION_COLUMNS))); - - let folded_bytecode = fold_bytecode(bytecode, &bytecode_compression_challenges); - - let bytecode_air_entry = &mut committed_statements.get_mut(&Table::execution()).unwrap()[2]; - let bytecode_air_point = bytecode_air_entry.0.clone(); - let mut bytecode_air_values = vec![]; - for bytecode_col_index in N_COMMITTED_EXEC_COLUMNS..N_COMMITTED_EXEC_COLUMNS + N_INSTRUCTION_COLUMNS { - bytecode_air_values.push(bytecode_air_entry.1.remove(&bytecode_col_index).unwrap()); - } - - let bytecode_lookup_claim = Evaluation::new( - bytecode_air_point.clone(), - padd_with_zero_to_next_power_of_two(&bytecode_air_values).evaluate(&bytecode_compression_challenges), - ); - let bytecode_poly_eq_point = eval_eq(&bytecode_lookup_claim.point); - let bytecode_pushforward = MleOwned::Extension(compute_pushforward( - &traces[&Table::execution()].base[COL_PC], - folded_bytecode.len(), - &bytecode_poly_eq_point, - )); - - let bytecode_pushforward_commitment = - WhirConfig::new(¶ms.second_whir, log2_ceil_usize(bytecode.instructions.len())) - .commit(&mut prover_state, &bytecode_pushforward); - - let bytecode_logup_star_statements = prove_logup_star( - &mut prover_state, - &MleRef::Extension(&folded_bytecode), - &traces[&Table::execution()].base[COL_PC], - bytecode_lookup_claim.value, - &bytecode_poly_eq_point, - &bytecode_pushforward.by_ref(), - Some(bytecode.instructions.len()), - ); - - committed_statements.get_mut(&Table::execution()).unwrap().push(( - bytecode_logup_star_statements.on_indexes.point.clone(), - BTreeMap::from_iter([(COL_PC, bytecode_logup_star_statements.on_indexes.value)]), - )); - let public_memory_random_point = MultilinearPoint(prover_state.sample_vec(log2_strict_usize(public_memory_size))); prover_state.duplexing(); let public_memory_eval = (&memory[..public_memory_size]).evaluate(&public_memory_random_point); @@ -197,7 +153,7 @@ pub fn prove_execution( ); WhirConfig::new( - ¶ms.first_whir, + &whir_config, packed_pcs_witness_base.packed_polynomial.by_ref().n_vars(), ) .prove( @@ -207,16 +163,6 @@ pub fn prove_execution( &packed_pcs_witness_base.packed_polynomial.by_ref(), ); - WhirConfig::new(¶ms.second_whir, log2_ceil_usize(bytecode.instructions.len())).prove( - &mut prover_state, - bytecode_logup_star_statements - .on_pushforward - .into_iter() - .map(|smt| SparseStatement::dense(smt.point, smt.value)) - .collect::>(), - bytecode_pushforward_commitment, - &bytecode_pushforward.by_ref(), - ); let proof_size_fe = prover_state.pruned_proof().proof_size_fe(); ExecutionProof { proof: prover_state.raw_proof(), diff --git a/crates/lean_prover/src/test_zkvm.rs b/crates/lean_prover/src/test_zkvm.rs index 7e8971ae..aa48194e 100644 --- a/crates/lean_prover/src/test_zkvm.rs +++ b/crates/lean_prover/src/test_zkvm.rs @@ -1,4 +1,4 @@ -use crate::{prove_execution::prove_execution, verify_execution::verify_execution}; +use crate::{default_whir_config, prove_execution::prove_execution, verify_execution::verify_execution}; use lean_compiler::*; use lean_vm::*; use multilinear_toolkit::prelude::*; @@ -158,11 +158,11 @@ fn test_zk_vm_helper(program_str: &str, (public_input, private_input): (&[F], &[ &bytecode, (public_input, private_input), &vec![], - &Default::default(), + &default_whir_config(), false, ); let proof_time = time.elapsed(); - verify_execution(&bytecode, public_input, proof.proof.clone(), &Default::default()).unwrap(); + verify_execution(&bytecode, public_input, proof.proof.clone(), &default_whir_config()).unwrap(); println!("{}", proof.exec_summary); println!("Proof time: {:.3} s", proof_time.as_secs_f32()); @@ -177,7 +177,7 @@ fn test_zk_vm_helper(program_str: &str, (public_input, private_input): (&[F], &[ } let mut fuzzed_proof = proof.proof.clone(); fuzzed_proof[i] += F::ONE; - let verify_result = verify_execution(&bytecode, public_input, fuzzed_proof, &Default::default()); + let verify_result = verify_execution(&bytecode, public_input, fuzzed_proof, &default_whir_config()); assert!(verify_result.is_err(), "Fuzzing failed at index {}", i); } } diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index e2d227b9..a85f201d 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -1,11 +1,9 @@ use std::collections::BTreeMap; use crate::*; -use crate::{SnarkParams, common::*}; use air::verify_air; use lean_vm::*; -use p3_util::{log2_ceil_usize, log2_strict_usize}; -use sub_protocols::verify_logup_star; +use p3_util::log2_strict_usize; use sub_protocols::*; use utils::ToUsize; @@ -18,10 +16,10 @@ pub struct ProofVerificationDetails { } pub fn verify_execution( - bytecode: &Bytecode, + _bytecode: &Bytecode, public_input: &[F], proof: Vec, - params: &SnarkParams, + whir_config: &WhirConfigBuilder, ) -> Result { let mut verifier_state = VerifierState::::new(proof, get_poseidon16().clone()); verifier_state.duplexing(); @@ -59,7 +57,7 @@ pub fn verify_execution( } let parsed_commitment_base = - packed_pcs_parse_commitment(¶ms.first_whir, &mut verifier_state, log_memory, &table_n_vars)?; + packed_pcs_parse_commitment(&whir_config, &mut verifier_state, log_memory, &table_n_vars)?; let logup_c = verifier_state.sample(); verifier_state.duplexing(); @@ -99,43 +97,6 @@ pub fn verify_execution( committed_statements.get_mut(table).unwrap().extend(this_air_claims); } - let bytecode_compression_challenges = - MultilinearPoint(verifier_state.sample_vec(log2_ceil_usize(N_INSTRUCTION_COLUMNS))); - - let bytecode_air_entry = &mut committed_statements.get_mut(&Table::execution()).unwrap()[2]; - let bytecode_air_point = bytecode_air_entry.0.clone(); - let mut bytecode_air_values = vec![]; - for bytecode_col_index in N_COMMITTED_EXEC_COLUMNS..N_COMMITTED_EXEC_COLUMNS + N_INSTRUCTION_COLUMNS { - bytecode_air_values.push(bytecode_air_entry.1.remove(&bytecode_col_index).unwrap()); - } - - let bytecode_lookup_claim = Evaluation::new( - bytecode_air_point.clone(), - padd_with_zero_to_next_power_of_two(&bytecode_air_values).evaluate(&bytecode_compression_challenges), - ); - - let bytecode_pushforward_parsed_commitment = - WhirConfig::new(¶ms.second_whir, log2_ceil_usize(bytecode.instructions.len())) - .parse_commitment::(&mut verifier_state)?; - - let bytecode_logup_star_statements = verify_logup_star( - &mut verifier_state, - log2_ceil_usize(bytecode.instructions.len()), - table_n_vars[&Table::execution()], - bytecode_lookup_claim, - )?; - let folded_bytecode = fold_bytecode(bytecode, &bytecode_compression_challenges); - if folded_bytecode.evaluate(&bytecode_logup_star_statements.on_table.point) - != bytecode_logup_star_statements.on_table.value - { - return Err(ProofError::InvalidProof); - } - - committed_statements.get_mut(&Table::execution()).unwrap().push(( - bytecode_logup_star_statements.on_indexes.point.clone(), - BTreeMap::from_iter([(COL_PC, bytecode_logup_star_statements.on_indexes.value)]), - )); - let public_memory_random_point = MultilinearPoint(verifier_state.sample_vec(log2_strict_usize(public_memory.len()))); verifier_state.duplexing(); @@ -165,22 +126,12 @@ pub fn verify_execution( &committed_statements, ); let total_whir_statements_base = global_statements_base.iter().map(|s| s.values.len()).sum(); - WhirConfig::new(¶ms.first_whir, parsed_commitment_base.num_variables).verify( + WhirConfig::new(&whir_config, parsed_commitment_base.num_variables).verify( &mut verifier_state, &parsed_commitment_base, global_statements_base, )?; - WhirConfig::new(¶ms.second_whir, log2_ceil_usize(bytecode.instructions.len())).verify( - &mut verifier_state, - &bytecode_pushforward_parsed_commitment, - bytecode_logup_star_statements - .on_pushforward - .into_iter() - .map(|smt| SparseStatement::dense(smt.point, smt.value)) - .collect::>(), - )?; - Ok(ProofVerificationDetails { log_memory, table_n_vars, diff --git a/crates/lean_vm/src/tables/table_trait.rs b/crates/lean_vm/src/tables/table_trait.rs index 4243090b..e9a299d2 100644 --- a/crates/lean_vm/src/tables/table_trait.rs +++ b/crates/lean_vm/src/tables/table_trait.rs @@ -1,4 +1,4 @@ -use crate::{DIMENSION, EF, F, InstructionContext, N_COMMITTED_EXEC_COLUMNS, RunnerError, Table}; +use crate::{DIMENSION, EF, F, InstructionContext, RunnerError, Table}; use multilinear_toolkit::prelude::*; use std::{any::TypeId, cmp::Reverse, collections::BTreeMap, mem::transmute}; @@ -144,31 +144,8 @@ pub trait TableT: Air { false } - fn n_commited_columns_f(&self) -> usize { - if self.is_execution_table() { - N_COMMITTED_EXEC_COLUMNS - } else { - self.n_columns_f_air() - } - } - - fn n_commited_columns_ef(&self) -> usize { - self.n_columns_ef_air() - } - fn n_commited_columns(&self) -> usize { - self.n_commited_columns_ef() * DIMENSION + self.n_commited_columns_f() - } - - fn commited_air_values(&self, air_evals: &[EF]) -> BTreeMap { - // the intermidiate columns are not commited - // (they correspond to decoded instructions, in execution table, obtained via logup* into the bytecode) - air_evals - .iter() - .copied() - .enumerate() - .filter(|(i, _)| *i < self.n_commited_columns_f() || *i >= self.n_columns_f_air()) - .collect::>() + self.n_columns_ef_air() * DIMENSION + self.n_columns_f_air() } fn lookup_index_columns_f<'a>(&'a self, trace: &'a TableTrace) -> Vec<&'a [F]> { diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index bd2b90b8..fca374e7 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -4,9 +4,9 @@ use std::rc::Rc; use std::time::Instant; use lean_compiler::{CompilationFlags, ProgramSource, compile_program, compile_program_with_flags}; +use lean_prover::default_whir_config; use lean_prover::prove_execution::prove_execution; use lean_prover::verify_execution::verify_execution; -use lean_prover::{STARTING_LOG_INV_RATE_BASE, STARTING_LOG_INV_RATE_EXTENSION, SnarkParams, whir_config_builder}; use lean_vm::*; use multilinear_toolkit::prelude::symbolic::{ SymbolicExpression, SymbolicOperation, get_symbolic_constraints_and_bus_data_values, @@ -24,10 +24,9 @@ pub fn run_recursion_benchmark(count: usize, tracing: bool) { .unwrap() .to_string(); - let snark_params = SnarkParams { - first_whir: whir_config_builder(STARTING_LOG_INV_RATE_BASE, 3, 1), - second_whir: whir_config_builder(STARTING_LOG_INV_RATE_EXTENSION, 4, 1), - }; + let mut inner_whir_config = default_whir_config(); + inner_whir_config.folding_factor = FoldingFactor::new(3, 4); + inner_whir_config.rs_domain_initial_reduction_factor = 1; let program_to_prove = r#" DIM = 5 COMPRESSION = 1 @@ -67,16 +66,18 @@ def main(): &bytecode_to_prove, (&outer_public_input, &outer_private_input), &vec![], - &snark_params, + &inner_whir_config, false, ); - let verif_details = verify_execution(&bytecode_to_prove, &[], proof_to_prove.proof.clone(), &snark_params).unwrap(); + let verif_details = verify_execution( + &bytecode_to_prove, + &[], + proof_to_prove.proof.clone(), + &inner_whir_config, + ) + .unwrap(); - let base_whir = WhirConfig::::new(&snark_params.first_whir, proof_to_prove.first_whir_n_vars); - let ext_whir = WhirConfig::::new( - &snark_params.second_whir, - log2_ceil_usize(bytecode_to_prove.instructions.len()), - ); + let outer_whir_config = WhirConfig::::new(&inner_whir_config, proof_to_prove.first_whir_n_vars); // let guest_program_commitment = { // let mut prover_state = build_prover_state(); @@ -86,8 +87,7 @@ def main(): // assert_eq!(commitment_transcript.len(), ext_whir.committment_ood_samples * DIMENSION + VECTOR_LEN); // }; - let mut replacements = whir_recursion_placeholder_replacements(&base_whir, true); - replacements.extend(whir_recursion_placeholder_replacements(&ext_whir, false)); + let mut replacements = whir_recursion_placeholder_replacements(&outer_whir_config); assert!( verif_details.log_memory >= verif_details.table_n_vars[&Table::execution()] @@ -179,7 +179,7 @@ def main(): lookup_ef_indexes_str.push(format!("[{}]", this_look_ef_indexes_str.join(", "))); num_cols_f_air.push(table.n_columns_f_air().to_string()); num_cols_ef_air.push(table.n_columns_ef_air().to_string()); - num_cols_f_committed.push(table.n_commited_columns_f().to_string()); + num_cols_f_committed.push(table.n_columns_f_air().to_string()); let this_lookup_f_values_str = table .lookups_f() .iter() @@ -320,7 +320,7 @@ def main(): &recursion_bytecode, (&inner_public_input, &inner_private_input), &vec![], // TODO precompute poseidons - &Default::default(), + &default_whir_config(), false, ); let proving_time = time.elapsed(); @@ -328,7 +328,7 @@ def main(): &recursion_bytecode, &inner_public_input, recursion_proof.proof, - &Default::default(), + &default_whir_config(), ) .unwrap(); println!( @@ -348,10 +348,7 @@ def main(): ); } -pub(crate) fn whir_recursion_placeholder_replacements( - whir_config: &WhirConfig, - base: bool, -) -> BTreeMap { +pub(crate) fn whir_recursion_placeholder_replacements(whir_config: &WhirConfig) -> BTreeMap { let mut num_queries = vec![]; let mut ood_samples = vec![]; let mut grinding_bits = vec![]; @@ -369,7 +366,7 @@ pub(crate) fn whir_recursion_placeholder_replacements( grinding_bits.push(whir_config.final_pow_bits.to_string()); num_queries.push(whir_config.final_queries.to_string()); - let end = if base { "_BASE_PLACEHOLDER" } else { "_EXT_PLACEHOLDER" }; + let end = "_PLACEHOLDER"; let mut replacements = BTreeMap::new(); replacements.insert( format!("MERKLE_HEIGHTS{}", end), diff --git a/crates/rec_aggregation/src/xmss_aggregate.rs b/crates/rec_aggregation/src/xmss_aggregate.rs index c26070d0..8c870406 100644 --- a/crates/rec_aggregation/src/xmss_aggregate.rs +++ b/crates/rec_aggregation/src/xmss_aggregate.rs @@ -1,5 +1,5 @@ use lean_compiler::*; -use lean_prover::{SnarkParams, prove_execution::prove_execution, verify_execution::verify_execution}; +use lean_prover::{default_whir_config, prove_execution::prove_execution, verify_execution::verify_execution}; use lean_vm::*; use multilinear_toolkit::prelude::*; use rand::{Rng, SeedableRng, rngs::StdRng}; @@ -140,7 +140,7 @@ fn xmss_aggregate_signatures_helper( program, (&public_input, &private_input), &poseidons_16_precomputed, - &SnarkParams::default(), + &default_whir_config(), false, ); @@ -164,7 +164,7 @@ pub fn xmss_verify_aggregated_signatures( let public_input = build_public_input(xmss_pub_keys, message_hash, slot); - verify_execution(program, &public_input, proof, &SnarkParams::default()).map(|_| ()) + verify_execution(program, &public_input, proof, &default_whir_config()).map(|_| ()) } #[instrument(skip_all)] diff --git a/crates/sub_protocols/src/generic_logup.rs b/crates/sub_protocols/src/generic_logup.rs index 28d471e4..d7407c8c 100644 --- a/crates/sub_protocols/src/generic_logup.rs +++ b/crates/sub_protocols/src/generic_logup.rs @@ -239,7 +239,7 @@ pub fn prove_generic_logup( { let value_eval = col.evaluate(&inner_point); prover_state.add_extension_scalar(value_eval); - let global_index = table.n_commited_columns_f() + lookup_ef.values * DIMENSION + i; + let global_index = table.n_columns_f_air() + lookup_ef.values * DIMENSION + i; assert!(!table_values.contains_key(&global_index)); table_values.insert(global_index, value_eval); } @@ -375,7 +375,7 @@ pub fn verify_generic_logup( &[index_eval + F::from_usize(i), value_eval], &alpha_powers, )); - let global_index = table.n_commited_columns_f() + lookup_ef.values * DIMENSION + i; + let global_index = table.n_columns_f_air() + lookup_ef.values * DIMENSION + i; assert!(!table_values.contains_key(&global_index)); table_values.insert(global_index, value_eval); offset += 1 << log_n_rows; diff --git a/crates/sub_protocols/src/lib.rs b/crates/sub_protocols/src/lib.rs index 32ffb821..3f43e4b7 100644 --- a/crates/sub_protocols/src/lib.rs +++ b/crates/sub_protocols/src/lib.rs @@ -7,7 +7,4 @@ pub use packed_pcs::*; mod quotient_gkr; pub use quotient_gkr::*; -mod logup_star; -pub use logup_star::*; - pub(crate) const MIN_VARS_FOR_PACKING: usize = 8; diff --git a/crates/sub_protocols/src/logup_star.rs b/crates/sub_protocols/src/logup_star.rs deleted file mode 100644 index cbf34e19..00000000 --- a/crates/sub_protocols/src/logup_star.rs +++ /dev/null @@ -1,285 +0,0 @@ -/* -Logup* (Lev Soukhanov) - -https://eprint.iacr.org/2025/946.pdf - -*/ - -use multilinear_toolkit::prelude::*; -use utils::{ToUsize, mle_of_01234567_etc}; - -use tracing::{info_span, instrument}; - -use crate::{ - MIN_VARS_FOR_PACKING, - quotient_gkr::{prove_gkr_quotient, verify_gkr_quotient}, -}; - -#[derive(Debug, PartialEq)] -pub struct LogupStarStatements { - pub on_indexes: Evaluation, - pub on_table: Evaluation, - pub on_pushforward: Vec>, -} - -#[instrument(skip_all)] -pub fn prove_logup_star( - prover_state: &mut impl FSProver, - table: &MleRef<'_, EF>, - indexes: &[PF], - claimed_value: EF, - poly_eq_point: &[EF], - pushforward: &MleRef<'_, EF>, // already commited - max_index: Option, -) -> LogupStarStatements -where - EF: ExtensionField>, - PF: PrimeField64, -{ - let table_length = table.unpacked_len(); - let indexes_length = indexes.len(); - let packing = log2_strict_usize(table_length) >= MIN_VARS_FOR_PACKING - && log2_strict_usize(indexes_length) >= MIN_VARS_FOR_PACKING; - let mut max_index = max_index.unwrap_or(table_length); - if packing { - max_index = max_index.div_ceil(packing_width::()); - } - // TODO use max_index - let _ = max_index; - - let (poly_eq_point_packed, pushforward_packed, table_packed) = info_span!("packing").in_scope(|| { - ( - MleRef::Extension(poly_eq_point).pack_if(packing), - pushforward.pack_if(packing), - table.pack_if(packing), - ) - }); - - let (sc_point, inner_evals, prod) = - info_span!("logup_star sumcheck", table_length, indexes_length).in_scope(|| { - let (sc_point, prod, table_folded, pushforward_folded) = run_product_sumcheck( - &table_packed.by_ref(), - &pushforward_packed.by_ref(), - prover_state, - claimed_value, - table.n_vars(), - ); - let inner_evals = vec![ - table_folded.as_extension().unwrap()[0], - pushforward_folded.as_extension().unwrap()[0], - ]; - (sc_point, inner_evals, prod) - }); - - let table_eval = inner_evals[0]; - prover_state.add_extension_scalar(table_eval); - // delayed opening - let on_table = Evaluation::new(sc_point.clone(), table_eval); - - let pushforwardt_eval = inner_evals[1]; - prover_state.add_extension_scalar(pushforwardt_eval); - // delayed opening - let mut on_pushforward = vec![Evaluation::new(sc_point, pushforwardt_eval)]; - - // sanity check - assert_eq!(prod, table_eval * pushforwardt_eval); - - let c = prover_state.sample(); - - let c_minus_indexes = indexes - .par_iter() - .map(|i| c - PF::::from_usize(i.to_usize())) - .collect::>(); - let c_minus_indexes_packed = MleRef::Extension(&c_minus_indexes).pack_if(packing); - - let (_, claim_point_left, _, eval_c_minus_indexes) = prove_gkr_quotient( - prover_state, - &poly_eq_point_packed.by_ref(), - &c_minus_indexes_packed.by_ref(), - ); - - let c_minus_increments = MleRef::Extension( - &(0..table.unpacked_len()) - .into_par_iter() - .map(|i| c - PF::::from_usize(i)) - .collect::>(), - ); - let c_minus_increments_packed = c_minus_increments.pack_if(packing); - let (_, claim_point_right, pushforward_final_eval, _) = prove_gkr_quotient( - prover_state, - &pushforward_packed.by_ref(), - &c_minus_increments_packed.by_ref(), - ); - - let on_indexes = Evaluation::new(claim_point_left, c - eval_c_minus_indexes); - - on_pushforward.push(Evaluation::new(claim_point_right, pushforward_final_eval)); - - // These statements remained to be proven - LogupStarStatements { - on_indexes, - on_table, - on_pushforward, - } -} - -pub fn verify_logup_star( - verifier_state: &mut impl FSVerifier, - log_table_len: usize, - log_indexes_len: usize, - claim: Evaluation, -) -> Result, ProofError> -where - EF: ExtensionField>, - PF: PrimeField64, -{ - let (sum, postponed) = sumcheck_verify(verifier_state, log_table_len, 2).map_err(|_| ProofError::InvalidProof)?; - - if sum != claim.value { - return Err(ProofError::InvalidProof); - } - - let table_eval = verifier_state.next_extension_scalar()?; - let pushforward_eval = verifier_state.next_extension_scalar()?; - - let on_table = Evaluation::new(postponed.point.clone(), table_eval); - let mut on_pushforward = vec![Evaluation::new(postponed.point, pushforward_eval)]; - - if table_eval * pushforward_eval != postponed.value { - return Err(ProofError::InvalidProof); - } - - let c = verifier_state.sample(); - - let (quotient_left, claim_point_left, claim_num_left, eval_c_minus_indexes) = - verify_gkr_quotient(verifier_state, log_indexes_len)?; - let (quotient_right, claim_point_right, pushforward_final_eval, claim_den_right) = - verify_gkr_quotient(verifier_state, log_table_len)?; - - if quotient_left != quotient_right { - return Err(ProofError::InvalidProof); - } - - let on_indexes = Evaluation::new(claim_point_left.clone(), c - eval_c_minus_indexes); - if claim_num_left != claim_point_left.eq_poly_outside(&claim.point) { - return Err(ProofError::InvalidProof); - } - - on_pushforward.push(Evaluation::new(claim_point_right.clone(), pushforward_final_eval)); - - if claim_den_right != c - mle_of_01234567_etc(&claim_point_right) { - return Err(ProofError::InvalidProof); - } - - // these statements remained to be verified - Ok(LogupStarStatements { - on_indexes, - on_table, - on_pushforward, - }) -} - -#[instrument(skip_all)] -pub fn compute_pushforward>( - indexes: &[F], - table_length: usize, - poly_eq_point: &[EF], -) -> Vec { - assert_eq!(indexes.len(), poly_eq_point.len()); - // TODO there are a lot of fun optimizations here - let mut pushforward = EF::zero_vec(table_length); - for (index, value) in indexes.iter().zip(poly_eq_point) { - let index_usize = index.to_usize(); - pushforward[index_usize] += *value; - } - pushforward -} - -#[cfg(test)] -mod tests { - use super::*; - use p3_koala_bear::{KoalaBear, QuinticExtensionFieldKB}; - use rand::{Rng, SeedableRng, rngs::StdRng}; - use utils::{build_prover_state, build_verifier_state, init_tracing}; - - type F = KoalaBear; - type EF = QuinticExtensionFieldKB; - - #[test] - fn test_logup_star() { - for log_table_len in [3, 10] { - for log_indexes_len in 3..10 { - test_logup_star_helper(log_table_len, log_indexes_len); - } - } - - test_logup_star_helper(12, 14); - } - - fn test_logup_star_helper(log_table_len: usize, log_indexes_len: usize) { - init_tracing(); - - let table_length = 1 << log_table_len; - - let indexes_len = 1 << log_indexes_len; - - let mut rng = StdRng::seed_from_u64(0); - - let table = (0..table_length).map(|_| rng.random()).collect::>(); - - let mut indexes = vec![]; - let mut values = vec![]; - let max_index = table_length * 3 / 4; - for _ in 0..indexes_len { - let index = rng.random_range(0..max_index); - indexes.push(F::from_usize(index)); - values.push(table[index]); - } - - // Commit to the table - let commited_table = table.clone(); // Phony commitment for the example - // commit to the indexes - let commited_indexes = indexes.clone(); // Phony commitment for the example - - let point = MultilinearPoint((0..log_indexes_len).map(|_| rng.random()).collect::>()); - - let mut prover_state = build_prover_state(); - let eval = values.evaluate(&point); - - let time = std::time::Instant::now(); - let poly_eq_point = info_span!("eval_eq").in_scope(|| eval_eq(&point)); - let pushforward = compute_pushforward(&indexes, table_length, &poly_eq_point); - let claim = Evaluation::new(point, eval); - - let prover_statements = prove_logup_star( - &mut prover_state, - &MleRef::Base(&commited_table), - &commited_indexes, - claim.value, - &poly_eq_point, - &MleRef::Extension(&pushforward), - Some(max_index), - ); - println!("Proving logup_star took {} ms", time.elapsed().as_millis()); - - let last_prover_state = prover_state.state(); - let mut verifier_state = build_verifier_state(prover_state); - let verifier_statements = - verify_logup_star(&mut verifier_state, log_table_len, log_indexes_len, claim).unwrap(); - - assert_eq!(&verifier_statements, &prover_statements); - assert_eq!(last_prover_state, verifier_state.state()); - - assert_eq!( - indexes.evaluate(&verifier_statements.on_indexes.point), - verifier_statements.on_indexes.value - ); - assert_eq!( - table.evaluate(&verifier_statements.on_table.point), - verifier_statements.on_table.value - ); - for eval in &verifier_statements.on_pushforward { - assert_eq!(pushforward.evaluate(&eval.point), eval.value); - } - } -} diff --git a/crates/sub_protocols/src/packed_pcs.rs b/crates/sub_protocols/src/packed_pcs.rs index 68c7c47b..efe3a36c 100644 --- a/crates/sub_protocols/src/packed_pcs.rs +++ b/crates/sub_protocols/src/packed_pcs.rs @@ -80,12 +80,12 @@ pub fn packed_pcs_commit( offset += acc.len(); for (table, log_n_rows) in &tables_heights_sorted { let n_rows = 1 << *log_n_rows; - for col_index_f in 0..table.n_commited_columns_f() { + for col_index_f in 0..table.n_columns_f_air() { let col = &traces[table].base[col_index_f]; packed_polynomial[offset..offset + n_rows].copy_from_slice(&col[..n_rows]); offset += n_rows; } - for col_index_ef in 0..table.n_commited_columns_ef() { + for col_index_ef in 0..table.n_columns_ef_air() { let col = &traces[table].ext[col_index_ef]; let transposed = transpose_slice_to_basis_coefficients(col); for basis_col in transposed { From 007f42008aa96c79508fc1631d2116215e58dd86 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 10:26:29 +0400 Subject: [PATCH 02/47] commit to bytecode acc --- crates/lean_prover/src/prove_execution.rs | 26 +++++++++++++++---- crates/lean_prover/src/test_zkvm.rs | 5 +++- crates/lean_prover/src/verify_execution.rs | 12 ++++++--- crates/lean_vm/src/isa/bytecode.rs | 16 ++++++++++++ crates/sub_protocols/src/packed_pcs.rs | 30 ++++++++++++++++------ 5 files changed, 72 insertions(+), 17 deletions(-) diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index ec6ddbf1..63ced840 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -60,28 +60,43 @@ pub fn prove_execution( ); // TODO parrallelize - let mut acc = F::zero_vec(memory.len()); + let mut memory_acc = F::zero_vec(memory.len()); info_span!("Building memory access count").in_scope(|| { for (table, trace) in &traces { for lookup in table.lookups_f() { for i in &trace.base[lookup.index] { for j in 0..lookup.values.len() { - acc[i.to_usize() + j] += F::ONE; + memory_acc[i.to_usize() + j] += F::ONE; } } } for lookup in table.lookups_ef() { for i in &trace.base[lookup.index] { for j in 0..DIMENSION { - acc[i.to_usize() + j] += F::ONE; + memory_acc[i.to_usize() + j] += F::ONE; } } } } }); + // // TODO parrallelize + let mut bytecode_acc = F::zero_vec(bytecode.padded_size()); + info_span!("Building bytecode access count").in_scope(|| { + for pc in traces[&Table::execution()].base[COL_PC].iter() { + bytecode_acc[pc.to_usize()] += F::ONE; + } + }); + // 1st Commitment - let packed_pcs_witness_base = packed_pcs_commit(&mut prover_state, &whir_config, &memory, &acc, &traces); + let packed_pcs_witness_base = packed_pcs_commit( + &mut prover_state, + &whir_config, + &memory, + &memory_acc, + &bytecode_acc, + &traces, + ); let first_whir_n_vars = packed_pcs_witness_base.packed_polynomial.by_ref().n_vars(); // logup (GKR) @@ -90,7 +105,7 @@ pub fn prove_execution( let logup_alpha = prover_state.sample(); prover_state.duplexing(); - let logup_statements = prove_generic_logup(&mut prover_state, logup_c, logup_alpha, &memory, &acc, &traces); + let logup_statements = prove_generic_logup(&mut prover_state, logup_c, logup_alpha, &memory, &memory_acc, &traces); let mut committed_statements: CommittedStatements = Default::default(); for table in ALL_TABLES { committed_statements.insert( @@ -147,6 +162,7 @@ pub fn prove_execution( let global_statements_base = packed_pcs_global_statements( packed_pcs_witness_base.packed_n_vars, log2_strict_usize(memory.len()), + bytecode.log_size(), memory_acc_statements, &table_heights, &committed_statements, diff --git a/crates/lean_prover/src/test_zkvm.rs b/crates/lean_prover/src/test_zkvm.rs index aa48194e..4efba454 100644 --- a/crates/lean_prover/src/test_zkvm.rs +++ b/crates/lean_prover/src/test_zkvm.rs @@ -3,7 +3,7 @@ use lean_compiler::*; use lean_vm::*; use multilinear_toolkit::prelude::*; use rand::{Rng, SeedableRng, rngs::StdRng}; -use utils::poseidon16_permute; +use utils::{init_tracing, poseidon16_permute}; #[test] fn test_zk_vm_all_precompiles() { @@ -114,6 +114,9 @@ def main(): #[test] fn test_prove_fibonacci() { + if std::env::var("FIB_TRACING") == Ok("true".to_string()) { + init_tracing(); + } let n = std::env::var("FIB_N") .unwrap_or("10000".to_string()) .parse::() diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index a85f201d..f5731eeb 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -16,7 +16,7 @@ pub struct ProofVerificationDetails { } pub fn verify_execution( - _bytecode: &Bytecode, + bytecode: &Bytecode, public_input: &[F], proof: Vec, whir_config: &WhirConfigBuilder, @@ -56,8 +56,13 @@ pub fn verify_execution( return Err(ProofError::InvalidProof); } - let parsed_commitment_base = - packed_pcs_parse_commitment(&whir_config, &mut verifier_state, log_memory, &table_n_vars)?; + let parsed_commitment_base = packed_pcs_parse_commitment( + &whir_config, + &mut verifier_state, + log_memory, + bytecode.log_size(), + &table_n_vars, + )?; let logup_c = verifier_state.sample(); verifier_state.duplexing(); @@ -121,6 +126,7 @@ pub fn verify_execution( let global_statements_base = packed_pcs_global_statements( parsed_commitment_base.num_variables, log_memory, + bytecode.log_size(), memory_acc_statements, &table_n_vars, &committed_statements, diff --git a/crates/lean_vm/src/isa/bytecode.rs b/crates/lean_vm/src/isa/bytecode.rs index 9529184c..ee2ddc75 100644 --- a/crates/lean_vm/src/isa/bytecode.rs +++ b/crates/lean_vm/src/isa/bytecode.rs @@ -1,5 +1,7 @@ //! Bytecode representation and management +use p3_util::log2_ceil_usize; + use crate::{CodeAddress, FileId, FunctionName, Hint, SourceLocation}; use super::Instruction; @@ -18,6 +20,20 @@ pub struct Bytecode { pub source_code: BTreeMap, } +impl Bytecode { + pub fn size(&self) -> usize { + self.instructions.len() + } + + pub fn padded_size(&self) -> usize { + self.size().next_power_of_two() + } + + pub fn log_size(&self) -> usize { + log2_ceil_usize(self.size()) + } +} + impl Display for Bytecode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { for (pc, instruction) in self.instructions.iter().enumerate() { diff --git a/crates/sub_protocols/src/packed_pcs.rs b/crates/sub_protocols/src/packed_pcs.rs index efe3a36c..547b4103 100644 --- a/crates/sub_protocols/src/packed_pcs.rs +++ b/crates/sub_protocols/src/packed_pcs.rs @@ -17,6 +17,7 @@ pub struct MultiCommitmentWitness { pub fn packed_pcs_global_statements( packed_n_vars: usize, memory_n_vars: usize, + bytecode_n_vars: usize, memory_acc_statements: Vec>, tables_heights: &BTreeMap, committed_statements: &CommittedStatements, @@ -26,7 +27,10 @@ pub fn packed_pcs_global_statements( let tables_heights_sorted = sort_tables_by_height(tables_heights); let mut global_statements = memory_acc_statements; - let mut offset = 2 << memory_n_vars; + let mut offset = 2 << memory_n_vars; // memory + memory_acc + + let max_table_n_vars = tables_heights_sorted[0].1; + offset += 1 << bytecode_n_vars.max(max_table_n_vars); // bytecode acc for (table, n_vars) in tables_heights_sorted { if table.is_execution_table() { @@ -62,22 +66,29 @@ pub fn packed_pcs_commit( prover_state: &mut impl FSProver, whir_config_builder: &WhirConfigBuilder, memory: &[F], - acc: &[F], + memory_acc: &[F], + bytecode_acc: &[F], traces: &BTreeMap, ) -> MultiCommitmentWitness { - assert_eq!(memory.len(), acc.len()); + assert_eq!(memory.len(), memory_acc.len()); let tables_heights = traces.iter().map(|(table, trace)| (*table, trace.log_n_rows)).collect(); let tables_heights_sorted = sort_tables_by_height(&tables_heights); - + assert!(memory.len() >= 1 << tables_heights_sorted.last().unwrap().1); // memory must be at least as large as the largest table (TODO add some padding at execution when this is not the case) let packed_n_vars = compute_total_n_vars( log2_strict_usize(memory.len()), + log2_strict_usize(bytecode_acc.len()), &tables_heights_sorted.iter().cloned().collect(), ); let mut packed_polynomial = F::zero_vec(1 << packed_n_vars); // TODO avoid cloning all witness data packed_polynomial[..memory.len()].copy_from_slice(memory); let mut offset = memory.len(); - packed_polynomial[offset..offset + acc.len()].copy_from_slice(acc); - offset += acc.len(); + packed_polynomial[offset..offset + memory_acc.len()].copy_from_slice(memory_acc); + offset += memory_acc.len(); + + packed_polynomial[offset..offset + bytecode_acc.len()].copy_from_slice(bytecode_acc); + let largest_table_height = 1 << tables_heights_sorted[0].1; + offset += largest_table_height.max(bytecode_acc.len()); // we may pad bytecode_acc to match largest table height + for (table, log_n_rows) in &tables_heights_sorted { let n_rows = 1 << *log_n_rows; for col_index_f in 0..table.n_columns_f_air() { @@ -111,14 +122,17 @@ pub fn packed_pcs_parse_commitment( whir_config_builder: &WhirConfigBuilder, verifier_state: &mut impl FSVerifier, log_memory: usize, + log_bytecode: usize, tables_heights: &BTreeMap, ) -> Result, ProofError> { - let packed_n_vars = compute_total_n_vars(log_memory, tables_heights); + let packed_n_vars = compute_total_n_vars(log_memory, log_bytecode, tables_heights); WhirConfig::new(whir_config_builder, packed_n_vars).parse_commitment(verifier_state) } -fn compute_total_n_vars(log_memory: usize, tables_heights: &BTreeMap) -> usize { +fn compute_total_n_vars(log_memory: usize, log_bytecode: usize, tables_heights: &BTreeMap) -> usize { + let max_table_log_n_rows = tables_heights.values().copied().max().unwrap(); let total_len = (2 << log_memory) + + (1 << log_bytecode.max(max_table_log_n_rows)) + tables_heights .iter() .map(|(table, log_n_rows)| table.n_commited_columns() << log_n_rows) From c3ec537de8132eb2d96dc4f9901b17273bded410 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 16:09:04 +0400 Subject: [PATCH 03/47] bytecode lookup via logup --- Cargo.lock | 38 +--- Cargo.toml | 6 +- crates/lean_compiler/src/c_compile_final.rs | 10 +- .../src/instruction_encoder.rs | 9 - crates/lean_compiler/src/lib.rs | 1 + crates/lean_prover/Cargo.toml | 1 - crates/lean_prover/src/common.rs | 20 -- crates/lean_prover/src/lib.rs | 9 +- crates/lean_prover/src/prove_execution.rs | 2 + .../execution_trace.rs => src/trace_gen.rs} | 7 +- crates/lean_prover/src/verify_execution.rs | 1 + .../lean_prover/witness_generation/Cargo.toml | 25 --- .../lean_prover/witness_generation/src/lib.rs | 7 - crates/lean_vm/src/isa/bytecode.rs | 3 +- crates/lean_vm/src/tables/execution/air.rs | 8 +- crates/lean_vm/src/tables/execution/mod.rs | 4 +- crates/lean_vm/src/tables/table_enum.rs | 13 +- crates/rec_aggregation/src/recursion.rs | 2 +- crates/sub_protocols/src/generic_logup.rs | 196 ++++++++++++++++-- crates/utils/src/multilinear.rs | 12 +- 20 files changed, 228 insertions(+), 146 deletions(-) rename crates/{lean_prover/witness_generation => lean_compiler}/src/instruction_encoder.rs (91%) delete mode 100644 crates/lean_prover/src/common.rs rename crates/lean_prover/{witness_generation/src/execution_trace.rs => src/trace_gen.rs} (95%) delete mode 100644 crates/lean_prover/witness_generation/Cargo.toml delete mode 100644 crates/lean_prover/witness_generation/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2049508d..0401f0af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,7 +26,7 @@ dependencies = [ [[package]] name = "air" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#bd8a91199f45fcb3c21af056468340e2f9de2d1d" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#1341f4fef5dbb4d32373495d4f0f12047601e16c" dependencies = [ "p3-field", ] @@ -99,7 +99,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backend" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#bd8a91199f45fcb3c21af056468340e2f9de2d1d" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#1341f4fef5dbb4d32373495d4f0f12047601e16c" dependencies = [ "itertools", "p3-field", @@ -190,7 +190,7 @@ dependencies = [ [[package]] name = "constraints-folder" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#bd8a91199f45fcb3c21af056468340e2f9de2d1d" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#1341f4fef5dbb4d32373495d4f0f12047601e16c" dependencies = [ "air 0.3.0", "backend", @@ -266,7 +266,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "fiat-shamir" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#bd8a91199f45fcb3c21af056468340e2f9de2d1d" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#1341f4fef5dbb4d32373495d4f0f12047601e16c" dependencies = [ "p3-field", "p3-koala-bear", @@ -411,7 +411,6 @@ dependencies = [ "sub_protocols", "tracing", "utils", - "witness_generation", "xmss", ] @@ -468,7 +467,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "multilinear-toolkit" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#bd8a91199f45fcb3c21af056468340e2f9de2d1d" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#1341f4fef5dbb4d32373495d4f0f12047601e16c" dependencies = [ "air 0.3.0", "backend", @@ -1049,7 +1048,7 @@ dependencies = [ [[package]] name = "sumcheck" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#bd8a91199f45fcb3c21af056468340e2f9de2d1d" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#1341f4fef5dbb4d32373495d4f0f12047601e16c" dependencies = [ "air 0.3.0", "backend", @@ -1277,7 +1276,7 @@ dependencies = [ [[package]] name = "whir" version = "0.3.0" -source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#bd8a91199f45fcb3c21af056468340e2f9de2d1d" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#1341f4fef5dbb4d32373495d4f0f12047601e16c" dependencies = [ "backend", "fiat-shamir", @@ -1287,6 +1286,7 @@ dependencies = [ "p3-field", "p3-koala-bear", "p3-matrix", + "p3-maybe-rayon", "p3-merkle-tree", "p3-symmetric", "p3-util", @@ -1348,28 +1348,6 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -[[package]] -name = "witness_generation" -version = "0.1.0" -dependencies = [ - "air 0.1.0", - "lean_compiler", - "lean_vm", - "multilinear-toolkit", - "p3-koala-bear", - "p3-monty-31", - "p3-poseidon2", - "p3-symmetric", - "p3-util", - "pest", - "pest_derive", - "rand", - "sub_protocols", - "tracing", - "utils", - "xmss", -] - [[package]] name = "xmss" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9e8d3fd1..09382385 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,7 @@ version = "0.1.0" edition = "2024" [workspace] -members = [ - "crates/*", - "crates/lean_prover/witness_generation", -] +members = ["crates/*"] [workspace.lints] rust.missing_debug_implementations = "warn" @@ -51,7 +48,6 @@ sub_protocols = { path = "crates/sub_protocols" } lean_compiler = { path = "crates/lean_compiler" } lean_prover = { path = "crates/lean_prover" } rec_aggregation = { path = "crates/rec_aggregation" } -witness_generation = { path = "crates/lean_prover/witness_generation" } # External thiserror = "2.0" diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index b1136066..86c3b529 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -1,4 +1,4 @@ -use crate::{F, ir::*, lang::*}; +use crate::{F, instruction_encoder::field_representation, ir::*, lang::*}; use lean_vm::*; use multilinear_toolkit::prelude::*; use std::collections::BTreeMap; @@ -144,9 +144,15 @@ pub fn compile_to_low_level_bytecode( &mut hints, ); } - + let mut encoded_instructions = instructions + .par_iter() + .map(|instr| field_representation(instr)) + .collect::>(); + encoded_instructions.resize(instructions.len().next_power_of_two(), [F::ZERO; N_INSTRUCTION_COLUMNS]); + Ok(Bytecode { instructions, + encoded_instructions, hints, starting_frame_memory, function_locations, diff --git a/crates/lean_prover/witness_generation/src/instruction_encoder.rs b/crates/lean_compiler/src/instruction_encoder.rs similarity index 91% rename from crates/lean_prover/witness_generation/src/instruction_encoder.rs rename to crates/lean_compiler/src/instruction_encoder.rs index 4b883763..8ca28538 100644 --- a/crates/lean_prover/witness_generation/src/instruction_encoder.rs +++ b/crates/lean_compiler/src/instruction_encoder.rs @@ -1,6 +1,5 @@ use lean_vm::*; use multilinear_toolkit::prelude::*; -use utils::padd_with_zero_to_next_power_of_two; pub fn field_representation(instr: &Instruction) -> [F; N_INSTRUCTION_COLUMNS] { let mut fields = [F::ZERO; N_INSTRUCTION_COLUMNS]; @@ -115,11 +114,3 @@ fn set_nu_c(fields: &mut [F; N_INSTRUCTION_COLUMNS], c: &MemOrFp) { } } } - -pub fn bytecode_to_multilinear_polynomial(instructions: &[Instruction]) -> Vec { - let res = instructions - .par_iter() - .flat_map(|instr| padd_with_zero_to_next_power_of_two(&field_representation(instr))) - .collect::>(); - padd_with_zero_to_next_power_of_two(&res) -} diff --git a/crates/lean_compiler/src/lib.rs b/crates/lean_compiler/src/lib.rs index a59e0d7c..6728e6b4 100644 --- a/crates/lean_compiler/src/lib.rs +++ b/crates/lean_compiler/src/lib.rs @@ -11,6 +11,7 @@ use crate::{ mod a_simplify_lang; mod b_compile_intermediate; mod c_compile_final; +mod instruction_encoder; pub mod ir; mod lang; mod parser; diff --git a/crates/lean_prover/Cargo.toml b/crates/lean_prover/Cargo.toml index caf9d445..48ec0a47 100644 --- a/crates/lean_prover/Cargo.toml +++ b/crates/lean_prover/Cargo.toml @@ -21,7 +21,6 @@ air.workspace = true sub_protocols.workspace = true lean_vm.workspace = true lean_compiler.workspace = true -witness_generation.workspace = true multilinear-toolkit.workspace = true itertools.workspace = true diff --git a/crates/lean_prover/src/common.rs b/crates/lean_prover/src/common.rs deleted file mode 100644 index 411cf433..00000000 --- a/crates/lean_prover/src/common.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::*; -use lean_vm::*; - -pub(crate) fn fold_bytecode(bytecode: &Bytecode, folding_challenges: &MultilinearPoint) -> Vec { - let encoded_bytecode = padd_with_zero_to_next_power_of_two( - &bytecode - .instructions - .par_iter() - .flat_map(|i| padd_with_zero_to_next_power_of_two(&field_representation(i))) - .collect::>(), - ); - fold_multilinear_chunks(&encoded_bytecode, folding_challenges) -} - -fn split_at(stmt: &MultiEvaluation, start: usize, end: usize) -> Vec> { - vec![MultiEvaluation::new( - stmt.point.clone(), - stmt.values[start..end].to_vec(), - )] -} diff --git a/crates/lean_prover/src/lib.rs b/crates/lean_prover/src/lib.rs index d9cde5dc..5b7736c3 100644 --- a/crates/lean_prover/src/lib.rs +++ b/crates/lean_prover/src/lib.rs @@ -4,16 +4,15 @@ use lean_vm::{EF, F}; use multilinear_toolkit::prelude::*; use utils::*; -use lean_vm::execute_bytecode; -use witness_generation::*; +mod trace_gen; -mod common; pub mod prove_execution; +pub mod verify_execution; + #[cfg(test)] mod test_zkvm; -pub mod verify_execution; -pub use witness_generation::bytecode_to_multilinear_polynomial; +use trace_gen::*; // Right now, hash digests = 8 koala-bear (p = 2^31 - 2^24 + 1, i.e. ≈ 30.98 bits per field element) // so ≈ 123.92 bits of security against collisions diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index eb60e788..95619186 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -112,6 +112,8 @@ pub fn prove_execution( &logup_alphas_eq_poly, &memory, &memory_acc, + &bytecode.encoded_instructions, + &bytecode_acc, &traces, ); let mut committed_statements: CommittedStatements = Default::default(); diff --git a/crates/lean_prover/witness_generation/src/execution_trace.rs b/crates/lean_prover/src/trace_gen.rs similarity index 95% rename from crates/lean_prover/witness_generation/src/execution_trace.rs rename to crates/lean_prover/src/trace_gen.rs index cbedbf18..4bc69239 100644 --- a/crates/lean_prover/witness_generation/src/execution_trace.rs +++ b/crates/lean_prover/src/trace_gen.rs @@ -1,4 +1,3 @@ -use crate::instruction_encoder::field_representation; use lean_vm::*; use multilinear_toolkit::prelude::*; use std::{array, collections::BTreeMap, iter::repeat_n}; @@ -17,7 +16,7 @@ pub fn get_execution_trace(bytecode: &Bytecode, execution_result: ExecutionResul let n_cycles = execution_result.pcs.len(); let memory = &execution_result.memory; - let mut main_trace: [Vec; N_EXEC_AIR_COLUMNS + N_TEMPORARY_EXEC_COLUMNS] = + let mut main_trace: [Vec; N_TOTAL_EXECUTION_COLUMNS + N_TEMPORARY_EXEC_COLUMNS] = array::from_fn(|_| F::zero_vec(n_cycles.next_power_of_two())); for col in &mut main_trace { unsafe { @@ -30,7 +29,7 @@ pub fn get_execution_trace(bytecode: &Bytecode, execution_result: ExecutionResul .zip(execution_result.fps.par_iter()) .for_each(|((trace_row, &pc), &fp)| { let instruction = &bytecode.instructions[pc]; - let field_repr = field_representation(instruction); + let field_repr = bytecode.encoded_instructions[pc]; let mut addr_a = F::ZERO; if field_repr[instr_idx(COL_FLAG_A)].is_zero() { @@ -57,7 +56,7 @@ pub fn get_execution_trace(bytecode: &Bytecode, execution_result: ExecutionResul let value_c = memory.0[addr_c.to_usize()].unwrap(); for (j, field) in field_repr.iter().enumerate() { - *trace_row[j + N_COMMITTED_EXEC_COLUMNS] = *field; + *trace_row[j + N_RUNTIME_COLUMNS] = *field; } let nu_a = field_repr[instr_idx(COL_FLAG_A)] * field_repr[instr_idx(COL_OPERAND_A)] diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index 1f6ae551..7e17bbd9 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -75,6 +75,7 @@ pub fn verify_execution( logup_c, &logup_alphas_eq_poly, log_memory, + &bytecode.encoded_instructions, &table_n_vars, )?; let mut committed_statements: CommittedStatements = Default::default(); diff --git a/crates/lean_prover/witness_generation/Cargo.toml b/crates/lean_prover/witness_generation/Cargo.toml deleted file mode 100644 index 714476e9..00000000 --- a/crates/lean_prover/witness_generation/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "witness_generation" -version.workspace = true -edition.workspace = true - -[lints] -workspace = true - -[dependencies] -pest.workspace = true -pest_derive.workspace = true -utils.workspace = true -xmss.workspace = true -rand.workspace = true -p3-poseidon2.workspace = true -p3-koala-bear.workspace = true -p3-symmetric.workspace = true -p3-util.workspace = true -tracing.workspace = true -air.workspace = true -sub_protocols.workspace = true -lean_vm.workspace = true -lean_compiler.workspace = true -multilinear-toolkit.workspace = true -p3-monty-31.workspace = true \ No newline at end of file diff --git a/crates/lean_prover/witness_generation/src/lib.rs b/crates/lean_prover/witness_generation/src/lib.rs deleted file mode 100644 index e665ab96..00000000 --- a/crates/lean_prover/witness_generation/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![cfg_attr(not(test), allow(unused_crate_dependencies))] - -mod execution_trace; -mod instruction_encoder; - -pub use execution_trace::*; -pub use instruction_encoder::*; diff --git a/crates/lean_vm/src/isa/bytecode.rs b/crates/lean_vm/src/isa/bytecode.rs index ee2ddc75..87e6aa54 100644 --- a/crates/lean_vm/src/isa/bytecode.rs +++ b/crates/lean_vm/src/isa/bytecode.rs @@ -2,7 +2,7 @@ use p3_util::log2_ceil_usize; -use crate::{CodeAddress, FileId, FunctionName, Hint, SourceLocation}; +use crate::{CodeAddress, F, FileId, FunctionName, Hint, N_INSTRUCTION_COLUMNS, SourceLocation}; use super::Instruction; use std::collections::BTreeMap; @@ -12,6 +12,7 @@ use std::fmt::{Display, Formatter}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Bytecode { pub instructions: Vec, + pub encoded_instructions: Vec<[F; N_INSTRUCTION_COLUMNS]>, // padded to power of two length (with zero rows) pub hints: BTreeMap>, // pc -> hints pub starting_frame_memory: usize, // debug diff --git a/crates/lean_vm/src/tables/execution/air.rs b/crates/lean_vm/src/tables/execution/air.rs index 8d843f27..b476ed6c 100644 --- a/crates/lean_vm/src/tables/execution/air.rs +++ b/crates/lean_vm/src/tables/execution/air.rs @@ -2,9 +2,9 @@ use multilinear_toolkit::prelude::*; use crate::{EF, ExecutionTable, ExtraDataForBuses, eval_virtual_bus_column}; -pub const N_COMMITTED_EXEC_COLUMNS: usize = 8; +pub const N_RUNTIME_COLUMNS: usize = 8; pub const N_INSTRUCTION_COLUMNS: usize = 14; -pub const N_EXEC_AIR_COLUMNS: usize = N_INSTRUCTION_COLUMNS + N_COMMITTED_EXEC_COLUMNS; +pub const N_TOTAL_EXECUTION_COLUMNS: usize = N_INSTRUCTION_COLUMNS + N_RUNTIME_COLUMNS; // Committed columns (IMPORTANT: they must be the first columns) pub const COL_PC: usize = 0; @@ -42,7 +42,7 @@ impl Air for ExecutionTable { type ExtraData = ExtraDataForBuses; fn n_columns_f_air(&self) -> usize { - N_EXEC_AIR_COLUMNS + N_TOTAL_EXECUTION_COLUMNS } fn n_columns_ef_air(&self) -> usize { 0 @@ -144,5 +144,5 @@ impl Air for ExecutionTable { } pub const fn instr_idx(col_index_in_air: usize) -> usize { - col_index_in_air - N_COMMITTED_EXEC_COLUMNS + col_index_in_air - N_RUNTIME_COLUMNS } diff --git a/crates/lean_vm/src/tables/execution/mod.rs b/crates/lean_vm/src/tables/execution/mod.rs index d0c799e4..b0fd7c52 100644 --- a/crates/lean_vm/src/tables/execution/mod.rs +++ b/crates/lean_vm/src/tables/execution/mod.rs @@ -21,7 +21,7 @@ impl TableT for ExecutionTable { } fn n_columns_f_total(&self) -> usize { - N_EXEC_AIR_COLUMNS + N_TEMPORARY_EXEC_COLUMNS + N_TOTAL_EXECUTION_COLUMNS + N_TEMPORARY_EXEC_COLUMNS } fn lookups_f(&self) -> Vec { @@ -55,7 +55,7 @@ impl TableT for ExecutionTable { } fn padding_row_f(&self) -> Vec { - let mut padding_row = vec![F::ZERO; N_EXEC_AIR_COLUMNS + N_TEMPORARY_EXEC_COLUMNS]; + let mut padding_row = vec![F::ZERO; N_TOTAL_EXECUTION_COLUMNS + N_TEMPORARY_EXEC_COLUMNS]; padding_row[COL_PC] = F::from_usize(ENDING_PC); padding_row[COL_JUMP] = F::ONE; padding_row[COL_FLAG_A] = F::ONE; diff --git a/crates/lean_vm/src/tables/table_enum.rs b/crates/lean_vm/src/tables/table_enum.rs index a74acd6b..7084f38d 100644 --- a/crates/lean_vm/src/tables/table_enum.rs +++ b/crates/lean_vm/src/tables/table_enum.rs @@ -1,5 +1,5 @@ use multilinear_toolkit::prelude::*; -use utils::MEMORY_TABLE_INDEX; +use utils::FIRST_NORMAL_TABLE_INDEX; use crate::*; @@ -48,7 +48,7 @@ impl Table { PF::from_usize(self.index()) } pub const fn index(&self) -> usize { - unsafe { *(self as *const Self as *const usize) + MEMORY_TABLE_INDEX + 1 } + unsafe { *(self as *const Self as *const usize) + FIRST_NORMAL_TABLE_INDEX } } } @@ -122,7 +122,8 @@ impl Air for Table { } pub fn max_bus_width() -> usize { - 1 + ALL_TABLES.iter().map(|table| table.bus().data.len()).max().unwrap() + let max_bus_in_table = ALL_TABLES.iter().map(|table| table.bus().data.len()).max().unwrap(); + 1 + max_bus_in_table.max(N_INSTRUCTION_COLUMNS) } pub fn max_air_constraints() -> usize { @@ -131,12 +132,16 @@ pub fn max_air_constraints() -> usize { #[cfg(test)] mod tests { + use utils::{BYTECODE_TABLE_INDEX, MEMORY_TABLE_INDEX}; + use super::*; #[test] fn test_table_indices() { for (i, table) in ALL_TABLES.iter().enumerate() { - assert_eq!(table.index(), i + MEMORY_TABLE_INDEX + 1); + assert_ne!(table.index(), MEMORY_TABLE_INDEX); + assert_ne!(table.index(), BYTECODE_TABLE_INDEX); + assert_eq!(table.index(), i + FIRST_NORMAL_TABLE_INDEX); } } } diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index fca374e7..537eb9ad 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -290,7 +290,7 @@ def main(): ); replacements.insert( "N_COMMITTED_EXEC_COLUMNS_PLACEHOLDER".to_string(), - N_COMMITTED_EXEC_COLUMNS.to_string(), + N_RUNTIME_COLUMNS.to_string(), ); replacements.insert( "TOTAL_WHIR_STATEMENTS_BASE_PLACEHOLDER".to_string(), diff --git a/crates/sub_protocols/src/generic_logup.rs b/crates/sub_protocols/src/generic_logup.rs index 398b4518..5f1ac54a 100644 --- a/crates/sub_protocols/src/generic_logup.rs +++ b/crates/sub_protocols/src/generic_logup.rs @@ -1,16 +1,20 @@ use crate::{prove_gkr_quotient, verify_gkr_quotient}; use lean_vm::BusDirection; use lean_vm::BusTable; +use lean_vm::COL_PC; use lean_vm::ColIndex; use lean_vm::DIMENSION; use lean_vm::EF; use lean_vm::F; +use lean_vm::N_INSTRUCTION_COLUMNS; +use lean_vm::N_RUNTIME_COLUMNS; use lean_vm::Table; use lean_vm::TableT; use lean_vm::TableTrace; use lean_vm::sort_tables_by_height; use multilinear_toolkit::prelude::*; use std::collections::BTreeMap; +use utils::BYTECODE_TABLE_INDEX; use utils::MEMORY_TABLE_INDEX; use utils::VarCount; use utils::VecOrSlice; @@ -39,12 +43,14 @@ pub fn prove_generic_logup( c: EF, alphas_eq_poly: &[EF], memory: &[F], - acc: &[F], + memory_acc: &[F], + bytecode: &[[F; N_INSTRUCTION_COLUMNS]], + bytecode_acc: &[F], traces: &BTreeMap, ) -> GenericLogupStatements { assert!(memory[0].is_zero()); assert!(memory.len().is_power_of_two()); - assert_eq!(memory.len(), acc.len()); + assert_eq!(memory.len(), memory_acc.len()); assert!(memory.len() >= traces.values().map(|t| 1 << t.log_n_rows).max().unwrap()); let tables_heights = traces.iter().map(|(table, trace)| (*table, trace.log_n_rows)).collect(); @@ -52,17 +58,21 @@ pub fn prove_generic_logup( let total_n_vars = compute_total_n_vars( log2_strict_usize(memory.len()), + log2_strict_usize(bytecode.len()), &tables_heights_sorted.iter().cloned().collect(), ); let mut numerators = EF::zero_vec(1 << total_n_vars); let mut denominators = EF::zero_vec(1 << total_n_vars); + let mut offset = 0; + // Memory: ... - numerators[..memory.len()] + assert_eq!(memory.len(), memory_acc.len()); + numerators[offset..][..memory.len()] .par_iter_mut() - .zip(acc) // TODO embedding overhead + .zip(memory_acc) // TODO embedding overhead .for_each(|(num, a)| *num = EF::from(-*a)); // Note the negative sign here - denominators[..memory.len()] + denominators[offset..][..memory.len()] .par_iter_mut() .zip(memory.par_iter().enumerate()) .for_each(|(denom, (i, &mem_value))| { @@ -72,15 +82,61 @@ pub fn prove_generic_logup( alphas_eq_poly, ) }); + offset += memory.len(); + + // Bytecode + assert_eq!(bytecode.len(), bytecode_acc.len()); + numerators[offset..][..bytecode_acc.len()] + .par_iter_mut() + .zip(bytecode_acc) // TODO embedding overhead + .for_each(|(num, a)| *num = EF::from(-*a)); // Note the negative sign here + denominators[offset..][..bytecode.len()] + .par_iter_mut() + .zip(bytecode.par_iter().enumerate()) + .for_each(|(denom, (i, &instr))| { + *denom = c - finger_print( + F::from_usize(BYTECODE_TABLE_INDEX), + &[vec![F::from_usize(i)], instr.to_vec()].concat(), + alphas_eq_poly, + ) + }); + let max_table_height = 1 << tables_heights_sorted[0].1; + if bytecode.len() < max_table_height { + // padding + denominators[offset + bytecode.len()..offset + max_table_height] + .par_iter_mut() + .for_each(|d| *d = EF::ONE); + } + offset += max_table_height.max(bytecode.len()); // ... Rest of the tables: - let mut offset = memory.len(); for (table, _) in &tables_heights_sorted { let trace = &traces[table]; let log_n_rows = trace.log_n_rows; - // I] Bus (data flow between tables) + if *table == Table::execution() { + // 0] bytecode lookup + let pc_column = &trace.base[COL_PC]; + let bytecode_columns = trace.base[N_RUNTIME_COLUMNS..][..N_INSTRUCTION_COLUMNS] + .iter() + .collect::>(); + numerators[offset..][..1 << log_n_rows].par_iter_mut().for_each(|num| { + *num = EF::ONE; + }); // TODO embedding overhead + denominators[offset..][..1 << log_n_rows] + .par_iter_mut() + .enumerate() + .for_each(|(i, denom)| { + let mut data = vec![pc_column[i]]; + for col in &bytecode_columns { + data.push(col[i]); + } + *denom = c - finger_print(F::from_usize(BYTECODE_TABLE_INDEX), &data, alphas_eq_poly) + }); + offset += 1 << log_n_rows; + } + // I] Bus (data flow between tables) let bus = table.bus(); numerators[offset..][..1 << log_n_rows] .par_iter_mut() @@ -114,7 +170,6 @@ pub fn prove_generic_logup( offset += 1 << log_n_rows; // II] Lookup into memory - let mut value_columns_f = Vec::>::new(); for cols_f in table.lookup_f_value_columns(trace) { value_columns_f.push(cols_f.iter().map(|s| VecOrSlice::Slice(s)).collect()); @@ -174,27 +229,56 @@ pub fn prove_generic_logup( // Memory: ... let memory_acc_point = MultilinearPoint(from_end(&claim_point_gkr, log2_strict_usize(memory.len())).to_vec()); - let value_acc = acc.evaluate(&memory_acc_point); + let value_acc = memory_acc.evaluate(&memory_acc_point); prover_state.add_extension_scalar(value_acc); let value_memory = memory.evaluate(&memory_acc_point); prover_state.add_extension_scalar(value_memory); + let bytecode_acc_point = MultilinearPoint(from_end(&claim_point_gkr, log2_strict_usize(bytecode.len())).to_vec()); + let value_bytecode_acc = bytecode_acc.evaluate(&bytecode_acc_point); + prover_state.add_extension_scalar(value_bytecode_acc); + + // evaluation on bytecode itself can be done directly by the verifier + // ... Rest of the tables: let mut points = BTreeMap::new(); let mut bus_numerators_values = BTreeMap::new(); let mut bus_denominators_values = BTreeMap::new(); let mut columns_values = BTreeMap::new(); - let mut offset = memory.len(); + let mut offset = memory.len() + max_table_height.max(bytecode.len()); for (table, _) in &tables_heights_sorted { let trace = &traces[table]; let log_n_rows = trace.log_n_rows; let inner_point = MultilinearPoint(from_end(&claim_point_gkr, log_n_rows).to_vec()); points.insert(*table, inner_point.clone()); + let mut table_values = BTreeMap::::new(); - // I] Bus (data flow between tables) + if table == &Table::execution() { + // 0] bytecode lookup + let pc_column = &trace.base[COL_PC]; + let bytecode_columns = trace.base[N_RUNTIME_COLUMNS..][..N_INSTRUCTION_COLUMNS] + .iter() + .collect::>(); + + let eval_on_pc = pc_column.evaluate(&inner_point); + prover_state.add_extension_scalar(eval_on_pc); + assert!(!table_values.contains_key(&COL_PC)); + table_values.insert(COL_PC, eval_on_pc); + + for (i, col) in bytecode_columns.iter().enumerate() { + let eval_on_instr_col = col.evaluate(&inner_point); + prover_state.add_extension_scalar(eval_on_instr_col); + let global_index = N_RUNTIME_COLUMNS + i; + assert!(!table_values.contains_key(&global_index)); + table_values.insert(global_index, eval_on_instr_col); + } + + offset += 1 << log_n_rows; + } + // I] Bus (data flow between tables) let eval_on_selector = trace.base[table.bus().selector].evaluate(&inner_point) * table.bus().direction.to_field_flag(); prover_state.add_extension_scalar(eval_on_selector); @@ -206,8 +290,6 @@ pub fn prove_generic_logup( bus_denominators_values.insert(*table, eval_on_data); // II] Lookup into memory - - let mut table_values = BTreeMap::::new(); for lookup_f in table.lookups_f() { let index_eval = trace.base[lookup_f.index].evaluate(&inner_point); prover_state.add_extension_scalar(index_eval); @@ -265,11 +347,16 @@ pub fn verify_generic_logup( c: EF, alphas_eq_poly: &[EF], log_memory: usize, + bytecode: &[[F; N_INSTRUCTION_COLUMNS]], table_log_n_rows: &BTreeMap, ) -> ProofResult { let tables_heights_sorted = sort_tables_by_height(table_log_n_rows); - let total_n_vars = compute_total_n_vars(log_memory, &tables_heights_sorted.iter().cloned().collect()); + let total_n_vars = compute_total_n_vars( + log_memory, + log2_strict_usize(bytecode.len()), + &tables_heights_sorted.iter().cloned().collect(), + ); let (sum, point_gkr, numerators_value, denominators_value) = verify_gkr_quotient(verifier_state, total_n_vars)?; @@ -297,22 +384,87 @@ pub fn verify_generic_logup( &[value_index, value_memory], alphas_eq_poly, )); + let mut offset = 1 << log_memory; + + // Bytecode + let log_bytecode = log2_strict_usize(bytecode.len()); + let log_bytecode_padded = log2_strict_usize(bytecode.len()).max(tables_heights_sorted[0].1); + let bytecode_acc_point = MultilinearPoint(from_end(&point_gkr, log_bytecode).to_vec()); + let bits = to_big_endian_in_field::(offset >> log_bytecode, total_n_vars - log_bytecode); + let pref = MultilinearPoint(bits).eq_poly_outside(&MultilinearPoint( + point_gkr[..total_n_vars - log_bytecode].to_vec(), + )); + let bits_padded = to_big_endian_in_field::(offset >> log_bytecode_padded, total_n_vars - log_bytecode_padded); + let pref_padded = MultilinearPoint(bits_padded).eq_poly_outside(&MultilinearPoint( + point_gkr[..total_n_vars - log_bytecode_padded].to_vec(), + )); + + let value_bytecode_acc = verifier_state.next_extension_scalar()?; + retrieved_numerators_value -= pref * value_bytecode_acc; + + // Bytecode denominator - computed directly by verifier + let bytecode_index = mle_of_01234567_etc(&bytecode_acc_point); + let bytecode_value: Vec = (0..N_INSTRUCTION_COLUMNS) + .map(|col| { + bytecode + .iter() + .map(|row| row[col]) + .collect::>() + .evaluate(&bytecode_acc_point) + }) + .collect(); + retrieved_denominators_value += pref + * (c - finger_print( + F::from_usize(BYTECODE_TABLE_INDEX), + &[vec![bytecode_index], bytecode_value].concat(), + alphas_eq_poly, + )); + // Padding for bytecode + retrieved_denominators_value += + pref_padded * mle_of_zeros_then_ones(bytecode.len(), &from_end(&point_gkr, log_bytecode_padded)); + offset += 1 << log_bytecode_padded; // ... Rest of the tables: let mut points = BTreeMap::new(); let mut bus_numerators_values = BTreeMap::new(); let mut bus_denominators_values = BTreeMap::new(); let mut columns_values = BTreeMap::new(); - let mut offset = 1 << log_memory; + for &(table, log_n_rows) in &tables_heights_sorted { let n_missing_vars = total_n_vars - log_n_rows; let inner_point = MultilinearPoint(from_end(&point_gkr, log_n_rows).to_vec()); let missing_point = MultilinearPoint(point_gkr[..n_missing_vars].to_vec()); points.insert(table, inner_point.clone()); + let mut table_values = BTreeMap::::new(); - // I] Bus (data flow between tables) + if table == Table::execution() { + // 0] bytecode lookup + let eval_on_pc = verifier_state.next_extension_scalar()?; + table_values.insert(COL_PC, eval_on_pc); + + let mut instr_evals = Vec::with_capacity(N_INSTRUCTION_COLUMNS); + for i in 0..N_INSTRUCTION_COLUMNS { + let eval_on_instr_col = verifier_state.next_extension_scalar()?; + let global_index = N_RUNTIME_COLUMNS + i; + table_values.insert(global_index, eval_on_instr_col); + instr_evals.push(eval_on_instr_col); + } + + let bits = to_big_endian_in_field::(offset >> log_n_rows, n_missing_vars); + let pref = MultilinearPoint(bits).eq_poly_outside(&missing_point); + retrieved_numerators_value += pref; // numerator is 1 + retrieved_denominators_value += pref + * (c - finger_print( + F::from_usize(BYTECODE_TABLE_INDEX), + &[vec![eval_on_pc], instr_evals].concat(), + alphas_eq_poly, + )); + + offset += 1 << log_n_rows; + } + // I] Bus (data flow between tables) let eval_on_selector = verifier_state.next_extension_scalar()?; let bits = to_big_endian_in_field::(offset >> log_n_rows, n_missing_vars); @@ -328,8 +480,6 @@ pub fn verify_generic_logup( offset += 1 << log_n_rows; // II] Lookup into memory - - let mut table_values = BTreeMap::::new(); for lookup_f in table.lookups_f() { let index_eval = verifier_state.next_extension_scalar()?; assert!(!table_values.contains_key(&lookup_f.index)); @@ -342,7 +492,7 @@ pub fn verify_generic_logup( let bits = to_big_endian_in_field::(offset >> log_n_rows, n_missing_vars); let pref = MultilinearPoint(bits).eq_poly_outside(&missing_point); - retrieved_numerators_value += pref; + retrieved_numerators_value += pref; // numerator is 1 retrieved_denominators_value += pref * (c - finger_print( F::from_usize(MEMORY_TABLE_INDEX), @@ -381,10 +531,10 @@ pub fn verify_generic_logup( retrieved_denominators_value += mle_of_zeros_then_ones(offset, &point_gkr); // to compensate for the final padding: XYZ111111...1 if retrieved_numerators_value != numerators_value { - return Err(ProofError::InvalidProof); + panic!() } if retrieved_denominators_value != denominators_value { - return Err(ProofError::InvalidProof); + panic!() } Ok(GenericLogupStatements { @@ -405,8 +555,10 @@ fn offset_for_table(table: &Table, log_n_rows: usize) -> usize { num_cols << log_n_rows } -fn compute_total_n_vars(log_memory: usize, tables_heights: &BTreeMap) -> usize { +fn compute_total_n_vars(log_memory: usize, log_bytecode: usize, tables_heights: &BTreeMap) -> usize { + let max_table_height = 1 << tables_heights.values().copied().max().unwrap(); let total_len = (1 << log_memory) + + (1 << log_bytecode).max(max_table_height) + (1 << tables_heights[&Table::execution()]) // bytecode + tables_heights .iter() .map(|(table, log_n_rows)| offset_for_table(table, *log_n_rows)) diff --git a/crates/utils/src/multilinear.rs b/crates/utils/src/multilinear.rs index fd516733..50c4932f 100644 --- a/crates/utils/src/multilinear.rs +++ b/crates/utils/src/multilinear.rs @@ -116,16 +116,20 @@ pub fn mle_of_01234567_etc(point: &[F]) -> F { } } -/// table = 0 is reversed for memory +/// table = 0 is reversed for memory lookup pub const MEMORY_TABLE_INDEX: usize = 0; +/// table = 1 is reversed for bytecode lookup +pub const BYTECODE_TABLE_INDEX: usize = 1; +/// All the remaining tables come after memory and bytecode (special cases) +pub const FIRST_NORMAL_TABLE_INDEX: usize = 2; pub fn finger_print>, EF: ExtensionField + ExtensionField>( table: F, data: &[IF], - alpha_powers: &[EF], + alphas_eq_poly: &[EF], ) -> EF { - assert!(alpha_powers.len() > data.len()); - dot_product::(alpha_powers[1..].iter().copied(), data.iter().copied()) + table + assert!(alphas_eq_poly.len() > data.len()); + dot_product::(alphas_eq_poly[1..].iter().copied(), data.iter().copied()) + table } #[cfg(test)] From bce4dd8f75769eb88a6cf288488c35d7999d2343 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 16:53:35 +0400 Subject: [PATCH 04/47] all good --- TODO.md | 1 + crates/lean_compiler/src/c_compile_final.rs | 2 +- crates/lean_prover/src/prove_execution.rs | 36 ++++++++------- crates/lean_prover/src/test_zkvm.rs | 7 +++ crates/lean_prover/src/verify_execution.rs | 30 ++++++++----- crates/lean_vm/src/isa/bytecode.rs | 2 +- crates/rec_aggregation/recursion.py | 10 ++--- crates/sub_protocols/src/generic_logup.rs | 50 ++++++++++++--------- crates/sub_protocols/src/packed_pcs.rs | 12 ++--- 9 files changed, 88 insertions(+), 62 deletions(-) diff --git a/TODO.md b/TODO.md index 83747478..9bb2b600 100644 --- a/TODO.md +++ b/TODO.md @@ -37,6 +37,7 @@ But we can get the bost of both worlds (suggested by Lev, TODO implement): - Fiat Shamir: add a claim tracing feature, to ensure all the claims are indeed checked (Lev) - Double Check AIR constraints, logup overflows etc - Formal Verification +- Padd with noop cycles to always ensure memory size >= bytecode size (liveness), and ensure this condition is checked by the verifier (soundness) # Ideas diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index 86c3b529..5ee0a6b4 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -149,7 +149,7 @@ pub fn compile_to_low_level_bytecode( .map(|instr| field_representation(instr)) .collect::>(); encoded_instructions.resize(instructions.len().next_power_of_two(), [F::ZERO; N_INSTRUCTION_COLUMNS]); - + Ok(Bytecode { instructions, encoded_instructions, diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index 95619186..d70ef187 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -89,7 +89,7 @@ pub fn prove_execution( }); // 1st Commitment - let packed_pcs_witness_base = packed_pcs_commit( + let packed_pcs_witness = packed_pcs_commit( &mut prover_state, &whir_config, &memory, @@ -97,7 +97,7 @@ pub fn prove_execution( &bytecode_acc, &traces, ); - let first_whir_n_vars = packed_pcs_witness_base.packed_polynomial.by_ref().n_vars(); + let first_whir_n_vars = packed_pcs_witness.packed_polynomial.by_ref().n_vars(); // logup (GKR) let logup_c = prover_state.sample(); @@ -152,41 +152,45 @@ pub fn prove_execution( prover_state.duplexing(); let public_memory_eval = (&memory[..public_memory_size]).evaluate(&public_memory_random_point); - let memory_acc_statements = vec![ + let previous_statements = vec![ SparseStatement::new( - packed_pcs_witness_base.packed_n_vars, - logup_statements.memory_acc_point, + packed_pcs_witness.packed_n_vars, + logup_statements.memory_and_acc_point, vec![ SparseValue::new(0, logup_statements.value_memory), - SparseValue::new(1, logup_statements.value_acc), + SparseValue::new(1, logup_statements.value_memory_acc), ], ), SparseStatement::new( - packed_pcs_witness_base.packed_n_vars, + packed_pcs_witness.packed_n_vars, public_memory_random_point, vec![SparseValue::new(0, public_memory_eval)], ), + SparseStatement::new( + packed_pcs_witness.packed_n_vars, + logup_statements.bytecode_and_acc_point, + vec![SparseValue::new( + (2 * memory.len()) >> bytecode.log_size(), + logup_statements.value_bytecode_acc, + )], + ), ]; let table_heights = traces.iter().map(|(table, trace)| (*table, trace.log_n_rows)).collect(); let global_statements_base = packed_pcs_global_statements( - packed_pcs_witness_base.packed_n_vars, + packed_pcs_witness.packed_n_vars, log2_strict_usize(memory.len()), bytecode.log_size(), - memory_acc_statements, + previous_statements, &table_heights, &committed_statements, ); - WhirConfig::new( - &whir_config, - packed_pcs_witness_base.packed_polynomial.by_ref().n_vars(), - ) - .prove( + WhirConfig::new(&whir_config, packed_pcs_witness.packed_polynomial.by_ref().n_vars()).prove( &mut prover_state, global_statements_base, - packed_pcs_witness_base.inner_witness, - &packed_pcs_witness_base.packed_polynomial.by_ref(), + packed_pcs_witness.inner_witness, + &packed_pcs_witness.packed_polynomial.by_ref(), ); let proof_size_fe = prover_state.pruned_proof().proof_size_fe(); diff --git a/crates/lean_prover/src/test_zkvm.rs b/crates/lean_prover/src/test_zkvm.rs index 4efba454..a2a02db5 100644 --- a/crates/lean_prover/src/test_zkvm.rs +++ b/crates/lean_prover/src/test_zkvm.rs @@ -41,6 +41,13 @@ def main(): return +def aux(): + a: Mut = 10 + for i in unroll(0,1000): + a += 1 + assert a == 1010 + return + "#; const N: usize = 11; diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index 7e17bbd9..335abf7e 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -46,7 +46,7 @@ pub fn verify_execution( } } // check memory is bigger than any other table - if log_memory < *table_n_vars.values().max().unwrap() { + if log_memory < (*table_n_vars.values().max().unwrap()).max(bytecode.log_size()) { return Err(ProofError::InvalidProof); } @@ -56,7 +56,7 @@ pub fn verify_execution( return Err(ProofError::InvalidProof); } - let parsed_commitment_base = packed_pcs_parse_commitment( + let parsed_commitment = packed_pcs_parse_commitment( &whir_config, &mut verifier_state, log_memory, @@ -115,34 +115,42 @@ pub fn verify_execution( verifier_state.duplexing(); let public_memory_eval = public_memory.evaluate(&public_memory_random_point); - let memory_acc_statements = vec![ + let previous_statements = vec![ SparseStatement::new( - parsed_commitment_base.num_variables, - logup_statements.memory_acc_point, + parsed_commitment.num_variables, + logup_statements.memory_and_acc_point, vec![ SparseValue::new(0, logup_statements.value_memory), - SparseValue::new(1, logup_statements.value_acc), + SparseValue::new(1, logup_statements.value_memory_acc), ], ), SparseStatement::new( - parsed_commitment_base.num_variables, + parsed_commitment.num_variables, public_memory_random_point, vec![SparseValue::new(0, public_memory_eval)], ), + SparseStatement::new( + parsed_commitment.num_variables, + logup_statements.bytecode_and_acc_point, + vec![SparseValue::new( + (2 << log_memory) >> bytecode.log_size(), + logup_statements.value_bytecode_acc, + )], + ), ]; let global_statements_base = packed_pcs_global_statements( - parsed_commitment_base.num_variables, + parsed_commitment.num_variables, log_memory, bytecode.log_size(), - memory_acc_statements, + previous_statements, &table_n_vars, &committed_statements, ); let total_whir_statements_base = global_statements_base.iter().map(|s| s.values.len()).sum(); - WhirConfig::new(&whir_config, parsed_commitment_base.num_variables).verify( + WhirConfig::new(&whir_config, parsed_commitment.num_variables).verify( &mut verifier_state, - &parsed_commitment_base, + &parsed_commitment, global_statements_base, )?; diff --git a/crates/lean_vm/src/isa/bytecode.rs b/crates/lean_vm/src/isa/bytecode.rs index 87e6aa54..49de9cc6 100644 --- a/crates/lean_vm/src/isa/bytecode.rs +++ b/crates/lean_vm/src/isa/bytecode.rs @@ -13,7 +13,7 @@ use std::fmt::{Display, Formatter}; pub struct Bytecode { pub instructions: Vec, pub encoded_instructions: Vec<[F; N_INSTRUCTION_COLUMNS]>, // padded to power of two length (with zero rows) - pub hints: BTreeMap>, // pc -> hints + pub hints: BTreeMap>, // pc -> hints pub starting_frame_memory: usize, // debug pub function_locations: BTreeMap, diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index e0791dec..92ed54eb 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -215,7 +215,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip copy_5(retrieved_numerators_value, numerators_value) copy_5(retrieved_denominators_value, denominators_value) - memory_acc_point = point_gkr + (N_VARS_FIRST_GKR - log_memory) * DIM + memory_and_acc_point = point_gkr + (N_VARS_FIRST_GKR - log_memory) * DIM # END OF GENERIC LOGUP @@ -481,22 +481,22 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip curr_randomness = combination_randomness_powers + NUM_OOD_COMMIT_BASE * DIM - eq_memory_acc_point = eq_mle_extension( + eq_memory_and_acc_point = eq_mle_extension( folding_randomness_global + (N_VARS_BASE - log_memory) * DIM, - memory_acc_point, + memory_and_acc_point, log_memory, ) prefix_mem = multilinear_location_prefix(0, N_VARS_BASE - log_memory, folding_randomness_global) s = add_extension_ret( s, - mul_extension_ret(mul_extension_ret(curr_randomness, prefix_mem), eq_memory_acc_point), + mul_extension_ret(mul_extension_ret(curr_randomness, prefix_mem), eq_memory_and_acc_point), ) curr_randomness += DIM prefix_acc = multilinear_location_prefix(1, N_VARS_BASE - log_memory, folding_randomness_global) s = add_extension_ret( s, - mul_extension_ret(mul_extension_ret(curr_randomness, prefix_acc), eq_memory_acc_point), + mul_extension_ret(mul_extension_ret(curr_randomness, prefix_acc), eq_memory_and_acc_point), ) curr_randomness += DIM diff --git a/crates/sub_protocols/src/generic_logup.rs b/crates/sub_protocols/src/generic_logup.rs index 5f1ac54a..eb513e22 100644 --- a/crates/sub_protocols/src/generic_logup.rs +++ b/crates/sub_protocols/src/generic_logup.rs @@ -26,9 +26,11 @@ use utils::transpose_slice_to_basis_coefficients; #[derive(Debug, PartialEq, Hash, Clone)] pub struct GenericLogupStatements { - pub memory_acc_point: MultilinearPoint, + pub memory_and_acc_point: MultilinearPoint, pub value_memory: EF, - pub value_acc: EF, + pub value_memory_acc: EF, + pub bytecode_and_acc_point: MultilinearPoint, + pub value_bytecode_acc: EF, pub bus_numerators_values: BTreeMap, pub bus_denominators_values: BTreeMap, pub points: BTreeMap>, @@ -228,15 +230,16 @@ pub fn prove_generic_logup( assert_eq!(sum, EF::ZERO); // Memory: ... - let memory_acc_point = MultilinearPoint(from_end(&claim_point_gkr, log2_strict_usize(memory.len())).to_vec()); - let value_acc = memory_acc.evaluate(&memory_acc_point); - prover_state.add_extension_scalar(value_acc); + let memory_and_acc_point = MultilinearPoint(from_end(&claim_point_gkr, log2_strict_usize(memory.len())).to_vec()); + let value_memory_acc = memory_acc.evaluate(&memory_and_acc_point); + prover_state.add_extension_scalar(value_memory_acc); - let value_memory = memory.evaluate(&memory_acc_point); + let value_memory = memory.evaluate(&memory_and_acc_point); prover_state.add_extension_scalar(value_memory); - let bytecode_acc_point = MultilinearPoint(from_end(&claim_point_gkr, log2_strict_usize(bytecode.len())).to_vec()); - let value_bytecode_acc = bytecode_acc.evaluate(&bytecode_acc_point); + let bytecode_and_acc_point = + MultilinearPoint(from_end(&claim_point_gkr, log2_strict_usize(bytecode.len())).to_vec()); + let value_bytecode_acc = bytecode_acc.evaluate(&bytecode_and_acc_point); prover_state.add_extension_scalar(value_bytecode_acc); // evaluation on bytecode itself can be done directly by the verifier @@ -330,9 +333,11 @@ pub fn prove_generic_logup( } GenericLogupStatements { - memory_acc_point, + memory_and_acc_point, value_memory, - value_acc, + value_memory_acc, + bytecode_and_acc_point, + value_bytecode_acc, bus_numerators_values, bus_denominators_values, points, @@ -368,16 +373,16 @@ pub fn verify_generic_logup( let mut retrieved_denominators_value = EF::ZERO; // Memory ... - let memory_acc_point = MultilinearPoint(from_end(&point_gkr, log_memory).to_vec()); + let memory_and_acc_point = MultilinearPoint(from_end(&point_gkr, log_memory).to_vec()); let bits = to_big_endian_in_field::(0, total_n_vars - log_memory); let pref = MultilinearPoint(bits).eq_poly_outside(&MultilinearPoint(point_gkr[..total_n_vars - log_memory].to_vec())); - let value_acc = verifier_state.next_extension_scalar()?; - retrieved_numerators_value -= pref * value_acc; + let value_memory_acc = verifier_state.next_extension_scalar()?; + retrieved_numerators_value -= pref * value_memory_acc; let value_memory = verifier_state.next_extension_scalar()?; - let value_index = mle_of_01234567_etc(&memory_acc_point); + let value_index = mle_of_01234567_etc(&memory_and_acc_point); retrieved_denominators_value += pref * (c - finger_print( F::from_usize(MEMORY_TABLE_INDEX), @@ -389,11 +394,10 @@ pub fn verify_generic_logup( // Bytecode let log_bytecode = log2_strict_usize(bytecode.len()); let log_bytecode_padded = log2_strict_usize(bytecode.len()).max(tables_heights_sorted[0].1); - let bytecode_acc_point = MultilinearPoint(from_end(&point_gkr, log_bytecode).to_vec()); + let bytecode_and_acc_point = MultilinearPoint(from_end(&point_gkr, log_bytecode).to_vec()); let bits = to_big_endian_in_field::(offset >> log_bytecode, total_n_vars - log_bytecode); - let pref = MultilinearPoint(bits).eq_poly_outside(&MultilinearPoint( - point_gkr[..total_n_vars - log_bytecode].to_vec(), - )); + let pref = + MultilinearPoint(bits).eq_poly_outside(&MultilinearPoint(point_gkr[..total_n_vars - log_bytecode].to_vec())); let bits_padded = to_big_endian_in_field::(offset >> log_bytecode_padded, total_n_vars - log_bytecode_padded); let pref_padded = MultilinearPoint(bits_padded).eq_poly_outside(&MultilinearPoint( point_gkr[..total_n_vars - log_bytecode_padded].to_vec(), @@ -403,14 +407,14 @@ pub fn verify_generic_logup( retrieved_numerators_value -= pref * value_bytecode_acc; // Bytecode denominator - computed directly by verifier - let bytecode_index = mle_of_01234567_etc(&bytecode_acc_point); + let bytecode_index = mle_of_01234567_etc(&bytecode_and_acc_point); let bytecode_value: Vec = (0..N_INSTRUCTION_COLUMNS) .map(|col| { bytecode .iter() .map(|row| row[col]) .collect::>() - .evaluate(&bytecode_acc_point) + .evaluate(&bytecode_and_acc_point) }) .collect(); retrieved_denominators_value += pref @@ -538,9 +542,11 @@ pub fn verify_generic_logup( } Ok(GenericLogupStatements { - memory_acc_point, + memory_and_acc_point, value_memory, - value_acc, + value_memory_acc, + bytecode_and_acc_point, + value_bytecode_acc, bus_numerators_values, bus_denominators_values, points, diff --git a/crates/sub_protocols/src/packed_pcs.rs b/crates/sub_protocols/src/packed_pcs.rs index 547b4103..04ff64c5 100644 --- a/crates/sub_protocols/src/packed_pcs.rs +++ b/crates/sub_protocols/src/packed_pcs.rs @@ -18,7 +18,7 @@ pub fn packed_pcs_global_statements( packed_n_vars: usize, memory_n_vars: usize, bytecode_n_vars: usize, - memory_acc_statements: Vec>, + previous_statements: Vec>, tables_heights: &BTreeMap, committed_statements: &CommittedStatements, ) -> Vec> { @@ -26,7 +26,7 @@ pub fn packed_pcs_global_statements( let tables_heights_sorted = sort_tables_by_height(tables_heights); - let mut global_statements = memory_acc_statements; + let mut global_statements = previous_statements; let mut offset = 2 << memory_n_vars; // memory + memory_acc let max_table_n_vars = tables_heights_sorted[0].1; @@ -82,10 +82,10 @@ pub fn packed_pcs_commit( let mut packed_polynomial = F::zero_vec(1 << packed_n_vars); // TODO avoid cloning all witness data packed_polynomial[..memory.len()].copy_from_slice(memory); let mut offset = memory.len(); - packed_polynomial[offset..offset + memory_acc.len()].copy_from_slice(memory_acc); + packed_polynomial[offset..][..memory_acc.len()].copy_from_slice(memory_acc); offset += memory_acc.len(); - packed_polynomial[offset..offset + bytecode_acc.len()].copy_from_slice(bytecode_acc); + packed_polynomial[offset..][..bytecode_acc.len()].copy_from_slice(bytecode_acc); let largest_table_height = 1 << tables_heights_sorted[0].1; offset += largest_table_height.max(bytecode_acc.len()); // we may pad bytecode_acc to match largest table height @@ -93,14 +93,14 @@ pub fn packed_pcs_commit( let n_rows = 1 << *log_n_rows; for col_index_f in 0..table.n_columns_f_air() { let col = &traces[table].base[col_index_f]; - packed_polynomial[offset..offset + n_rows].copy_from_slice(&col[..n_rows]); + packed_polynomial[offset..][..n_rows].copy_from_slice(&col[..n_rows]); offset += n_rows; } for col_index_ef in 0..table.n_columns_ef_air() { let col = &traces[table].ext[col_index_ef]; let transposed = transpose_slice_to_basis_coefficients(col); for basis_col in transposed { - packed_polynomial[offset..offset + n_rows].copy_from_slice(&basis_col); + packed_polynomial[offset..][..n_rows].copy_from_slice(&basis_col); offset += n_rows; } } From 72112a3e310cb6216a46f843797c0027ec29ea1c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 17:02:12 +0400 Subject: [PATCH 05/47] w --- crates/lean_prover/src/test_zkvm.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/lean_prover/src/test_zkvm.rs b/crates/lean_prover/src/test_zkvm.rs index a2a02db5..f676f4b0 100644 --- a/crates/lean_prover/src/test_zkvm.rs +++ b/crates/lean_prover/src/test_zkvm.rs @@ -40,14 +40,6 @@ def main(): assert c == 100 return - -def aux(): - a: Mut = 10 - for i in unroll(0,1000): - a += 1 - assert a == 1010 - return - "#; const N: usize = 11; From b93a41d43b0666e100b073e42b998499e90185d6 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 18:12:33 +0400 Subject: [PATCH 06/47] wip adapt recursion --- crates/lean_compiler/snark_lib.py | 2 + crates/lean_vm/src/isa/hint.rs | 10 + crates/lean_vm/src/tables/table_trait.rs | 2 +- crates/rec_aggregation/fiat_shamir.py | 10 +- crates/rec_aggregation/fiat_shamir.snark.py | 10 +- crates/rec_aggregation/hashing.py | 28 +- crates/rec_aggregation/recursion.py | 96 +++---- crates/rec_aggregation/src/recursion.rs | 24 +- crates/rec_aggregation/utils.py | 37 ++- crates/rec_aggregation/utils.snark.py | 17 +- crates/rec_aggregation/whir.py | 271 +++++--------------- crates/sub_protocols/src/packed_pcs.rs | 4 +- misc/minimal_zkVM.tex | 2 +- 13 files changed, 175 insertions(+), 338 deletions(-) diff --git a/crates/lean_compiler/snark_lib.py b/crates/lean_compiler/snark_lib.py index dcda6560..131a3ea4 100644 --- a/crates/lean_compiler/snark_lib.py +++ b/crates/lean_compiler/snark_lib.py @@ -77,6 +77,8 @@ def dot_product(a, b, result, length, mode): def hint_decompose_bits(value, bits, n_bits, endian): _ = value, bits, n_bits, endian +def hint_less_than(a, b, result_ptr): + _ = a, b, result_ptr def log2_ceil(x: int) -> int: assert x > 0 diff --git a/crates/lean_vm/src/isa/hint.rs b/crates/lean_vm/src/isa/hint.rs index 7625aa11..db581d3d 100644 --- a/crates/lean_vm/src/isa/hint.rs +++ b/crates/lean_vm/src/isa/hint.rs @@ -76,6 +76,7 @@ pub enum CustomHint { /// The decomposition is unique, and always exists (except for x = -1) DecomposeBitsXMSS, DecomposeBits, + LessThan, } impl CustomHint { @@ -83,6 +84,7 @@ impl CustomHint { match self { Self::DecomposeBitsXMSS => "hint_decompose_bits_xmss", Self::DecomposeBits => "hint_decompose_bits", + Self::LessThan => "hint_less_than", } } @@ -90,6 +92,7 @@ impl CustomHint { match self { Self::DecomposeBitsXMSS => 3..usize::MAX, Self::DecomposeBits => 4..5, + Self::LessThan => 3..4, } } @@ -132,6 +135,13 @@ impl CustomHint { .set_slice(memory_index, &to_little_endian_in_field::(to_decompose, num_bits))? } } + Self::LessThan => { + let a = args[0].read_value(ctx.memory, ctx.fp)?; + let b = args[1].read_value(ctx.memory, ctx.fp)?; + let result_addr = args[2].read_value(ctx.memory, ctx.fp)?.to_usize(); + let result = if a.to_usize() < b.to_usize() { F::ONE } else { F::ZERO }; + ctx.memory.set(result_addr, result)?; + } } Ok(()) } diff --git a/crates/lean_vm/src/tables/table_trait.rs b/crates/lean_vm/src/tables/table_trait.rs index e123fc94..0198f9c3 100644 --- a/crates/lean_vm/src/tables/table_trait.rs +++ b/crates/lean_vm/src/tables/table_trait.rs @@ -144,7 +144,7 @@ pub trait TableT: Air { false } - fn n_commited_columns(&self) -> usize { + fn n_committed_columns(&self) -> usize { self.n_columns_ef_air() * DIMENSION + self.n_columns_f_air() } diff --git a/crates/rec_aggregation/fiat_shamir.py b/crates/rec_aggregation/fiat_shamir.py index fe2614b8..c8327c49 100644 --- a/crates/rec_aggregation/fiat_shamir.py +++ b/crates/rec_aggregation/fiat_shamir.py @@ -105,12 +105,8 @@ def sample_bits_const(fs: Mut, n_samples: Const, K): def sample_bits_dynamic(fs_state, n_samples, K): new_fs_state: Imu sampled_bits: Imu - for r in unroll(0, N_ROUNDS_BASE + 1): - if n_samples == NUM_QUERIES_BASE[r]: - new_fs_state, sampled_bits = sample_bits_const(fs_state, NUM_QUERIES_BASE[r], K) - return new_fs_state, sampled_bits - for r in unroll(0, N_ROUNDS_EXT + 1): - if n_samples == NUM_QUERIES_EXT[r]: - new_fs_state, sampled_bits = sample_bits_const(fs_state, NUM_QUERIES_EXT[r], K) + for r in unroll(0, WHIR_N_ROUNDS + 1): + if n_samples == WHIR_NUM_QUERIES[r]: + new_fs_state, sampled_bits = sample_bits_const(fs_state, WHIR_NUM_QUERIES[r], K) return new_fs_state, sampled_bits assert False, "sample_bits_dynamic called with unsupported n_samples" diff --git a/crates/rec_aggregation/fiat_shamir.snark.py b/crates/rec_aggregation/fiat_shamir.snark.py index 7973b05b..e2f189b7 100644 --- a/crates/rec_aggregation/fiat_shamir.snark.py +++ b/crates/rec_aggregation/fiat_shamir.snark.py @@ -104,12 +104,8 @@ def sample_bits_const(fs: Mut, n_samples: Const, K): def sample_bits_dynamic(fs_state, n_samples, K): new_fs_state: Imu sampled_bits: Imu - for r in unroll(0, N_ROUNDS_BASE + 1): - if n_samples == NUM_QUERIES_BASE[r]: - new_fs_state, sampled_bits = sample_bits_const(fs_state, NUM_QUERIES_BASE[r], K) - return new_fs_state, sampled_bits - for r in unroll(0, N_ROUNDS_EXT + 1): - if n_samples == NUM_QUERIES_EXT[r]: - new_fs_state, sampled_bits = sample_bits_const(fs_state, NUM_QUERIES_EXT[r], K) + for r in unroll(0, WHIR_N_ROUNDS + 1): + if n_samples == WHIR_NUM_QUERIES[r]: + new_fs_state, sampled_bits = sample_bits_const(fs_state, WHIR_NUM_QUERIES[r], K) return new_fs_state, sampled_bits assert False, "sample_bits_dynamic called with unsupported n_samples" diff --git a/crates/rec_aggregation/hashing.py b/crates/rec_aggregation/hashing.py index bf67a9ef..8e37ef89 100644 --- a/crates/rec_aggregation/hashing.py +++ b/crates/rec_aggregation/hashing.py @@ -6,12 +6,9 @@ DIM = 5 # extension degree VECTOR_LEN = 8 -MERKLE_HEIGHTS_BASE = MERKLE_HEIGHTS_BASE_PLACEHOLDER -MERKLE_HEIGHTS_EXT = MERKLE_HEIGHTS_EXT_PLACEHOLDER -NUM_QUERIES_BASE = NUM_QUERIES_BASE_PLACEHOLDER -NUM_QUERIES_EXT = NUM_QUERIES_EXT_PLACEHOLDER -N_ROUNDS_BASE = len(NUM_QUERIES_BASE) - 1 -N_ROUNDS_EXT = len(NUM_QUERIES_EXT) - 1 +WHIR_MERKLE_HEIGHTS = WHIR_MERKLE_HEIGHTS_PLACEHOLDER +WHIR_NUM_QUERIES = WHIR_NUM_QUERIES_PLACEHOLDER +WHIR_N_ROUNDS = len(WHIR_NUM_QUERIES) - 1 def batch_hash_slice(num_queries, all_data_to_hash, all_resulting_hashes, len): @@ -47,26 +44,15 @@ def slice_hash(seed, data, len: Const): def merkle_verif_batch(n_paths, merkle_paths, leaves_digests, leave_positions, root, height, num_queries): - for i in unroll(0, N_ROUNDS_BASE + 1): - if height + num_queries * 1000 == MERKLE_HEIGHTS_BASE[i] + NUM_QUERIES_BASE[i] * 1000: + for i in unroll(0, WHIR_N_ROUNDS + 1): + if height + num_queries * 1000 == WHIR_MERKLE_HEIGHTS[i] + WHIR_NUM_QUERIES[i] * 1000: merkle_verif_batch_const( - NUM_QUERIES_BASE[i], + WHIR_NUM_QUERIES[i], merkle_paths, leaves_digests, leave_positions, root, - MERKLE_HEIGHTS_BASE[i], - ) - return - for i in unroll(0, N_ROUNDS_EXT + 1): - if height + num_queries * 1000 == MERKLE_HEIGHTS_EXT[i] + NUM_QUERIES_EXT[i] * 1000: - merkle_verif_batch_const( - NUM_QUERIES_EXT[i], - merkle_paths, - leaves_digests, - leave_positions, - root, - MERKLE_HEIGHTS_EXT[i], + WHIR_MERKLE_HEIGHTS[i], ) return print(12345555) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index 92ed54eb..89be13c8 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -6,7 +6,7 @@ MAX_LOG_N_ROWS_PER_TABLE = MAX_LOG_N_ROWS_PER_TABLE_PLACEHOLDER MIN_LOG_MEMORY_SIZE = MIN_LOG_MEMORY_SIZE_PLACEHOLDER MAX_LOG_MEMORY_SIZE = MAX_LOG_MEMORY_SIZE_PLACEHOLDER -N_VARS_FIRST_GKR = N_VARS_FIRST_GKR_PLACEHOLDER +N_VARS_LOGUP_GKR = N_VARS_LOGUP_GKR_PLACEHOLDER MAX_BUS_WIDTH = MAX_BUS_WIDTH_PLACEHOLDER MAX_NUM_AIR_CONSTRAINTS = MAX_NUM_AIR_CONSTRAINTS_PLACEHOLDER MEMORY_TABLE_INDEX = MEMORY_TABLE_INDEX_PLACEHOLDER @@ -20,7 +20,7 @@ NUM_COLS_F_AIR = NUM_COLS_F_AIR_PLACEHOLDER NUM_COLS_EF_AIR = NUM_COLS_EF_AIR_PLACEHOLDER -NUM_COLS_F_COMMITED = NUM_COLS_F_COMMITED_PLACEHOLDER +NUM_COLS_F_COMMITTED = NUM_COLS_F_COMMITTED_PLACEHOLDER EXECUTION_TABLE_INDEX = EXECUTION_TABLE_INDEX_PLACEHOLDER AIR_DEGREES = AIR_DEGREES_PLACEHOLDER # [_; N_TABLES] @@ -34,7 +34,7 @@ GUEST_BYTECODE_LEN = GUEST_BYTECODE_LEN_PLACEHOLDER COL_PC = COL_PC_PLACEHOLDER -TOTAL_WHIR_STATEMENTS_BASE = TOTAL_WHIR_STATEMENTS_BASE_PLACEHOLDER +TOTAL_WHIR_STATEMENTS = TOTAL_WHIR_STATEMENTS_PLACEHOLDER STARTING_PC = STARTING_PC_PLACEHOLDER ENDING_PC = ENDING_PC_PLACEHOLDER NONRESERVED_PROGRAM_INPUT_START = NONRESERVED_PROGRAM_INPUT_START_PLACEHOLDER @@ -73,9 +73,9 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip assert n_vars_for_table <= MAX_LOG_N_ROWS_PER_TABLE[i] assert MIN_LOG_MEMORY_SIZE <= log_memory assert log_memory <= MAX_LOG_MEMORY_SIZE + assert log_memory <= GUEST_BYTECODE_LEN - # parse 1st whir commitment - fs, whir_base_root, whir_base_ood_points, whir_base_ood_evals = parse_whir_commitment_const(fs, NUM_OOD_COMMIT_BASE) + fs, whir_base_root, whir_base_ood_points, whir_base_ood_evals = parse_whir_commitment_const(fs, WHIR_NUM_OOD_COMMIT) logup_c = fs_sample_ef(fs) fs = duplexing(fs) @@ -87,23 +87,30 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip logup_alphas_eq_poly = poly_eq_extension(logup_alphas, log2_ceil(MAX_BUS_WIDTH)) # GENRIC LOGUP - fs, quotient_gkr, point_gkr, numerators_value, denominators_value = verify_gkr_quotient(fs, N_VARS_FIRST_GKR) + fs, quotient_gkr, point_gkr, numerators_value, denominators_value = verify_gkr_quotient(fs, N_VARS_LOGUP_GKR) set_to_5_zeros(quotient_gkr) - memory_and_acc_prefix = multilinear_location_prefix(0, N_VARS_FIRST_GKR - log_memory, point_gkr) + memory_and_acc_prefix = multilinear_location_prefix(0, N_VARS_LOGUP_GKR - log_memory, point_gkr) fs, value_acc = fs_receive_ef(fs, 1) fs, value_memory = fs_receive_ef(fs, 1) retrieved_numerators_value: Mut = opposite_extension_ret(mul_extension_ret(memory_and_acc_prefix, value_acc)) - value_index = mle_of_01234567_etc(point_gkr + (N_VARS_FIRST_GKR - log_memory) * DIM, log_memory) + value_index = mle_of_01234567_etc(point_gkr + (N_VARS_LOGUP_GKR - log_memory) * DIM, log_memory) fingerprint_memory = fingerprint_2(MEMORY_TABLE_INDEX, value_index, value_memory, logup_alphas_eq_poly) retrieved_denominators_value: Mut = mul_extension_ret( memory_and_acc_prefix, sub_extension_ret(logup_c, fingerprint_memory) ) offset: Mut = powers_of_two(log_memory) + + log_bytecode = log2_ceil(GUEST_BYTECODE_LEN) + log_n_cycles = table_dims[EXECUTION_TABLE_INDEX - 1] + log_bytecode_padded = maximum(log_bytecode, log_n_cycles) + bytecode_multilinear_location_prefix = multilinear_location_prefix(offset / log_bytecode, N_VARS_LOGUP_GKR - log_bytecode, point_gkr) + bytecode_padded_multilinear_location_prefix = multilinear_location_prefix(offset / log_bytecode_padded, N_VARS_LOGUP_GKR - log_bytecode_padded, point_gkr) + bus_numerators_values = DynArray([]) bus_denominators_values = DynArray([]) pcs_points = DynArray([]) # [[_; N]; N_TABLES] @@ -117,10 +124,10 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip log_n_rows = table_dims[table_index] n_rows = powers_of_two(log_n_rows) - inner_point = point_gkr + (N_VARS_FIRST_GKR - log_n_rows) * DIM + inner_point = point_gkr + (N_VARS_LOGUP_GKR - log_n_rows) * DIM pcs_points[table_index].push(inner_point) - prefix = multilinear_location_prefix(offset / n_rows, N_VARS_FIRST_GKR - log_n_rows, point_gkr) + prefix = multilinear_location_prefix(offset / n_rows, N_VARS_LOGUP_GKR - log_n_rows, point_gkr) fs, eval_on_selector = fs_receive_ef(fs, 1) retrieved_numerators_value = add_extension_ret( @@ -157,7 +164,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip pcs_values[table_index][0][col_index].push(value_eval) pref = multilinear_location_prefix( - offset / n_rows, N_VARS_FIRST_GKR - log_n_rows, point_gkr + offset / n_rows, N_VARS_LOGUP_GKR - log_n_rows, point_gkr ) # TODO there is some duplication here retrieved_numerators_value = add_extension_ret(retrieved_numerators_value, pref) fingerp = fingerprint_2( @@ -185,7 +192,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip for i in unroll(0, DIM): fs, value_eval = fs_receive_ef(fs, 1) pref = multilinear_location_prefix( - offset / n_rows, N_VARS_FIRST_GKR - log_n_rows, point_gkr + offset / n_rows, N_VARS_LOGUP_GKR - log_n_rows, point_gkr ) # TODO there is some duplication here retrieved_numerators_value = add_extension_ret(retrieved_numerators_value, pref) fingerp = fingerprint_2( @@ -200,7 +207,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip ) global_index = ( - NUM_COLS_F_COMMITED[table_index] + LOOKUPS_EF_VALUES[table_index][lookup_ef_index] * DIM + i + NUM_COLS_F_COMMITTED[table_index] + LOOKUPS_EF_VALUES[table_index][lookup_ef_index] * DIM + i ) debug_assert(len(pcs_values[table_index][0][global_index]) == 0) pcs_values[table_index][0][global_index].push(value_eval) @@ -209,13 +216,13 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip retrieved_denominators_value = add_extension_ret( retrieved_denominators_value, - mle_of_zeros_then_ones(point_gkr, offset, N_VARS_FIRST_GKR), + mle_of_zeros_then_ones(point_gkr, offset, N_VARS_LOGUP_GKR), ) copy_5(retrieved_numerators_value, numerators_value) copy_5(retrieved_denominators_value, denominators_value) - memory_and_acc_point = point_gkr + (N_VARS_FIRST_GKR - log_memory) * DIM + memory_and_acc_point = point_gkr + (N_VARS_LOGUP_GKR - log_memory) * DIM # END OF GENERIC LOGUP @@ -374,7 +381,6 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip # VERIFY LOGUP* log_table_len = log2_ceil(GUEST_BYTECODE_LEN) - log_n_cycles = table_dims[EXECUTION_TABLE_INDEX - 1] fs, ls_sumcheck_point, ls_sumcheck_value = sumcheck_verify(fs, log_table_len, bytecode_lookup_claim, 2) fs, table_eval = fs_receive_ef(fs, 1) fs, pushforward_eval = fs_receive_ef(fs, 1) @@ -438,10 +444,10 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip # WHIR BASE combination_randomness_gen: Mut = fs_sample_ef(fs) combination_randomness_powers: Mut = powers_const( - combination_randomness_gen, NUM_OOD_COMMIT_BASE + TOTAL_WHIR_STATEMENTS_BASE + combination_randomness_gen, WHIR_NUM_OOD_COMMIT + TOTAL_WHIR_STATEMENTS ) - whir_sum: Mut = dot_product_ret(whir_base_ood_evals, combination_randomness_powers, NUM_OOD_COMMIT_BASE, EE) - curr_randomness: Mut = combination_randomness_powers + NUM_OOD_COMMIT_BASE * DIM + whir_sum: Mut = dot_product_ret(whir_base_ood_evals, combination_randomness_powers, WHIR_NUM_OOD_COMMIT, EE) + curr_randomness: Mut = combination_randomness_powers + WHIR_NUM_OOD_COMMIT * DIM whir_sum = add_extension_ret(mul_extension_ret(value_memory, curr_randomness), whir_sum) curr_randomness += DIM @@ -471,7 +477,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip s: Mut final_value: Mut end_sum: Mut - fs, folding_randomness_global, s, final_value, end_sum = whir_open_base( + fs, folding_randomness_global, s, final_value, end_sum = whir_open( fs, whir_base_root, whir_base_ood_points, @@ -479,21 +485,21 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip whir_sum, ) - curr_randomness = combination_randomness_powers + NUM_OOD_COMMIT_BASE * DIM + curr_randomness = combination_randomness_powers + WHIR_NUM_OOD_COMMIT * DIM eq_memory_and_acc_point = eq_mle_extension( - folding_randomness_global + (N_VARS_BASE - log_memory) * DIM, + folding_randomness_global + (WHIR_N_VARS - log_memory) * DIM, memory_and_acc_point, log_memory, ) - prefix_mem = multilinear_location_prefix(0, N_VARS_BASE - log_memory, folding_randomness_global) + prefix_mem = multilinear_location_prefix(0, WHIR_N_VARS - log_memory, folding_randomness_global) s = add_extension_ret( s, mul_extension_ret(mul_extension_ret(curr_randomness, prefix_mem), eq_memory_and_acc_point), ) curr_randomness += DIM - prefix_acc = multilinear_location_prefix(1, N_VARS_BASE - log_memory, folding_randomness_global) + prefix_acc = multilinear_location_prefix(1, WHIR_N_VARS - log_memory, folding_randomness_global) s = add_extension_ret( s, mul_extension_ret(mul_extension_ret(curr_randomness, prefix_acc), eq_memory_and_acc_point), @@ -501,12 +507,12 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip curr_randomness += DIM eq_pub_mem = eq_mle_extension( - folding_randomness_global + (N_VARS_BASE - outer_public_memory_log_size) * DIM, + folding_randomness_global + (WHIR_N_VARS - outer_public_memory_log_size) * DIM, public_memory_random_point, outer_public_memory_log_size, ) prefix_pub_mem = multilinear_location_prefix( - 0, N_VARS_BASE - outer_public_memory_log_size, folding_randomness_global + 0, WHIR_N_VARS - outer_public_memory_log_size, folding_randomness_global ) s = add_extension_ret( s, @@ -518,7 +524,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip prefix_pc_start = multilinear_location_prefix( offset + COL_PC * powers_of_two(log_n_cycles), - N_VARS_BASE, + WHIR_N_VARS, folding_randomness_global, ) s = add_extension_ret(s, mul_extension_ret(curr_randomness, prefix_pc_start)) @@ -526,7 +532,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip prefix_pc_end = multilinear_location_prefix( offset + (COL_PC + 1) * powers_of_two(log_n_cycles) - 1, - N_VARS_BASE, + WHIR_N_VARS, folding_randomness_global, ) s = add_extension_ret(s, mul_extension_ret(curr_randomness, prefix_pc_end)) @@ -540,14 +546,14 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip point = pcs_points[table_index][i] eq_factor = eq_mle_extension( point, - folding_randomness_global + (N_VARS_BASE - log_n_rows) * DIM, + folding_randomness_global + (WHIR_N_VARS - log_n_rows) * DIM, log_n_rows, ) for j in unroll(0, total_num_cols): if len(pcs_values[table_index][i][j]) == 1: prefix = multilinear_location_prefix( offset / n_rows + j, - N_VARS_BASE - log_n_rows, + WHIR_N_VARS - log_n_rows, folding_randomness_global, ) s = add_extension_ret( @@ -555,37 +561,15 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip mul_extension_ret(mul_extension_ret(curr_randomness, prefix), eq_factor), ) curr_randomness += DIM - num_commited_cols: Imu + num_committed_cols: Imu if table_index == EXECUTION_TABLE_INDEX - 1: - num_commited_cols = N_COMMITTED_EXEC_COLUMNS + num_committed_cols = N_COMMITTED_EXEC_COLUMNS else: - num_commited_cols = total_num_cols - offset += n_rows * num_commited_cols + num_committed_cols = total_num_cols + offset += n_rows * num_committed_cols copy_5(mul_extension_ret(s, final_value), end_sum) - # WHIR EXT (Pushforward) - combination_randomness_gen = fs_sample_ef(fs) - combination_randomness_powers = powers_const(combination_randomness_gen, NUM_OOD_COMMIT_EXT + 2) - whir_sum = dot_product_ret(whir_ext_ood_evals, combination_randomness_powers, NUM_OOD_COMMIT_EXT, EE) - whir_sum = add_extension_ret( - whir_sum, - mul_extension_ret( - combination_randomness_powers + NUM_OOD_COMMIT_EXT * DIM, - ls_on_pushforward_eval_1, - ), - ) - whir_sum = add_extension_ret( - whir_sum, - mul_extension_ret( - combination_randomness_powers + (NUM_OOD_COMMIT_EXT + 1) * DIM, - ls_on_pushforward_eval_2, - ), - ) - fs, folding_randomness_global, s, final_value, end_sum = whir_open_ext( - fs, whir_ext_root, whir_ext_ood_points, combination_randomness_powers, whir_sum - ) - # Last TODO = Opening on the guest bytecode, but there are multiple ways to handle this return diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index 537eb9ad..bf66cb5f 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -106,7 +106,7 @@ def main(): // VM recursion parameters (different from WHIR) replacements.insert( - "N_VARS_FIRST_GKR_PLACEHOLDER".to_string(), + "N_VARS_LOGUP_GKR_PLACEHOLDER".to_string(), verif_details.first_quotient_gkr_n_vars.to_string(), ); replacements.insert("N_TABLES_PLACEHOLDER".to_string(), N_TABLES.to_string()); @@ -241,7 +241,7 @@ def main(): format!("[{}]", num_cols_ef_air.join(", ")), ); replacements.insert( - "NUM_COLS_F_COMMITED_PLACEHOLDER".to_string(), + "NUM_COLS_F_COMMITTED_PLACEHOLDER".to_string(), format!("[{}]", num_cols_f_committed.join(", ")), ); replacements.insert( @@ -369,34 +369,34 @@ pub(crate) fn whir_recursion_placeholder_replacements(whir_config: &WhirConfig(); log2_ceil_usize(total_len) } diff --git a/misc/minimal_zkVM.tex b/misc/minimal_zkVM.tex index ac80d6d9..305a7b08 100644 --- a/misc/minimal_zkVM.tex +++ b/misc/minimal_zkVM.tex @@ -535,7 +535,7 @@ \section{Annex: simple packing of multilinear polynomials} \vspace{3mm} % One of the advantage of multilinear polynomials versus univariate polynomials is the ability to efficienty commit to multiple polynomials at once. -In order to commit to multiple univariate polynomials with FRI, each polynomial must be FFT-ed + Merkle-commited. +In order to commit to multiple univariate polynomials with FRI, each polynomial must be FFT-ed + Merkle-committed. Even if it's possible to have some batching at the Merkle tree level (see 'MMCS' in \href{https://github.com/Plonky3/Plonky3}{Plonky3}), the proof size for multiple, complex AIR tables quickly reach the megabyte scale. With a multilinear PCS (such as WHIR), we can "concatenate" multiple multilinear polynomials into a single one, and commit to it once (offering significant proof size savings). From dfaf8f240aec5d72d2e48777e8f1768e4b38d6a6 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 19:26:25 +0400 Subject: [PATCH 07/47] fix challenges --- crates/lean_vm/src/tables/utils.rs | 12 ++++++------ crates/utils/src/multilinear.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/lean_vm/src/tables/utils.rs b/crates/lean_vm/src/tables/utils.rs index e9e3e5f9..4e3191e1 100644 --- a/crates/lean_vm/src/tables/utils.rs +++ b/crates/lean_vm/src/tables/utils.rs @@ -11,12 +11,12 @@ pub(crate) fn eval_virtual_bus_column> let (logup_alphas_eq_poly, bus_beta) = extra_data.transmute_bus_data::(); assert!(data.len() < logup_alphas_eq_poly.len()); - (logup_alphas_eq_poly[1..] - .iter() - .zip(data) - .map(|(c, d)| c.clone() * d.clone()) - .sum::() - + bus_index) + (logup_alphas_eq_poly[0].clone() * bus_index + + logup_alphas_eq_poly[1..] + .iter() + .zip(data) + .map(|(c, d)| c.clone() * d.clone()) + .sum::()) * bus_beta.clone() + flag } diff --git a/crates/utils/src/multilinear.rs b/crates/utils/src/multilinear.rs index 50c4932f..369cdc38 100644 --- a/crates/utils/src/multilinear.rs +++ b/crates/utils/src/multilinear.rs @@ -129,7 +129,7 @@ pub fn finger_print>, EF: ExtensionField alphas_eq_poly: &[EF], ) -> EF { assert!(alphas_eq_poly.len() > data.len()); - dot_product::(alphas_eq_poly[1..].iter().copied(), data.iter().copied()) + table + alphas_eq_poly[0] * table + dot_product::(alphas_eq_poly[1..].iter().copied(), data.iter().copied()) } #[cfg(test)] From 0301054be069814abb434304935792ddf66ca78e Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 20:13:20 +0400 Subject: [PATCH 08/47] again --- crates/lean_vm/src/tables/utils.rs | 12 +++---- crates/sub_protocols/src/generic_logup.rs | 43 +++++++---------------- crates/utils/src/multilinear.rs | 13 +++---- 3 files changed, 25 insertions(+), 43 deletions(-) diff --git a/crates/lean_vm/src/tables/utils.rs b/crates/lean_vm/src/tables/utils.rs index 4e3191e1..4d8f3412 100644 --- a/crates/lean_vm/src/tables/utils.rs +++ b/crates/lean_vm/src/tables/utils.rs @@ -11,12 +11,12 @@ pub(crate) fn eval_virtual_bus_column> let (logup_alphas_eq_poly, bus_beta) = extra_data.transmute_bus_data::(); assert!(data.len() < logup_alphas_eq_poly.len()); - (logup_alphas_eq_poly[0].clone() * bus_index - + logup_alphas_eq_poly[1..] - .iter() - .zip(data) - .map(|(c, d)| c.clone() * d.clone()) - .sum::()) + (logup_alphas_eq_poly + .iter() + .zip(data) + .map(|(c, d)| c.clone() * d.clone()) + .sum::() + + logup_alphas_eq_poly.last().unwrap().clone() * bus_index) * bus_beta.clone() + flag } diff --git a/crates/sub_protocols/src/generic_logup.rs b/crates/sub_protocols/src/generic_logup.rs index eb513e22..d3e59523 100644 --- a/crates/sub_protocols/src/generic_logup.rs +++ b/crates/sub_protocols/src/generic_logup.rs @@ -1,28 +1,8 @@ use crate::{prove_gkr_quotient, verify_gkr_quotient}; -use lean_vm::BusDirection; -use lean_vm::BusTable; -use lean_vm::COL_PC; -use lean_vm::ColIndex; -use lean_vm::DIMENSION; -use lean_vm::EF; -use lean_vm::F; -use lean_vm::N_INSTRUCTION_COLUMNS; -use lean_vm::N_RUNTIME_COLUMNS; -use lean_vm::Table; -use lean_vm::TableT; -use lean_vm::TableTrace; -use lean_vm::sort_tables_by_height; +use lean_vm::*; use multilinear_toolkit::prelude::*; use std::collections::BTreeMap; -use utils::BYTECODE_TABLE_INDEX; -use utils::MEMORY_TABLE_INDEX; -use utils::VarCount; -use utils::VecOrSlice; -use utils::finger_print; -use utils::from_end; -use utils::mle_of_01234567_etc; -use utils::to_big_endian_in_field; -use utils::transpose_slice_to_basis_coefficients; +use utils::*; #[derive(Debug, PartialEq, Hash, Clone)] pub struct GenericLogupStatements { @@ -80,7 +60,7 @@ pub fn prove_generic_logup( .for_each(|(denom, (i, &mem_value))| { *denom = c - finger_print( F::from_usize(MEMORY_TABLE_INDEX), - &[F::from_usize(i), mem_value], + &[mem_value, F::from_usize(i)], alphas_eq_poly, ) }); @@ -98,7 +78,7 @@ pub fn prove_generic_logup( .for_each(|(denom, (i, &instr))| { *denom = c - finger_print( F::from_usize(BYTECODE_TABLE_INDEX), - &[vec![F::from_usize(i)], instr.to_vec()].concat(), + &[instr.to_vec(), vec![F::from_usize(i)]].concat(), alphas_eq_poly, ) }); @@ -129,10 +109,11 @@ pub fn prove_generic_logup( .par_iter_mut() .enumerate() .for_each(|(i, denom)| { - let mut data = vec![pc_column[i]]; + let mut data = vec![]; for col in &bytecode_columns { data.push(col[i]); } + data.push(pc_column[i]); *denom = c - finger_print(F::from_usize(BYTECODE_TABLE_INDEX), &data, alphas_eq_poly) }); offset += 1 << log_n_rows; @@ -204,7 +185,7 @@ pub fn prove_generic_logup( let index = col_index[j] + i_field; let mem_value = col_values[i].as_slice()[j]; *denom = - c - finger_print(F::from_usize(MEMORY_TABLE_INDEX), &[index, mem_value], alphas_eq_poly) + c - finger_print(F::from_usize(MEMORY_TABLE_INDEX), &[mem_value, index], alphas_eq_poly) }); }); offset += col_values.len() << log_n_rows; @@ -386,7 +367,7 @@ pub fn verify_generic_logup( retrieved_denominators_value += pref * (c - finger_print( F::from_usize(MEMORY_TABLE_INDEX), - &[value_index, value_memory], + &[value_memory, value_index], alphas_eq_poly, )); let mut offset = 1 << log_memory; @@ -420,7 +401,7 @@ pub fn verify_generic_logup( retrieved_denominators_value += pref * (c - finger_print( F::from_usize(BYTECODE_TABLE_INDEX), - &[vec![bytecode_index], bytecode_value].concat(), + &[bytecode_value, vec![bytecode_index]].concat(), alphas_eq_poly, )); // Padding for bytecode @@ -461,7 +442,7 @@ pub fn verify_generic_logup( retrieved_denominators_value += pref * (c - finger_print( F::from_usize(BYTECODE_TABLE_INDEX), - &[vec![eval_on_pc], instr_evals].concat(), + &[instr_evals, vec![eval_on_pc]].concat(), alphas_eq_poly, )); @@ -500,7 +481,7 @@ pub fn verify_generic_logup( retrieved_denominators_value += pref * (c - finger_print( F::from_usize(MEMORY_TABLE_INDEX), - &[index_eval + F::from_usize(i), value_eval], + &[value_eval, index_eval + F::from_usize(i)], alphas_eq_poly, )); offset += 1 << log_n_rows; @@ -521,7 +502,7 @@ pub fn verify_generic_logup( retrieved_denominators_value += pref * (c - finger_print( F::from_usize(MEMORY_TABLE_INDEX), - &[index_eval + F::from_usize(i), value_eval], + &[value_eval, index_eval + F::from_usize(i)], alphas_eq_poly, )); let global_index = table.n_columns_f_air() + lookup_ef.values * DIMENSION + i; diff --git a/crates/utils/src/multilinear.rs b/crates/utils/src/multilinear.rs index 369cdc38..2a945beb 100644 --- a/crates/utils/src/multilinear.rs +++ b/crates/utils/src/multilinear.rs @@ -116,12 +116,12 @@ pub fn mle_of_01234567_etc(point: &[F]) -> F { } } -/// table = 0 is reversed for memory lookup -pub const MEMORY_TABLE_INDEX: usize = 0; -/// table = 1 is reversed for bytecode lookup -pub const BYTECODE_TABLE_INDEX: usize = 1; +/// table = 1 is reversed for memory lookup +pub const MEMORY_TABLE_INDEX: usize = 1; +/// table = 2 is reversed for bytecode lookup +pub const BYTECODE_TABLE_INDEX: usize = 2; /// All the remaining tables come after memory and bytecode (special cases) -pub const FIRST_NORMAL_TABLE_INDEX: usize = 2; +pub const FIRST_NORMAL_TABLE_INDEX: usize = 3; pub fn finger_print>, EF: ExtensionField + ExtensionField>( table: F, @@ -129,7 +129,8 @@ pub fn finger_print>, EF: ExtensionField alphas_eq_poly: &[EF], ) -> EF { assert!(alphas_eq_poly.len() > data.len()); - alphas_eq_poly[0] * table + dot_product::(alphas_eq_poly[1..].iter().copied(), data.iter().copied()) + dot_product::(alphas_eq_poly.iter().copied(), data.iter().copied()) + + *alphas_eq_poly.last().unwrap() * table } #[cfg(test)] From 8e02f16db6252559a0f6bb0a397966d73619b718 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 20:18:39 +0400 Subject: [PATCH 09/47] interpret the bytecode as a trivial multilinear polynomial --- crates/lean_compiler/src/c_compile_final.rs | 14 ++++++-- crates/lean_prover/src/prove_execution.rs | 2 +- crates/lean_prover/src/trace_gen.rs | 2 +- crates/lean_prover/src/verify_execution.rs | 3 +- crates/lean_vm/src/isa/bytecode.rs | 3 +- crates/sub_protocols/src/generic_logup.rs | 39 ++++++++++----------- 6 files changed, 35 insertions(+), 28 deletions(-) diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index 5ee0a6b4..85bc6344 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -144,15 +144,23 @@ pub fn compile_to_low_level_bytecode( &mut hints, ); } - let mut encoded_instructions = instructions + let mut instructions_encoded = instructions .par_iter() .map(|instr| field_representation(instr)) .collect::>(); - encoded_instructions.resize(instructions.len().next_power_of_two(), [F::ZERO; N_INSTRUCTION_COLUMNS]); + instructions_encoded.resize(instructions.len().next_power_of_two(), [F::ZERO; N_INSTRUCTION_COLUMNS]); + + let mut instructions_multilinear = vec![]; + for instr in &instructions_encoded { + instructions_multilinear.extend_from_slice(instr); + let padding = N_INSTRUCTION_COLUMNS.next_power_of_two() - N_INSTRUCTION_COLUMNS; + instructions_multilinear.extend(vec![F::ZERO; padding]); + } Ok(Bytecode { instructions, - encoded_instructions, + instructions_encoded, + instructions_multilinear, hints, starting_frame_memory, function_locations, diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index d70ef187..43105beb 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -112,7 +112,7 @@ pub fn prove_execution( &logup_alphas_eq_poly, &memory, &memory_acc, - &bytecode.encoded_instructions, + &bytecode.instructions_encoded, &bytecode_acc, &traces, ); diff --git a/crates/lean_prover/src/trace_gen.rs b/crates/lean_prover/src/trace_gen.rs index 4bc69239..fdbe8700 100644 --- a/crates/lean_prover/src/trace_gen.rs +++ b/crates/lean_prover/src/trace_gen.rs @@ -29,7 +29,7 @@ pub fn get_execution_trace(bytecode: &Bytecode, execution_result: ExecutionResul .zip(execution_result.fps.par_iter()) .for_each(|((trace_row, &pc), &fp)| { let instruction = &bytecode.instructions[pc]; - let field_repr = bytecode.encoded_instructions[pc]; + let field_repr = bytecode.instructions_encoded[pc]; let mut addr_a = F::ZERO; if field_repr[instr_idx(COL_FLAG_A)].is_zero() { diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index 335abf7e..e822bf9b 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -73,9 +73,10 @@ pub fn verify_execution( let logup_statements = verify_generic_logup( &mut verifier_state, logup_c, + &logup_alphas, &logup_alphas_eq_poly, log_memory, - &bytecode.encoded_instructions, + &bytecode.instructions_multilinear, &table_n_vars, )?; let mut committed_statements: CommittedStatements = Default::default(); diff --git a/crates/lean_vm/src/isa/bytecode.rs b/crates/lean_vm/src/isa/bytecode.rs index 49de9cc6..c64d9506 100644 --- a/crates/lean_vm/src/isa/bytecode.rs +++ b/crates/lean_vm/src/isa/bytecode.rs @@ -12,7 +12,8 @@ use std::fmt::{Display, Formatter}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Bytecode { pub instructions: Vec, - pub encoded_instructions: Vec<[F; N_INSTRUCTION_COLUMNS]>, // padded to power of two length (with zero rows) + pub instructions_encoded: Vec<[F; N_INSTRUCTION_COLUMNS]>, // padded to power of two length (with zero rows) + pub instructions_multilinear: Vec, pub hints: BTreeMap>, // pc -> hints pub starting_frame_memory: usize, // debug diff --git a/crates/sub_protocols/src/generic_logup.rs b/crates/sub_protocols/src/generic_logup.rs index d3e59523..60e3d14f 100644 --- a/crates/sub_protocols/src/generic_logup.rs +++ b/crates/sub_protocols/src/generic_logup.rs @@ -331,16 +331,17 @@ pub fn prove_generic_logup( pub fn verify_generic_logup( verifier_state: &mut impl FSVerifier, c: EF, + alphas: &[EF], alphas_eq_poly: &[EF], log_memory: usize, - bytecode: &[[F; N_INSTRUCTION_COLUMNS]], + bytecode_multilinear: &[F], table_log_n_rows: &BTreeMap, ) -> ProofResult { let tables_heights_sorted = sort_tables_by_height(table_log_n_rows); - + let log_bytecode = log2_strict_usize(bytecode_multilinear.len() / N_INSTRUCTION_COLUMNS.next_power_of_two()); let total_n_vars = compute_total_n_vars( log_memory, - log2_strict_usize(bytecode.len()), + log_bytecode, &tables_heights_sorted.iter().cloned().collect(), ); @@ -373,8 +374,7 @@ pub fn verify_generic_logup( let mut offset = 1 << log_memory; // Bytecode - let log_bytecode = log2_strict_usize(bytecode.len()); - let log_bytecode_padded = log2_strict_usize(bytecode.len()).max(tables_heights_sorted[0].1); + let log_bytecode_padded = log_bytecode.max(tables_heights_sorted[0].1); let bytecode_and_acc_point = MultilinearPoint(from_end(&point_gkr, log_bytecode).to_vec()); let bits = to_big_endian_in_field::(offset >> log_bytecode, total_n_vars - log_bytecode); let pref = @@ -388,25 +388,22 @@ pub fn verify_generic_logup( retrieved_numerators_value -= pref * value_bytecode_acc; // Bytecode denominator - computed directly by verifier - let bytecode_index = mle_of_01234567_etc(&bytecode_and_acc_point); - let bytecode_value: Vec = (0..N_INSTRUCTION_COLUMNS) - .map(|col| { - bytecode - .iter() - .map(|row| row[col]) - .collect::>() - .evaluate(&bytecode_and_acc_point) - }) - .collect(); + let bytecode_index_value = mle_of_01234567_etc(&bytecode_and_acc_point); + + let mut bytecode_evaluation_point = bytecode_and_acc_point.0.clone(); + bytecode_evaluation_point.extend(from_end(alphas, log2_ceil_usize(N_INSTRUCTION_COLUMNS))); + let mut bytecode_value = bytecode_multilinear.evaluate(&MultilinearPoint(bytecode_evaluation_point)); + bytecode_value *= alphas[..alphas.len() - log2_ceil_usize(N_INSTRUCTION_COLUMNS)] + .iter() + .map(|x| EF::ONE - *x) + .product::(); retrieved_denominators_value += pref - * (c - finger_print( - F::from_usize(BYTECODE_TABLE_INDEX), - &[bytecode_value, vec![bytecode_index]].concat(), - alphas_eq_poly, - )); + * (c - (bytecode_value + + bytecode_index_value * alphas_eq_poly[N_INSTRUCTION_COLUMNS] + + *alphas_eq_poly.last().unwrap() * F::from_usize(BYTECODE_TABLE_INDEX))); // Padding for bytecode retrieved_denominators_value += - pref_padded * mle_of_zeros_then_ones(bytecode.len(), &from_end(&point_gkr, log_bytecode_padded)); + pref_padded * mle_of_zeros_then_ones(1 << log_bytecode, &from_end(&point_gkr, log_bytecode_padded)); offset += 1 << log_bytecode_padded; // ... Rest of the tables: From a33c6c377d43ecdd53d8384d48fc24720c4ead6e Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 20:39:40 +0400 Subject: [PATCH 10/47] prepare recursion --- crates/lean_compiler/src/c_compile_final.rs | 5 +- crates/lean_prover/src/prove_execution.rs | 3 +- crates/lean_prover/src/trace_gen.rs | 3 +- crates/lean_prover/src/verify_execution.rs | 2 +- crates/lean_vm/src/isa/bytecode.rs | 5 +- crates/sub_protocols/src/generic_logup.rs | 102 ++++++++++++-------- 6 files changed, 72 insertions(+), 48 deletions(-) diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index 85bc6344..0ec6003c 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -144,11 +144,10 @@ pub fn compile_to_low_level_bytecode( &mut hints, ); } - let mut instructions_encoded = instructions + let instructions_encoded = instructions .par_iter() .map(|instr| field_representation(instr)) .collect::>(); - instructions_encoded.resize(instructions.len().next_power_of_two(), [F::ZERO; N_INSTRUCTION_COLUMNS]); let mut instructions_multilinear = vec![]; for instr in &instructions_encoded { @@ -156,10 +155,10 @@ pub fn compile_to_low_level_bytecode( let padding = N_INSTRUCTION_COLUMNS.next_power_of_two() - N_INSTRUCTION_COLUMNS; instructions_multilinear.extend(vec![F::ZERO; padding]); } + instructions_multilinear.resize(instructions_multilinear.len().next_power_of_two(), F::ZERO); Ok(Bytecode { instructions, - instructions_encoded, instructions_multilinear, hints, starting_frame_memory, diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index 43105beb..73df3950 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -109,10 +109,11 @@ pub fn prove_execution( let logup_statements = prove_generic_logup( &mut prover_state, logup_c, + &logup_alphas, &logup_alphas_eq_poly, &memory, &memory_acc, - &bytecode.instructions_encoded, + &bytecode.instructions_multilinear, &bytecode_acc, &traces, ); diff --git a/crates/lean_prover/src/trace_gen.rs b/crates/lean_prover/src/trace_gen.rs index fdbe8700..237ecb33 100644 --- a/crates/lean_prover/src/trace_gen.rs +++ b/crates/lean_prover/src/trace_gen.rs @@ -29,7 +29,8 @@ pub fn get_execution_trace(bytecode: &Bytecode, execution_result: ExecutionResul .zip(execution_result.fps.par_iter()) .for_each(|((trace_row, &pc), &fp)| { let instruction = &bytecode.instructions[pc]; - let field_repr = bytecode.instructions_encoded[pc]; + let field_repr = &bytecode.instructions_multilinear[pc * N_INSTRUCTION_COLUMNS.next_power_of_two()..] + [..N_INSTRUCTION_COLUMNS]; let mut addr_a = F::ZERO; if field_repr[instr_idx(COL_FLAG_A)].is_zero() { diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index e822bf9b..a1178acd 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -158,7 +158,7 @@ pub fn verify_execution( Ok(ProofVerificationDetails { log_memory, table_n_vars, - first_quotient_gkr_n_vars: logup_statements.total_n_vars, + first_quotient_gkr_n_vars: logup_statements.total_gkr_n_vars, total_whir_statements_base, }) } diff --git a/crates/lean_vm/src/isa/bytecode.rs b/crates/lean_vm/src/isa/bytecode.rs index c64d9506..c2f02854 100644 --- a/crates/lean_vm/src/isa/bytecode.rs +++ b/crates/lean_vm/src/isa/bytecode.rs @@ -2,7 +2,7 @@ use p3_util::log2_ceil_usize; -use crate::{CodeAddress, F, FileId, FunctionName, Hint, N_INSTRUCTION_COLUMNS, SourceLocation}; +use crate::{CodeAddress, F, FileId, FunctionName, Hint, SourceLocation}; use super::Instruction; use std::collections::BTreeMap; @@ -12,9 +12,8 @@ use std::fmt::{Display, Formatter}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Bytecode { pub instructions: Vec, - pub instructions_encoded: Vec<[F; N_INSTRUCTION_COLUMNS]>, // padded to power of two length (with zero rows) pub instructions_multilinear: Vec, - pub hints: BTreeMap>, // pc -> hints + pub hints: BTreeMap>, // pc -> hints pub starting_frame_memory: usize, // debug pub function_locations: BTreeMap, diff --git a/crates/sub_protocols/src/generic_logup.rs b/crates/sub_protocols/src/generic_logup.rs index 60e3d14f..a46ef2e2 100644 --- a/crates/sub_protocols/src/generic_logup.rs +++ b/crates/sub_protocols/src/generic_logup.rs @@ -16,17 +16,20 @@ pub struct GenericLogupStatements { pub points: BTreeMap>, pub columns_values: BTreeMap>, // Used in recursion - pub total_n_vars: usize, + pub total_gkr_n_vars: usize, + pub bytecode_point: MultilinearPoint, + pub bytecode_value: EF, } #[allow(clippy::too_many_arguments)] pub fn prove_generic_logup( prover_state: &mut impl FSProver, c: EF, + alphas: &[EF], alphas_eq_poly: &[EF], memory: &[F], memory_acc: &[F], - bytecode: &[[F; N_INSTRUCTION_COLUMNS]], + bytecode_multilinear: &[F], bytecode_acc: &[F], traces: &BTreeMap, ) -> GenericLogupStatements { @@ -35,16 +38,17 @@ pub fn prove_generic_logup( assert_eq!(memory.len(), memory_acc.len()); assert!(memory.len() >= traces.values().map(|t| 1 << t.log_n_rows).max().unwrap()); + let log_bytecode = log2_strict_usize(bytecode_multilinear.len() / N_INSTRUCTION_COLUMNS.next_power_of_two()); let tables_heights = traces.iter().map(|(table, trace)| (*table, trace.log_n_rows)).collect(); let tables_heights_sorted = sort_tables_by_height(&tables_heights); - let total_n_vars = compute_total_n_vars( + let total_gkr_n_vars = compute_total_gkr_n_vars( log2_strict_usize(memory.len()), - log2_strict_usize(bytecode.len()), + log_bytecode, &tables_heights_sorted.iter().cloned().collect(), ); - let mut numerators = EF::zero_vec(1 << total_n_vars); - let mut denominators = EF::zero_vec(1 << total_n_vars); + let mut numerators = EF::zero_vec(1 << total_gkr_n_vars); + let mut denominators = EF::zero_vec(1 << total_gkr_n_vars); let mut offset = 0; @@ -67,30 +71,33 @@ pub fn prove_generic_logup( offset += memory.len(); // Bytecode - assert_eq!(bytecode.len(), bytecode_acc.len()); + assert_eq!(1 << log_bytecode, bytecode_acc.len()); numerators[offset..][..bytecode_acc.len()] .par_iter_mut() .zip(bytecode_acc) // TODO embedding overhead .for_each(|(num, a)| *num = EF::from(-*a)); // Note the negative sign here - denominators[offset..][..bytecode.len()] + denominators[offset..][..1 << log_bytecode] .par_iter_mut() - .zip(bytecode.par_iter().enumerate()) - .for_each(|(denom, (i, &instr))| { + .zip( + bytecode_multilinear + .par_chunks_exact(N_INSTRUCTION_COLUMNS.next_power_of_two()) + .enumerate(), + ) + .for_each(|(denom, (i, instr))| { *denom = c - finger_print( F::from_usize(BYTECODE_TABLE_INDEX), - &[instr.to_vec(), vec![F::from_usize(i)]].concat(), + &[instr[..N_INSTRUCTION_COLUMNS].to_vec(), vec![F::from_usize(i)]].concat(), alphas_eq_poly, ) }); let max_table_height = 1 << tables_heights_sorted[0].1; - if bytecode.len() < max_table_height { + if 1 << log_bytecode < max_table_height { // padding - denominators[offset + bytecode.len()..offset + max_table_height] + denominators[offset + (1 << log_bytecode)..offset + max_table_height] .par_iter_mut() .for_each(|d| *d = EF::ONE); } - offset += max_table_height.max(bytecode.len()); - + offset += max_table_height.max(1 << log_bytecode); // ... Rest of the tables: for (table, _) in &tables_heights_sorted { let trace = &traces[table]; @@ -193,7 +200,7 @@ pub fn prove_generic_logup( } } - assert_eq!(log2_ceil_usize(offset), total_n_vars); + assert_eq!(log2_ceil_usize(offset), total_gkr_n_vars); tracing::info!("Logup data: {} = 2^{:.2}", offset, (offset as f64).log2()); denominators[offset..].par_iter_mut().for_each(|d| *d = EF::ONE); // padding @@ -219,7 +226,7 @@ pub fn prove_generic_logup( prover_state.add_extension_scalar(value_memory); let bytecode_and_acc_point = - MultilinearPoint(from_end(&claim_point_gkr, log2_strict_usize(bytecode.len())).to_vec()); + MultilinearPoint(from_end(&claim_point_gkr, log_bytecode).to_vec()); let value_bytecode_acc = bytecode_acc.evaluate(&bytecode_and_acc_point); prover_state.add_extension_scalar(value_bytecode_acc); @@ -230,7 +237,7 @@ pub fn prove_generic_logup( let mut bus_numerators_values = BTreeMap::new(); let mut bus_denominators_values = BTreeMap::new(); let mut columns_values = BTreeMap::new(); - let mut offset = memory.len() + max_table_height.max(bytecode.len()); + let mut offset = memory.len() + max_table_height.max(1 << log_bytecode); for (table, _) in &tables_heights_sorted { let trace = &traces[table]; let log_n_rows = trace.log_n_rows; @@ -313,6 +320,12 @@ pub fn prove_generic_logup( offset += offset_for_table(table, log_n_rows); } + // recursion purpose + let mut bytecode_point = bytecode_and_acc_point.0.clone(); + bytecode_point.extend(from_end(alphas, log2_ceil_usize(N_INSTRUCTION_COLUMNS))); + let bytecode_point = MultilinearPoint(bytecode_point); + let bytecode_value = bytecode_multilinear.evaluate(&bytecode_point); + GenericLogupStatements { memory_and_acc_point, value_memory, @@ -323,7 +336,9 @@ pub fn prove_generic_logup( bus_denominators_values, points, columns_values, - total_n_vars, + total_gkr_n_vars, + bytecode_point, + bytecode_value, } } @@ -339,13 +354,13 @@ pub fn verify_generic_logup( ) -> ProofResult { let tables_heights_sorted = sort_tables_by_height(table_log_n_rows); let log_bytecode = log2_strict_usize(bytecode_multilinear.len() / N_INSTRUCTION_COLUMNS.next_power_of_two()); - let total_n_vars = compute_total_n_vars( + let total_gkr_n_vars = compute_total_gkr_n_vars( log_memory, log_bytecode, &tables_heights_sorted.iter().cloned().collect(), ); - let (sum, point_gkr, numerators_value, denominators_value) = verify_gkr_quotient(verifier_state, total_n_vars)?; + let (sum, point_gkr, numerators_value, denominators_value) = verify_gkr_quotient(verifier_state, total_gkr_n_vars)?; if sum != EF::ZERO { return Err(ProofError::InvalidProof); @@ -356,9 +371,9 @@ pub fn verify_generic_logup( // Memory ... let memory_and_acc_point = MultilinearPoint(from_end(&point_gkr, log_memory).to_vec()); - let bits = to_big_endian_in_field::(0, total_n_vars - log_memory); + let bits = to_big_endian_in_field::(0, total_gkr_n_vars - log_memory); let pref = - MultilinearPoint(bits).eq_poly_outside(&MultilinearPoint(point_gkr[..total_n_vars - log_memory].to_vec())); + MultilinearPoint(bits).eq_poly_outside(&MultilinearPoint(point_gkr[..total_gkr_n_vars - log_memory].to_vec())); let value_memory_acc = verifier_state.next_extension_scalar()?; retrieved_numerators_value -= pref * value_memory_acc; @@ -376,12 +391,13 @@ pub fn verify_generic_logup( // Bytecode let log_bytecode_padded = log_bytecode.max(tables_heights_sorted[0].1); let bytecode_and_acc_point = MultilinearPoint(from_end(&point_gkr, log_bytecode).to_vec()); - let bits = to_big_endian_in_field::(offset >> log_bytecode, total_n_vars - log_bytecode); - let pref = - MultilinearPoint(bits).eq_poly_outside(&MultilinearPoint(point_gkr[..total_n_vars - log_bytecode].to_vec())); - let bits_padded = to_big_endian_in_field::(offset >> log_bytecode_padded, total_n_vars - log_bytecode_padded); + let bits = to_big_endian_in_field::(offset >> log_bytecode, total_gkr_n_vars - log_bytecode); + let pref = MultilinearPoint(bits) + .eq_poly_outside(&MultilinearPoint(point_gkr[..total_gkr_n_vars - log_bytecode].to_vec())); + let bits_padded = + to_big_endian_in_field::(offset >> log_bytecode_padded, total_gkr_n_vars - log_bytecode_padded); let pref_padded = MultilinearPoint(bits_padded).eq_poly_outside(&MultilinearPoint( - point_gkr[..total_n_vars - log_bytecode_padded].to_vec(), + point_gkr[..total_gkr_n_vars - log_bytecode_padded].to_vec(), )); let value_bytecode_acc = verifier_state.next_extension_scalar()?; @@ -390,15 +406,17 @@ pub fn verify_generic_logup( // Bytecode denominator - computed directly by verifier let bytecode_index_value = mle_of_01234567_etc(&bytecode_and_acc_point); - let mut bytecode_evaluation_point = bytecode_and_acc_point.0.clone(); - bytecode_evaluation_point.extend(from_end(alphas, log2_ceil_usize(N_INSTRUCTION_COLUMNS))); - let mut bytecode_value = bytecode_multilinear.evaluate(&MultilinearPoint(bytecode_evaluation_point)); - bytecode_value *= alphas[..alphas.len() - log2_ceil_usize(N_INSTRUCTION_COLUMNS)] - .iter() - .map(|x| EF::ONE - *x) - .product::(); + let mut bytecode_point = bytecode_and_acc_point.0.clone(); + bytecode_point.extend(from_end(alphas, log2_ceil_usize(N_INSTRUCTION_COLUMNS))); + let bytecode_point = MultilinearPoint(bytecode_point); + let bytecode_value = bytecode_multilinear.evaluate(&bytecode_point); + let bytecode_value_corrected = bytecode_value + * alphas[..alphas.len() - log2_ceil_usize(N_INSTRUCTION_COLUMNS)] + .iter() + .map(|x| EF::ONE - *x) + .product::(); retrieved_denominators_value += pref - * (c - (bytecode_value + * (c - (bytecode_value_corrected + bytecode_index_value * alphas_eq_poly[N_INSTRUCTION_COLUMNS] + *alphas_eq_poly.last().unwrap() * F::from_usize(BYTECODE_TABLE_INDEX))); // Padding for bytecode @@ -413,7 +431,7 @@ pub fn verify_generic_logup( let mut columns_values = BTreeMap::new(); for &(table, log_n_rows) in &tables_heights_sorted { - let n_missing_vars = total_n_vars - log_n_rows; + let n_missing_vars = total_gkr_n_vars - log_n_rows; let inner_point = MultilinearPoint(from_end(&point_gkr, log_n_rows).to_vec()); let missing_point = MultilinearPoint(point_gkr[..n_missing_vars].to_vec()); @@ -529,7 +547,9 @@ pub fn verify_generic_logup( bus_denominators_values, points, columns_values, - total_n_vars, + total_gkr_n_vars, + bytecode_point, + bytecode_value, }) } @@ -539,7 +559,11 @@ fn offset_for_table(table: &Table, log_n_rows: usize) -> usize { num_cols << log_n_rows } -fn compute_total_n_vars(log_memory: usize, log_bytecode: usize, tables_heights: &BTreeMap) -> usize { +fn compute_total_gkr_n_vars( + log_memory: usize, + log_bytecode: usize, + tables_heights: &BTreeMap, +) -> usize { let max_table_height = 1 << tables_heights.values().copied().max().unwrap(); let total_len = (1 << log_memory) + (1 << log_bytecode).max(max_table_height) + (1 << tables_heights[&Table::execution()]) // bytecode From fa17c479fc5674dbeb40e92f98a8936e788dcb23 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 20:47:28 +0400 Subject: [PATCH 11/47] wip --- crates/lean_prover/src/prove_execution.rs | 1 - crates/lean_prover/src/verify_execution.rs | 2 ++ crates/rec_aggregation/recursion.py | 2 ++ crates/rec_aggregation/src/recursion.rs | 16 ++++++++++++++-- crates/sub_protocols/src/generic_logup.rs | 19 ++++--------------- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index 73df3950..2d38df20 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -109,7 +109,6 @@ pub fn prove_execution( let logup_statements = prove_generic_logup( &mut prover_state, logup_c, - &logup_alphas, &logup_alphas_eq_poly, &memory, &memory_acc, diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index a1178acd..0d012f12 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -13,6 +13,7 @@ pub struct ProofVerificationDetails { pub table_n_vars: BTreeMap, pub first_quotient_gkr_n_vars: usize, pub total_whir_statements_base: usize, + pub bytecode_evaluation: Evaluation, } pub fn verify_execution( @@ -160,6 +161,7 @@ pub fn verify_execution( table_n_vars, first_quotient_gkr_n_vars: logup_statements.total_gkr_n_vars, total_whir_statements_base, + bytecode_evaluation: logup_statements.bytecode_evaluation.unwrap(), }) } diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index 89be13c8..37ce9103 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -110,6 +110,8 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip log_bytecode_padded = maximum(log_bytecode, log_n_cycles) bytecode_multilinear_location_prefix = multilinear_location_prefix(offset / log_bytecode, N_VARS_LOGUP_GKR - log_bytecode, point_gkr) bytecode_padded_multilinear_location_prefix = multilinear_location_prefix(offset / log_bytecode_padded, N_VARS_LOGUP_GKR - log_bytecode_padded, point_gkr) + NONRESERVED_PROGRAM_INPUT_START_ = NONRESERVED_PROGRAM_INPUT_START + bus_numerators_values = DynArray([]) bus_denominators_values = DynArray([]) diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index bf66cb5f..d38e41e5 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -299,7 +299,16 @@ def main(): replacements.insert("STARTING_PC_PLACEHOLDER".to_string(), STARTING_PC.to_string()); replacements.insert("ENDING_PC_PLACEHOLDER".to_string(), ENDING_PC.to_string()); - let inner_public_input = vec![]; + let mut inner_public_input = vec![F::from_usize(verif_details.bytecode_evaluation.point.num_variables())]; + inner_public_input.extend( + verif_details + .bytecode_evaluation + .point + .0 + .iter() + .flat_map(|c| c.as_basis_coefficients_slice()), + ); + inner_public_input.extend(verif_details.bytecode_evaluation.value.as_basis_coefficients_slice()); let outer_public_memory = build_public_memory(&outer_public_input); let mut inner_private_input = vec![ F::from_usize(proof_to_prove.proof.len()), @@ -372,7 +381,10 @@ pub(crate) fn whir_recursion_placeholder_replacements(whir_config: &WhirConfig>, // Used in recursion pub total_gkr_n_vars: usize, - pub bytecode_point: MultilinearPoint, - pub bytecode_value: EF, + pub bytecode_evaluation: Option>, } #[allow(clippy::too_many_arguments)] pub fn prove_generic_logup( prover_state: &mut impl FSProver, c: EF, - alphas: &[EF], alphas_eq_poly: &[EF], memory: &[F], memory_acc: &[F], @@ -225,8 +223,7 @@ pub fn prove_generic_logup( let value_memory = memory.evaluate(&memory_and_acc_point); prover_state.add_extension_scalar(value_memory); - let bytecode_and_acc_point = - MultilinearPoint(from_end(&claim_point_gkr, log_bytecode).to_vec()); + let bytecode_and_acc_point = MultilinearPoint(from_end(&claim_point_gkr, log_bytecode).to_vec()); let value_bytecode_acc = bytecode_acc.evaluate(&bytecode_and_acc_point); prover_state.add_extension_scalar(value_bytecode_acc); @@ -320,12 +317,6 @@ pub fn prove_generic_logup( offset += offset_for_table(table, log_n_rows); } - // recursion purpose - let mut bytecode_point = bytecode_and_acc_point.0.clone(); - bytecode_point.extend(from_end(alphas, log2_ceil_usize(N_INSTRUCTION_COLUMNS))); - let bytecode_point = MultilinearPoint(bytecode_point); - let bytecode_value = bytecode_multilinear.evaluate(&bytecode_point); - GenericLogupStatements { memory_and_acc_point, value_memory, @@ -337,8 +328,7 @@ pub fn prove_generic_logup( points, columns_values, total_gkr_n_vars, - bytecode_point, - bytecode_value, + bytecode_evaluation: None, } } @@ -548,8 +538,7 @@ pub fn verify_generic_logup( points, columns_values, total_gkr_n_vars, - bytecode_point, - bytecode_value, + bytecode_evaluation: Some(Evaluation::new(bytecode_point, bytecode_value)), }) } From f64901acaa03291d46b2d7b3135fe4b28b2e2ec1 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 22:15:55 +0400 Subject: [PATCH 12/47] w --- crates/utils/src/multilinear.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/utils/src/multilinear.rs b/crates/utils/src/multilinear.rs index 2a945beb..9b9e413c 100644 --- a/crates/utils/src/multilinear.rs +++ b/crates/utils/src/multilinear.rs @@ -116,12 +116,12 @@ pub fn mle_of_01234567_etc(point: &[F]) -> F { } } -/// table = 1 is reversed for memory lookup -pub const MEMORY_TABLE_INDEX: usize = 1; -/// table = 2 is reversed for bytecode lookup -pub const BYTECODE_TABLE_INDEX: usize = 2; +/// table = 0 is reversed for memory lookup +pub const MEMORY_TABLE_INDEX: usize = 0; +/// table = 1 is reversed for bytecode lookup +pub const BYTECODE_TABLE_INDEX: usize = 1; /// All the remaining tables come after memory and bytecode (special cases) -pub const FIRST_NORMAL_TABLE_INDEX: usize = 3; +pub const FIRST_NORMAL_TABLE_INDEX: usize = 2; pub fn finger_print>, EF: ExtensionField + ExtensionField>( table: F, From 5219341f9162248527269a05f5b73e9d726d2126 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 22:19:41 +0400 Subject: [PATCH 13/47] wip recursion --- crates/lean_compiler/snark_lib.py | 2 + crates/rec_aggregation/recursion.py | 65 ++++++++++++++++++++++--- crates/rec_aggregation/src/recursion.rs | 2 +- crates/rec_aggregation/utils.py | 4 +- crates/rec_aggregation/whir.py | 2 +- 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/crates/lean_compiler/snark_lib.py b/crates/lean_compiler/snark_lib.py index 131a3ea4..827b83c4 100644 --- a/crates/lean_compiler/snark_lib.py +++ b/crates/lean_compiler/snark_lib.py @@ -77,9 +77,11 @@ def dot_product(a, b, result, length, mode): def hint_decompose_bits(value, bits, n_bits, endian): _ = value, bits, n_bits, endian + def hint_less_than(a, b, result_ptr): _ = a, b, result_ptr + def log2_ceil(x: int) -> int: assert x > 0 return math.ceil(math.log2(x)) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index 37ce9103..de326810 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -29,7 +29,7 @@ AIR_DOWN_COLUMNS_F = AIR_DOWN_COLUMNS_F_PLACEHOLDER # [[_; ?]; N_TABLES] AIR_DOWN_COLUMNS_EF = AIR_DOWN_COLUMNS_EF_PLACEHOLDER # [[_; _]; N_TABLES] -NUM_BYTECODE_INSTRUCTIONS = NUM_BYTECODE_INSTRUCTIONS_PLACEHOLDER +N_INSTRUCTION_COLUMNS = N_INSTRUCTION_COLUMNS_PLACEHOLDER N_COMMITTED_EXEC_COLUMNS = N_COMMITTED_EXEC_COLUMNS_PLACEHOLDER GUEST_BYTECODE_LEN = GUEST_BYTECODE_LEN_PLACEHOLDER @@ -108,10 +108,63 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip log_bytecode = log2_ceil(GUEST_BYTECODE_LEN) log_n_cycles = table_dims[EXECUTION_TABLE_INDEX - 1] log_bytecode_padded = maximum(log_bytecode, log_n_cycles) - bytecode_multilinear_location_prefix = multilinear_location_prefix(offset / log_bytecode, N_VARS_LOGUP_GKR - log_bytecode, point_gkr) - bytecode_padded_multilinear_location_prefix = multilinear_location_prefix(offset / log_bytecode_padded, N_VARS_LOGUP_GKR - log_bytecode_padded, point_gkr) + bytecode_and_acc_point = point_gkr + (N_VARS_LOGUP_GKR - log_bytecode) * DIM + bytecode_multilinear_location_prefix = multilinear_location_prefix( + offset / log_bytecode, N_VARS_LOGUP_GKR - log_bytecode, point_gkr + ) + bytecode_padded_multilinear_location_prefix = multilinear_location_prefix( + offset / log_bytecode_padded, N_VARS_LOGUP_GKR - log_bytecode_padded, point_gkr + ) NONRESERVED_PROGRAM_INPUT_START_ = NONRESERVED_PROGRAM_INPUT_START + assert NONRESERVED_PROGRAM_INPUT_START_[0] == log_bytecode + log2_ceil(N_INSTRUCTION_COLUMNS) + copy_many_ef(bytecode_and_acc_point, NONRESERVED_PROGRAM_INPUT_START + 1, log_bytecode) + copy_many_ef( + logup_alphas + (log2_ceil(MAX_BUS_WIDTH) - log2_ceil(N_INSTRUCTION_COLUMNS)) * DIM, + NONRESERVED_PROGRAM_INPUT_START + 1 + log_bytecode * DIM, + log2_ceil(N_INSTRUCTION_COLUMNS), + ) + bytecode_value = NONRESERVED_PROGRAM_INPUT_START_ + 1 + log_bytecode * DIM + bytecode_value_corrected: Mut = bytecode_value + for i in log2_ceil(MAX_BUS_WIDTH) - log2_ceil(N_INSTRUCTION_COLUMNS): + bytecode_value_corrected = mul_extension_ret( + bytecode_value_corrected, one_minus_self_extension_ret(logup_alphas + i * DIM) + ) + fs, value_bytecode_acc = fs_receive_ef(fs, 1) + retrieved_numerators_value = sub_extension_ret( + retrieved_numerators_value, mul_extension_ret(bytecode_multilinear_location_prefix, value_bytecode_acc) + ) + + bytecode_index_value = mle_of_01234567_etc(bytecode_and_acc_point, log_bytecode) + retrieved_denominators_value = add_extension_ret( + retrieved_denominators_value, + mul_extension_ret( + bytecode_multilinear_location_prefix, + sub_extension_ret( + logup_c, + add_extension_ret( + bytecode_value_corrected, + add_extension_ret( + mul_extension_ret(bytecode_index_value, logup_alphas_eq_poly + N_INSTRUCTION_COLUMNS * DIM), + mul_base_extension_ret( + EXECUTION_TABLE_INDEX, logup_alphas_eq_poly + (2 ** log2_ceil(MAX_BUS_WIDTH) - 1) * DIM + ), + ), + ), + ), + ), + ) + retrieved_denominators_value = add_extension_ret( + retrieved_denominators_value, + mul_extension_ret( + bytecode_padded_multilinear_location_prefix, + mle_of_zeros_then_ones( + point_gkr + (N_VARS_LOGUP_GKR - log_bytecode_padded) * DIM, + log2_ceil(GUEST_BYTECODE_LEN), + log_bytecode_padded, + ), + ), + ) bus_numerators_values = DynArray([]) bus_denominators_values = DynArray([]) @@ -352,7 +405,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip virtual_col_index = n_up_columns_f + i * DIM + j pcs_values[table_index][last_index_2][virtual_col_index].push(transposed + j * DIM) - log_num_instrs = log2_ceil(NUM_BYTECODE_INSTRUCTIONS) + log_num_instrs = log2_ceil(N_INSTRUCTION_COLUMNS) bytecode_compression_challenges = Array(DIM * log_num_instrs) for i in unroll(0, log_num_instrs): copy_5(fs_sample_ef(fs), bytecode_compression_challenges + i * DIM) # TODO avoid duplication @@ -360,14 +413,14 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip fs = duplexing(fs) bytecode_air_values = Array(DIM * 2**log_num_instrs) - for i in unroll(0, NUM_BYTECODE_INSTRUCTIONS): + for i in unroll(0, N_INSTRUCTION_COLUMNS): col = N_COMMITTED_EXEC_COLUMNS + i copy_5( pcs_values[EXECUTION_TABLE_INDEX - 1][2][col][0], bytecode_air_values + i * DIM, ) pcs_values[EXECUTION_TABLE_INDEX - 1][2][col].pop() - for i in unroll(NUM_BYTECODE_INSTRUCTIONS, 2**log_num_instrs): + for i in unroll(N_INSTRUCTION_COLUMNS, 2**log_num_instrs): set_to_5_zeros(bytecode_air_values + i * DIM) bytecode_air_point = pcs_points[EXECUTION_TABLE_INDEX - 1][2] diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index d38e41e5..7c4baf27 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -285,7 +285,7 @@ def main(): all_air_evals_in_zk_dsl(), ); replacements.insert( - "NUM_BYTECODE_INSTRUCTIONS_PLACEHOLDER".to_string(), + "N_INSTRUCTION_COLUMNS_PLACEHOLDER".to_string(), N_INSTRUCTION_COLUMNS.to_string(), ); replacements.insert( diff --git a/crates/rec_aggregation/utils.py b/crates/rec_aggregation/utils.py index 3887c8b7..6ca8a4c2 100644 --- a/crates/rec_aggregation/utils.py +++ b/crates/rec_aggregation/utils.py @@ -383,7 +383,8 @@ def mle_of_01234567_etc(point, n): d = mul_extension_ret(point, c) res = add_extension_ret(b, d) return res - + + def checked_less_than(a, b): res = Array(1) hint_less_than(a, b, res) @@ -394,6 +395,7 @@ def checked_less_than(a, b): assert b <= a return res + def maximum(a, b): is_a_less_than_b = checked_less_than(a, b) res: Imu diff --git a/crates/rec_aggregation/whir.py b/crates/rec_aggregation/whir.py index 289a96df..e9d3f7a8 100644 --- a/crates/rec_aggregation/whir.py +++ b/crates/rec_aggregation/whir.py @@ -11,7 +11,6 @@ WHIR_GRINDING_BITS = WHIR_GRINDING_BITS_PLACEHOLDER - def whir_open( fs: Mut, root: Mut, @@ -153,6 +152,7 @@ def whir_open( return fs, folding_randomness_global, s, final_value, end_sum + def sumcheck_verify(fs: Mut, n_steps, claimed_sum, degree: Const): challenges = Array(n_steps * DIM) fs, new_claimed_sum = sumcheck_verify_helper(fs, n_steps, claimed_sum, degree, challenges) From 4a4258e25d9075f8d9c324258f26cd834f272f2b Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 22:33:02 +0400 Subject: [PATCH 14/47] w --- crates/lean_vm/src/tables/table_enum.rs | 5 ++--- crates/utils/src/multilinear.rs | 10 ++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/lean_vm/src/tables/table_enum.rs b/crates/lean_vm/src/tables/table_enum.rs index 7084f38d..12887ef4 100644 --- a/crates/lean_vm/src/tables/table_enum.rs +++ b/crates/lean_vm/src/tables/table_enum.rs @@ -1,5 +1,4 @@ use multilinear_toolkit::prelude::*; -use utils::FIRST_NORMAL_TABLE_INDEX; use crate::*; @@ -48,7 +47,7 @@ impl Table { PF::from_usize(self.index()) } pub const fn index(&self) -> usize { - unsafe { *(self as *const Self as *const usize) + FIRST_NORMAL_TABLE_INDEX } + unsafe { *(self as *const Self as *const usize) } } } @@ -139,9 +138,9 @@ mod tests { #[test] fn test_table_indices() { for (i, table) in ALL_TABLES.iter().enumerate() { + assert_eq!(table.index(), i); assert_ne!(table.index(), MEMORY_TABLE_INDEX); assert_ne!(table.index(), BYTECODE_TABLE_INDEX); - assert_eq!(table.index(), i + FIRST_NORMAL_TABLE_INDEX); } } } diff --git a/crates/utils/src/multilinear.rs b/crates/utils/src/multilinear.rs index 9b9e413c..b0959c5b 100644 --- a/crates/utils/src/multilinear.rs +++ b/crates/utils/src/multilinear.rs @@ -116,12 +116,10 @@ pub fn mle_of_01234567_etc(point: &[F]) -> F { } } -/// table = 0 is reversed for memory lookup -pub const MEMORY_TABLE_INDEX: usize = 0; -/// table = 1 is reversed for bytecode lookup -pub const BYTECODE_TABLE_INDEX: usize = 1; -/// All the remaining tables come after memory and bytecode (special cases) -pub const FIRST_NORMAL_TABLE_INDEX: usize = 2; +/// table = 3 is reversed for memory lookup +pub const MEMORY_TABLE_INDEX: usize = 3; +/// table = 4 is reversed for bytecode lookup +pub const BYTECODE_TABLE_INDEX: usize = 4; pub fn finger_print>, EF: ExtensionField + ExtensionField>( table: F, From d61b9113152c640d39736c3835e3a31eb602993a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 22:36:19 +0400 Subject: [PATCH 15/47] w --- crates/rec_aggregation/recursion.py | 28 +++++++++++++------------ crates/rec_aggregation/src/recursion.rs | 6 +++++- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index de326810..053678d8 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -9,7 +9,10 @@ N_VARS_LOGUP_GKR = N_VARS_LOGUP_GKR_PLACEHOLDER MAX_BUS_WIDTH = MAX_BUS_WIDTH_PLACEHOLDER MAX_NUM_AIR_CONSTRAINTS = MAX_NUM_AIR_CONSTRAINTS_PLACEHOLDER + MEMORY_TABLE_INDEX = MEMORY_TABLE_INDEX_PLACEHOLDER +BYTECODE_TABLE_INDEX = BYTECODE_TABLE_INDEX_PLACEHOLDER +EXECUTION_TABLE_INDEX = EXECUTION_TABLE_INDEX_PLACEHOLDER LOOKUPS_F_INDEXES = LOOKUPS_F_INDEXES_PLACEHOLDER # [[_; ?]; N_TABLES] LOOKUPS_F_VALUES = LOOKUPS_F_VALUES_PLACEHOLDER # [[[_; ?]; ?]; N_TABLES] @@ -22,7 +25,6 @@ NUM_COLS_F_COMMITTED = NUM_COLS_F_COMMITTED_PLACEHOLDER -EXECUTION_TABLE_INDEX = EXECUTION_TABLE_INDEX_PLACEHOLDER AIR_DEGREES = AIR_DEGREES_PLACEHOLDER # [_; N_TABLES] N_AIR_COLUMNS_F = N_AIR_COLUMNS_F_PLACEHOLDER # [_; N_TABLES] N_AIR_COLUMNS_EF = N_AIR_COLUMNS_EF_PLACEHOLDER # [_; N_TABLES] @@ -106,7 +108,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip offset: Mut = powers_of_two(log_memory) log_bytecode = log2_ceil(GUEST_BYTECODE_LEN) - log_n_cycles = table_dims[EXECUTION_TABLE_INDEX - 1] + log_n_cycles = table_dims[EXECUTION_TABLE_INDEX] log_bytecode_padded = maximum(log_bytecode, log_n_cycles) bytecode_and_acc_point = point_gkr + (N_VARS_LOGUP_GKR - log_bytecode) * DIM bytecode_multilinear_location_prefix = multilinear_location_prefix( @@ -296,7 +298,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip total_num_cols = NUM_COLS_F_AIR[table_index] + DIM * NUM_COLS_EF_AIR[table_index] bus_final_value: Mut = bus_numerator_value - if table_index != EXECUTION_TABLE_INDEX - 1: # -1 because shift due to memory + if table_index != EXECUTION_TABLE_INDEX: bus_final_value = opposite_extension_ret(bus_final_value) bus_final_value = add_extension_ret( bus_final_value, @@ -416,14 +418,14 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip for i in unroll(0, N_INSTRUCTION_COLUMNS): col = N_COMMITTED_EXEC_COLUMNS + i copy_5( - pcs_values[EXECUTION_TABLE_INDEX - 1][2][col][0], + pcs_values[EXECUTION_TABLE_INDEX][2][col][0], bytecode_air_values + i * DIM, ) - pcs_values[EXECUTION_TABLE_INDEX - 1][2][col].pop() + pcs_values[EXECUTION_TABLE_INDEX][2][col].pop() for i in unroll(N_INSTRUCTION_COLUMNS, 2**log_num_instrs): set_to_5_zeros(bytecode_air_values + i * DIM) - bytecode_air_point = pcs_points[EXECUTION_TABLE_INDEX - 1][2] + bytecode_air_point = pcs_points[EXECUTION_TABLE_INDEX][2] bytecode_lookup_claim = dot_product_ret( bytecode_air_values, poly_eq_extension(bytecode_compression_challenges, log_num_instrs), @@ -471,13 +473,13 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip # TODO evaluate the folded bytecode - pcs_points[EXECUTION_TABLE_INDEX - 1].push(ls_on_indexes_point) - pcs_values[EXECUTION_TABLE_INDEX - 1].push(DynArray([])) - last_len = len(pcs_values[EXECUTION_TABLE_INDEX - 1]) - 1 - total_exec_cols = NUM_COLS_F_AIR[EXECUTION_TABLE_INDEX - 1] + DIM * NUM_COLS_EF_AIR[EXECUTION_TABLE_INDEX - 1] + pcs_points[EXECUTION_TABLE_INDEX].push(ls_on_indexes_point) + pcs_values[EXECUTION_TABLE_INDEX].push(DynArray([])) + last_len = len(pcs_values[EXECUTION_TABLE_INDEX]) - 1 + total_exec_cols = NUM_COLS_F_AIR[EXECUTION_TABLE_INDEX] + DIM * NUM_COLS_EF_AIR[EXECUTION_TABLE_INDEX] for _ in unroll(0, total_exec_cols): - pcs_values[EXECUTION_TABLE_INDEX - 1][last_len].push(DynArray([])) - pcs_values[EXECUTION_TABLE_INDEX - 1][last_len][COL_PC].push(ls_on_indexes_eval) + pcs_values[EXECUTION_TABLE_INDEX][last_len].push(DynArray([])) + pcs_values[EXECUTION_TABLE_INDEX][last_len][COL_PC].push(ls_on_indexes_eval) # verify the outer public memory is well constructed (with the conventions) for i in unroll(0, next_multiple_of(NONRESERVED_PROGRAM_INPUT_START, DIM) / DIM): @@ -617,7 +619,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip ) curr_randomness += DIM num_committed_cols: Imu - if table_index == EXECUTION_TABLE_INDEX - 1: + if table_index == EXECUTION_TABLE_INDEX: num_committed_cols = N_COMMITTED_EXEC_COLUMNS else: num_committed_cols = total_num_cols diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index 7c4baf27..e2e32d8c 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -12,7 +12,7 @@ use multilinear_toolkit::prelude::symbolic::{ SymbolicExpression, SymbolicOperation, get_symbolic_constraints_and_bus_data_values, }; use multilinear_toolkit::prelude::*; -use utils::{Counter, MEMORY_TABLE_INDEX}; +use utils::{BYTECODE_TABLE_INDEX, Counter, MEMORY_TABLE_INDEX}; pub fn run_recursion_benchmark(count: usize, tracing: bool) { if tracing { @@ -142,6 +142,10 @@ def main(): "MEMORY_TABLE_INDEX_PLACEHOLDER".to_string(), MEMORY_TABLE_INDEX.to_string(), ); + replacements.insert( + "BYTECODE_TABLE_INDEX_PLACEHOLDER".to_string(), + BYTECODE_TABLE_INDEX.to_string(), + ); replacements.insert( "GUEST_BYTECODE_LEN_PLACEHOLDER".to_string(), bytecode_to_prove.instructions.len().to_string(), From 4692dc7ff6eccff14c8f47687e4518546fcc251c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Sun, 1 Feb 2026 23:11:47 +0400 Subject: [PATCH 16/47] wip --- crates/rec_aggregation/recursion.py | 123 +++++++--------------- crates/sub_protocols/src/generic_logup.rs | 19 ++-- 2 files changed, 47 insertions(+), 95 deletions(-) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index 053678d8..43019f05 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -100,7 +100,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip retrieved_numerators_value: Mut = opposite_extension_ret(mul_extension_ret(memory_and_acc_prefix, value_acc)) value_index = mle_of_01234567_etc(point_gkr + (N_VARS_LOGUP_GKR - log_memory) * DIM, log_memory) - fingerprint_memory = fingerprint_2(MEMORY_TABLE_INDEX, value_index, value_memory, logup_alphas_eq_poly) + fingerprint_memory = fingerprint_2(MEMORY_TABLE_INDEX, value_memory, value_index, logup_alphas_eq_poly) retrieved_denominators_value: Mut = mul_extension_ret( memory_and_acc_prefix, sub_extension_ret(logup_c, fingerprint_memory) ) @@ -149,7 +149,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip add_extension_ret( mul_extension_ret(bytecode_index_value, logup_alphas_eq_poly + N_INSTRUCTION_COLUMNS * DIM), mul_base_extension_ret( - EXECUTION_TABLE_INDEX, logup_alphas_eq_poly + (2 ** log2_ceil(MAX_BUS_WIDTH) - 1) * DIM + BYTECODE_TABLE_INDEX, logup_alphas_eq_poly + (2 ** log2_ceil(MAX_BUS_WIDTH) - 1) * DIM ), ), ), @@ -176,6 +176,11 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip pcs_values = DynArray([]) # [[[[] or [_]; num cols]; N]; N_TABLES] for i in unroll(0, N_TABLES): pcs_values.push(DynArray([])) + pcs_values[i].push(DynArray([])) + total_num_cols = NUM_COLS_F_AIR[i] + DIM * NUM_COLS_EF_AIR[i] + for col in unroll(0, total_num_cols): + pcs_values[i][0].push(DynArray([])) + for table_index in unroll(0, N_TABLES): # I] Bus (data flow between tables) @@ -186,6 +191,22 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip prefix = multilinear_location_prefix(offset / n_rows, N_VARS_LOGUP_GKR - log_n_rows, point_gkr) + if table_index == EXECUTION_TABLE_INDEX: + # 0] Bytecode lookup + fs, eval_on_pc = fs_receive_ef(fs, 1) + pcs_values[EXECUTION_TABLE_INDEX][0][COL_PC].push(eval_on_pc) + fs, instr_evals = fs_receive_ef(fs, N_INSTRUCTION_COLUMNS) + for i in unroll(0, N_INSTRUCTION_COLUMNS): + global_index = N_COMMITTED_EXEC_COLUMNS + i + pcs_values[EXECUTION_TABLE_INDEX][0][global_index].push(instr_evals + i * DIM) + retrieved_numerators_value = add_extension_ret(retrieved_numerators_value, prefix) + fingerp = fingerprint_bytecode(instr_evals, eval_on_pc, logup_alphas_eq_poly) + retrieved_denominators_value = add_extension_ret( + retrieved_denominators_value, + mul_extension_ret(prefix, sub_extension_ret(logup_c, fingerp)), + ) + offset += n_rows + fs, eval_on_selector = fs_receive_ef(fs, 1) retrieved_numerators_value = add_extension_ret( retrieved_numerators_value, mul_extension_ret(prefix, eval_on_selector) @@ -204,11 +225,6 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip # II] Lookup into memory - pcs_values[table_index].push(DynArray([])) - total_num_cols = NUM_COLS_F_AIR[table_index] + DIM * NUM_COLS_EF_AIR[table_index] - for col in unroll(0, total_num_cols): - pcs_values[table_index][0].push(DynArray([])) - for lookup_f_index in unroll(0, len(LOOKUPS_F_INDEXES[table_index])): col_index = LOOKUPS_F_INDEXES[table_index][lookup_f_index] fs, index_eval = fs_receive_ef(fs, 1) @@ -226,8 +242,8 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip retrieved_numerators_value = add_extension_ret(retrieved_numerators_value, pref) fingerp = fingerprint_2( MEMORY_TABLE_INDEX, - add_base_extension_ret(i, index_eval), value_eval, + add_base_extension_ret(i, index_eval), logup_alphas_eq_poly, ) retrieved_denominators_value = add_extension_ret( @@ -254,8 +270,8 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip retrieved_numerators_value = add_extension_ret(retrieved_numerators_value, pref) fingerp = fingerprint_2( MEMORY_TABLE_INDEX, - add_base_extension_ret(i, index_eval), value_eval, + add_base_extension_ret(i, index_eval), logup_alphas_eq_poly, ) retrieved_denominators_value = add_extension_ret( @@ -407,80 +423,6 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip virtual_col_index = n_up_columns_f + i * DIM + j pcs_values[table_index][last_index_2][virtual_col_index].push(transposed + j * DIM) - log_num_instrs = log2_ceil(N_INSTRUCTION_COLUMNS) - bytecode_compression_challenges = Array(DIM * log_num_instrs) - for i in unroll(0, log_num_instrs): - copy_5(fs_sample_ef(fs), bytecode_compression_challenges + i * DIM) # TODO avoid duplication - if i != log_num_instrs - 1: - fs = duplexing(fs) - - bytecode_air_values = Array(DIM * 2**log_num_instrs) - for i in unroll(0, N_INSTRUCTION_COLUMNS): - col = N_COMMITTED_EXEC_COLUMNS + i - copy_5( - pcs_values[EXECUTION_TABLE_INDEX][2][col][0], - bytecode_air_values + i * DIM, - ) - pcs_values[EXECUTION_TABLE_INDEX][2][col].pop() - for i in unroll(N_INSTRUCTION_COLUMNS, 2**log_num_instrs): - set_to_5_zeros(bytecode_air_values + i * DIM) - - bytecode_air_point = pcs_points[EXECUTION_TABLE_INDEX][2] - bytecode_lookup_claim = dot_product_ret( - bytecode_air_values, - poly_eq_extension(bytecode_compression_challenges, log_num_instrs), - 2**log_num_instrs, - EE, - ) - - fs, whir_ext_root, whir_ext_ood_points, whir_ext_ood_evals = parse_whir_commitment_const(fs, NUM_OOD_COMMIT_EXT) - - # VERIFY LOGUP* - - log_table_len = log2_ceil(GUEST_BYTECODE_LEN) - fs, ls_sumcheck_point, ls_sumcheck_value = sumcheck_verify(fs, log_table_len, bytecode_lookup_claim, 2) - fs, table_eval = fs_receive_ef(fs, 1) - fs, pushforward_eval = fs_receive_ef(fs, 1) - mul_extension(table_eval, pushforward_eval, ls_sumcheck_value) - - ls_c = fs_sample_ef(fs) - - fs, quotient_left, claim_point_left, claim_num_left, eval_c_minus_indexes = verify_gkr_quotient(fs, log_n_cycles) - fs, quotient_right, claim_point_right, pushforward_final_eval, claim_den_right = verify_gkr_quotient( - fs, log_table_len - ) - - copy_5(quotient_left, quotient_right) - - copy_5( - eq_mle_extension(claim_point_left, bytecode_air_point, log_n_cycles), - claim_num_left, - ) - copy_5( - sub_extension_ret(ls_c, mle_of_01234567_etc(claim_point_right, log_table_len)), - claim_den_right, - ) - - # logupstar statements: - ls_on_indexes_point = claim_point_left - ls_on_indexes_eval = sub_extension_ret(ls_c, eval_c_minus_indexes) - ls_on_table_point = ls_sumcheck_point - ls_on_table_eval = table_eval - ls_on_pushforward_point_1 = ls_sumcheck_point - ls_on_pushforward_eval_1 = pushforward_eval - ls_on_pushforward_point_2 = claim_point_right - ls_on_pushforward_eval_2 = pushforward_final_eval - - # TODO evaluate the folded bytecode - - pcs_points[EXECUTION_TABLE_INDEX].push(ls_on_indexes_point) - pcs_values[EXECUTION_TABLE_INDEX].push(DynArray([])) - last_len = len(pcs_values[EXECUTION_TABLE_INDEX]) - 1 - total_exec_cols = NUM_COLS_F_AIR[EXECUTION_TABLE_INDEX] + DIM * NUM_COLS_EF_AIR[EXECUTION_TABLE_INDEX] - for _ in unroll(0, total_exec_cols): - pcs_values[EXECUTION_TABLE_INDEX][last_len].push(DynArray([])) - pcs_values[EXECUTION_TABLE_INDEX][last_len][COL_PC].push(ls_on_indexes_eval) - # verify the outer public memory is well constructed (with the conventions) for i in unroll(0, next_multiple_of(NONRESERVED_PROGRAM_INPUT_START, DIM) / DIM): copy_5(i * DIM, outer_public_memory + i * DIM) @@ -579,6 +521,8 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip offset = powers_of_two(log_memory) * 2 # memory and acc + + prefix_pc_start = multilinear_location_prefix( offset + COL_PC * powers_of_two(log_n_cycles), WHIR_N_VARS, @@ -638,15 +582,22 @@ def multilinear_location_prefix(offset, n_vars, point): return res -def fingerprint_2(table_index, data_1, data_2, alpha_powers): +def fingerprint_2(table_index, data_1, data_2, logup_alphas_eq_poly): buff = Array(DIM * 2) copy_5(data_1, buff) copy_5(data_2, buff + DIM) - res: Mut = dot_product_ret(buff, alpha_powers + DIM, 2, EE) - res = add_base_extension_ret(table_index, res) + res: Mut = dot_product_ret(buff, logup_alphas_eq_poly, 2, EE) + res = add_extension_ret(res, mul_base_extension_ret(table_index, logup_alphas_eq_poly + (2**log2_ceil(MAX_BUS_WIDTH) - 1) * DIM)) + return res + +def fingerprint_bytecode(instr_evals, eval_on_pc, logup_alphas_eq_poly): + res: Mut = dot_product_ret(instr_evals, logup_alphas_eq_poly, N_INSTRUCTION_COLUMNS, EE) + res = add_extension_ret(res, mul_extension_ret(eval_on_pc, logup_alphas_eq_poly + N_INSTRUCTION_COLUMNS * DIM)) + res = add_extension_ret(res, mul_base_extension_ret(BYTECODE_TABLE_INDEX, logup_alphas_eq_poly + (2**log2_ceil(MAX_BUS_WIDTH) - 1) * DIM)) return res + def verify_gkr_quotient(fs: Mut, n_vars): fs, nums = fs_receive_ef(fs, 2) fs, denoms = fs_receive_ef(fs, 2) diff --git a/crates/sub_protocols/src/generic_logup.rs b/crates/sub_protocols/src/generic_logup.rs index b722d18b..94593cb3 100644 --- a/crates/sub_protocols/src/generic_logup.rs +++ b/crates/sub_protocols/src/generic_logup.rs @@ -255,12 +255,15 @@ pub fn prove_generic_logup( assert!(!table_values.contains_key(&COL_PC)); table_values.insert(COL_PC, eval_on_pc); - for (i, col) in bytecode_columns.iter().enumerate() { - let eval_on_instr_col = col.evaluate(&inner_point); - prover_state.add_extension_scalar(eval_on_instr_col); + let instr_evals = bytecode_columns + .iter() + .map(|col| col.evaluate(&inner_point)) + .collect::>(); + prover_state.add_extension_scalars(&instr_evals); + for (i, eval_on_instr_col) in instr_evals.iter().enumerate() { let global_index = N_RUNTIME_COLUMNS + i; assert!(!table_values.contains_key(&global_index)); - table_values.insert(global_index, eval_on_instr_col); + table_values.insert(global_index, *eval_on_instr_col); } offset += 1 << log_n_rows; @@ -433,12 +436,10 @@ pub fn verify_generic_logup( let eval_on_pc = verifier_state.next_extension_scalar()?; table_values.insert(COL_PC, eval_on_pc); - let mut instr_evals = Vec::with_capacity(N_INSTRUCTION_COLUMNS); - for i in 0..N_INSTRUCTION_COLUMNS { - let eval_on_instr_col = verifier_state.next_extension_scalar()?; + let instr_evals = verifier_state.next_extension_scalars_vec(N_INSTRUCTION_COLUMNS)?; + for (i, eval_on_instr_col) in instr_evals.iter().enumerate() { let global_index = N_RUNTIME_COLUMNS + i; - table_values.insert(global_index, eval_on_instr_col); - instr_evals.push(eval_on_instr_col); + table_values.insert(global_index, *eval_on_instr_col); } let bits = to_big_endian_in_field::(offset >> log_n_rows, n_missing_vars); From 8c06c68497995e7558ca0a46fe2d90ae1558db13 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 10:52:07 +0400 Subject: [PATCH 17/47] w --- crates/rec_aggregation/recursion.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index 43019f05..8f96495b 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -454,6 +454,8 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip curr_randomness += DIM whir_sum = add_extension_ret(mul_extension_ret(public_memory_eval, curr_randomness), whir_sum) curr_randomness += DIM + whir_sum = add_extension_ret(mul_extension_ret(value_bytecode_acc, curr_randomness), whir_sum) + curr_randomness += DIM whir_sum = add_extension_ret(mul_extension_ret(embed_in_ef(STARTING_PC), curr_randomness), whir_sum) curr_randomness += DIM @@ -491,17 +493,17 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip memory_and_acc_point, log_memory, ) - prefix_mem = multilinear_location_prefix(0, WHIR_N_VARS - log_memory, folding_randomness_global) + prefix_memory = multilinear_location_prefix(0, WHIR_N_VARS - log_memory, folding_randomness_global) s = add_extension_ret( s, - mul_extension_ret(mul_extension_ret(curr_randomness, prefix_mem), eq_memory_and_acc_point), + mul_extension_ret(mul_extension_ret(curr_randomness, prefix_memory), eq_memory_and_acc_point), ) curr_randomness += DIM - prefix_acc = multilinear_location_prefix(1, WHIR_N_VARS - log_memory, folding_randomness_global) + prefix_acc_memory = multilinear_location_prefix(1, WHIR_N_VARS - log_memory, folding_randomness_global) s = add_extension_ret( s, - mul_extension_ret(mul_extension_ret(curr_randomness, prefix_acc), eq_memory_and_acc_point), + mul_extension_ret(mul_extension_ret(curr_randomness, prefix_acc_memory), eq_memory_and_acc_point), ) curr_randomness += DIM @@ -519,9 +521,22 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip ) curr_randomness += DIM - offset = powers_of_two(log_memory) * 2 # memory and acc + offset = powers_of_two(log_memory) * 2 # memory and acc_memory - + eq_bytecode_acc = eq_mle_extension( + folding_randomness_global + (WHIR_N_VARS - log2_ceil(GUEST_BYTECODE_LEN)) * DIM, + bytecode_and_acc_point, + log2_ceil(GUEST_BYTECODE_LEN), + ) + prefix_bytecode_acc = multilinear_location_prefix( + 0, WHIR_N_VARS - log2_ceil(GUEST_BYTECODE_LEN), folding_randomness_global + ) + s = add_extension_ret( + s, + mul_extension_ret(mul_extension_ret(curr_randomness, prefix_bytecode_acc), eq_bytecode_acc), + ) + curr_randomness += DIM + offset += next_multiple_of(GUEST_BYTECODE_LEN, 2) prefix_pc_start = multilinear_location_prefix( offset + COL_PC * powers_of_two(log_n_cycles), From d506e06faddd0091db20ff6f94cb11a384cfdeb8 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 10:59:16 +0400 Subject: [PATCH 18/47] wip --- crates/rec_aggregation/recursion.py | 2 +- crates/rec_aggregation/src/recursion.rs | 4 ++-- crates/rec_aggregation/whir.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index 8f96495b..c001d2d9 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -127,7 +127,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip ) bytecode_value = NONRESERVED_PROGRAM_INPUT_START_ + 1 + log_bytecode * DIM bytecode_value_corrected: Mut = bytecode_value - for i in log2_ceil(MAX_BUS_WIDTH) - log2_ceil(N_INSTRUCTION_COLUMNS): + for i in unroll(0, log2_ceil(MAX_BUS_WIDTH) - log2_ceil(N_INSTRUCTION_COLUMNS)): bytecode_value_corrected = mul_extension_ret( bytecode_value_corrected, one_minus_self_extension_ret(logup_alphas + i * DIM) ) diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index e2e32d8c..0d5fc9c9 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -297,7 +297,7 @@ def main(): N_RUNTIME_COLUMNS.to_string(), ); replacements.insert( - "TOTAL_WHIR_STATEMENTS_BASE_PLACEHOLDER".to_string(), + "TOTAL_WHIR_STATEMENTS_PLACEHOLDER".to_string(), verif_details.total_whir_statements_base.to_string(), ); replacements.insert("STARTING_PC_PLACEHOLDER".to_string(), STARTING_PC.to_string()); @@ -438,7 +438,7 @@ where let mut res = format!( "def evaluate_air_constraints_table_{}({}, air_alpha_powers, bus_beta, bus_alpha_powers):\n", - table.table().index() - 1, + table.table().index(), AIR_INNER_VALUES_VAR ); diff --git a/crates/rec_aggregation/whir.py b/crates/rec_aggregation/whir.py index e9d3f7a8..28ce1075 100644 --- a/crates/rec_aggregation/whir.py +++ b/crates/rec_aggregation/whir.py @@ -44,7 +44,7 @@ def whir_open( WHIR_FOLDING_FACTORS[r], 2 ** WHIR_FOLDING_FACTORS[r], is_first_round, - NUM_QUERIES[r], + WHIR_NUM_QUERIES[r], domain_sz, claimed_sum, WHIR_GRINDING_BITS[r], @@ -63,7 +63,7 @@ def whir_open( fs, all_circle_values[WHIR_N_ROUNDS], final_folds = sample_stir_indexes_and_fold( fs, - NUM_QUERIES[WHIR_N_ROUNDS], + WHIR_NUM_QUERIES[WHIR_N_ROUNDS], 0, WHIR_FOLDING_FACTORS[WHIR_N_ROUNDS], 2 ** WHIR_FOLDING_FACTORS[WHIR_N_ROUNDS], @@ -74,7 +74,7 @@ def whir_open( ) final_circle_values = all_circle_values[WHIR_N_ROUNDS] - for i in range(0, NUM_QUERIES[WHIR_N_ROUNDS]): + for i in range(0, WHIR_NUM_QUERIES[WHIR_N_ROUNDS]): powers_of_2_rev = expand_from_univariate_base_const(final_circle_values[i], WHIR_FINAL_VARS) poly_eq = poly_eq_base(powers_of_2_rev, WHIR_FINAL_VARS) final_pol_evaluated_on_circle = Array(DIM) @@ -130,16 +130,16 @@ def whir_open( WHIR_NUM_OODS[i], ) - s6s = Array((NUM_QUERIES[i]) * DIM) + s6s = Array((WHIR_NUM_QUERIES[i]) * DIM) circle_value_i = all_circle_values[i] - for j in range(0, NUM_QUERIES[i]): # unroll ? + for j in range(0, WHIR_NUM_QUERIES[i]): # unroll ? expanded_from_univariate = expand_from_univariate_base(circle_value_i[j], n_vars) temp = eq_mle_base_extension(expanded_from_univariate, my_folding_randomness, n_vars) copy_5(temp, s6s + j * DIM) s7 = dot_product_ret( s6s, combination_randomness_powers + WHIR_NUM_OODS[i] * DIM, - NUM_QUERIES[i], + WHIR_NUM_QUERIES[i], EE, ) s = add_extension_ret(s, s7) From 121ff916b32243d966e1d6cf0cfd7a46b290175c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 11:19:19 +0400 Subject: [PATCH 19/47] fix checked_less_than --- crates/lean_vm/src/isa/hint.rs | 4 ++-- crates/rec_aggregation/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/lean_vm/src/isa/hint.rs b/crates/lean_vm/src/isa/hint.rs index db581d3d..603445ef 100644 --- a/crates/lean_vm/src/isa/hint.rs +++ b/crates/lean_vm/src/isa/hint.rs @@ -138,9 +138,9 @@ impl CustomHint { Self::LessThan => { let a = args[0].read_value(ctx.memory, ctx.fp)?; let b = args[1].read_value(ctx.memory, ctx.fp)?; - let result_addr = args[2].read_value(ctx.memory, ctx.fp)?.to_usize(); + let res_ptr = args[2].memory_address(ctx.fp)?; let result = if a.to_usize() < b.to_usize() { F::ONE } else { F::ZERO }; - ctx.memory.set(result_addr, result)?; + ctx.memory.set(res_ptr, result)?; } } Ok(()) diff --git a/crates/rec_aggregation/utils.py b/crates/rec_aggregation/utils.py index 6ca8a4c2..41f84383 100644 --- a/crates/rec_aggregation/utils.py +++ b/crates/rec_aggregation/utils.py @@ -386,7 +386,7 @@ def mle_of_01234567_etc(point, n): def checked_less_than(a, b): - res = Array(1) + res: Imu hint_less_than(a, b, res) assert res * (1 - res) == 0 if res == 1: From 42c72daea42d2e06eb963b8b2118a3ce47c43b7d Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 11:55:46 +0400 Subject: [PATCH 20/47] wip --- crates/rec_aggregation/recursion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index c001d2d9..ce507505 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -112,10 +112,10 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip log_bytecode_padded = maximum(log_bytecode, log_n_cycles) bytecode_and_acc_point = point_gkr + (N_VARS_LOGUP_GKR - log_bytecode) * DIM bytecode_multilinear_location_prefix = multilinear_location_prefix( - offset / log_bytecode, N_VARS_LOGUP_GKR - log_bytecode, point_gkr + offset / 2**log2_ceil(GUEST_BYTECODE_LEN), N_VARS_LOGUP_GKR - log_bytecode, point_gkr ) bytecode_padded_multilinear_location_prefix = multilinear_location_prefix( - offset / log_bytecode_padded, N_VARS_LOGUP_GKR - log_bytecode_padded, point_gkr + offset / powers_of_two(log_bytecode_padded), N_VARS_LOGUP_GKR - log_bytecode_padded, point_gkr ) NONRESERVED_PROGRAM_INPUT_START_ = NONRESERVED_PROGRAM_INPUT_START assert NONRESERVED_PROGRAM_INPUT_START_[0] == log_bytecode + log2_ceil(N_INSTRUCTION_COLUMNS) @@ -536,7 +536,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip mul_extension_ret(mul_extension_ret(curr_randomness, prefix_bytecode_acc), eq_bytecode_acc), ) curr_randomness += DIM - offset += next_multiple_of(GUEST_BYTECODE_LEN, 2) + offset += 2**log2_ceil(GUEST_BYTECODE_LEN) prefix_pc_start = multilinear_location_prefix( offset + COL_PC * powers_of_two(log_n_cycles), From 2ea7da43357924745812c75e2bca9ca452a8cbc3 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 12:17:08 +0400 Subject: [PATCH 21/47] wip --- crates/rec_aggregation/recursion.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index ce507505..4efb70a1 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -162,11 +162,12 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip bytecode_padded_multilinear_location_prefix, mle_of_zeros_then_ones( point_gkr + (N_VARS_LOGUP_GKR - log_bytecode_padded) * DIM, - log2_ceil(GUEST_BYTECODE_LEN), + 2**log2_ceil(GUEST_BYTECODE_LEN), log_bytecode_padded, ), ), ) + offset += powers_of_two(log_bytecode_padded) bus_numerators_values = DynArray([]) bus_denominators_values = DynArray([]) @@ -189,24 +190,27 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip inner_point = point_gkr + (N_VARS_LOGUP_GKR - log_n_rows) * DIM pcs_points[table_index].push(inner_point) - prefix = multilinear_location_prefix(offset / n_rows, N_VARS_LOGUP_GKR - log_n_rows, point_gkr) if table_index == EXECUTION_TABLE_INDEX: # 0] Bytecode lookup + bytecode_prefix = multilinear_location_prefix(offset / n_rows, N_VARS_LOGUP_GKR - log_n_rows, point_gkr) + fs, eval_on_pc = fs_receive_ef(fs, 1) pcs_values[EXECUTION_TABLE_INDEX][0][COL_PC].push(eval_on_pc) fs, instr_evals = fs_receive_ef(fs, N_INSTRUCTION_COLUMNS) for i in unroll(0, N_INSTRUCTION_COLUMNS): global_index = N_COMMITTED_EXEC_COLUMNS + i pcs_values[EXECUTION_TABLE_INDEX][0][global_index].push(instr_evals + i * DIM) - retrieved_numerators_value = add_extension_ret(retrieved_numerators_value, prefix) + retrieved_numerators_value = add_extension_ret(retrieved_numerators_value, bytecode_prefix) fingerp = fingerprint_bytecode(instr_evals, eval_on_pc, logup_alphas_eq_poly) retrieved_denominators_value = add_extension_ret( retrieved_denominators_value, - mul_extension_ret(prefix, sub_extension_ret(logup_c, fingerp)), + mul_extension_ret(bytecode_prefix, sub_extension_ret(logup_c, fingerp)), ) offset += n_rows + prefix = multilinear_location_prefix(offset / n_rows, N_VARS_LOGUP_GKR - log_n_rows, point_gkr) + fs, eval_on_selector = fs_receive_ef(fs, 1) retrieved_numerators_value = add_extension_ret( retrieved_numerators_value, mul_extension_ret(prefix, eval_on_selector) From c305aadc2445607b5fdb4e027fb2f33943889d04 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 12:46:37 +0400 Subject: [PATCH 22/47] fix --- crates/rec_aggregation/recursion.py | 2 +- crates/rec_aggregation/src/recursion.rs | 24 +++++++++++------------ crates/sub_protocols/src/generic_logup.rs | 1 - 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index 4efb70a1..689d36ff 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -125,7 +125,7 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip NONRESERVED_PROGRAM_INPUT_START + 1 + log_bytecode * DIM, log2_ceil(N_INSTRUCTION_COLUMNS), ) - bytecode_value = NONRESERVED_PROGRAM_INPUT_START_ + 1 + log_bytecode * DIM + bytecode_value = NONRESERVED_PROGRAM_INPUT_START + 1 + (log_bytecode + log2_ceil(N_INSTRUCTION_COLUMNS)) * DIM bytecode_value_corrected: Mut = bytecode_value for i in unroll(0, log2_ceil(MAX_BUS_WIDTH) - log2_ceil(N_INSTRUCTION_COLUMNS)): bytecode_value_corrected = mul_extension_ret( diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index 0d5fc9c9..fd9e8cbd 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -60,18 +60,18 @@ def main(): .replace("POSEIDON_OF_ZERO_PLACEHOLDER", &POSEIDON_16_NULL_HASH_PTR.to_string()); let bytecode_to_prove = compile_program(&ProgramSource::Raw(program_to_prove.to_string())); precompute_dft_twiddles::(1 << 24); - let outer_public_input = vec![]; - let outer_private_input = vec![]; + let inner_public_input = vec![]; + let inner_private_input = vec![]; let proof_to_prove = prove_execution( &bytecode_to_prove, - (&outer_public_input, &outer_private_input), + (&inner_public_input, &inner_private_input), &vec![], &inner_whir_config, false, ); let verif_details = verify_execution( &bytecode_to_prove, - &[], + &inner_public_input, proof_to_prove.proof.clone(), &inner_whir_config, ) @@ -303,8 +303,8 @@ def main(): replacements.insert("STARTING_PC_PLACEHOLDER".to_string(), STARTING_PC.to_string()); replacements.insert("ENDING_PC_PLACEHOLDER".to_string(), ENDING_PC.to_string()); - let mut inner_public_input = vec![F::from_usize(verif_details.bytecode_evaluation.point.num_variables())]; - inner_public_input.extend( + let mut outer_public_input = vec![F::from_usize(verif_details.bytecode_evaluation.point.num_variables())]; + outer_public_input.extend( verif_details .bytecode_evaluation .point @@ -312,16 +312,16 @@ def main(): .iter() .flat_map(|c| c.as_basis_coefficients_slice()), ); - inner_public_input.extend(verif_details.bytecode_evaluation.value.as_basis_coefficients_slice()); + outer_public_input.extend(dbg!(verif_details.bytecode_evaluation.value.as_basis_coefficients_slice())); let outer_public_memory = build_public_memory(&outer_public_input); - let mut inner_private_input = vec![ + let mut outer_private_input = vec![ F::from_usize(proof_to_prove.proof.len()), F::from_usize(log2_strict_usize(outer_public_memory.len())), F::from_usize(count), ]; - inner_private_input.extend(outer_public_memory); + outer_private_input.extend(outer_public_memory); for _ in 0..count { - inner_private_input.extend(proof_to_prove.proof.to_vec()); + outer_private_input.extend(proof_to_prove.proof.to_vec()); } let recursion_bytecode = @@ -331,7 +331,7 @@ def main(): let recursion_proof = prove_execution( &recursion_bytecode, - (&inner_public_input, &inner_private_input), + (&outer_public_input, &outer_private_input), &vec![], // TODO precompute poseidons &default_whir_config(), false, @@ -339,7 +339,7 @@ def main(): let proving_time = time.elapsed(); verify_execution( &recursion_bytecode, - &inner_public_input, + &outer_public_input, recursion_proof.proof, &default_whir_config(), ) diff --git a/crates/sub_protocols/src/generic_logup.rs b/crates/sub_protocols/src/generic_logup.rs index 94593cb3..7acf810a 100644 --- a/crates/sub_protocols/src/generic_logup.rs +++ b/crates/sub_protocols/src/generic_logup.rs @@ -422,7 +422,6 @@ pub fn verify_generic_logup( let mut bus_numerators_values = BTreeMap::new(); let mut bus_denominators_values = BTreeMap::new(); let mut columns_values = BTreeMap::new(); - for &(table, log_n_rows) in &tables_heights_sorted { let n_missing_vars = total_gkr_n_vars - log_n_rows; let inner_point = MultilinearPoint(from_end(&point_gkr, log_n_rows).to_vec()); From 441bf49b36b70fed8d0ba7e1426493861bbb8f35 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 13:19:00 +0400 Subject: [PATCH 23/47] wip --- crates/lean_compiler/snark_lib.py | 2 +- .../src/parser/parsers/literal.rs | 3 +- crates/lean_vm/src/core/constants.rs | 5 +- crates/lean_vm/src/execution/runner.rs | 3 +- crates/rec_aggregation/recursion.py | 56 +++++++++---------- crates/rec_aggregation/src/recursion.rs | 18 +++--- crates/rec_aggregation/src/xmss_aggregate.rs | 2 + crates/rec_aggregation/xmss_aggregate.py | 9 ++- 8 files changed, 49 insertions(+), 49 deletions(-) diff --git a/crates/lean_compiler/snark_lib.py b/crates/lean_compiler/snark_lib.py index 827b83c4..dc990f86 100644 --- a/crates/lean_compiler/snark_lib.py +++ b/crates/lean_compiler/snark_lib.py @@ -63,7 +63,7 @@ def pop(self): # Built-in constants ZERO_VEC_PTR = 0 ONE_VEC_PTR = 16 -NONRESERVED_PROGRAM_INPUT_START = 58 +NONRESERVED_PROGRAM_INPUT_START = 60 def poseidon16(left, right, output, mode): diff --git a/crates/lean_compiler/src/parser/parsers/literal.rs b/crates/lean_compiler/src/parser/parsers/literal.rs index 5bcdb0f3..9e0a8586 100644 --- a/crates/lean_compiler/src/parser/parsers/literal.rs +++ b/crates/lean_compiler/src/parser/parsers/literal.rs @@ -1,4 +1,4 @@ -use lean_vm::{NONRESERVED_PROGRAM_INPUT_START, ONE_VEC_PTR, PRIVATE_INPUT_START_PTR, ZERO_VEC_PTR}; +use lean_vm::{NONRESERVED_PROGRAM_INPUT_START, ONE_VEC_PTR, ZERO_VEC_PTR}; use multilinear_toolkit::prelude::*; use super::expression::ExpressionParser; @@ -131,7 +131,6 @@ impl VarOrConstantParser { "NONRESERVED_PROGRAM_INPUT_START" => Ok(SimpleExpr::Constant(ConstExpression::from( NONRESERVED_PROGRAM_INPUT_START, ))), - "PRIVATE_INPUT_START_PTR" => Ok(SimpleExpr::Constant(ConstExpression::from(PRIVATE_INPUT_START_PTR))), "ZERO_VEC_PTR" => Ok(SimpleExpr::Constant(ConstExpression::from(ZERO_VEC_PTR))), "ONE_VEC_PTR" => Ok(SimpleExpr::Constant(ConstExpression::from(ONE_VEC_PTR))), _ => { diff --git a/crates/lean_vm/src/core/constants.rs b/crates/lean_vm/src/core/constants.rs index bf118c4a..7457404a 100644 --- a/crates/lean_vm/src/core/constants.rs +++ b/crates/lean_vm/src/core/constants.rs @@ -49,11 +49,8 @@ pub const EXTENSION_BASIS_PTR: usize = 2 * DIGEST_LEN; /// Convention: pointing to the 16 elements of poseidon_16(0) pub const POSEIDON_16_NULL_HASH_PTR: usize = EXTENSION_BASIS_PTR + DIMENSION.pow(2); -/// Pointer to start of private input -pub const PRIVATE_INPUT_START_PTR: usize = POSEIDON_16_NULL_HASH_PTR + DIGEST_LEN * 2; - /// Normal pointer to start of program input -pub const NONRESERVED_PROGRAM_INPUT_START: usize = PRIVATE_INPUT_START_PTR + 1; +pub const NONRESERVED_PROGRAM_INPUT_START: usize = (POSEIDON_16_NULL_HASH_PTR + DIGEST_LEN * 2).next_multiple_of(DIMENSION); /// The first element of basis corresponds to one pub const ONE_VEC_PTR: usize = EXTENSION_BASIS_PTR; diff --git a/crates/lean_vm/src/execution/runner.rs b/crates/lean_vm/src/execution/runner.rs index d1165707..8493b953 100644 --- a/crates/lean_vm/src/execution/runner.rs +++ b/crates/lean_vm/src/execution/runner.rs @@ -8,7 +8,7 @@ use crate::execution::{ExecutionHistory, Memory}; use crate::isa::Bytecode; use crate::isa::instruction::InstructionContext; use crate::{ - ALL_TABLES, CodeAddress, ENDING_PC, EXTENSION_BASIS_PTR, HintExecutionContext, N_TABLES, PRIVATE_INPUT_START_PTR, + ALL_TABLES, CodeAddress, ENDING_PC, EXTENSION_BASIS_PTR, HintExecutionContext, N_TABLES, STARTING_PC, SourceLocation, Table, TableTrace, }; use multilinear_toolkit::prelude::*; @@ -37,7 +37,6 @@ pub fn build_public_memory(public_input: &[F]) -> Vec { } public_memory[POSEIDON_16_NULL_HASH_PTR..][..2 * DIGEST_LEN].copy_from_slice(&poseidon16_permute([F::ZERO; 16])); - public_memory[PRIVATE_INPUT_START_PTR] = F::from_usize(public_memory_len); public_memory } diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index 689d36ff..68a1f4c6 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -43,21 +43,21 @@ def main(): - mem = 0 - priv_start = mem[PRIVATE_INPUT_START_PTR] + pub_mem = NONRESERVED_PROGRAM_INPUT_START + priv_start = pub_mem[0] proof_size = priv_start[0] - outer_public_memory_log_size = priv_start[1] - outer_public_memory_size = powers_of_two(outer_public_memory_log_size) + inner_public_memory_log_size = priv_start[1] + inner_public_memory_size = powers_of_two(inner_public_memory_log_size) n_recursions = priv_start[2] - outer_public_memory = priv_start + 3 - proofs_start = outer_public_memory + outer_public_memory_size + inner_public_memory = priv_start + 3 + proofs_start = inner_public_memory + inner_public_memory_size for i in range(0, n_recursions): proof_transcript = proofs_start + i * proof_size - recursion(outer_public_memory_log_size, outer_public_memory, proof_transcript) + recursion(inner_public_memory_log_size, inner_public_memory, proof_transcript) return -def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcript): +def recursion(inner_public_memory_log_size, inner_public_memory, proof_transcript): fs: Mut = fs_new(proof_transcript) # table dims @@ -117,15 +117,15 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip bytecode_padded_multilinear_location_prefix = multilinear_location_prefix( offset / powers_of_two(log_bytecode_padded), N_VARS_LOGUP_GKR - log_bytecode_padded, point_gkr ) - NONRESERVED_PROGRAM_INPUT_START_ = NONRESERVED_PROGRAM_INPUT_START - assert NONRESERVED_PROGRAM_INPUT_START_[0] == log_bytecode + log2_ceil(N_INSTRUCTION_COLUMNS) - copy_many_ef(bytecode_and_acc_point, NONRESERVED_PROGRAM_INPUT_START + 1, log_bytecode) + pub_mem = NONRESERVED_PROGRAM_INPUT_START + assert pub_mem[1] == log_bytecode + log2_ceil(N_INSTRUCTION_COLUMNS) + copy_many_ef(bytecode_and_acc_point, pub_mem + 2, log_bytecode) copy_many_ef( logup_alphas + (log2_ceil(MAX_BUS_WIDTH) - log2_ceil(N_INSTRUCTION_COLUMNS)) * DIM, - NONRESERVED_PROGRAM_INPUT_START + 1 + log_bytecode * DIM, + pub_mem + 2 + log_bytecode * DIM, log2_ceil(N_INSTRUCTION_COLUMNS), ) - bytecode_value = NONRESERVED_PROGRAM_INPUT_START + 1 + (log_bytecode + log2_ceil(N_INSTRUCTION_COLUMNS)) * DIM + bytecode_value = pub_mem + 2 + (log_bytecode + log2_ceil(N_INSTRUCTION_COLUMNS)) * DIM bytecode_value_corrected: Mut = bytecode_value for i in unroll(0, log2_ceil(MAX_BUS_WIDTH) - log2_ceil(N_INSTRUCTION_COLUMNS)): bytecode_value_corrected = mul_extension_ret( @@ -427,21 +427,21 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip virtual_col_index = n_up_columns_f + i * DIM + j pcs_values[table_index][last_index_2][virtual_col_index].push(transposed + j * DIM) - # verify the outer public memory is well constructed (with the conventions) - for i in unroll(0, next_multiple_of(NONRESERVED_PROGRAM_INPUT_START, DIM) / DIM): - copy_5(i * DIM, outer_public_memory + i * DIM) + # verify the inner public memory is well constructed (with the conventions) (NONRESERVED_PROGRAM_INPUT_START is a multiple of DIM) + for i in unroll(0, NONRESERVED_PROGRAM_INPUT_START / DIM): + copy_5(i * DIM, inner_public_memory + i * DIM) - public_memory_random_point = Array(outer_public_memory_log_size * DIM) - for i in range(0, outer_public_memory_log_size): + public_memory_random_point = Array(inner_public_memory_log_size * DIM) + for i in range(0, inner_public_memory_log_size): copy_5(fs_sample_ef(fs), public_memory_random_point + i * DIM) fs = duplexing(fs) - poly_eq_public_mem = poly_eq_extension_dynamic(public_memory_random_point, outer_public_memory_log_size) + poly_eq_public_mem = poly_eq_extension_dynamic(public_memory_random_point, inner_public_memory_log_size) public_memory_eval = Array(DIM) dot_product_be_dynamic( - outer_public_memory, + inner_public_memory, poly_eq_public_mem, public_memory_eval, - powers_of_two(outer_public_memory_log_size), + powers_of_two(inner_public_memory_log_size), ) # WHIR BASE @@ -512,12 +512,12 @@ def recursion(outer_public_memory_log_size, outer_public_memory, proof_transcrip curr_randomness += DIM eq_pub_mem = eq_mle_extension( - folding_randomness_global + (WHIR_N_VARS - outer_public_memory_log_size) * DIM, + folding_randomness_global + (WHIR_N_VARS - inner_public_memory_log_size) * DIM, public_memory_random_point, - outer_public_memory_log_size, + inner_public_memory_log_size, ) prefix_pub_mem = multilinear_location_prefix( - 0, WHIR_N_VARS - outer_public_memory_log_size, folding_randomness_global + 0, WHIR_N_VARS - inner_public_memory_log_size, folding_randomness_global ) s = add_extension_ret( s, @@ -681,16 +681,16 @@ def verify_gkr_quotient_step(fs: Mut, n_vars, point, claim_num, claim_den): return fs, postponed_point, new_claim_num, new_claim_den -def evaluate_air_constraints(table_index, inner_evals, air_alpha_powers, bus_beta, bus_alpha_powers): +def evaluate_air_constraints(table_index, inner_evals, air_alpha_powers, bus_beta, logup_alphas_eq_poly): res: Imu debug_assert(table_index < 3) match table_index: case 0: - res = evaluate_air_constraints_table_0(inner_evals, air_alpha_powers, bus_beta, bus_alpha_powers) + res = evaluate_air_constraints_table_0(inner_evals, air_alpha_powers, bus_beta, logup_alphas_eq_poly) case 1: - res = evaluate_air_constraints_table_1(inner_evals, air_alpha_powers, bus_beta, bus_alpha_powers) + res = evaluate_air_constraints_table_1(inner_evals, air_alpha_powers, bus_beta, logup_alphas_eq_poly) case 2: - res = evaluate_air_constraints_table_2(inner_evals, air_alpha_powers, bus_beta, bus_alpha_powers) + res = evaluate_air_constraints_table_2(inner_evals, air_alpha_powers, bus_beta, logup_alphas_eq_poly) return res diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index fd9e8cbd..92f44aec 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -312,14 +312,16 @@ def main(): .iter() .flat_map(|c| c.as_basis_coefficients_slice()), ); - outer_public_input.extend(dbg!(verif_details.bytecode_evaluation.value.as_basis_coefficients_slice())); - let outer_public_memory = build_public_memory(&outer_public_input); + outer_public_input.extend(verif_details.bytecode_evaluation.value.as_basis_coefficients_slice()); + let outer_private_input_start = (NONRESERVED_PROGRAM_INPUT_START + 1 + outer_public_input.len()).next_power_of_two(); + outer_public_input.insert(0, F::from_usize(outer_private_input_start)); + let inner_public_memory = build_public_memory(&inner_public_input); let mut outer_private_input = vec![ F::from_usize(proof_to_prove.proof.len()), - F::from_usize(log2_strict_usize(outer_public_memory.len())), + F::from_usize(log2_strict_usize(inner_public_memory.len())), F::from_usize(count), ]; - outer_private_input.extend(outer_public_memory); + outer_private_input.extend(inner_public_memory); for _ in 0..count { outer_private_input.extend(proof_to_prove.proof.to_vec()); } @@ -437,7 +439,7 @@ where let mut cache: HashMap<*const (), String> = HashMap::new(); let mut res = format!( - "def evaluate_air_constraints_table_{}({}, air_alpha_powers, bus_beta, bus_alpha_powers):\n", + "def evaluate_air_constraints_table_{}({}, air_alpha_powers, bus_beta, logup_alphas_eq_poly):\n", table.table().index(), AIR_INNER_VALUES_VAR ); @@ -464,13 +466,15 @@ where res += &format!("\n copy_5({}, buff + DIM * {})", data_str, i); } res += &format!( - "\n bus_res: Mut = dot_product_ret(buff, bus_alpha_powers + DIM, {}, EE)", + "\n bus_res: Mut = dot_product_ret(buff, logup_alphas_eq_poly, {}, EE)", bus_data.len() ); - res += &format!("\n bus_res = add_extension_ret({}, bus_res)", table_index); + res += &format!("\n bus_res = add_extension_ret(mul_extension_ret({}, logup_alphas_eq_poly + {} * DIM), bus_res)", table_index, max_bus_width().next_power_of_two() - 1); res += "\n bus_res = mul_extension_ret(bus_res, bus_beta)"; res += &format!("\n sum: Mut = add_extension_ret(bus_res, {})", flag); + println!("AIR constraints for table {}: {}", table.table().index(), res); + for (index, constraint_eval) in constraints_evals.iter().enumerate() { res += format!( "\n sum = add_extension_ret(sum, mul_extension_ret(air_alpha_powers + {} * DIM, {}))", diff --git a/crates/rec_aggregation/src/xmss_aggregate.rs b/crates/rec_aggregation/src/xmss_aggregate.rs index 8c870406..ee03f0c3 100644 --- a/crates/rec_aggregation/src/xmss_aggregate.rs +++ b/crates/rec_aggregation/src/xmss_aggregate.rs @@ -40,6 +40,8 @@ fn build_public_input(xmss_pub_keys: &[XmssPublicKey], message_hash: [F; 8], slo public_input.push(acc); acc += F::from_usize((1 + V + pk.log_lifetime) * DIGEST_LEN); // size of the signature } + let private_input_start = (NONRESERVED_PROGRAM_INPUT_START + 1 + public_input.len()).next_power_of_two(); + public_input.insert(0, F::from_usize(private_input_start)); public_input } diff --git a/crates/rec_aggregation/xmss_aggregate.py b/crates/rec_aggregation/xmss_aggregate.py index ea6101c5..0a99895f 100644 --- a/crates/rec_aggregation/xmss_aggregate.py +++ b/crates/rec_aggregation/xmss_aggregate.py @@ -18,16 +18,15 @@ def main(): - NONRESERVED_PROGRAM_INPUT_START_ = NONRESERVED_PROGRAM_INPUT_START - n_signatures = NONRESERVED_PROGRAM_INPUT_START_[0] - message_hash = NONRESERVED_PROGRAM_INPUT_START + 1 + pub_mem = NONRESERVED_PROGRAM_INPUT_START + signatures_start = pub_mem[0] + n_signatures = pub_mem[1] + message_hash = pub_mem + 2 all_public_keys = message_hash + VECTOR_LEN all_log_lifetimes = all_public_keys + n_signatures * VECTOR_LEN all_merkle_indexes = all_log_lifetimes + n_signatures sig_sizes = all_merkle_indexes + n_signatures * MAX_LOG_LIFETIME - mem = 0 - signatures_start = mem[PRIVATE_INPUT_START_PTR] for i in range(0, n_signatures): xmss_public_key = all_public_keys + i * VECTOR_LEN From 0c6ed63a524b3c8a21c0fdcfa21ef38cbf518be3 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 13:21:24 +0400 Subject: [PATCH 24/47] wip --- crates/rec_aggregation/recursion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index 68a1f4c6..9aae63e8 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -540,7 +540,7 @@ def recursion(inner_public_memory_log_size, inner_public_memory, proof_transcrip mul_extension_ret(mul_extension_ret(curr_randomness, prefix_bytecode_acc), eq_bytecode_acc), ) curr_randomness += DIM - offset += 2**log2_ceil(GUEST_BYTECODE_LEN) + offset += powers_of_two(log_bytecode_padded) prefix_pc_start = multilinear_location_prefix( offset + COL_PC * powers_of_two(log_n_cycles), From 098182928bac7599b117decd4fe108d8707ef97b Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 13:26:06 +0400 Subject: [PATCH 25/47] w --- crates/rec_aggregation/recursion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index 9aae63e8..ce234954 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -533,7 +533,7 @@ def recursion(inner_public_memory_log_size, inner_public_memory, proof_transcrip log2_ceil(GUEST_BYTECODE_LEN), ) prefix_bytecode_acc = multilinear_location_prefix( - 0, WHIR_N_VARS - log2_ceil(GUEST_BYTECODE_LEN), folding_randomness_global + offset / 2**log2_ceil(GUEST_BYTECODE_LEN), WHIR_N_VARS - log2_ceil(GUEST_BYTECODE_LEN), folding_randomness_global ) s = add_extension_ret( s, From 87f49d1d9e197a19db7c68c916ea0ceec896300f Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 13:27:07 +0400 Subject: [PATCH 26/47] w --- crates/rec_aggregation/src/recursion.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index 92f44aec..be21adef 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -473,8 +473,6 @@ where res += "\n bus_res = mul_extension_ret(bus_res, bus_beta)"; res += &format!("\n sum: Mut = add_extension_ret(bus_res, {})", flag); - println!("AIR constraints for table {}: {}", table.table().index(), res); - for (index, constraint_eval) in constraints_evals.iter().enumerate() { res += format!( "\n sum = add_extension_ret(sum, mul_extension_ret(air_alpha_powers + {} * DIM, {}))", From 2f7bc2a41cc2953e9d33dac43af4f6112c2519af Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 13:40:44 +0400 Subject: [PATCH 27/47] w --- crates/lean_prover/src/verify_execution.rs | 6 +++--- crates/rec_aggregation/src/recursion.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index 0d012f12..1223b505 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -12,7 +12,7 @@ pub struct ProofVerificationDetails { pub log_memory: usize, pub table_n_vars: BTreeMap, pub first_quotient_gkr_n_vars: usize, - pub total_whir_statements_base: usize, + pub total_whir_statements: usize, pub bytecode_evaluation: Evaluation, } @@ -149,7 +149,7 @@ pub fn verify_execution( &table_n_vars, &committed_statements, ); - let total_whir_statements_base = global_statements_base.iter().map(|s| s.values.len()).sum(); + let total_whir_statements = global_statements_base.iter().map(|s| s.values.len()).sum(); WhirConfig::new(&whir_config, parsed_commitment.num_variables).verify( &mut verifier_state, &parsed_commitment, @@ -160,7 +160,7 @@ pub fn verify_execution( log_memory, table_n_vars, first_quotient_gkr_n_vars: logup_statements.total_gkr_n_vars, - total_whir_statements_base, + total_whir_statements, bytecode_evaluation: logup_statements.bytecode_evaluation.unwrap(), }) } diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index be21adef..5e109e66 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -298,7 +298,7 @@ def main(): ); replacements.insert( "TOTAL_WHIR_STATEMENTS_PLACEHOLDER".to_string(), - verif_details.total_whir_statements_base.to_string(), + verif_details.total_whir_statements.to_string(), ); replacements.insert("STARTING_PC_PLACEHOLDER".to_string(), STARTING_PC.to_string()); replacements.insert("ENDING_PC_PLACEHOLDER".to_string(), ENDING_PC.to_string()); From 1c94a4cf986b7c3aa90f335e28578dc2d4f50c75 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 13:59:25 +0400 Subject: [PATCH 28/47] superb --- crates/rec_aggregation/recursion.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index ce234954..a3dc7fdc 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -581,12 +581,7 @@ def recursion(inner_public_memory_log_size, inner_public_memory, proof_transcrip mul_extension_ret(mul_extension_ret(curr_randomness, prefix), eq_factor), ) curr_randomness += DIM - num_committed_cols: Imu - if table_index == EXECUTION_TABLE_INDEX: - num_committed_cols = N_COMMITTED_EXEC_COLUMNS - else: - num_committed_cols = total_num_cols - offset += n_rows * num_committed_cols + offset += n_rows * total_num_cols copy_5(mul_extension_ret(s, final_value), end_sum) From 70274f207f8fe8b7ecc527fdcdfad5227db38bef Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 14:00:57 +0400 Subject: [PATCH 29/47] fmt --- crates/lean_compiler/src/c_compile_final.rs | 5 +--- crates/lean_prover/src/prove_execution.rs | 4 ++-- crates/lean_prover/src/verify_execution.rs | 4 ++-- crates/lean_vm/src/core/constants.rs | 3 ++- crates/lean_vm/src/execution/runner.rs | 4 ++-- crates/rec_aggregation/recursion.py | 26 +++++++++++++-------- crates/rec_aggregation/src/recursion.rs | 9 +++++-- crates/rec_aggregation/xmss_aggregate.py | 1 - crates/sub_protocols/src/generic_logup.rs | 2 +- 9 files changed, 33 insertions(+), 25 deletions(-) diff --git a/crates/lean_compiler/src/c_compile_final.rs b/crates/lean_compiler/src/c_compile_final.rs index e3c9ce81..1583e40e 100644 --- a/crates/lean_compiler/src/c_compile_final.rs +++ b/crates/lean_compiler/src/c_compile_final.rs @@ -144,10 +144,7 @@ pub fn compile_to_low_level_bytecode( &mut hints, ); } - let instructions_encoded = instructions - .par_iter() - .map(|instr| field_representation(instr)) - .collect::>(); + let instructions_encoded = instructions.par_iter().map(field_representation).collect::>(); let mut instructions_multilinear = vec![]; for instr in &instructions_encoded { diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index 2d38df20..6854b492 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -91,7 +91,7 @@ pub fn prove_execution( // 1st Commitment let packed_pcs_witness = packed_pcs_commit( &mut prover_state, - &whir_config, + whir_config, &memory, &memory_acc, &bytecode_acc, @@ -186,7 +186,7 @@ pub fn prove_execution( &committed_statements, ); - WhirConfig::new(&whir_config, packed_pcs_witness.packed_polynomial.by_ref().n_vars()).prove( + WhirConfig::new(whir_config, packed_pcs_witness.packed_polynomial.by_ref().n_vars()).prove( &mut prover_state, global_statements_base, packed_pcs_witness.inner_witness, diff --git a/crates/lean_prover/src/verify_execution.rs b/crates/lean_prover/src/verify_execution.rs index 1223b505..43158d6d 100644 --- a/crates/lean_prover/src/verify_execution.rs +++ b/crates/lean_prover/src/verify_execution.rs @@ -58,7 +58,7 @@ pub fn verify_execution( } let parsed_commitment = packed_pcs_parse_commitment( - &whir_config, + whir_config, &mut verifier_state, log_memory, bytecode.log_size(), @@ -150,7 +150,7 @@ pub fn verify_execution( &committed_statements, ); let total_whir_statements = global_statements_base.iter().map(|s| s.values.len()).sum(); - WhirConfig::new(&whir_config, parsed_commitment.num_variables).verify( + WhirConfig::new(whir_config, parsed_commitment.num_variables).verify( &mut verifier_state, &parsed_commitment, global_statements_base, diff --git a/crates/lean_vm/src/core/constants.rs b/crates/lean_vm/src/core/constants.rs index 7457404a..65588bd4 100644 --- a/crates/lean_vm/src/core/constants.rs +++ b/crates/lean_vm/src/core/constants.rs @@ -50,7 +50,8 @@ pub const EXTENSION_BASIS_PTR: usize = 2 * DIGEST_LEN; pub const POSEIDON_16_NULL_HASH_PTR: usize = EXTENSION_BASIS_PTR + DIMENSION.pow(2); /// Normal pointer to start of program input -pub const NONRESERVED_PROGRAM_INPUT_START: usize = (POSEIDON_16_NULL_HASH_PTR + DIGEST_LEN * 2).next_multiple_of(DIMENSION); +pub const NONRESERVED_PROGRAM_INPUT_START: usize = + (POSEIDON_16_NULL_HASH_PTR + DIGEST_LEN * 2).next_multiple_of(DIMENSION); /// The first element of basis corresponds to one pub const ONE_VEC_PTR: usize = EXTENSION_BASIS_PTR; diff --git a/crates/lean_vm/src/execution/runner.rs b/crates/lean_vm/src/execution/runner.rs index 8493b953..2fb41b5a 100644 --- a/crates/lean_vm/src/execution/runner.rs +++ b/crates/lean_vm/src/execution/runner.rs @@ -8,8 +8,8 @@ use crate::execution::{ExecutionHistory, Memory}; use crate::isa::Bytecode; use crate::isa::instruction::InstructionContext; use crate::{ - ALL_TABLES, CodeAddress, ENDING_PC, EXTENSION_BASIS_PTR, HintExecutionContext, N_TABLES, - STARTING_PC, SourceLocation, Table, TableTrace, + ALL_TABLES, CodeAddress, ENDING_PC, EXTENSION_BASIS_PTR, HintExecutionContext, N_TABLES, STARTING_PC, + SourceLocation, Table, TableTrace, }; use multilinear_toolkit::prelude::*; use std::collections::{BTreeMap, BTreeSet}; diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index a3dc7fdc..38c5da7a 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -112,7 +112,7 @@ def recursion(inner_public_memory_log_size, inner_public_memory, proof_transcrip log_bytecode_padded = maximum(log_bytecode, log_n_cycles) bytecode_and_acc_point = point_gkr + (N_VARS_LOGUP_GKR - log_bytecode) * DIM bytecode_multilinear_location_prefix = multilinear_location_prefix( - offset / 2**log2_ceil(GUEST_BYTECODE_LEN), N_VARS_LOGUP_GKR - log_bytecode, point_gkr + offset / 2 ** log2_ceil(GUEST_BYTECODE_LEN), N_VARS_LOGUP_GKR - log_bytecode, point_gkr ) bytecode_padded_multilinear_location_prefix = multilinear_location_prefix( offset / powers_of_two(log_bytecode_padded), N_VARS_LOGUP_GKR - log_bytecode_padded, point_gkr @@ -162,7 +162,7 @@ def recursion(inner_public_memory_log_size, inner_public_memory, proof_transcrip bytecode_padded_multilinear_location_prefix, mle_of_zeros_then_ones( point_gkr + (N_VARS_LOGUP_GKR - log_bytecode_padded) * DIM, - 2**log2_ceil(GUEST_BYTECODE_LEN), + 2 ** log2_ceil(GUEST_BYTECODE_LEN), log_bytecode_padded, ), ), @@ -190,7 +190,6 @@ def recursion(inner_public_memory_log_size, inner_public_memory, proof_transcrip inner_point = point_gkr + (N_VARS_LOGUP_GKR - log_n_rows) * DIM pcs_points[table_index].push(inner_point) - if table_index == EXECUTION_TABLE_INDEX: # 0] Bytecode lookup bytecode_prefix = multilinear_location_prefix(offset / n_rows, N_VARS_LOGUP_GKR - log_n_rows, point_gkr) @@ -204,9 +203,9 @@ def recursion(inner_public_memory_log_size, inner_public_memory, proof_transcrip retrieved_numerators_value = add_extension_ret(retrieved_numerators_value, bytecode_prefix) fingerp = fingerprint_bytecode(instr_evals, eval_on_pc, logup_alphas_eq_poly) retrieved_denominators_value = add_extension_ret( - retrieved_denominators_value, - mul_extension_ret(bytecode_prefix, sub_extension_ret(logup_c, fingerp)), - ) + retrieved_denominators_value, + mul_extension_ret(bytecode_prefix, sub_extension_ret(logup_c, fingerp)), + ) offset += n_rows prefix = multilinear_location_prefix(offset / n_rows, N_VARS_LOGUP_GKR - log_n_rows, point_gkr) @@ -533,7 +532,9 @@ def recursion(inner_public_memory_log_size, inner_public_memory, proof_transcrip log2_ceil(GUEST_BYTECODE_LEN), ) prefix_bytecode_acc = multilinear_location_prefix( - offset / 2**log2_ceil(GUEST_BYTECODE_LEN), WHIR_N_VARS - log2_ceil(GUEST_BYTECODE_LEN), folding_randomness_global + offset / 2 ** log2_ceil(GUEST_BYTECODE_LEN), + WHIR_N_VARS - log2_ceil(GUEST_BYTECODE_LEN), + folding_randomness_global, ) s = add_extension_ret( s, @@ -601,17 +602,22 @@ def fingerprint_2(table_index, data_1, data_2, logup_alphas_eq_poly): copy_5(data_1, buff) copy_5(data_2, buff + DIM) res: Mut = dot_product_ret(buff, logup_alphas_eq_poly, 2, EE) - res = add_extension_ret(res, mul_base_extension_ret(table_index, logup_alphas_eq_poly + (2**log2_ceil(MAX_BUS_WIDTH) - 1) * DIM)) + res = add_extension_ret( + res, mul_base_extension_ret(table_index, logup_alphas_eq_poly + (2 ** log2_ceil(MAX_BUS_WIDTH) - 1) * DIM) + ) return res + def fingerprint_bytecode(instr_evals, eval_on_pc, logup_alphas_eq_poly): res: Mut = dot_product_ret(instr_evals, logup_alphas_eq_poly, N_INSTRUCTION_COLUMNS, EE) res = add_extension_ret(res, mul_extension_ret(eval_on_pc, logup_alphas_eq_poly + N_INSTRUCTION_COLUMNS * DIM)) - res = add_extension_ret(res, mul_base_extension_ret(BYTECODE_TABLE_INDEX, logup_alphas_eq_poly + (2**log2_ceil(MAX_BUS_WIDTH) - 1) * DIM)) + res = add_extension_ret( + res, + mul_base_extension_ret(BYTECODE_TABLE_INDEX, logup_alphas_eq_poly + (2 ** log2_ceil(MAX_BUS_WIDTH) - 1) * DIM), + ) return res - def verify_gkr_quotient(fs: Mut, n_vars): fs, nums = fs_receive_ef(fs, 2) fs, denoms = fs_receive_ef(fs, 2) diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index 5e109e66..918eaaaf 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -313,7 +313,8 @@ def main(): .flat_map(|c| c.as_basis_coefficients_slice()), ); outer_public_input.extend(verif_details.bytecode_evaluation.value.as_basis_coefficients_slice()); - let outer_private_input_start = (NONRESERVED_PROGRAM_INPUT_START + 1 + outer_public_input.len()).next_power_of_two(); + let outer_private_input_start = + (NONRESERVED_PROGRAM_INPUT_START + 1 + outer_public_input.len()).next_power_of_two(); outer_public_input.insert(0, F::from_usize(outer_private_input_start)); let inner_public_memory = build_public_memory(&inner_public_input); let mut outer_private_input = vec![ @@ -469,7 +470,11 @@ where "\n bus_res: Mut = dot_product_ret(buff, logup_alphas_eq_poly, {}, EE)", bus_data.len() ); - res += &format!("\n bus_res = add_extension_ret(mul_extension_ret({}, logup_alphas_eq_poly + {} * DIM), bus_res)", table_index, max_bus_width().next_power_of_two() - 1); + res += &format!( + "\n bus_res = add_extension_ret(mul_extension_ret({}, logup_alphas_eq_poly + {} * DIM), bus_res)", + table_index, + max_bus_width().next_power_of_two() - 1 + ); res += "\n bus_res = mul_extension_ret(bus_res, bus_beta)"; res += &format!("\n sum: Mut = add_extension_ret(bus_res, {})", flag); diff --git a/crates/rec_aggregation/xmss_aggregate.py b/crates/rec_aggregation/xmss_aggregate.py index 0a99895f..51fb0ff7 100644 --- a/crates/rec_aggregation/xmss_aggregate.py +++ b/crates/rec_aggregation/xmss_aggregate.py @@ -27,7 +27,6 @@ def main(): all_merkle_indexes = all_log_lifetimes + n_signatures sig_sizes = all_merkle_indexes + n_signatures * MAX_LOG_LIFETIME - for i in range(0, n_signatures): xmss_public_key = all_public_keys + i * VECTOR_LEN signature = signatures_start + sig_sizes[i] diff --git a/crates/sub_protocols/src/generic_logup.rs b/crates/sub_protocols/src/generic_logup.rs index 7acf810a..d692b2c7 100644 --- a/crates/sub_protocols/src/generic_logup.rs +++ b/crates/sub_protocols/src/generic_logup.rs @@ -414,7 +414,7 @@ pub fn verify_generic_logup( + *alphas_eq_poly.last().unwrap() * F::from_usize(BYTECODE_TABLE_INDEX))); // Padding for bytecode retrieved_denominators_value += - pref_padded * mle_of_zeros_then_ones(1 << log_bytecode, &from_end(&point_gkr, log_bytecode_padded)); + pref_padded * mle_of_zeros_then_ones(1 << log_bytecode, from_end(&point_gkr, log_bytecode_padded)); offset += 1 << log_bytecode_padded; // ... Rest of the tables: From b38e117a796cf4fc01d9564813493fbe30c5c6be Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 14:09:07 +0400 Subject: [PATCH 30/47] wip --- crates/lean_prover/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/lean_prover/src/lib.rs b/crates/lean_prover/src/lib.rs index 5b7736c3..e795baa1 100644 --- a/crates/lean_prover/src/lib.rs +++ b/crates/lean_prover/src/lib.rs @@ -16,14 +16,14 @@ use trace_gen::*; // Right now, hash digests = 8 koala-bear (p = 2^31 - 2^24 + 1, i.e. ≈ 30.98 bits per field element) // so ≈ 123.92 bits of security against collisions -pub const SECURITY_BITS: usize = 123; // TODO 128 bits security (with Poseidon over 20 field elements) +pub const SECURITY_BITS: usize = 123; // TODO 128 bits security? (with Poseidon over 20 field elements or with a more subtle soundness analysis (cf. https://eprint.iacr.org/2021/188.pdf)) // Provable security (no proximity gaps conjectures) pub const SECURITY_REGIME: SecurityAssumption = SecurityAssumption::JohnsonBound; pub const GRINDING_BITS: usize = 16; -pub const STARTING_LOG_INV_RATE_BASE: usize = 2; +pub const STARTING_LOG_INV_RATE: usize = 2; pub fn default_whir_config() -> WhirConfigBuilder { WhirConfigBuilder { @@ -33,6 +33,6 @@ pub fn default_whir_config() -> WhirConfigBuilder { max_num_variables_to_send_coeffs: 6, rs_domain_initial_reduction_factor: 5, security_level: SECURITY_BITS, - starting_log_inv_rate: STARTING_LOG_INV_RATE_BASE, + starting_log_inv_rate: STARTING_LOG_INV_RATE, } } From bce9c7d1be69f04b0225b8fd9c00816f53e0470b Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 16:25:08 +0400 Subject: [PATCH 31/47] w --- crates/sub_protocols/src/generic_logup.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/sub_protocols/src/generic_logup.rs b/crates/sub_protocols/src/generic_logup.rs index d692b2c7..84eb3ce0 100644 --- a/crates/sub_protocols/src/generic_logup.rs +++ b/crates/sub_protocols/src/generic_logup.rs @@ -2,6 +2,7 @@ use crate::{prove_gkr_quotient, verify_gkr_quotient}; use lean_vm::*; use multilinear_toolkit::prelude::*; use std::collections::BTreeMap; +use tracing::instrument; use utils::*; #[derive(Debug, PartialEq, Hash, Clone)] @@ -21,6 +22,7 @@ pub struct GenericLogupStatements { } #[allow(clippy::too_many_arguments)] +#[instrument(skip_all)] pub fn prove_generic_logup( prover_state: &mut impl FSProver, c: EF, From 2a07834915603a6aabd6e9a6af2cf5c0b9b58bc8 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 2 Feb 2026 17:24:27 +0400 Subject: [PATCH 32/47] wip --- crates/rec_aggregation/src/recursion.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index 918eaaaf..68103c0d 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -15,9 +15,6 @@ use multilinear_toolkit::prelude::*; use utils::{BYTECODE_TABLE_INDEX, Counter, MEMORY_TABLE_INDEX}; pub fn run_recursion_benchmark(count: usize, tracing: bool) { - if tracing { - utils::init_tracing(); - } let filepath = Path::new(env!("CARGO_MANIFEST_DIR")) .join("recursion.py") .to_str() @@ -330,6 +327,9 @@ def main(): let recursion_bytecode = compile_program_with_flags(&ProgramSource::Filepath(filepath), CompilationFlags { replacements }); + if tracing { + utils::init_tracing(); + } let time = Instant::now(); let recursion_proof = prove_execution( From 64b6d125e17a8b38902c539c7ad69580241dfa06 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 3 Feb 2026 10:39:49 +0400 Subject: [PATCH 33/47] 100K cycles opti --- crates/rec_aggregation/recursion.py | 2 - crates/rec_aggregation/utils.py | 73 ++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/crates/rec_aggregation/recursion.py b/crates/rec_aggregation/recursion.py index 38c5da7a..82bd8885 100644 --- a/crates/rec_aggregation/recursion.py +++ b/crates/rec_aggregation/recursion.py @@ -586,8 +586,6 @@ def recursion(inner_public_memory_log_size, inner_public_memory, proof_transcrip copy_5(mul_extension_ret(s, final_value), end_sum) - # Last TODO = Opening on the guest bytecode, but there are multiple ways to handle this - return diff --git a/crates/rec_aggregation/utils.py b/crates/rec_aggregation/utils.py index 41f84383..41ef9594 100644 --- a/crates/rec_aggregation/utils.py +++ b/crates/rec_aggregation/utils.py @@ -735,17 +735,86 @@ def checked_decompose_bits(a, k): return bits, partial_sum -def checked_decompose_bits_small_value(to_decompose, n_bits): +def checked_decompose_bits_small_value_const(to_decompose, n_bits: Const): bits = Array(n_bits) hint_decompose_bits(to_decompose, bits, n_bits, BIG_ENDIAN) sum: Mut = bits[n_bits - 1] power_of_2: Mut = 1 - for i in range(1, n_bits): + for i in unroll(1, n_bits): power_of_2 *= 2 sum += bits[n_bits - 1 - i] * power_of_2 assert to_decompose == sum return bits +@inline +def checked_decompose_bits_small_value(to_decompose, n_bits): + res : Imu + debug_assert(n_bits < 30) + debug_assert(0 < n_bits) + match n_bits: + case 0: + _ = 0 # unreachable + case 1: + res = checked_decompose_bits_small_value_const(to_decompose, 1) + case 2: + res = checked_decompose_bits_small_value_const(to_decompose, 2) + case 3: + res = checked_decompose_bits_small_value_const(to_decompose, 3) + case 4: + res = checked_decompose_bits_small_value_const(to_decompose, 4) + case 5: + res = checked_decompose_bits_small_value_const(to_decompose, 5) + case 6: + res = checked_decompose_bits_small_value_const(to_decompose, 6) + case 7: + res = checked_decompose_bits_small_value_const(to_decompose, 7) + case 8: + res = checked_decompose_bits_small_value_const(to_decompose, 8) + case 9: + res = checked_decompose_bits_small_value_const(to_decompose, 9) + case 10: + res = checked_decompose_bits_small_value_const(to_decompose, 10) + case 11: + res = checked_decompose_bits_small_value_const(to_decompose, 11) + case 12: + res = checked_decompose_bits_small_value_const(to_decompose, 12) + case 13: + res = checked_decompose_bits_small_value_const(to_decompose, 13) + case 14: + res = checked_decompose_bits_small_value_const(to_decompose, 14) + case 15: + res = checked_decompose_bits_small_value_const(to_decompose, 15) + case 16: + res = checked_decompose_bits_small_value_const(to_decompose, 16) + case 17: + res = checked_decompose_bits_small_value_const(to_decompose, 17) + case 18: + res = checked_decompose_bits_small_value_const(to_decompose, 18) + case 19: + res = checked_decompose_bits_small_value_const(to_decompose, 19) + case 20: + res = checked_decompose_bits_small_value_const(to_decompose, 20) + case 21: + res = checked_decompose_bits_small_value_const(to_decompose, 21) + case 22: + res = checked_decompose_bits_small_value_const(to_decompose, 22) + case 23: + res = checked_decompose_bits_small_value_const(to_decompose, 23) + case 24: + res = checked_decompose_bits_small_value_const(to_decompose, 24) + case 25: + res = checked_decompose_bits_small_value_const(to_decompose, 25) + case 26: + res = checked_decompose_bits_small_value_const(to_decompose, 26) + case 27: + res = checked_decompose_bits_small_value_const(to_decompose, 27) + case 28: + res = checked_decompose_bits_small_value_const(to_decompose, 28) + case 29: + res = checked_decompose_bits_small_value_const(to_decompose, 29) + return res + + @inline def dot_product_ret(a, b, n, mode): From 7c9be508963c4f083b18ab498d7defe13b42f06a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 3 Feb 2026 10:52:00 +0400 Subject: [PATCH 34/47] wip --- crates/rec_aggregation/fiat_shamir.snark.py | 111 --- crates/rec_aggregation/utils.snark.py | 830 -------------------- 2 files changed, 941 deletions(-) delete mode 100644 crates/rec_aggregation/fiat_shamir.snark.py delete mode 100644 crates/rec_aggregation/utils.snark.py diff --git a/crates/rec_aggregation/fiat_shamir.snark.py b/crates/rec_aggregation/fiat_shamir.snark.py deleted file mode 100644 index e2f189b7..00000000 --- a/crates/rec_aggregation/fiat_shamir.snark.py +++ /dev/null @@ -1,111 +0,0 @@ -# FIAT SHAMIR layout: 17 field elements -# 0..8 -> first half of sponge state -# 8..16 -> second half of sponge state -# 16 -> transcript pointer - -from utils import * - - -def fs_new(transcript_ptr): - fs_state = Array(17) - set_to_16_zeros(fs_state) - fs_state[16] = transcript_ptr - duplexed = duplexing(fs_state) - return duplexed - - -def duplexing(fs): - new_fs = Array(17) - poseidon16(fs, fs + 8, new_fs, PERMUTATION) - new_fs[16] = fs[16] - return new_fs - - -def fs_grinding(fs, bits): - if bits == 0: - return fs # no grinding - left = Array(8) - grinding_witness = read_memory(fs[16]) - left[0] = grinding_witness - set_to_7_zeros(left + 1) - - fs_after_poseidon = Array(17) - poseidon16(left, fs + 8, fs_after_poseidon, PERMUTATION) - fs_after_poseidon[16] = fs[16] + 1 # one element read from transcript - - sampled = fs_after_poseidon[0] - _, sampled_low_bits_value = checked_decompose_bits(sampled, bits) - assert sampled_low_bits_value == 0 - - fs_duplexed = duplexing(fs_after_poseidon) - - return fs_duplexed - - -def fs_sample_ef(fs): - return fs - - -def fs_hint(fs, n): - # return the updated fiat-shamir, and a pointer to n field elements from the transcript - - transcript_ptr = fs[16] - new_fs = Array(17) - copy_16(fs, new_fs) - new_fs[16] = fs[16] + n # advance transcript pointer - return new_fs, transcript_ptr - - -def fs_receive_chunks(fs, n_chunks: Const): - # each chunk = 8 field elements - new_fs = Array(1 + 16 * n_chunks) - transcript_ptr = fs[16] - new_fs[16 * n_chunks] = transcript_ptr + 8 * n_chunks # advance transcript pointer - - poseidon16(transcript_ptr, fs + 8, new_fs, PERMUTATION) - for i in unroll(1, n_chunks): - poseidon16( - transcript_ptr + i * 8, - new_fs + ((i - 1) * 16 + 8), - new_fs + i * 16, - PERMUTATION, - ) - return new_fs + 16 * (n_chunks - 1), transcript_ptr - - -def fs_receive_ef(fs, n: Const): - new_fs, ef_ptr = fs_receive_chunks(fs, next_multiple_of(n * DIM, 8) / 8) - for i in unroll(n * DIM, next_multiple_of(n * DIM, 8)): - assert ef_ptr[i] == 0 - return new_fs, ef_ptr - - -def fs_print_state(fs_state): - for i in unroll(0, 17): - print(i, fs_state[i]) - return - - -def sample_bits_const(fs: Mut, n_samples: Const, K): - # return the updated fiat-shamir, and a pointer to n pointers, each pointing to 31 (boolean) field elements, - sampled_bits = Array(n_samples) - for i in unroll(0, (next_multiple_of(n_samples, 8) / 8) - 1): - for j in unroll(0, 8): - bits, _ = checked_decompose_bits(fs[j], K) - sampled_bits[i * 8 + j] = bits - fs = duplexing(fs) - # Last batch (may be partial) - for j in unroll(0, 8 - ((8 - (n_samples % 8)) % 8)): - bits, _ = checked_decompose_bits(fs[j], K) - sampled_bits[((next_multiple_of(n_samples, 8) / 8) - 1) * 8 + j] = bits - return duplexing(fs), sampled_bits - - -def sample_bits_dynamic(fs_state, n_samples, K): - new_fs_state: Imu - sampled_bits: Imu - for r in unroll(0, WHIR_N_ROUNDS + 1): - if n_samples == WHIR_NUM_QUERIES[r]: - new_fs_state, sampled_bits = sample_bits_const(fs_state, WHIR_NUM_QUERIES[r], K) - return new_fs_state, sampled_bits - assert False, "sample_bits_dynamic called with unsupported n_samples" diff --git a/crates/rec_aggregation/utils.snark.py b/crates/rec_aggregation/utils.snark.py deleted file mode 100644 index 255f12b5..00000000 --- a/crates/rec_aggregation/utils.snark.py +++ /dev/null @@ -1,830 +0,0 @@ -from hashing import * - -F_BITS = 31 # koala-bear = 31 bits - -TWO_ADICITY = 24 -ROOT = 1791270792 # of order 2^TWO_ADICITY - -# Dot product precompile: -BE = 1 # base-extension -EE = 0 # extension-extension - -# bit decomposition hint -BIG_ENDIAN = 0 -LITTLE_ENDIAN = 1 - - -def powers(alpha, n): - # alpha: EF - # n: F - - res = Array(n * DIM) - set_to_one(res) - for i in range(0, n - 1): - mul_extension(res + i * DIM, alpha, res + (i + 1) * DIM) - return res - - -def powers_const(alpha, n: Const): - # alpha: EF - # n: F - - res = Array(n * DIM) - set_to_one(res) - for i in unroll(0, n - 1): - mul_extension(res + i * DIM, alpha, res + (i + 1) * DIM) - return res - - -def unit_root_pow_dynamic(domain_size, index_bits): - # index_bits is a pointer to domain_size bits - res: Imu - debug_assert(domain_size < 26) - match domain_size: - case 0: - _ = 0 # unreachable - case 1: - res = unit_root_pow_const(1, index_bits) - case 2: - res = unit_root_pow_const(2, index_bits) - case 3: - res = unit_root_pow_const(3, index_bits) - case 4: - res = unit_root_pow_const(4, index_bits) - case 5: - res = unit_root_pow_const(5, index_bits) - case 6: - res = unit_root_pow_const(6, index_bits) - case 7: - res = unit_root_pow_const(7, index_bits) - case 8: - res = unit_root_pow_const(8, index_bits) - case 9: - res = unit_root_pow_const(9, index_bits) - case 10: - res = unit_root_pow_const(10, index_bits) - case 11: - res = unit_root_pow_const(11, index_bits) - case 12: - res = unit_root_pow_const(12, index_bits) - case 13: - res = unit_root_pow_const(13, index_bits) - case 14: - res = unit_root_pow_const(14, index_bits) - case 15: - res = unit_root_pow_const(15, index_bits) - case 16: - res = unit_root_pow_const(16, index_bits) - case 17: - res = unit_root_pow_const(17, index_bits) - case 18: - res = unit_root_pow_const(18, index_bits) - case 19: - res = unit_root_pow_const(19, index_bits) - case 20: - res = unit_root_pow_const(20, index_bits) - case 21: - res = unit_root_pow_const(21, index_bits) - case 22: - res = unit_root_pow_const(22, index_bits) - case 23: - res = unit_root_pow_const(23, index_bits) - case 24: - res = unit_root_pow_const(24, index_bits) - case 25: - res = unit_root_pow_const(25, index_bits) - return res - - -def unit_root_pow_const(domain_size: Const, index_bits): - prod: Mut = (index_bits[0] * ROOT ** (2 ** (TWO_ADICITY - domain_size))) + (1 - index_bits[0]) - for i in unroll(1, domain_size): - prod *= (index_bits[i] * ROOT ** (2 ** (TWO_ADICITY - domain_size + i))) + (1 - index_bits[i]) - return prod - - -def poly_eq_extension_dynamic(point, n): - debug_assert(n < 8) - res: Imu - match n: - case 0: - res = ONE_VEC_PTR - case 1: - res = poly_eq_extension(point, 1) - case 2: - res = poly_eq_extension(point, 2) - case 3: - res = poly_eq_extension(point, 3) - case 4: - res = poly_eq_extension(point, 4) - case 5: - res = poly_eq_extension(point, 5) - case 6: - res = poly_eq_extension(point, 6) - case 7: - res = poly_eq_extension(point, 7) - return res - - -def poly_eq_extension(point, n: Const): - # Example: for n = 2: eq(x, y) = [(1 - x)(1 - y), (1 - x)y, x(1 - y), xy] - - res = Array((2 ** (n + 1) - 1) * DIM) - set_to_one(res) - - for s in unroll(0, n): - p = point + (n - 1 - s) * DIM - for i in unroll(0, 2**s): - mul_extension(p, res + (2**s - 1 + i) * DIM, res + (2 ** (s + 1) - 1 + 2**s + i) * DIM) - sub_extension( - res + (2**s - 1 + i) * DIM, - res + (2 ** (s + 1) - 1 + 2**s + i) * DIM, - res + (2 ** (s + 1) - 1 + i) * DIM, - ) - return res + (2**n - 1) * DIM - - -def poly_eq_base(point, n: Const): - # Example: for n = 2: eq(x, y) = [(1 - x)(1 - y), (1 - x)y, x(1 - y), xy] - - res = Array((2 ** (n + 1) - 1)) - res[0] = 1 - for s in unroll(0, n): - p = point[n - 1 - s] - for i in unroll(0, 2**s): - res[2 ** (s + 1) - 1 + 2**s + i] = p * res[2**s - 1 + i] - res[2 ** (s + 1) - 1 + i] = res[2**s - 1 + i] - res[2 ** (s + 1) - 1 + 2**s + i] - return res + (2**n - 1) - - -def pow(a, b): - if b == 0: - return 1 # a^0 = 1 - else: - p = pow(a, b - 1) - return a * p - - -def eq_mle_extension(a, b, n): - buff = Array(n * DIM) - - for i in range(0, n): - shift = i * DIM - ai = a + shift - bi = b + shift - buffi = buff + shift - ab = mul_extension_ret(ai, bi) - buffi[0] = 1 + 2 * ab[0] - ai[0] - bi[0] - for j in unroll(1, DIM): - buffi[j] = 2 * ab[j] - ai[j] - bi[j] - - current_prod: Mut = buff - for i in range(0, n - 1): - next_prod = Array(DIM) - mul_extension(current_prod, buff + (i + 1) * DIM, next_prod) - current_prod = next_prod - - return current_prod - - -def eq_mle_base_extension(a, b, n): - res: Imu - debug_assert(n < 26) - match n: - case 0: - _ = 0 # unreachable - case 1: - res = eq_mle_extension_base_const(a, b, 1) - case 2: - res = eq_mle_extension_base_const(a, b, 2) - case 3: - res = eq_mle_extension_base_const(a, b, 3) - case 4: - res = eq_mle_extension_base_const(a, b, 4) - case 5: - res = eq_mle_extension_base_const(a, b, 5) - case 6: - res = eq_mle_extension_base_const(a, b, 6) - case 7: - res = eq_mle_extension_base_const(a, b, 7) - case 8: - res = eq_mle_extension_base_const(a, b, 8) - case 9: - res = eq_mle_extension_base_const(a, b, 9) - case 10: - res = eq_mle_extension_base_const(a, b, 10) - case 11: - res = eq_mle_extension_base_const(a, b, 11) - case 12: - res = eq_mle_extension_base_const(a, b, 12) - case 13: - res = eq_mle_extension_base_const(a, b, 13) - case 14: - res = eq_mle_extension_base_const(a, b, 14) - case 15: - res = eq_mle_extension_base_const(a, b, 15) - case 16: - res = eq_mle_extension_base_const(a, b, 16) - case 17: - res = eq_mle_extension_base_const(a, b, 17) - case 18: - res = eq_mle_extension_base_const(a, b, 18) - case 19: - res = eq_mle_extension_base_const(a, b, 19) - case 20: - res = eq_mle_extension_base_const(a, b, 20) - case 21: - res = eq_mle_extension_base_const(a, b, 21) - case 22: - res = eq_mle_extension_base_const(a, b, 22) - case 23: - res = eq_mle_extension_base_const(a, b, 23) - case 24: - res = eq_mle_extension_base_const(a, b, 24) - case 25: - res = eq_mle_extension_base_const(a, b, 25) - return res - - -def eq_mle_extension_base_const(a, b, n: Const): - # a: base - # b: extension - - buff = Array(n * DIM) - - for i in unroll(0, n): - ai = a[i] - bi = b + i * DIM - buffi = buff + i * DIM - ai_double = ai * 2 - buffi[0] = 1 + ai_double * bi[0] - ai - bi[0] - for j in unroll(1, DIM): - buffi[j] = ai_double * bi[j] - bi[j] - - prods = Array(n * DIM) - copy_5(buff, prods) - for i in unroll(0, n - 1): - mul_extension(prods + i * DIM, buff + (i + 1) * DIM, prods + (i + 1) * DIM) - return prods + (n - 1) * DIM - - -def expand_from_univariate_base(alpha, n): - res: Imu - debug_assert(n < 23) - match n: - case 0: - _ = 0 # unreachable - case 1: - res = expand_from_univariate_base_const(alpha, 1) - case 2: - res = expand_from_univariate_base_const(alpha, 2) - case 3: - res = expand_from_univariate_base_const(alpha, 3) - case 4: - res = expand_from_univariate_base_const(alpha, 4) - case 5: - res = expand_from_univariate_base_const(alpha, 5) - case 6: - res = expand_from_univariate_base_const(alpha, 6) - case 7: - res = expand_from_univariate_base_const(alpha, 7) - case 8: - res = expand_from_univariate_base_const(alpha, 8) - case 9: - res = expand_from_univariate_base_const(alpha, 9) - case 10: - res = expand_from_univariate_base_const(alpha, 10) - case 11: - res = expand_from_univariate_base_const(alpha, 11) - case 12: - res = expand_from_univariate_base_const(alpha, 12) - case 13: - res = expand_from_univariate_base_const(alpha, 13) - case 14: - res = expand_from_univariate_base_const(alpha, 14) - case 15: - res = expand_from_univariate_base_const(alpha, 15) - case 16: - res = expand_from_univariate_base_const(alpha, 16) - case 17: - res = expand_from_univariate_base_const(alpha, 17) - case 18: - res = expand_from_univariate_base_const(alpha, 18) - case 19: - res = expand_from_univariate_base_const(alpha, 19) - case 20: - res = expand_from_univariate_base_const(alpha, 20) - case 21: - res = expand_from_univariate_base_const(alpha, 21) - case 22: - res = expand_from_univariate_base_const(alpha, 22) - return res - - -def expand_from_univariate_base_const(alpha, n: Const): - # "expand_from_univariate" - # alpha: F - - res = Array(n) - current: Mut = alpha - for i in unroll(0, n): - res[i] = current - current *= current - return res - - -def expand_from_univariate_ext(alpha, n): - res = Array(n * DIM) - copy_5(alpha, res) - for i in range(0, n - 1): - mul_extension(res + i * DIM, res + i * DIM, res + (i + 1) * DIM) - return res - - -def dot_product_be_dynamic(a, b, res, n): - for i in unroll(6, 10): - if n == 2**i: - dot_product(a, b, res, 2**i, BE) - return - assert False, "dot_product_be_dynamic called with unsupported n" - - -def dot_product_ee_dynamic(a, b, res, n): - if n == 16: - dot_product(a, b, res, 16, EE) - return - if n == 1: - dot_product(a, b, res, 1, EE) - return - if n == 2: - dot_product(a, b, res, 2, EE) - return - - for i in unroll(0, WHIR_N_ROUNDS + 1): - if n == WHIR_NUM_QUERIES[i]: - dot_product(a, b, res, WHIR_NUM_QUERIES[i], EE) - return - if n == WHIR_NUM_QUERIES[i] + 1: - dot_product(a, b, res, WHIR_NUM_QUERIES[i] + 1, EE) - return - - assert False, "dot_product_ee_dynamic called with unsupported n" - - -def mle_of_01234567_etc(point, n): - if n == 0: - return ZERO_VEC_PTR - else: - e = mle_of_01234567_etc(point + DIM, n - 1) - a = one_minus_self_extension_ret(point) - b = mul_extension_ret(a, e) - power_of_2 = powers_of_two(n - 1) - c = add_base_extension_ret(power_of_2, e) - d = mul_extension_ret(point, c) - res = add_extension_ret(b, d) - return res - - -def powers_of_two(n): - debug_assert(n < 32) - res: Imu - match n: - case 0: - res = 0 + 2**0 - case 1: - res = 0 + 2**1 - case 2: - res = 0 + 2**2 - case 3: - res = 0 + 2**3 - case 4: - res = 0 + 2**4 - case 5: - res = 0 + 2**5 - case 6: - res = 0 + 2**6 - case 7: - res = 0 + 2**7 - case 8: - res = 0 + 2**8 - case 9: - res = 0 + 2**9 - case 10: - res = 0 + 2**10 - case 11: - res = 0 + 2**11 - case 12: - res = 0 + 2**12 - case 13: - res = 0 + 2**13 - case 14: - res = 0 + 2**14 - case 15: - res = 0 + 2**15 - case 16: - res = 0 + 2**16 - case 17: - res = 0 + 2**17 - case 18: - res = 0 + 2**18 - case 19: - res = 0 + 2**19 - case 20: - res = 0 + 2**20 - case 21: - res = 0 + 2**21 - case 22: - res = 0 + 2**22 - case 23: - res = 0 + 2**23 - case 24: - res = 0 + 2**24 - case 25: - res = 0 + 2**25 - case 26: - res = 0 + 2**26 - case 27: - res = 0 + 2**27 - case 28: - res = 0 + 2**28 - case 29: - res = 0 + 2**29 - case 30: - res = 0 + 2**30 - case 31: - res = 0 + 2**31 - return res - - -@inline -def mul_extension_ret(a, b): - return dot_product_ret(a, b, 1, EE) - - -@inline -def mul_extension(a, b, c): - dot_product(a, b, c, 1, EE) - return - - -@inline -def add_extension_ret(a, b): - # TODO if a and b are adjacent we can do it in one cycle using the dot_product precompile - c = Array(DIM) - for i in unroll(0, DIM): - c[i] = a[i] + b[i] - return c - - -@inline -def add_extension(a, b, c): - # TODO if a and b are adjacent we can do it in one cycle using the dot_product precompile - for i in unroll(0, DIM): - c[i] = a[i] + b[i] - return - - -@inline -def one_minus_self_extension_ret(a): - res = Array(DIM) - res[0] = 1 - a[0] - for i in unroll(1, DIM): - res[i] = 0 - a[i] - return res - - -@inline -def opposite_extension_ret(a): - # todo use dot_product precompile - res = Array(DIM) - for i in unroll(0, DIM): - res[i] = 0 - a[i] - return res - - -@inline -def add_base_extension_ret(a, b): - # a: base - # b: extension - res = Array(DIM) - res[0] = a + b[0] - for i in unroll(1, DIM): - res[i] = b[i] - return res - - -@inline -def mul_base_extension_ret(a, b): - # a: base - # b: extension - - # TODO: use dot_product_be - - res = Array(DIM) - for i in unroll(0, DIM): - res[i] = a * b[i] - return res - - -def div_extension_ret(n, d): - quotient = Array(DIM) - dot_product(d, quotient, n, 1, EE) - return quotient - - -@inline -def sub_extension(a, b, c): - # TODO if a and b are adjacent we can do it in one cycle using the dot_product precompile - for i in unroll(0, DIM): - c[i] = a[i] - b[i] - return - - -@inline -def sub_base_extension_ret(a, b): - # a: base - # b: extension - # return a - b - res = Array(DIM) - res[0] = a - b[0] - for i in unroll(1, DIM): - res[i] = 0 - b[i] - return res - - -@inline -def sub_extension_base_ret(a, b): - # a: extension - # b: base - # return a - b - res = Array(DIM) - res[0] = a[0] - b - for i in unroll(1, DIM): - res[i] = a[i] - return res - - -@inline -def sub_extension_ret(a, b): - # TODO if a and b are adjacent we can do it in one cycle using the dot_product precompile - c = Array(DIM) - for i in unroll(0, DIM): - c[i] = a[i] - b[i] - return c - - -@inline -def copy_5(a, b): - dot_product(a, ONE_VEC_PTR, b, 1, EE) - return - - -@inline -def set_to_5_zeros(a): - zero_ptr = ZERO_VEC_PTR - dot_product(a, ONE_VEC_PTR, zero_ptr, 1, EE) - return - - -@inline -def set_to_7_zeros(a): - zero_ptr = ZERO_VEC_PTR - dot_product(a, ONE_VEC_PTR, zero_ptr, 1, EE) - a[5] = 0 - a[6] = 0 - return - - -@inline -def set_to_16_zeros(a): - zero_ptr = ZERO_VEC_PTR - dot_product(a, ONE_VEC_PTR, zero_ptr, 1, EE) - dot_product(a + 5, ONE_VEC_PTR, zero_ptr, 1, EE) - dot_product(a + 10, ONE_VEC_PTR, zero_ptr, 1, EE) - a[15] = 0 - return - - -@inline -def copy_8(a, b): - dot_product(a, ONE_VEC_PTR, b, 1, EE) - assert a[5] == b[5] - assert a[6] == b[6] - assert a[7] == b[7] - return - - -@inline -def copy_16(a, b): - dot_product(a, ONE_VEC_PTR, b, 1, EE) - dot_product(a + 5, ONE_VEC_PTR, b + 5, 1, EE) - dot_product(a + 10, ONE_VEC_PTR, b + 10, 1, EE) - a[15] = b[15] - return - - -def copy_many_ef(a, b, n): - for i in range(0, n): - dot_product(a + i * DIM, ONE_VEC_PTR, b + i * DIM, 1, EE) - return - - -@inline -def set_to_one(a): - a[0] = 1 - for i in unroll(1, DIM): - a[i] = 0 - return - - -def print_ef(a): - for i in unroll(0, DIM): - print(a[i]) - return - - -def print_vec(a): - for i in unroll(0, VECTOR_LEN): - print(a[i]) - return - - -def print_many(a, n): - for i in range(0, n): - print(a[i]) - return - - -def next_multiple_of_8(a: Const): - return a + (8 - (a % 8)) % 8 - - -@inline -def read_memory(ptr): - mem = 0 - return mem[ptr] - - -def univariate_polynomial_eval(coeffs, point, degree: Const): - powers = powers(point, degree + 1) # TODO use a parameter: Const version - res = Array(DIM) - dot_product(coeffs, powers, res, degree + 1, EE) - return res - - -def sum_2_ef_fractions(a_num, a_den, b_num, b_den): - common_den = mul_extension_ret(a_den, b_den) - a_num_mul_b_den = mul_extension_ret(a_num, b_den) - b_num_mul_a_den = mul_extension_ret(b_num, a_den) - sum_num = add_extension_ret(a_num_mul_b_den, b_num_mul_a_den) - return sum_num, common_den - - -# p = 2^31 - 2^24 + 1 -# in binary: p = 1111111000000000000000000000001 -# p - 1 = 1111111000000000000000000000000 -# p - 2 = 1111110111111111111111111111111 -# p - 3 = 1111110111111111111111111111110 -# ... -# Any field element (< p) is either: -# - 1111111 | 00...00 -# - not(1111111) | xx...xx -def checked_decompose_bits(a, k): - # return a pointer to the 31 bits of a - # .. and the partial value, reading the first K bits (with k <= 24) - bits = Array(F_BITS) - hint_decompose_bits(a, bits, F_BITS, LITTLE_ENDIAN) - - for i in unroll(0, F_BITS): - assert bits[i] * (1 - bits[i]) == 0 - partial_sums_24 = Array(24) - sum_24: Mut = bits[0] - partial_sums_24[0] = sum_24 - for i in unroll(1, 24): - sum_24 += bits[i] * 2**i - partial_sums_24[i] = sum_24 - sum_7: Mut = bits[24] - for i in unroll(1, 7): - sum_7 += bits[24 + i] * 2**i - if sum_7 == 127: - assert sum_24 == 0 - - assert a == sum_24 + sum_7 * 2**24 - partial_sum = partial_sums_24[k - 1] - return bits, partial_sum - - -def checked_decompose_bits_small_value(to_decompose, n_bits): - bits = Array(n_bits) - hint_decompose_bits(to_decompose, bits, n_bits, BIG_ENDIAN) - sum: Mut = bits[n_bits - 1] - power_of_2: Mut = 1 - for i in range(1, n_bits): - power_of_2 *= 2 - sum += bits[n_bits - 1 - i] * power_of_2 - assert to_decompose == sum - return bits - - -@inline -def dot_product_ret(a, b, n, mode): - res = Array(DIM) - dot_product(a, b, res, n, mode) - return res - - -def mle_of_zeros_then_ones(point, n_zeros, n_vars): - if n_vars == 0: - res = Array(DIM) - res[0] = 1 - n_zeros - for i in unroll(1, DIM): - res[i] = 0 - return res - - n_values = powers_of_two(n_vars) - debug_assert(n_zeros <= n_values) - - if n_zeros == n_values: - return ZERO_VEC_PTR - - bits, _ = checked_decompose_bits(n_zeros, 0) - - res: Mut = Array(DIM) - set_to_one(res) - - for i in range(0, n_vars): - p = point + (n_vars - 1 - i) * DIM - if bits[i] == 0: - one_minus_p = one_minus_self_extension_ret(p) - tmp = mul_extension_ret(one_minus_p, res) - res = add_extension_ret(tmp, p) - else: - res = mul_extension_ret(p, res) - return res - - -@inline -def embed_in_ef(f): - res = Array(DIM) - res[0] = f - for i in unroll(1, DIM): - res[i] = 0 - return res - - -def next_mle(x, y, n): - # x and y are pointers to n elements of extension field - - # Build eq_prefix[0..n+1] where eq_prefix[i] = prod_{j=i} (x[j] * (1-y[j])) - low_suffix = Array((n + 1) * DIM) - set_to_one(low_suffix + n * DIM) - for i in range(0, n): - idx = n - 1 - i - xi = x + idx * DIM - yi = y + idx * DIM - one_minus_y = one_minus_self_extension_ret(yi) - x_one_minus_y = mul_extension_ret(xi, one_minus_y) - mul_extension(low_suffix + (idx + 1) * DIM, x_one_minus_y, low_suffix + idx * DIM) - - # Compute sum = Σ_{arr=0..n} (eq_prefix[arr] * (1-x[arr]) * y[arr] * low_suffix[arr+1]) - sum: Mut = ZERO_VEC_PTR - for arr in range(0, n): - x_arr = x + arr * DIM - y_arr = y + arr * DIM - one_minus_x = one_minus_self_extension_ret(x_arr) - carry = mul_extension_ret(one_minus_x, y_arr) - eq_carry = mul_extension_ret(eq_prefix + arr * DIM, carry) - term = mul_extension_ret(eq_carry, low_suffix + (arr + 1) * DIM) - sum = add_extension_ret(sum, term) - - # Compute prod = product of all x[i] * product of all y[i] - prod: Mut = Array(DIM) - set_to_one(prod) - for i in range(0, n): - prod = mul_extension_ret(prod, x + i * DIM) - for i in range(0, n): - prod = mul_extension_ret(prod, y + i * DIM) - - result = add_extension_ret(sum, prod) - return result - - -@inline -def dot_product_with_the_base_vectors(slice): - # slice: pointer to DIM extension field elements - # cf constants.rs: by convention, [10000] [01000] [00100] [00010] [00001] is harcoded in memory, starting at ONE_VEC_PTR - return dot_product_ret(slice, ONE_VEC_PTR, 1, EE) From 1c687814c8bcbf11aacb47c7d5a35d4a5ed6b742 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 3 Feb 2026 13:21:08 +0400 Subject: [PATCH 35/47] w --- crates/rec_aggregation/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rec_aggregation/utils.py b/crates/rec_aggregation/utils.py index 41ef9594..f507f2ea 100644 --- a/crates/rec_aggregation/utils.py +++ b/crates/rec_aggregation/utils.py @@ -268,7 +268,7 @@ def eq_mle_extension_base_const(a, b, n: Const): mul_extension(prods + i * DIM, buff + (i + 1) * DIM, prods + (i + 1) * DIM) return prods + (n - 1) * DIM - +@inline def expand_from_univariate_base(alpha, n): res: Imu debug_assert(n < 23) From 87199b46d372696fc546177f261386a5e0ea6bc6 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 3 Feb 2026 16:21:04 +0400 Subject: [PATCH 36/47] wip --- .../lean_compiler/tests/test_data/error_34.py | 5 +- .../lean_compiler/tests/test_data/error_35.py | 1 + .../tests/test_data/program_171.py | 33 ++++++++ .../tests/test_data/program_172.py | 72 +++++++---------- .../tests/test_data/program_173.py | 31 +++----- .../tests/test_data/program_174.py | 1 + .../tests/test_data/program_175.py | 14 ++-- crates/rec_aggregation/utils.py | 77 +++---------------- 8 files changed, 93 insertions(+), 141 deletions(-) diff --git a/crates/lean_compiler/tests/test_data/error_34.py b/crates/lean_compiler/tests/test_data/error_34.py index 6f6acded..3575f3fb 100644 --- a/crates/lean_compiler/tests/test_data/error_34.py +++ b/crates/lean_compiler/tests/test_data/error_34.py @@ -1,9 +1,8 @@ from snark_lib import * + # Error: match_range with non-continuous ranges (gap between 2 and 5) def main(): x = 1 - result = match_range(x, - range(0, 2), lambda i: i * 10, - range(5, 8), lambda i: i * 100) + result = match_range(x, range(0, 2), lambda i: i * 10, range(5, 8), lambda i: i * 100) return diff --git a/crates/lean_compiler/tests/test_data/error_35.py b/crates/lean_compiler/tests/test_data/error_35.py index 07066aea..2eba4756 100644 --- a/crates/lean_compiler/tests/test_data/error_35.py +++ b/crates/lean_compiler/tests/test_data/error_35.py @@ -1,5 +1,6 @@ from snark_lib import * + # Error: match_range results are always immutable, cannot use Mut def main(): x = 1 diff --git a/crates/lean_compiler/tests/test_data/program_171.py b/crates/lean_compiler/tests/test_data/program_171.py index 7c88b222..bb3c47c6 100644 --- a/crates/lean_compiler/tests/test_data/program_171.py +++ b/crates/lean_compiler/tests/test_data/program_171.py @@ -7,6 +7,7 @@ # Simple inline functions with mutable variables # ============================================================================ + @inline def count_up(n): """Count from 0 to n-1, return the sum""" @@ -15,6 +16,7 @@ def count_up(n): acc = acc + 1 return acc + @inline def sum_range(start, end): """Sum integers from start to end-1""" @@ -23,6 +25,7 @@ def sum_range(start, end): total = total + i return total + @inline def double_count(n): """Two mutable variables in same function""" @@ -33,10 +36,12 @@ def double_count(n): b = b - 1 return a + b + # ============================================================================ # Nested inline functions (inline calling inline) # ============================================================================ + @inline def inner_loop(k): """Inner inline function""" @@ -45,6 +50,7 @@ def inner_loop(k): x = x + j return x + @inline def outer_with_inner(n): """Outer inline that calls inner inline""" @@ -53,6 +59,7 @@ def outer_with_inner(n): result = result + inner_loop(i) return result + @inline def deep_nested(a): """Deeply nested: calls outer_with_inner which calls inner_loop""" @@ -60,10 +67,12 @@ def deep_nested(a): base = base + outer_with_inner(a) return base + # ============================================================================ # Inline functions with multiple mutable variables and complex flow # ============================================================================ + @inline def complex_muts(n): """Multiple mutable variables with interdependencies""" @@ -77,6 +86,7 @@ def complex_muts(n): z = temp + z return x + y + z + @inline def with_immutable(n): """Mix of mutable and immutable inside inline""" @@ -87,10 +97,12 @@ def with_immutable(n): final_imm = m + 1000 return final_imm + # ============================================================================ # Inline functions with internal branching # ============================================================================ + @inline def inline_with_if(x): """Inline function that itself contains if/else""" @@ -102,6 +114,7 @@ def inline_with_if(x): result = result + x return result + @inline def inline_with_match(selector): """Inline function that itself contains match""" @@ -115,6 +128,7 @@ def inline_with_match(selector): out = 3000 return out + @inline def inline_with_nested_branch(a, b): """Inline with nested if inside match""" @@ -132,10 +146,12 @@ def inline_with_nested_branch(a, b): res = 40 return res + # ============================================================================ # Inline functions returning multiple values # ============================================================================ + @inline def multi_return_inline(n): """Inline returning multiple values""" @@ -146,6 +162,7 @@ def multi_return_inline(n): b = b + 2 return a, b + @inline def triple_return(x): """Inline returning three values with different computations""" @@ -158,10 +175,12 @@ def triple_return(x): m3 = m3 + 3 return m1, m2, m3 + # ============================================================================ # Deeper nesting of inline functions # ============================================================================ + @inline def level_d(x): """Deepest level""" @@ -170,6 +189,7 @@ def level_d(x): acc = acc + 1 return acc + @inline def level_c(x): """Calls level_d""" @@ -179,6 +199,7 @@ def level_c(x): acc = acc + 10 return acc + @inline def level_b(x): """Calls level_c""" @@ -188,6 +209,7 @@ def level_b(x): acc = acc + 100 return acc + @inline def level_a(x): """Calls level_b - 4 levels deep""" @@ -197,10 +219,12 @@ def level_a(x): acc = acc + 1000 return acc + # ============================================================================ # Inline with Array operations # ============================================================================ + @inline def inline_with_array(n): """Inline that allocates and uses an array""" @@ -214,6 +238,7 @@ def inline_with_array(n): total = total + arr[i] return total + @inline def inline_modify_array(base): """Inline that creates array and does complex operations""" @@ -224,10 +249,12 @@ def inline_modify_array(base): acc = acc * 2 return buf[0] + buf[1] + buf[2] + # ============================================================================ # Chained inline calls # ============================================================================ + @inline def chain_a(x): m: Mut = x @@ -235,6 +262,7 @@ def chain_a(x): m = m + 1 return m + @inline def chain_b(x): m: Mut = x @@ -242,6 +270,7 @@ def chain_b(x): m = m * 2 return m + @inline def chain_c(x): m: Mut = x @@ -249,10 +278,12 @@ def chain_c(x): m = m + 10 return m + # ============================================================================ # Stress test inline with many variables # ============================================================================ + @inline def many_vars(seed): """Inline with 10 mutable variables""" @@ -279,10 +310,12 @@ def many_vars(seed): v9 = v9 + 1 return v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + # ============================================================================ # Main test function # ============================================================================ + def main(): # ------------------------------------------------------------------- # TEST 1: Basic inline in match arms (different inlined vars per arm) diff --git a/crates/lean_compiler/tests/test_data/program_172.py b/crates/lean_compiler/tests/test_data/program_172.py index f4accb9c..da813966 100644 --- a/crates/lean_compiler/tests/test_data/program_172.py +++ b/crates/lean_compiler/tests/test_data/program_172.py @@ -2,9 +2,11 @@ # Test match_range feature + def helper_const(n: Const): return n * 10 + def main(): # Test 1: Basic match_range - no forward declaration needed (auto-generated as Imu) x = 2 @@ -34,29 +36,21 @@ def main(): # Test 6: match_range with multiple continuous ranges d = 0 - r6a = match_range(d, - range(0, 2), lambda i: 100 + i, - range(2, 5), lambda i: 200 + i) + r6a = match_range(d, range(0, 2), lambda i: 100 + i, range(2, 5), lambda i: 200 + i) assert r6a == 100 # d=0 -> 100+0=100 e = 3 - r6b = match_range(e, - range(0, 2), lambda i: 100 + i, - range(2, 5), lambda i: 200 + i) + r6b = match_range(e, range(0, 2), lambda i: 100 + i, range(2, 5), lambda i: 200 + i) assert r6b == 203 # e=3 -> 200+3=203 # Test 7: match_range with different lambdas calling functions f = 1 - r7 = match_range(f, - range(0, 1), lambda i: 999, - range(1, 4), lambda i: helper_const(i)) + r7 = match_range(f, range(0, 1), lambda i: 999, range(1, 4), lambda i: helper_const(i)) assert r7 == 10 # f=1 -> helper_const(1)=10 # Test 8: match_range first range (special case) g = 0 - r8 = match_range(g, - range(0, 1), lambda i: 42, - range(1, 3), lambda i: i * 7) + r8 = match_range(g, range(0, 1), lambda i: 42, range(1, 3), lambda i: i * 7) assert r8 == 42 # g=0 -> 42 # Test 9: Results are always immutable @@ -70,36 +64,32 @@ def main(): # Test 10: Basic multiple return values (2 values) v10 = 1 a10, b10 = match_range(v10, range(0, 3), lambda i: two_values_const(i)) - assert a10 == 10 # 1 * 10 + assert a10 == 10 # 1 * 10 assert b10 == 101 # 1 + 100 # Test 11: Multiple return values with different case v11 = 2 a11, b11 = match_range(v11, range(0, 3), lambda i: two_values_const(i)) - assert a11 == 20 # 2 * 10 + assert a11 == 20 # 2 * 10 assert b11 == 102 # 2 + 100 # Test 12: Three return values v12 = 1 x12, y12, z12 = match_range(v12, range(0, 3), lambda i: three_values_const(i)) - assert x12 == 1 # i - assert y12 == 10 # i * 10 - assert z12 == 1001 # i + 1000 + assert x12 == 1 # i + assert y12 == 10 # i * 10 + assert z12 == 1001 # i + 1000 # Test 13: Multiple return values with multiple ranges v13 = 3 - a13, b13 = match_range(v13, - range(0, 2), lambda i: pair_small(i), - range(2, 5), lambda i: pair_large(i)) - assert a13 == 300 # 3 * 100 (pair_large) + a13, b13 = match_range(v13, range(0, 2), lambda i: pair_small(i), range(2, 5), lambda i: pair_large(i)) + assert a13 == 300 # 3 * 100 (pair_large) assert b13 == 3000 # 3 * 1000 # Test 14: Multiple return values with multiple ranges - different range v14 = 1 - a14, b14 = match_range(v14, - range(0, 2), lambda i: pair_small(i), - range(2, 5), lambda i: pair_large(i)) - assert a14 == 1 # 1 * 1 (pair_small) + a14, b14 = match_range(v14, range(0, 2), lambda i: pair_small(i), range(2, 5), lambda i: pair_large(i)) + assert a14 == 1 # 1 * 1 (pair_small) assert b14 == 10 # 1 * 10 # Test 15: Multiple return values - edge case first element @@ -137,11 +127,11 @@ def main(): # Test 20: Three values with multiple ranges v20 = 4 - x20, y20, z20 = match_range(v20, - range(0, 3), lambda i: three_values_const(i), - range(3, 6), lambda i: three_values_offset(i)) - assert x20 == 104 # 4 + 100 - assert y20 == 1004 # 4 + 1000 + x20, y20, z20 = match_range( + v20, range(0, 3), lambda i: three_values_const(i), range(3, 6), lambda i: three_values_offset(i) + ) + assert x20 == 104 # 4 + 100 + assert y20 == 1004 # 4 + 1000 assert z20 == 10004 # 4 + 10000 # ========== INLINED FUNCTION TESTS ========== @@ -154,29 +144,25 @@ def main(): # Test 22: Inlined function - two return values v22 = 3 a22, b22 = match_range(v22, range(0, 5), lambda i: inlined_pair(i)) - assert a22 == 30 # 3 * 10 + assert a22 == 30 # 3 * 10 assert b22 == 300 # 3 * 100 # Test 23: Inlined function with multiple ranges v23 = 4 - r23 = match_range(v23, - range(0, 3), lambda i: inlined_small(i), - range(3, 6), lambda i: inlined_large(i)) + r23 = match_range(v23, range(0, 3), lambda i: inlined_small(i), range(3, 6), lambda i: inlined_large(i)) assert r23 == 4000 # 4 * 1000 (inlined_large) # Test 24: Inlined function - first range v24 = 1 - r24 = match_range(v24, - range(0, 3), lambda i: inlined_small(i), - range(3, 6), lambda i: inlined_large(i)) + r24 = match_range(v24, range(0, 3), lambda i: inlined_small(i), range(3, 6), lambda i: inlined_large(i)) assert r24 == 10 # 1 * 10 (inlined_small) # Test 25: Inlined function with three return values v25 = 2 x25, y25, z25 = match_range(v25, range(0, 4), lambda i: inlined_triple(i)) - assert x25 == 2 # i - assert y25 == 20 # i * 10 - assert z25 == 200 # i * 100 + assert x25 == 2 # i + assert y25 == 20 # i * 10 + assert z25 == 200 # i * 100 # Test 26: Inlined function with complex body v26 = 3 @@ -185,10 +171,8 @@ def main(): # Test 27: Mix of inlined and const functions in multiple ranges v27 = 2 - a27, b27 = match_range(v27, - range(0, 2), lambda i: inlined_pair(i), - range(2, 5), lambda i: two_values_const(i)) - assert a27 == 20 # 2 * 10 (two_values_const) + a27, b27 = match_range(v27, range(0, 2), lambda i: inlined_pair(i), range(2, 5), lambda i: two_values_const(i)) + assert a27 == 20 # 2 * 10 (two_values_const) assert b27 == 102 # 2 + 100 # Test 28: Inlined with expression as match value diff --git a/crates/lean_compiler/tests/test_data/program_173.py b/crates/lean_compiler/tests/test_data/program_173.py index 906197c7..5cd16de1 100644 --- a/crates/lean_compiler/tests/test_data/program_173.py +++ b/crates/lean_compiler/tests/test_data/program_173.py @@ -2,6 +2,7 @@ # Test match_range with non-zero starting indices + def main(): # Test 1: Range starting at 1 x1 = 2 @@ -30,30 +31,26 @@ def main(): # Test 6: Multiple ranges, first starting at non-zero x6 = 2 - r6 = match_range(x6, - range(1, 3), lambda i: i * 10, - range(3, 6), lambda i: i * 100) + r6 = match_range(x6, range(1, 3), lambda i: i * 10, range(3, 6), lambda i: i * 100) assert r6 == 20 # 2 * 10 # Test 7: Multiple ranges, selecting from second range x7 = 4 - r7 = match_range(x7, - range(1, 3), lambda i: i * 10, - range(3, 6), lambda i: i * 100) + r7 = match_range(x7, range(1, 3), lambda i: i * 10, range(3, 6), lambda i: i * 100) assert r7 == 400 # 4 * 100 # Test 8: Non-zero start with multiple return values x8 = 3 a8, b8 = match_range(x8, range(1, 5), lambda i: two_vals(i)) - assert a8 == 30 # 3 * 10 + assert a8 == 30 # 3 * 10 assert b8 == 300 # 3 * 100 # Test 9: Non-zero start with three return values x9 = 2 p9, q9, r9 = match_range(x9, range(1, 4), lambda i: three_vals(i)) - assert p9 == 2 # i - assert q9 == 20 # i * 10 - assert r9 == 200 # i * 100 + assert p9 == 2 # i + assert q9 == 20 # i * 10 + assert r9 == 200 # i * 100 # Test 10: Non-zero start with expression as match value a10 = 7 @@ -73,19 +70,15 @@ def main(): # Test 13: Multiple return values with multiple non-zero ranges x13 = 5 - a13, b13 = match_range(x13, - range(2, 4), lambda i: pair_small(i), - range(4, 7), lambda i: pair_large(i)) - assert a13 == 500 # 5 * 100 + a13, b13 = match_range(x13, range(2, 4), lambda i: pair_small(i), range(4, 7), lambda i: pair_large(i)) + assert a13 == 500 # 5 * 100 assert b13 == 5000 # 5 * 1000 # Test 14: First range selected in multiple non-zero ranges x14 = 3 - a14, b14 = match_range(x14, - range(2, 4), lambda i: pair_small(i), - range(4, 7), lambda i: pair_large(i)) - assert a14 == 3 # 3 * 1 - assert b14 == 30 # 3 * 10 + a14, b14 = match_range(x14, range(2, 4), lambda i: pair_small(i), range(4, 7), lambda i: pair_large(i)) + assert a14 == 3 # 3 * 1 + assert b14 == 30 # 3 * 10 return diff --git a/crates/lean_compiler/tests/test_data/program_174.py b/crates/lean_compiler/tests/test_data/program_174.py index 7bfb0d1d..19be7961 100644 --- a/crates/lean_compiler/tests/test_data/program_174.py +++ b/crates/lean_compiler/tests/test_data/program_174.py @@ -2,6 +2,7 @@ # Test classical match statement with cases starting after 0 + def main(): # Test 1: Basic match starting at 1 r1 = match_start_at_1(2) diff --git a/crates/lean_compiler/tests/test_data/program_175.py b/crates/lean_compiler/tests/test_data/program_175.py index ba0cb120..b68d9003 100644 --- a/crates/lean_compiler/tests/test_data/program_175.py +++ b/crates/lean_compiler/tests/test_data/program_175.py @@ -27,9 +27,7 @@ def inlined_with_match_range_two_args(a, x): @inline def inlined_with_match_range_multi_range(x): - res = match_range(x, - range(0, 3), lambda i: i * 10, - range(3, 6), lambda i: helper_const(i)) + res = match_range(x, range(0, 3), lambda i: i * 10, range(3, 6), lambda i: helper_const(i)) return res @@ -66,9 +64,9 @@ def main(): a = inlined_with_match_range(1) b = inlined_with_match_range(2) c = inlined_with_match_range(3) - assert a == 1 # 1*1 - assert b == 4 # 2*2 - assert c == 9 # 3*3 + assert a == 1 # 1*1 + assert b == 4 # 2*2 + assert c == 9 # 3*3 # Test 5: Inlined function with two match_ranges r5 = inlined_nested_match_range(2, 3) @@ -77,7 +75,7 @@ def main(): # Test 6: Edge cases - first and last values first = inlined_with_match_range(1) last = inlined_with_match_range(4) - assert first == 1 # 1*1 - assert last == 16 # 4*4 + assert first == 1 # 1*1 + assert last == 16 # 4*4 return diff --git a/crates/rec_aggregation/utils.py b/crates/rec_aggregation/utils.py index b798dcc0..a430c1ba 100644 --- a/crates/rec_aggregation/utils.py +++ b/crates/rec_aggregation/utils.py @@ -54,9 +54,7 @@ def unit_root_pow_const(domain_size: Const, index_bits): def poly_eq_extension_dynamic(point, n): debug_assert(n < 8) - res = match_range(n, - range(0, 1), lambda i: ONE_VEC_PTR, - range(1, 8), lambda i: poly_eq_extension(point, i)) + res = match_range(n, range(0, 1), lambda i: ONE_VEC_PTR, range(1, 8), lambda i: poly_eq_extension(point, i)) return res @@ -149,6 +147,7 @@ def eq_mle_extension_base_const(a, b, n: Const): mul_extension(prods + i * DIM, buff + (i + 1) * DIM, prods + (i + 1) * DIM) return prods + (n - 1) * DIM + @inline def expand_from_univariate_base(alpha, n): debug_assert(n < 23) @@ -516,74 +515,18 @@ def checked_decompose_bits_small_value_const(to_decompose, n_bits: Const): assert to_decompose == sum return bits + @inline def checked_decompose_bits_small_value(to_decompose, n_bits): - res : Imu debug_assert(n_bits < 30) debug_assert(0 < n_bits) - match n_bits: - case 0: - _ = 0 # unreachable - case 1: - res = checked_decompose_bits_small_value_const(to_decompose, 1) - case 2: - res = checked_decompose_bits_small_value_const(to_decompose, 2) - case 3: - res = checked_decompose_bits_small_value_const(to_decompose, 3) - case 4: - res = checked_decompose_bits_small_value_const(to_decompose, 4) - case 5: - res = checked_decompose_bits_small_value_const(to_decompose, 5) - case 6: - res = checked_decompose_bits_small_value_const(to_decompose, 6) - case 7: - res = checked_decompose_bits_small_value_const(to_decompose, 7) - case 8: - res = checked_decompose_bits_small_value_const(to_decompose, 8) - case 9: - res = checked_decompose_bits_small_value_const(to_decompose, 9) - case 10: - res = checked_decompose_bits_small_value_const(to_decompose, 10) - case 11: - res = checked_decompose_bits_small_value_const(to_decompose, 11) - case 12: - res = checked_decompose_bits_small_value_const(to_decompose, 12) - case 13: - res = checked_decompose_bits_small_value_const(to_decompose, 13) - case 14: - res = checked_decompose_bits_small_value_const(to_decompose, 14) - case 15: - res = checked_decompose_bits_small_value_const(to_decompose, 15) - case 16: - res = checked_decompose_bits_small_value_const(to_decompose, 16) - case 17: - res = checked_decompose_bits_small_value_const(to_decompose, 17) - case 18: - res = checked_decompose_bits_small_value_const(to_decompose, 18) - case 19: - res = checked_decompose_bits_small_value_const(to_decompose, 19) - case 20: - res = checked_decompose_bits_small_value_const(to_decompose, 20) - case 21: - res = checked_decompose_bits_small_value_const(to_decompose, 21) - case 22: - res = checked_decompose_bits_small_value_const(to_decompose, 22) - case 23: - res = checked_decompose_bits_small_value_const(to_decompose, 23) - case 24: - res = checked_decompose_bits_small_value_const(to_decompose, 24) - case 25: - res = checked_decompose_bits_small_value_const(to_decompose, 25) - case 26: - res = checked_decompose_bits_small_value_const(to_decompose, 26) - case 27: - res = checked_decompose_bits_small_value_const(to_decompose, 27) - case 28: - res = checked_decompose_bits_small_value_const(to_decompose, 28) - case 29: - res = checked_decompose_bits_small_value_const(to_decompose, 29) - return res - + return match_range( + n_bits, + range(0, 1), + lambda _: 0, + range(1, 30), + lambda i: checked_decompose_bits_small_value_const(to_decompose, i), + ) @inline From 7c9adac2a16b13a4f90c2c753e15300f93843df8 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 3 Feb 2026 17:11:58 +0400 Subject: [PATCH 37/47] wip --- crates/rec_aggregation/fiat_shamir.py | 2 +- crates/rec_aggregation/utils.py | 39 +++++---------------------- 2 files changed, 8 insertions(+), 33 deletions(-) diff --git a/crates/rec_aggregation/fiat_shamir.py b/crates/rec_aggregation/fiat_shamir.py index c8327c49..d4a6d892 100644 --- a/crates/rec_aggregation/fiat_shamir.py +++ b/crates/rec_aggregation/fiat_shamir.py @@ -42,7 +42,7 @@ def fs_grinding(fs, bits): return fs_duplexed - +@inline def fs_sample_ef(fs): return fs diff --git a/crates/rec_aggregation/utils.py b/crates/rec_aggregation/utils.py index a430c1ba..dc5e5b95 100644 --- a/crates/rec_aggregation/utils.py +++ b/crates/rec_aggregation/utils.py @@ -18,11 +18,9 @@ def powers(alpha, n): # alpha: EF # n: F - - res = Array(n * DIM) - set_to_one(res) - for i in range(0, n - 1): - mul_extension(res + i * DIM, alpha, res + (i + 1) * DIM) + assert n < 128 + assert 0 < n + res = match_range(n, range(1, 128), lambda i: powers_const(alpha, i)) return res @@ -88,15 +86,6 @@ def poly_eq_base(point, n: Const): res[2 ** (s + 1) - 1 + i] = res[2**s - 1 + i] - res[2 ** (s + 1) - 1 + 2**s + i] return res + (2**n - 1) - -def pow(a, b): - if b == 0: - return 1 # a^0 = 1 - else: - p = pow(a, b - 1) - return a * p - - def eq_mle_extension(a, b, n): buff = Array(n * DIM) @@ -398,9 +387,7 @@ def set_to_16_zeros(a): @inline def copy_8(a, b): dot_product(a, ONE_VEC_PTR, b, 1, EE) - assert a[5] == b[5] - assert a[6] == b[6] - assert a[7] == b[7] + dot_product(a + (8 - DIM), ONE_VEC_PTR, b + (8 - DIM), 1, EE) return @@ -421,9 +408,7 @@ def copy_many_ef(a, b, n): @inline def set_to_one(a): - a[0] = 1 - for i in unroll(1, DIM): - a[i] = 0 + dot_product(ONE_VEC_PTR, ONE_VEC_PTR, a, 1, EE) return @@ -439,16 +424,6 @@ def print_vec(a): return -def print_many(a, n): - for i in range(0, n): - print(a[i]) - return - - -def next_multiple_of_8(a: Const): - return a + (8 - (a % 8)) % 8 - - @inline def read_memory(ptr): mem = 0 @@ -456,12 +431,12 @@ def read_memory(ptr): def univariate_polynomial_eval(coeffs, point, degree: Const): - powers = powers(point, degree + 1) # TODO use a parameter: Const version + powers = powers_const(point, degree + 1) res = Array(DIM) dot_product(coeffs, powers, res, degree + 1, EE) return res - +@inline def sum_2_ef_fractions(a_num, a_den, b_num, b_den): common_den = mul_extension_ret(a_den, b_den) a_num_mul_b_den = mul_extension_ret(a_num, b_den) From f1209f27f6f27f8f5c7d69433a53986ba389112d Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 3 Feb 2026 17:18:38 +0400 Subject: [PATCH 38/47] wip --- crates/lean_compiler/src/a_simplify_lang.rs | 2 +- crates/rec_aggregation/fiat_shamir.py | 5 +++-- crates/rec_aggregation/hashing.py | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/lean_compiler/src/a_simplify_lang.rs b/crates/lean_compiler/src/a_simplify_lang.rs index 83dad72e..3efdddeb 100644 --- a/crates/lean_compiler/src/a_simplify_lang.rs +++ b/crates/lean_compiler/src/a_simplify_lang.rs @@ -409,7 +409,7 @@ fn compile_time_transform_in_program( return Err("Inlined functions with mutable arguments are not supported yet".to_string()); } if func.has_const_arguments() { - return Err("Inlined functions with constant arguments are not supported yet".to_string()); + return Err("Inlined function should not have \"Const\" arguments".to_string()); } } diff --git a/crates/rec_aggregation/fiat_shamir.py b/crates/rec_aggregation/fiat_shamir.py index d4a6d892..f6cc66b8 100644 --- a/crates/rec_aggregation/fiat_shamir.py +++ b/crates/rec_aggregation/fiat_shamir.py @@ -47,6 +47,7 @@ def fs_sample_ef(fs): return fs +@inline def fs_hint(fs, n): # return the updated fiat-shamir, and a pointer to n field elements from the transcript @@ -73,8 +74,8 @@ def fs_receive_chunks(fs, n_chunks: Const): ) return new_fs + 16 * (n_chunks - 1), transcript_ptr - -def fs_receive_ef(fs, n: Const): +@inline +def fs_receive_ef(fs, n): new_fs, ef_ptr = fs_receive_chunks(fs, next_multiple_of(n * DIM, 8) / 8) for i in unroll(n * DIM, next_multiple_of(n * DIM, 8)): assert ef_ptr[i] == 0 diff --git a/crates/rec_aggregation/hashing.py b/crates/rec_aggregation/hashing.py index 8e37ef89..51889d1f 100644 --- a/crates/rec_aggregation/hashing.py +++ b/crates/rec_aggregation/hashing.py @@ -27,12 +27,12 @@ def batch_hash_slice(num_queries, all_data_to_hash, all_resulting_hashes, len): def batch_hash_slice_const(num_queries, all_data_to_hash, all_resulting_hashes, len: Const): for i in range(0, num_queries): data = all_data_to_hash[i] - res = slice_hash(ZERO_VEC_PTR, data, len) + res = slice_hash(data, len) all_resulting_hashes[i] = res return - -def slice_hash(seed, data, len: Const): +@inline +def slice_hash(data, len): states = Array(len * VECTOR_LEN) poseidon16(ZERO_VEC_PTR, data, states, COMPRESSION) state_indexes = Array(len) From e9177d744ee2deea74d645c40633f7f5d1859042 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 3 Feb 2026 17:45:24 +0400 Subject: [PATCH 39/47] wip --- crates/lean_prover/src/lib.rs | 2 +- crates/rec_aggregation/hashing.py | 10 ++++++++++ crates/rec_aggregation/src/recursion.rs | 4 +--- crates/rec_aggregation/utils.py | 3 +++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/lean_prover/src/lib.rs b/crates/lean_prover/src/lib.rs index e795baa1..4a17eb77 100644 --- a/crates/lean_prover/src/lib.rs +++ b/crates/lean_prover/src/lib.rs @@ -21,7 +21,7 @@ pub const SECURITY_BITS: usize = 123; // TODO 128 bits security? (with Poseidon // Provable security (no proximity gaps conjectures) pub const SECURITY_REGIME: SecurityAssumption = SecurityAssumption::JohnsonBound; -pub const GRINDING_BITS: usize = 16; +pub const GRINDING_BITS: usize = 18; pub const STARTING_LOG_INV_RATE: usize = 2; diff --git a/crates/rec_aggregation/hashing.py b/crates/rec_aggregation/hashing.py index 51889d1f..0831e89d 100644 --- a/crates/rec_aggregation/hashing.py +++ b/crates/rec_aggregation/hashing.py @@ -18,9 +18,19 @@ def batch_hash_slice(num_queries, all_data_to_hash, all_resulting_hashes, len): if len == 16: batch_hash_slice_const(num_queries, all_data_to_hash, all_resulting_hashes, 16) return + if len == 8: + batch_hash_slice_const(num_queries, all_data_to_hash, all_resulting_hashes, 8) + return + if len == 20: + batch_hash_slice_const(num_queries, all_data_to_hash, all_resulting_hashes, 20) + return if len == 1: batch_hash_slice_const(num_queries, all_data_to_hash, all_resulting_hashes, 1) return + if len == 4: + batch_hash_slice_const(num_queries, all_data_to_hash, all_resulting_hashes, 4) + return + print(len) assert False, "batch_hash_slice called with unsupported len" diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index 68103c0d..cd214108 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -21,9 +21,7 @@ pub fn run_recursion_benchmark(count: usize, tracing: bool) { .unwrap() .to_string(); - let mut inner_whir_config = default_whir_config(); - inner_whir_config.folding_factor = FoldingFactor::new(3, 4); - inner_whir_config.rs_domain_initial_reduction_factor = 1; + let inner_whir_config = default_whir_config(); let program_to_prove = r#" DIM = 5 COMPRESSION = 1 diff --git a/crates/rec_aggregation/utils.py b/crates/rec_aggregation/utils.py index dc5e5b95..f20248c7 100644 --- a/crates/rec_aggregation/utils.py +++ b/crates/rec_aggregation/utils.py @@ -174,6 +174,9 @@ def dot_product_be_dynamic(a, b, res, n): def dot_product_ee_dynamic(a, b, res, n): + if n == 32: + dot_product(a, b, res, 32, EE) + return if n == 16: dot_product(a, b, res, 16, EE) return From bcfae37b7e9140f114895dd983c8aca6f4964c41 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 3 Feb 2026 18:27:03 +0400 Subject: [PATCH 40/47] wip --- crates/rec_aggregation/utils.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/rec_aggregation/utils.py b/crates/rec_aggregation/utils.py index f20248c7..96b63bc3 100644 --- a/crates/rec_aggregation/utils.py +++ b/crates/rec_aggregation/utils.py @@ -107,7 +107,7 @@ def eq_mle_extension(a, b, n): return current_prod - +@inline def eq_mle_base_extension(a, b, n): debug_assert(n < 26) debug_assert(0 < n) @@ -210,7 +210,7 @@ def mle_of_01234567_etc(point, n): res = add_extension_ret(b, d) return res - +@inline def checked_less_than(a, b): res: Imu hint_less_than(a, b, res) @@ -221,7 +221,7 @@ def checked_less_than(a, b): assert b <= a return res - +@inline def maximum(a, b): is_a_less_than_b = checked_less_than(a, b) res: Imu @@ -231,7 +231,7 @@ def maximum(a, b): res = a return res - +@inline def powers_of_two(n): debug_assert(n < 32) res = match_range(n, range(0, 32), lambda i: 2**i) @@ -308,6 +308,7 @@ def mul_base_extension_ret(a, b): return res +@inline def div_extension_ret(n, d): quotient = Array(DIM) dot_product(d, quotient, n, 1, EE) @@ -402,9 +403,9 @@ def copy_16(a, b): a[15] = b[15] return - +@inline def copy_many_ef(a, b, n): - for i in range(0, n): + for i in unroll(0, n): dot_product(a + i * DIM, ONE_VEC_PTR, b + i * DIM, 1, EE) return From ce31aa9d8d53472f4757333a1fa0c8d4acb53cf9 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 4 Feb 2026 12:45:32 +0400 Subject: [PATCH 41/47] wip --- crates/lean_prover/src/trace_gen.rs | 6 +++--- crates/rec_aggregation/utils.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/lean_prover/src/trace_gen.rs b/crates/lean_prover/src/trace_gen.rs index 237ecb33..3fe0869d 100644 --- a/crates/lean_prover/src/trace_gen.rs +++ b/crates/lean_prover/src/trace_gen.rs @@ -37,13 +37,13 @@ pub fn get_execution_trace(bytecode: &Bytecode, execution_result: ExecutionResul // flag_a == 0 addr_a = F::from_usize(fp) + field_repr[instr_idx(COL_OPERAND_A)]; // fp + operand_a } - let value_a = memory.0[addr_a.to_usize()].unwrap(); + let value_a = memory.0[addr_a.to_usize()].unwrap_or_default(); let mut addr_b = F::ZERO; if field_repr[instr_idx(COL_FLAG_B)].is_zero() { // flag_b == 0 addr_b = F::from_usize(fp) + field_repr[instr_idx(COL_OPERAND_B)]; // fp + operand_b } - let value_b = memory.0[addr_b.to_usize()].unwrap(); + let value_b = memory.0[addr_b.to_usize()].unwrap_or_default(); let mut addr_c = F::ZERO; if field_repr[instr_idx(COL_FLAG_C)].is_zero() { @@ -54,7 +54,7 @@ pub fn get_execution_trace(bytecode: &Bytecode, execution_result: ExecutionResul assert_eq!(field_repr[instr_idx(COL_OPERAND_C)], operand_c); // debug purpose addr_c = value_a + operand_c; } - let value_c = memory.0[addr_c.to_usize()].unwrap(); + let value_c = memory.0[addr_c.to_usize()].unwrap_or_default(); for (j, field) in field_repr.iter().enumerate() { *trace_row[j + N_RUNTIME_COLUMNS] = *field; diff --git a/crates/rec_aggregation/utils.py b/crates/rec_aggregation/utils.py index 96b63bc3..0495449d 100644 --- a/crates/rec_aggregation/utils.py +++ b/crates/rec_aggregation/utils.py @@ -434,7 +434,8 @@ def read_memory(ptr): return mem[ptr] -def univariate_polynomial_eval(coeffs, point, degree: Const): +@inline +def univariate_polynomial_eval(coeffs, point, degree): powers = powers_const(point, degree + 1) res = Array(DIM) dot_product(coeffs, powers, res, degree + 1, EE) From c6a642023c94eec16b9cfe99f8aa03894e7b8bf5 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 4 Feb 2026 13:19:18 +0400 Subject: [PATCH 42/47] wip --- crates/lean_prover/src/lib.rs | 4 ++-- crates/rec_aggregation/utils.py | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/lean_prover/src/lib.rs b/crates/lean_prover/src/lib.rs index 4a17eb77..9ee2afaa 100644 --- a/crates/lean_prover/src/lib.rs +++ b/crates/lean_prover/src/lib.rs @@ -27,10 +27,10 @@ pub const STARTING_LOG_INV_RATE: usize = 2; pub fn default_whir_config() -> WhirConfigBuilder { WhirConfigBuilder { - folding_factor: FoldingFactor::new(7, 4), + folding_factor: FoldingFactor::new(7, 5), soundness_type: SECURITY_REGIME, pow_bits: GRINDING_BITS, - max_num_variables_to_send_coeffs: 6, + max_num_variables_to_send_coeffs: 9, rs_domain_initial_reduction_factor: 5, security_level: SECURITY_BITS, starting_log_inv_rate: STARTING_LOG_INV_RATE, diff --git a/crates/rec_aggregation/utils.py b/crates/rec_aggregation/utils.py index 0495449d..e01eb8a0 100644 --- a/crates/rec_aggregation/utils.py +++ b/crates/rec_aggregation/utils.py @@ -15,6 +15,7 @@ LITTLE_ENDIAN = 1 +@inline def powers(alpha, n): # alpha: EF # n: F @@ -35,6 +36,7 @@ def powers_const(alpha, n: Const): return res +@inline def unit_root_pow_dynamic(domain_size, index_bits): # index_bits is a pointer to domain_size bits debug_assert(domain_size < 26) @@ -74,7 +76,8 @@ def poly_eq_extension(point, n: Const): return res + (2**n - 1) * DIM -def poly_eq_base(point, n: Const): +@inline +def poly_eq_base(point, n): # Example: for n = 2: eq(x, y) = [(1 - x)(1 - y), (1 - x)y, x(1 - y), xy] res = Array((2 ** (n + 1) - 1)) @@ -86,10 +89,17 @@ def poly_eq_base(point, n: Const): res[2 ** (s + 1) - 1 + i] = res[2**s - 1 + i] - res[2 ** (s + 1) - 1 + 2**s + i] return res + (2**n - 1) + def eq_mle_extension(a, b, n): + debug_assert(n < 30) + debug_assert(0 < n) + res = match_range(n, range(1, 30), lambda i: eq_mle_extension_const(a, b, i)) + return res + +def eq_mle_extension_const(a, b, n: Const): buff = Array(n * DIM) - for i in range(0, n): + for i in unroll(0, n): shift = i * DIM ai = a + shift bi = b + shift @@ -100,13 +110,14 @@ def eq_mle_extension(a, b, n): buffi[j] = 2 * ab[j] - ai[j] - bi[j] current_prod: Mut = buff - for i in range(0, n - 1): + for i in unroll(0, n - 1): next_prod = Array(DIM) mul_extension(current_prod, buff + (i + 1) * DIM, next_prod) current_prod = next_prod return current_prod + @inline def eq_mle_base_extension(a, b, n): debug_assert(n < 26) From d5dfb1141b767088661643a313d44043d6e12f08 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Wed, 4 Feb 2026 13:33:58 +0400 Subject: [PATCH 43/47] wip --- crates/lean_prover/src/prove_execution.rs | 7 +++++++ crates/lean_prover/src/trace_gen.rs | 2 ++ crates/lean_vm/src/tables/table_trait.rs | 4 +++- crates/rec_aggregation/hashing.py | 3 +++ crates/rec_aggregation/utils.py | 3 +++ 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/lean_prover/src/prove_execution.rs b/crates/lean_prover/src/prove_execution.rs index 6854b492..4be1c383 100644 --- a/crates/lean_prover/src/prove_execution.rs +++ b/crates/lean_prover/src/prove_execution.rs @@ -59,6 +59,13 @@ pub fn prove_execution( .collect::>(), ); + let mut table_log = String::new(); + for (table, trace) in &traces { + table_log.push_str(&format!("{}: 2^{:.2} rows |", table.name(), f64::log2(trace.non_padded_n_rows as f64))); + } + table_log.pop(); // remove last '|' + info_span!("Trace tables sizes: {}", table_log).in_scope(|| {}); + // TODO parrallelize let mut memory_acc = F::zero_vec(memory.len()); info_span!("Building memory access count").in_scope(|| { diff --git a/crates/lean_prover/src/trace_gen.rs b/crates/lean_prover/src/trace_gen.rs index 3fe0869d..f487ded8 100644 --- a/crates/lean_prover/src/trace_gen.rs +++ b/crates/lean_prover/src/trace_gen.rs @@ -98,6 +98,7 @@ pub fn get_execution_trace(bytecode: &Bytecode, execution_result: ExecutionResul TableTrace { base: Vec::from(main_trace), ext: vec![], + non_padded_n_rows: n_cycles, log_n_rows: log2_ceil_usize(n_cycles), }, ); @@ -122,6 +123,7 @@ fn padd_table(table: &Table, traces: &mut BTreeMap) { .enumerate() .for_each(|(i, col)| assert_eq!(col.len(), h, "column {}, table {}", i, table.name())); + trace.non_padded_n_rows = h; trace.log_n_rows = log2_ceil_usize(h + 1).max(MIN_LOG_N_ROWS_PER_TABLE); let padding_len = (1 << trace.log_n_rows) - h; let padding_row_f = table.padding_row_f(); diff --git a/crates/lean_vm/src/tables/table_trait.rs b/crates/lean_vm/src/tables/table_trait.rs index 0198f9c3..ea4f4272 100644 --- a/crates/lean_vm/src/tables/table_trait.rs +++ b/crates/lean_vm/src/tables/table_trait.rs @@ -54,6 +54,7 @@ pub enum BusTable { pub struct TableTrace { pub base: Vec>, pub ext: Vec>, + pub non_padded_n_rows: usize, pub log_n_rows: VarCount, } @@ -62,7 +63,8 @@ impl TableTrace { Self { base: vec![Vec::new(); air.n_columns_f_total()], ext: vec![Vec::new(); air.n_columns_ef_total()], - log_n_rows: 0, // filled later + non_padded_n_rows: 0, // filled later + log_n_rows: 0, // filled later } } } diff --git a/crates/rec_aggregation/hashing.py b/crates/rec_aggregation/hashing.py index 0831e89d..957999fa 100644 --- a/crates/rec_aggregation/hashing.py +++ b/crates/rec_aggregation/hashing.py @@ -30,6 +30,9 @@ def batch_hash_slice(num_queries, all_data_to_hash, all_resulting_hashes, len): if len == 4: batch_hash_slice_const(num_queries, all_data_to_hash, all_resulting_hashes, 4) return + if len == 5: + batch_hash_slice_const(num_queries, all_data_to_hash, all_resulting_hashes, 5) + return print(len) assert False, "batch_hash_slice called with unsupported len" diff --git a/crates/rec_aggregation/utils.py b/crates/rec_aggregation/utils.py index e01eb8a0..66d1bc26 100644 --- a/crates/rec_aggregation/utils.py +++ b/crates/rec_aggregation/utils.py @@ -205,6 +205,9 @@ def dot_product_ee_dynamic(a, b, res, n): if n == WHIR_NUM_QUERIES[i] + 1: dot_product(a, b, res, WHIR_NUM_QUERIES[i] + 1, EE) return + if n == 8: + dot_product(a, b, res, 8, EE) + return assert False, "dot_product_ee_dynamic called with unsupported n" From 01fdcb341a4aad44e523de4c6b4388d4c0323872 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 5 Feb 2026 14:35:31 +0400 Subject: [PATCH 44/47] fix max table heights to avoid overflow in logup --- crates/lean_vm/src/core/constants.rs | 43 +++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/crates/lean_vm/src/core/constants.rs b/crates/lean_vm/src/core/constants.rs index b869cd64..21dd3836 100644 --- a/crates/lean_vm/src/core/constants.rs +++ b/crates/lean_vm/src/core/constants.rs @@ -15,10 +15,10 @@ pub const MAX_RUNNER_MEMORY_SIZE: usize = 1 << 24; /// Minimum and maximum number of rows per table (as powers of two), both inclusive pub const MIN_LOG_N_ROWS_PER_TABLE: usize = 8; // Zero padding will be added to each at least, if this minimum is not reached, (ensuring AIR / GKR work fine, with SIMD, without too much edge cases). Long term, we should find a more elegant solution. pub const MAX_LOG_N_ROWS_PER_TABLE: [(Table, usize); 3] = [ - (Table::execution(), 29), // 3 lookups - (Table::dot_product(), 25), // 4 lookups - (Table::poseidon16(), 25), // 4 lookups -]; // No overflow in logup: (TODO triple check) 3.2^29 + 4.2^25 + 4.2^25 < p = 2^31 - 2^24 + 1 + (Table::execution(), 29), + (Table::dot_product(), 24), + (Table::poseidon16(), 23), +]; /// Starting program counter pub const STARTING_PC: usize = 1; @@ -57,3 +57,38 @@ pub const NONRESERVED_PROGRAM_INPUT_START: usize = (POSEIDON_16_NULL_HASH_PTR + /// The first element of basis corresponds to one pub const ONE_VEC_PTR: usize = EXTENSION_BASIS_PTR; + +#[cfg(test)] +mod tests { + use multilinear_toolkit::prelude::PrimeField64; + use p3_util::log2_ceil_usize; + + use crate::{DIMENSION, F, MAX_LOG_N_ROWS_PER_TABLE, Table, TableT}; + + /// CRITICAL FOUR SOUNDNESS: TODO tripple check + #[test] + fn ensure_no_overflow_in_logup() { + fn memory_lookups_count(t: &T) -> usize { + t.lookups_f().iter().map(|l| l.values.len()).sum::() + t.lookups_ef().len() * DIMENSION + } + // memory lookup + let mut max_memory_logup_sum: u64 = 0; + for (table, max_log_n_rows) in MAX_LOG_N_ROWS_PER_TABLE { + let n_rows = 1 << max_log_n_rows; + let num_lookups = memory_lookups_count(&table); + max_memory_logup_sum += (num_lookups * n_rows) as u64; + println!("Table {} has {} memory lookups", table.name(), num_lookups * n_rows); + } + assert!(max_memory_logup_sum < F::ORDER_U64); + + // bytecode lookup + assert!( + MAX_LOG_N_ROWS_PER_TABLE + .iter() + .find(|(table, _)| *table == Table::execution()) + .unwrap() + .1 + < log2_ceil_usize(F::ORDER_U64 as usize) + ); + } +} From c6ec8283e4bb04a6722b68eef31ec8a42f5ad13c Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 5 Feb 2026 15:12:15 +0400 Subject: [PATCH 45/47] fix check_air_validity (debugging purpose) --- crates/air/src/prove.rs | 10 +--------- crates/air/src/validity_check.rs | 16 ++++++++++------ src/prove_poseidons.rs | 2 -- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/crates/air/src/prove.rs b/crates/air/src/prove.rs index 51cf62ca..48fdb96d 100644 --- a/crates/air/src/prove.rs +++ b/crates/air/src/prove.rs @@ -37,15 +37,7 @@ where "TODO handle the case UNIVARIATE_SKIPS >= log_length" ); - // crate::check_air_validity( - // air, - // &extra_data, - // &columns_f, - // &columns_ef, - // last_row_shifted_f, - // last_row_shifted_ef, - // ) - // .unwrap(); + // crate::check_air_validity(air, &extra_data, &columns_f, &columns_ef).unwrap(); assert!(extra_data.alpha_powers().len() >= air.n_constraints() + virtual_column_statement.is_some() as usize); diff --git a/crates/air/src/validity_check.rs b/crates/air/src/validity_check.rs index 1afd6007..b5c407de 100644 --- a/crates/air/src/validity_check.rs +++ b/crates/air/src/validity_check.rs @@ -8,8 +8,6 @@ pub fn check_air_validity>>( extra_data: &A::ExtraData, columns_f: &[&[PF]], columns_ef: &[&[EF]], - last_row_f: &[PF], - last_row_ef: &[EF], ) -> Result<(), String> { let n_rows = columns_f[0].len(); assert!(columns_f.iter().all(|col| col.len() == n_rows)); @@ -67,13 +65,19 @@ pub fn check_air_validity>>( let up_ef = (0..air.n_columns_ef_air()) .map(|j| columns_ef[j][n_rows - 1]) .collect::>(); - assert_eq!(last_row_f.len(), air.n_down_columns_f()); - assert_eq!(last_row_ef.len(), air.n_down_columns_ef()); let mut constraints_checker = ConstraintChecker { up_f, up_ef, - down_f: last_row_f.to_vec(), - down_ef: last_row_ef.to_vec(), + down_f: air + .down_column_indexes_f() + .iter() + .map(|j| columns_f[*j][n_rows - 1]) + .collect::>(), + down_ef: air + .down_column_indexes_ef() + .iter() + .map(|j| columns_ef[*j][n_rows - 1]) + .collect::>(), constraint_index: 0, errors: Vec::new(), }; diff --git a/src/prove_poseidons.rs b/src/prove_poseidons.rs index ff4e582e..ec863b76 100644 --- a/src/prove_poseidons.rs +++ b/src/prove_poseidons.rs @@ -52,8 +52,6 @@ pub fn benchmark_prove_poseidon_16(log_n_rows: usize, tracing: bool) { &ExtraDataForBuses::default(), &collect_refs(&trace), &[] as &[&[EF]], - &[], - &[], ) .unwrap(); From fc567c2a72a5061ebed80c09574b2a1c5072eeed Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 5 Feb 2026 16:58:31 +0400 Subject: [PATCH 46/47] Remove again 1 column from execution table (merge AUX_1 and AUX_2 into a single AUX) --- .../lean_compiler/src/instruction_encoder.rs | 10 +++--- crates/lean_vm/src/tables/dot_product/air.rs | 34 +++++++------------ crates/lean_vm/src/tables/dot_product/exec.rs | 2 ++ crates/lean_vm/src/tables/dot_product/mod.rs | 5 +-- crates/lean_vm/src/tables/execution/air.rs | 26 +++++++------- crates/lean_vm/src/tables/execution/mod.rs | 2 +- 6 files changed, 36 insertions(+), 43 deletions(-) diff --git a/crates/lean_compiler/src/instruction_encoder.rs b/crates/lean_compiler/src/instruction_encoder.rs index e0df511e..3ec29895 100644 --- a/crates/lean_compiler/src/instruction_encoder.rs +++ b/crates/lean_compiler/src/instruction_encoder.rs @@ -31,17 +31,17 @@ pub fn field_representation(instr: &Instruction) -> [F; N_INSTRUCTION_COLUMNS] { fields[instr_idx(COL_OPERAND_C)] = F::from_usize(*shift_1); match res { MemOrFpOrConstant::Constant(cst) => { - fields[instr_idx(COL_AUX_1)] = F::ONE; + fields[instr_idx(COL_AUX)] = F::ONE; fields[instr_idx(COL_FLAG_B)] = F::ONE; fields[instr_idx(COL_OPERAND_B)] = *cst; } MemOrFpOrConstant::MemoryAfterFp { offset } => { - fields[instr_idx(COL_AUX_1)] = F::ONE; + fields[instr_idx(COL_AUX)] = F::ONE; fields[instr_idx(COL_FLAG_B)] = F::ZERO; fields[instr_idx(COL_OPERAND_B)] = F::from_usize(*offset); } MemOrFpOrConstant::Fp => { - fields[instr_idx(COL_AUX_1)] = F::ZERO; + fields[instr_idx(COL_AUX)] = F::ZERO; fields[instr_idx(COL_FLAG_B)] = F::ONE; } } @@ -69,8 +69,8 @@ pub fn field_representation(instr: &Instruction) -> [F; N_INSTRUCTION_COLUMNS] { set_nu_a(&mut fields, arg_a); set_nu_b(&mut fields, arg_b); set_nu_c(&mut fields, arg_c); - fields[instr_idx(COL_AUX_1)] = F::from_usize(*aux_1); - fields[instr_idx(COL_AUX_2)] = F::from_usize(*aux_2); + assert!(*aux_2 == 0 || *aux_2 == 1); + fields[instr_idx(COL_AUX)] = F::from_usize(2 * *aux_1 + *aux_2); } } fields diff --git a/crates/lean_vm/src/tables/dot_product/air.rs b/crates/lean_vm/src/tables/dot_product/air.rs index 3d9e9157..1b0c0899 100644 --- a/crates/lean_vm/src/tables/dot_product/air.rs +++ b/crates/lean_vm/src/tables/dot_product/air.rs @@ -24,12 +24,13 @@ pub(super) const DOT_COL_IS_BE: usize = 0; pub(super) const DOT_COL_FLAG: usize = 1; pub(super) const DOT_COL_START: usize = 2; pub(super) const DOT_COL_LEN: usize = 3; -pub const DOT_COL_A: usize = 4; -pub(super) const DOT_COL_B: usize = 5; -pub(super) const DOT_COL_RES: usize = 6; -pub const DOT_COL_VALUE_A_F: usize = 7; +pub(super) const DOT_COL_AUX: usize = 4; // = DOT_COL_IS_BE + DOT_COL_LEN * 2 (used for data flow with execution table. Soundness: if an adversary tries to cheat and switch the value of DOT_COL_IS_BE, then DOT_COL_LEN would be > (p-1)/2 (otherwise no overflow, and thus no cheating), which force a an AIR table of height > 2^29, which would contradict MAX_LOG_N_ROWS_PER_TABLE (in constants.rs)) +pub(super) const DOT_COL_A: usize = 5; +pub(super) const DOT_COL_B: usize = 6; +pub(super) const DOT_COL_RES: usize = 7; +pub(super) const DOT_COL_VALUE_A_F: usize = 8; // EF columns -pub const DOT_COL_VALUE_A_EF: usize = 0; +pub(super) const DOT_COL_VALUE_A_EF: usize = 0; pub(super) const DOT_COL_VALUE_B: usize = 1; pub(super) const DOT_COL_VALUE_RES: usize = 2; pub(super) const DOT_COL_COMPUTATION: usize = 3; @@ -38,7 +39,7 @@ impl Air for DotProductPrecompile { type ExtraData = ExtraDataForBuses; fn n_columns_f_air(&self) -> usize { - 8 + 9 } fn n_columns_ef_air(&self) -> usize { 4 @@ -47,7 +48,7 @@ impl Air for DotProductPrecompile { 3 } fn n_constraints(&self) -> usize { - 15 // TODO: update + 16 // TODO: update } fn down_column_indexes_f(&self) -> Vec { vec![DOT_COL_START, DOT_COL_IS_BE, DOT_COL_LEN, DOT_COL_A, DOT_COL_B] @@ -67,6 +68,7 @@ impl Air for DotProductPrecompile { let flag = up_f[DOT_COL_FLAG].clone(); let start = up_f[DOT_COL_START].clone(); let len = up_f[DOT_COL_LEN].clone(); + let aux = up_f[DOT_COL_AUX].clone(); let index_a = up_f[DOT_COL_A].clone(); let index_b = up_f[DOT_COL_B].clone(); let index_res = up_f[DOT_COL_RES].clone(); @@ -90,23 +92,11 @@ impl Air for DotProductPrecompile { extra_data, AB::F::from_usize(self.table().index()), flag.clone(), - &[ - index_a.clone(), - index_b.clone(), - index_res.clone(), - len.clone(), - is_be.clone(), - ], + &[index_a.clone(), index_b.clone(), index_res.clone(), aux.clone()], )); } else { builder.declare_values(std::slice::from_ref(&flag)); - builder.declare_values(&[ - index_a.clone(), - index_b.clone(), - index_res.clone(), - len.clone(), - is_be.clone(), - ]); + builder.declare_values(&[index_a.clone(), index_b.clone(), index_res.clone(), aux.clone()]); } let is_ee = AB::F::ONE - is_be.clone(); @@ -116,6 +106,8 @@ impl Air for DotProductPrecompile { builder.assert_zero(flag.clone() * (AB::F::ONE - start.clone())); builder.assert_bool(is_be.clone()); + builder.assert_eq(aux, is_be.clone() + len.double()); + let mode_switch = (is_be_down.clone() - is_be.clone()).square(); builder.assert_zero(mode_switch.clone() * (AB::F::ONE - start_down.clone())); diff --git a/crates/lean_vm/src/tables/dot_product/exec.rs b/crates/lean_vm/src/tables/dot_product/exec.rs index 2333c72b..f1d1ad59 100644 --- a/crates/lean_vm/src/tables/dot_product/exec.rs +++ b/crates/lean_vm/src/tables/dot_product/exec.rs @@ -40,6 +40,7 @@ pub(super) fn exec_dot_product_be( trace.base[DOT_COL_START].push(F::ONE); trace.base[DOT_COL_START].extend(F::zero_vec(size - 1)); trace.base[DOT_COL_LEN].extend(((1..=size).rev()).map(F::from_usize)); + trace.base[DOT_COL_AUX].extend(((1..=size).rev()).map(|x| F::from_bool(true) + F::from_usize(2 * x))); trace.base[DOT_COL_A].extend((0..size).map(|i| F::from_usize(ptr_arg_0.to_usize() + i))); trace.base[DOT_COL_B].extend((0..size).map(|i| F::from_usize(ptr_arg_1.to_usize() + i * DIMENSION))); trace.base[DOT_COL_RES].extend(vec![F::from_usize(ptr_res.to_usize()); size]); @@ -126,6 +127,7 @@ pub(super) fn exec_dot_product_ee( trace.base[DOT_COL_START].push(F::ONE); trace.base[DOT_COL_START].extend(F::zero_vec(size - 1)); trace.base[DOT_COL_LEN].extend(((1..=size).rev()).map(F::from_usize)); + trace.base[DOT_COL_AUX].extend(((1..=size).rev()).map(|x| F::from_bool(false) + F::from_usize(2 * x))); trace.base[DOT_COL_A].extend((0..size).map(|i| F::from_usize(ptr_arg_0.to_usize() + i * DIMENSION))); trace.base[DOT_COL_B].extend((0..size).map(|i| F::from_usize(ptr_arg_1.to_usize() + i * DIMENSION))); trace.base[DOT_COL_RES].extend(vec![F::from_usize(ptr_res.to_usize()); size]); diff --git a/crates/lean_vm/src/tables/dot_product/mod.rs b/crates/lean_vm/src/tables/dot_product/mod.rs index 0c3a4d8e..9b6752f6 100644 --- a/crates/lean_vm/src/tables/dot_product/mod.rs +++ b/crates/lean_vm/src/tables/dot_product/mod.rs @@ -51,7 +51,7 @@ impl TableT for DotProductPrecompile { table: BusTable::Constant(self.table()), direction: BusDirection::Pull, selector: DOT_COL_FLAG, - data: vec![DOT_COL_A, DOT_COL_B, DOT_COL_RES, DOT_COL_LEN, DOT_COL_IS_BE], + data: vec![DOT_COL_A, DOT_COL_B, DOT_COL_RES, DOT_COL_AUX], } } @@ -62,8 +62,9 @@ impl TableT for DotProductPrecompile { F::ZERO, // Flag F::ONE, // Start F::ONE, // Len + F::TWO, // Aux (0 + 2*1) ], - vec![F::ZERO; self.n_columns_f_air() - 4], + vec![F::ZERO; self.n_columns_f_air() - 5], ] .concat() } diff --git a/crates/lean_vm/src/tables/execution/air.rs b/crates/lean_vm/src/tables/execution/air.rs index 0370f9c7..95d13879 100644 --- a/crates/lean_vm/src/tables/execution/air.rs +++ b/crates/lean_vm/src/tables/execution/air.rs @@ -2,7 +2,7 @@ use crate::{ALL_TABLES, EF, ExecutionTable, ExtraDataForBuses, F, eval_virtual_b use multilinear_toolkit::prelude::*; pub const N_RUNTIME_COLUMNS: usize = 8; -pub const N_INSTRUCTION_COLUMNS: usize = 13; +pub const N_INSTRUCTION_COLUMNS: usize = 12; pub const N_TOTAL_EXECUTION_COLUMNS: usize = N_INSTRUCTION_COLUMNS + N_RUNTIME_COLUMNS; // Committed columns (IMPORTANT: they must be the first columns) @@ -26,16 +26,15 @@ pub const COL_ADD: usize = 14; pub const COL_MUL: usize = 15; pub const COL_DEREF: usize = 16; pub const COL_JUMP: usize = 17; -pub const COL_AUX_1: usize = 18; -pub const COL_AUX_2: usize = 19; -pub const COL_PRECOMPILE_INDEX: usize = 20; +pub const COL_AUX: usize = 18; +pub const COL_PRECOMPILE_INDEX: usize = 19; // Temporary columns (stored to avoid duplicate computations) pub const N_TEMPORARY_EXEC_COLUMNS: usize = 4; -pub const COL_IS_PRECOMPILE: usize = 21; -pub const COL_EXEC_NU_A: usize = 22; -pub const COL_EXEC_NU_B: usize = 23; -pub const COL_EXEC_NU_C: usize = 24; +pub const COL_IS_PRECOMPILE: usize = 20; +pub const COL_EXEC_NU_A: usize = 21; +pub const COL_EXEC_NU_B: usize = 22; +pub const COL_EXEC_NU_C: usize = 23; const PRECOMPILE_A_INDEX: F = F::new(ALL_TABLES[1].index() as u32); const PRECOMPILE_B_INDEX: F = F::new(ALL_TABLES[2].index() as u32); @@ -96,8 +95,7 @@ impl Air for ExecutionTable { let mul = up[COL_MUL].clone(); let deref = up[COL_DEREF].clone(); let jump = up[COL_JUMP].clone(); - let aux_1 = up[COL_AUX_1].clone(); - let aux_2 = up[COL_AUX_2].clone(); + let aux = up[COL_AUX].clone(); let precompile_index = up[COL_PRECOMPILE_INDEX].clone(); let (value_a, value_b, value_c) = ( @@ -142,11 +140,11 @@ impl Air for ExecutionTable { extra_data, precompile_index.clone(), is_precompile.clone(), - &[nu_a.clone(), nu_b.clone(), nu_c.clone(), aux_1.clone(), aux_2.clone()], + &[nu_a.clone(), nu_b.clone(), nu_c.clone(), aux.clone()], )); } else { builder.declare_values(&[is_precompile]); - builder.declare_values(&[nu_a.clone(), nu_b.clone(), nu_c.clone(), aux_1.clone(), aux_2.clone()]); + builder.declare_values(&[nu_a.clone(), nu_b.clone(), nu_c.clone(), aux.clone()]); } builder.assert_zero(flag_a_minus_one * (addr_a.clone() - fp_plus_operand_a)); @@ -157,8 +155,8 @@ impl Air for ExecutionTable { builder.assert_zero(mul * (nu_b.clone() - nu_a.clone() * nu_c.clone())); builder.assert_zero(deref.clone() * (addr_c.clone() - (value_a.clone() + operand_c.clone()))); - builder.assert_zero(deref.clone() * aux_1.clone() * (value_c.clone() - nu_b.clone())); - builder.assert_zero(deref.clone() * (aux_1.clone() - AB::F::ONE) * (value_c.clone() - fp.clone())); + builder.assert_zero(deref.clone() * aux.clone() * (value_c.clone() - nu_b.clone())); + builder.assert_zero(deref.clone() * (aux.clone() - AB::F::ONE) * (value_c.clone() - fp.clone())); builder.assert_zero((jump.clone() - AB::F::ONE) * (next_pc.clone() - pc_plus_one.clone())); builder.assert_zero((jump.clone() - AB::F::ONE) * (next_fp.clone() - fp.clone())); diff --git a/crates/lean_vm/src/tables/execution/mod.rs b/crates/lean_vm/src/tables/execution/mod.rs index b0fd7c52..0677ce07 100644 --- a/crates/lean_vm/src/tables/execution/mod.rs +++ b/crates/lean_vm/src/tables/execution/mod.rs @@ -50,7 +50,7 @@ impl TableT for ExecutionTable { table: BusTable::Variable(COL_PRECOMPILE_INDEX), direction: BusDirection::Push, selector: COL_IS_PRECOMPILE, - data: vec![COL_EXEC_NU_A, COL_EXEC_NU_B, COL_EXEC_NU_C, COL_AUX_1, COL_AUX_2], + data: vec![COL_EXEC_NU_A, COL_EXEC_NU_B, COL_EXEC_NU_C, COL_AUX], } } From 07f029f6461029cf2f179fcd982863d0d9119f89 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 5 Feb 2026 18:11:02 +0400 Subject: [PATCH 47/47] update doc --- README.md | 16 ++++++------- TODO.md | 2 +- crates/lean_prover/src/lib.rs | 6 ++--- crates/lean_prover/src/test_zkvm.rs | 18 +++++++++++--- crates/lean_vm/src/tables/execution/air.rs | 2 +- crates/rec_aggregation/src/recursion.rs | 8 ++++--- crates/rec_aggregation/src/xmss_aggregate.rs | 5 ++-- minimal_zkVM.pdf | Bin 303234 -> 302506 bytes misc/minimal_zkVM.tex | 24 ++++++++----------- 9 files changed, 45 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index c89c64b4..6271a1df 100644 --- a/README.md +++ b/README.md @@ -11,25 +11,25 @@ Documentation: [PDF](minimal_zkVM.pdf) ## Proving System -- multilinear with [WHIR](https://eprint.iacr.org/2024/1586.pdf) +- multilinear with [WHIR](https://eprint.iacr.org/2024/1586.pdf), allowing polynomial stacking (reducing proof size) - [SuperSpartan](https://eprint.iacr.org/2023/552.pdf), with [AIR-specific optimizations](https://solvable.group/posts/super-air/#fnref:1) -- [Logup](https://eprint.iacr.org/2023/1284.pdf) / [Logup*](https://eprint.iacr.org/2025/946.pdf) +- [Logup](https://eprint.iacr.org/2023/1284.pdf), with a system of buses similar to [OpenVM](https://openvm.dev/whitepaper.pdf) The VM design is inspired by the famous [Cairo paper](https://eprint.iacr.org/2021/1063.pdf). ## Security -123 bits of security. Johnson bound + degree 5 extension of koala-bear -> **no proximity gaps conjecture**. (TODO 128 bits, which requires hash digests bigger than 8 koala-bears). +123 bits of security. Johnson bound + degree 5 extension of koala-bear -> **no proximity gaps conjecture**. (TODO 128 bits? this would require hash digests bigger than 8 koala-bears). -## Benchmarks +## Benchmarks (Slightly outdated, new benchmarks incoming) Machine: M4 Max 48GB (CPU only) | Benchmark | Current | Target | | -------------------------- | -------------------- | --------------- | | Poseidon2 (16 koala-bears) | `560K Poseidon2 / s` | n/a | -| 2 -> 1 Recursion | `1.35 s` | `0.25 s ` | +| 2 -> 1 Recursion | `1.15 s` | `0.25 s ` | | XMSS aggregation | `554 XMSS / s` | `1000 XMSS / s` | *Expect incoming perf improvements.* @@ -39,11 +39,11 @@ To reproduce: - `cargo run --release -- recursion --n 2` - `cargo run --release -- xmss --n-signatures 1350` -(Small detail remaining in recursion: final (multilinear) evaluation of the guest program bytecode, there are multiple ways of handling it... TBD soon) - ## Proof size -WHIR intial rate = 1/4. Proof size ≈ 325 KiB. TODO: WHIR batch opening + [2024/108](https://eprint.iacr.org/2024/108.pdf) section 3.1 -> close to 256 KiB. (To go below 256 KiB -> rate 1/8 or 1/16 in the final recursion). +WHIR intial rate = 1/4 -> proof size ≈ 225 KiB. (150 KiB with rate 1/16, and < 100 KiB is possible with poximity gaps conjecture + rate 1/16). + +(TODO: remaining optimization = [2024/108](https://eprint.iacr.org/2024/108.pdf) section 3.1) ## Credits diff --git a/TODO.md b/TODO.md index 9bb2b600..b64b7fc7 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,7 @@ - 128 bits security - Merkle pruning - the interpreter of leanISA (+ witness generation) can be partially parallelized when there are some independent loops -- Make everything "padding aware" (including WHIR, logup*, AIR, etc) +- Make everything "padding aware" (including WHIR, logup, AIR, etc) - Opti WHIR: in sumcheck we know more than f(0) + f(1), we know f(0) and f(1) - Opti WHIR https://github.com/tcoratger/whir-p3/issues/303 and https://github.com/tcoratger/whir-p3/issues/306 ? - Avoid the embedding overhead in logup, when denominators = "c - index" diff --git a/crates/lean_prover/src/lib.rs b/crates/lean_prover/src/lib.rs index 9ee2afaa..b235a92e 100644 --- a/crates/lean_prover/src/lib.rs +++ b/crates/lean_prover/src/lib.rs @@ -23,9 +23,7 @@ pub const SECURITY_REGIME: SecurityAssumption = SecurityAssumption::JohnsonBound pub const GRINDING_BITS: usize = 18; -pub const STARTING_LOG_INV_RATE: usize = 2; - -pub fn default_whir_config() -> WhirConfigBuilder { +pub fn default_whir_config(starting_log_inv_rate: usize) -> WhirConfigBuilder { WhirConfigBuilder { folding_factor: FoldingFactor::new(7, 5), soundness_type: SECURITY_REGIME, @@ -33,6 +31,6 @@ pub fn default_whir_config() -> WhirConfigBuilder { max_num_variables_to_send_coeffs: 9, rs_domain_initial_reduction_factor: 5, security_level: SECURITY_BITS, - starting_log_inv_rate: STARTING_LOG_INV_RATE, + starting_log_inv_rate, } } diff --git a/crates/lean_prover/src/test_zkvm.rs b/crates/lean_prover/src/test_zkvm.rs index 22a7cf8d..4f0dedea 100644 --- a/crates/lean_prover/src/test_zkvm.rs +++ b/crates/lean_prover/src/test_zkvm.rs @@ -149,15 +149,22 @@ fn test_zk_vm_helper(program_str: &str, (public_input, private_input): (&[F], &[ } let bytecode = compile_program(&ProgramSource::Raw(program_str.to_string())); let time = std::time::Instant::now(); + let starting_log_inv_rate = 1; let proof = prove_execution( &bytecode, (public_input, private_input), &vec![], - &default_whir_config(), + &default_whir_config(starting_log_inv_rate), false, ); let proof_time = time.elapsed(); - verify_execution(&bytecode, public_input, proof.proof.clone(), &default_whir_config()).unwrap(); + verify_execution( + &bytecode, + public_input, + proof.proof.clone(), + &default_whir_config(starting_log_inv_rate), + ) + .unwrap(); println!("{}", proof.exec_summary); println!("Proof time: {:.3} s", proof_time.as_secs_f32()); @@ -172,7 +179,12 @@ fn test_zk_vm_helper(program_str: &str, (public_input, private_input): (&[F], &[ } let mut fuzzed_proof = proof.proof.clone(); fuzzed_proof[i] += F::ONE; - let verify_result = verify_execution(&bytecode, public_input, fuzzed_proof, &default_whir_config()); + let verify_result = verify_execution( + &bytecode, + public_input, + fuzzed_proof, + &default_whir_config(starting_log_inv_rate), + ); assert!(verify_result.is_err(), "Fuzzing failed at index {}", i); } } diff --git a/crates/lean_vm/src/tables/execution/air.rs b/crates/lean_vm/src/tables/execution/air.rs index 95d13879..4964207e 100644 --- a/crates/lean_vm/src/tables/execution/air.rs +++ b/crates/lean_vm/src/tables/execution/air.rs @@ -15,7 +15,7 @@ pub const COL_MEM_VALUE_A: usize = 5; pub const COL_MEM_VALUE_B: usize = 6; pub const COL_MEM_VALUE_C: usize = 7; -// Decoded instruction columns (lookup into bytecode with logup*) +// Decoded instruction columns pub const COL_OPERAND_A: usize = 8; pub const COL_OPERAND_B: usize = 9; pub const COL_OPERAND_C: usize = 10; diff --git a/crates/rec_aggregation/src/recursion.rs b/crates/rec_aggregation/src/recursion.rs index cac4c22a..5fe842ae 100644 --- a/crates/rec_aggregation/src/recursion.rs +++ b/crates/rec_aggregation/src/recursion.rs @@ -14,6 +14,8 @@ use multilinear_toolkit::prelude::symbolic::{ use multilinear_toolkit::prelude::*; use utils::{BYTECODE_TABLE_INDEX, Counter, MEMORY_TABLE_INDEX}; +const LOG_INV_RATE: usize = 2; + pub fn run_recursion_benchmark(count: usize, tracing: bool) { let filepath = Path::new(env!("CARGO_MANIFEST_DIR")) .join("recursion.py") @@ -21,7 +23,7 @@ pub fn run_recursion_benchmark(count: usize, tracing: bool) { .unwrap() .to_string(); - let inner_whir_config = default_whir_config(); + let inner_whir_config = default_whir_config(LOG_INV_RATE); let program_to_prove = r#" DIM = 5 POSEIDON_OF_ZERO = POSEIDON_OF_ZERO_PLACEHOLDER @@ -331,7 +333,7 @@ def main(): &recursion_bytecode, (&outer_public_input, &outer_private_input), &vec![], // TODO precompute poseidons - &default_whir_config(), + &default_whir_config(LOG_INV_RATE), false, ); let proving_time = time.elapsed(); @@ -339,7 +341,7 @@ def main(): &recursion_bytecode, &outer_public_input, recursion_proof.proof, - &default_whir_config(), + &default_whir_config(LOG_INV_RATE), ) .unwrap(); println!( diff --git a/crates/rec_aggregation/src/xmss_aggregate.rs b/crates/rec_aggregation/src/xmss_aggregate.rs index ee03f0c3..c61b2a08 100644 --- a/crates/rec_aggregation/src/xmss_aggregate.rs +++ b/crates/rec_aggregation/src/xmss_aggregate.rs @@ -14,6 +14,7 @@ use xmss::{ }; static XMSS_AGGREGATION_PROGRAM: OnceLock = OnceLock::new(); +const LOG_INV_RATE: usize = 1; fn get_xmss_aggregation_program() -> &'static Bytecode { XMSS_AGGREGATION_PROGRAM.get_or_init(compile_xmss_aggregation_program) @@ -142,7 +143,7 @@ fn xmss_aggregate_signatures_helper( program, (&public_input, &private_input), &poseidons_16_precomputed, - &default_whir_config(), + &default_whir_config(LOG_INV_RATE), false, ); @@ -166,7 +167,7 @@ pub fn xmss_verify_aggregated_signatures( let public_input = build_public_input(xmss_pub_keys, message_hash, slot); - verify_execution(program, &public_input, proof, &default_whir_config()).map(|_| ()) + verify_execution(program, &public_input, proof, &default_whir_config(LOG_INV_RATE)).map(|_| ()) } #[instrument(skip_all)] diff --git a/minimal_zkVM.pdf b/minimal_zkVM.pdf index 60d3102f7ddc283ec4d9a9606f9c7aee40b30c42..0756e1b73d07a051f636a555f78280e240c5c0de 100644 GIT binary patch delta 61353 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%0A@RD`*GnVvlVpd3CO=Jv+HkxTk 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 Gk^cvWFgZ&A delta 62030 zcmV(_K-9mgyAp!I5)vg)L`E$!E;kA#(A34)3NbM@lR=9U12#7>lMyv2f6ZE3bK5o+ ze)q57_GPJA3&E8jJ(JyuJ56WWW;cyJ?anxPpe0)3SRz%DO5*(bJqG|~kpe|I_QO62 zBEWOMd;md%ML>g}&*;eS<@MRCw?YR@6V8+juI~aS8Brn%qF4|`S#UiMZo+qYk+0L$ zWGW~Rzuo`w&W`TN$_{?Zf6~H^erlFkm2KDd-$$zGyh`sH{L4a0)MUy+Lg6`^e7gSS z)msJ3B@r^>$b70IA`=}t_1<7K{Vq2CZTiteUI>TKQ*uoRP^?auNQgaFo)`rYciQ z-abK9kYrQZ|J%A1qDhE zb}p^$Z}t_8kQePMe|celZ;(C&4GmaOVdUW8!_Q~NId;m%6Fm6js2_QUAH0KXbeI1&PLgnmF-yT>0e)Hki?kh_mvdVnMOc#qPTY$^T0^KEL_y(d&$Z!U) z{uPFa9o=n<87{ZWOfbSz7Zh1OFAHuT9Z^;uC`*iRe~>YWlwAq`oWvq@QhY%! zF6b=Ggp%k3j)wEsr3_r}dsQ5wE+R?9r; zcVk4r7_=g^;W=R_9t-QE0)a3wNtk?!H1D!%F)T^5ENf?tbSJi%c}yH*{OO@JuCZ81 zLBmzrKx*Tde_`@~sZENPE1T)HovE_fwyItBuO95H_Ft9XJ(wDV>ry*o0+rbDWr!_Sxwa%1 ztF3cdKYUtYQfdzbx}z;m7xCyU2DU7?j4*c9hvQyE*Fx94$>f z$GLLHe`GHJ4jt{3Meo&FHs}{)uztlicC| zU7!7R#vG4W5eg|K0fEpY31;iFn@=>D!yMFOq5&6tF)P<*Ny72HRd99oyTvh`ly7MX zMs)1b^v!BfR?wyqoWwF@gSBAi^j*pD3(7T#f1Xbz!5xQGL-dE7rBB->cea6xku?ObC_KYP< z>+qjmHMx#MI$#}a(OXa8bE*jwA`pore>{Fx-~+xON;5->#?DHQp;r%9l3nbJW3i7K zJT>;Q*6zcruI@rn$WZLhI#p@8uKc`}x$52bg5XtRs7{t;bj= zBvz~&xnxk2%f}*PL4R4)dEG#&J@n%`37|A;+&4ph2q{Rcd6y0A=R%Jm=E9H5f6Tyu z`wOpnh{MJTABg?h&Fm`<;X?@VZ#AE~6n9d)-?@)MTd8zp?Oe`n3hW7eTv z93n?bB?lP*Uldv8fxRLjQ4IbT;CJ>Me>2HRq=lg*T6bGWe#kX~#sbCc1-NQXVyKS$ zTn&`+dc^kI0ax!f<{qaEZX39WOs5#vWW1OrP`_D6lVh8P@ToWt%8r1PegR3L>}Z{I zBt4<(56e4*+F2BOz;&-(e~gl z6!69i@D;E_qLJ$IHCg<9sNO*E_%siWpn7a#yeHWRs>fI#4O@Zze~tA}PIN+l%Zj-> zIk$~&tVcM~dIUgmK@&a*pdX{z-J2EbkRF@XLQ6k z7XIAfxjgp=DU(q5e=tZRZW`Nj&i*6>Dc~t2rF3#)bF8QA6Yh)Le$<$D*hY|h#ZfCo zg798PWNf4*3Uj#BW{hHC<~t*01Ue&E6fl>3R%g_b#8G5OMO>cR8NJYK06m1mXC50b zJ_650^hE7O0Fnaz0p51BJBsCw!XxVytY8R2D;#7KHgFbpe{(mx=<-d^btkjU=Lij(&0$lKh1P9~tF z#pYYGXpfeDe=XBhdYPr}d)B7P*V%>F{dfwu8Ei4H?Qu5u9CI-$%@-L#BGw)NFi-e_ zeIo_IytnR@U3Zt4{mi#Vf=dqp535f6`uxvCOcXtOw#=|u@W+bMp}~;`z)u>BV>DoO zL?rA3`Xsp2L`3>oG+I^0N%U1YgmDQBfd-?B$f*5%e?0#@QIWZep~EI!Wc5iGEg*}j z7`)ryIWVr*yD!_FQ{jVS0%=1?Lxlcq@KAA;)$W7({(TiozACrH{F#?CJXrIqS6-

{SWq`>HDlOuhA- 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%0DU$S&7s8US^Zs#`?Ct$Di`;JWExVc5C<*kL=e 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 diff --git a/misc/minimal_zkVM.tex b/misc/minimal_zkVM.tex index 305a7b08..44b8a064 100644 --- a/misc/minimal_zkVM.tex +++ b/misc/minimal_zkVM.tex @@ -45,7 +45,7 @@ \newtheorem{lemma}{Lemma} -\title{Minimal zkVM for Lean Ethereum (draft 0.5.0)} +\title{Minimal zkVM for Lean Ethereum (draft 0.6.0)} \date{} \begin{document} @@ -335,15 +335,10 @@ \section{Proving system} \subsection{Execution table} -\subsubsection{Reduced commitment via logup*} - -In Cairo each instruction is encoded with 15 boolean flags, and 3 offsets. In the execution trace, this leads to committing to 18 field elements at each instruction. - -We can significantly reduce the commitments cost using logup*\cite{logup_star}. In the the execution table, we only need to commit to the pc column, and all the flags / offsets describing the current instruction can be fetched by an indexed lookup argument (for which logup* drastically reduces commitment costs). \subsubsection{Commitment} -\fbox{At each cycle, we commit to 8 (base) field elements:} +\fbox{At each cycle, we commit to 20 (base) field elements:} \begin{itemize} \item pc (program counter) @@ -351,18 +346,19 @@ \subsubsection{Commitment} % \item jump (non zero when a jump occurs) \item $\text{addr}_A$, $\text{addr}_B$, $\text{addr}_C$ \item $\text{value}_A = \textbf{m}[\text{addr}_A]$, $\text{value}_B = \textbf{m}[\text{addr}_B]$, $\text{value}_C = \textbf{m}[\text{addr}_C]$ + \item 12 field elements describing the instruction being executed (see below) \end{itemize} \subsubsection{Instruction Encoding} -Each instruction is described by 14 field elements: +Each instruction is described by 12 field elements: \begin{itemize} \item 3 operands ($\in \Fp$): $\text{operand}_A$, $\text{operand}_B$, $\text{operand}_C$ \item 3 associated flags ($\in \{0, 1\}$): $\text{flag}_A$, $\text{flag}_B$, $\text{flag}_C$ - \item 6 opcode flags ($\in \{0, 1\}$): ADD, MUL, DEREF, JUMP, IS\_PRECOMPILE, PRECOMPILE\_INDEX - \item 2 multi-purpose operands: AUX\_1, AUX\_2 + \item 5 opcode flags ($\in \{0, 1\}$): ADD, MUL, DEREF, JUMP, PRECOMPILE\_INDEX + \item 1 multi-purpose operand: AUX \end{itemize} @@ -525,7 +521,7 @@ \subsubsection{Buses: Data flow between tables} A detailled soundness analysis can be found in \href{https://github.com/openvm-org/stark-backend/blob/main/docs/Soundness_of_Interactions_via_LogUp.pdf}{Soundness of Interactions via LogUp}. -\section{Annex: simple packing of multilinear polynomials} +\section{Annex: simple stacking of multilinear polynomials} \textit{Note 1}: It's always possible to reduce $n$ claims about a multilinear polynomial to a single one, using sumcheck. But this trick is not necessary with WHIR, which natively supports an arbitrary number of claims about the committed polynomial. \vspace{3mm} @@ -590,8 +586,8 @@ \section{Annex: simple packing of multilinear polynomials} \node[above] at (20,1.6) {$P({\mathbf{1}}, {\mathbf{0}}, x_1, x_2, x_3)$}; \node[above] at (26,1.6) {$P({\mathbf{1}}, {\mathbf{1}}, {\mathbf{0}}, x_1, x_2)$}; \end{tikzpicture} -\caption{Simple packing of $P_1, P_2, P_3$ into a single polynomial $P$} -\label{fig:packing} +\caption{Simple stacking of $P_1, P_2, P_3$ into a single polynomial $P$} +\label{fig:stacking} \end{figure} Advantage of this approach: simplicity. @@ -600,7 +596,7 @@ \section{Annex: simple packing of multilinear polynomials} \vspace{5mm} -There are alterntive ways to handle the packing of multiple multilinear polynomials: +There are alterntive ways to handle the stacking of multiple multilinear polynomials: \begin{itemize} \item \textbf{Jagged PCS} \cite{jagged_pcs}: No padding overhead, at the cost of an additional sumcheck.