From e9ec21c7b2b0e95f30ba3570ad802005449ceed7 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Fri, 13 Mar 2026 16:43:59 -0700 Subject: [PATCH 01/20] fix(zkvm): make stage6 split compatible with zk flow Use phase-specific stage6a/stage6b sumcheck params and proof wiring so zk transcript and constraints stay aligned after splitting stage6. This preserves the split design while fixing zk failures without introducing bytecode-commitment features. Made-with: Cursor Co-authored-by: Quang Dao --- jolt-core/src/poly/opening_proof.rs | 2 + jolt-core/src/subprotocols/booleanity.rs | 549 ++++++++++++++++-- .../src/zkvm/bytecode/read_raf_checking.rs | 503 +++++++++++++++- jolt-core/src/zkvm/proof_serialization.rs | 11 +- jolt-core/src/zkvm/prover.rs | 130 ++++- jolt-core/src/zkvm/verifier.rs | 79 ++- jolt-core/src/zkvm/witness.rs | 2 + 7 files changed, 1192 insertions(+), 84 deletions(-) diff --git a/jolt-core/src/poly/opening_proof.rs b/jolt-core/src/poly/opening_proof.rs index dc2bfbd1b4..43c4631860 100644 --- a/jolt-core/src/poly/opening_proof.rs +++ b/jolt-core/src/poly/opening_proof.rs @@ -151,7 +151,9 @@ pub enum SumcheckId { RegistersClaimReduction, RegistersReadWriteChecking, RegistersValEvaluation, + BytecodeReadRafAddressPhase, BytecodeReadRaf, + BooleanityAddressPhase, Booleanity, AdviceClaimReductionCyclePhase, AdviceClaimReduction, diff --git a/jolt-core/src/subprotocols/booleanity.rs b/jolt-core/src/subprotocols/booleanity.rs index 1f3bf6cadd..c74d689ba8 100644 --- a/jolt-core/src/subprotocols/booleanity.rs +++ b/jolt-core/src/subprotocols/booleanity.rs @@ -446,6 +446,48 @@ impl BooleanitySumcheckProver { gruen_poly * self.eq_r_r } + + fn ingest_address_challenge(&mut self, r_j: F::Challenge, round: usize) { + self.B.bind(r_j); + self.F.update(r_j); + + if round == self.params.log_k_chunk - 1 { + self.eq_r_r = self.B.get_current_scalar(); + + let F_table = std::mem::take(&mut self.F); + let ra_indices = std::mem::take(&mut self.ra_indices); + let base_eq = F_table.clone_values(); + let num_polys = self.params.polynomial_types.len(); + debug_assert!( + num_polys == self.gamma_powers.len(), + "gamma_powers length mismatch: got {}, expected {}", + self.gamma_powers.len(), + num_polys + ); + let tables: Vec> = (0..num_polys) + .into_par_iter() + .map(|i| { + let rho = self.gamma_powers[i]; + base_eq.iter().map(|v| rho * *v).collect() + }) + .collect(); + self.H = Some(SharedRaPolynomials::new( + tables, + ra_indices, + self.params.one_hot_params.clone(), + )); + + let g = std::mem::take(&mut self.G); + drop_in_background_thread(g); + } + } + + fn ingest_cycle_challenge(&mut self, r_j: F::Challenge) { + self.D.bind(r_j); + if let Some(ref mut h) = self.H { + h.bind_in_place(r_j, BindingOrder::LowToHigh); + } + } } impl SumcheckInstanceProver for BooleanitySumcheckProver { @@ -465,48 +507,9 @@ impl SumcheckInstanceProver for BooleanitySum #[tracing::instrument(skip_all, name = "BooleanitySumcheckProver::ingest_challenge")] fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { if round < self.params.log_k_chunk { - // Phase 1: Bind B and update F - self.B.bind(r_j); - self.F.update(r_j); - - // Transition to phase 2 - if round == self.params.log_k_chunk - 1 { - self.eq_r_r = self.B.get_current_scalar(); - - // Initialize SharedRaPolynomials with per-poly pre-scaled eq tables (by rho_i) - let F_table = std::mem::take(&mut self.F); - let ra_indices = std::mem::take(&mut self.ra_indices); - let base_eq = F_table.clone_values(); - let num_polys = self.params.polynomial_types.len(); - debug_assert!( - num_polys == self.gamma_powers.len(), - "gamma_powers length mismatch: got {}, expected {}", - self.gamma_powers.len(), - num_polys - ); - let tables: Vec> = (0..num_polys) - .into_par_iter() - .map(|i| { - let rho = self.gamma_powers[i]; - base_eq.iter().map(|v| rho * *v).collect() - }) - .collect(); - self.H = Some(SharedRaPolynomials::new( - tables, - ra_indices, - self.params.one_hot_params.clone(), - )); - - // Drop G arrays - let g = std::mem::take(&mut self.G); - drop_in_background_thread(g); - } + self.ingest_address_challenge(r_j, round); } else { - // Phase 2: Bind D and H - self.D.bind(r_j); - if let Some(ref mut h) = self.H { - h.bind_in_place(r_j, BindingOrder::LowToHigh); - } + self.ingest_cycle_challenge(r_j); } } @@ -539,6 +542,177 @@ impl SumcheckInstanceProver for BooleanitySum } } +#[derive(Allocative)] +pub struct BooleanityAddressSumcheckProver { + inner: BooleanitySumcheckProver, + address_params: BooleanityAddressPhaseParams, + last_round_poly: Option>, + address_claim: Option, +} + +impl BooleanityAddressSumcheckProver { + pub fn initialize( + params: BooleanitySumcheckParams, + trace: &[Cycle], + bytecode: &BytecodePreprocessing, + memory_layout: &MemoryLayout, + ) -> Self { + let address_params = BooleanityAddressPhaseParams::new(params.clone()); + Self { + inner: BooleanitySumcheckProver::initialize(params, trace, bytecode, memory_layout), + address_params, + last_round_poly: None, + address_claim: None, + } + } +} + +impl SumcheckInstanceProver + for BooleanityAddressSumcheckProver +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.address_params + } + + fn degree(&self) -> usize { + self.inner.params.degree() + } + + fn num_rounds(&self) -> usize { + self.inner.params.log_k_chunk + } + + fn input_claim(&self, accumulator: &ProverOpeningAccumulator) -> F { + self.inner.params.input_claim(accumulator) + } + + fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { + let poly = self.inner.compute_phase1_message(round, previous_claim); + self.last_round_poly = Some(poly.clone()); + poly + } + + fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { + if let Some(poly) = self.last_round_poly.take() { + let claim = poly.evaluate(&r_j); + if round == self.inner.params.log_k_chunk - 1 { + self.address_claim = Some(claim); + } + } + self.inner.ingest_address_challenge(r_j, round); + } + + fn cache_openings( + &self, + accumulator: &mut ProverOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let mut r_address = sumcheck_challenges.to_vec(); + r_address.reverse(); + accumulator.append_virtual( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + OpeningPoint::::new(r_address), + self.address_claim + .expect("Booleanity address-phase claim missing"), + ); + } + + #[cfg(feature = "allocative")] + fn update_flamegraph(&self, flamegraph: &mut FlameGraphBuilder) { + flamegraph.visit_root(self); + } +} + +#[derive(Allocative)] +pub struct BooleanityCycleSumcheckProver { + inner: BooleanitySumcheckProver, + cycle_params: BooleanityCyclePhaseParams, +} + +impl BooleanityCycleSumcheckProver { + #[tracing::instrument(skip_all, name = "BooleanityCycleSumcheckProver::initialize")] + pub fn initialize( + params: BooleanitySumcheckParams, + trace: &[Cycle], + bytecode: &BytecodePreprocessing, + memory_layout: &MemoryLayout, + accumulator: &ProverOpeningAccumulator, + ) -> Self { + let (r_address_point, _) = accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ); + let mut r_address_low_to_high = r_address_point.r; + r_address_low_to_high.reverse(); + let cycle_params = + BooleanityCyclePhaseParams::new(params.clone(), r_address_low_to_high.clone()); + + let mut inner = + BooleanitySumcheckProver::initialize(params, trace, bytecode, memory_layout); + for (round, r_j) in r_address_low_to_high.iter().cloned().enumerate() { + inner.ingest_address_challenge(r_j, round); + } + + Self { + inner, + cycle_params, + } + } +} + +impl SumcheckInstanceProver + for BooleanityCycleSumcheckProver +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.cycle_params + } + + fn degree(&self) -> usize { + self.inner.params.degree() + } + + fn num_rounds(&self) -> usize { + self.inner.params.log_t + } + + fn input_claim(&self, accumulator: &ProverOpeningAccumulator) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ) + .1 + } + + fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { + self.inner.compute_phase2_message(round, previous_claim) + } + + fn ingest_challenge(&mut self, r_j: F::Challenge, _round: usize) { + self.inner.ingest_cycle_challenge(r_j); + } + + fn cache_openings( + &self, + accumulator: &mut ProverOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let mut full_challenges = self.cycle_params.r_address_low_to_high.clone(); + full_challenges.extend_from_slice(sumcheck_challenges); + as SumcheckInstanceProver>::cache_openings( + &self.inner, + accumulator, + &full_challenges, + ); + } + + #[cfg(feature = "allocative")] + fn update_flamegraph(&self, flamegraph: &mut FlameGraphBuilder) { + flamegraph.visit_root(self); + } +} + /// Booleanity Sumcheck Verifier. pub struct BooleanitySumcheckVerifier { params: BooleanitySumcheckParams, @@ -599,3 +773,296 @@ impl SumcheckInstanceVerifier for BooleanityS ); } } + +pub struct BooleanityAddressSumcheckVerifier { + params: BooleanitySumcheckParams, + address_params: BooleanityAddressPhaseParams, +} + +impl BooleanityAddressSumcheckVerifier { + pub fn new(params: BooleanitySumcheckParams) -> Self { + let address_params = BooleanityAddressPhaseParams::new(params.clone()); + Self { + params, + address_params, + } + } + + pub fn into_params(self) -> BooleanitySumcheckParams { + self.params + } +} + +impl SumcheckInstanceVerifier + for BooleanityAddressSumcheckVerifier +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.address_params + } + + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.params.log_k_chunk + } + + fn input_claim(&self, accumulator: &VerifierOpeningAccumulator) -> F { + self.params.input_claim(accumulator) + } + + fn expected_output_claim( + &self, + accumulator: &VerifierOpeningAccumulator, + _sumcheck_challenges: &[F::Challenge], + ) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ) + .1 + } + + fn cache_openings( + &self, + accumulator: &mut VerifierOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let mut r_address = sumcheck_challenges.to_vec(); + r_address.reverse(); + accumulator.append_virtual( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + OpeningPoint::::new(r_address), + ); + } +} + +pub struct BooleanityCycleSumcheckVerifier { + params: BooleanitySumcheckParams, + cycle_params: BooleanityCyclePhaseParams, +} + +impl BooleanityCycleSumcheckVerifier { + pub fn new( + params: BooleanitySumcheckParams, + opening_accumulator: &VerifierOpeningAccumulator, + ) -> Self { + let (r_address_point, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ); + let mut r_address_low_to_high = r_address_point.r; + r_address_low_to_high.reverse(); + let cycle_params = BooleanityCyclePhaseParams::new(params.clone(), r_address_low_to_high); + Self { + params, + cycle_params, + } + } +} + +impl SumcheckInstanceVerifier + for BooleanityCycleSumcheckVerifier +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.cycle_params + } + + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.params.log_t + } + + fn input_claim(&self, accumulator: &VerifierOpeningAccumulator) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ) + .1 + } + + fn expected_output_claim( + &self, + accumulator: &VerifierOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) -> F { + let mut full_challenges = self.cycle_params.r_address_low_to_high.clone(); + full_challenges.extend_from_slice(sumcheck_challenges); + + let inner = BooleanitySumcheckVerifier { + params: self.params.clone(), + }; + as SumcheckInstanceVerifier>::expected_output_claim( + &inner, + accumulator, + &full_challenges, + ) + } + + fn cache_openings( + &self, + accumulator: &mut VerifierOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let mut full_challenges = self.cycle_params.r_address_low_to_high.clone(); + full_challenges.extend_from_slice(sumcheck_challenges); + + let inner = BooleanitySumcheckVerifier { + params: self.params.clone(), + }; + as SumcheckInstanceVerifier>::cache_openings( + &inner, + accumulator, + &full_challenges, + ); + } +} + +#[derive(Allocative, Clone)] +struct BooleanityCyclePhaseParams { + inner: BooleanitySumcheckParams, + r_address_low_to_high: Vec, +} + +impl BooleanityCyclePhaseParams { + fn new(inner: BooleanitySumcheckParams, r_address_low_to_high: Vec) -> Self { + Self { + inner, + r_address_low_to_high, + } + } + + fn full_challenges(&self, cycle_challenges: &[F::Challenge]) -> Vec { + let mut full = self.r_address_low_to_high.clone(); + full.extend_from_slice(cycle_challenges); + full + } +} + +impl SumcheckInstanceParams for BooleanityCyclePhaseParams { + fn degree(&self) -> usize { + as SumcheckInstanceParams>::degree(&self.inner) + } + + fn num_rounds(&self) -> usize { + self.inner.log_t + } + + fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ) + .1 + } + + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + let full = self.full_challenges(challenges); + as SumcheckInstanceParams>::normalize_opening_point( + &self.inner, + &full, + ) + } + + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + InputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + )) + } + + #[cfg(feature = "zk")] + fn input_constraint_challenge_values( + &self, + _accumulator: &dyn OpeningAccumulator, + ) -> Vec { + Vec::new() + } + + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + as SumcheckInstanceParams>::output_claim_constraint( + &self.inner, + ) + } + + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { + let full = self.full_challenges(sumcheck_challenges); + as SumcheckInstanceParams>::output_constraint_challenge_values( + &self.inner, + &full, + ) + } +} + +#[derive(Allocative, Clone)] +struct BooleanityAddressPhaseParams { + inner: BooleanitySumcheckParams, +} + +impl BooleanityAddressPhaseParams { + fn new(inner: BooleanitySumcheckParams) -> Self { + Self { inner } + } +} + +impl SumcheckInstanceParams for BooleanityAddressPhaseParams { + fn degree(&self) -> usize { + as SumcheckInstanceParams>::degree(&self.inner) + } + + fn num_rounds(&self) -> usize { + self.inner.log_k_chunk + } + + fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { + as SumcheckInstanceParams>::input_claim( + &self.inner, + accumulator, + ) + } + + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + let mut r = challenges.to_vec(); + r.reverse(); + OpeningPoint::new(r) + } + + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + as SumcheckInstanceParams>::input_claim_constraint( + &self.inner, + ) + } + + #[cfg(feature = "zk")] + fn input_constraint_challenge_values(&self, accumulator: &dyn OpeningAccumulator) -> Vec { + as SumcheckInstanceParams>::input_constraint_challenge_values( + &self.inner, + accumulator, + ) + } + + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + Some(OutputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ))) + } + + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, _sumcheck_challenges: &[F::Challenge]) -> Vec { + Vec::new() + } +} diff --git a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs index d965b0e12a..54a23f5e33 100644 --- a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs +++ b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs @@ -29,7 +29,7 @@ use crate::{ sumcheck_prover::SumcheckInstanceProver, sumcheck_verifier::{SumcheckInstanceParams, SumcheckInstanceVerifier}, }, - transcripts::Transcript, + transcripts::{KeccakTranscript, Transcript}, utils::{math::Math, small_scalar::SmallScalar, thread::unsafe_allocate_zero_vec}, zkvm::{ bytecode::BytecodePreprocessing, @@ -697,6 +697,197 @@ impl SumcheckInstanceProver } } +#[derive(Allocative)] +pub struct BytecodeReadRafAddressSumcheckProver { + inner: BytecodeReadRafSumcheckProver, + address_params: BytecodeReadRafAddressPhaseParams, +} + +impl BytecodeReadRafAddressSumcheckProver { + pub fn initialize( + params: BytecodeReadRafSumcheckParams, + trace: Arc>, + bytecode_preprocessing: Arc, + ) -> Self { + let address_params = BytecodeReadRafAddressPhaseParams::new(params.clone()); + Self { + inner: BytecodeReadRafSumcheckProver::initialize(params, trace, bytecode_preprocessing), + address_params, + } + } +} + +impl SumcheckInstanceProver + for BytecodeReadRafAddressSumcheckProver +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.address_params + } + + fn degree(&self) -> usize { + self.inner.params.degree() + } + + fn num_rounds(&self) -> usize { + self.inner.params.log_K + } + + fn input_claim(&self, accumulator: &ProverOpeningAccumulator) -> F { + self.inner.params.input_claim(accumulator) + } + + fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { + as SumcheckInstanceProver>::compute_message( + &mut self.inner, + round, + previous_claim, + ) + } + + fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { + as SumcheckInstanceProver>::ingest_challenge( + &mut self.inner, + r_j, + round, + ); + } + + fn cache_openings( + &self, + accumulator: &mut ProverOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let mut r_address = sumcheck_challenges.to_vec(); + r_address.reverse(); + let opening_point = OpeningPoint::::new(r_address); + let address_claim: F = self + .inner + .prev_round_claims + .iter() + .zip(self.inner.params.gamma_powers.iter()) + .take(N_STAGES) + .map(|(claim, gamma)| *claim * *gamma) + .sum::() + + self.inner.params.entry_gamma * self.inner.prev_entry_claim; + accumulator.append_virtual( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + opening_point, + address_claim, + ); + } + + #[cfg(feature = "allocative")] + fn update_flamegraph(&self, flamegraph: &mut FlameGraphBuilder) { + flamegraph.visit_root(self); + } +} + +#[derive(Allocative)] +pub struct BytecodeReadRafCycleSumcheckProver { + inner: BytecodeReadRafSumcheckProver, + cycle_params: BytecodeReadRafCyclePhaseParams, +} + +impl BytecodeReadRafCycleSumcheckProver { + #[tracing::instrument(skip_all, name = "BytecodeReadRafCycleSumcheckProver::initialize")] + pub fn initialize( + params: BytecodeReadRafSumcheckParams, + trace: Arc>, + bytecode_preprocessing: Arc, + accumulator: &ProverOpeningAccumulator, + ) -> Self { + let (r_address_point, _) = accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ); + let mut r_address_low_to_high = r_address_point.r; + r_address_low_to_high.reverse(); + let cycle_params = + BytecodeReadRafCyclePhaseParams::new(params.clone(), r_address_low_to_high.clone()); + + let mut inner = + BytecodeReadRafSumcheckProver::initialize(params, trace, bytecode_preprocessing); + for (round, r_j) in r_address_low_to_high.iter().cloned().enumerate() { + let _ = as SumcheckInstanceProver< + F, + KeccakTranscript, + >>::compute_message(&mut inner, round, F::zero()); + as SumcheckInstanceProver< + F, + KeccakTranscript, + >>::ingest_challenge(&mut inner, r_j, round); + } + + Self { + inner, + cycle_params, + } + } +} + +impl SumcheckInstanceProver + for BytecodeReadRafCycleSumcheckProver +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.cycle_params + } + + fn degree(&self) -> usize { + self.inner.params.degree() + } + + fn num_rounds(&self) -> usize { + self.inner.params.log_T + } + + fn input_claim(&self, accumulator: &ProverOpeningAccumulator) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ) + .1 + } + + fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { + let log_k = self.inner.params.log_K; + as SumcheckInstanceProver>::compute_message( + &mut self.inner, + round + log_k, + previous_claim, + ) + } + + fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { + let log_k = self.inner.params.log_K; + as SumcheckInstanceProver>::ingest_challenge( + &mut self.inner, + r_j, + round + log_k, + ); + } + + fn cache_openings( + &self, + accumulator: &mut ProverOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let mut full_challenges = self.cycle_params.r_address_low_to_high.clone(); + full_challenges.extend_from_slice(sumcheck_challenges); + as SumcheckInstanceProver>::cache_openings( + &self.inner, + accumulator, + &full_challenges, + ); + } + + #[cfg(feature = "allocative")] + fn update_flamegraph(&self, flamegraph: &mut FlameGraphBuilder) { + flamegraph.visit_root(self); + } +} + pub struct BytecodeReadRafSumcheckVerifier { params: BytecodeReadRafSumcheckParams, } @@ -822,6 +1013,316 @@ impl SumcheckInstanceVerifier } } +pub struct BytecodeReadRafAddressSumcheckVerifier { + params: BytecodeReadRafSumcheckParams, + address_params: BytecodeReadRafAddressPhaseParams, +} + +impl BytecodeReadRafAddressSumcheckVerifier { + pub fn new( + bytecode_preprocessing: &BytecodePreprocessing, + n_cycle_vars: usize, + one_hot_params: &OneHotParams, + opening_accumulator: &VerifierOpeningAccumulator, + transcript: &mut impl Transcript, + ) -> Self { + let params = BytecodeReadRafSumcheckParams::gen( + bytecode_preprocessing, + n_cycle_vars, + one_hot_params, + opening_accumulator, + transcript, + ); + let address_params = BytecodeReadRafAddressPhaseParams::new(params.clone()); + Self { + params, + address_params, + } + } + + pub fn into_params(self) -> BytecodeReadRafSumcheckParams { + self.params + } +} + +impl SumcheckInstanceVerifier + for BytecodeReadRafAddressSumcheckVerifier +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.address_params + } + + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.params.log_K + } + + fn input_claim(&self, accumulator: &VerifierOpeningAccumulator) -> F { + self.params.input_claim(accumulator) + } + + fn expected_output_claim( + &self, + accumulator: &VerifierOpeningAccumulator, + _sumcheck_challenges: &[F::Challenge], + ) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ) + .1 + } + + fn cache_openings( + &self, + accumulator: &mut VerifierOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let mut r_address = sumcheck_challenges.to_vec(); + r_address.reverse(); + accumulator.append_virtual( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + OpeningPoint::::new(r_address), + ); + } +} + +pub struct BytecodeReadRafCycleSumcheckVerifier { + params: BytecodeReadRafSumcheckParams, + cycle_params: BytecodeReadRafCyclePhaseParams, +} + +impl BytecodeReadRafCycleSumcheckVerifier { + pub fn new( + params: BytecodeReadRafSumcheckParams, + opening_accumulator: &VerifierOpeningAccumulator, + ) -> Self { + let (r_address_point, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ); + let mut r_address_low_to_high = r_address_point.r; + r_address_low_to_high.reverse(); + let cycle_params = + BytecodeReadRafCyclePhaseParams::new(params.clone(), r_address_low_to_high); + Self { + params, + cycle_params, + } + } +} + +impl SumcheckInstanceVerifier + for BytecodeReadRafCycleSumcheckVerifier +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.cycle_params + } + + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.params.log_T + } + + fn input_claim(&self, accumulator: &VerifierOpeningAccumulator) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ) + .1 + } + + fn expected_output_claim( + &self, + accumulator: &VerifierOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) -> F { + let mut full_challenges = self.cycle_params.r_address_low_to_high.clone(); + full_challenges.extend_from_slice(sumcheck_challenges); + + let inner = BytecodeReadRafSumcheckVerifier { + params: self.params.clone(), + }; + as SumcheckInstanceVerifier>::expected_output_claim( + &inner, + accumulator, + &full_challenges, + ) + } + + fn cache_openings( + &self, + accumulator: &mut VerifierOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let mut full_challenges = self.cycle_params.r_address_low_to_high.clone(); + full_challenges.extend_from_slice(sumcheck_challenges); + + let inner = BytecodeReadRafSumcheckVerifier { + params: self.params.clone(), + }; + as SumcheckInstanceVerifier>::cache_openings( + &inner, + accumulator, + &full_challenges, + ); + } +} + +#[derive(Allocative, Clone)] +struct BytecodeReadRafCyclePhaseParams { + inner: BytecodeReadRafSumcheckParams, + r_address_low_to_high: Vec, +} + +impl BytecodeReadRafCyclePhaseParams { + fn new( + inner: BytecodeReadRafSumcheckParams, + r_address_low_to_high: Vec, + ) -> Self { + Self { + inner, + r_address_low_to_high, + } + } + + fn full_challenges(&self, cycle_challenges: &[F::Challenge]) -> Vec { + let mut full = self.r_address_low_to_high.clone(); + full.extend_from_slice(cycle_challenges); + full + } +} + +impl SumcheckInstanceParams for BytecodeReadRafCyclePhaseParams { + fn degree(&self) -> usize { + as SumcheckInstanceParams>::degree(&self.inner) + } + + fn num_rounds(&self) -> usize { + self.inner.log_T + } + + fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ) + .1 + } + + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + let full = self.full_challenges(challenges); + as SumcheckInstanceParams>::normalize_opening_point( + &self.inner, + &full, + ) + } + + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + InputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + )) + } + + #[cfg(feature = "zk")] + fn input_constraint_challenge_values( + &self, + _accumulator: &dyn OpeningAccumulator, + ) -> Vec { + Vec::new() + } + + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + as SumcheckInstanceParams>::output_claim_constraint( + &self.inner, + ) + } + + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { + let full = self.full_challenges(sumcheck_challenges); + as SumcheckInstanceParams>::output_constraint_challenge_values( + &self.inner, + &full, + ) + } +} + +#[derive(Allocative, Clone)] +struct BytecodeReadRafAddressPhaseParams { + inner: BytecodeReadRafSumcheckParams, +} + +impl BytecodeReadRafAddressPhaseParams { + fn new(inner: BytecodeReadRafSumcheckParams) -> Self { + Self { inner } + } +} + +impl SumcheckInstanceParams for BytecodeReadRafAddressPhaseParams { + fn degree(&self) -> usize { + as SumcheckInstanceParams>::degree(&self.inner) + } + + fn num_rounds(&self) -> usize { + self.inner.log_K + } + + fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { + as SumcheckInstanceParams>::input_claim( + &self.inner, + accumulator, + ) + } + + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + let mut r = challenges.to_vec(); + r.reverse(); + OpeningPoint::new(r) + } + + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + as SumcheckInstanceParams>::input_claim_constraint( + &self.inner, + ) + } + + #[cfg(feature = "zk")] + fn input_constraint_challenge_values(&self, accumulator: &dyn OpeningAccumulator) -> Vec { + as SumcheckInstanceParams>::input_constraint_challenge_values( + &self.inner, + accumulator, + ) + } + + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + Some(OutputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ))) + } + + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, _sumcheck_challenges: &[F::Challenge]) -> Vec { + Vec::new() + } +} + #[derive(Allocative, Clone)] pub struct BytecodeReadRafSumcheckParams { /// Index `i` stores `gamma^i`. diff --git a/jolt-core/src/zkvm/proof_serialization.rs b/jolt-core/src/zkvm/proof_serialization.rs index 343881168b..071d498a0e 100644 --- a/jolt-core/src/zkvm/proof_serialization.rs +++ b/jolt-core/src/zkvm/proof_serialization.rs @@ -47,7 +47,8 @@ pub struct JoltProof< pub stage3_sumcheck_proof: SumcheckInstanceProof, pub stage4_sumcheck_proof: SumcheckInstanceProof, pub stage5_sumcheck_proof: SumcheckInstanceProof, - pub stage6_sumcheck_proof: SumcheckInstanceProof, + pub stage6a_sumcheck_proof: SumcheckInstanceProof, + pub stage6b_sumcheck_proof: SumcheckInstanceProof, pub stage7_sumcheck_proof: SumcheckInstanceProof, #[cfg(feature = "zk")] pub blindfold_proof: BlindFoldProof, @@ -378,6 +379,8 @@ impl CanonicalSerialize for VirtualPolynomial { 38u8.serialize_with_mode(&mut writer, compress)?; (u8::try_from(*flag).unwrap()).serialize_with_mode(&mut writer, compress) } + Self::BytecodeReadRafAddrClaim => 39u8.serialize_with_mode(&mut writer, compress), + Self::BooleanityAddrClaim => 40u8.serialize_with_mode(&mut writer, compress), } } @@ -417,7 +420,9 @@ impl CanonicalSerialize for VirtualPolynomial { | Self::RamValInit | Self::RamValFinal | Self::RamHammingWeight - | Self::UnivariateSkip => 1, + | Self::UnivariateSkip + | Self::BytecodeReadRafAddrClaim + | Self::BooleanityAddrClaim => 1, Self::InstructionRa(_) | Self::OpFlags(_) | Self::InstructionFlags(_) @@ -495,6 +500,8 @@ impl CanonicalDeserialize for VirtualPolynomial { let flag = u8::deserialize_with_mode(&mut reader, compress, validate)?; Self::LookupTableFlag(flag as usize) } + 39 => Self::BytecodeReadRafAddrClaim, + 40 => Self::BooleanityAddrClaim, _ => return Err(SerializationError::InvalidData), }, ) diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index c34ccba98d..5ccd87f10f 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -47,7 +47,10 @@ use crate::{ }, pprof_scope, subprotocols::{ - booleanity::{BooleanitySumcheckParams, BooleanitySumcheckProver}, + booleanity::{ + BooleanityAddressSumcheckProver, BooleanityCycleSumcheckProver, + BooleanitySumcheckParams, + }, streaming_schedule::LinearOnlySchedule, sumcheck::{BatchedSumcheck, SumcheckInstanceProof}, sumcheck_prover::SumcheckInstanceProver, @@ -99,7 +102,9 @@ use crate::{ use crate::{ poly::commitment::commitment_scheme::CommitmentScheme, zkvm::{ - bytecode::read_raf_checking::BytecodeReadRafSumcheckProver, + bytecode::read_raf_checking::{ + BytecodeReadRafAddressSumcheckProver, BytecodeReadRafCycleSumcheckProver, + }, fiat_shamir_preamble, instruction_lookups::{ ra_virtual::InstructionRaSumcheckProver as LookupsRaSumcheckProver, @@ -522,7 +527,10 @@ impl< let (stage3_sumcheck_proof, r_stage3) = self.prove_stage3(); let (stage4_sumcheck_proof, r_stage4) = self.prove_stage4(); let (stage5_sumcheck_proof, r_stage5) = self.prove_stage5(); - let (stage6_sumcheck_proof, r_stage6) = self.prove_stage6(); + let (stage6a_sumcheck_proof, bytecode_read_raf_params, booleanity_params) = + self.prove_stage6a(); + let (stage6b_sumcheck_proof, r_stage6) = + self.prove_stage6b(bytecode_read_raf_params, booleanity_params); let (stage7_sumcheck_proof, r_stage7) = self.prove_stage7(); let _sumcheck_challenges = [ @@ -569,7 +577,8 @@ impl< stage3_sumcheck_proof, stage4_sumcheck_proof, stage5_sumcheck_proof, - stage6_sumcheck_proof, + stage6a_sumcheck_proof, + stage6b_sumcheck_proof, stage7_sumcheck_proof, #[cfg(feature = "zk")] blindfold_proof, @@ -1208,14 +1217,15 @@ impl< } #[tracing::instrument(skip_all)] - fn prove_stage6( + fn prove_stage6a( &mut self, ) -> ( SumcheckInstanceProof, - Vec, + BytecodeReadRafSumcheckParams, + BooleanitySumcheckParams, ) { #[cfg(not(target_arch = "wasm32"))] - print_current_memory_usage("Stage 6 baseline"); + print_current_memory_usage("Stage 6a baseline"); let bytecode_read_raf_params = BytecodeReadRafSumcheckParams::gen( &self.preprocessing.shared.bytecode, @@ -1225,9 +1235,6 @@ impl< &mut self.transcript, ); - let ram_hamming_booleanity_params = - HammingBooleanitySumcheckParams::new(&self.opening_accumulator); - let booleanity_params = BooleanitySumcheckParams::new( self.trace.len().log_2(), &self.one_hot_params, @@ -1235,6 +1242,77 @@ impl< &mut self.transcript, ); + let mut bytecode_read_raf = BytecodeReadRafAddressSumcheckProver::initialize( + bytecode_read_raf_params.clone(), + Arc::clone(&self.trace), + Arc::clone(&self.preprocessing.shared.bytecode), + ); + let mut booleanity = BooleanityAddressSumcheckProver::initialize( + booleanity_params.clone(), + &self.trace, + &self.preprocessing.shared.bytecode, + &self.program_io.memory_layout, + ); + + #[cfg(feature = "allocative")] + { + print_data_structure_heap_usage( + "BytecodeReadRafAddressSumcheckProver", + &bytecode_read_raf, + ); + print_data_structure_heap_usage("BooleanityAddressSumcheckProver", &booleanity); + } + + let mut instances: Vec<&mut dyn SumcheckInstanceProver<_, _>> = + vec![&mut bytecode_read_raf, &mut booleanity]; + + #[cfg(feature = "allocative")] + write_instance_flamegraph_svg(&instances, "stage6a_start_flamechart.svg"); + tracing::info!("Stage 6a proving"); + + #[cfg(feature = "zk")] + let (sumcheck_proof, _r_stage6a, _initial_claim) = { + // Stage 6a input claims depend on hidden prior-stage outputs in ZK mode, + // so we prove it with a ZK sumcheck proof. We keep a local blindfold + // accumulator so this split-internal phase does not add a new global + // BlindFold stage. + let mut rng = rand::thread_rng(); + let mut local_blindfold = + crate::subprotocols::blindfold::BlindFoldAccumulator::::new(); + BatchedSumcheck::prove_zk::( + instances.iter_mut().map(|v| &mut **v as _).collect(), + &mut self.opening_accumulator, + &mut local_blindfold, + &mut self.transcript, + &self.pedersen_generators, + &mut rng, + ) + }; + #[cfg(not(feature = "zk"))] + let (sumcheck_proof, _r_stage6a, _initial_claim) = + self.prove_batched_sumcheck(instances.iter_mut().map(|v| &mut **v as _).collect()); + + #[cfg(feature = "allocative")] + write_instance_flamegraph_svg(&instances, "stage6a_end_flamechart.svg"); + + (sumcheck_proof, bytecode_read_raf_params, booleanity_params) + } + + #[tracing::instrument(skip_all)] + fn prove_stage6b( + &mut self, + bytecode_read_raf_params: BytecodeReadRafSumcheckParams, + booleanity_params: BooleanitySumcheckParams, + ) -> ( + SumcheckInstanceProof, + Vec, + ) { + #[cfg(not(target_arch = "wasm32"))] + print_current_memory_usage("Stage 6b baseline"); + + let ram_hamming_booleanity_params = + HammingBooleanitySumcheckParams::new(&self.opening_accumulator); + let ram_ra_virtual_params = RamRaVirtualParams::new( self.trace.len(), &self.one_hot_params, @@ -1251,7 +1329,7 @@ impl< &mut self.transcript, ); - // Advice claim reduction (Phase 1 in Stage 6): trusted and untrusted are separate instances. + // Advice claim reduction (Phase 1 in Stage 6b): trusted and untrusted are separate instances. if self.advice.trusted_advice_polynomial.is_some() { let trusted_advice_params = AdviceClaimReductionParams::new( AdviceKind::Trusted, @@ -1296,20 +1374,21 @@ impl< }; } - let mut bytecode_read_raf = BytecodeReadRafSumcheckProver::initialize( + let mut bytecode_read_raf = BytecodeReadRafCycleSumcheckProver::initialize( bytecode_read_raf_params, Arc::clone(&self.trace), Arc::clone(&self.preprocessing.shared.bytecode), + &self.opening_accumulator, ); - let mut ram_hamming_booleanity = - HammingBooleanitySumcheckProver::initialize(ram_hamming_booleanity_params, &self.trace); - - let mut booleanity = BooleanitySumcheckProver::initialize( + let mut booleanity = BooleanityCycleSumcheckProver::initialize( booleanity_params, &self.trace, &self.preprocessing.shared.bytecode, &self.program_io.memory_layout, + &self.opening_accumulator, ); + let mut ram_hamming_booleanity = + HammingBooleanitySumcheckProver::initialize(ram_hamming_booleanity_params, &self.trace); let mut ram_ra_virtual = RamRaVirtualSumcheckProver::initialize( ram_ra_virtual_params, @@ -1324,8 +1403,11 @@ impl< #[cfg(feature = "allocative")] { - print_data_structure_heap_usage("BytecodeReadRafSumcheckProver", &bytecode_read_raf); - print_data_structure_heap_usage("BooleanitySumcheckProver", &booleanity); + print_data_structure_heap_usage( + "BytecodeReadRafCycleSumcheckProver", + &bytecode_read_raf, + ); + print_data_structure_heap_usage("BooleanityCycleSumcheckProver", &booleanity); print_data_structure_heap_usage( "ram HammingBooleanitySumcheckProver", &ram_hamming_booleanity, @@ -1360,13 +1442,13 @@ impl< } #[cfg(feature = "allocative")] - write_instance_flamegraph_svg(&instances, "stage6_start_flamechart.svg"); - tracing::info!("Stage 6 proving"); + write_instance_flamegraph_svg(&instances, "stage6b_start_flamechart.svg"); + tracing::info!("Stage 6b proving"); - let (sumcheck_proof, r_stage6, _initial_claim) = + let (sumcheck_proof, r_stage6b, _initial_claim) = self.prove_batched_sumcheck(instances.iter_mut().map(|v| &mut **v as _).collect()); #[cfg(feature = "allocative")] - write_instance_flamegraph_svg(&instances, "stage6_end_flamechart.svg"); + write_instance_flamegraph_svg(&instances, "stage6b_end_flamechart.svg"); drop_in_background_thread(bytecode_read_raf); drop_in_background_thread(booleanity); drop_in_background_thread(ram_hamming_booleanity); @@ -1377,7 +1459,7 @@ impl< self.advice_reduction_prover_trusted = advice_trusted; self.advice_reduction_prover_untrusted = advice_untrusted; - (sumcheck_proof, r_stage6) + (sumcheck_proof, r_stage6b) } #[tracing::instrument(skip_all)] @@ -3141,7 +3223,7 @@ mod tests { ("Stage 5 (Value+Lookup)", &jolt_proof.stage5_sumcheck_proof), ( "Stage 6 (OneHot+Hamming)", - &jolt_proof.stage6_sumcheck_proof, + &jolt_proof.stage6b_sumcheck_proof, ), ( "Stage 7 (HammingWeight+ClaimReduction)", diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index 4e25b68549..acf7d14d5f 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -40,7 +40,10 @@ use crate::zkvm::ram::RAMPreprocessing; use crate::zkvm::witness::all_committed_polynomials; use crate::zkvm::Serializable; use crate::zkvm::{ - bytecode::read_raf_checking::BytecodeReadRafSumcheckVerifier, + bytecode::read_raf_checking::{ + BytecodeReadRafAddressSumcheckVerifier, BytecodeReadRafCycleSumcheckVerifier, + BytecodeReadRafSumcheckParams, + }, claim_reductions::{ AdviceClaimReductionVerifier, AdviceKind, HammingWeightClaimReductionVerifier, IncClaimReductionSumcheckVerifier, InstructionLookupsClaimReductionSumcheckVerifier, @@ -82,7 +85,10 @@ use crate::{ }, pprof_scope, subprotocols::{ - booleanity::{BooleanitySumcheckParams, BooleanitySumcheckVerifier}, + booleanity::{ + BooleanityAddressSumcheckVerifier, BooleanityCycleSumcheckVerifier, + BooleanitySumcheckParams, + }, sumcheck_verifier::SumcheckInstanceVerifier, }, transcripts::Transcript, @@ -909,25 +915,66 @@ impl< #[cfg_attr(not(feature = "zk"), allow(unused_variables))] fn verify_stage6(&mut self) -> Result, ProofVerifyError> { + let (bytecode_read_raf_params, booleanity_params) = self.verify_stage6a()?; + self.verify_stage6b(bytecode_read_raf_params, booleanity_params) + } + + fn verify_stage6a( + &mut self, + ) -> Result< + ( + BytecodeReadRafSumcheckParams, + BooleanitySumcheckParams, + ), + ProofVerifyError, + > { let n_cycle_vars = self.proof.trace_length.log_2(); - let bytecode_read_raf = BytecodeReadRafSumcheckVerifier::gen( + let bytecode_read_raf = BytecodeReadRafAddressSumcheckVerifier::new( &self.preprocessing.shared.bytecode, n_cycle_vars, &self.one_hot_params, &self.opening_accumulator, &mut self.transcript, ); - - let ram_hamming_booleanity = - HammingBooleanitySumcheckVerifier::new(&self.opening_accumulator); - let booleanity_params = BooleanitySumcheckParams::new( + let booleanity = BooleanityAddressSumcheckVerifier::new(BooleanitySumcheckParams::new( n_cycle_vars, &self.one_hot_params, &self.opening_accumulator, &mut self.transcript, - ); + )); - let booleanity = BooleanitySumcheckVerifier::new(booleanity_params); + let instances: Vec<&dyn SumcheckInstanceVerifier> = + vec![&bytecode_read_raf, &booleanity]; + BatchedSumcheck::verify( + &self.proof.stage6a_sumcheck_proof, + instances, + &mut self.opening_accumulator, + &mut self.transcript, + )?; + #[cfg(feature = "zk")] + { + // Stage 6a is proven in clear and excluded from BlindFold stage data. + // Drop any pending OC IDs so Stage 6b OC blocks stay aligned. + let _ = self.opening_accumulator.take_pending_claim_ids(); + } + + Ok((bytecode_read_raf.into_params(), booleanity.into_params())) + } + + #[cfg_attr(not(feature = "zk"), allow(unused_variables))] + fn verify_stage6b( + &mut self, + bytecode_read_raf_params: BytecodeReadRafSumcheckParams, + booleanity_params: BooleanitySumcheckParams, + ) -> Result, ProofVerifyError> { + let bytecode_read_raf = BytecodeReadRafCycleSumcheckVerifier::new( + bytecode_read_raf_params, + &self.opening_accumulator, + ); + let ram_hamming_booleanity = + HammingBooleanitySumcheckVerifier::new(&self.opening_accumulator); + let booleanity = + BooleanityCycleSumcheckVerifier::new(booleanity_params, &self.opening_accumulator); let ram_ra_virtual = RamRaVirtualSumcheckVerifier::new( self.proof.trace_length, &self.one_hot_params, @@ -945,7 +992,7 @@ impl< &mut self.transcript, ); - // Advice claim reduction (Phase 1 in Stage 6): trusted and untrusted are separate instances. + // Advice claim reduction (Phase 1 in Stage 6b): trusted and untrusted are separate instances. if self.trusted_advice_commitment.is_some() { self.advice_reduction_verifier_trusted = Some(AdviceClaimReductionVerifier::new( AdviceKind::Trusted, @@ -978,8 +1025,8 @@ impl< instances.push(advice); } - let (batching_coefficients, r_stage6) = BatchedSumcheck::verify( - &self.proof.stage6_sumcheck_proof, + let (batching_coefficients, r_stage6b) = BatchedSumcheck::verify( + &self.proof.stage6b_sumcheck_proof, instances.clone(), &mut self.opening_accumulator, &mut self.transcript, @@ -997,7 +1044,7 @@ impl< for instance in &instances { let num_rounds = instance.num_rounds(); let offset = instance.round_offset(max_num_rounds); - let r_slice = &r_stage6[offset..offset + num_rounds]; + let r_slice = &r_stage6b[offset..offset + num_rounds]; output_constraint_challenge_values.extend( instance .get_params() @@ -1010,7 +1057,7 @@ impl< ); } Ok(StageVerifyResult::new( - r_stage6, + r_stage6b, batched_output_constraint, output_constraint_challenge_values, batched_input_constraint, @@ -1020,7 +1067,7 @@ impl< } #[cfg(not(feature = "zk"))] Ok(StageVerifyResult { - challenges: r_stage6, + challenges: r_stage6b, }) } @@ -1050,7 +1097,7 @@ impl< &self.proof.stage3_sumcheck_proof, &self.proof.stage4_sumcheck_proof, &self.proof.stage5_sumcheck_proof, - &self.proof.stage6_sumcheck_proof, + &self.proof.stage6b_sumcheck_proof, &self.proof.stage7_sumcheck_proof, ]; diff --git a/jolt-core/src/zkvm/witness.rs b/jolt-core/src/zkvm/witness.rs index 3746454b1b..fd91c8aaf8 100644 --- a/jolt-core/src/zkvm/witness.rs +++ b/jolt-core/src/zkvm/witness.rs @@ -269,4 +269,6 @@ pub enum VirtualPolynomial { OpFlags(CircuitFlags), InstructionFlags(InstructionFlags), LookupTableFlag(usize), + BytecodeReadRafAddrClaim, + BooleanityAddrClaim, } From d7b160bce6bf4910ab70470149defe3cee9dcb0b Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Sat, 14 Mar 2026 15:24:47 -0700 Subject: [PATCH 02/20] feat(zkvm): integrate precommitted Dory geometry across prover and verifier Adopt embedded-main Dory scheduling with shared precommitted claim-reduction plumbing so stage 6/8 can handle dominant precommitted contexts consistently in both zk and non-zk flows. Made-with: Cursor --- Cargo.lock | 17 + Cargo.toml | 2 + examples/fibonacci/Cargo.toml | 3 + examples/fibonacci2/Cargo.toml | 13 + examples/fibonacci2/guest/Cargo.toml | 11 + examples/fibonacci2/guest/src/lib.rs | 75 +++ examples/fibonacci2/guest/src/main.rs | 5 + examples/fibonacci2/src/main.rs | 158 +++++ .../src/poly/commitment/dory/dory_globals.rs | 173 ++++- .../src/poly/commitment/dory/wrappers.rs | 106 ++- jolt-core/src/poly/one_hot_polynomial.rs | 43 +- jolt-core/src/poly/opening_proof.rs | 20 +- jolt-core/src/poly/rlc_polynomial.rs | 40 +- jolt-core/src/zkvm/claim_reductions/advice.rs | 572 +++++----------- jolt-core/src/zkvm/claim_reductions/mod.rs | 6 + .../src/zkvm/claim_reductions/precommitted.rs | 615 ++++++++++++++++++ jolt-core/src/zkvm/prover.rs | 288 ++++---- jolt-core/src/zkvm/verifier.rs | 195 ++++-- jolt-sdk/src/host_utils.rs | 2 +- 19 files changed, 1641 insertions(+), 703 deletions(-) create mode 100644 examples/fibonacci2/Cargo.toml create mode 100644 examples/fibonacci2/guest/Cargo.toml create mode 100644 examples/fibonacci2/guest/src/lib.rs create mode 100644 examples/fibonacci2/guest/src/main.rs create mode 100644 examples/fibonacci2/src/main.rs create mode 100644 jolt-core/src/zkvm/claim_reductions/precommitted.rs diff --git a/Cargo.lock b/Cargo.lock index a27d1c6dc8..98128170a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2013,6 +2013,23 @@ dependencies = [ "jolt-sdk", ] +[[package]] +name = "fibonacci2" +version = "0.1.0" +dependencies = [ + "fibonacci2-guest", + "jolt-sdk", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "fibonacci2-guest" +version = "0.1.0" +dependencies = [ + "jolt-sdk", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 67c8a9364a..937e85c4b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,8 @@ members = [ "examples/collatz/guest", "examples/fibonacci", "examples/fibonacci/guest", + "examples/fibonacci2", + "examples/fibonacci2/guest", "examples/secp256k1-ecdsa-verify", "examples/secp256k1-ecdsa-verify/guest", "examples/sha2-ex", diff --git a/examples/fibonacci/Cargo.toml b/examples/fibonacci/Cargo.toml index d8d898a159..f20b3bc444 100644 --- a/examples/fibonacci/Cargo.toml +++ b/examples/fibonacci/Cargo.toml @@ -3,6 +3,9 @@ name = "fibonacci" version = "0.1.0" edition = "2021" +[features] +zk = ["jolt-sdk/zk"] + [dependencies] jolt-sdk = { workspace = true, features = ["host"] } tracing-subscriber.workspace = true diff --git a/examples/fibonacci2/Cargo.toml b/examples/fibonacci2/Cargo.toml new file mode 100644 index 0000000000..93e851a3dc --- /dev/null +++ b/examples/fibonacci2/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "fibonacci2" +version = "0.1.0" +edition = "2021" + +[features] +zk = ["jolt-sdk/zk", "guest/zk"] + +[dependencies] +jolt-sdk = { workspace = true, features = ["host"] } +tracing-subscriber.workspace = true +tracing.workspace = true +guest = { package = "fibonacci2-guest", path = "./guest" } diff --git a/examples/fibonacci2/guest/Cargo.toml b/examples/fibonacci2/guest/Cargo.toml new file mode 100644 index 0000000000..f2a640a425 --- /dev/null +++ b/examples/fibonacci2/guest/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "fibonacci2-guest" +version = "0.1.0" +edition = "2021" + +[features] +guest = [] +zk = [] + +[dependencies] +jolt = { package = "jolt-sdk", path = "../../../jolt-sdk" } diff --git a/examples/fibonacci2/guest/src/lib.rs b/examples/fibonacci2/guest/src/lib.rs new file mode 100644 index 0000000000..5cdaa5788e --- /dev/null +++ b/examples/fibonacci2/guest/src/lib.rs @@ -0,0 +1,75 @@ +#![cfg_attr(feature = "guest", no_std)] +use jolt::{end_cycle_tracking, start_cycle_tracking}; + +#[jolt::provable(heap_size = 32768, max_trace_length = 65536)] +fn fib(n: u32) -> u128 { + let mut a: u128 = 0; + let mut b: u128 = 1; + let mut sum: u128; + + start_cycle_tracking("fib_loop"); + for _ in 1..n { + sum = a + b; + a = b; + b = sum; + } + end_cycle_tracking("fib_loop"); + b +} + +#[cfg(any(feature = "guest", feature = "zk"))] +#[jolt::provable(heap_size = 32768, max_trace_length = 65536)] +fn fib_with_private_input(n: u32, private_bump: jolt::PrivateInput) -> u128 { + let adjusted_n = n + (*private_bump % 3); + + let mut a: u128 = 0; + let mut b: u128 = 1; + let mut sum: u128; + + start_cycle_tracking("fib_loop_private"); + for _ in 1..adjusted_n { + sum = a + b; + a = b; + b = sum; + } + end_cycle_tracking("fib_loop_private"); + b +} + +#[jolt::provable( + heap_size = 32768, + max_trace_length = 65536, + max_untrusted_advice_size = 131072 +)] +fn fib_with_large_advice_input(n: u32, advice: jolt::UntrustedAdvice<&[u8]>) -> u128 { + let advice = *advice; + jolt::check_advice!(advice.len() >= 2, "advice must contain at least 2 entries"); + + let last_idx = advice.len() - 1; + jolt::check_advice_eq!( + advice[last_idx] as u64, + 7u64, + "expected fixed marker in last advice byte" + ); + + let sampled_idx = (n as usize) % advice.len(); + jolt::check_advice_eq!( + advice[sampled_idx] as u64, + 7u64, + "expected fixed marker in sampled advice byte" + ); + + let mut a: u128 = 0; + let mut b: u128 = 1; + let mut sum: u128; + + start_cycle_tracking("fib_loop_large_advice"); + for _ in 1..n { + sum = a + b; + a = b; + b = sum; + } + end_cycle_tracking("fib_loop_large_advice"); + + b +} diff --git a/examples/fibonacci2/guest/src/main.rs b/examples/fibonacci2/guest/src/main.rs new file mode 100644 index 0000000000..82e3334ca7 --- /dev/null +++ b/examples/fibonacci2/guest/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(feature = "guest", no_std)] +#![no_main] + +#[allow(unused_imports)] +use fibonacci2_guest::*; diff --git a/examples/fibonacci2/src/main.rs b/examples/fibonacci2/src/main.rs new file mode 100644 index 0000000000..af4ce263a6 --- /dev/null +++ b/examples/fibonacci2/src/main.rs @@ -0,0 +1,158 @@ +use jolt_sdk::serialize_and_print_size; +use jolt_sdk::DoryContext; +use jolt_sdk::DoryGlobals; +use jolt_sdk::DoryLayout; +#[cfg(feature = "zk")] +use jolt_sdk::PrivateInput; +use jolt_sdk::UntrustedAdvice; +use std::time::Instant; +use tracing::info; + +pub fn main() { + tracing_subscriber::fmt::init(); + let layout = match std::env::var("FIB2_DORY_LAYOUT") + .ok() + .map(|v| v.to_ascii_lowercase()) + .as_deref() + { + Some("cycle") | Some("cyclemajor") => DoryLayout::CycleMajor, + Some("address") | Some("addressmajor") | None => DoryLayout::AddressMajor, + Some(other) => panic!( + "invalid FIB2_DORY_LAYOUT='{other}', expected one of: cycle, cyclemajor, address, addressmajor" + ), + }; + + DoryGlobals::initialize_context(1, 1, DoryContext::Main, Some(layout)) + .expect("failed to set Dory layout"); + info!("Using Dory layout: {layout:?}"); + + let save_to_disk = std::env::args().any(|arg| arg == "--save"); + let target_dir = "/tmp/jolt-guest-targets"; + + let mut program = guest::compile_fib(target_dir); + let shared_preprocessing = guest::preprocess_shared_fib(&mut program); + let prover_preprocessing = guest::preprocess_prover_fib(shared_preprocessing.clone()); + let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); + #[cfg(feature = "zk")] + let blindfold_setup = Some(prover_preprocessing.blindfold_setup()); + #[cfg(not(feature = "zk"))] + let blindfold_setup = None; + let verifier_preprocessing = + guest::preprocess_verifier_fib(shared_preprocessing, verifier_setup, blindfold_setup); + + if save_to_disk { + serialize_and_print_size( + "Verifier Preprocessing", + "/tmp/jolt_fib2_verifier_preprocessing.dat", + &verifier_preprocessing, + ) + .expect("Could not serialize preprocessing."); + } + + let prove_fib = guest::build_prover_fib(program, prover_preprocessing); + let verify_fib = guest::build_verifier_fib(verifier_preprocessing); + + let now = Instant::now(); + let (output, proof, io_device) = prove_fib(50); + info!("Prover runtime: {} s", now.elapsed().as_secs_f64()); + + if save_to_disk { + serialize_and_print_size("Proof", "/tmp/jolt_fib2_proof.bin", &proof) + .expect("Could not serialize proof."); + serialize_and_print_size("io_device", "/tmp/jolt_fib2_io_device.bin", &io_device) + .expect("Could not serialize io_device."); + } + + let is_valid = verify_fib(50, output, io_device.panic, proof); + info!("output: {output}"); + info!("valid: {is_valid}"); + + #[cfg(feature = "zk")] + { + let mut private_program = guest::compile_fib_with_private_input(target_dir); + let private_shared_preprocessing = + guest::preprocess_shared_fib_with_private_input(&mut private_program); + let private_prover_preprocessing = + guest::preprocess_prover_fib_with_private_input(private_shared_preprocessing.clone()); + let private_verifier_setup = private_prover_preprocessing.generators.to_verifier_setup(); + let private_verifier_preprocessing = guest::preprocess_verifier_fib_with_private_input( + private_shared_preprocessing, + private_verifier_setup, + Some(private_prover_preprocessing.blindfold_setup()), + ); + + let prove_fib_with_private_input = guest::build_prover_fib_with_private_input( + private_program, + private_prover_preprocessing, + ); + let verify_fib_with_private_input = + guest::build_verifier_fib_with_private_input(private_verifier_preprocessing); + + let now = Instant::now(); + let (private_output, private_proof, private_io_device) = + prove_fib_with_private_input(50, PrivateInput::new(0u32)); + info!( + "Prover runtime with private input: {} s", + now.elapsed().as_secs_f64() + ); + + let private_valid = verify_fib_with_private_input( + 50, + private_output, + private_io_device.panic, + private_proof, + ); + info!("output with private input: {private_output}"); + info!("valid with private input: {private_valid}"); + } + + let mut advice_program = guest::compile_fib_with_large_advice_input(target_dir); + let advice_shared_preprocessing = + guest::preprocess_shared_fib_with_large_advice_input(&mut advice_program); + let advice_prover_preprocessing = + guest::preprocess_prover_fib_with_large_advice_input(advice_shared_preprocessing.clone()); + let advice_verifier_setup = advice_prover_preprocessing.generators.to_verifier_setup(); + #[cfg(feature = "zk")] + let advice_blindfold_setup = Some(advice_prover_preprocessing.blindfold_setup()); + #[cfg(not(feature = "zk"))] + let advice_blindfold_setup = None; + let advice_verifier_preprocessing = guest::preprocess_verifier_fib_with_large_advice_input( + advice_shared_preprocessing, + advice_verifier_setup, + advice_blindfold_setup, + ); + + let prove_fib_with_large_advice = guest::build_prover_fib_with_large_advice_input( + advice_program, + advice_prover_preprocessing, + ); + let verify_fib_with_large_advice = + guest::build_verifier_fib_with_large_advice_input(advice_verifier_preprocessing); + + let advice_payload = vec![7u8; 65536]; + let advice_input = UntrustedAdvice::new(advice_payload.as_slice()); + let advice_input_bytes = jolt_sdk::postcard::to_stdvec(&advice_input) + .expect("failed to serialize advice input") + .len(); + + let now = Instant::now(); + let (advice_output, advice_proof, advice_io_device) = + prove_fib_with_large_advice(50, advice_input); + info!( + "Prover runtime with large advice input: {} s", + now.elapsed().as_secs_f64() + ); + + let advice_trace_length = advice_proof.trace_length as usize; + assert!( + advice_input_bytes > advice_trace_length, + "expected advice input bytes ({advice_input_bytes}) to exceed trace length ({advice_trace_length})", + ); + + let advice_valid = + verify_fib_with_large_advice(50, advice_output, advice_io_device.panic, advice_proof); + info!("output with large advice input: {advice_output}"); + info!("valid with large advice input: {advice_valid}"); + info!("advice input bytes: {advice_input_bytes}"); + info!("trace length with large advice input: {advice_trace_length}"); +} diff --git a/jolt-core/src/poly/commitment/dory/dory_globals.rs b/jolt-core/src/poly/commitment/dory/dory_globals.rs index 752fd6c40a..6958a5741a 100644 --- a/jolt-core/src/poly/commitment/dory/dory_globals.rs +++ b/jolt-core/src/poly/commitment/dory/dory_globals.rs @@ -4,7 +4,7 @@ use crate::utils::math::Math; use allocative::Allocative; use dory::backends::arkworks::{init_cache, ArkG1, ArkG2}; use std::sync::{ - atomic::{AtomicU8, Ordering}, + atomic::{AtomicU8, AtomicUsize, Ordering}, RwLock, }; @@ -138,6 +138,7 @@ impl From for u8 { // Main polynomial globals static GLOBAL_T: RwLock> = RwLock::new(None); +static MAIN_K_CHUNK: RwLock> = RwLock::new(None); static MAX_NUM_ROWS: RwLock> = RwLock::new(None); static NUM_COLUMNS: RwLock> = RwLock::new(None); @@ -156,6 +157,8 @@ static CURRENT_CONTEXT: AtomicU8 = AtomicU8::new(0); // Layout tracking: 0=CycleMajor, 1=AddressMajor static CURRENT_LAYOUT: AtomicU8 = AtomicU8::new(0); +// Largest Main log-embedding needed for precommitted/embed calculations. +static MAIN_LOG_EMBEDDING: AtomicUsize = AtomicUsize::new(0); /// Dory commitment context - determines which set of global parameters to use #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -229,6 +232,22 @@ impl DoryGlobals { log_t.saturating_sub(sigma_main) } + #[inline] + pub fn get_main_log_embedding() -> usize { + let stored = MAIN_LOG_EMBEDDING.load(Ordering::SeqCst); + if stored > 0 { + stored + } else { + let main_cols = Self::configured_main_num_columns(); + let main_rows = *MAX_NUM_ROWS + .read() + .unwrap() + .as_ref() + .expect("main max_num_rows not initialized"); + main_cols.log_2() + main_rows.log_2() + } + } + /// Get the current Dory context pub fn current_context() -> DoryContext { CURRENT_CONTEXT.load(Ordering::SeqCst).into() @@ -261,11 +280,84 @@ impl DoryGlobals { (Self::get_max_num_rows(), Self::get_num_columns()) } + #[inline] + pub(crate) fn main_k() -> usize { + *MAIN_K_CHUNK + .read() + .unwrap() + .as_ref() + .expect("main k not initialized") + } + + #[inline] + pub(crate) fn main_t() -> usize { + *GLOBAL_T + .read() + .unwrap() + .as_ref() + .expect("main t not initialized") + } + + #[inline] + pub(crate) fn configured_main_num_columns() -> usize { + *NUM_COLUMNS + .read() + .unwrap() + .as_ref() + .expect("main num_columns not initialized") + } + + #[inline] + fn main_embedding_extra_vars() -> usize { + let main_total_vars = Self::main_k().log_2() + Self::get_T().log_2(); + Self::get_main_log_embedding().saturating_sub(main_total_vars) + } + + /// Column stride for one-hot embeddings in the current layout/context. + pub fn one_hot_stride() -> usize { + if Self::current_context() != DoryContext::Main + || Self::get_layout() != DoryLayout::AddressMajor + { + return 1; + } + 1usize << Self::main_embedding_extra_vars() + } + + /// Column stride for dense trace-domain embeddings in the current layout/context. + pub fn dense_stride() -> usize { + if Self::current_context() != DoryContext::Main + || Self::get_layout() != DoryLayout::AddressMajor + { + return 1; + } + let dense_stride_log = Self::main_embedding_extra_vars() + Self::main_k().log_2(); + 1usize << dense_stride_log + } + + /// Returns the embedded cycle-domain size for the current Dory matrix. + pub fn get_embedded_t() -> usize { + let context = Self::current_context(); + if context != DoryContext::Main { + return Self::get_T(); + } + + let k = Self::main_k(); + let num_rows = Self::get_max_num_rows(); + let num_cols = Self::get_num_columns(); + let total = num_rows * num_cols; + debug_assert_eq!( + total % k, + 0, + "Invalid Main DoryGlobals: num_rows*num_cols must be divisible by K" + ); + total / k + } + /// Returns the "K" used to initialize the *main* Dory matrix for OneHot polynomials. - /// - /// This is derived from the identity: - /// `K * T == num_rows * num_cols` (all values are powers of two in our usage). pub fn k_from_matrix_shape() -> usize { + if Self::current_context() == DoryContext::Main { + return Self::main_k(); + } let (num_rows, num_cols) = Self::matrix_shape(); let t = Self::get_T(); debug_assert_eq!( @@ -278,18 +370,22 @@ impl DoryGlobals { /// For `AddressMajor`, each Dory matrix row corresponds to this many cycles. /// - /// Equivalent to `T / num_rows` and to `num_cols / K`. + /// Equivalent to `T / num_rows` and to `num_cols / dense_stride`. pub fn address_major_cycles_per_row() -> usize { - let (num_rows, num_cols) = Self::matrix_shape(); - let k = Self::k_from_matrix_shape(); - debug_assert!(k > 0); - debug_assert_eq!(num_cols % k, 0, "Expected num_cols to be divisible by K"); - debug_assert_eq!( - Self::get_T() % num_rows, + let num_cols = Self::get_num_columns(); + let dense_stride = Self::dense_stride(); + assert!(dense_stride > 0, "Dense stride must be positive"); + assert_eq!( + num_cols % dense_stride, 0, - "Expected T to be divisible by num_rows" + "Expected num_cols to be divisible by dense stride" + ); + let cycles_per_row = num_cols / dense_stride; + assert!( + cycles_per_row > 0, + "AddressMajor row must contain at least one cycle" ); - num_cols / k + cycles_per_row } fn set_max_num_rows_for_context(max_num_rows: usize, context: DoryContext) { @@ -338,6 +434,10 @@ impl DoryGlobals { } } + fn set_main_k(k: usize) { + *MAIN_K_CHUNK.write().unwrap() = Some(k); + } + pub fn get_num_columns() -> usize { let context = Self::current_context(); match context { @@ -403,6 +503,20 @@ impl DoryGlobals { (num_columns, num_rows, T) } + fn initialize_context_common( + K: usize, + embedded_t: usize, + stored_t: usize, + context: DoryContext, + ) -> Option<()> { + let (num_columns, num_rows, _) = Self::calculate_dimensions(K, embedded_t); + Self::set_num_columns_for_context(num_columns, context); + Self::set_T_for_context(stored_t, context); + Self::set_max_num_rows_for_context(num_rows, context); + + Some(()) + } + /// Initialize the globals for a specific Dory context /// /// # Arguments @@ -422,19 +536,30 @@ impl DoryGlobals { context: DoryContext, layout: Option, ) -> Option<()> { - let (num_columns, num_rows, t) = Self::calculate_dimensions(K, T); - Self::set_num_columns_for_context(num_columns, context); - Self::set_T_for_context(t, context); - Self::set_max_num_rows_for_context(num_rows, context); - - // For Main context, set layout (if provided) and ensure subsequent uses of `get_*` read from it if context == DoryContext::Main { - if let Some(l) = layout { - CURRENT_LAYOUT.store(l as u8, Ordering::SeqCst); - } - CURRENT_CONTEXT.store(DoryContext::Main as u8, Ordering::SeqCst); + return Self::initialize_main_with_log_embedding(K, T, K.log_2() + T.log_2(), layout); } + Self::initialize_context_common(K, T, T, context)?; + Some(()) + } + /// Initialize Main context with execution `T` and explicit `main_log_embedding` for + /// global precommitted geometry. + pub fn initialize_main_with_log_embedding( + K: usize, + T: usize, + matrix_total_vars: usize, + layout: Option, + ) -> Option<()> { + let log_k = K.log_2(); + let embedded_t = 1usize << matrix_total_vars.saturating_sub(log_k); + Self::initialize_context_common(K, embedded_t, T, DoryContext::Main)?; + Self::set_main_k(K); + if let Some(l) = layout { + CURRENT_LAYOUT.store(l as u8, Ordering::SeqCst); + } + CURRENT_CONTEXT.store(DoryContext::Main as u8, Ordering::SeqCst); + MAIN_LOG_EMBEDDING.store(matrix_total_vars, Ordering::SeqCst); Some(()) } @@ -443,6 +568,7 @@ impl DoryGlobals { pub fn reset() { // Reset main globals *GLOBAL_T.write().unwrap() = None; + *MAIN_K_CHUNK.write().unwrap() = None; *MAX_NUM_ROWS.write().unwrap() = None; *NUM_COLUMNS.write().unwrap() = None; @@ -460,6 +586,7 @@ impl DoryGlobals { *UNTRUSTED_ADVICE_NUM_COLUMNS.write().unwrap() = None; CURRENT_CONTEXT.store(0, Ordering::SeqCst); + MAIN_LOG_EMBEDDING.store(0, Ordering::SeqCst); } /// Initialize the prepared point cache for faster pairing operations diff --git a/jolt-core/src/poly/commitment/dory/wrappers.rs b/jolt-core/src/poly/commitment/dory/wrappers.rs index 18fb13d213..32050be144 100644 --- a/jolt-core/src/poly/commitment/dory/wrappers.rs +++ b/jolt-core/src/poly/commitment/dory/wrappers.rs @@ -202,31 +202,113 @@ where let dory_context = DoryGlobals::current_context(); let dory_layout = DoryGlobals::get_layout(); - // Dense polynomials (all scalar variants except OneHot/RLC) are committed row-wise. - // Under AddressMajor, dense coefficients occupy evenly-spaced columns, so each row - // commitment uses `cycles_per_row` bases (one per occupied column). - let (dense_affine_bases, dense_chunk_size): (Vec<_>, usize) = match (dory_context, dory_layout) - { - (DoryContext::Main, DoryLayout::AddressMajor) => { - let cycles_per_row = DoryGlobals::address_major_cycles_per_row(); - let bases: Vec<_> = g1_slice + let is_dense_poly = !matches!( + poly, + MultilinearPolynomial::OneHot(_) | MultilinearPolynomial::RLC(_) + ); + + let is_trace_dense_addr_major = matches!(dory_context, DoryContext::Main) + && dory_layout == DoryLayout::AddressMajor + && is_dense_poly; + debug_assert!( + !is_trace_dense_addr_major || poly.original_len() <= DoryGlobals::get_T(), + "Main+AddressMajor dense polynomial length exceeds trace T" + ); + + let (dense_affine_bases, dense_chunk_size, dense_sparse_row_terms): ( + Vec<_>, + usize, + Option>>, + ) = if is_trace_dense_addr_major { + let stride = DoryGlobals::dense_stride(); + let cycles_per_row = row_len / stride; + // This branch is taken when the AddressMajor trace-dense embedding stride exceeds + // the post-embedded Main row width (`row_len`), i.e. `row_len < stride`. + // + // With: + // - M = DoryGlobals::get_main_log_embedding() = total embedded Main vars + // - k = log2(main K) + // - t = log2(execution T) + // - e = embedding extra vars = M - (k + t) + // + // we have: + // - row_len = 2^sigma_main, where sigma_main = ceil(M/2) + // = 2^ceil((e + k + t)/2) + // - stride = 2^(main_embedding_extra_vars + k) = 2^(M - t) = 2^(e + k) + // + // so `cycles_per_row == 0` exactly when: + // ceil(M/2) < (M - t) <=> t < floor(M/2). + if cycles_per_row == 0 { + let dense_len = poly.original_len(); + let dense_affine_bases: Vec<_> = g1_slice .par_iter() .take(row_len) - .step_by(row_len / cycles_per_row) .map(|g| g.0.into_affine()) .collect(); - (bases, cycles_per_row) + let num_rows = DoryGlobals::get_max_num_rows(); + let sparse_terms: Vec<(usize, usize, Fr)> = (0..dense_len) + .into_par_iter() + .filter_map(|cycle| { + let coeff = poly.get_coeff(cycle); + if coeff.is_zero() { + return None; + } + let scaled_index = cycle.saturating_mul(stride); + let row_index = scaled_index / row_len; + let col_index = scaled_index % row_len; + debug_assert!(row_index < num_rows); + Some((row_index, col_index, coeff)) + }) + .collect(); + let mut row_terms: Vec> = vec![Vec::new(); num_rows]; + for (row_index, col_index, coeff) in sparse_terms { + row_terms[row_index].push((col_index, coeff)); + } + (dense_affine_bases, 1, Some(row_terms)) + } else { + let dense_affine_bases: Vec<_> = g1_slice + .par_iter() + .take(row_len) + .step_by(stride) + .map(|g| g.0.into_affine()) + .collect(); + (dense_affine_bases, cycles_per_row, None) } - _ => ( + } else { + ( g1_slice .par_iter() .take(row_len) .map(|g| g.0.into_affine()) .collect(), row_len, - ), + None, + ) }; + if let Some(row_terms) = dense_sparse_row_terms { + let result: Vec = row_terms + .into_par_iter() + .map(|terms| { + if terms.is_empty() { + return ArkG1(ark_bn254::G1Projective::zero()); + } + let mut bases = Vec::with_capacity(terms.len()); + let mut scalars = Vec::with_capacity(terms.len()); + for (col_index, scalar) in terms { + bases.push(dense_affine_bases[col_index]); + scalars.push(scalar); + } + ArkG1(VariableBaseMSM::msm_field_elements(&bases, &scalars).unwrap()) + }) + .collect(); + // SAFETY: Vec and Vec have the same memory layout when E = BN254. + #[allow(clippy::missing_transmute_annotations)] + unsafe { + return Ok(std::mem::transmute(result)); + } + } + let result: Vec = match poly { MultilinearPolynomial::LargeScalars(poly) => poly .Z diff --git a/jolt-core/src/poly/one_hot_polynomial.rs b/jolt-core/src/poly/one_hot_polynomial.rs index 5a807446f4..7134c04aac 100644 --- a/jolt-core/src/poly/one_hot_polynomial.rs +++ b/jolt-core/src/poly/one_hot_polynomial.rs @@ -56,9 +56,14 @@ impl OneHotPolynomial { /// /// Note: the Dory matrix may be square or almost-square depending on `log2(K*T)`. pub fn num_rows(&self) -> usize { - let t = self.nonzero_indices.len(); + let t = DoryGlobals::get_T(); match DoryGlobals::get_layout() { - DoryLayout::AddressMajor => t.div_ceil(DoryGlobals::address_major_cycles_per_row()), + DoryLayout::AddressMajor => { + if t == 0 { + return 0; + } + t.div_ceil(DoryGlobals::address_major_cycles_per_row()) + } DoryLayout::CycleMajor => (t * self.K).div_ceil(DoryGlobals::get_num_columns()), } } @@ -104,7 +109,7 @@ impl OneHotPolynomial { } pub fn from_indices(nonzero_indices: Vec>, K: usize) -> Self { - debug_assert_eq!(DoryGlobals::get_T(), nonzero_indices.len()); + debug_assert!(nonzero_indices.len() <= DoryGlobals::get_T()); assert!(K <= 1usize << u8::BITS, "K must be <= 256 for indices"); Self { @@ -120,9 +125,15 @@ impl OneHotPolynomial { bases: &[G::Affine], ) -> Vec { let layout = DoryGlobals::get_layout(); + let one_hot_stride = DoryGlobals::one_hot_stride(); let num_rows = self.num_rows(); let row_len = DoryGlobals::get_num_columns(); let t = self.nonzero_indices.len(); + let effective_t = DoryGlobals::get_T(); + debug_assert_eq!( + effective_t, t, + "one-hot polynomial length must match configured Main T" + ); debug_assert!( bases.len() >= row_len, @@ -172,11 +183,16 @@ impl OneHotPolynomial { // General path: collect column indices for each row based on layout let mut row_indices: Vec> = vec![Vec::new(); num_rows]; + let dense_stride = DoryGlobals::dense_stride(); for (cycle, k) in self.nonzero_indices.iter().enumerate() { if let Some(k) = k { - let global_index = layout.address_cycle_to_index(*k as usize, cycle, self.K, t); - let row_index = global_index / row_len; - let col_index = global_index % row_len; + let scaled_index = if layout == DoryLayout::AddressMajor { + cycle * dense_stride + (*k as usize) * one_hot_stride + } else { + layout.address_cycle_to_index(*k as usize, cycle, self.K, effective_t) + }; + let row_index = scaled_index / row_len; + let col_index = scaled_index % row_len; if row_index < num_rows { row_indices[row_index].push(col_index); } @@ -211,12 +227,13 @@ impl OneHotPolynomial { pub fn vector_matrix_product(&self, left_vec: &[F], coeff: F, result: &mut [F]) { let layout = DoryGlobals::get_layout(); let t = self.nonzero_indices.len(); + let effective_t = DoryGlobals::get_T(); let num_columns = DoryGlobals::get_num_columns(); debug_assert_eq!(result.len(), num_columns); // CycleMajor optimization for T >= row_len (typical case where T >= K) if layout == DoryLayout::CycleMajor && t >= num_columns { - let rows_per_k = t / num_columns; + let rows_per_k = effective_t / num_columns; result .par_iter_mut() .enumerate() @@ -234,11 +251,17 @@ impl OneHotPolynomial { } // General path: iterate through nonzero indices and compute contributions + let dense_stride = DoryGlobals::dense_stride(); + let one_hot_stride = DoryGlobals::one_hot_stride(); for (cycle, k) in self.nonzero_indices.iter().enumerate() { if let Some(k) = k { - let global_index = layout.address_cycle_to_index(*k as usize, cycle, self.K, t); - let row_index = global_index / num_columns; - let col_index = global_index % num_columns; + let scaled_index = if layout == DoryLayout::AddressMajor { + cycle * dense_stride + (*k as usize) * one_hot_stride + } else { + layout.address_cycle_to_index(*k as usize, cycle, self.K, effective_t) + }; + let row_index = scaled_index / num_columns; + let col_index = scaled_index % num_columns; if row_index < left_vec.len() && col_index < result.len() { result[col_index] += coeff * left_vec[row_index]; } diff --git a/jolt-core/src/poly/opening_proof.rs b/jolt-core/src/poly/opening_proof.rs index 43c4631860..7c5f0a50bc 100644 --- a/jolt-core/src/poly/opening_proof.rs +++ b/jolt-core/src/poly/opening_proof.rs @@ -811,37 +811,37 @@ where } } -/// Computes the Lagrange factor for embedding a smaller "advice" polynomial into the top-left -/// block of the main Dory matrix. +/// Computes the Lagrange factor for embedding a smaller polynomial into the top-left block of +/// the main Dory matrix. /// -/// Advice polynomials have fewer variables than main polynomials. To batch them together, -/// we embed advice in the top-left corner of the larger matrix and multiply by a Lagrange +/// Embedded polynomials can have fewer variables than main polynomials. To batch them together, +/// we embed them in the top-left corner of the larger matrix and multiply by a Lagrange /// selector that is 1 on that block and 0 elsewhere: /// /// ```text -/// Lagrange factor = ∏_{r ∈ opening_point, r ∉ advice_opening_point} (1 - r) +/// Lagrange factor = ∏_{r ∈ opening_point, r ∉ embedded_opening_point} (1 - r) /// ``` /// /// # Arguments /// - `opening_point`: The unified opening point for the Dory opening proof -/// - `advice_opening_point`: The opening point for the advice polynomial +/// - `embedded_opening_point`: The opening point for the embedded polynomial /// /// # Returns /// The Lagrange factor as a field element -pub fn compute_advice_lagrange_factor( +pub fn compute_lagrange_factor( opening_point: &[F::Challenge], - advice_opening_point: &[F::Challenge], + embedded_opening_point: &[F::Challenge], ) -> F { #[cfg(test)] { - for r in advice_opening_point.iter() { + for r in embedded_opening_point.iter() { assert!(opening_point.contains(r)); } } opening_point .iter() .map(|r| { - if advice_opening_point.contains(r) { + if embedded_opening_point.contains(r) { F::one() } else { F::one() - r diff --git a/jolt-core/src/poly/rlc_polynomial.rs b/jolt-core/src/poly/rlc_polynomial.rs index b60aa40c3a..3e02ddc198 100644 --- a/jolt-core/src/poly/rlc_polynomial.rs +++ b/jolt-core/src/poly/rlc_polynomial.rs @@ -295,21 +295,31 @@ impl RLCPolynomial { }); } DoryLayout::AddressMajor => { - let cycles_per_row = DoryGlobals::address_major_cycles_per_row(); - dense_result - .par_iter_mut() - .step_by(num_columns / cycles_per_row) + let dense_stride = DoryGlobals::dense_stride(); + dense_result = self + .dense_rlc + .par_iter() .enumerate() - .for_each(|(offset, dot_product_result)| { - *dot_product_result = self - .dense_rlc - .par_iter() - .skip(offset) - .step_by(cycles_per_row) - .zip(left_vec.par_iter()) - .map(|(&a, &b)| -> F { a * b }) - .sum::(); - }); + .fold( + || unsafe_allocate_zero_vec(num_columns), + |mut acc, (cycle, coeff)| { + let scaled_index = cycle.saturating_mul(dense_stride); + let row_index = scaled_index / num_columns; + if row_index >= left_vec.len() { + return acc; + } + let col_index = scaled_index % num_columns; + acc[col_index] += *coeff * left_vec[row_index]; + acc + }, + ) + .reduce( + || unsafe_allocate_zero_vec(num_columns), + |mut a, b| { + a.iter_mut().zip(b.iter()).for_each(|(x, y)| *x += *y); + a + }, + ); } } dense_result @@ -415,7 +425,7 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." return self.address_major_vector_matrix_product(left_vec, num_columns, &ctx); } - let T = DoryGlobals::get_T(); + let T = DoryGlobals::get_embedded_t(); match &ctx.trace_source { TraceSource::Materialized(trace) => { self.materialized_vector_matrix_product(left_vec, num_columns, trace, &ctx, T) diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index f6b66613ac..16d5da4adb 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -1,42 +1,11 @@ -//! Two-phase advice claim reduction (Stage 6 cycle → Stage 7 address) -//! -//! This module generalizes the previous single-phase `AdviceClaimReduction` so that trusted and -//! untrusted advice can be committed as an arbitrary Dory matrix `2^{nu_a} x 2^{sigma_a}` (balanced -//! by default), while still keeping a **single Stage 8 Dory opening** at the unified Dory point. -//! -//! For an advice matrix embedded as the **top-left block** `2^{nu_a} x 2^{sigma_a}`, the *native* -//! advice evaluation point (in Dory order, LSB-first) is: -//! - `advice_cols = col_coords[0..sigma_a]` -//! - `advice_rows = row_coords[0..nu_a]` -//! - `advice_point = [advice_cols || advice_rows]` -//! -//! In our current pipeline, `cycle` coordinates come from Stage 6 and `addr` coordinates come from -//! Stage 7. -//! - **Phase 1 (Stage 6)**: bind the cycle-derived advice coordinates and output an intermediate -//! scalar claim `C_mid`. -//! - **Phase 2 (Stage 7)**: resume from `C_mid`, bind the address-derived advice coordinates, and -//! cache the final advice opening `AdviceMLE(advice_point)` for batching into Stage 8. -//! -//! ## Dummy-gap scaling (within Stage 6) -//! With cycle-major order, there may be a gap during the cycle phase where the cycle variables -//! being bound in the batched sumcheck do not appear in the advice polynommial. -//! -//! We handle this without modifying the generic batched sumcheck by treating those intervening -//! rounds as **dummy internal rounds** (constant univariates), and maintaining a running scaling -//! factor `2^{-dummy_done}` so the per-round univariates remain consistent. -//! -//! Trusted and untrusted advice run as **separate** sumcheck instances (each may have different -//! dimensions). -//! +//! Two-phase advice claim reduction (Stage 6 cycle -> Stage 7 address). use std::cell::RefCell; -use std::cmp::{min, Ordering}; -use std::ops::Range; use crate::field::JoltField; -use crate::poly::commitment::dory::{DoryGlobals, DoryLayout}; +use crate::poly::commitment::dory::DoryGlobals; use crate::poly::eq_poly::EqPolynomial; -use crate::poly::multilinear_polynomial::{BindingOrder, MultilinearPolynomial, PolynomialBinding}; +use crate::poly::multilinear_polynomial::MultilinearPolynomial; #[cfg(feature = "zk")] use crate::poly::opening_proof::OpeningId; use crate::poly::opening_proof::{ @@ -50,12 +19,12 @@ use crate::subprotocols::sumcheck_prover::SumcheckInstanceProver; use crate::subprotocols::sumcheck_verifier::{SumcheckInstanceParams, SumcheckInstanceVerifier}; use crate::transcripts::Transcript; use crate::utils::math::Math; -use crate::zkvm::config::OneHotConfig; +use crate::zkvm::claim_reductions::{ + permute_precommitted_polys, precommitted_eq_evals_with_scaling, precommitted_skip_round_scale, + PrecomittedParams, PrecomittedProver, PrecommittedClaimReduction, PrecommittedPhase, + PrecommittedSchedulingReference, TWO_PHASE_DEGREE_BOUND, +}; use allocative::Allocative; -use common::jolt_device::MemoryLayout; -use rayon::prelude::*; - -const DEGREE_BOUND: usize = 2; #[derive(Clone, Copy, Debug, PartialEq, Eq, Allocative)] pub enum AdviceKind { @@ -63,134 +32,78 @@ pub enum AdviceKind { Untrusted, } -#[derive(Debug, Clone, Allocative, PartialEq, Eq)] -pub enum ReductionPhase { - CycleVariables, - AddressVariables, -} - #[derive(Clone, Allocative)] pub struct AdviceClaimReductionParams { pub kind: AdviceKind, - pub phase: ReductionPhase, - pub log_k_chunk: usize, + pub phase: PrecommittedPhase, + pub precommitted: PrecommittedClaimReduction, pub log_t: usize, pub advice_col_vars: usize, pub advice_row_vars: usize, - /// Number of column variables in the main Dory matrix - pub main_col_vars: usize, - /// Number of row variables in the main Dory matrix - pub main_row_vars: usize, - #[allocative(skip)] - pub cycle_phase_row_rounds: Range, - #[allocative(skip)] - pub cycle_phase_col_rounds: Range, pub r_val: OpeningPoint, - /// (little-endian) challenges for the cycle phase variables - pub cycle_var_challenges: Vec, -} - -fn cycle_phase_round_schedule( - log_T: usize, - log_k_chunk: usize, - main_col_vars: usize, - advice_row_vars: usize, - advice_col_vars: usize, -) -> (Range, Range) { - match DoryGlobals::get_layout() { - DoryLayout::CycleMajor => { - // Low-order cycle variables correspond to the low-order bits of the - // column index - let col_binding_rounds = 0..min(log_T, advice_col_vars); - // High-order cycle variables correspond to the low-order bits of the - // rows index - let row_binding_rounds = - min(log_T, main_col_vars)..min(log_T, main_col_vars + advice_row_vars); - (col_binding_rounds, row_binding_rounds) - } - DoryLayout::AddressMajor => { - // Low-order cycle variables correspond to the high-order bits of the - // column index - let col_binding_rounds = 0..advice_col_vars.saturating_sub(log_k_chunk); - // High-order cycle variables correspond to the bits of the row index - let row_binding_rounds = main_col_vars.saturating_sub(log_k_chunk) - ..min( - log_T, - main_col_vars.saturating_sub(log_k_chunk) + advice_row_vars, - ); - (col_binding_rounds, row_binding_rounds) - } - } } impl AdviceClaimReductionParams { pub fn new( kind: AdviceKind, - memory_layout: &MemoryLayout, + advice_size_bytes: usize, trace_len: usize, + scheduling_reference: PrecommittedSchedulingReference, accumulator: &dyn OpeningAccumulator, ) -> Self { - let max_advice_size_bytes = match kind { - AdviceKind::Trusted => memory_layout.max_trusted_advice_size as usize, - AdviceKind::Untrusted => memory_layout.max_untrusted_advice_size as usize, - }; - let log_t = trace_len.log_2(); - let log_k_chunk = OneHotConfig::new(log_t).log_k_chunk as usize; - let (main_col_vars, main_row_vars) = DoryGlobals::main_sigma_nu(log_k_chunk, log_t); - let r_val = accumulator .get_advice_opening(kind, SumcheckId::RamValCheck) .map(|(p, _)| p) .unwrap(); let (advice_col_vars, advice_row_vars) = - DoryGlobals::advice_sigma_nu_from_max_bytes(max_advice_size_bytes); - let (col_binding_rounds, row_binding_rounds) = cycle_phase_round_schedule( - log_t, - log_k_chunk, - main_col_vars, + DoryGlobals::advice_sigma_nu_from_max_bytes(advice_size_bytes); + let total_vars = advice_row_vars + advice_col_vars; + let precommitted = PrecommittedClaimReduction::new( + total_vars, advice_row_vars, advice_col_vars, + scheduling_reference, ); Self { kind, - phase: ReductionPhase::CycleVariables, + phase: PrecommittedPhase::CycleVariables, + precommitted, advice_col_vars, advice_row_vars, - log_k_chunk, log_t, - main_col_vars, - main_row_vars, - cycle_phase_row_rounds: row_binding_rounds, - cycle_phase_col_rounds: col_binding_rounds, r_val, - cycle_var_challenges: vec![], } } - /// (Total # advice variables) - (# variables bound during cycle phase) pub fn num_address_phase_rounds(&self) -> usize { - (self.advice_col_vars + self.advice_row_vars) - - (self.cycle_phase_col_rounds.len() + self.cycle_phase_row_rounds.len()) + self.precommitted.num_address_phase_rounds() + } + + pub fn transition_to_address_phase(&mut self) { + self.phase = PrecommittedPhase::AddressVariables; + } + + pub fn round_offset(&self, max_num_rounds: usize) -> usize { + self.precommitted.round_offset( + self.phase == PrecommittedPhase::CycleVariables, + max_num_rounds, + ) } } impl SumcheckInstanceParams for AdviceClaimReductionParams { fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { match self.phase { - ReductionPhase::CycleVariables => { - let mut claim = F::zero(); - if let Some((_, eval)) = - accumulator.get_advice_opening(self.kind, SumcheckId::RamValCheck) - { - claim += eval; - } - claim + PrecommittedPhase::CycleVariables => { + accumulator + .get_advice_opening(self.kind, SumcheckId::RamValCheck) + .expect("RamValCheck advice opening missing") + .1 } - ReductionPhase::AddressVariables => { - // Address phase starts from the cycle phase intermediate claim. + PrecommittedPhase::AddressVariables => { accumulator .get_advice_opening(self.kind, SumcheckId::AdviceClaimReductionCyclePhase) .expect("Cycle phase intermediate claim not found") @@ -200,77 +113,39 @@ impl SumcheckInstanceParams for AdviceClaimReductionParams { } fn degree(&self) -> usize { - DEGREE_BOUND + TWO_PHASE_DEGREE_BOUND } fn num_rounds(&self) -> usize { - match self.phase { - ReductionPhase::CycleVariables => { - if !self.cycle_phase_row_rounds.is_empty() { - self.cycle_phase_row_rounds.end - self.cycle_phase_col_rounds.start - } else { - self.cycle_phase_col_rounds.len() - } - } - ReductionPhase::AddressVariables => { - let first_phase_rounds = - self.cycle_phase_row_rounds.len() + self.cycle_phase_col_rounds.len(); - // Total advice variables, minus the variables bound during the cycle phase - (self.advice_col_vars + self.advice_row_vars) - first_phase_rounds - } - } + self.precommitted + .num_rounds_for_phase(self.phase == PrecommittedPhase::CycleVariables) } - /// Rearrange the opening point so that it is big-endian with respect to the original, - /// unpermuted advice/EQ polynomials. - fn normalize_opening_point( - &self, - challenges: &[::Challenge], - ) -> OpeningPoint { - if self.phase == ReductionPhase::CycleVariables { - let advice_vars = self.advice_col_vars + self.advice_row_vars; - let mut advice_var_challenges: Vec = Vec::with_capacity(advice_vars); - advice_var_challenges - .extend_from_slice(&challenges[self.cycle_phase_col_rounds.clone()]); - advice_var_challenges - .extend_from_slice(&challenges[self.cycle_phase_row_rounds.clone()]); - return OpeningPoint::::new(advice_var_challenges).match_endianness(); - } - - match DoryGlobals::get_layout() { - DoryLayout::CycleMajor => OpeningPoint::::new( - [self.cycle_var_challenges.as_slice(), challenges].concat(), - ) - .match_endianness(), - DoryLayout::AddressMajor => OpeningPoint::::new( - [challenges, self.cycle_var_challenges.as_slice()].concat(), - ) - .match_endianness(), - } + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + self.precommitted.normalize_opening_point( + self.phase == PrecommittedPhase::CycleVariables, + challenges, + self.log_t, + ) } #[cfg(feature = "zk")] fn input_claim_constraint(&self) -> InputClaimConstraint { - match self.phase { - ReductionPhase::CycleVariables => { - let val_opening = match self.kind { - AdviceKind::Trusted => OpeningId::TrustedAdvice(SumcheckId::RamValCheck), - AdviceKind::Untrusted => OpeningId::UntrustedAdvice(SumcheckId::RamValCheck), - }; - InputClaimConstraint::direct(val_opening) - } - ReductionPhase::AddressVariables => { - let cycle_phase_opening = match self.kind { - AdviceKind::Trusted => { - OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) - } - AdviceKind::Untrusted => { - OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) - } - }; - InputClaimConstraint::direct(cycle_phase_opening) - } - } + let opening = match self.phase { + PrecommittedPhase::CycleVariables => match self.kind { + AdviceKind::Trusted => OpeningId::TrustedAdvice(SumcheckId::RamValCheck), + AdviceKind::Untrusted => OpeningId::UntrustedAdvice(SumcheckId::RamValCheck), + }, + PrecommittedPhase::AddressVariables => match self.kind { + AdviceKind::Trusted => { + OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) + } + AdviceKind::Untrusted => { + OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) + } + }, + }; + InputClaimConstraint::direct(opening) } #[cfg(feature = "zk")] @@ -281,8 +156,8 @@ impl SumcheckInstanceParams for AdviceClaimReductionParams { #[cfg(feature = "zk")] fn output_claim_constraint(&self) -> Option { match self.phase { - ReductionPhase::CycleVariables => { - let advice_opening = match self.kind { + PrecommittedPhase::CycleVariables => { + let opening = match self.kind { AdviceKind::Trusted => { OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) } @@ -290,10 +165,10 @@ impl SumcheckInstanceParams for AdviceClaimReductionParams { OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) } }; - Some(OutputClaimConstraint::direct(advice_opening)) + Some(OutputClaimConstraint::direct(opening)) } - ReductionPhase::AddressVariables => { - let advice_opening = match self.kind { + PrecommittedPhase::AddressVariables => { + let opening = match self.kind { AdviceKind::Trusted => { OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReduction) } @@ -301,11 +176,9 @@ impl SumcheckInstanceParams for AdviceClaimReductionParams { OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReduction) } }; - // output = (eq_combined * scale) * advice_claim - // Challenge(0) holds eq_combined * scale (computed in output_constraint_challenge_values) Some(OutputClaimConstraint::linear(vec![( ValueSource::Challenge(0), - ValueSource::Opening(advice_opening), + ValueSource::Opening(opening), )])) } } @@ -314,193 +187,97 @@ impl SumcheckInstanceParams for AdviceClaimReductionParams { #[cfg(feature = "zk")] fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { match self.phase { - ReductionPhase::CycleVariables => vec![], - ReductionPhase::AddressVariables => { + PrecommittedPhase::CycleVariables => vec![], + PrecommittedPhase::AddressVariables => { let opening_point = self.normalize_opening_point(sumcheck_challenges); let eq_eval = EqPolynomial::mle(&opening_point.r, &self.r_val.r); - - let gap_len = if self.cycle_phase_row_rounds.is_empty() - || self.cycle_phase_col_rounds.is_empty() - { - 0 - } else { - self.cycle_phase_row_rounds.start - self.cycle_phase_col_rounds.end - }; - let two_inv = F::from_u64(2).inverse().unwrap(); - let scale = (0..gap_len).fold(F::one(), |acc, _| acc * two_inv); - + let scale: F = precommitted_skip_round_scale(&self.precommitted); vec![eq_eval * scale] } } } } +impl PrecomittedParams for AdviceClaimReductionParams { + fn is_cycle_phase(&self) -> bool { + self.phase == PrecommittedPhase::CycleVariables + } + + fn is_cycle_phase_round(&self, round: usize) -> bool { + self.precommitted.is_cycle_phase_round(round) + } + + fn is_address_phase_round(&self, round: usize) -> bool { + self.precommitted.is_address_phase_round(round) + } + + fn cycle_alignment_rounds(&self) -> usize { + self.precommitted.cycle_alignment_rounds() + } + + fn address_alignment_rounds(&self) -> usize { + self.precommitted.address_alignment_rounds() + } + + fn record_cycle_challenge(&mut self, challenge: F::Challenge) { + self.precommitted.record_cycle_challenge(challenge); + } +} + #[derive(Allocative)] pub struct AdviceClaimReductionProver { - pub params: AdviceClaimReductionParams, - advice_poly: MultilinearPolynomial, - eq_poly: MultilinearPolynomial, - /// Maintains the running internal scaling factor 2^{-dummy_done}. - scale: F, + core: PrecomittedProver>, } impl AdviceClaimReductionProver { + pub fn params(&self) -> &AdviceClaimReductionParams { + self.core.params() + } + + pub fn transition_to_address_phase(&mut self) { + self.core.params_mut().transition_to_address_phase(); + } + pub fn initialize( params: AdviceClaimReductionParams, advice_poly: MultilinearPolynomial, ) -> Self { - let eq_evals = EqPolynomial::evals(¶ms.r_val.r); - - let main_cols = 1 << params.main_col_vars; - // Maps a (row, col) position in the Dory matrix layout to its - // implied (address, cycle). - let row_col_to_address_cycle = |row: usize, col: usize| -> (usize, usize) { - match DoryGlobals::get_layout() { - DoryLayout::CycleMajor => { - let global_index = row as u128 * main_cols + col as u128; - let address = global_index / (1 << params.log_t); - let cycle = global_index % (1 << params.log_t); - (address as usize, cycle as usize) - } - DoryLayout::AddressMajor => { - let global_index = row as u128 * main_cols + col as u128; - let address = global_index % (1 << params.log_k_chunk); - let cycle = global_index / (1 << params.log_k_chunk); - (address as usize, cycle as usize) - } - } - }; - - let advice_cols = 1 << params.advice_col_vars; - // Maps an index in the advice vector to its implied (address, cycle), based - // on the position the index maps to in the Dory matrix layout. - let advice_index_to_address_cycle = |index: usize| -> (usize, usize) { - let row = index / advice_cols; - let col = index % advice_cols; - row_col_to_address_cycle(row, col) - }; - - let mut permuted_coeffs: Vec<(usize, (u64, F))> = match advice_poly { - MultilinearPolynomial::U64Scalars(poly) => poly - .coeffs - .into_par_iter() - .zip(eq_evals.into_par_iter()) - .enumerate() - .collect(), - _ => panic!("Advice should have u64 coefficients"), + let eq_evals = + precommitted_eq_evals_with_scaling(¶ms.r_val.r, None, ¶ms.precommitted); + let (advice_poly, eq_poly): (MultilinearPolynomial, MultilinearPolynomial) = { + let MultilinearPolynomial::U64Scalars(poly) = advice_poly else { + panic!("Advice should have u64 coefficients"); + }; + let mut permuted = + permute_precommitted_polys(vec![poly.coeffs], ¶ms.precommitted).into_iter(); + let advice_poly = permuted + .next() + .expect("expected one permuted advice polynomial"); + let eq_poly = eq_evals.into(); + (advice_poly, eq_poly) }; - // Sort the advice and EQ polynomial coefficients by (address, cycle). - // By sorting this way, binding the resulting polynomials in low-to-high - // order is equivalent to binding the original polynomials' "cycle" variables - // low-to-high, then their "address" variables low-to-high. - permuted_coeffs.par_sort_by(|&(index_a, _), &(index_b, _)| { - let (address_a, cycle_a) = advice_index_to_address_cycle(index_a); - let (address_b, cycle_b) = advice_index_to_address_cycle(index_b); - match address_a.cmp(&address_b) { - Ordering::Less => Ordering::Less, - Ordering::Greater => Ordering::Greater, - Ordering::Equal => cycle_a.cmp(&cycle_b), - } - }); - - let (advice_coeffs, eq_coeffs): (Vec<_>, Vec<_>) = permuted_coeffs - .into_par_iter() - .map(|(_, coeffs)| coeffs) - .unzip(); - let advice_poly = advice_coeffs.into(); - let eq_poly = eq_coeffs.into(); Self { - params, - advice_poly, - eq_poly, - scale: F::one(), + core: PrecomittedProver::new(params, advice_poly, eq_poly), } } - - fn compute_message_unscaled(&mut self, previous_claim_unscaled: F) -> UniPoly { - let half = self.advice_poly.len() / 2; - let evals: [F; DEGREE_BOUND] = (0..half) - .into_par_iter() - .map(|j| { - let a_evals = self - .advice_poly - .sumcheck_evals_array::(j, BindingOrder::LowToHigh); - let eq_evals = self - .eq_poly - .sumcheck_evals_array::(j, BindingOrder::LowToHigh); - - let mut out = [F::zero(); DEGREE_BOUND]; - for i in 0..DEGREE_BOUND { - out[i] = a_evals[i] * eq_evals[i]; - } - out - }) - .reduce( - || [F::zero(); DEGREE_BOUND], - |mut acc, arr| { - acc.par_iter_mut() - .zip(arr.par_iter()) - .for_each(|(a, b)| *a += *b); - acc - }, - ); - UniPoly::from_evals_and_hint(previous_claim_unscaled, &evals) - } } impl SumcheckInstanceProver for AdviceClaimReductionProver { fn get_params(&self) -> &dyn SumcheckInstanceParams { - &self.params + self.core.params() + } + + fn round_offset(&self, max_num_rounds: usize) -> usize { + self.core.params().round_offset(max_num_rounds) } fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { - if self.params.phase == ReductionPhase::CycleVariables - && !self.params.cycle_phase_col_rounds.contains(&round) - && !self.params.cycle_phase_row_rounds.contains(&round) - { - // Current sumcheck variable does not appear in advice polynomial, so we - // can simply send a constant polynomial equal to the previous claim divided by 2 - UniPoly::from_coeff(vec![previous_claim * F::from_u64(2).inverse().unwrap()]) - } else { - // Account for (1) internal dummy rounds already traversed and - // (2) trailing dummy rounds after this instance's active window in the batched sumcheck. - let num_trailing_variables = match self.params.phase { - ReductionPhase::CycleVariables => { - self.params.log_t.saturating_sub(self.params.num_rounds()) - } - ReductionPhase::AddressVariables => self - .params - .log_k_chunk - .saturating_sub(self.params.num_rounds()), - }; - let scaling_factor = self.scale * F::one().mul_pow_2(num_trailing_variables); - let prev_unscaled = previous_claim * scaling_factor.inverse().unwrap(); - let poly_unscaled = self.compute_message_unscaled(prev_unscaled); - poly_unscaled * scaling_factor - } + self.core.compute_message(round, previous_claim) } fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { - match self.params.phase { - ReductionPhase::CycleVariables => { - if !self.params.cycle_phase_col_rounds.contains(&round) - && !self.params.cycle_phase_row_rounds.contains(&round) - { - // Each dummy internal round halves the running claim; equivalently, we multiply the - // scaling factor by 1/2. - self.scale *= F::from_u64(2).inverse().unwrap(); - } else { - self.advice_poly.bind_parallel(r_j, BindingOrder::LowToHigh); - self.eq_poly.bind_parallel(r_j, BindingOrder::LowToHigh); - self.params.cycle_var_challenges.push(r_j); - } - } - ReductionPhase::AddressVariables => { - self.advice_poly.bind_parallel(r_j, BindingOrder::LowToHigh); - self.eq_poly.bind_parallel(r_j, BindingOrder::LowToHigh); - } - } + self.core.ingest_challenge(r_j, round); } fn cache_openings( @@ -508,43 +285,27 @@ impl SumcheckInstanceProver for AdviceClaimRe accumulator: &mut ProverOpeningAccumulator, sumcheck_challenges: &[F::Challenge], ) { - let opening_point = self.params.normalize_opening_point(sumcheck_challenges); - if self.params.phase == ReductionPhase::CycleVariables { - // Compute the intermediate claim C_mid = (2^{-gap}) * Σ_y advice(y) * eq(y), - // where y are the remaining (address-derived) advice row variables. - let len = self.advice_poly.len(); - debug_assert_eq!(len, self.eq_poly.len()); - - let mut sum = F::zero(); - for i in 0..len { - sum += self.advice_poly.get_bound_coeff(i) * self.eq_poly.get_bound_coeff(i); - } - let c_mid = sum * self.scale; + let params = self.core.params(); + let opening_point = params.normalize_opening_point(sumcheck_challenges); + if params.phase == PrecommittedPhase::CycleVariables { + let c_mid = self.core.cycle_intermediate_claim(); - match self.params.kind { + match params.kind { AdviceKind::Trusted => accumulator.append_trusted_advice( SumcheckId::AdviceClaimReductionCyclePhase, - // This is a phase-boundary intermediate reduction claim (c_mid), not an advice - // polynomial opening. Store it without an opening point so it can't be deduped - // against the final advice opening. OpeningPoint::::new(vec![]), c_mid, ), AdviceKind::Untrusted => accumulator.append_untrusted_advice( SumcheckId::AdviceClaimReductionCyclePhase, - // This is a phase-boundary intermediate reduction claim (c_mid), not an advice - // polynomial opening. Store it without an opening point so it can't be deduped - // against the final advice opening. OpeningPoint::::new(vec![]), c_mid, ), } } - // If we're done binding advice variables, cache the final advice opening - if self.advice_poly.len() == 1 { - let advice_claim = self.advice_poly.final_sumcheck_claim(); - match self.params.kind { + if let Some(advice_claim) = self.core.final_claim_if_ready() { + match params.kind { AdviceKind::Trusted => accumulator.append_trusted_advice( SumcheckId::AdviceClaimReduction, opening_point, @@ -559,19 +320,6 @@ impl SumcheckInstanceProver for AdviceClaimRe } } - fn round_offset(&self, max_num_rounds: usize) -> usize { - match self.params.phase { - ReductionPhase::CycleVariables => { - // Align to the *start* of Booleanity's cycle segment, so local rounds correspond - // to low Dory column bits in the unified point ordering. - let booleanity_rounds = self.params.log_k_chunk + self.params.log_t; - let booleanity_offset = max_num_rounds - booleanity_rounds; - booleanity_offset + self.params.log_k_chunk - } - ReductionPhase::AddressVariables => 0, - } - } - #[cfg(feature = "allocative")] fn update_flamegraph(&self, flamegraph: &mut allocative::FlameGraphBuilder) { flamegraph.visit_root(self); @@ -585,11 +333,18 @@ pub struct AdviceClaimReductionVerifier { impl AdviceClaimReductionVerifier { pub fn new( kind: AdviceKind, - memory_layout: &MemoryLayout, + advice_size_bytes: usize, trace_len: usize, + scheduling_reference: PrecommittedSchedulingReference, accumulator: &VerifierOpeningAccumulator, ) -> Self { - let params = AdviceClaimReductionParams::new(kind, memory_layout, trace_len, accumulator); + let params = AdviceClaimReductionParams::new( + kind, + advice_size_bytes, + trace_len, + scheduling_reference, + accumulator, + ); Self { params: RefCell::new(params), @@ -611,32 +366,20 @@ impl SumcheckInstanceVerifier ) -> F { let params = self.params.borrow(); match params.phase { - ReductionPhase::CycleVariables => { + PrecommittedPhase::CycleVariables => { accumulator .get_advice_opening(params.kind, SumcheckId::AdviceClaimReductionCyclePhase) - .unwrap_or_else(|| panic!("Cycle phase intermediate claim not found",)) + .unwrap_or_else(|| panic!("Cycle phase intermediate claim not found")) .1 } - ReductionPhase::AddressVariables => { + PrecommittedPhase::AddressVariables => { let opening_point = params.normalize_opening_point(sumcheck_challenges); let advice_claim = accumulator .get_advice_opening(params.kind, SumcheckId::AdviceClaimReduction) .expect("Final advice claim not found") .1; - let eq_eval = EqPolynomial::mle(&opening_point.r, ¶ms.r_val.r); - - let gap_len = if params.cycle_phase_row_rounds.is_empty() - || params.cycle_phase_col_rounds.is_empty() - { - 0 - } else { - params.cycle_phase_row_rounds.start - params.cycle_phase_col_rounds.end - }; - let two_inv = F::from_u64(2).inverse().unwrap(); - let scale = (0..gap_len).fold(F::one(), |acc, _| acc * two_inv); - - // Account for Phase 1's internal dummy-gap traversal via constant scaling. + let scale: F = precommitted_skip_round_scale(¶ms.precommitted); advice_claim * eq_eval * scale } } @@ -648,30 +391,26 @@ impl SumcheckInstanceVerifier sumcheck_challenges: &[F::Challenge], ) { let mut params = self.params.borrow_mut(); - if params.phase == ReductionPhase::CycleVariables { + if params.phase == PrecommittedPhase::CycleVariables { let opening_point = params.normalize_opening_point(sumcheck_challenges); match params.kind { AdviceKind::Trusted => accumulator.append_trusted_advice( SumcheckId::AdviceClaimReductionCyclePhase, - // This is a phase-boundary intermediate reduction claim (c_mid), not an advice - // polynomial opening. Store it without an opening point so it can't be deduped - // against the final advice opening. OpeningPoint::::new(vec![]), ), AdviceKind::Untrusted => accumulator.append_untrusted_advice( SumcheckId::AdviceClaimReductionCyclePhase, - // This is a phase-boundary intermediate reduction claim (c_mid), not an advice - // polynomial opening. Store it without an opening point so it can't be deduped - // against the final advice opening. OpeningPoint::::new(vec![]), ), } let opening_point_le: OpeningPoint = opening_point.match_endianness(); - params.cycle_var_challenges = opening_point_le.r; + params + .precommitted + .set_cycle_var_challenges(opening_point_le.r); } if params.num_address_phase_rounds() == 0 - || params.phase == ReductionPhase::AddressVariables + || params.phase == PrecommittedPhase::AddressVariables { let opening_point = params.normalize_opening_point(sumcheck_challenges); match params.kind { @@ -685,13 +424,6 @@ impl SumcheckInstanceVerifier fn round_offset(&self, max_num_rounds: usize) -> usize { let params = self.params.borrow(); - match params.phase { - ReductionPhase::CycleVariables => { - let booleanity_rounds = params.log_k_chunk + params.log_t; - let booleanity_offset = max_num_rounds - booleanity_rounds; - booleanity_offset + params.log_k_chunk - } - ReductionPhase::AddressVariables => 0, - } + params.round_offset(max_num_rounds) } } diff --git a/jolt-core/src/zkvm/claim_reductions/mod.rs b/jolt-core/src/zkvm/claim_reductions/mod.rs index 5d19f993a1..b68d052551 100644 --- a/jolt-core/src/zkvm/claim_reductions/mod.rs +++ b/jolt-core/src/zkvm/claim_reductions/mod.rs @@ -2,6 +2,7 @@ pub mod advice; pub mod hamming_weight; pub mod increments; pub mod instruction_lookups; +mod precommitted; pub mod ram_ra; pub mod registers; @@ -21,6 +22,11 @@ pub use instruction_lookups::{ InstructionLookupsClaimReductionSumcheckParams, InstructionLookupsClaimReductionSumcheckProver, InstructionLookupsClaimReductionSumcheckVerifier, }; +pub use precommitted::{ + permute_precommitted_polys, precommitted_eq_evals_with_scaling, precommitted_skip_round_scale, + PrecomittedParams, PrecomittedProver, PrecommittedClaimReduction, PrecommittedEmbeddingMode, + PrecommittedPhase, PrecommittedSchedulingReference, TWO_PHASE_DEGREE_BOUND, +}; pub use ram_ra::{ RaReductionParams, RamRaClaimReductionSumcheckProver, RamRaClaimReductionSumcheckVerifier, }; diff --git a/jolt-core/src/zkvm/claim_reductions/precommitted.rs b/jolt-core/src/zkvm/claim_reductions/precommitted.rs new file mode 100644 index 0000000000..3f201d482f --- /dev/null +++ b/jolt-core/src/zkvm/claim_reductions/precommitted.rs @@ -0,0 +1,615 @@ +use allocative::Allocative; +use rayon::prelude::*; + +use crate::field::JoltField; +use crate::poly::commitment::dory::{DoryGlobals, DoryLayout}; +use crate::poly::eq_poly::EqPolynomial; +use crate::poly::multilinear_polynomial::{BindingOrder, MultilinearPolynomial, PolynomialBinding}; +use crate::poly::opening_proof::{OpeningPoint, BIG_ENDIAN, LITTLE_ENDIAN}; +use crate::poly::unipoly::UniPoly; +use crate::subprotocols::sumcheck_verifier::SumcheckInstanceParams; +use crate::utils::math::Math; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Allocative)] +pub enum PrecommittedEmbeddingMode { + DominantPrecommitted, + EmbeddedPrecommitted, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Allocative)] +pub enum PrecommittedPhase { + CycleVariables, + AddressVariables, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Allocative)] +pub struct PrecommittedSchedulingReference { + pub main_total_vars: usize, + pub reference_total_vars: usize, + pub cycle_alignment_rounds: usize, + pub address_rounds: usize, + pub joint_col_vars: usize, +} + +#[derive(Debug, Clone, Allocative)] +pub struct PrecommittedClaimReduction { + pub scheduling_reference: PrecommittedSchedulingReference, + pub embedding_mode: PrecommittedEmbeddingMode, + pub cycle_var_challenges: Vec, + dory_opening_round_permutation_be: Vec, + poly_opening_round_permutation_be: Vec, + cycle_phase_rounds: Vec, + cycle_phase_total_rounds: usize, + address_phase_rounds: Vec, + address_phase_total_rounds: usize, +} + +impl PrecommittedClaimReduction { + /// Compute shared scheduling dimensions from Main and precommitted candidates. + /// + /// `reference_total_vars` is the largest total var count across Main and candidates. + pub fn scheduling_reference( + main_total_vars: usize, + candidates: &[usize], + ) -> PrecommittedSchedulingReference { + let address_rounds = DoryGlobals::main_k().log_2(); + let max_precommitted = candidates.iter().copied().max().unwrap_or(0); + let reference_total_vars = std::cmp::max(main_total_vars, max_precommitted); + let cycle_alignment_rounds = reference_total_vars.saturating_sub(address_rounds); + let (reference_sigma, _) = DoryGlobals::balanced_sigma_nu(reference_total_vars); + let joint_col_vars = std::cmp::max( + DoryGlobals::configured_main_num_columns().log_2(), + reference_sigma, + ); + PrecommittedSchedulingReference { + main_total_vars, + reference_total_vars, + cycle_alignment_rounds, + address_rounds, + joint_col_vars, + } + } + + #[inline] + pub fn new( + poly_total_vars: usize, + poly_row_vars: usize, + poly_col_vars: usize, + scheduling_reference: PrecommittedSchedulingReference, + ) -> Self { + let has_precommitted_dominance = + scheduling_reference.reference_total_vars > scheduling_reference.main_total_vars; + let embedding_mode = Self::embedding_mode_for_poly(poly_total_vars, &scheduling_reference); + let dory_opening_round_permutation_be = Self::reference_dory_opening_round_permutation_be( + &scheduling_reference, + has_precommitted_dominance, + DoryGlobals::main_t().log_2(), + ); + let poly_opening_round_permutation_be = Self::project_dory_round_permutation_for_poly( + &dory_opening_round_permutation_be, + &scheduling_reference, + poly_row_vars, + poly_col_vars, + ); + let (cycle_phase_rounds, address_phase_rounds) = Self::active_rounds_from_poly_permutation( + &poly_opening_round_permutation_be, + scheduling_reference.cycle_alignment_rounds, + ); + Self { + scheduling_reference, + embedding_mode, + cycle_var_challenges: vec![], + dory_opening_round_permutation_be, + poly_opening_round_permutation_be, + cycle_phase_rounds, + cycle_phase_total_rounds: scheduling_reference.cycle_alignment_rounds, + address_phase_rounds, + address_phase_total_rounds: scheduling_reference.address_rounds, + } + } + + #[inline] + fn embedding_mode_for_poly( + poly_total_vars: usize, + reference: &PrecommittedSchedulingReference, + ) -> PrecommittedEmbeddingMode { + let has_precommitted_dominance = reference.reference_total_vars > reference.main_total_vars; + let embedding_mode = + if has_precommitted_dominance && poly_total_vars == reference.reference_total_vars { + PrecommittedEmbeddingMode::DominantPrecommitted + } else { + PrecommittedEmbeddingMode::EmbeddedPrecommitted + }; + if embedding_mode == PrecommittedEmbeddingMode::DominantPrecommitted { + assert_eq!(poly_total_vars, reference.reference_total_vars); + } + embedding_mode + } + + fn reference_dory_opening_round_permutation_be( + reference: &PrecommittedSchedulingReference, + has_precommitted_dominance: bool, + dense_cycle_prefix_rounds: usize, + ) -> Vec { + let cycle_rounds = reference.cycle_alignment_rounds; + let address_rounds = reference.address_rounds; + let total_rounds = cycle_rounds + address_rounds; + if has_precommitted_dominance { + let address_rev = (cycle_rounds..total_rounds).rev(); + match DoryGlobals::get_layout() { + DoryLayout::CycleMajor => { + let t = dense_cycle_prefix_rounds.min(cycle_rounds); + let prefix_rev = (0..cycle_rounds.saturating_sub(t)).rev(); + let dense_rev = (cycle_rounds.saturating_sub(t)..cycle_rounds).rev(); + return prefix_rev.chain(address_rev).chain(dense_rev).collect(); + } + DoryLayout::AddressMajor => { + let t = dense_cycle_prefix_rounds.min(cycle_rounds); + let prefix_rev = (0..cycle_rounds.saturating_sub(t)).rev(); + let dense_rev = (cycle_rounds.saturating_sub(t)..cycle_rounds).rev(); + return dense_rev.chain(address_rev).chain(prefix_rev).collect(); + } + } + } + + match DoryGlobals::get_layout() { + DoryLayout::CycleMajor => (0..total_rounds).rev().collect(), + DoryLayout::AddressMajor => { + let cycle_rev = (0..cycle_rounds).rev(); + let address_rev = (cycle_rounds..total_rounds).rev(); + cycle_rev.chain(address_rev).collect() + } + } + } + + fn project_dory_round_permutation_for_poly( + dory_opening_round_permutation_be: &[usize], + reference: &PrecommittedSchedulingReference, + poly_row_vars: usize, + poly_col_vars: usize, + ) -> Vec { + let total_full = reference.reference_total_vars; + let sigma_full = reference.joint_col_vars; + let nu_full = total_full.saturating_sub(sigma_full); + assert_eq!( + dory_opening_round_permutation_be.len(), + total_full, + "reference dory round permutation length mismatch", + ); + assert!( + poly_row_vars <= nu_full && poly_col_vars <= sigma_full, + "top-left projection requires poly dims <= full dims (poly row/col vars={poly_row_vars}/{poly_col_vars}, full row/col vars={nu_full}/{sigma_full})" + ); + let row_be = &dory_opening_round_permutation_be[..nu_full]; + let col_be = &dory_opening_round_permutation_be[nu_full..nu_full + sigma_full]; + let row_tail = &row_be[nu_full - poly_row_vars..]; + let col_tail = &col_be[sigma_full - poly_col_vars..]; + [row_tail, col_tail].concat() + } + + fn active_rounds_from_poly_permutation( + poly_opening_round_permutation_be: &[usize], + cycle_alignment_rounds: usize, + ) -> (Vec, Vec) { + let mut cycle_phase_rounds = Vec::new(); + let mut address_phase_rounds = Vec::new(); + for &global_round in poly_opening_round_permutation_be.iter() { + if global_round < cycle_alignment_rounds { + cycle_phase_rounds.push(global_round); + } else { + address_phase_rounds.push(global_round - cycle_alignment_rounds); + } + } + cycle_phase_rounds.sort_unstable(); + cycle_phase_rounds.dedup(); + address_phase_rounds.sort_unstable(); + address_phase_rounds.dedup(); + (cycle_phase_rounds, address_phase_rounds) + } + + #[inline] + pub fn num_address_phase_rounds(&self) -> usize { + self.address_phase_rounds.len() + } + + #[inline] + pub fn is_cycle_phase_round(&self, round: usize) -> bool { + self.cycle_phase_rounds + .iter() + .any(|&scheduled| scheduled == round) + } + + #[inline] + pub fn is_address_phase_round(&self, round: usize) -> bool { + self.address_phase_rounds + .iter() + .any(|&scheduled| scheduled == round) + } + + #[inline] + pub fn cycle_alignment_rounds(&self) -> usize { + self.scheduling_reference.cycle_alignment_rounds + } + + #[inline] + pub fn address_alignment_rounds(&self) -> usize { + self.scheduling_reference.address_rounds + } + + #[inline] + pub fn num_rounds_for_phase(&self, is_cycle_phase: bool) -> usize { + if is_cycle_phase { + self.cycle_phase_total_rounds + } else { + self.address_phase_total_rounds + } + } + + pub fn round_offset(&self, is_cycle_phase: bool, max_num_rounds: usize) -> usize { + let _ = (is_cycle_phase, max_num_rounds); + 0 + } + + fn cycle_challenge_for_round(&self, round: usize) -> F::Challenge { + let idx = self + .cycle_phase_rounds + .iter() + .position(|&scheduled_round| scheduled_round == round) + .unwrap_or_else(|| { + panic!( + "missing recorded cycle challenge for round={} (active rounds={:?})", + round, self.cycle_phase_rounds + ) + }); + assert!( + idx < self.cycle_var_challenges.len(), + "cycle challenge vector too short: idx={} len={}", + idx, + self.cycle_var_challenges.len() + ); + self.cycle_var_challenges[idx] + } + + pub fn normalize_opening_point( + &self, + is_cycle_phase: bool, + challenges: &[F::Challenge], + dense_cycle_prefix_rounds: usize, + ) -> OpeningPoint { + let _ = dense_cycle_prefix_rounds; + if is_cycle_phase { + let local_cycle_challenges: Vec = self + .cycle_phase_rounds + .iter() + .map(|&round| { + assert!( + round < challenges.len(), + "cycle round index out of local bounds: round={} local_len={}", + round, + challenges.len() + ); + challenges[round] + }) + .collect(); + return OpeningPoint::::new(local_cycle_challenges) + .match_endianness(); + } + + debug_assert_eq!( + self.dory_opening_round_permutation_be.len(), + self.scheduling_reference.reference_total_vars + ); + let cycle_round_limit = self.cycle_alignment_rounds(); + let opening_rounds = &self.poly_opening_round_permutation_be; + let mut opening_point_be = Vec::with_capacity(opening_rounds.len()); + for &global_round in opening_rounds.iter() { + if global_round < cycle_round_limit { + opening_point_be.push(self.cycle_challenge_for_round(global_round)); + } else { + let address_round = global_round - cycle_round_limit; + assert!( + address_round < challenges.len(), + "address round index out of local bounds: round={} local_len={}", + address_round, + challenges.len() + ); + opening_point_be.push(challenges[address_round]); + } + } + OpeningPoint::::new(opening_point_be) + } + + #[inline] + pub fn record_cycle_challenge(&mut self, challenge: F::Challenge) { + self.cycle_var_challenges.push(challenge); + } + + #[inline] + pub fn set_cycle_var_challenges(&mut self, challenges: Vec) { + self.cycle_var_challenges = challenges; + } +} + +pub fn permute_precommitted_polys( + coeffs_by_poly: Vec>, + precommitted: &PrecommittedClaimReduction, +) -> Vec> +where + MultilinearPolynomial: From>, +{ + if coeffs_by_poly.is_empty() { + return Vec::new(); + } + let coeffs_len = coeffs_by_poly[0].len(); + assert!( + coeffs_by_poly + .iter() + .all(|coeffs| coeffs.len() == coeffs_len), + "all precommitted polynomials must have equal coefficient lengths", + ); + let inverse_permutation = precommitted_sumcheck_inverse_index_permutation( + coeffs_len, + &precommitted.poly_opening_round_permutation_be, + ); + let permuted_coeffs_by_poly: Vec> = + if let Some(inverse_permutation) = inverse_permutation { + coeffs_by_poly + .into_iter() + .map(|coeffs| { + (0..coeffs_len) + .into_par_iter() + .map(|new_idx| { + let old_idx = inverse_permutation[new_idx]; + coeffs[old_idx] + }) + .collect() + }) + .collect() + } else { + coeffs_by_poly + }; + permuted_coeffs_by_poly + .into_iter() + .map(Into::into) + .collect() +} + +pub fn precommitted_eq_evals_with_scaling( + challenges_be: &[F::Challenge], + scaling_factor: Option, + precommitted: &PrecommittedClaimReduction, +) -> Vec +where + F: std::ops::Mul + std::ops::SubAssign, +{ + let permuted_challenges = precommitted_permute_eq_challenges( + challenges_be, + &precommitted.poly_opening_round_permutation_be, + ); + if let Some(permuted_challenges) = permuted_challenges { + EqPolynomial::evals_with_scaling(&permuted_challenges, scaling_factor) + } else { + EqPolynomial::evals_with_scaling(challenges_be, scaling_factor) + } +} + +fn precommitted_permute_eq_challenges( + challenges_be: &[C], + poly_opening_round_permutation_be: &[usize], +) -> Option> { + let old_lsb_to_new_lsb = + precommitted_sumcheck_lsb_permutation(poly_opening_round_permutation_be)?; + assert_eq!( + challenges_be.len(), + old_lsb_to_new_lsb.len(), + "challenge vector length mismatch for precommitted eq permutation", + ); + let num_vars = challenges_be.len(); + let mut permuted_challenges = challenges_be.to_vec(); + for old_be in 0..num_vars { + let old_lsb = num_vars - 1 - old_be; + let new_lsb = old_lsb_to_new_lsb[old_lsb]; + let new_be = num_vars - 1 - new_lsb; + permuted_challenges[new_be] = challenges_be[old_be]; + } + Some(permuted_challenges) +} + +fn precommitted_sumcheck_lsb_permutation( + poly_opening_round_permutation_be: &[usize], +) -> Option> { + let num_vars = poly_opening_round_permutation_be.len(); + let mut be_var_by_round: Vec = (0..num_vars).collect(); + be_var_by_round.sort_unstable_by_key(|&be_idx| poly_opening_round_permutation_be[be_idx]); + + let mut old_lsb_to_new_lsb = vec![0usize; num_vars]; + for (new_lsb, be_var_idx) in be_var_by_round.into_iter().enumerate() { + let old_lsb = num_vars - 1 - be_var_idx; + old_lsb_to_new_lsb[old_lsb] = new_lsb; + } + + if old_lsb_to_new_lsb + .iter() + .enumerate() + .all(|(old_lsb, &new_lsb)| old_lsb == new_lsb) + { + return None; + } + Some(old_lsb_to_new_lsb) +} + +fn precommitted_sumcheck_inverse_index_permutation( + coeffs_len: usize, + poly_opening_round_permutation_be: &[usize], +) -> Option> { + let num_vars = poly_opening_round_permutation_be.len(); + assert_eq!( + coeffs_len, + 1usize << num_vars, + "precommitted coeff vector length mismatch: len={} expected=2^{}", + coeffs_len, + num_vars + ); + let old_lsb_to_new_lsb = + precommitted_sumcheck_lsb_permutation(poly_opening_round_permutation_be)?; + + let mut new_lsb_to_old_lsb = vec![0usize; num_vars]; + for (old_lsb, &new_lsb) in old_lsb_to_new_lsb.iter().enumerate() { + new_lsb_to_old_lsb[new_lsb] = old_lsb; + } + + let inverse_permutation: Vec = (0..coeffs_len) + .into_par_iter() + .map(|new_idx| { + let mut old_idx = 0usize; + for new_lsb in 0..num_vars { + let bit = (new_idx >> new_lsb) & 1usize; + let old_lsb = new_lsb_to_old_lsb[new_lsb]; + old_idx |= bit << old_lsb; + } + old_idx + }) + .collect(); + Some(inverse_permutation) +} + +pub const TWO_PHASE_DEGREE_BOUND: usize = 2; + +pub trait PrecomittedParams: SumcheckInstanceParams { + fn is_cycle_phase(&self) -> bool; + fn is_cycle_phase_round(&self, round: usize) -> bool; + fn is_address_phase_round(&self, round: usize) -> bool; + fn cycle_alignment_rounds(&self) -> usize; + fn address_alignment_rounds(&self) -> usize; + fn record_cycle_challenge(&mut self, challenge: F::Challenge); +} + +#[derive(Allocative)] +pub struct PrecomittedProver> { + params: P, + value_poly: MultilinearPolynomial, + eq_poly: MultilinearPolynomial, + scale: F, +} + +impl> PrecomittedProver { + pub fn new( + params: P, + value_poly: MultilinearPolynomial, + eq_poly: MultilinearPolynomial, + ) -> Self { + Self { + params, + value_poly, + eq_poly, + scale: F::one(), + } + } + + pub fn params(&self) -> &P { + &self.params + } + + pub fn params_mut(&mut self) -> &mut P { + &mut self.params + } + + fn compute_message_unscaled(&self, previous_claim_unscaled: F) -> UniPoly { + let half = self.value_poly.len() / 2; + let value_poly = &self.value_poly; + let eq_poly = &self.eq_poly; + let evals: [F; TWO_PHASE_DEGREE_BOUND] = (0..half) + .into_par_iter() + .map(|j| { + let value_evals = value_poly + .sumcheck_evals_array::(j, BindingOrder::LowToHigh); + let eq_evals = eq_poly + .sumcheck_evals_array::(j, BindingOrder::LowToHigh); + + let mut out = [F::zero(); TWO_PHASE_DEGREE_BOUND]; + for i in 0..TWO_PHASE_DEGREE_BOUND { + out[i] = value_evals[i] * eq_evals[i]; + } + out + }) + .reduce( + || [F::zero(); TWO_PHASE_DEGREE_BOUND], + |mut acc, arr| { + acc.iter_mut().zip(arr.iter()).for_each(|(a, b)| *a += *b); + acc + }, + ); + UniPoly::from_evals_and_hint(previous_claim_unscaled, &evals) + } + + pub fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { + let is_active_round = if self.params.is_cycle_phase() { + self.params.is_cycle_phase_round(round) + } else { + self.params.is_address_phase_round(round) + }; + if !is_active_round { + return UniPoly::from_coeff(vec![previous_claim * F::from_u64(2).inverse().unwrap()]); + } + + let trailing_cap = if self.params.is_cycle_phase() { + self.params.cycle_alignment_rounds() + } else { + self.params.address_alignment_rounds() + }; + let num_trailing_variables = trailing_cap.saturating_sub(self.params.num_rounds()); + let scaling_factor = self.scale * F::one().mul_pow_2(num_trailing_variables); + let prev_unscaled = previous_claim * scaling_factor.inverse().unwrap(); + let poly_unscaled = self.compute_message_unscaled(prev_unscaled); + poly_unscaled * scaling_factor + } + + pub fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { + let is_active_round = if self.params.is_cycle_phase() { + self.params.is_cycle_phase_round(round) + } else { + self.params.is_address_phase_round(round) + }; + if !is_active_round { + self.scale *= F::from_u64(2).inverse().unwrap(); + return; + } + + self.value_poly.bind_parallel(r_j, BindingOrder::LowToHigh); + self.eq_poly.bind_parallel(r_j, BindingOrder::LowToHigh); + if self.params.is_cycle_phase() { + self.params.record_cycle_challenge(r_j); + } + } + + pub fn cycle_intermediate_claim(&self) -> F { + let len = self.value_poly.len(); + assert_eq!(len, self.eq_poly.len()); + + let mut sum = F::zero(); + for i in 0..len { + sum += self.value_poly.get_bound_coeff(i) * self.eq_poly.get_bound_coeff(i); + } + sum * self.scale + } + + pub fn final_claim_if_ready(&self) -> Option { + if self.value_poly.len() == 1 { + Some(self.value_poly.get_bound_coeff(0)) + } else { + None + } + } +} + +pub fn precommitted_skip_round_scale( + precommitted: &PrecommittedClaimReduction, +) -> F { + let cycle_gap_len = + precommitted.cycle_phase_total_rounds - precommitted.cycle_phase_rounds.len(); + let address_gap_len = + precommitted.address_phase_total_rounds - precommitted.address_phase_rounds.len(); + let gap_len = cycle_gap_len + address_gap_len; + let two_inv = F::from_u64(2).inverse().unwrap(); + (0..gap_len).fold(F::one(), |acc, _| acc * two_inv) +} diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 5ccd87f10f..13be300147 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -2,7 +2,6 @@ use crate::poly::opening_proof::OpeningId; #[cfg(feature = "zk")] use crate::zkvm::stage8_opening_ids; -use crate::zkvm::{claim_reductions::advice::ReductionPhase, config::OneHotConfig}; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; use std::{ @@ -37,11 +36,10 @@ use crate::{ commitment_scheme::{StreamingCommitmentScheme, ZkEvalCommitment}, dory::{DoryGlobals, DoryLayout}, }, - eq_poly::EqPolynomial, multilinear_polynomial::MultilinearPolynomial, opening_proof::{ - compute_advice_lagrange_factor, DoryOpeningState, OpeningAccumulator, - ProverOpeningAccumulator, SumcheckId, + compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningPoint, + ProverOpeningAccumulator, SumcheckId, BIG_ENDIAN, }, rlc_polynomial::{RLCStreamingData, TraceSource}, }, @@ -65,9 +63,9 @@ use crate::{ HammingWeightClaimReductionParams, HammingWeightClaimReductionProver, IncClaimReductionSumcheckParams, IncClaimReductionSumcheckProver, InstructionLookupsClaimReductionSumcheckParams, - InstructionLookupsClaimReductionSumcheckProver, RaReductionParams, - RamRaClaimReductionSumcheckProver, RegistersClaimReductionSumcheckParams, - RegistersClaimReductionSumcheckProver, + InstructionLookupsClaimReductionSumcheckProver, PrecommittedClaimReduction, + RaReductionParams, RamRaClaimReductionSumcheckProver, + RegistersClaimReductionSumcheckParams, RegistersClaimReductionSumcheckProver, }, config::OneHotParams, instruction_lookups::{ @@ -279,81 +277,81 @@ impl< ) } - /// Adjusts the padded trace length to ensure the main Dory matrix is large enough - /// to embed advice polynomials as the top-left block. - /// - /// Returns the adjusted padded_trace_len that satisfies: - /// - `sigma_main >= max_sigma_a` - /// - `nu_main >= max_nu_a` - /// - /// Panics if `max_padded_trace_length` is too small for the configured advice sizes. - fn adjust_trace_length_for_advice( - mut padded_trace_len: usize, - max_padded_trace_length: usize, - max_trusted_advice_size: u64, - max_untrusted_advice_size: u64, - has_trusted_advice: bool, - has_untrusted_advice: bool, - ) -> usize { - // Canonical advice shape policy (balanced): - // - advice_vars = log2(advice_len) - // - sigma_a = ceil(advice_vars/2) - // - nu_a = advice_vars - sigma_a - let mut max_sigma_a = 0usize; - let mut max_nu_a = 0usize; - - if has_trusted_advice { - let (sigma_a, nu_a) = - DoryGlobals::advice_sigma_nu_from_max_bytes(max_trusted_advice_size as usize); - max_sigma_a = max_sigma_a.max(sigma_a); - max_nu_a = max_nu_a.max(nu_a); + #[inline] + fn main_total_vars(&self) -> usize { + let trace_log_t = self.trace.len().log_2(); + let log_k_chunk = self.one_hot_params.log_k_chunk; + let mut max_total_vars = trace_log_t + log_k_chunk; + for total_vars in self.precommitted_candidate_total_vars() { + max_total_vars = max_total_vars.max(total_vars); } - if has_untrusted_advice { - let (sigma_a, nu_a) = - DoryGlobals::advice_sigma_nu_from_max_bytes(max_untrusted_advice_size as usize); - max_sigma_a = max_sigma_a.max(sigma_a); - max_nu_a = max_nu_a.max(nu_a); + max_total_vars + } + + #[inline] + fn precommitted_candidate_total_vars(&self) -> Vec { + let mut candidates = Vec::new(); + if !self.program_io.trusted_advice.is_empty() { + let (sigma, nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( + self.program_io.memory_layout.max_trusted_advice_size as usize, + ); + candidates.push(sigma + nu); } - if max_sigma_a == 0 && max_nu_a == 0 { - return padded_trace_len; + if !self.program_io.untrusted_advice.is_empty() { + let (sigma, nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( + self.program_io.memory_layout.max_untrusted_advice_size as usize, + ); + candidates.push(sigma + nu); } + candidates + } - // Require main matrix dimensions to be large enough to embed advice as the top-left - // block: sigma_main >= sigma_a and nu_main >= nu_a. - // - // This loop doubles padded_trace_len until the main Dory matrix is large enough. - // Each doubling increases log_t by 1, which increases total_vars by 1 (since - // log_k_chunk stays constant for a given log_t range), increasing both sigma_main - // and nu_main by roughly 0.5 each iteration. - while { - let log_t = padded_trace_len.log_2(); - let log_k_chunk = OneHotConfig::new(log_t).log_k_chunk as usize; - let (sigma_main, nu_main) = DoryGlobals::main_sigma_nu(log_k_chunk, log_t); - sigma_main < max_sigma_a || nu_main < max_nu_a - } { - if padded_trace_len >= max_padded_trace_length { - // This is a configuration error: the preprocessing was set up with - // max_padded_trace_length too small for the configured advice sizes. - // Cannot recover at runtime - user must fix their configuration. - let log_t = padded_trace_len.log_2(); - let log_k_chunk = OneHotConfig::new(log_t).log_k_chunk as usize; - let total_vars = log_k_chunk + log_t; - let (sigma_main, nu_main) = DoryGlobals::main_sigma_nu(log_k_chunk, log_t); - panic!( - "Configuration error: trace too small to embed advice into Dory batch opening.\n\ - Current: (sigma_main={sigma_main}, nu_main={nu_main}) from total_vars={total_vars} (log_t={log_t}, log_k_chunk={log_k_chunk})\n\ - Required: (sigma_a={max_sigma_a}, nu_a={max_nu_a}) for advice embedding\n\ - Solutions:\n\ - 1. Increase max_trace_length in preprocessing (currently {max_padded_trace_length})\n\ - 2. Reduce max_trusted_advice_size or max_untrusted_advice_size\n\ - 3. Run a program with more cycles" + fn stage8_opening_point(&self) -> OpeningPoint { + let native_main_vars = self.trace.len().log_2() + self.one_hot_params.log_k_chunk; + let mut opening_candidates: Vec<(&str, OpeningPoint)> = Vec::new(); + if let Some((point, _)) = self + .opening_accumulator + .get_advice_opening(AdviceKind::Trusted, SumcheckId::AdviceClaimReduction) + { + opening_candidates.push(("trusted_advice", point)); + } + if let Some((point, _)) = self + .opening_accumulator + .get_advice_opening(AdviceKind::Untrusted, SumcheckId::AdviceClaimReduction) + { + opening_candidates.push(("untrusted_advice", point)); + } + + let max_len = opening_candidates + .iter() + .map(|(_, p)| p.r.len()) + .max() + .unwrap_or(0); + if max_len > native_main_vars { + let dominant = opening_candidates + .iter() + .find(|(_, p)| p.r.len() == max_len) + .expect("at least one dominant precommitted candidate expected"); + for (name, point) in opening_candidates + .iter() + .filter(|(_, p)| p.r.len() == max_len) + { + assert_eq!( + point.r, dominant.1.r, + "incompatible dominant precommitted anchors: {} and {} have equal dimensionality {} but different opening points", + dominant.0, name, max_len ); } - padded_trace_len = (padded_trace_len * 2).min(max_padded_trace_length); + OpeningPoint::::new(dominant.1.r.clone()) + } else { + self.opening_accumulator + .get_committed_polynomial_opening( + CommittedPolynomial::InstructionRa(0), + SumcheckId::HammingWeightClaimReduction, + ) + .0 } - - padded_trace_len } pub fn gen_from_trace( @@ -389,20 +387,6 @@ impl< ); } - // We may need extra padding so the main Dory matrix has enough (row, col) variables - // to embed advice commitments committed in their own preprocessing-only contexts. - let has_trusted_advice = !program_io.trusted_advice.is_empty(); - let has_untrusted_advice = !program_io.untrusted_advice.is_empty(); - - let padded_trace_len = Self::adjust_trace_length_for_advice( - padded_trace_len, - preprocessing.shared.max_padded_trace_length, - preprocessing.shared.memory_layout.max_trusted_advice_size, - preprocessing.shared.memory_layout.max_untrusted_advice_size, - has_trusted_advice, - has_untrusted_advice, - ); - trace.resize(padded_trace_len, Cycle::NoOp); // Calculate K for DoryGlobals initialization @@ -672,30 +656,28 @@ impl< Vec, HashMap, ) { - let _guard = DoryGlobals::initialize_context( + let main_total_vars = self.main_total_vars(); + let trace = Arc::clone(&self.trace); + let _guard = DoryGlobals::initialize_main_with_log_embedding( 1 << self.one_hot_params.log_k_chunk, - self.padded_trace_len, - DoryContext::Main, + trace.len(), + main_total_vars, Some(DoryGlobals::get_layout()), ); let polys = all_committed_polynomials(&self.one_hot_params); - let T = DoryGlobals::get_T(); + let T = DoryGlobals::get_embedded_t(); - // For AddressMajor, use non-streaming commit path since streaming assumes CycleMajor layout - let (commitments, hint_map) = if DoryGlobals::get_layout() == DoryLayout::AddressMajor { + // AddressMajor uses non-streaming commit path, and we also use non-streaming when + // Stage 6/8 embedding domain exceeds the trace domain. + let use_materialized_commit = + DoryGlobals::get_layout() == DoryLayout::AddressMajor || self.trace.len() != T; + let (commitments, hint_map) = if use_materialized_commit { tracing::debug!( - "Using non-streaming commit path for AddressMajor layout with {} polynomials", + "Using non-streaming commit path with {} polynomials", polys.len() ); - // Materialize the trace for non-streaming commit - let trace: Vec = self - .lazy_trace - .clone() - .pad_using(T, |_| Cycle::NoOp) - .collect(); - // Generate witnesses and commit using the regular (non-streaming) path let (commitments, hints): (Vec<_>, Vec<_>) = polys .par_iter() @@ -1329,12 +1311,21 @@ impl< &mut self.transcript, ); + let main_total_vars = self.trace.len().log_2() + self.one_hot_params.log_k_chunk; + let precommitted_candidates = self.precommitted_candidate_total_vars(); + let precommitted_scheduling_reference = + PrecommittedClaimReduction::::scheduling_reference( + main_total_vars, + &precommitted_candidates, + ); + // Advice claim reduction (Phase 1 in Stage 6b): trusted and untrusted are separate instances. if self.advice.trusted_advice_polynomial.is_some() { let trusted_advice_params = AdviceClaimReductionParams::new( AdviceKind::Trusted, - &self.program_io.memory_layout, + self.program_io.memory_layout.max_trusted_advice_size as usize, self.trace.len(), + precommitted_scheduling_reference, &self.opening_accumulator, ); // Note: We clone the advice polynomial here because Stage 8 needs the original polynomial @@ -1355,8 +1346,9 @@ impl< if self.advice.untrusted_advice_polynomial.is_some() { let untrusted_advice_params = AdviceClaimReductionParams::new( AdviceKind::Untrusted, - &self.program_io.memory_layout, + self.program_io.memory_layout.max_untrusted_advice_size as usize, self.trace.len(), + precommitted_scheduling_reference, &self.opening_accumulator, ); // Note: We clone the advice polynomial here because Stage 8 needs the original polynomial @@ -1943,12 +1935,12 @@ impl< self.advice_reduction_prover_trusted.take() { if advice_reduction_prover_trusted - .params + .params() .num_address_phase_rounds() > 0 { // Transition phase - advice_reduction_prover_trusted.params.phase = ReductionPhase::AddressVariables; + advice_reduction_prover_trusted.transition_to_address_phase(); instances.push(Box::new(advice_reduction_prover_trusted)); } } @@ -1956,12 +1948,12 @@ impl< self.advice_reduction_prover_untrusted.take() { if advice_reduction_prover_untrusted - .params + .params() .num_address_phase_rounds() > 0 { // Transition phase - advice_reduction_prover_untrusted.params.phase = ReductionPhase::AddressVariables; + advice_reduction_prover_untrusted.transition_to_address_phase(); instances.push(Box::new(advice_reduction_prover_untrusted)); } } @@ -1988,70 +1980,60 @@ impl< ) -> PCS::Proof { tracing::info!("Stage 8 proving (Dory batch opening)"); - let _guard = DoryGlobals::initialize_context( - self.one_hot_params.k_chunk, - self.padded_trace_len, - DoryContext::Main, - Some(DoryGlobals::get_layout()), - ); - - // Get the unified opening point from HammingWeightClaimReduction - // This contains (r_address_stage7 || r_cycle_stage6) in big-endian - let (opening_point, _) = self.opening_accumulator.get_committed_polynomial_opening( - CommittedPolynomial::InstructionRa(0), - SumcheckId::HammingWeightClaimReduction, - ); - - let log_k_chunk = self.one_hot_params.log_k_chunk; - let r_address_stage7 = &opening_point.r[..log_k_chunk]; + let opening_point = self.stage8_opening_point(); let mut polynomial_claims = Vec::new(); let mut scaling_factors = Vec::new(); - // Dense polynomials: RamInc and RdInc (from IncClaimReduction in Stage 6) - // at r_cycle_stage6 only (length log_T) - let (_, ram_inc_claim) = self.opening_accumulator.get_committed_polynomial_opening( - CommittedPolynomial::RamInc, - SumcheckId::IncClaimReduction, - ); - let (_, rd_inc_claim) = self.opening_accumulator.get_committed_polynomial_opening( - CommittedPolynomial::RdInc, - SumcheckId::IncClaimReduction, - ); + let (ram_inc_point, ram_inc_claim) = + self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::RamInc, + SumcheckId::IncClaimReduction, + ); + let (rd_inc_point, rd_inc_claim) = + self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::RdInc, + SumcheckId::IncClaimReduction, + ); - // Dense polynomials are zero-padded in the Dory matrix, so their evaluation - // includes a factor eq(r_addr, 0) = ∏(1 − r_addr_i). - let lagrange_factor: F = EqPolynomial::zero_selector(r_address_stage7); - polynomial_claims.push((CommittedPolynomial::RamInc, ram_inc_claim * lagrange_factor)); - scaling_factors.push(lagrange_factor); - polynomial_claims.push((CommittedPolynomial::RdInc, rd_inc_claim * lagrange_factor)); - scaling_factors.push(lagrange_factor); + let ram_inc_lagrange = compute_lagrange_factor::(&opening_point.r, &ram_inc_point.r); + let rd_inc_lagrange = compute_lagrange_factor::(&opening_point.r, &rd_inc_point.r); + polynomial_claims.push(( + CommittedPolynomial::RamInc, + ram_inc_claim * ram_inc_lagrange, + )); + scaling_factors.push(ram_inc_lagrange); + polynomial_claims.push((CommittedPolynomial::RdInc, rd_inc_claim * rd_inc_lagrange)); + scaling_factors.push(rd_inc_lagrange); // Sparse polynomials: all RA polys (from HammingWeightClaimReduction) // These are at (r_address_stage7, r_cycle_stage6) for i in 0..self.one_hot_params.instruction_d { - let (_, claim) = self.opening_accumulator.get_committed_polynomial_opening( + let (ra_point, claim) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::InstructionRa(i), SumcheckId::HammingWeightClaimReduction, ); - polynomial_claims.push((CommittedPolynomial::InstructionRa(i), claim)); - scaling_factors.push(F::one()); + let lagrange = compute_lagrange_factor::(&opening_point.r, &ra_point.r); + polynomial_claims.push((CommittedPolynomial::InstructionRa(i), claim * lagrange)); + scaling_factors.push(lagrange); } for i in 0..self.one_hot_params.bytecode_d { - let (_, claim) = self.opening_accumulator.get_committed_polynomial_opening( + let (ra_point, claim) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::BytecodeRa(i), SumcheckId::HammingWeightClaimReduction, ); - polynomial_claims.push((CommittedPolynomial::BytecodeRa(i), claim)); - scaling_factors.push(F::one()); + let lagrange = compute_lagrange_factor::(&opening_point.r, &ra_point.r); + polynomial_claims.push((CommittedPolynomial::BytecodeRa(i), claim * lagrange)); + scaling_factors.push(lagrange); } for i in 0..self.one_hot_params.ram_d { - let (_, claim) = self.opening_accumulator.get_committed_polynomial_opening( + let (ra_point, claim) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::RamRa(i), SumcheckId::HammingWeightClaimReduction, ); - polynomial_claims.push((CommittedPolynomial::RamRa(i), claim)); - scaling_factors.push(F::one()); + let lagrange = compute_lagrange_factor::(&opening_point.r, &ra_point.r); + polynomial_claims.push((CommittedPolynomial::RamRa(i), claim * lagrange)); + scaling_factors.push(lagrange); } // Advice polynomials: TrustedAdvice and UntrustedAdvice (from AdviceClaimReduction in Stage 6) @@ -2066,8 +2048,7 @@ impl< .opening_accumulator .get_advice_opening(AdviceKind::Trusted, SumcheckId::AdviceClaimReduction) { - let lagrange_factor = - compute_advice_lagrange_factor::(&opening_point.r, &advice_point.r); + let lagrange_factor = compute_lagrange_factor::(&opening_point.r, &advice_point.r); polynomial_claims.push(( CommittedPolynomial::TrustedAdvice, advice_claim * lagrange_factor, @@ -2083,8 +2064,7 @@ impl< .opening_accumulator .get_advice_opening(AdviceKind::Untrusted, SumcheckId::AdviceClaimReduction) { - let lagrange_factor = - compute_advice_lagrange_factor::(&opening_point.r, &advice_point.r); + let lagrange_factor = compute_lagrange_factor::(&opening_point.r, &advice_point.r); polynomial_claims.push(( CommittedPolynomial::UntrustedAdvice, advice_claim * lagrange_factor, diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index acf7d14d5f..2feb3fe68f 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -8,7 +8,7 @@ use crate::curve::JoltCurve; use crate::poly::commitment::commitment_scheme::{CommitmentScheme, ZkEvalCommitment}; #[cfg(feature = "zk")] use crate::poly::commitment::dory::bind_opening_inputs_zk; -use crate::poly::commitment::dory::{bind_opening_inputs, DoryContext, DoryGlobals}; +use crate::poly::commitment::dory::{bind_opening_inputs, DoryGlobals}; use crate::poly::commitment::pedersen::PedersenGenerators; #[cfg(feature = "zk")] use crate::poly::lagrange_poly::LagrangeHelper; @@ -26,7 +26,6 @@ use crate::subprotocols::sumcheck_verifier::SumcheckInstanceParams; #[cfg(feature = "zk")] use crate::subprotocols::univariate_skip::UniSkipFirstRoundProofVariant; use crate::zkvm::bytecode::BytecodePreprocessing; -use crate::zkvm::claim_reductions::advice::ReductionPhase; use crate::zkvm::claim_reductions::RegistersClaimReductionSumcheckVerifier; use crate::zkvm::config::OneHotParams; #[cfg(feature = "prover")] @@ -47,7 +46,7 @@ use crate::zkvm::{ claim_reductions::{ AdviceClaimReductionVerifier, AdviceKind, HammingWeightClaimReductionVerifier, IncClaimReductionSumcheckVerifier, InstructionLookupsClaimReductionSumcheckVerifier, - RamRaClaimReductionSumcheckVerifier, + PrecommittedClaimReduction, RamRaClaimReductionSumcheckVerifier, }, fiat_shamir_preamble, instruction_lookups::{ @@ -76,12 +75,9 @@ use crate::zkvm::{ }; use crate::{ field::JoltField, - poly::{ - eq_poly::EqPolynomial, - opening_proof::{ - compute_advice_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningId, - SumcheckId, VerifierOpeningAccumulator, - }, + poly::opening_proof::{ + compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningId, OpeningPoint, + SumcheckId, VerifierOpeningAccumulator, BIG_ENDIAN, }, pprof_scope, subprotocols::{ @@ -240,6 +236,85 @@ impl< ProofTranscript: Transcript, > JoltVerifier<'a, F, C, PCS, ProofTranscript> { + #[inline] + fn main_total_vars(&self) -> usize { + let trace_log_t = self.proof.trace_length.log_2(); + let log_k_chunk = self.one_hot_params.log_k_chunk; + let mut max_total_vars = trace_log_t + log_k_chunk; + for total_vars in self.precommitted_candidate_total_vars() { + max_total_vars = max_total_vars.max(total_vars); + } + max_total_vars + } + + #[inline] + fn precommitted_candidate_total_vars(&self) -> Vec { + let mut candidates = Vec::new(); + if self.trusted_advice_commitment.is_some() { + let (sigma, nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( + self.program_io.memory_layout.max_trusted_advice_size as usize, + ); + candidates.push(sigma + nu); + } + + if self.proof.untrusted_advice_commitment.is_some() { + let (sigma, nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( + self.program_io.memory_layout.max_untrusted_advice_size as usize, + ); + candidates.push(sigma + nu); + } + candidates + } + + fn stage8_opening_point(&self) -> Result, ProofVerifyError> { + let native_main_vars = self.proof.trace_length.log_2() + self.one_hot_params.log_k_chunk; + let mut opening_candidates: Vec<(&str, OpeningPoint)> = Vec::new(); + if let Some((point, _)) = self + .opening_accumulator + .get_advice_opening(AdviceKind::Trusted, SumcheckId::AdviceClaimReduction) + { + opening_candidates.push(("trusted_advice", point)); + } + if let Some((point, _)) = self + .opening_accumulator + .get_advice_opening(AdviceKind::Untrusted, SumcheckId::AdviceClaimReduction) + { + opening_candidates.push(("untrusted_advice", point)); + } + + let max_len = opening_candidates + .iter() + .map(|(_, p)| p.r.len()) + .max() + .unwrap_or(0); + if max_len > native_main_vars { + let dominant = opening_candidates + .iter() + .find(|(_, p)| p.r.len() == max_len) + .expect("at least one dominant precommitted candidate expected"); + for (name, point) in opening_candidates + .iter() + .filter(|(_, p)| p.r.len() == max_len) + { + if point.r != dominant.1.r { + return Err(ProofVerifyError::DoryError(format!( + "incompatible dominant precommitted anchors: {} and {} have equal dimensionality {} but different opening points", + dominant.0, name, max_len + ))); + } + } + Ok(OpeningPoint::::new(dominant.1.r.clone())) + } else { + Ok(self + .opening_accumulator + .get_committed_polynomial_opening( + CommittedPolynomial::InstructionRa(0), + SumcheckId::HammingWeightClaimReduction, + ) + .0) + } + } + pub fn new( preprocessing: &'a JoltVerifierPreprocessing, proof: JoltProof, @@ -915,6 +990,12 @@ impl< #[cfg_attr(not(feature = "zk"), allow(unused_variables))] fn verify_stage6(&mut self) -> Result, ProofVerifyError> { + let _ = DoryGlobals::initialize_main_with_log_embedding( + self.one_hot_params.k_chunk, + self.proof.trace_length, + self.main_total_vars(), + Some(self.proof.dory_layout), + ); let (bytecode_read_raf_params, booleanity_params) = self.verify_stage6a()?; self.verify_stage6b(bytecode_read_raf_params, booleanity_params) } @@ -992,20 +1073,30 @@ impl< &mut self.transcript, ); + let main_total_vars = self.proof.trace_length.log_2() + self.one_hot_params.log_k_chunk; + let precommitted_candidates = self.precommitted_candidate_total_vars(); + let precommitted_scheduling_reference = + PrecommittedClaimReduction::::scheduling_reference( + main_total_vars, + &precommitted_candidates, + ); + // Advice claim reduction (Phase 1 in Stage 6b): trusted and untrusted are separate instances. if self.trusted_advice_commitment.is_some() { self.advice_reduction_verifier_trusted = Some(AdviceClaimReductionVerifier::new( AdviceKind::Trusted, - &self.program_io.memory_layout, + self.program_io.memory_layout.max_trusted_advice_size as usize, self.proof.trace_length, + precommitted_scheduling_reference, &self.opening_accumulator, )); } if self.proof.untrusted_advice_commitment.is_some() { self.advice_reduction_verifier_untrusted = Some(AdviceClaimReductionVerifier::new( AdviceKind::Untrusted, - &self.program_io.memory_layout, + self.program_io.memory_layout.max_untrusted_advice_size as usize, self.proof.trace_length, + precommitted_scheduling_reference, &self.opening_accumulator, )); } @@ -1345,7 +1436,7 @@ impl< let mut params = advice_reduction_verifier_trusted.params.borrow_mut(); if params.num_address_phase_rounds() > 0 { // Transition phase - params.phase = ReductionPhase::AddressVariables; + params.transition_to_address_phase(); instances.push(advice_reduction_verifier_trusted); } } @@ -1355,7 +1446,7 @@ impl< let mut params = advice_reduction_verifier_untrusted.params.borrow_mut(); if params.num_address_phase_rounds() > 0 { // Transition phase - params.phase = ReductionPhase::AddressVariables; + params.transition_to_address_phase(); instances.push(advice_reduction_verifier_untrusted); } } @@ -1408,70 +1499,60 @@ impl< /// Stage 8: Dory batch opening verification. fn verify_stage8(&mut self) -> Result, ProofVerifyError> { - // Initialize DoryGlobals with the layout from the proof - // This ensures the verifier uses the same layout as the prover - let _guard = DoryGlobals::initialize_context( - 1 << self.one_hot_params.log_k_chunk, - self.proof.trace_length.next_power_of_two(), - DoryContext::Main, - Some(self.proof.dory_layout), - ); - - // Get the unified opening point from HammingWeightClaimReduction - // This contains (r_address_stage7 || r_cycle_stage6) in big-endian - let (opening_point, _) = self.opening_accumulator.get_committed_polynomial_opening( - CommittedPolynomial::InstructionRa(0), - SumcheckId::HammingWeightClaimReduction, - ); - let log_k_chunk = self.one_hot_params.log_k_chunk; - let r_address_stage7 = &opening_point.r[..log_k_chunk]; + let opening_point = self.stage8_opening_point()?; // 1. Collect all (polynomial, claim) pairs let mut polynomial_claims = Vec::new(); let mut scaling_factors = Vec::new(); // Dense polynomials: RamInc and RdInc (from IncClaimReduction in Stage 6) - let (_, ram_inc_claim) = self.opening_accumulator.get_committed_polynomial_opening( + let (ram_inc_point, ram_inc_claim) = + self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::RamInc, + SumcheckId::IncClaimReduction, + ); + let (rd_inc_point, rd_inc_claim) = + self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::RdInc, + SumcheckId::IncClaimReduction, + ); + let ram_inc_lagrange = compute_lagrange_factor::(&opening_point.r, &ram_inc_point.r); + let rd_inc_lagrange = compute_lagrange_factor::(&opening_point.r, &rd_inc_point.r); + polynomial_claims.push(( CommittedPolynomial::RamInc, - SumcheckId::IncClaimReduction, - ); - let (_, rd_inc_claim) = self.opening_accumulator.get_committed_polynomial_opening( - CommittedPolynomial::RdInc, - SumcheckId::IncClaimReduction, - ); - - // Dense polynomials are zero-padded in the Dory matrix, so their evaluation - // includes a factor eq(r_addr, 0) = ∏(1 − r_addr_i). - let lagrange_factor: F = EqPolynomial::zero_selector(r_address_stage7); - polynomial_claims.push((CommittedPolynomial::RamInc, ram_inc_claim * lagrange_factor)); - scaling_factors.push(lagrange_factor); - polynomial_claims.push((CommittedPolynomial::RdInc, rd_inc_claim * lagrange_factor)); - scaling_factors.push(lagrange_factor); + ram_inc_claim * ram_inc_lagrange, + )); + scaling_factors.push(ram_inc_lagrange); + polynomial_claims.push((CommittedPolynomial::RdInc, rd_inc_claim * rd_inc_lagrange)); + scaling_factors.push(rd_inc_lagrange); // Sparse polynomials: all RA polys (from HammingWeightClaimReduction) for i in 0..self.one_hot_params.instruction_d { - let (_, claim) = self.opening_accumulator.get_committed_polynomial_opening( + let (ra_point, claim) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::InstructionRa(i), SumcheckId::HammingWeightClaimReduction, ); - polynomial_claims.push((CommittedPolynomial::InstructionRa(i), claim)); - scaling_factors.push(F::one()); + let lagrange = compute_lagrange_factor::(&opening_point.r, &ra_point.r); + polynomial_claims.push((CommittedPolynomial::InstructionRa(i), claim * lagrange)); + scaling_factors.push(lagrange); } for i in 0..self.one_hot_params.bytecode_d { - let (_, claim) = self.opening_accumulator.get_committed_polynomial_opening( + let (ra_point, claim) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::BytecodeRa(i), SumcheckId::HammingWeightClaimReduction, ); - polynomial_claims.push((CommittedPolynomial::BytecodeRa(i), claim)); - scaling_factors.push(F::one()); + let lagrange = compute_lagrange_factor::(&opening_point.r, &ra_point.r); + polynomial_claims.push((CommittedPolynomial::BytecodeRa(i), claim * lagrange)); + scaling_factors.push(lagrange); } for i in 0..self.one_hot_params.ram_d { - let (_, claim) = self.opening_accumulator.get_committed_polynomial_opening( + let (ra_point, claim) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::RamRa(i), SumcheckId::HammingWeightClaimReduction, ); - polynomial_claims.push((CommittedPolynomial::RamRa(i), claim)); - scaling_factors.push(F::one()); + let lagrange = compute_lagrange_factor::(&opening_point.r, &ra_point.r); + polynomial_claims.push((CommittedPolynomial::RamRa(i), claim * lagrange)); + scaling_factors.push(lagrange); } // Advice polynomials: TrustedAdvice and UntrustedAdvice (from AdviceClaimReduction in Stage 6) @@ -1484,8 +1565,7 @@ impl< .opening_accumulator .get_advice_opening(AdviceKind::Trusted, SumcheckId::AdviceClaimReduction) { - let lagrange_factor = - compute_advice_lagrange_factor::(&opening_point.r, &advice_point.r); + let lagrange_factor = compute_lagrange_factor::(&opening_point.r, &advice_point.r); polynomial_claims.push(( CommittedPolynomial::TrustedAdvice, advice_claim * lagrange_factor, @@ -1498,8 +1578,7 @@ impl< .opening_accumulator .get_advice_opening(AdviceKind::Untrusted, SumcheckId::AdviceClaimReduction) { - let lagrange_factor = - compute_advice_lagrange_factor::(&opening_point.r, &advice_point.r); + let lagrange_factor = compute_lagrange_factor::(&opening_point.r, &advice_point.r); polynomial_claims.push(( CommittedPolynomial::UntrustedAdvice, advice_claim * lagrange_factor, diff --git a/jolt-sdk/src/host_utils.rs b/jolt-sdk/src/host_utils.rs index 8f1f474d8b..5da343ce19 100644 --- a/jolt-sdk/src/host_utils.rs +++ b/jolt-sdk/src/host_utils.rs @@ -20,7 +20,7 @@ pub use jolt_core::AdviceTape; // Re-exports needed by the provable macro pub use jolt_core::poly::commitment::commitment_scheme::CommitmentScheme; -pub use jolt_core::poly::commitment::dory::{DoryContext, DoryGlobals}; +pub use jolt_core::poly::commitment::dory::{DoryContext, DoryGlobals, DoryLayout}; pub use jolt_core::poly::multilinear_polynomial::MultilinearPolynomial; pub use jolt_core::zkvm::ram::populate_memory_states; pub use jolt_core::zkvm::verifier::BlindfoldSetup; From 2ca8a60b6674a20a11ed0e45beec266b263a9ad6 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Mon, 16 Mar 2026 17:01:28 -0700 Subject: [PATCH 03/20] feat(zkvm): integrate committed preprocessing across prover and verifier Add committed bytecode/program-image claim-reduction and opening-flow updates so verifier-side checks stay consistent with staged precommitted claims. Extend SDK macro helpers for committed prover/verifier preprocessing and align guest/shared verifier preprocessing construction. Made-with: Cursor Co-authored-by: Quang Dao --- jolt-core/benches/e2e_profiling.rs | 26 +- jolt-core/src/guest/prover.rs | 16 +- jolt-core/src/guest/verifier.rs | 23 +- .../poly/commitment/dory/commitment_scheme.rs | 30 +- jolt-core/src/poly/commitment/dory/tests.rs | 18 +- jolt-core/src/poly/opening_proof.rs | 18 +- jolt-core/src/poly/rlc_polynomial.rs | 284 ++-- jolt-core/src/subprotocols/sumcheck.rs | 111 +- jolt-core/src/utils/errors.rs | 4 + jolt-core/src/zkvm/bytecode/mod.rs | 87 +- .../src/zkvm/bytecode/read_raf_checking.rs | 243 +++- .../src/zkvm/claim_reductions/bytecode.rs | 697 +++++++++ .../zkvm/claim_reductions/hamming_weight.rs | 4 +- jolt-core/src/zkvm/claim_reductions/mod.rs | 9 + .../zkvm/claim_reductions/program_image.rs | 450 ++++++ jolt-core/src/zkvm/config.rs | 60 +- jolt-core/src/zkvm/mod.rs | 57 +- jolt-core/src/zkvm/proof_serialization.rs | 47 +- jolt-core/src/zkvm/prover.rs | 1294 ++++++++++++++--- jolt-core/src/zkvm/ram/mod.rs | 102 +- jolt-core/src/zkvm/ram/val_check.rs | 68 +- jolt-core/src/zkvm/verifier.rs | 526 ++++++- jolt-core/src/zkvm/witness.rs | 47 +- jolt-sdk/macros/src/lib.rs | 116 +- jolt-sdk/src/host_utils.rs | 7 +- 25 files changed, 3833 insertions(+), 511 deletions(-) create mode 100644 jolt-core/src/zkvm/claim_reductions/bytecode.rs create mode 100644 jolt-core/src/zkvm/claim_reductions/program_image.rs diff --git a/jolt-core/benches/e2e_profiling.rs b/jolt-core/benches/e2e_profiling.rs index 91acb47b7f..805e997634 100644 --- a/jolt-core/benches/e2e_profiling.rs +++ b/jolt-core/benches/e2e_profiling.rs @@ -1,10 +1,12 @@ use ark_serialize::CanonicalSerialize; use jolt_core::host; +use jolt_core::zkvm::program::ProgramPreprocessing; use jolt_core::zkvm::prover::JoltProverPreprocessing; use jolt_core::zkvm::verifier::{JoltSharedPreprocessing, JoltVerifierPreprocessing}; use jolt_core::zkvm::{RV64IMACProver, RV64IMACVerifier}; use std::fs; use std::io::Write; +use std::sync::Arc; use std::time::Instant; // Empirically measured cycles per operation for RV64IMAC @@ -201,20 +203,22 @@ fn prove_example( ) -> Vec<(tracing::Span, Box)> { let mut tasks = Vec::new(); let mut program = host::Program::new(example_name); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let (_lazy_trace, trace, _, program_io) = program.trace(&serialized_input, &[], &[]); let padded_trace_len = (trace.len() + 1).next_power_of_two(); drop(trace); let task = move || { - let shared_preprocessing = JoltSharedPreprocessing::new( + let program_data = Arc::new(ProgramPreprocessing::preprocess( bytecode, - program_io.memory_layout.clone(), init_memory_state, + )); + let shared_preprocessing = JoltSharedPreprocessing::new( + program_data.meta(), + program_io.memory_layout.clone(), padded_trace_len, - e_entry, ); - let preprocessing = JoltProverPreprocessing::new(shared_preprocessing); + let preprocessing = JoltProverPreprocessing::new(shared_preprocessing, program_data); let elf_contents_opt = program.get_elf_contents(); let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); @@ -254,7 +258,7 @@ fn prove_example_with_trace( _scale: usize, ) -> (std::time::Duration, usize, usize, usize) { let mut program = host::Program::new(example_name); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let (_, trace, _, program_io) = program.trace(&serialized_input, &[], &[]); assert!( @@ -262,14 +266,16 @@ fn prove_example_with_trace( "Trace is longer than expected" ); + let program_data = Arc::new(ProgramPreprocessing::preprocess( + bytecode, + init_memory_state, + )); let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), + program_data.meta(), program_io.memory_layout.clone(), - init_memory_state, trace.len().next_power_of_two(), - e_entry, ); - let preprocessing = JoltProverPreprocessing::new(shared_preprocessing); + let preprocessing = JoltProverPreprocessing::new(shared_preprocessing, program_data); let elf_contents_opt = program.get_elf_contents(); let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); diff --git a/jolt-core/src/guest/prover.rs b/jolt-core/src/guest/prover.rs index 83e133ac83..5cb575d7bb 100644 --- a/jolt-core/src/guest/prover.rs +++ b/jolt-core/src/guest/prover.rs @@ -5,10 +5,12 @@ use crate::poly::commitment::commitment_scheme::CommitmentScheme; use crate::poly::commitment::commitment_scheme::{StreamingCommitmentScheme, ZkEvalCommitment}; use crate::poly::commitment::dory::DoryCommitmentScheme; use crate::transcripts::Transcript; +use crate::zkvm::program::ProgramPreprocessing; use crate::zkvm::proof_serialization::JoltProof; use crate::zkvm::prover::JoltProverPreprocessing; use crate::zkvm::ProverDebugInfo; use common::jolt_device::MemoryLayout; +use std::sync::Arc; use tracer::JoltDevice; #[allow(clippy::type_complexity)] @@ -19,19 +21,15 @@ pub fn preprocess( ) -> JoltProverPreprocessing { use crate::zkvm::verifier::JoltSharedPreprocessing; - let (bytecode, memory_init, program_size, e_entry) = guest.decode(); + let (bytecode, memory_init, program_size, _e_entry) = guest.decode(); let mut memory_config = guest.memory_config; memory_config.program_size = Some(program_size); let memory_layout = MemoryLayout::new(&memory_config); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode, - memory_layout, - memory_init, - max_trace_length, - e_entry, - ); - JoltProverPreprocessing::new(shared_preprocessing) + let program = Arc::new(ProgramPreprocessing::preprocess(bytecode, memory_init)); + let shared_preprocessing = + JoltSharedPreprocessing::new(program.meta(), memory_layout, max_trace_length); + JoltProverPreprocessing::new(shared_preprocessing, program) } #[allow(clippy::type_complexity, clippy::too_many_arguments)] diff --git a/jolt-core/src/guest/verifier.rs b/jolt-core/src/guest/verifier.rs index 64790bc34d..e92aa1544f 100644 --- a/jolt-core/src/guest/verifier.rs +++ b/jolt-core/src/guest/verifier.rs @@ -8,12 +8,14 @@ use crate::zkvm::verifier::BlindfoldSetup; use crate::guest::program::Program; use crate::poly::commitment::dory::DoryCommitmentScheme; use crate::transcripts::Transcript; +use crate::zkvm::program::ProgramPreprocessing; use crate::zkvm::proof_serialization::JoltProof; use crate::zkvm::verifier::JoltSharedPreprocessing; use crate::zkvm::verifier::JoltVerifier; use crate::zkvm::verifier::JoltVerifierPreprocessing; use common::jolt_device::MemoryConfig; use common::jolt_device::MemoryLayout; +use std::sync::Arc; pub fn preprocess( guest: &Program, @@ -21,23 +23,22 @@ pub fn preprocess( verifier_setup: ::VerifierSetup, blindfold_setup: Option>, ) -> JoltVerifierPreprocessing { - let shared = preprocess_shared(guest, max_trace_length); - JoltVerifierPreprocessing::new(shared, verifier_setup, blindfold_setup) + let (shared, program) = preprocess_shared(guest, max_trace_length); + JoltVerifierPreprocessing::new_full(shared, verifier_setup, program, blindfold_setup) } -fn preprocess_shared(guest: &Program, max_trace_length: usize) -> JoltSharedPreprocessing { - let (bytecode, memory_init, program_size, e_entry) = guest.decode(); +fn preprocess_shared( + guest: &Program, + max_trace_length: usize, +) -> (JoltSharedPreprocessing, Arc) { + let (bytecode, memory_init, program_size, _e_entry) = guest.decode(); let mut memory_config = guest.memory_config; memory_config.program_size = Some(program_size); let memory_layout = MemoryLayout::new(&memory_config); - JoltSharedPreprocessing::new( - bytecode, - memory_layout, - memory_init, - max_trace_length, - e_entry, - ) + let program = Arc::new(ProgramPreprocessing::preprocess(bytecode, memory_init)); + let shared = JoltSharedPreprocessing::new(program.meta(), memory_layout, max_trace_length); + (shared, program) } pub fn verify< diff --git a/jolt-core/src/poly/commitment/dory/commitment_scheme.rs b/jolt-core/src/poly/commitment/dory/commitment_scheme.rs index ccfd96957f..f7a00e8ee4 100644 --- a/jolt-core/src/poly/commitment/dory/commitment_scheme.rs +++ b/jolt-core/src/poly/commitment/dory/commitment_scheme.rs @@ -1,6 +1,6 @@ //! Dory polynomial commitment scheme implementation -use super::dory_globals::{DoryGlobals, DoryLayout}; +use super::dory_globals::DoryGlobals; use super::jolt_dory_routines::{JoltG1Routines, JoltG2Routines}; use super::wrappers::{ ark_to_jolt, jolt_to_ark, ArkDoryProof, ArkFr, ArkG1, ArkGT, ArkworksProverSetup, @@ -163,8 +163,7 @@ impl CommitmentScheme for DoryCommitmentScheme { let sigma = num_cols.log_2(); let nu = num_rows.log_2(); - let reordered_point = reorder_opening_point_for_layout::(opening_point); - let ark_point: Vec = reordered_point + let ark_point: Vec = opening_point .iter() .rev() .map(|p| { @@ -206,10 +205,8 @@ impl CommitmentScheme for DoryCommitmentScheme { ) -> Result<(), ProofVerifyError> { let _span = trace_span!("DoryCommitmentScheme::verify").entered(); - let reordered_point = reorder_opening_point_for_layout::(opening_point); - // Dory uses the opposite endian-ness as Jolt - let ark_point: Vec = reordered_point + let ark_point: Vec = opening_point .iter() .rev() .map(|p| { @@ -440,24 +437,3 @@ where Some((g1s, h1)) } } - -/// Reorders opening_point for AddressMajor layout. -/// -/// For AddressMajor layout, reorders opening_point from [r_address, r_cycle] to [r_cycle, r_address]. -/// This ensures that after Dory's reversal and splitting: -/// - Column (right) vector gets address variables (matching AddressMajor column indexing) -/// - Row (left) vector gets cycle variables (matching AddressMajor row indexing) -/// -/// For CycleMajor layout, returns the point unchanged. -fn reorder_opening_point_for_layout( - opening_point: &[F::Challenge], -) -> Vec { - if DoryGlobals::get_layout() == DoryLayout::AddressMajor { - let log_T = DoryGlobals::get_T().log_2(); - let log_K = opening_point.len().saturating_sub(log_T); - let (r_address, r_cycle) = opening_point.split_at(log_K); - [r_cycle, r_address].concat() - } else { - opening_point.to_vec() - } -} diff --git a/jolt-core/src/poly/commitment/dory/tests.rs b/jolt-core/src/poly/commitment/dory/tests.rs index 0237a55490..06100570fe 100644 --- a/jolt-core/src/poly/commitment/dory/tests.rs +++ b/jolt-core/src/poly/commitment/dory/tests.rs @@ -8,6 +8,7 @@ mod tests { use crate::poly::dense_mlpoly::DensePolynomial; use crate::poly::multilinear_polynomial::{MultilinearPolynomial, PolynomialEvaluation}; use crate::transcripts::{Blake2bTranscript, Transcript}; + use crate::utils::math::Math; use ark_ff::biginteger::S128; use ark_std::rand::{thread_rng, Rng}; use ark_std::{UniformRand, Zero}; @@ -879,19 +880,26 @@ mod tests { let num_vars = one_hot_poly.get_num_vars(); let poly = MultilinearPolynomial::OneHot(one_hot_poly); - let opening_point: Vec<::Challenge> = (0..num_vars) + // AddressMajor Dory opening points are consumed as [cycle vars || address vars], + // while OneHotPolynomial::evaluate expects [address vars || cycle vars]. + let log_t = T.log_2(); + let log_k = num_vars - log_t; + let r_cycle: Vec<::Challenge> = (0..log_t) + .map(|_| ::Challenge::random(&mut rng)) + .collect(); + let r_address: Vec<::Challenge> = (0..log_k) .map(|_| ::Challenge::random(&mut rng)) .collect(); + let opening_point = [r_cycle.clone(), r_address.clone()].concat(); + let eval_point = [r_address, r_cycle].concat(); let prover_setup = DoryCommitmentScheme::setup_prover(num_vars); let verifier_setup = DoryCommitmentScheme::setup_verifier(&prover_setup); let (commitment, row_commitments) = DoryCommitmentScheme::commit(&poly, &prover_setup); - let evaluation = as PolynomialEvaluation>::evaluate( - &poly, - &opening_point, - ); + let evaluation = + as PolynomialEvaluation>::evaluate(&poly, &eval_point); let mut prove_transcript = Blake2bTranscript::new(b"dory_test"); bind_opening_inputs::(&mut prove_transcript, &opening_point, &evaluation); diff --git a/jolt-core/src/poly/opening_proof.rs b/jolt-core/src/poly/opening_proof.rs index 7c5f0a50bc..c2d8a0e563 100644 --- a/jolt-core/src/poly/opening_proof.rs +++ b/jolt-core/src/poly/opening_proof.rs @@ -157,6 +157,10 @@ pub enum SumcheckId { Booleanity, AdviceClaimReductionCyclePhase, AdviceClaimReduction, + BytecodeClaimReductionCyclePhase, + BytecodeClaimReduction, + ProgramImageClaimReductionCyclePhase, + ProgramImageClaimReduction, IncClaimReduction, HammingWeightClaimReduction, } @@ -285,7 +289,7 @@ pub struct DoryOpeningState { impl DoryOpeningState { /// Build streaming RLC polynomial from this state. /// Streams directly from trace - no witness regeneration needed. - /// Advice polynomials are passed separately (not streamed from trace). + /// Precommitted polynomials are passed separately (not streamed from trace). #[tracing::instrument(skip_all)] pub fn build_streaming_rlc>( &self, @@ -293,7 +297,7 @@ impl DoryOpeningState { trace_source: TraceSource, rlc_streaming_data: Arc, mut opening_hints: HashMap, - advice_polys: HashMap>, + precommitted_polys: HashMap>, ) -> (MultilinearPolynomial, PCS::OpeningProofHint) { // Accumulate gamma coefficients per polynomial let mut rlc_map = BTreeMap::new(); @@ -310,7 +314,7 @@ impl DoryOpeningState { trace_source, poly_ids.clone(), &coeffs, - advice_polys, + precommitted_polys, )); let hints: Vec = rlc_map @@ -579,6 +583,10 @@ where pub fn take_pending_claim_ids(&mut self) -> Vec { std::mem::take(&mut self.pending_claim_ids) } + + pub fn pending_claim_ids_debug(&self) -> &[OpeningId] { + &self.pending_claim_ids + } } impl Default for VerifierOpeningAccumulator @@ -809,6 +817,10 @@ where pub fn take_pending_claim_ids(&mut self) -> Vec { std::mem::take(&mut self.pending_claim_ids) } + + pub fn pending_claim_ids_debug(&self) -> &[OpeningId] { + &self.pending_claim_ids + } } /// Computes the Lagrange factor for embedding a smaller polynomial into the top-left block of diff --git a/jolt-core/src/poly/rlc_polynomial.rs b/jolt-core/src/poly/rlc_polynomial.rs index 3e02ddc198..e681e0c199 100644 --- a/jolt-core/src/poly/rlc_polynomial.rs +++ b/jolt-core/src/poly/rlc_polynomial.rs @@ -56,9 +56,9 @@ impl TraceSource { pub struct StreamingRLCContext { pub dense_polys: Vec<(CommittedPolynomial, F)>, pub onehot_polys: Vec<(CommittedPolynomial, F)>, - /// Advice polynomials with their RLC coefficients. + /// Precommitted polynomials with their RLC coefficients. /// These are NOT streamed from trace - they're passed in directly. - pub advice_polys: Vec<(F, MultilinearPolynomial)>, + pub precommitted_polys: Vec<(F, MultilinearPolynomial)>, pub trace_source: TraceSource, pub preprocessing: Arc, pub one_hot_params: OneHotParams, @@ -165,7 +165,7 @@ impl RLCPolynomial { /// * `trace_source` - Either materialized trace (default) or lazy trace (experimental) /// * `poly_ids` - List of polynomial identifiers /// * `coefficients` - RLC coefficients for each polynomial - /// * `advice_poly_map` - Map of advice polynomial IDs to their actual polynomials + /// * `precommitted_poly_map` - Map of precommitted polynomial IDs to their actual polynomials #[tracing::instrument(skip_all)] pub fn new_streaming( one_hot_params: OneHotParams, @@ -173,13 +173,13 @@ impl RLCPolynomial { trace_source: TraceSource, poly_ids: Vec, coefficients: &[F], - mut advice_poly_map: HashMap>, + mut precommitted_poly_map: HashMap>, ) -> Self { debug_assert_eq!(poly_ids.len(), coefficients.len()); let mut dense_polys = Vec::new(); let mut onehot_polys = Vec::new(); - let mut advice_polys = Vec::new(); + let mut precommitted_polys = Vec::new(); for (poly_id, coeff) in poly_ids.iter().zip(coefficients.iter()) { match poly_id { @@ -191,10 +191,16 @@ impl RLCPolynomial { | CommittedPolynomial::RamRa(_) => { onehot_polys.push((*poly_id, *coeff)); } - CommittedPolynomial::TrustedAdvice | CommittedPolynomial::UntrustedAdvice => { - // Advice polynomials are passed in directly (not streamed from trace) - if advice_poly_map.contains_key(poly_id) { - advice_polys.push((*coeff, advice_poly_map.remove(poly_id).unwrap())); + CommittedPolynomial::TrustedAdvice + | CommittedPolynomial::UntrustedAdvice + | CommittedPolynomial::BytecodeChunk(_) + | CommittedPolynomial::ProgramImageInit => { + // Precommitted polynomials are passed in directly (not streamed from trace). + if precommitted_poly_map.contains_key(poly_id) { + precommitted_polys.push(( + *coeff, + precommitted_poly_map.remove(poly_id).unwrap(), + )); } } } @@ -206,7 +212,7 @@ impl RLCPolynomial { streaming_context: Some(Arc::new(StreamingRLCContext { dense_polys, onehot_polys, - advice_polys, + precommitted_polys, trace_source, preprocessing, one_hot_params, @@ -338,51 +344,51 @@ impl RLCPolynomial { result } - /// Adds the advice polynomial contribution to the vector-matrix-vector product result. + /// Adds the precommitted polynomial contribution to the vector-matrix-vector product result. /// - /// In Dory's batch opening, advice polynomials are embedded as the top-left block of the + /// In Dory's batch opening, precommitted polynomials are embedded as the top-left block of the /// main matrix. This function computes their contribution to the VMV product: /// ```text - /// result[col] += left_vec[row] * (coeff * advice[row, col]) + /// result[col] += left_vec[row] * (coeff * precommitted[row, col]) /// ``` - /// for rows and columns within the advice block. + /// for rows and columns within the precommitted block. /// - /// The advice block occupies: - /// - `sigma_a = ceil(advice_vars/2)`, `nu_a = advice_vars - sigma_a` - /// - `advice` occupies rows `[0 .. 2^{nu_a})` and cols `[0 .. 2^{sigma_a})` + /// The precommitted block occupies: + /// - `sigma_a = ceil(poly_vars/2)`, `nu_a = poly_vars - sigma_a` + /// - each precommitted polynomial occupies rows `[0 .. 2^{nu_a})` and cols `[0 .. 2^{sigma_a})` /// /// # Complexity /// It uses O(m + a) space where m is the number of rows - /// and a is the advice size, so even though it is linear it is negl space overall. - fn vmp_advice_contribution( + /// and a is the precommitted size, so even though it is linear it is negl space overall. + fn vmp_precommitted_contribution( result: &mut [F], left_vec: &[F], num_columns: usize, ctx: &StreamingRLCContext, ) { - // For each advice polynomial, compute its contribution to the result - ctx.advice_polys + // For each precommitted polynomial, compute its contribution to the result + ctx.precommitted_polys .iter() - .filter(|(_, advice_poly)| advice_poly.original_len() > 0) - .for_each(|(coeff, advice_poly)| { - let advice_len = advice_poly.original_len(); - let advice_vars = advice_len.log_2(); - let (sigma_a, nu_a) = DoryGlobals::balanced_sigma_nu(advice_vars); - let advice_cols = 1usize << sigma_a; - let advice_rows = 1usize << nu_a; + .filter(|(_, precommitted_poly)| precommitted_poly.original_len() > 0) + .for_each(|(coeff, precommitted_poly)| { + let precommitted_len = precommitted_poly.original_len(); + let precommitted_vars = precommitted_len.log_2(); + let (sigma_a, nu_a) = DoryGlobals::balanced_sigma_nu(precommitted_vars); + let precommitted_cols = 1usize << sigma_a; + let precommitted_rows = 1usize << nu_a; debug_assert!( - advice_cols <= num_columns, - "Advice columns (2^{{sigma_a}}={advice_cols}) must fit in main num_columns={num_columns}; \ + precommitted_cols <= num_columns, + "Precommitted columns (2^{{sigma_a}}={precommitted_cols}) must fit in main num_columns={num_columns}; \ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." ); - // Only the top-left block contributes: rows [0..advice_rows), cols [0..advice_cols) - let effective_rows = advice_rows.min(left_vec.len()); + // Only the top-left block contributes: rows [0..precommitted_rows), cols [0..precommitted_cols) + let effective_rows = precommitted_rows.min(left_vec.len()); // Compute column contributions: for each column, sum contributions from all rows - // Note: advice_len is always advice_cols * advice_rows (advice size must be power of 2) - let column_contributions: Vec = (0..advice_cols) + // Note: precommitted_len is always precommitted_cols * precommitted_rows (size must be power of 2) + let column_contributions: Vec = (0..precommitted_cols) .into_par_iter() .map(|col_idx| { // For this column, sum contributions from all non-zero rows @@ -391,16 +397,16 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." .enumerate() .filter(|(_, &left)| !left.is_zero()) .map(|(row_idx, &left)| { - let coeff_idx = row_idx * advice_cols + col_idx; - let advice_val = advice_poly.get_coeff(coeff_idx); - left * *coeff * advice_val + let coeff_idx = row_idx * precommitted_cols + col_idx; + let precommitted_val = precommitted_poly.get_coeff(coeff_idx); + left * *coeff * precommitted_val }) .sum() }) .collect(); // Add column contributions to result in parallel - result[..advice_cols] + result[..precommitted_cols] .par_iter_mut() .zip(column_contributions.par_iter()) .for_each(|(res, &contrib)| { @@ -459,7 +465,7 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." // Use the regular vector_matrix_product on the materialized polynomial let mut result = materialized.vector_matrix_product(left_vec); - Self::vmp_advice_contribution(&mut result, left_vec, num_columns, ctx); + Self::vmp_precommitted_contribution(&mut result, left_vec, num_columns, ctx); result } @@ -526,8 +532,14 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." let num_rows = T / num_columns; let trace_len = trace.len(); - // Setup: precompute coefficients, row factors, and folded one-hot tables. - let setup = VmvSetup::new(ctx, left_vec, num_rows); + let has_onehot = !ctx.onehot_polys.is_empty(); + let exact_onehot_prefix_mode = + DoryGlobals::get_layout() == DoryLayout::CycleMajor && has_onehot && trace_len < T; + + // When the dominant Stage-8 matrix is larger than the trace-backed prefix, one-hot + // witnesses still live on the exact trace prefix rather than the expanded matrix T. + let onehot_rows_per_k = trace_len.div_ceil(num_columns).min(num_rows); + let setup = VmvSetup::new(ctx, left_vec, num_rows, onehot_rows_per_k); // Divide rows evenly among threads using par_chunks on left_vec // Only use first num_rows elements (left_vec may be longer due to padding) @@ -548,7 +560,6 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." let scaled_rd_inc = row_weight * setup.rd_inc_coeff; let scaled_ram_inc = row_weight * setup.ram_inc_coeff; - let row_factor = setup.row_factors[row_idx]; // Split into valid trace range vs padding range. let valid_end = std::cmp::min(chunk_start + num_columns, trace_len); @@ -560,14 +571,33 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." // Process valid trace elements. for (col_idx, cycle) in row_cycles.iter().enumerate() { - setup.process_cycle( - cycle, - scaled_rd_inc, - scaled_ram_inc, - row_factor, - &mut dense_accs[col_idx], - &mut onehot_accs[col_idx], - ); + if exact_onehot_prefix_mode { + setup.process_cycle_dense( + cycle, + scaled_rd_inc, + scaled_ram_inc, + &mut dense_accs[col_idx], + ); + setup.process_cycle_onehot_prefix_exact( + cycle, + chunk_start + col_idx, + trace_len, + num_columns, + left_vec, + &ctx.onehot_polys, + &mut onehot_accs, + ); + } else { + let row_factor = setup.row_factors[row_idx]; + setup.process_cycle( + cycle, + scaled_rd_inc, + scaled_ram_inc, + row_factor, + &mut dense_accs[col_idx], + &mut onehot_accs[col_idx], + ); + } } } @@ -580,8 +610,8 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." let mut result = VmvSetup::::finalize(dense_accs, onehot_accs, num_columns); - // Advice contribution is small and independent of the trace; add it after the streamed pass. - Self::vmp_advice_contribution(&mut result, left_vec, num_columns, ctx); + // Precommitted contribution is small and independent of the trace; add it after the streamed pass. + Self::vmp_precommitted_contribution(&mut result, left_vec, num_columns, ctx); result } @@ -596,9 +626,14 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." T: usize, ) -> Vec { let num_rows = T / num_columns; + let trace_len = DoryGlobals::main_t(); + let has_onehot = !ctx.onehot_polys.is_empty(); + let exact_onehot_prefix_mode = + DoryGlobals::get_layout() == DoryLayout::CycleMajor && has_onehot && trace_len < T; // Setup: precompute coefficients, row factors, and folded one-hot tables. - let setup = VmvSetup::new(ctx, left_vec, num_rows); + let onehot_rows_per_k = trace_len.div_ceil(num_columns).min(num_rows); + let setup = VmvSetup::new(ctx, left_vec, num_rows, onehot_rows_per_k); let (dense_accs, onehot_accs) = lazy_trace .pad_using(T, |_| Cycle::NoOp) @@ -611,18 +646,37 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." let row_weight = left_vec[row_idx]; let scaled_rd_inc = row_weight * setup.rd_inc_coeff; let scaled_ram_inc = row_weight * setup.ram_inc_coeff; - let row_factor = setup.row_factors[row_idx]; // Process columns within chunk sequentially. for (col_idx, cycle) in chunk.iter().enumerate() { - setup.process_cycle( - cycle, - scaled_rd_inc, - scaled_ram_inc, - row_factor, - &mut dense_accs[col_idx], - &mut onehot_accs[col_idx], - ); + let cycle_idx = row_idx * num_columns + col_idx; + if exact_onehot_prefix_mode && cycle_idx < trace_len { + setup.process_cycle_dense( + cycle, + scaled_rd_inc, + scaled_ram_inc, + &mut dense_accs[col_idx], + ); + setup.process_cycle_onehot_prefix_exact( + cycle, + cycle_idx, + trace_len, + num_columns, + left_vec, + &ctx.onehot_polys, + &mut onehot_accs, + ); + } else { + let row_factor = setup.row_factors[row_idx]; + setup.process_cycle( + cycle, + scaled_rd_inc, + scaled_ram_inc, + row_factor, + &mut dense_accs[col_idx], + &mut onehot_accs[col_idx], + ); + } } (dense_accs, onehot_accs) @@ -634,8 +688,8 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." ); let mut result = VmvSetup::::finalize(dense_accs, onehot_accs, num_columns); - // Advice contribution is small and independent of the trace; add it after the streamed pass. - Self::vmp_advice_contribution(&mut result, left_vec, num_columns, ctx); + // Precommitted contribution is small and independent of the trace; add it after the streamed pass. + Self::vmp_precommitted_contribution(&mut result, left_vec, num_columns, ctx); result } } @@ -669,19 +723,29 @@ struct VmvSetup<'a, F: JoltField> { } impl<'a, F: JoltField> VmvSetup<'a, F> { - fn new(ctx: &'a StreamingRLCContext, left_vec: &[F], num_rows: usize) -> Self { + fn new( + ctx: &'a StreamingRLCContext, + left_vec: &[F], + matrix_rows_per_k: usize, + active_onehot_rows_per_k: usize, + ) -> Self { let one_hot_params = &ctx.one_hot_params; let k_chunk = one_hot_params.k_chunk; debug_assert!( - left_vec.len() >= k_chunk * num_rows, + left_vec.len() >= k_chunk * matrix_rows_per_k, "left_vec too short for one-hot VMV: len={} need_at_least={}", left_vec.len(), - k_chunk * num_rows + k_chunk * matrix_rows_per_k ); // Compute row_factors and eq_k from left vector - let (row_factors, eq_k) = Self::compute_row_factors_and_eq_k(left_vec, num_rows, k_chunk); + let (row_factors, eq_k) = Self::compute_row_factors_and_eq_k( + left_vec, + matrix_rows_per_k, + active_onehot_rows_per_k, + k_chunk, + ); // Extract dense coefficients let mut rd_inc_coeff = F::zero(); @@ -713,16 +777,17 @@ impl<'a, F: JoltField> VmvSetup<'a, F> { #[inline] fn compute_row_factors_and_eq_k( left_vec: &[F], - rows_per_k: usize, + matrix_rows_per_k: usize, + active_onehot_rows_per_k: usize, k_chunk: usize, ) -> (Vec, Vec) { - let mut row_factors: Vec = unsafe_allocate_zero_vec(rows_per_k); + let mut row_factors: Vec = unsafe_allocate_zero_vec(matrix_rows_per_k); let mut eq_k: Vec = unsafe_allocate_zero_vec(k_chunk); for k in 0..k_chunk { - let base = k * rows_per_k; + let base = k * matrix_rows_per_k; let mut sum_k = F::zero(); - for row in 0..rows_per_k { + for row in 0..active_onehot_rows_per_k { let v = left_vec[base + row]; sum_k += v; row_factors[row] += v; @@ -733,6 +798,71 @@ impl<'a, F: JoltField> VmvSetup<'a, F> { (row_factors, eq_k) } + #[inline(always)] + fn process_cycle_dense( + &self, + cycle: &Cycle, + scaled_rd_inc: F, + scaled_ram_inc: F, + dense_acc: &mut MedAccumS, + ) { + let (_, pre_value, post_value) = cycle.rd_write().unwrap_or_default(); + let diff = s64_from_diff_u64s(post_value, pre_value); + dense_acc.fmadd(&scaled_rd_inc, &diff); + + if let tracer::instruction::RAMAccess::Write(write) = cycle.ram_access() { + let diff = s64_from_diff_u64s(write.post_value, write.pre_value); + dense_acc.fmadd(&scaled_ram_inc, &diff); + } + } + + #[allow(clippy::too_many_arguments)] + #[inline(always)] + fn process_cycle_onehot_prefix_exact( + &self, + cycle: &Cycle, + cycle_idx: usize, + trace_len: usize, + num_columns: usize, + left_vec: &[F], + onehot_polys: &[(CommittedPolynomial, F)], + onehot_accs: &mut [F::UnreducedProductAccum], + ) { + let lookup_index = LookupQuery::::to_lookup_index(cycle); + let pc = self.bytecode.get_pc(cycle); + let remapped_address = + remap_address(cycle.ram_access().address() as u64, self.memory_layout); + + for (poly_id, coeff) in onehot_polys.iter() { + if coeff.is_zero() { + continue; + } + + let k = match poly_id { + CommittedPolynomial::InstructionRa(idx) => { + self.one_hot_params.lookup_index_chunk(lookup_index, *idx) as usize + } + CommittedPolynomial::BytecodeRa(idx) => { + self.one_hot_params.bytecode_pc_chunk(pc, *idx) as usize + } + CommittedPolynomial::RamRa(idx) => { + let Some(addr) = remapped_address else { + continue; + }; + self.one_hot_params.ram_address_chunk(addr, *idx) as usize + } + _ => unreachable!("dense polynomial found in onehot_polys"), + }; + + let global_index = k * trace_len + cycle_idx; + let row_index = global_index / num_columns; + let col_index = global_index % num_columns; + if row_index < left_vec.len() && col_index < onehot_accs.len() { + onehot_accs[col_index] += left_vec[row_index].mul_to_product_accum(*coeff); + } + } + } + /// Build per-polynomial folded one-hot tables (non-flattened). fn build_folded_tables( onehot_polys: &[(CommittedPolynomial, F)], @@ -798,15 +928,7 @@ impl<'a, F: JoltField> VmvSetup<'a, F> { dense_acc: &mut MedAccumS, onehot_acc: &mut F::UnreducedProductAccum, ) { - // Dense polynomials: accumulate scaled_coeff * (post - pre) - let (_, pre_value, post_value) = cycle.rd_write().unwrap_or_default(); - let diff = s64_from_diff_u64s(post_value, pre_value); - dense_acc.fmadd(&scaled_rd_inc, &diff); - - if let tracer::instruction::RAMAccess::Write(write) = cycle.ram_access() { - let diff = s64_from_diff_u64s(write.post_value, write.pre_value); - dense_acc.fmadd(&scaled_ram_inc, &diff); - } + self.process_cycle_dense(cycle, scaled_rd_inc, scaled_ram_inc, dense_acc); // One-hot polynomials: accumulate using pre-folded K tables (unreduced) let mut inner_sum = F::UnreducedMulU64::default(); diff --git a/jolt-core/src/subprotocols/sumcheck.rs b/jolt-core/src/subprotocols/sumcheck.rs index 803486ec19..49ac01cf55 100644 --- a/jolt-core/src/subprotocols/sumcheck.rs +++ b/jolt-core/src/subprotocols/sumcheck.rs @@ -30,6 +30,24 @@ pub use crate::subprotocols::univariate_skip::UniSkipFirstRoundProof; /// We do what they describe as "front-loaded" batch sumcheck. pub enum BatchedSumcheck {} impl BatchedSumcheck { + fn debug_final_claims_enabled() -> bool { + std::env::var("JOLT_DEBUG_BATCHED_SUMCHECK_FINAL_CLAIMS") + .map(|v| { + let value = v.trim().to_ascii_lowercase(); + !matches!(value.as_str(), "" | "0" | "false" | "off") + }) + .unwrap_or(false) + } + + fn debug_pending_ids_enabled() -> bool { + std::env::var("JOLT_DEBUG_PENDING_IDS") + .map(|v| { + let value = v.trim().to_ascii_lowercase(); + !matches!(value.as_str(), "" | "0" | "false" | "off") + }) + .unwrap_or(false) + } + /// Returns (proof, challenges, initial_batched_claim) /// For non-ZK mode - returns ClearSumcheckProof with polynomial coefficients visible. pub fn prove( @@ -43,9 +61,15 @@ impl BatchedSumcheck { .max() .unwrap(); - sumcheck_instances.iter().for_each(|sumcheck| { - let input_claim = sumcheck.input_claim(opening_accumulator); - transcript.append_scalar(b"sumcheck_claim", &input_claim); + let input_claims: Vec = sumcheck_instances + .iter() + .map(|sumcheck| sumcheck.input_claim(opening_accumulator)) + .collect(); + if Self::debug_final_claims_enabled() { + tracing::info!("BatchedSumcheck::prove input claims: {:?}", input_claims); + } + input_claims.iter().for_each(|input_claim| { + transcript.append_scalar(b"sumcheck_claim", input_claim); }); let batching_coeffs: Vec = transcript.challenge_vector(sumcheck_instances.len()); @@ -60,9 +84,9 @@ impl BatchedSumcheck { // = A * 2^N * claim_a + B * claim_b let mut individual_claims: Vec = sumcheck_instances .iter() - .map(|sumcheck| { + .zip(input_claims.iter()) + .map(|(sumcheck, input_claim)| { let num_rounds = sumcheck.num_rounds(); - let input_claim = sumcheck.input_claim(opening_accumulator); input_claim.mul_pow_2(max_num_rounds - num_rounds) }) .collect(); @@ -165,6 +189,26 @@ impl BatchedSumcheck { .max() .unwrap(); + if Self::debug_final_claims_enabled() { + let final_claims: Vec<(usize, usize, usize, F)> = sumcheck_instances + .iter() + .zip(individual_claims.iter()) + .enumerate() + .map(|(idx, (sumcheck, claim))| { + ( + idx, + sumcheck.round_offset(max_num_rounds), + sumcheck.num_rounds(), + *claim, + ) + }) + .collect(); + tracing::info!( + "BatchedSumcheck::prove final individual claims: {:?}", + final_claims + ); + } + for sumcheck in sumcheck_instances.iter() { // Instance-local slice can start at a custom global offset. let offset = sumcheck.round_offset(max_num_rounds); @@ -175,6 +219,15 @@ impl BatchedSumcheck { sumcheck.cache_openings(opening_accumulator, r_slice); } + if Self::debug_pending_ids_enabled() { + tracing::info!( + "BatchedSumcheck::prove pending_ids (instances={} rounds={}): {:?}", + sumcheck_instances.len(), + max_num_rounds, + opening_accumulator.pending_claim_ids_debug() + ); + } + opening_accumulator.flush_to_transcript(transcript); ( @@ -449,12 +502,22 @@ impl BatchedSumcheck { .sum(); let (output_claim, r_sumcheck) = - proof.verify(claim, max_num_rounds, max_degree, transcript)?; - - let expected_output_claim: F = sumcheck_instances + proof.verify(claim, max_num_rounds, max_degree, transcript) + .map_err(|err| { + tracing::error!( + "BatchedSumcheck::verify failed inside proof.verify: claim={} max_num_rounds={} max_degree={}", + claim, + max_num_rounds, + max_degree + ); + err + })?; + + let expected_output_terms: Vec<(usize, usize, F, F, F)> = sumcheck_instances .iter() .zip(batching_coeffs.iter()) - .map(|(sumcheck, coeff)| { + .enumerate() + .map(|(idx, (sumcheck, coeff))| { let offset = sumcheck.round_offset(max_num_rounds); let r_slice = &r_sumcheck[offset..offset + sumcheck.num_rounds()]; @@ -463,10 +526,29 @@ impl BatchedSumcheck { sumcheck.cache_openings(opening_accumulator, r_slice); let claim = sumcheck.expected_output_claim(opening_accumulator, r_slice); - claim * coeff + (idx, sumcheck.num_rounds(), *coeff, claim, claim * coeff) }) + .collect(); + if Self::debug_final_claims_enabled() { + tracing::info!( + "BatchedSumcheck::verify expected output terms: {:?}", + expected_output_terms + ); + } + let expected_output_claim: F = expected_output_terms + .iter() + .map(|(_, _, _, _, weighted_claim)| *weighted_claim) .sum(); + if Self::debug_pending_ids_enabled() { + tracing::info!( + "BatchedSumcheck::verify pending_ids (instances={} rounds={}): {:?}", + sumcheck_instances.len(), + max_num_rounds, + opening_accumulator.pending_claim_ids_debug() + ); + } + if !is_zk { opening_accumulator.flush_to_transcript(transcript); } else if let SumcheckInstanceProof::Zk(zk_proof) = proof { @@ -477,6 +559,15 @@ impl BatchedSumcheck { // In ZK mode, skip output claim verification — BlindFold proves this if !is_zk && output_claim != expected_output_claim { + tracing::error!( + "BatchedSumcheck::verify output-claim mismatch: output_claim={} expected_output_claim={}", + output_claim, + expected_output_claim + ); + tracing::error!( + "BatchedSumcheck::verify expected output terms: {:?}", + expected_output_terms + ); return Err(ProofVerifyError::SumcheckVerificationError); } diff --git a/jolt-core/src/utils/errors.rs b/jolt-core/src/utils/errors.rs index 2ca3d05e99..4580f8fa96 100644 --- a/jolt-core/src/utils/errors.rs +++ b/jolt-core/src/utils/errors.rs @@ -28,6 +28,8 @@ pub enum ProofVerifyError { InvalidReadWriteConfig(String), #[error("Invalid one-hot configuration: {0}")] InvalidOneHotConfig(String), + #[error("Invalid bytecode commitment configuration: {0}")] + InvalidBytecodeConfig(String), #[error("Invalid ram_K: got {0}, minimum required {1}")] InvalidRamK(usize, usize), #[error("Dory proof verification failed: {0}")] @@ -42,4 +44,6 @@ pub enum ProofVerifyError { ZkFeatureRequired, #[error("BlindFold verification failed: {0}")] BlindFoldError(String), + #[error("Bytecode type mismatch: {0}")] + BytecodeTypeMismatch(String), } diff --git a/jolt-core/src/zkvm/bytecode/mod.rs b/jolt-core/src/zkvm/bytecode/mod.rs index c4761e27b9..635ba3a394 100644 --- a/jolt-core/src/zkvm/bytecode/mod.rs +++ b/jolt-core/src/zkvm/bytecode/mod.rs @@ -1,9 +1,82 @@ use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use common::constants::{ALIGNMENT_FACTOR_BYTECODE, RAM_START_ADDRESS}; +use rayon::prelude::*; use tracer::instruction::{Cycle, Instruction}; +use crate::poly::commitment::commitment_scheme::CommitmentScheme; +use crate::poly::commitment::dory::{DoryContext, DoryGlobals}; +use crate::utils::math::Math; +use crate::zkvm::bytecode::chunks::{ + build_committed_bytecode_chunk_polynomials, committed_bytecode_chunk_cycle_len, + committed_lanes, validate_committed_bytecode_chunking_for_len, +}; + +pub mod chunks; pub mod read_raf_checking; +#[derive(Clone, Debug, PartialEq, CanonicalSerialize, CanonicalDeserialize)] +pub struct TrustedBytecodeCommitments { + pub commitments: Vec, + pub num_columns: usize, + pub log_k_chunk: u8, + pub bytecode_chunk_count: usize, + pub bytecode_len: usize, + pub bytecode_T: usize, +} + +#[derive(Clone)] +pub struct TrustedBytecodeHints { + pub hints: Vec, +} + +impl TrustedBytecodeCommitments { + #[tracing::instrument(skip_all, name = "TrustedBytecodeCommitments::derive")] + pub fn derive( + bytecode: &BytecodePreprocessing, + generators: &PCS::ProverSetup, + log_k_chunk: usize, + bytecode_chunk_count: usize, + ) -> (Self, TrustedBytecodeHints) { + let bytecode_len = bytecode.code_size; + validate_committed_bytecode_chunking_for_len(bytecode_len, bytecode_chunk_count); + let bytecode_T = committed_bytecode_chunk_cycle_len(bytecode_len, bytecode_chunk_count); + + let total_vars = bytecode_T.log_2() + committed_lanes().log_2(); + let (bytecode_sigma, _) = DoryGlobals::balanced_sigma_nu(total_vars); + let num_columns = 1usize << bytecode_sigma; + + let bytecode_chunk_polys = build_committed_bytecode_chunk_polynomials::( + &bytecode.bytecode, + bytecode_chunk_count, + ); + let _bytecode_guard = DoryGlobals::initialize_context( + committed_lanes(), + bytecode_T, + DoryContext::UntrustedAdvice, + None, + ); + let (commitments, hints): (Vec<_>, Vec<_>) = bytecode_chunk_polys + .par_iter() + .map(|poly| { + let _ctx = DoryGlobals::with_context(DoryContext::UntrustedAdvice); + PCS::commit(poly, generators) + }) + .unzip(); + + ( + Self { + commitments, + num_columns, + log_k_chunk: log_k_chunk as u8, + bytecode_chunk_count, + bytecode_len, + bytecode_T, + }, + TrustedBytecodeHints { hints }, + ) + } +} + #[derive(Default, Debug, Clone, CanonicalSerialize, CanonicalDeserialize)] pub struct BytecodePreprocessing { pub code_size: usize, @@ -66,13 +139,17 @@ impl BytecodePCMapper { let mut indices: Vec> = { // For read-raf tests we simulate bytecode being empty #[cfg(test)] - if bytecode.len() == 1 { - vec![None; 1] - } else { - vec![None; Self::get_index(bytecode.last().unwrap().normalize().address) + 1] + { + if bytecode.len() == 1 { + vec![None; 1] + } else { + vec![None; Self::get_index(bytecode.last().unwrap().normalize().address) + 1] + } } #[cfg(not(test))] - vec![None; Self::get_index(bytecode.last().unwrap().normalize().address) + 1] + { + vec![None; Self::get_index(bytecode.last().unwrap().normalize().address) + 1] + } }; let mut last_pc = 0; // Push the initial noop instruction diff --git a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs index 54a23f5e33..8d3997ddd8 100644 --- a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs +++ b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs @@ -30,15 +30,19 @@ use crate::{ sumcheck_verifier::{SumcheckInstanceParams, SumcheckInstanceVerifier}, }, transcripts::{KeccakTranscript, Transcript}, - utils::{math::Math, small_scalar::SmallScalar, thread::unsafe_allocate_zero_vec}, + utils::{ + errors::ProofVerifyError, math::Math, small_scalar::SmallScalar, + thread::unsafe_allocate_zero_vec, + }, zkvm::{ bytecode::BytecodePreprocessing, - config::OneHotParams, + config::{OneHotParams, ProgramMode}, instruction::{ CircuitFlags, Flags, InstructionFlags, InstructionLookup, InterleavedBitsMarker, NUM_CIRCUIT_FLAGS, }, lookup_table::{LookupTables, NUM_LOOKUP_TABLES}, + program::ProgramPreprocessing, witness::{CommittedPolynomial, VirtualPolynomial}, }, }; @@ -701,6 +705,7 @@ impl SumcheckInstanceProver pub struct BytecodeReadRafAddressSumcheckProver { inner: BytecodeReadRafSumcheckProver, address_params: BytecodeReadRafAddressPhaseParams, + staged_val_polys: Option<[MultilinearPolynomial; N_STAGES]>, } impl BytecodeReadRafAddressSumcheckProver { @@ -709,10 +714,16 @@ impl BytecodeReadRafAddressSumcheckProver { trace: Arc>, bytecode_preprocessing: Arc, ) -> Self { + let staged_val_polys = if params.use_staged_val_claims { + Some(params.val_polys.clone()) + } else { + None + }; let address_params = BytecodeReadRafAddressPhaseParams::new(params.clone()); Self { inner: BytecodeReadRafSumcheckProver::initialize(params, trace, bytecode_preprocessing), address_params, + staged_val_polys, } } } @@ -772,9 +783,23 @@ impl SumcheckInstanceProver accumulator.append_virtual( VirtualPolynomial::BytecodeReadRafAddrClaim, SumcheckId::BytecodeReadRafAddressPhase, - opening_point, + opening_point.clone(), address_claim, ); + if self.inner.params.use_staged_val_claims { + let staged_val_polys = self + .staged_val_polys + .as_ref() + .expect("staged val polynomials must be present in committed mode"); + for stage in 0..N_STAGES { + accumulator.append_virtual( + VirtualPolynomial::BytecodeValStage(stage), + SumcheckId::BytecodeReadRafAddressPhase, + opening_point.clone(), + staged_val_polys[stage].evaluate(&opening_point.r), + ); + } + } } #[cfg(feature = "allocative")] @@ -894,17 +919,19 @@ pub struct BytecodeReadRafSumcheckVerifier { impl BytecodeReadRafSumcheckVerifier { pub fn gen( - bytecode_preprocessing: &BytecodePreprocessing, + program: &ProgramPreprocessing, n_cycle_vars: usize, one_hot_params: &OneHotParams, + use_staged_val_claims: bool, opening_accumulator: &VerifierOpeningAccumulator, transcript: &mut impl Transcript, ) -> Self { Self { params: BytecodeReadRafSumcheckParams::gen( - bytecode_preprocessing, + program, n_cycle_vars, one_hot_params, + use_staged_val_claims, opening_accumulator, transcript, ), @@ -930,14 +957,16 @@ impl SumcheckInstanceVerifier let int_poly = self.params.int_poly.evaluate(&r_address_prime.r); - let ra_claims = (0..self.params.d).map(|i| { - accumulator - .get_committed_polynomial_opening( - CommittedPolynomial::BytecodeRa(i), - SumcheckId::BytecodeReadRaf, - ) - .1 - }); + let ra_claims: Vec = (0..self.params.d) + .map(|i| { + accumulator + .get_committed_polynomial_opening( + CommittedPolynomial::BytecodeRa(i), + SumcheckId::BytecodeReadRaf, + ) + .1 + }) + .collect(); // We have a separate Val polynomial for each stage // Additionally, for stages 1 and 3 we have an Int polynomial for RAF @@ -949,29 +978,43 @@ impl SumcheckInstanceVerifier // Stage 5: gamma^4 * (Val_5) // Which matches with the input claim: // rv_1 + gamma * rv_2 + gamma^2 * rv_3 + gamma^3 * rv_4 + gamma^4 * rv_5 + gamma^5 * raf_1 + gamma^6 * raf_3 + let stage_val_claim = |stage: usize| { + if self.params.use_staged_val_claims { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeValStage(stage), + SumcheckId::BytecodeReadRafAddressPhase, + ) + .1 + } else { + self.params.val_polys[stage].evaluate(&r_address_prime.r) + } + }; + // Always add RAF-int terms here. In committed mode, staged Stage 6a BytecodeValStage + // openings carry val-only claims and the RAF contribution is reconstructed at verification. + let int_poly_contrib_by_stage = [ + int_poly * self.params.gamma_powers[5], // RAF for Stage1 + F::zero(), // There's no raf for Stage2 + int_poly * self.params.gamma_powers[4], // RAF for Stage3 + F::zero(), // There's no raf for Stage4 + F::zero(), // There's no raf for Stage5 + ]; + let val = self .params - .val_polys + .r_cycles .iter() - .zip(&self.params.r_cycles) .zip(&self.params.gamma_powers) - .zip([ - int_poly * self.params.gamma_powers[5], // RAF for Stage1 - F::zero(), // There's no raf for Stage2 - int_poly * self.params.gamma_powers[4], // RAF for Stage3 - F::zero(), // There's no raf for Stage4 - F::zero(), // There's no raf for Stage5 - ]) - .map(|(((val, r_cycle), gamma), int_poly)| { - (val.evaluate(&r_address_prime.r) + int_poly) + .zip(int_poly_contrib_by_stage) + .enumerate() + .map(|(stage, ((r_cycle, gamma), int_poly))| { + (stage_val_claim(stage) + int_poly) * EqPolynomial::::mle(r_cycle, &r_cycle_prime.r) * gamma }) .sum::(); // Entry constraint: γ_entry · eq(r_addr, entry_bits) · eq_zero(r_cycle). - // r_address_prime.r is MSB-first (after normalize_opening_point reversal), - // so entry_bits must also be MSB-first: entry_bits[j] = (e >> (log_K-1-j)) & 1. let entry_f_at_r_addr = { let log_k = self.params.log_K; let e = self.params.entry_bytecode_index; @@ -985,7 +1028,9 @@ impl SumcheckInstanceVerifier let eq_zero_at_r_cycle = EqPolynomial::::mle(&zeros, &r_cycle_prime.r); let entry_contrib = self.params.entry_gamma * entry_f_at_r_addr * eq_zero_at_r_cycle; - ra_claims.fold(val + entry_contrib, |running, ra_claim| running * ra_claim) + ra_claims + .iter() + .fold(val + entry_contrib, |running, ra_claim| running * *ra_claim) } fn cache_openings( @@ -1020,24 +1065,40 @@ pub struct BytecodeReadRafAddressSumcheckVerifier { impl BytecodeReadRafAddressSumcheckVerifier { pub fn new( - bytecode_preprocessing: &BytecodePreprocessing, + program: Option<&ProgramPreprocessing>, n_cycle_vars: usize, one_hot_params: &OneHotParams, opening_accumulator: &VerifierOpeningAccumulator, transcript: &mut impl Transcript, - ) -> Self { - let params = BytecodeReadRafSumcheckParams::gen( - bytecode_preprocessing, - n_cycle_vars, - one_hot_params, - opening_accumulator, - transcript, - ); + program_mode: ProgramMode, + entry_bytecode_index: usize, + ) -> Result { + let params = match program_mode { + ProgramMode::Committed => BytecodeReadRafSumcheckParams::gen_verifier( + n_cycle_vars, + one_hot_params, + entry_bytecode_index, + opening_accumulator, + transcript, + ), + ProgramMode::Full => BytecodeReadRafSumcheckParams::gen( + program.ok_or_else(|| { + ProofVerifyError::BytecodeTypeMismatch( + "expected Full program preprocessing, got Committed".to_string(), + ) + })?, + n_cycle_vars, + one_hot_params, + false, + opening_accumulator, + transcript, + ), + }; let address_params = BytecodeReadRafAddressPhaseParams::new(params.clone()); - Self { + Ok(Self { params, address_params, - } + }) } pub fn into_params(self) -> BytecodeReadRafSumcheckParams { @@ -1087,8 +1148,17 @@ impl SumcheckInstanceVerifier accumulator.append_virtual( VirtualPolynomial::BytecodeReadRafAddrClaim, SumcheckId::BytecodeReadRafAddressPhase, - OpeningPoint::::new(r_address), + OpeningPoint::::new(r_address.clone()), ); + if self.params.use_staged_val_claims { + for stage in 0..N_STAGES { + accumulator.append_virtual( + VirtualPolynomial::BytecodeValStage(stage), + SumcheckId::BytecodeReadRafAddressPhase, + OpeningPoint::::new(r_address.clone()), + ); + } + } } } @@ -1325,6 +1395,8 @@ impl SumcheckInstanceParams for BytecodeReadRafAddressPhasePara #[derive(Allocative, Clone)] pub struct BytecodeReadRafSumcheckParams { + /// Whether Stage 6a should stage per-stage Val claims for BytecodeClaimReduction. + pub use_staged_val_claims: bool, /// Index `i` stores `gamma^i`. pub gamma_powers: Vec, /// Stage-specific gamma powers for input_claim_constraint @@ -1367,15 +1439,57 @@ pub struct BytecodeReadRafSumcheckParams { impl BytecodeReadRafSumcheckParams { #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckParams::gen")] pub fn gen( - bytecode_preprocessing: &BytecodePreprocessing, + program: &ProgramPreprocessing, n_cycle_vars: usize, one_hot_params: &OneHotParams, + use_staged_val_claims: bool, opening_accumulator: &dyn OpeningAccumulator, transcript: &mut impl Transcript, ) -> Self { - let gamma_powers = transcript.challenge_scalar_powers(8); + Self::gen_impl( + Some(program), + n_cycle_vars, + one_hot_params, + use_staged_val_claims, + Some(program.entry_bytecode_index()), + opening_accumulator, + transcript, + true, + ) + } - let bytecode = &bytecode_preprocessing.bytecode; + #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckParams::gen_verifier")] + pub fn gen_verifier( + n_cycle_vars: usize, + one_hot_params: &OneHotParams, + entry_bytecode_index: usize, + opening_accumulator: &dyn OpeningAccumulator, + transcript: &mut impl Transcript, + ) -> Self { + Self::gen_impl( + None, + n_cycle_vars, + one_hot_params, + true, + Some(entry_bytecode_index), + opening_accumulator, + transcript, + false, + ) + } + + #[allow(clippy::too_many_arguments)] + fn gen_impl( + program: Option<&ProgramPreprocessing>, + n_cycle_vars: usize, + one_hot_params: &OneHotParams, + use_staged_val_claims: bool, + entry_bytecode_index: Option, + opening_accumulator: &dyn OpeningAccumulator, + transcript: &mut impl Transcript, + compute_val_polys: bool, + ) -> Self { + let gamma_powers = transcript.challenge_scalar_powers(8); // Generate all stage-specific gamma powers upfront (order must match verifier) let stage1_gammas: Vec = transcript.challenge_scalar_powers(2 + NUM_CIRCUIT_FLAGS); @@ -1414,16 +1528,25 @@ impl BytecodeReadRafSumcheckParams { EqPolynomial::::evals(&r_register_5[..(REGISTER_COUNT as usize).log_2()]); // Fused pass: compute all val polynomials in a single parallel iteration - let val_polys = Self::compute_val_polys( - bytecode, - &eq_r_register_4, - &eq_r_register_5, - &stage1_gammas, - &stage2_gammas, - &stage3_gammas, - &stage4_gammas, - &stage5_gammas, - ); + let val_polys = if compute_val_polys { + Self::compute_val_polys( + &program + .expect("compute_val_polys requires program preprocessing") + .bytecode + .bytecode, + &eq_r_register_4, + &eq_r_register_5, + &stage1_gammas, + &stage2_gammas, + &stage3_gammas, + &stage4_gammas, + &stage5_gammas, + ) + } else { + array::from_fn(|_| { + MultilinearPolynomial::from(vec![F::zero(); one_hot_params.bytecode_k]) + }) + }; let int_poly = IdentityPolynomial::new(one_hot_params.bytecode_k.log_2()); @@ -1432,11 +1555,10 @@ impl BytecodeReadRafSumcheckParams { let (_, raf_shift_claim) = opening_accumulator .get_virtual_polynomial_opening(VirtualPolynomial::PC, SumcheckId::SpartanShift); let entry_gamma = gamma_powers[7]; - let entry_bytecode_index = bytecode_preprocessing.entry_bytecode_index(); - // Both prover and verifier add entry_gamma unconditionally. - // The security comes from the sumcheck: if ra(entry_index, 0) != 1, the sum - // won't match input_claim and the sumcheck fails. - let input_claim: F = [ + let entry_bytecode_index = entry_bytecode_index + .or_else(|| program.map(|program| program.entry_bytecode_index())) + .unwrap_or_default(); + let mut input_claim: F = [ rv_claim_1, rv_claim_2, rv_claim_3, @@ -1448,8 +1570,8 @@ impl BytecodeReadRafSumcheckParams { .iter() .zip(&gamma_powers) .map(|(claim, g)| *claim * g) - .sum::() - + entry_gamma; + .sum::(); + input_claim += entry_gamma; let (r_cycle_1, _) = opening_accumulator .get_virtual_polynomial_opening(VirtualPolynomial::Imm, SumcheckId::SpartanOuter); @@ -1480,6 +1602,7 @@ impl BytecodeReadRafSumcheckParams { ]; Self { + use_staged_val_claims, gamma_powers, entry_gamma, entry_bytecode_index, @@ -2241,8 +2364,6 @@ impl SumcheckInstanceParams for BytecodeReadRafSumcheckParams> (log_K-1-j)) & 1. let f_entry_at_r_addr = if let Some(v) = self.bound_f_entry { v } else { diff --git a/jolt-core/src/zkvm/claim_reductions/bytecode.rs b/jolt-core/src/zkvm/claim_reductions/bytecode.rs new file mode 100644 index 0000000000..90f87c3875 --- /dev/null +++ b/jolt-core/src/zkvm/claim_reductions/bytecode.rs @@ -0,0 +1,697 @@ +//! Two-phase bytecode claim reduction (Stage 6b cycle -> Stage 7 address). + +use std::cell::RefCell; + +use allocative::Allocative; +use rayon::prelude::*; + +use crate::field::JoltField; +use crate::poly::commitment::dory::{DoryGlobals, DoryLayout}; +use crate::poly::eq_poly::EqPolynomial; +use crate::poly::multilinear_polynomial::{BindingOrder, MultilinearPolynomial, PolynomialBinding}; +use crate::poly::opening_proof::{ + OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, SumcheckId, + VerifierOpeningAccumulator, BIG_ENDIAN, LITTLE_ENDIAN, +}; +use crate::poly::unipoly::UniPoly; +use crate::subprotocols::sumcheck_prover::SumcheckInstanceProver; +use crate::subprotocols::sumcheck_verifier::{SumcheckInstanceParams, SumcheckInstanceVerifier}; +use crate::transcripts::Transcript; +use crate::utils::math::Math; +use crate::zkvm::bytecode::chunks::committed_lanes; +use crate::zkvm::bytecode::read_raf_checking::BytecodeReadRafSumcheckParams; +use crate::zkvm::claim_reductions::{ + permute_precommitted_polys, precommitted_skip_round_scale, PrecommittedClaimReduction, + PrecommittedPhase, PrecommittedSchedulingReference, TWO_PHASE_DEGREE_BOUND, +}; +use crate::zkvm::instruction::{ + CircuitFlags, InstructionFlags, NUM_CIRCUIT_FLAGS, NUM_INSTRUCTION_FLAGS, +}; +use crate::zkvm::lookup_table::LookupTables; +use crate::zkvm::witness::{CommittedPolynomial, VirtualPolynomial}; +use common::constants::{REGISTER_COUNT, XLEN}; +use strum::EnumCount; + +const NUM_VAL_STAGES: usize = 5; + +fn debug_bytecode_reduction_enabled() -> bool { + std::env::var("JOLT_DEBUG_BYTECODE_REDUCTION") + .map(|v| { + let value = v.trim().to_ascii_lowercase(); + !matches!(value.as_str(), "" | "0" | "false" | "off") + }) + .unwrap_or(false) +} + +#[derive(Clone, Allocative)] +pub struct BytecodeClaimReductionParams { + pub phase: PrecommittedPhase, + pub precommitted: PrecommittedClaimReduction, + pub eta: F, + pub eta_powers: [F; NUM_VAL_STAGES], + /// Eq weights over high bytecode address bits (one per committed chunk). + pub chunk_rbc_weights: Vec, + pub bytecode_T: usize, + pub log_t: usize, + /// Number of initial cycle rounds that must follow IncClaimReduction ordering. + pub dense_cycle_prefix_vars: usize, + pub bytecode_chunk_count: usize, + pub bytecode_col_vars: usize, + pub bytecode_row_vars: usize, + pub r_bc: OpeningPoint, + pub lane_weights: Vec, +} + +impl BytecodeClaimReductionParams { + pub fn new( + bytecode_read_raf_params: &BytecodeReadRafSumcheckParams, + full_bytecode_len: usize, + bytecode_chunk_count: usize, + scheduling_reference: PrecommittedSchedulingReference, + accumulator: &dyn OpeningAccumulator, + transcript: &mut impl Transcript, + ) -> Self { + let log_t = DoryGlobals::main_t().log_2(); + assert!( + full_bytecode_len.is_multiple_of(bytecode_chunk_count), + "bytecode chunk count ({bytecode_chunk_count}) must divide bytecode_len ({full_bytecode_len})" + ); + let bytecode_t = (full_bytecode_len / bytecode_chunk_count).log_2(); + let bytecode_t_full = full_bytecode_len.log_2(); + + let eta: F = transcript.challenge_scalar(); + let mut eta_powers = [F::one(); NUM_VAL_STAGES]; + for i in 1..NUM_VAL_STAGES { + eta_powers[i] = eta_powers[i - 1] * eta; + } + + let (r_bc_full, _) = accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ); + debug_assert_eq!(r_bc_full.r.len(), bytecode_t_full); + let dropped_bits = bytecode_t_full - bytecode_t; + let chunk_rbc_weights = if dropped_bits == 0 { + vec![F::one()] + } else { + EqPolynomial::::evals(&r_bc_full.r[..dropped_bits]) + }; + debug_assert_eq!(chunk_rbc_weights.len(), bytecode_chunk_count); + let r_bc = OpeningPoint::new(r_bc_full.r[dropped_bits..].to_vec()); + + let lane_weights = compute_lane_weights(bytecode_read_raf_params, accumulator, &eta_powers); + + // bytecode_K is the committed lane capacity (already next-power-of-two padded). + let bytecode_k = committed_lanes(); + let total_vars = bytecode_k.log_2() + bytecode_t; + // Bytecode uses its own balanced dimensions (independent from Main). + // In Stage 8 it is embedded as a top-left block in Joint. + let (bytecode_col_vars, bytecode_row_vars) = DoryGlobals::balanced_sigma_nu(total_vars); + let precommitted = PrecommittedClaimReduction::new( + total_vars, + bytecode_row_vars, + bytecode_col_vars, + scheduling_reference, + ); + // Align all precommitted scheduling/permutation to the shared reference domain. + + Self { + phase: PrecommittedPhase::CycleVariables, + precommitted, + eta, + eta_powers, + chunk_rbc_weights, + bytecode_T: bytecode_t, + log_t, + dense_cycle_prefix_vars: log_t, + bytecode_chunk_count, + bytecode_col_vars, + bytecode_row_vars, + r_bc, + lane_weights, + } + } + + pub fn num_address_phase_rounds(&self) -> usize { + self.precommitted.num_address_phase_rounds() + } +} + +impl BytecodeClaimReductionParams { + fn is_cycle_phase(&self) -> bool { + self.phase == PrecommittedPhase::CycleVariables + } + + fn is_cycle_phase_round(&self, round: usize) -> bool { + self.precommitted.is_cycle_phase_round(round) + } + + fn is_address_phase_round(&self, round: usize) -> bool { + self.precommitted.is_address_phase_round(round) + } + + fn cycle_alignment_rounds(&self) -> usize { + self.precommitted.cycle_alignment_rounds() + } + + fn address_alignment_rounds(&self) -> usize { + self.precommitted.address_alignment_rounds() + } + + pub fn transition_to_address_phase(&mut self) { + self.phase = PrecommittedPhase::AddressVariables; + } + + fn num_rounds_for_current_phase(&self) -> usize { + self.precommitted + .num_rounds_for_phase(self.is_cycle_phase()) + } + + pub fn round_offset(&self, max_num_rounds: usize) -> usize { + self.precommitted + .round_offset(self.is_cycle_phase(), max_num_rounds) + } +} + +impl SumcheckInstanceParams for BytecodeClaimReductionParams { + fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { + match self.phase { + PrecommittedPhase::CycleVariables => (0..NUM_VAL_STAGES) + .map(|stage| { + let (_, val_claim) = accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeValStage(stage), + SumcheckId::BytecodeReadRafAddressPhase, + ); + self.eta_powers[stage] * val_claim + }) + .sum(), + PrecommittedPhase::AddressVariables => { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeClaimReductionIntermediate, + SumcheckId::BytecodeClaimReductionCyclePhase, + ) + .1 + } + } + } + + fn degree(&self) -> usize { + TWO_PHASE_DEGREE_BOUND + } + + fn num_rounds(&self) -> usize { + self.num_rounds_for_current_phase() + } + + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + self.precommitted.normalize_opening_point( + self.is_cycle_phase(), + challenges, + self.dense_cycle_prefix_vars, + ) + } +} + +#[derive(Allocative)] +pub struct BytecodeClaimReductionProver { + params: BytecodeClaimReductionParams, + value_poly: MultilinearPolynomial, + eq_poly: MultilinearPolynomial, + scale: F, + chunk_value_polys: Vec>, + pending_round_poly: Option>, + running_claim: Option, +} + +impl BytecodeClaimReductionProver { + pub fn params(&self) -> &BytecodeClaimReductionParams { + &self.params + } + + pub fn transition_to_address_phase(&mut self) { + self.params.transition_to_address_phase(); + } + + pub fn initialize( + params: BytecodeClaimReductionParams, + raw_chunk_polys: &[MultilinearPolynomial], + ) -> Self { + let eq_cycle = EqPolynomial::::evals(¶ms.r_bc.r); + let eq_coeffs_template: Vec = (0..raw_chunk_polys[0].len()) + .map(|idx| { + let (lane, cycle) = native_index_to_lane_cycle(¶ms, idx); + params.lane_weights[lane] * eq_cycle[cycle] + }) + .collect(); + + let raw_value_coeffs: Vec = (0..raw_chunk_polys[0].len()) + .into_par_iter() + .map(|idx| { + raw_chunk_polys + .iter() + .zip(params.chunk_rbc_weights.iter()) + .map(|(poly, weight)| poly.get_coeff(idx) * *weight) + .sum::() + }) + .collect(); + let mut coeffs_by_poly = Vec::with_capacity(2 + raw_chunk_polys.len()); + coeffs_by_poly.push(raw_value_coeffs); + coeffs_by_poly.push(eq_coeffs_template); + for raw_chunk_poly in raw_chunk_polys.iter() { + let raw_chunk_coeffs: Vec = (0..raw_chunk_poly.len()) + .map(|idx| raw_chunk_poly.get_coeff(idx)) + .collect(); + coeffs_by_poly.push(raw_chunk_coeffs); + } + let mut permuted_polys = + permute_precommitted_polys(coeffs_by_poly, ¶ms.precommitted).into_iter(); + let value_poly = permuted_polys + .next() + .expect("expected permuted bytecode value polynomial"); + let eq_poly = permuted_polys + .next() + .expect("expected permuted bytecode eq polynomial"); + let chunk_value_polys: Vec> = permuted_polys.collect(); + + if debug_bytecode_reduction_enabled() { + let initial_true_claim: F = (0..value_poly.len()) + .map(|i| value_poly.get_bound_coeff(i) * eq_poly.get_bound_coeff(i)) + .sum(); + tracing::info!( + "BytecodeClaimReduction initialize value_len={} eq_len={} initial_true_claim={}", + value_poly.len(), + eq_poly.len(), + initial_true_claim + ); + } + + Self { + params, + value_poly, + eq_poly, + scale: F::one(), + chunk_value_polys, + pending_round_poly: None, + running_claim: None, + } + } + + fn bind_aux_polys(&mut self, r_j: F::Challenge) { + for poly in self.chunk_value_polys.iter_mut() { + poly.bind_parallel(r_j, BindingOrder::LowToHigh); + } + } + + fn compute_message_unscaled(&self, previous_claim_unscaled: F) -> UniPoly { + let half = self.value_poly.len() / 2; + let evals: [F; TWO_PHASE_DEGREE_BOUND] = (0..half) + .into_par_iter() + .map(|j| { + let value_evals = self + .value_poly + .sumcheck_evals_array::(j, BindingOrder::LowToHigh); + let eq_evals = self + .eq_poly + .sumcheck_evals_array::(j, BindingOrder::LowToHigh); + let mut out = [F::zero(); TWO_PHASE_DEGREE_BOUND]; + for i in 0..TWO_PHASE_DEGREE_BOUND { + out[i] = value_evals[i] * eq_evals[i]; + } + out + }) + .reduce( + || [F::zero(); TWO_PHASE_DEGREE_BOUND], + |mut acc, arr| { + acc.iter_mut().zip(arr.iter()).for_each(|(a, b)| *a += *b); + acc + }, + ); + UniPoly::from_evals_and_hint(previous_claim_unscaled, &evals) + } + + fn cycle_intermediate_claim(&self) -> F { + let len = self.value_poly.len(); + debug_assert_eq!(len, self.eq_poly.len()); + let mut sum = F::zero(); + for i in 0..len { + sum += self.value_poly.get_bound_coeff(i) * self.eq_poly.get_bound_coeff(i); + } + sum * self.scale + } + + fn final_claim_if_ready(&self) -> Option { + if self.value_poly.len() == 1 { + Some(self.value_poly.get_bound_coeff(0)) + } else { + None + } + } +} + +impl SumcheckInstanceProver for BytecodeClaimReductionProver { + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.params + } + + fn round_offset(&self, max_num_rounds: usize) -> usize { + self.params.round_offset(max_num_rounds) + } + + fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { + let is_active_round = if self.params.is_cycle_phase() { + self.params.is_cycle_phase_round(round) + } else { + self.params.is_address_phase_round(round) + }; + if !is_active_round { + let round_poly = + UniPoly::from_coeff(vec![previous_claim * F::from_u64(2).inverse().unwrap()]); + self.pending_round_poly = Some(round_poly.clone()); + return round_poly; + } + + let trailing_cap = if self.params.is_cycle_phase() { + self.params.cycle_alignment_rounds() + } else { + self.params.address_alignment_rounds() + }; + let num_trailing_variables = + trailing_cap.saturating_sub(self.params.num_rounds_for_current_phase()); + let scaling_factor = self.scale * F::one().mul_pow_2(num_trailing_variables); + let prev_unscaled = previous_claim * scaling_factor.inverse().unwrap(); + let poly_unscaled = self.compute_message_unscaled(prev_unscaled); + let round_poly = poly_unscaled * scaling_factor; + self.pending_round_poly = Some(round_poly.clone()); + round_poly + } + + fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { + if let Some(round_poly) = self.pending_round_poly.take() { + self.running_claim = Some(round_poly.evaluate(&r_j)); + } + let is_active_round = if self.params.is_cycle_phase() { + self.params.is_cycle_phase_round(round) + } else { + self.params.is_address_phase_round(round) + }; + if !is_active_round { + self.scale *= F::from_u64(2).inverse().unwrap(); + return; + } + + self.value_poly.bind_parallel(r_j, BindingOrder::LowToHigh); + self.eq_poly.bind_parallel(r_j, BindingOrder::LowToHigh); + self.bind_aux_polys(r_j); + if self.params.is_cycle_phase() { + self.params.precommitted.record_cycle_challenge(r_j); + } + } + + fn cache_openings( + &self, + accumulator: &mut ProverOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let params = &self.params; + let opening_point = params.normalize_opening_point(sumcheck_challenges); + + if params.phase == PrecommittedPhase::CycleVariables { + let c_mid = self.cycle_intermediate_claim(); + let synced_cycle_claim = self.running_claim.unwrap_or(c_mid); + if debug_bytecode_reduction_enabled() { + tracing::info!( + "BytecodeClaimReduction cache cycle len={} bound_value={} bound_eq={} scale={} cycle_claim={} synced_cycle_claim={}", + self.value_poly.len(), + self.value_poly.get_bound_coeff(0), + self.eq_poly.get_bound_coeff(0), + self.scale, + c_mid, + synced_cycle_claim, + ); + } + accumulator.append_virtual( + VirtualPolynomial::BytecodeClaimReductionIntermediate, + SumcheckId::BytecodeClaimReductionCyclePhase, + opening_point.clone(), + synced_cycle_claim, + ); + } + + if let Some(bytecode_claim) = self.final_claim_if_ready() { + let chunk_claims: Vec = self + .chunk_value_polys + .iter() + .map(|poly| poly.final_sumcheck_claim()) + .collect(); + let weighted_chunk_sum = chunk_claims + .iter() + .zip(params.chunk_rbc_weights.iter()) + .map(|(claim, weight)| *claim * *weight) + .sum::(); + debug_assert_eq!(weighted_chunk_sum, bytecode_claim); + for (chunk_idx, claim) in chunk_claims.into_iter().enumerate() { + accumulator.append_dense( + CommittedPolynomial::BytecodeChunk(chunk_idx), + SumcheckId::BytecodeClaimReduction, + opening_point.r.clone(), + claim, + ); + } + } + } + + #[cfg(feature = "allocative")] + fn update_flamegraph(&self, flamegraph: &mut allocative::FlameGraphBuilder) { + flamegraph.visit_root(self); + } +} + +pub struct BytecodeClaimReductionVerifier { + pub params: RefCell>, +} + +impl BytecodeClaimReductionVerifier { + pub fn new(params: BytecodeClaimReductionParams) -> Self { + Self { + params: RefCell::new(params), + } + } +} + +impl SumcheckInstanceVerifier + for BytecodeClaimReductionVerifier +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + unsafe { &*self.params.as_ptr() } + } + + fn round_offset(&self, max_num_rounds: usize) -> usize { + let params = self.params.borrow(); + params.round_offset(max_num_rounds) + } + + fn expected_output_claim( + &self, + accumulator: &VerifierOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) -> F { + let params = self.params.borrow(); + match params.phase { + PrecommittedPhase::CycleVariables => { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeClaimReductionIntermediate, + SumcheckId::BytecodeClaimReductionCyclePhase, + ) + .1 + } + PrecommittedPhase::AddressVariables => { + let bytecode_opening: F = (0..params.bytecode_chunk_count) + .map(|chunk_idx| { + params.chunk_rbc_weights[chunk_idx] + * accumulator + .get_committed_polynomial_opening( + CommittedPolynomial::BytecodeChunk(chunk_idx), + SumcheckId::BytecodeClaimReduction, + ) + .1 + }) + .sum(); + let eq_combined = evaluate_bytecode_eq_combined(¶ms, sumcheck_challenges); + let scale: F = precommitted_skip_round_scale(¶ms.precommitted); + + bytecode_opening * eq_combined * scale + } + } + } + + fn cache_openings( + &self, + accumulator: &mut VerifierOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let mut params = self.params.borrow_mut(); + if params.phase == PrecommittedPhase::CycleVariables { + let opening_point = params.normalize_opening_point(sumcheck_challenges); + accumulator.append_virtual( + VirtualPolynomial::BytecodeClaimReductionIntermediate, + SumcheckId::BytecodeClaimReductionCyclePhase, + opening_point.clone(), + ); + let opening_point_le: OpeningPoint = opening_point.match_endianness(); + params + .precommitted + .set_cycle_var_challenges(opening_point_le.r); + } + + if params.num_address_phase_rounds() == 0 + || params.phase == PrecommittedPhase::AddressVariables + { + let opening_point = params.normalize_opening_point(sumcheck_challenges); + for chunk_idx in 0..params.bytecode_chunk_count { + accumulator.append_dense( + CommittedPolynomial::BytecodeChunk(chunk_idx), + SumcheckId::BytecodeClaimReduction, + opening_point.r.clone(), + ); + } + } + } +} + +fn evaluate_bytecode_eq_combined( + params: &BytecodeClaimReductionParams, + sumcheck_challenges: &[F::Challenge], +) -> F { + let opening_point = params.normalize_opening_point(sumcheck_challenges); + let lane_var_count = committed_lanes().log_2(); + + let (lane_challenges, cycle_challenges) = match DoryGlobals::get_layout() { + DoryLayout::CycleMajor => { + let (lane, cycle) = opening_point.r.split_at(lane_var_count); + (lane, cycle) + } + DoryLayout::AddressMajor => { + let (cycle, lane) = opening_point.r.split_at(params.bytecode_T); + (lane, cycle) + } + }; + + debug_assert_eq!(lane_challenges.len(), lane_var_count); + debug_assert_eq!(cycle_challenges.len(), params.r_bc.r.len()); + + let eq_cycle = EqPolynomial::mle(cycle_challenges, ¶ms.r_bc.r); + let eq_lane = EqPolynomial::::evals(lane_challenges); + let lane_weight_eval: F = params + .lane_weights + .iter() + .zip(eq_lane.iter()) + .map(|(w, eq)| *w * *eq) + .sum(); + + lane_weight_eval * eq_cycle +} + +#[inline(always)] +fn native_index_to_lane_cycle( + params: &BytecodeClaimReductionParams, + index: usize, +) -> (usize, usize) { + let bytecode_len = 1usize << params.bytecode_T; + match DoryGlobals::get_layout() { + DoryLayout::CycleMajor => (index / bytecode_len, index % bytecode_len), + DoryLayout::AddressMajor => (index % committed_lanes(), index / committed_lanes()), + } +} + +fn compute_lane_weights( + bytecode_read_raf_params: &BytecodeReadRafSumcheckParams, + accumulator: &dyn OpeningAccumulator, + eta_powers: &[F; NUM_VAL_STAGES], +) -> Vec { + let reg_count = REGISTER_COUNT as usize; + let total = crate::zkvm::bytecode::chunks::total_lanes(); + + let rs1_start = 0usize; + let rs2_start = rs1_start + reg_count; + let rd_start = rs2_start + reg_count; + let unexp_pc_idx = rd_start + reg_count; + let imm_idx = unexp_pc_idx + 1; + let circuit_start = imm_idx + 1; + let instr_start = circuit_start + NUM_CIRCUIT_FLAGS; + let lookup_start = instr_start + NUM_INSTRUCTION_FLAGS; + let raf_flag_idx = lookup_start + LookupTables::::COUNT; + debug_assert_eq!(raf_flag_idx + 1, total); + + let log_reg = reg_count.log_2(); + let r_register_4 = accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::RdWa, + SumcheckId::RegistersReadWriteChecking, + ) + .0 + .r; + let eq_r_register_4 = EqPolynomial::::evals(&r_register_4[..log_reg]); + + let r_register_5 = accumulator + .get_virtual_polynomial_opening(VirtualPolynomial::RdWa, SumcheckId::RegistersValEvaluation) + .0 + .r; + let eq_r_register_5 = EqPolynomial::::evals(&r_register_5[..log_reg]); + + let mut weights = vec![F::zero(); committed_lanes()]; + + { + let coeff = eta_powers[0]; + let g = &bytecode_read_raf_params.stage1_gammas; + weights[unexp_pc_idx] += coeff * g[0]; + weights[imm_idx] += coeff * g[1]; + for i in 0..NUM_CIRCUIT_FLAGS { + weights[circuit_start + i] += coeff * g[2 + i]; + } + } + { + let coeff = eta_powers[1]; + let g = &bytecode_read_raf_params.stage2_gammas; + weights[circuit_start + (CircuitFlags::Jump as usize)] += coeff * g[0]; + weights[instr_start + (InstructionFlags::Branch as usize)] += coeff * g[1]; + weights[circuit_start + (CircuitFlags::WriteLookupOutputToRD as usize)] += coeff * g[2]; + weights[circuit_start + (CircuitFlags::VirtualInstruction as usize)] += coeff * g[3]; + } + { + let coeff = eta_powers[2]; + let g = &bytecode_read_raf_params.stage3_gammas; + weights[imm_idx] += coeff * g[0]; + weights[unexp_pc_idx] += coeff * g[1]; + weights[instr_start + (InstructionFlags::LeftOperandIsRs1Value as usize)] += coeff * g[2]; + weights[instr_start + (InstructionFlags::LeftOperandIsPC as usize)] += coeff * g[3]; + weights[instr_start + (InstructionFlags::RightOperandIsRs2Value as usize)] += coeff * g[4]; + weights[instr_start + (InstructionFlags::RightOperandIsImm as usize)] += coeff * g[5]; + weights[instr_start + (InstructionFlags::IsNoop as usize)] += coeff * g[6]; + weights[circuit_start + (CircuitFlags::VirtualInstruction as usize)] += coeff * g[7]; + weights[circuit_start + (CircuitFlags::IsFirstInSequence as usize)] += coeff * g[8]; + } + { + let coeff = eta_powers[3]; + let g = &bytecode_read_raf_params.stage4_gammas; + for r in 0..reg_count { + weights[rd_start + r] += coeff * g[0] * eq_r_register_4[r]; + weights[rs1_start + r] += coeff * g[1] * eq_r_register_4[r]; + weights[rs2_start + r] += coeff * g[2] * eq_r_register_4[r]; + } + } + { + let coeff = eta_powers[4]; + let g = &bytecode_read_raf_params.stage5_gammas; + for r in 0..reg_count { + weights[rd_start + r] += coeff * g[0] * eq_r_register_5[r]; + } + weights[raf_flag_idx] += coeff * g[1]; + for i in 0..LookupTables::::COUNT { + weights[lookup_start + i] += coeff * g[2 + i]; + } + } + + weights +} diff --git a/jolt-core/src/zkvm/claim_reductions/hamming_weight.rs b/jolt-core/src/zkvm/claim_reductions/hamming_weight.rs index 91c2ff3f43..fd874fabd4 100644 --- a/jolt-core/src/zkvm/claim_reductions/hamming_weight.rs +++ b/jolt-core/src/zkvm/claim_reductions/hamming_weight.rs @@ -105,6 +105,7 @@ use crate::subprotocols::{ use crate::transcripts::Transcript; use crate::zkvm::{ config::OneHotParams, + program::ProgramPreprocessing, verifier::JoltSharedPreprocessing, witness::{CommittedPolynomial, VirtualPolynomial}, }; @@ -428,13 +429,14 @@ impl HammingWeightClaimReductionProver { params: HammingWeightClaimReductionParams, trace: &[Cycle], preprocessing: &JoltSharedPreprocessing, + program: &ProgramPreprocessing, one_hot_params: &OneHotParams, ) -> Self { // Compute all G_i polynomials via streaming. // `params.r_cycle` is in BIG_ENDIAN (OpeningPoint) convention. let G_vecs = compute_all_G::( trace, - &preprocessing.bytecode, + &program.bytecode, &preprocessing.memory_layout, one_hot_params, ¶ms.r_cycle, diff --git a/jolt-core/src/zkvm/claim_reductions/mod.rs b/jolt-core/src/zkvm/claim_reductions/mod.rs index b68d052551..787f5d6efa 100644 --- a/jolt-core/src/zkvm/claim_reductions/mod.rs +++ b/jolt-core/src/zkvm/claim_reductions/mod.rs @@ -1,8 +1,10 @@ pub mod advice; +pub mod bytecode; pub mod hamming_weight; pub mod increments; pub mod instruction_lookups; mod precommitted; +pub mod program_image; pub mod ram_ra; pub mod registers; @@ -10,6 +12,9 @@ pub use advice::{ AdviceClaimReductionParams, AdviceClaimReductionProver, AdviceClaimReductionVerifier, AdviceKind, }; +pub use bytecode::{ + BytecodeClaimReductionParams, BytecodeClaimReductionProver, BytecodeClaimReductionVerifier, +}; pub use hamming_weight::{ HammingWeightClaimReductionParams, HammingWeightClaimReductionProver, HammingWeightClaimReductionVerifier, @@ -27,6 +32,10 @@ pub use precommitted::{ PrecomittedParams, PrecomittedProver, PrecommittedClaimReduction, PrecommittedEmbeddingMode, PrecommittedPhase, PrecommittedSchedulingReference, TWO_PHASE_DEGREE_BOUND, }; +pub use program_image::{ + ProgramImageClaimReductionParams, ProgramImageClaimReductionProver, + ProgramImageClaimReductionVerifier, +}; pub use ram_ra::{ RaReductionParams, RamRaClaimReductionSumcheckProver, RamRaClaimReductionSumcheckVerifier, }; diff --git a/jolt-core/src/zkvm/claim_reductions/program_image.rs b/jolt-core/src/zkvm/claim_reductions/program_image.rs new file mode 100644 index 0000000000..438cbec072 --- /dev/null +++ b/jolt-core/src/zkvm/claim_reductions/program_image.rs @@ -0,0 +1,450 @@ +//! Program-image (initial RAM) claim reduction. +//! +//! In committed bytecode mode, Stage 4 consumes prover-supplied scalar claims for the +//! program-image contribution to `Val_init(r_address)` without materializing the initial RAM. +//! This sumcheck binds those scalars to a trusted commitment to the program-image words polynomial. + +use allocative::Allocative; +use std::cell::RefCell; + +use crate::field::JoltField; +use crate::poly::commitment::dory::DoryGlobals; +use crate::poly::eq_poly::EqPolynomial; +use crate::poly::multilinear_polynomial::MultilinearPolynomial; +use crate::poly::opening_proof::{ + OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, SumcheckId, + VerifierOpeningAccumulator, BIG_ENDIAN, LITTLE_ENDIAN, +}; +use crate::poly::unipoly::UniPoly; +use crate::subprotocols::sumcheck_prover::SumcheckInstanceProver; +use crate::subprotocols::sumcheck_verifier::{SumcheckInstanceParams, SumcheckInstanceVerifier}; +use crate::transcripts::Transcript; +use crate::utils::math::Math; +use crate::zkvm::claim_reductions::{ + permute_precommitted_polys, precommitted_eq_evals_with_scaling, precommitted_skip_round_scale, + PrecomittedParams, PrecomittedProver, PrecommittedClaimReduction, PrecommittedPhase, + PrecommittedSchedulingReference, TWO_PHASE_DEGREE_BOUND, +}; +use crate::zkvm::ram::remap_address; +use crate::zkvm::witness::{CommittedPolynomial, VirtualPolynomial}; +use tracer::JoltDevice; + +fn debug_program_image_reduction_enabled() -> bool { + std::env::var("JOLT_DEBUG_PROGRAM_IMAGE_REDUCTION") + .map(|v| { + let value = v.trim().to_ascii_lowercase(); + !matches!(value.as_str(), "" | "0" | "false" | "off") + }) + .unwrap_or(false) +} + +#[derive(Clone, Allocative)] +pub struct ProgramImageClaimReductionParams { + pub phase: PrecommittedPhase, + pub precommitted: PrecommittedClaimReduction, + pub log_t: usize, + pub prog_col_vars: usize, + pub prog_row_vars: usize, + pub ram_num_vars: usize, + pub start_index: usize, + pub padded_len_words: usize, + pub m: usize, + pub r_addr_rw: Vec, + pub r_addr_rw_reduced: Vec, + pub selector_rw: F, +} + +impl ProgramImageClaimReductionParams { + pub fn num_address_phase_rounds(&self) -> usize { + self.precommitted.num_address_phase_rounds() + } + + #[allow(clippy::too_many_arguments)] + pub fn new( + program_io: &JoltDevice, + ram_min_bytecode_address: u64, + padded_len_words: usize, + ram_K: usize, + scheduling_reference: PrecommittedSchedulingReference, + accumulator: &dyn OpeningAccumulator, + _transcript: &mut impl Transcript, + ) -> Self { + let ram_num_vars = ram_K.log_2(); + let start_index = + remap_address(ram_min_bytecode_address, &program_io.memory_layout).unwrap() as usize; + let m = padded_len_words.log_2(); + debug_assert!(padded_len_words.is_power_of_two()); + debug_assert!(padded_len_words > 0); + let (prog_col_vars, prog_row_vars) = DoryGlobals::balanced_sigma_nu(m); + let log_t = DoryGlobals::main_t().log_2(); + let total_vars = prog_row_vars + prog_col_vars; + let precommitted = PrecommittedClaimReduction::new( + total_vars, + prog_row_vars, + prog_col_vars, + scheduling_reference, + ); + + let (r_rw, _) = accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::RamVal, + SumcheckId::RamReadWriteChecking, + ); + let (r_addr_rw, _) = r_rw.split_at(ram_num_vars); + let (r_addr_rw_reduced, selector_rw) = top_left_program_image_point_and_selector::( + &r_addr_rw.r, + start_index, + padded_len_words, + ); + + Self { + phase: PrecommittedPhase::CycleVariables, + precommitted, + log_t, + prog_col_vars, + prog_row_vars, + ram_num_vars, + start_index, + padded_len_words, + m, + r_addr_rw: r_addr_rw.r, + r_addr_rw_reduced, + selector_rw, + } + } +} + +impl ProgramImageClaimReductionParams { + fn is_cycle_phase(&self) -> bool { + self.phase == PrecommittedPhase::CycleVariables + } + + pub fn transition_to_address_phase(&mut self) { + self.phase = PrecommittedPhase::AddressVariables; + } + + pub fn round_offset(&self, max_num_rounds: usize) -> usize { + self.precommitted + .round_offset(self.is_cycle_phase(), max_num_rounds) + } +} + +impl SumcheckInstanceParams for ProgramImageClaimReductionParams { + fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { + let claim = match self.phase { + PrecommittedPhase::CycleVariables => { + // Scalar claims were staged in Stage 4 as virtual openings. + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::ProgramImageInitContributionRw, + SumcheckId::RamValCheck, + ) + .1 + } + PrecommittedPhase::AddressVariables => { + accumulator + .get_committed_polynomial_opening( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReductionCyclePhase, + ) + .1 + } + }; + if debug_program_image_reduction_enabled() { + tracing::info!( + "ProgramImageClaimReduction input_claim phase={} claim={}", + if self.phase == PrecommittedPhase::CycleVariables { + "cycle" + } else { + "address" + }, + claim + ); + } + claim + } + + fn degree(&self) -> usize { + TWO_PHASE_DEGREE_BOUND + } + + fn num_rounds(&self) -> usize { + self.precommitted + .num_rounds_for_phase(self.is_cycle_phase()) + } + + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + self.precommitted + .normalize_opening_point(self.is_cycle_phase(), challenges, self.log_t) + } +} + +impl PrecomittedParams for ProgramImageClaimReductionParams { + fn is_cycle_phase(&self) -> bool { + self.phase == PrecommittedPhase::CycleVariables + } + + fn is_cycle_phase_round(&self, round: usize) -> bool { + self.precommitted.is_cycle_phase_round(round) + } + + fn is_address_phase_round(&self, round: usize) -> bool { + self.precommitted.is_address_phase_round(round) + } + + fn cycle_alignment_rounds(&self) -> usize { + self.precommitted.cycle_alignment_rounds() + } + + fn address_alignment_rounds(&self) -> usize { + self.precommitted.address_alignment_rounds() + } + + fn record_cycle_challenge(&mut self, challenge: F::Challenge) { + self.precommitted.record_cycle_challenge(challenge); + } +} + +#[derive(Allocative)] +pub struct ProgramImageClaimReductionProver { + core: PrecomittedProver>, +} + +fn top_left_program_image_point_and_selector( + r_addr: &[F::Challenge], + start_index: usize, + padded_len_words: usize, +) -> (Vec, F) { + assert!( + padded_len_words.is_power_of_two() && padded_len_words > 0, + "padded_len_words must be a non-zero power of two" + ); + assert_eq!( + start_index % padded_len_words, + 0, + "program-image block must be aligned to padded_len_words for top-left embedding" + ); + + let m = padded_len_words.log_2(); + assert!( + m <= r_addr.len(), + "program-image variable count exceeds RAM address variable count" + ); + let prefix_len = r_addr.len() - m; + let start_prefix = start_index / padded_len_words; + + let mut selector = F::one(); + for (i, r_i) in r_addr[..prefix_len].iter().enumerate() { + let bit_index = prefix_len - 1 - i; + let prefix_bit = (start_prefix >> bit_index) & 1; + let r_i_f: F = (*r_i).into(); + selector *= if prefix_bit == 1 { + r_i_f + } else { + F::one() - r_i_f + }; + } + + (r_addr[prefix_len..].to_vec(), selector) +} + +impl ProgramImageClaimReductionProver { + pub fn params(&self) -> &ProgramImageClaimReductionParams { + self.core.params() + } + + pub fn transition_to_address_phase(&mut self) { + self.core.params_mut().transition_to_address_phase(); + } + + #[tracing::instrument(skip_all, name = "ProgramImageClaimReductionProver::initialize")] + pub fn initialize( + params: ProgramImageClaimReductionParams, + program_image_words_padded: Vec, + ) -> Self { + debug_assert_eq!(program_image_words_padded.len(), params.padded_len_words); + debug_assert_eq!(params.padded_len_words, 1usize << params.m); + + let eq_evals = precommitted_eq_evals_with_scaling( + ¶ms.r_addr_rw_reduced, + Some(params.selector_rw), + ¶ms.precommitted, + ); + + // Permute ProgramWord and eq_slice so low-to-high binding follows the two-phase + // schedule while preserving top-left projection semantics against the joint point. + let (program_word, eq_slice): (MultilinearPolynomial, MultilinearPolynomial) = { + let mut permuted = + permute_precommitted_polys(vec![program_image_words_padded], ¶ms.precommitted) + .into_iter(); + let program_word = permuted + .next() + .expect("expected one permuted program image polynomial"); + let eq_slice = eq_evals.into(); + (program_word, eq_slice) + }; + + Self { + core: PrecomittedProver::new(params, program_word, eq_slice), + } + } +} + +impl SumcheckInstanceProver + for ProgramImageClaimReductionProver +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + self.core.params() + } + + fn round_offset(&self, max_num_rounds: usize) -> usize { + self.core.params().round_offset(max_num_rounds) + } + + fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { + self.core.compute_message(round, previous_claim) + } + + fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { + self.core.ingest_challenge(r_j, round); + } + + fn cache_openings( + &self, + accumulator: &mut ProverOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let params = self.core.params(); + let opening_point = params.normalize_opening_point(sumcheck_challenges); + if params.phase == PrecommittedPhase::CycleVariables { + let c_mid = self.core.cycle_intermediate_claim(); + if debug_program_image_reduction_enabled() { + tracing::info!( + "ProgramImageClaimReduction prover cycle output claim={}", + c_mid + ); + } + accumulator.append_dense( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReductionCyclePhase, + // This is a phase-boundary intermediate claim, not a real program-image opening. + // Keep a sentinel point so it cannot alias with the final opening claim. + vec![], + c_mid, + ); + } + + if let Some(claim) = self.core.final_claim_if_ready() { + accumulator.append_dense( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReduction, + opening_point.r, + claim, + ); + } + } + + #[cfg(feature = "allocative")] + fn update_flamegraph(&self, flamegraph: &mut allocative::FlameGraphBuilder) { + flamegraph.visit_root(self); + } +} + +pub struct ProgramImageClaimReductionVerifier { + pub params: RefCell>, +} + +impl ProgramImageClaimReductionVerifier { + pub fn new(params: ProgramImageClaimReductionParams) -> Self { + Self { + params: RefCell::new(params), + } + } +} + +impl SumcheckInstanceVerifier + for ProgramImageClaimReductionVerifier +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + unsafe { &*self.params.as_ptr() } + } + + fn round_offset(&self, max_num_rounds: usize) -> usize { + let params = self.params.borrow(); + params.round_offset(max_num_rounds) + } + + fn expected_output_claim( + &self, + accumulator: &VerifierOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) -> F { + let params = self.params.borrow(); + let claim = match params.phase { + PrecommittedPhase::CycleVariables => { + accumulator + .get_committed_polynomial_opening( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReductionCyclePhase, + ) + .1 + } + PrecommittedPhase::AddressVariables => { + let opening_point = params.normalize_opening_point(sumcheck_challenges); + debug_assert_eq!(opening_point.len(), params.m); + let pw_eval = accumulator + .get_committed_polynomial_opening( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReduction, + ) + .1; + let eq_combined = params.selector_rw + * EqPolynomial::mle(&opening_point.r, ¶ms.r_addr_rw_reduced); + let scale: F = precommitted_skip_round_scale(¶ms.precommitted); + pw_eval * eq_combined * scale + } + }; + if debug_program_image_reduction_enabled() { + tracing::info!( + "ProgramImageClaimReduction verifier expected_output phase={} claim={}", + if params.phase == PrecommittedPhase::CycleVariables { + "cycle" + } else { + "address" + }, + claim + ); + } + claim + } + + fn cache_openings( + &self, + accumulator: &mut VerifierOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let mut params = self.params.borrow_mut(); + let opening_point = params.normalize_opening_point(sumcheck_challenges); + if params.phase == PrecommittedPhase::CycleVariables { + accumulator.append_dense( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReductionCyclePhase, + // Match prover behavior: the cycle-phase intermediate claim is not a real opening. + vec![], + ); + let opening_point_le: OpeningPoint = opening_point.match_endianness(); + params + .precommitted + .set_cycle_var_challenges(opening_point_le.r); + } + + if params.phase == PrecommittedPhase::AddressVariables + || params.num_address_phase_rounds() == 0 + { + accumulator.append_dense( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReduction, + opening_point.r, + ); + } + } +} diff --git a/jolt-core/src/zkvm/config.rs b/jolt-core/src/zkvm/config.rs index 59d6b29d29..02e2c55e68 100644 --- a/jolt-core/src/zkvm/config.rs +++ b/jolt-core/src/zkvm/config.rs @@ -1,5 +1,8 @@ use allocative::Allocative; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_serialize::{ + CanonicalDeserialize, CanonicalSerialize, Compress, SerializationError, Valid, Validate, +}; +use std::io::{Read, Write}; use crate::field::JoltField; use crate::utils::math::Math; @@ -20,6 +23,52 @@ pub fn get_instruction_sumcheck_phases(log_t: usize) -> usize { } } +/// Controls how bytecode and program-image data are handled by the verifier. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Allocative, Default)] +pub enum ProgramMode { + /// Verifier has full bytecode and program image available. + #[default] + Full = 0, + /// Verifier uses commitments for bytecode/program-image openings in Stage 8. + Committed = 1, +} + +impl CanonicalSerialize for ProgramMode { + fn serialize_with_mode( + &self, + writer: W, + compress: Compress, + ) -> Result<(), SerializationError> { + (*self as u8).serialize_with_mode(writer, compress) + } + + fn serialized_size(&self, compress: Compress) -> usize { + (*self as u8).serialized_size(compress) + } +} + +impl Valid for ProgramMode { + fn check(&self) -> Result<(), SerializationError> { + Ok(()) + } +} + +impl CanonicalDeserialize for ProgramMode { + fn deserialize_with_mode( + reader: R, + compress: Compress, + validate: Validate, + ) -> Result { + let value = u8::deserialize_with_mode(reader, compress, validate)?; + match value { + 0 => Ok(Self::Full), + 1 => Ok(Self::Committed), + _ => Err(SerializationError::InvalidData), + } + } +} + /// Configuration for read-write checking sumchecks. /// /// Contains parameters that control phase structure for RAM and register @@ -91,15 +140,6 @@ impl ReadWriteConfig { } Ok(()) } - - /// Returns true if all cycle variables are bound in phase 1. - /// - /// When this returns true, the advice opening points for `RamValCheck` and - /// `RamValCheck` are identical, so we only need one advice opening. - #[inline] - pub fn needs_single_advice_opening(&self, log_T: usize) -> bool { - self.ram_rw_phase1_num_rounds as usize == log_T - } } /// Minimal configuration for one-hot encoding that gets serialized in the proof. diff --git a/jolt-core/src/zkvm/mod.rs b/jolt-core/src/zkvm/mod.rs index d9ef947104..2699581e11 100644 --- a/jolt-core/src/zkvm/mod.rs +++ b/jolt-core/src/zkvm/mod.rs @@ -1,6 +1,6 @@ use std::fs::File; -use crate::zkvm::config::OneHotParams; +use crate::zkvm::config::{OneHotParams, ProgramMode}; use crate::zkvm::witness::CommittedPolynomial; use crate::{ curve::Bn254Curve, @@ -30,6 +30,7 @@ pub mod config; pub mod instruction; pub mod instruction_lookups; pub mod lookup_table; +pub mod program; pub mod proof_serialization; #[cfg(feature = "prover")] pub mod prover; @@ -40,10 +41,50 @@ pub mod spartan; pub mod verifier; pub mod witness; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Stage8ProgramOpenings { + Both, + Bytecode, + ProgramImage, + None, +} + +impl Stage8ProgramOpenings { + pub(crate) fn includes_bytecode(self) -> bool { + matches!(self, Self::Both | Self::Bytecode) + } + + pub(crate) fn includes_program_image(self) -> bool { + matches!(self, Self::Both | Self::ProgramImage) + } +} + +pub(crate) fn stage8_program_openings_from_env() -> Stage8ProgramOpenings { + let Ok(raw) = std::env::var("JOLT_STAGE8_PROGRAM_OPENINGS") else { + return Stage8ProgramOpenings::Both; + }; + + match raw.trim().to_ascii_lowercase().as_str() { + "" | "both" => Stage8ProgramOpenings::Both, + "bytecode" => Stage8ProgramOpenings::Bytecode, + "program_image" | "program-image" => Stage8ProgramOpenings::ProgramImage, + "none" => Stage8ProgramOpenings::None, + other => { + tracing::warn!( + "Unrecognized JOLT_STAGE8_PROGRAM_OPENINGS value `{other}`; defaulting to `both`" + ); + Stage8ProgramOpenings::Both + } + } +} + pub(crate) fn stage8_opening_ids( one_hot_params: &OneHotParams, include_trusted_advice: bool, include_untrusted_advice: bool, + program_mode: ProgramMode, + bytecode_chunk_count: usize, + stage8_program_openings: Stage8ProgramOpenings, ) -> Vec { let mut opening_ids = Vec::new(); @@ -81,6 +122,20 @@ pub(crate) fn stage8_opening_ids( if include_untrusted_advice { opening_ids.push(OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReduction)); } + if program_mode == ProgramMode::Committed && stage8_program_openings.includes_bytecode() { + for i in 0..bytecode_chunk_count { + opening_ids.push(OpeningId::committed( + CommittedPolynomial::BytecodeChunk(i), + SumcheckId::BytecodeClaimReduction, + )); + } + } + if program_mode == ProgramMode::Committed && stage8_program_openings.includes_program_image() { + opening_ids.push(OpeningId::committed( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReduction, + )); + } opening_ids } diff --git a/jolt-core/src/zkvm/proof_serialization.rs b/jolt-core/src/zkvm/proof_serialization.rs index 071d498a0e..7d480401ed 100644 --- a/jolt-core/src/zkvm/proof_serialization.rs +++ b/jolt-core/src/zkvm/proof_serialization.rs @@ -274,13 +274,25 @@ impl CanonicalSerialize for CommittedPolynomial { } Self::TrustedAdvice => 5u8.serialize_with_mode(writer, compress), Self::UntrustedAdvice => 6u8.serialize_with_mode(writer, compress), + Self::BytecodeChunk(i) => { + 7u8.serialize_with_mode(&mut writer, compress)?; + (u8::try_from(*i).unwrap()).serialize_with_mode(writer, compress) + } + Self::ProgramImageInit => 8u8.serialize_with_mode(writer, compress), } } fn serialized_size(&self, _compress: Compress) -> usize { match self { - Self::RdInc | Self::RamInc | Self::TrustedAdvice | Self::UntrustedAdvice => 1, - Self::InstructionRa(_) | Self::BytecodeRa(_) | Self::RamRa(_) => 2, + Self::RdInc + | Self::RamInc + | Self::TrustedAdvice + | Self::UntrustedAdvice + | Self::ProgramImageInit => 1, + Self::InstructionRa(_) + | Self::BytecodeRa(_) + | Self::RamRa(_) + | Self::BytecodeChunk(_) => 2, } } } @@ -315,6 +327,11 @@ impl CanonicalDeserialize for CommittedPolynomial { } 5 => Self::TrustedAdvice, 6 => Self::UntrustedAdvice, + 7 => { + let i = u8::deserialize_with_mode(reader, compress, validate)?; + Self::BytecodeChunk(i as usize) + } + 8 => Self::ProgramImageInit, _ => return Err(SerializationError::InvalidData), }, ) @@ -381,6 +398,17 @@ impl CanonicalSerialize for VirtualPolynomial { } Self::BytecodeReadRafAddrClaim => 39u8.serialize_with_mode(&mut writer, compress), Self::BooleanityAddrClaim => 40u8.serialize_with_mode(&mut writer, compress), + Self::BytecodeValStage(i) => { + 41u8.serialize_with_mode(&mut writer, compress)?; + (u8::try_from(*i).unwrap()).serialize_with_mode(&mut writer, compress) + } + Self::BytecodeClaimReductionIntermediate => { + 42u8.serialize_with_mode(&mut writer, compress) + } + Self::ProgramImageInitContributionRw => 43u8.serialize_with_mode(&mut writer, compress), + Self::ProgramImageInitContributionRaf => { + 44u8.serialize_with_mode(&mut writer, compress) + } } } @@ -422,11 +450,15 @@ impl CanonicalSerialize for VirtualPolynomial { | Self::RamHammingWeight | Self::UnivariateSkip | Self::BytecodeReadRafAddrClaim - | Self::BooleanityAddrClaim => 1, + | Self::BooleanityAddrClaim + | Self::BytecodeClaimReductionIntermediate + | Self::ProgramImageInitContributionRw + | Self::ProgramImageInitContributionRaf => 1, Self::InstructionRa(_) | Self::OpFlags(_) | Self::InstructionFlags(_) - | Self::LookupTableFlag(_) => 2, + | Self::LookupTableFlag(_) + | Self::BytecodeValStage(_) => 2, } } } @@ -502,6 +534,13 @@ impl CanonicalDeserialize for VirtualPolynomial { } 39 => Self::BytecodeReadRafAddrClaim, 40 => Self::BooleanityAddrClaim, + 41 => { + let i = u8::deserialize_with_mode(&mut reader, compress, validate)?; + Self::BytecodeValStage(i as usize) + } + 42 => Self::BytecodeClaimReductionIntermediate, + 43 => Self::ProgramImageInitContributionRw, + 44 => Self::ProgramImageInitContributionRaf, _ => return Err(SerializationError::InvalidData), }, ) diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 13be300147..4a7ef109b9 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -2,6 +2,7 @@ use crate::poly::opening_proof::OpeningId; #[cfg(feature = "zk")] use crate::zkvm::stage8_opening_ids; +use crate::zkvm::stage8_program_openings_from_env; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; use std::{ @@ -19,7 +20,10 @@ use crate::poly::commitment::dory::bind_opening_inputs_zk; use crate::poly::commitment::dory::DoryContext; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +#[cfg(feature = "zk")] +use crate::zkvm::config::ProgramMode; use crate::zkvm::config::ReadWriteConfig; +use crate::zkvm::program::{ProgramPreprocessing, TrustedProgramCommitments, TrustedProgramHints}; use crate::zkvm::ram::remap_address; use crate::zkvm::verifier::JoltSharedPreprocessing; use crate::zkvm::Serializable; @@ -36,7 +40,7 @@ use crate::{ commitment_scheme::{StreamingCommitmentScheme, ZkEvalCommitment}, dory::{DoryGlobals, DoryLayout}, }, - multilinear_polynomial::MultilinearPolynomial, + multilinear_polynomial::{MultilinearPolynomial, PolynomialEvaluation}, opening_proof::{ compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, SumcheckId, BIG_ENDIAN, @@ -52,20 +56,27 @@ use crate::{ streaming_schedule::LinearOnlySchedule, sumcheck::{BatchedSumcheck, SumcheckInstanceProof}, sumcheck_prover::SumcheckInstanceProver, + sumcheck_verifier::SumcheckInstanceParams, univariate_skip::UniSkipFirstRoundProofVariant, }, transcripts::Transcript, utils::{math::Math, thread::drop_in_background_thread}, zkvm::{ - bytecode::read_raf_checking::BytecodeReadRafSumcheckParams, + bytecode::{ + chunks::{build_committed_bytecode_chunk_polynomials, committed_lanes}, + read_raf_checking::BytecodeReadRafSumcheckParams, + TrustedBytecodeCommitments, + }, claim_reductions::{ AdviceClaimReductionParams, AdviceClaimReductionProver, AdviceKind, + BytecodeClaimReductionParams, BytecodeClaimReductionProver, HammingWeightClaimReductionParams, HammingWeightClaimReductionProver, IncClaimReductionSumcheckParams, IncClaimReductionSumcheckProver, InstructionLookupsClaimReductionSumcheckParams, InstructionLookupsClaimReductionSumcheckProver, PrecommittedClaimReduction, - RaReductionParams, RamRaClaimReductionSumcheckProver, - RegistersClaimReductionSumcheckParams, RegistersClaimReductionSumcheckProver, + ProgramImageClaimReductionParams, ProgramImageClaimReductionProver, RaReductionParams, + RamRaClaimReductionSumcheckProver, RegistersClaimReductionSumcheckParams, + RegistersClaimReductionSumcheckProver, }, config::OneHotParams, instruction_lookups::{ @@ -75,7 +86,7 @@ use crate::{ ram::{ hamming_booleanity::HammingBooleanitySumcheckParams, output_check::OutputSumcheckParams, - populate_memory_states, + populate_memory_states, prover_accumulate_program_image, ra_virtual::RamRaVirtualParams, raf_evaluation::RafEvaluationSumcheckParams, read_write_checking::RamReadWriteCheckingParams, @@ -166,6 +177,50 @@ use crate::zkvm::r1cs::constraints::{ #[cfg(feature = "zk")] use crate::zkvm::verifier::BlindfoldSetup; +pub(crate) fn derive_poly_source_point_from_matrix_dims( + stage8_opening_point: &OpeningPoint, + poly_num_rows: usize, + poly_num_columns: usize, +) -> OpeningPoint { + assert!( + poly_num_rows.is_power_of_two() && poly_num_columns.is_power_of_two(), + "polynomial matrix dimensions must be powers of two (rows={poly_num_rows}, cols={poly_num_columns})" + ); + let nu_poly = poly_num_rows.log_2(); + let sigma_poly = poly_num_columns.log_2(); + let nu_full = DoryGlobals::get_max_num_rows().log_2(); + let sigma_full = DoryGlobals::get_num_columns().log_2(); + assert!( + sigma_poly <= sigma_full && nu_poly <= nu_full, + "top-left projection requires poly dims <= full dims (poly sigma/nu={sigma_poly}/{nu_poly}, full sigma/nu={sigma_full}/{nu_full})" + ); + + // Dimension-only projection: + // - Treat full point as [row_variables || column_variables] + // - For target dims (nu_poly rows, sigma_poly cols), take tails: + // [last nu_poly row vars || last sigma_poly col vars] + let row_be = &stage8_opening_point.r[..nu_full]; + let col_be = &stage8_opening_point.r[nu_full..nu_full + sigma_full]; + let row_tail = &row_be[nu_full - nu_poly..]; + let col_tail = &col_be[sigma_full - sigma_poly..]; + + let mut projected = Vec::with_capacity(nu_poly + sigma_poly); + projected.extend_from_slice(row_tail); + projected.extend_from_slice(col_tail); + OpeningPoint::::new(projected) +} + +#[inline] +pub(crate) fn derive_poly_source_point_from_dory_dims( + stage8_opening_point: &OpeningPoint, + poly_num_vars: usize, +) -> OpeningPoint { + let (sigma_poly, nu_poly) = DoryGlobals::balanced_sigma_nu(poly_num_vars); + let poly_num_rows = 1usize << nu_poly; + let poly_num_columns = 1usize << sigma_poly; + derive_poly_source_point_from_matrix_dims(stage8_opening_point, poly_num_rows, poly_num_columns) +} + /// Jolt CPU prover for RV64IMAC. pub struct JoltCpuProver< 'a, @@ -185,6 +240,10 @@ pub struct JoltCpuProver< /// The advice claim reduction sumcheck effectively spans two stages (6 and 7). /// Cache the prover state here between stages. advice_reduction_prover_untrusted: Option>, + /// Bytecode claim reduction spans stages 6b and 7 in committed mode. + bytecode_reduction_prover: Option>, + /// Program-image claim reduction spans stages 6b and 7 in committed mode. + program_image_reduction_prover: Option>, pub unpadded_trace_len: usize, pub padded_trace_len: usize, pub transcript: ProofTranscript, @@ -291,6 +350,14 @@ impl< #[inline] fn precommitted_candidate_total_vars(&self) -> Vec { let mut candidates = Vec::new(); + if self.preprocessing.is_committed_mode() { + let bytecode_t_full = self.preprocessing.shared.bytecode_size().log_2(); + let chunk_log = self.preprocessing.shared.bytecode_chunk_count.log_2(); + let chunk_cycle_log_t = bytecode_t_full.saturating_sub(chunk_log); + candidates.push(committed_lanes().log_2() + chunk_cycle_log_t); + let program_image_words = self.preprocessing.shared.program_image_len_words().max(1); + candidates.push(program_image_words.next_power_of_two().log_2()); + } if !self.program_io.trusted_advice.is_empty() { let (sigma, nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( self.program_io.memory_layout.max_trusted_advice_size as usize, @@ -307,20 +374,50 @@ impl< candidates } + #[inline] + fn include_bytecode_in_stage8(&self) -> bool { + self.preprocessing.is_committed_mode() + && stage8_program_openings_from_env().includes_bytecode() + } + + #[inline] + fn include_program_image_in_stage8(&self) -> bool { + self.preprocessing.is_committed_mode() + && stage8_program_openings_from_env().includes_program_image() + } + fn stage8_opening_point(&self) -> OpeningPoint { let native_main_vars = self.trace.len().log_2() + self.one_hot_params.log_k_chunk; - let mut opening_candidates: Vec<(&str, OpeningPoint)> = Vec::new(); + let debug_stage8_point = Self::stage8_debug_enabled(); + let mut opening_candidates: Vec<(String, OpeningPoint)> = Vec::new(); if let Some((point, _)) = self .opening_accumulator .get_advice_opening(AdviceKind::Trusted, SumcheckId::AdviceClaimReduction) { - opening_candidates.push(("trusted_advice", point)); + opening_candidates.push(("trusted_advice".to_string(), point)); } if let Some((point, _)) = self .opening_accumulator .get_advice_opening(AdviceKind::Untrusted, SumcheckId::AdviceClaimReduction) { - opening_candidates.push(("untrusted_advice", point)); + opening_candidates.push(("untrusted_advice".to_string(), point)); + } + if self.include_bytecode_in_stage8() { + for chunk_idx in 0..self.preprocessing.shared.bytecode_chunk_count { + let (point, _) = self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::BytecodeChunk(chunk_idx), + SumcheckId::BytecodeClaimReduction, + ); + opening_candidates.push((format!("bytecode_chunk[{chunk_idx}]"), point)); + } + } + if self.include_program_image_in_stage8() { + let (program_image_point, _) = + self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReduction, + ); + opening_candidates.push(("program_image".to_string(), program_image_point)); } let max_len = opening_candidates @@ -328,7 +425,7 @@ impl< .map(|(_, p)| p.r.len()) .max() .unwrap_or(0); - if max_len > native_main_vars { + let final_point = if max_len > native_main_vars { let dominant = opening_candidates .iter() .find(|(_, p)| p.r.len() == max_len) @@ -343,14 +440,407 @@ impl< dominant.0, name, max_len ); } + if debug_stage8_point { + tracing::info!( + "Stage8 opening point: dominant polynomial anchor = {} (len={})", + dominant.0, + max_len + ); + } OpeningPoint::::new(dominant.1.r.clone()) } else { - self.opening_accumulator + let (hamming_point, _) = self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::InstructionRa(0), + SumcheckId::HammingWeightClaimReduction, + ); + let r_address_stage7 = hamming_point.r[..self.one_hot_params.log_k_chunk].to_vec(); + let r_cycle_stage6 = self + .opening_accumulator .get_committed_polynomial_opening( - CommittedPolynomial::InstructionRa(0), - SumcheckId::HammingWeightClaimReduction, + CommittedPolynomial::RamInc, + SumcheckId::IncClaimReduction, ) .0 + .r; + + match DoryGlobals::get_layout() { + DoryLayout::AddressMajor => OpeningPoint::::new( + [r_cycle_stage6.as_slice(), r_address_stage7.as_slice()].concat(), + ), + DoryLayout::CycleMajor => { + let native_cycle = &hamming_point.r[self.one_hot_params.log_k_chunk..]; + assert!( + r_cycle_stage6.len() >= native_cycle.len(), + "stage6 cycle challenges shorter than native cycle vars" + ); + assert!( + r_cycle_stage6[..native_cycle.len()] == *native_cycle, + "cycle-major Stage-8 expects stage6 cycle prefix to equal native cycle vars \ + (cycle_full_len={}, native_len={})", + r_cycle_stage6.len(), + native_cycle.len() + ); + let cycle_extra = &r_cycle_stage6[native_cycle.len()..]; + let cycle_extra_and_anchor = + [cycle_extra, r_address_stage7.as_slice(), native_cycle].concat(); + OpeningPoint::::new(cycle_extra_and_anchor) + } + } + }; + + if debug_stage8_point { + if max_len <= native_main_vars { + tracing::info!( + "Stage8 opening point: no dominant precommitted polynomial (max_candidate_len={} <= native_main_vars={}); fallback anchor = hamming+stage6-cycle (with Dory layout ordering already encoded here)", + max_len, + native_main_vars + ); + } + tracing::info!("Stage8 final Dory opening point (BE): {:?}", final_point.r); + } + + final_point + } + + #[inline] + fn stage8_debug_enabled() -> bool { + let parse_env_bool = |key: &str| { + std::env::var(key).is_ok_and(|v| { + matches!( + v.trim().to_ascii_lowercase().as_str(), + "1" | "true" | "yes" | "on" | "all" + ) + }) + }; + parse_env_bool("JOLT_DEBUG_STAGE8_POLY_CLAIMS") || parse_env_bool("JOLT_DEBUG_DORY_CLASSES") + } + + fn stage8_sumcheck_opening( + &self, + poly: CommittedPolynomial, + ) -> (OpeningPoint, F) { + match poly { + CommittedPolynomial::RdInc | CommittedPolynomial::RamInc => self + .opening_accumulator + .get_committed_polynomial_opening(poly, SumcheckId::IncClaimReduction), + CommittedPolynomial::InstructionRa(_) + | CommittedPolynomial::BytecodeRa(_) + | CommittedPolynomial::RamRa(_) => self + .opening_accumulator + .get_committed_polynomial_opening(poly, SumcheckId::HammingWeightClaimReduction), + CommittedPolynomial::TrustedAdvice => self + .opening_accumulator + .get_advice_opening(AdviceKind::Trusted, SumcheckId::AdviceClaimReduction) + .expect("missing trusted advice opening for Stage 8 debug"), + CommittedPolynomial::UntrustedAdvice => self + .opening_accumulator + .get_advice_opening(AdviceKind::Untrusted, SumcheckId::AdviceClaimReduction) + .expect("missing untrusted advice opening for Stage 8 debug"), + CommittedPolynomial::BytecodeChunk(_) => self + .opening_accumulator + .get_committed_polynomial_opening(poly, SumcheckId::BytecodeClaimReduction), + CommittedPolynomial::ProgramImageInit => self + .opening_accumulator + .get_committed_polynomial_opening(poly, SumcheckId::ProgramImageClaimReduction), + } + } + + fn debug_verify_stage8_polynomial_claims( + &self, + stage8_opening_point: &OpeningPoint, + polynomial_claims: &[(CommittedPolynomial, F)], + direct_polys: &HashMap>, + ) { + let mut generated_polys: HashMap> = + HashMap::new(); + + let eval_poly = |poly_id: CommittedPolynomial, + point: &[F::Challenge], + generated: &mut HashMap>| + -> F { + if let Some(poly) = direct_polys.get(&poly_id) { + return poly.evaluate(point); + } + let poly = generated.entry(poly_id).or_insert_with(|| { + poly_id.generate_witness( + &self.preprocessing.program.bytecode, + &self.preprocessing.shared.memory_layout, + self.trace.as_slice(), + Some(&self.one_hot_params), + ) + }); + poly.evaluate(point) + }; + + let poly_num_vars = + |poly_id: CommittedPolynomial, + generated: &mut HashMap>| + -> usize { + if let Some(poly) = direct_polys.get(&poly_id) { + return poly.get_num_vars(); + } + let poly = generated.entry(poly_id).or_insert_with(|| { + poly_id.generate_witness( + &self.preprocessing.program.bytecode, + &self.preprocessing.shared.memory_layout, + self.trace.as_slice(), + Some(&self.one_hot_params), + ) + }); + poly.get_num_vars() + }; + + let derive_from_prefix = |num_vars: usize| -> OpeningPoint { + assert!( + num_vars <= stage8_opening_point.r.len(), + "cannot derive source point of len {} from stage8 point len {}", + num_vars, + stage8_opening_point.r.len() + ); + OpeningPoint::::new(stage8_opening_point.r[..num_vars].to_vec()) + }; + let derive_from_suffix = |num_vars: usize| -> OpeningPoint { + assert!( + num_vars <= stage8_opening_point.r.len(), + "cannot derive source point of len {} from stage8 point len {}", + num_vars, + stage8_opening_point.r.len() + ); + let start = stage8_opening_point.r.len() - num_vars; + OpeningPoint::::new(stage8_opening_point.r[start..].to_vec()) + }; + let derive_ra_from_prefix_rotated = |num_vars: usize| -> OpeningPoint { + assert!( + num_vars <= stage8_opening_point.r.len(), + "cannot derive source point of len {} from stage8 point len {}", + num_vars, + stage8_opening_point.r.len() + ); + let k = self.one_hot_params.log_k_chunk; + assert!( + k <= num_vars, + "ra opening point shorter than log_k_chunk (len={}, log_k_chunk={})", + num_vars, + k + ); + let t = num_vars - k; + let prefix = &stage8_opening_point.r[..num_vars]; + let mut rotated = Vec::with_capacity(num_vars); + // Move the last k address variables to the front: [t | k] -> [k | t]. + rotated.extend_from_slice(&prefix[t..]); + rotated.extend_from_slice(&prefix[..t]); + OpeningPoint::::new(rotated) + }; + + for (poly_id, _) in polynomial_claims.iter() { + let (sumcheck_point, sumcheck_claim) = self.stage8_sumcheck_opening(*poly_id); + let num_vars = poly_num_vars(*poly_id, &mut generated_polys); + let projected_point = match poly_id { + CommittedPolynomial::RdInc | CommittedPolynomial::RamInc => { + match DoryGlobals::get_layout() { + DoryLayout::AddressMajor => derive_from_prefix(num_vars), + DoryLayout::CycleMajor => derive_from_suffix(num_vars), + } + } + CommittedPolynomial::InstructionRa(_) + | CommittedPolynomial::BytecodeRa(_) + | CommittedPolynomial::RamRa(_) => match DoryGlobals::get_layout() { + DoryLayout::AddressMajor => derive_ra_from_prefix_rotated(num_vars), + DoryLayout::CycleMajor => derive_from_suffix(num_vars), + }, + CommittedPolynomial::TrustedAdvice + | CommittedPolynomial::UntrustedAdvice + | CommittedPolynomial::BytecodeChunk(_) + | CommittedPolynomial::ProgramImageInit => { + derive_poly_source_point_from_dory_dims(stage8_opening_point, num_vars) + } + }; + + let eval_at_sumcheck = eval_poly(*poly_id, &sumcheck_point.r, &mut generated_polys); + assert_eq!( + eval_at_sumcheck, sumcheck_claim, + "Stage8 debug mismatch for {poly_id:?}: sumcheck claim != direct eval at sumcheck point; sumcheck_point={:?}; projected_point={:?}", + sumcheck_point.r, + projected_point.r, + ); + + let eval_at_projected = eval_poly(*poly_id, &projected_point.r, &mut generated_polys); + assert_eq!( + eval_at_projected, sumcheck_claim, + "Stage8 debug mismatch for {poly_id:?}: sumcheck claim != direct eval at projected Dory point; sumcheck_point={:?}; projected_point={:?}", + sumcheck_point.r, + projected_point.r, + ); + } + + tracing::info!( + "Stage8 debug validated {} polynomial claims", + polynomial_claims.len() + ); + } + + fn stage8_debug_joint_commitment( + &self, + commitments: &[PCS::Commitment], + untrusted_advice_commitment: Option<&PCS::Commitment>, + state: &DoryOpeningState, + ) -> PCS::Commitment { + let expected_polynomials = all_committed_polynomials(&self.one_hot_params); + assert_eq!( + expected_polynomials.len(), + commitments.len(), + "Stage8 debug: expected {} commitments but prover produced {}", + expected_polynomials.len(), + commitments.len() + ); + + let mut commitment_map: HashMap = + expected_polynomials + .into_iter() + .zip(commitments.iter().cloned()) + .collect(); + + if let Some(commitment) = self.advice.trusted_advice_commitment.as_ref() { + if state + .polynomial_claims + .iter() + .any(|(p, _)| *p == CommittedPolynomial::TrustedAdvice) + { + commitment_map.insert(CommittedPolynomial::TrustedAdvice, commitment.clone()); + } + } + if let Some(commitment) = untrusted_advice_commitment { + if state + .polynomial_claims + .iter() + .any(|(p, _)| *p == CommittedPolynomial::UntrustedAdvice) + { + commitment_map.insert(CommittedPolynomial::UntrustedAdvice, commitment.clone()); + } + } + if let Some(bytecode_commitments) = &self.preprocessing.bytecode_commitments { + for (chunk_idx, commitment) in bytecode_commitments.commitments.iter().enumerate() { + if state + .polynomial_claims + .iter() + .any(|(p, _)| *p == CommittedPolynomial::BytecodeChunk(chunk_idx)) + { + commitment_map.insert( + CommittedPolynomial::BytecodeChunk(chunk_idx), + commitment.clone(), + ); + } + } + } + if let Some(program_commitments) = &self.preprocessing.program_commitments { + if state + .polynomial_claims + .iter() + .any(|(p, _)| *p == CommittedPolynomial::ProgramImageInit) + { + commitment_map.insert( + CommittedPolynomial::ProgramImageInit, + program_commitments.program_image_commitment.clone(), + ); + } + } + + let mut rlc_map: HashMap = HashMap::new(); + for (gamma, (poly, _claim)) in state + .gamma_powers + .iter() + .zip(state.polynomial_claims.iter()) + { + *rlc_map.entry(*poly).or_insert(F::zero()) += *gamma; + } + + let (coeffs, commitments): (Vec, Vec) = rlc_map + .into_iter() + .map(|(poly, coeff)| { + let commitment = commitment_map.remove(&poly).unwrap_or_else(|| { + panic!("Stage8 debug: missing commitment for {poly:?} in joint commitment map") + }); + (coeff, commitment) + }) + .unzip(); + + PCS::combine_commitments(&commitments, &coeffs) + } + + fn debug_verify_stage8_dory_self( + &self, + proof: &PCS::Proof, + stage8_opening_point: &OpeningPoint, + state: &DoryOpeningState, + joint_claim: F, + commitments: &[PCS::Commitment], + untrusted_advice_commitment: Option<&PCS::Commitment>, + mut transcript: ProofTranscript, + ) { + let joint_commitment = + self.stage8_debug_joint_commitment(commitments, untrusted_advice_commitment, state); + + #[cfg(feature = "zk")] + let opening = F::zero(); + #[cfg(not(feature = "zk"))] + let opening = joint_claim; + let verifier_setup = PCS::setup_verifier(&self.preprocessing.generators); + + PCS::verify( + proof, + &verifier_setup, + &mut transcript, + &stage8_opening_point.r, + &opening, + &joint_commitment, + ) + .unwrap_or_else(|e| panic!("Stage8 debug Dory self-verification failed: {e:?}")); + + tracing::info!("Stage8 debug Dory self-verification passed"); + } + + fn debug_verify_omitted_program_openings(&self) { + if !self.preprocessing.is_committed_mode() { + return; + } + + if !self.include_bytecode_in_stage8() { + let bytecode_chunk_polys = build_committed_bytecode_chunk_polynomials::( + &self.preprocessing.program.bytecode.bytecode, + self.preprocessing.shared.bytecode_chunk_count, + ); + for (chunk_idx, poly) in bytecode_chunk_polys.into_iter().enumerate() { + let (point, claim) = self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::BytecodeChunk(chunk_idx), + SumcheckId::BytecodeClaimReduction, + ); + let eval = poly.evaluate(&point.r); + assert_eq!( + eval, claim, + "Stage8 debug mismatch for omitted bytecode chunk {chunk_idx}: direct evaluation does not match the cached opening claim" + ); + } + tracing::info!("Stage8 debug validated omitted bytecode chunk openings directly"); + } + + if !self.include_program_image_in_stage8() { + let mut program_image_words = self.preprocessing.program.ram.bytecode_words.clone(); + if program_image_words.is_empty() { + program_image_words.push(0); + } + let padded_len = program_image_words.len().next_power_of_two().max(2); + program_image_words.resize(padded_len, 0); + let program_image_poly = MultilinearPolynomial::from(program_image_words); + let (point, claim) = self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReduction, + ); + let eval = program_image_poly.evaluate(&point.r); + assert_eq!( + eval, claim, + "Stage8 debug mismatch for omitted program image: direct evaluation does not match the cached opening claim" + ); + tracing::info!("Stage8 debug validated omitted program image opening directly"); } } @@ -402,11 +892,11 @@ impl< .unwrap_or(0) .max( remap_address( - preprocessing.shared.ram.min_bytecode_address, + preprocessing.shared.program_meta.min_bytecode_address, &preprocessing.shared.memory_layout, ) .unwrap_or(0) - + preprocessing.shared.ram.bytecode_words.len() as u64 + + preprocessing.shared.program_meta.program_image_len_words as u64 + 1, ) .next_power_of_two() as usize; @@ -418,7 +908,7 @@ impl< let (initial_ram_state, final_ram_state) = gen_ram_memory_states::( ram_K, - &preprocessing.shared.ram, + &preprocessing.program.ram, &program_io, &final_memory_state, ); @@ -426,8 +916,7 @@ impl< let log_T = trace.len().log_2(); let ram_log_K = ram_K.log_2(); let rw_config = ReadWriteConfig::new(log_T, ram_log_K); - let one_hot_params = - OneHotParams::new(log_T, preprocessing.shared.bytecode.code_size, ram_K); + let one_hot_params = OneHotParams::new(log_T, preprocessing.shared.bytecode_size(), ram_K); #[cfg(feature = "zk")] let pedersen_generators = { @@ -449,6 +938,8 @@ impl< }, advice_reduction_prover_trusted: None, advice_reduction_prover_untrusted: None, + bytecode_reduction_prover: None, + program_image_reduction_prover: None, unpadded_trace_len, padded_trace_len, transcript, @@ -483,13 +974,13 @@ impl< &self.program_io, self.one_hot_params.ram_k, self.trace.len(), - self.preprocessing.shared.bytecode.entry_address, + self.preprocessing.shared.program_meta.entry_address, &mut self.transcript, ); tracing::info!( "bytecode size: {}", - self.preprocessing.shared.bytecode.code_size + self.preprocessing.shared.bytecode_size() ); let (commitments, mut opening_proof_hints) = self.generate_and_commit_witness_polynomials(); @@ -503,6 +994,29 @@ impl< if let Some(hint) = self.advice.untrusted_advice_hint.take() { opening_proof_hints.insert(CommittedPolynomial::UntrustedAdvice, hint); } + if let Some(bytecode_hints) = &self.preprocessing.bytecode_hints { + for (idx, hint) in bytecode_hints.iter().cloned().enumerate() { + opening_proof_hints.insert(CommittedPolynomial::BytecodeChunk(idx), hint); + } + } + if let Some(program_hints) = &self.preprocessing.program_hints { + opening_proof_hints.insert( + CommittedPolynomial::ProgramImageInit, + program_hints.program_image_hint.clone(), + ); + } + if let Some(bytecode_commitments) = &self.preprocessing.bytecode_commitments { + for commitment in &bytecode_commitments.commitments { + self.transcript + .append_serializable(b"bytecode_chunk_commit", commitment); + } + } + if let Some(program_commitments) = &self.preprocessing.program_commitments { + self.transcript.append_serializable( + b"program_image_commitment", + &program_commitments.program_image_commitment, + ); + } let (stage1_uni_skip_first_round_proof, stage1_sumcheck_proof, r_stage1) = self.prove_stage1(); @@ -521,7 +1035,11 @@ impl< r_stage1, r_stage2, r_stage3, r_stage4, r_stage5, r_stage6, r_stage7, ]; - let joint_opening_proof = self.prove_stage8(opening_proof_hints); + let joint_opening_proof = self.prove_stage8( + opening_proof_hints, + &commitments, + untrusted_advice_commitment.as_ref(), + ); #[cfg(feature = "zk")] let blindfold_proof = self.prove_blindfold(&joint_opening_proof); @@ -683,7 +1201,7 @@ impl< .par_iter() .map(|poly_id| { let witness: MultilinearPolynomial = poly_id.generate_witness( - &self.preprocessing.shared.bytecode, + &self.preprocessing.program.bytecode, &self.preprocessing.shared.memory_layout, &trace, Some(&self.one_hot_params), @@ -723,6 +1241,7 @@ impl< poly.stream_witness_and_commit_rows::<_, PCS>( &self.preprocessing.generators, &self.preprocessing.shared, + &self.preprocessing.program, &chunk, &self.one_hot_params, ) @@ -844,14 +1363,14 @@ impl< let mut uni_skip = OuterUniSkipProver::initialize( uni_skip_params.clone(), &self.trace, - &self.preprocessing.shared.bytecode, + &self.preprocessing.program.bytecode, ); let first_round_proof = self.prove_uniskip(&mut uni_skip); let schedule = LinearOnlySchedule::new(uni_skip_params.tau.len() - 1); let shared = OuterSharedState::new( Arc::clone(&self.trace), - &self.preprocessing.shared.bytecode, + &self.preprocessing.program.bytecode, &uni_skip_params, &self.opening_accumulator, ); @@ -919,7 +1438,7 @@ impl< let ram_read_write_checking = RamReadWriteCheckingProver::initialize( ram_read_write_checking_params, &self.trace, - &self.preprocessing.shared.bytecode, + &self.preprocessing.program.bytecode, &self.program_io.memory_layout, &self.initial_ram_state, ); @@ -1008,7 +1527,7 @@ impl< let spartan_shift = ShiftSumcheckProver::initialize( spartan_shift_params, Arc::clone(&self.trace), - &self.preprocessing.shared.bytecode, + &self.preprocessing.program.bytecode, ); let spartan_instruction_input = InstructionInputSumcheckProver::initialize( spartan_instruction_input_params, @@ -1074,6 +1593,14 @@ impl< &self.one_hot_params, &mut self.opening_accumulator, ); + if self.preprocessing.is_committed_mode() { + prover_accumulate_program_image( + self.one_hot_params.ram_k, + &self.preprocessing.program.ram, + &self.program_io, + &mut self.opening_accumulator, + ); + } // Domain-separate the batching challenge. self.transcript.append_bytes(b"ram_val_check_gamma", &[]); let ram_val_check_gamma: F = self.transcript.challenge_scalar::(); @@ -1083,20 +1610,22 @@ impl< &self.initial_ram_state, self.trace.len(), ram_val_check_gamma, - &self.preprocessing.shared.ram, + &self.preprocessing.program.ram, &self.program_io, + &self.rw_config, + self.preprocessing.is_committed_mode(), ); let registers_read_write_checking = RegistersReadWriteCheckingProver::initialize( registers_read_write_checking_params, self.trace.clone(), - &self.preprocessing.shared.bytecode, + &self.preprocessing.program.bytecode, &self.program_io.memory_layout, ); let ram_val_check = RamValCheckSumcheckProver::initialize( ram_val_check_params, &self.trace, - &self.preprocessing.shared.bytecode, + &self.preprocessing.program.bytecode, &self.program_io.memory_layout, ); @@ -1165,7 +1694,7 @@ impl< let registers_val_evaluation = RegistersValEvaluationSumcheckProver::initialize( registers_val_evaluation_params, &self.trace, - &self.preprocessing.shared.bytecode, + &self.preprocessing.program.bytecode, &self.program_io.memory_layout, ); @@ -1210,9 +1739,10 @@ impl< print_current_memory_usage("Stage 6a baseline"); let bytecode_read_raf_params = BytecodeReadRafSumcheckParams::gen( - &self.preprocessing.shared.bytecode, + &self.preprocessing.program, self.trace.len().log_2(), &self.one_hot_params, + self.preprocessing.is_committed_mode(), &self.opening_accumulator, &mut self.transcript, ); @@ -1223,16 +1753,21 @@ impl< &self.opening_accumulator, &mut self.transcript, ); + tracing::info!( + "Stage 6a prover input claims: bytecode_read_raf={} booleanity={}", + bytecode_read_raf_params.input_claim(&self.opening_accumulator), + booleanity_params.input_claim(&self.opening_accumulator), + ); let mut bytecode_read_raf = BytecodeReadRafAddressSumcheckProver::initialize( bytecode_read_raf_params.clone(), Arc::clone(&self.trace), - Arc::clone(&self.preprocessing.shared.bytecode), + Arc::new(self.preprocessing.program.bytecode.clone()), ); let mut booleanity = BooleanityAddressSumcheckProver::initialize( booleanity_params.clone(), &self.trace, - &self.preprocessing.shared.bytecode, + &self.preprocessing.program.bytecode, &self.program_io.memory_layout, ); @@ -1366,16 +1901,61 @@ impl< }; } + if self.preprocessing.is_committed_mode() { + let bytecode_chunk_count = self.preprocessing.shared.bytecode_chunk_count; + let bytecode_reduction_params = BytecodeClaimReductionParams::new( + &bytecode_read_raf_params, + self.preprocessing.shared.bytecode_size(), + bytecode_chunk_count, + precommitted_scheduling_reference, + &self.opening_accumulator, + &mut self.transcript, + ); + let bytecode_chunk_polys = build_committed_bytecode_chunk_polynomials( + &self.preprocessing.program.bytecode.bytecode, + bytecode_chunk_count, + ); + self.bytecode_reduction_prover = Some(BytecodeClaimReductionProver::initialize( + bytecode_reduction_params, + &bytecode_chunk_polys, + )); + + let padded_len_words = self + .preprocessing + .program + .ram + .bytecode_words + .len() + .max(1) + .next_power_of_two(); + let mut program_image_words = self.preprocessing.program.ram.bytecode_words.clone(); + program_image_words.resize(padded_len_words, 0); + let program_image_reduction_params = ProgramImageClaimReductionParams::new( + &self.program_io, + self.preprocessing.shared.program_meta.min_bytecode_address, + padded_len_words, + self.one_hot_params.ram_k, + precommitted_scheduling_reference, + &self.opening_accumulator, + &mut self.transcript, + ); + self.program_image_reduction_prover = + Some(ProgramImageClaimReductionProver::initialize( + program_image_reduction_params, + program_image_words, + )); + } + let mut bytecode_read_raf = BytecodeReadRafCycleSumcheckProver::initialize( bytecode_read_raf_params, Arc::clone(&self.trace), - Arc::clone(&self.preprocessing.shared.bytecode), + Arc::new(self.preprocessing.program.bytecode.clone()), &self.opening_accumulator, ); let mut booleanity = BooleanityCycleSumcheckProver::initialize( booleanity_params, &self.trace, - &self.preprocessing.shared.bytecode, + &self.preprocessing.program.bytecode, &self.program_io.memory_layout, &self.opening_accumulator, ); @@ -1417,6 +1997,8 @@ impl< let mut advice_trusted = self.advice_reduction_prover_trusted.take(); let mut advice_untrusted = self.advice_reduction_prover_untrusted.take(); + let mut bytecode_reduction = self.bytecode_reduction_prover.take(); + let mut program_image_reduction = self.program_image_reduction_prover.take(); let mut instances: Vec<&mut dyn SumcheckInstanceProver<_, _>> = vec![ &mut bytecode_read_raf, @@ -1432,6 +2014,12 @@ impl< if let Some(ref mut advice) = advice_untrusted { instances.push(advice); } + if let Some(ref mut reduction) = bytecode_reduction { + instances.push(reduction); + } + if let Some(ref mut reduction) = program_image_reduction { + instances.push(reduction); + } #[cfg(feature = "allocative")] write_instance_flamegraph_svg(&instances, "stage6b_start_flamechart.svg"); @@ -1450,6 +2038,8 @@ impl< self.advice_reduction_prover_trusted = advice_trusted; self.advice_reduction_prover_untrusted = advice_untrusted; + self.bytecode_reduction_prover = bytecode_reduction; + self.program_image_reduction_prover = program_image_reduction; (sumcheck_proof, r_stage6b) } @@ -1920,6 +2510,7 @@ impl< hw_params, &self.trace, &self.preprocessing.shared, + &self.preprocessing.program, &self.one_hot_params, ); @@ -1957,6 +2548,27 @@ impl< instances.push(Box::new(advice_reduction_prover_untrusted)); } } + if let Some(mut bytecode_reduction_prover) = self.bytecode_reduction_prover.take() { + if bytecode_reduction_prover + .params() + .num_address_phase_rounds() + > 0 + { + bytecode_reduction_prover.transition_to_address_phase(); + instances.push(Box::new(bytecode_reduction_prover)); + } + } + if let Some(mut program_image_reduction_prover) = self.program_image_reduction_prover.take() + { + if program_image_reduction_prover + .params() + .num_address_phase_rounds() + > 0 + { + program_image_reduction_prover.transition_to_address_phase(); + instances.push(Box::new(program_image_reduction_prover)); + } + } #[cfg(feature = "allocative")] write_boxed_instance_flamegraph_svg(&instances, "stage7_start_flamechart.svg"); @@ -1977,6 +2589,8 @@ impl< fn prove_stage8( &mut self, opening_proof_hints: HashMap, + commitments: &[PCS::Commitment], + untrusted_advice_commitment: Option<&PCS::Commitment>, ) -> PCS::Proof { tracing::info!("Stage 8 proving (Dory batch opening)"); @@ -1984,6 +2598,8 @@ impl< let mut polynomial_claims = Vec::new(); let mut scaling_factors = Vec::new(); + let mut extra_dense_polys: HashMap> = + HashMap::new(); let (ram_inc_point, ram_inc_claim) = self.opening_accumulator.get_committed_polynomial_opening( @@ -2076,6 +2692,51 @@ impl< } } + if self.include_bytecode_in_stage8() { + let chunk_count = self.preprocessing.shared.bytecode_chunk_count; + let bytecode_chunks = build_committed_bytecode_chunk_polynomials::( + &self.preprocessing.program.bytecode.bytecode, + chunk_count, + ); + for (chunk_idx, poly) in bytecode_chunks.into_iter().enumerate() { + let (chunk_point, chunk_claim) = + self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::BytecodeChunk(chunk_idx), + SumcheckId::BytecodeClaimReduction, + ); + let lagrange_factor = + compute_lagrange_factor::(&opening_point.r, &chunk_point.r); + polynomial_claims.push(( + CommittedPolynomial::BytecodeChunk(chunk_idx), + chunk_claim * lagrange_factor, + )); + scaling_factors.push(lagrange_factor); + extra_dense_polys.insert(CommittedPolynomial::BytecodeChunk(chunk_idx), poly); + } + } + + if self.include_program_image_in_stage8() { + let mut program_image_words = self.preprocessing.program.ram.bytecode_words.clone(); + if program_image_words.is_empty() { + program_image_words.push(0); + } + let padded_len = program_image_words.len().next_power_of_two().max(2); + program_image_words.resize(padded_len, 0); + let program_image_poly = MultilinearPolynomial::from(program_image_words); + let (program_point, program_claim) = + self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReduction, + ); + let lagrange_factor = compute_lagrange_factor::(&opening_point.r, &program_point.r); + polynomial_claims.push(( + CommittedPolynomial::ProgramImageInit, + program_claim * lagrange_factor, + )); + scaling_factors.push(lagrange_factor); + extra_dense_polys.insert(CommittedPolynomial::ProgramImageInit, program_image_poly); + } + // 2. Sample gamma and compute powers for RLC let claims: Vec = polynomial_claims.iter().map(|(_, c)| *c).collect(); // In non-ZK mode, absorb claims before sampling gamma for Fiat-Shamir binding. @@ -2094,12 +2755,22 @@ impl< .zip(claims.iter()) .map(|(gamma, claim)| *gamma * claim) .sum(); + if Self::stage8_debug_enabled() { + tracing::info!("Stage8 final Dory claim (joint_claim): {}", joint_claim); + } #[cfg(feature = "zk")] let opening_ids = stage8_opening_ids( &self.one_hot_params, include_trusted_advice, include_untrusted_advice, + if self.preprocessing.is_committed_mode() { + ProgramMode::Committed + } else { + ProgramMode::Full + }, + self.preprocessing.shared.bytecode_chunk_count, + stage8_program_openings_from_env(), ); // Build DoryOpeningState @@ -2110,18 +2781,26 @@ impl< }; let streaming_data = Arc::new(RLCStreamingData { - bytecode: Arc::clone(&self.preprocessing.shared.bytecode), + bytecode: Arc::new(self.preprocessing.program.bytecode.clone()), memory_layout: self.preprocessing.shared.memory_layout.clone(), }); - // Build advice polynomials map for RLC - let mut advice_polys = HashMap::new(); + // Build precommitted polynomials map for RLC + let mut precommitted_polys = HashMap::new(); if let Some(poly) = self.advice.trusted_advice_polynomial.take() { - advice_polys.insert(CommittedPolynomial::TrustedAdvice, poly); + precommitted_polys.insert(CommittedPolynomial::TrustedAdvice, poly); } if let Some(poly) = self.advice.untrusted_advice_polynomial.take() { - advice_polys.insert(CommittedPolynomial::UntrustedAdvice, poly); + precommitted_polys.insert(CommittedPolynomial::UntrustedAdvice, poly); } + precommitted_polys.extend(extra_dense_polys); + let debug_stage8_polys = if Self::stage8_debug_enabled() { + Some(precommitted_polys.clone()) + } else { + None + }; + let mut debug_stage8_verify_transcript = + debug_stage8_polys.as_ref().map(|_| self.transcript.clone()); // Build streaming RLC polynomial directly (no witness poly regeneration!) // Use materialized trace (default, single pass) instead of lazy trace @@ -2130,9 +2809,8 @@ impl< TraceSource::Materialized(Arc::clone(&self.trace)), streaming_data, opening_proof_hints, - advice_polys, + precommitted_polys, ); - let (proof, _y_blinding) = PCS::prove( &self.preprocessing.generators, &joint_poly, @@ -2158,6 +2836,26 @@ impl< { bind_opening_inputs::(&mut self.transcript, &opening_point.r, &joint_claim); } + if let Some(ref direct_polys) = debug_stage8_polys { + self.debug_verify_stage8_polynomial_claims( + &opening_point, + &state.polynomial_claims, + direct_polys, + ); + self.debug_verify_omitted_program_openings(); + let verify_transcript = debug_stage8_verify_transcript + .take() + .expect("Stage8 debug transcript clone should exist"); + self.debug_verify_stage8_dory_self( + &proof, + &opening_point, + &state, + joint_claim, + commitments, + untrusted_advice_commitment, + verify_transcript, + ); + } proof } @@ -2197,7 +2895,7 @@ fn write_instance_flamegraph_svg( write_flamegraph_svg(flamegraph, path); } -#[derive(Clone, CanonicalSerialize, CanonicalDeserialize)] +#[derive(Clone)] pub struct JoltProverPreprocessing< F: JoltField, C: JoltCurve, @@ -2205,17 +2903,127 @@ pub struct JoltProverPreprocessing< > { pub generators: PCS::ProverSetup, pub shared: JoltSharedPreprocessing, + pub program: Arc, + pub bytecode_commitments: Option>, + pub bytecode_hints: Option>, + pub program_commitments: Option>, + pub program_hints: Option>, _curve: std::marker::PhantomData, } +impl CanonicalSerialize for JoltProverPreprocessing +where + F: JoltField, + C: JoltCurve, + PCS: CommitmentScheme, + PCS::OpeningProofHint: CanonicalSerialize, +{ + fn serialize_with_mode( + &self, + mut writer: W, + compress: ark_serialize::Compress, + ) -> Result<(), ark_serialize::SerializationError> { + self.generators.serialize_with_mode(&mut writer, compress)?; + self.shared.serialize_with_mode(&mut writer, compress)?; + self.program.serialize_with_mode(&mut writer, compress)?; + self.bytecode_commitments + .serialize_with_mode(&mut writer, compress)?; + self.bytecode_hints + .serialize_with_mode(&mut writer, compress)?; + self.program_commitments + .serialize_with_mode(&mut writer, compress)?; + self.program_hints + .serialize_with_mode(&mut writer, compress)?; + Ok(()) + } + + fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { + self.generators.serialized_size(compress) + + self.shared.serialized_size(compress) + + self.program.serialized_size(compress) + + self.bytecode_commitments.serialized_size(compress) + + self.bytecode_hints.serialized_size(compress) + + self.program_commitments.serialized_size(compress) + + self.program_hints.serialized_size(compress) + } +} + +impl ark_serialize::Valid for JoltProverPreprocessing +where + F: JoltField, + C: JoltCurve, + PCS: CommitmentScheme, + PCS::OpeningProofHint: ark_serialize::Valid, +{ + fn check(&self) -> Result<(), ark_serialize::SerializationError> { + self.generators.check()?; + self.shared.check()?; + self.program.check()?; + self.bytecode_commitments.check()?; + self.bytecode_hints.check()?; + self.program_commitments.check()?; + self.program_hints.check()?; + Ok(()) + } +} + +impl CanonicalDeserialize for JoltProverPreprocessing +where + F: JoltField, + C: JoltCurve, + PCS: CommitmentScheme, + PCS::OpeningProofHint: CanonicalDeserialize, +{ + fn deserialize_with_mode( + mut reader: R, + compress: ark_serialize::Compress, + validate: ark_serialize::Validate, + ) -> Result { + Ok(Self { + generators: PCS::ProverSetup::deserialize_with_mode(&mut reader, compress, validate)?, + shared: JoltSharedPreprocessing::deserialize_with_mode( + &mut reader, + compress, + validate, + )?, + program: Arc::new(ProgramPreprocessing::deserialize_with_mode( + &mut reader, + compress, + validate, + )?), + bytecode_commitments: Option::>::deserialize_with_mode( + &mut reader, + compress, + validate, + )?, + bytecode_hints: Option::>::deserialize_with_mode( + &mut reader, + compress, + validate, + )?, + program_commitments: Option::>::deserialize_with_mode( + &mut reader, + compress, + validate, + )?, + program_hints: Option::>::deserialize_with_mode( + &mut reader, + compress, + validate, + )?, + _curve: std::marker::PhantomData, + }) + } +} + impl JoltProverPreprocessing where F: JoltField, C: JoltCurve, PCS: CommitmentScheme, { - #[tracing::instrument(skip_all, name = "JoltProverPreprocessing::gen")] - pub fn new(shared: JoltSharedPreprocessing) -> Self { + #[tracing::instrument(skip_all, name = "JoltProverPreprocessing::new")] + pub fn new(shared: JoltSharedPreprocessing, program: Arc) -> Self { use common::constants::ONEHOT_CHUNK_THRESHOLD_LOG_T; let max_T: usize = shared.max_padded_trace_length.next_power_of_two(); let max_log_T = max_T.log_2(); @@ -2224,11 +3032,78 @@ where } else { 8 }; - let generators = PCS::setup_prover(max_log_k_chunk + max_log_T); + let mut max_total_vars = max_log_k_chunk + max_log_T; + let (trusted_sigma, trusted_nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( + shared.memory_layout.max_trusted_advice_size as usize, + ); + max_total_vars = max_total_vars.max(trusted_sigma + trusted_nu); + let (untrusted_sigma, untrusted_nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( + shared.memory_layout.max_untrusted_advice_size as usize, + ); + max_total_vars = max_total_vars.max(untrusted_sigma + untrusted_nu); + let generators = PCS::setup_prover(max_total_vars); + + JoltProverPreprocessing { + generators, + shared, + program, + bytecode_commitments: None, + bytecode_hints: None, + program_commitments: None, + program_hints: None, + _curve: std::marker::PhantomData, + } + } + + #[tracing::instrument(skip_all, name = "JoltProverPreprocessing::new_committed")] + pub fn new_committed( + shared: JoltSharedPreprocessing, + program: Arc, + ) -> Self { + use common::constants::ONEHOT_CHUNK_THRESHOLD_LOG_T; + let max_t_any = shared + .max_padded_trace_length + .max(shared.bytecode_size()) + .max(program.program_image_len_words_padded()) + .next_power_of_two(); + let max_log_t = max_t_any.log_2(); + let max_log_k_chunk = if max_log_t < ONEHOT_CHUNK_THRESHOLD_LOG_T { + 4 + } else { + 8 + }; + let mut max_total_vars = max_log_k_chunk + max_log_t; + let (trusted_sigma, trusted_nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( + shared.memory_layout.max_trusted_advice_size as usize, + ); + max_total_vars = max_total_vars.max(trusted_sigma + trusted_nu); + let (untrusted_sigma, untrusted_nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( + shared.memory_layout.max_untrusted_advice_size as usize, + ); + max_total_vars = max_total_vars.max(untrusted_sigma + untrusted_nu); + let chunk_cycle_log_t = (shared.bytecode_size() / shared.bytecode_chunk_count) + .next_power_of_two() + .log_2(); + max_total_vars = max_total_vars.max(committed_lanes().log_2() + chunk_cycle_log_t); + max_total_vars = max_total_vars.max(program.program_image_len_words_padded().log_2()); + let generators = PCS::setup_prover(max_total_vars); + let (bytecode_commitments, bytecode_hints) = TrustedBytecodeCommitments::derive( + &program.bytecode, + &generators, + max_log_k_chunk, + shared.bytecode_chunk_count, + ); + let (program_commitments, program_hints) = + TrustedProgramCommitments::derive(&program, &generators); JoltProverPreprocessing { generators, shared, + program, + bytecode_commitments: Some(bytecode_commitments), + bytecode_hints: Some(bytecode_hints.hints), + program_commitments: Some(program_commitments), + program_hints: Some(program_hints), _curve: std::marker::PhantomData, } } @@ -2257,7 +3132,14 @@ where ) } - pub fn save_to_target_dir(&self, target_dir: &str) -> std::io::Result<()> { + pub fn is_committed_mode(&self) -> bool { + self.bytecode_commitments.is_some() && self.program_commitments.is_some() + } + + pub fn save_to_target_dir(&self, target_dir: &str) -> std::io::Result<()> + where + PCS::OpeningProofHint: CanonicalSerialize, + { let filename = Path::new(target_dir).join("jolt_prover_preprocessing.dat"); let mut file = File::create(filename.as_path())?; let mut data = Vec::new(); @@ -2266,7 +3148,10 @@ where Ok(()) } - pub fn read_from_target_dir(target_dir: &str) -> std::io::Result { + pub fn read_from_target_dir(target_dir: &str) -> std::io::Result + where + PCS::OpeningProofHint: CanonicalDeserialize, + { let filename = Path::new(target_dir).join("jolt_prover_preprocessing.dat"); let mut file = File::open(filename.as_path())?; let mut data = Vec::new(); @@ -2277,6 +3162,8 @@ where impl, PCS: CommitmentScheme> Serializable for JoltProverPreprocessing +where + PCS::OpeningProofHint: CanonicalSerialize + CanonicalDeserialize, { } @@ -2301,6 +3188,7 @@ mod tests { opening_proof::{OpeningAccumulator, SumcheckId}, }; use crate::zkvm::claim_reductions::AdviceKind; + use crate::zkvm::program::ProgramPreprocessing; use crate::zkvm::verifier::JoltSharedPreprocessing; use crate::zkvm::witness::CommittedPolynomial; use crate::zkvm::{ @@ -2361,23 +3249,55 @@ mod tests { (commitment, hint) } + fn test_shared_preprocessing( + bytecode: Vec, + init_memory_state: Vec<(u64, u8)>, + memory_layout: common::jolt_device::MemoryLayout, + max_trace_len: usize, + ) -> (JoltSharedPreprocessing, Arc) { + let program = Arc::new(ProgramPreprocessing::preprocess( + bytecode, + init_memory_state, + )); + let shared = JoltSharedPreprocessing::new(program.meta(), memory_layout, max_trace_len); + (shared, program) + } + + fn test_shared_preprocessing_committed( + bytecode: Vec, + init_memory_state: Vec<(u64, u8)>, + memory_layout: common::jolt_device::MemoryLayout, + max_trace_len: usize, + bytecode_chunk_count: usize, + ) -> (JoltSharedPreprocessing, Arc) { + let program = Arc::new(ProgramPreprocessing::preprocess( + bytecode, + init_memory_state, + )); + let shared = JoltSharedPreprocessing::new_committed( + program.meta(), + memory_layout, + max_trace_len, + bytecode_chunk_count, + ); + (shared, program) + } + #[test] #[serial] fn fib_e2e_dory() { DoryGlobals::reset(); let mut program = host::Program::new("fibonacci-guest"); let inputs = postcard::to_stdvec(&100u32).unwrap(); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let (_, _, _, io_device) = program.trace(&inputs, &[], &[]); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing); + let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing, program_data); let elf_contents_opt = program.get_elf_contents(); let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); let prover = RV64IMACProver::gen_from_elf( @@ -2411,18 +3331,16 @@ mod tests { DoryGlobals::reset(); let mut program = host::Program::new("fibonacci-guest"); let inputs = postcard::to_stdvec(&5u32).unwrap(); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let (_, _, _, io_device) = program.trace(&inputs, &[], &[]); - - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 8192, - e_entry, ); - - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); let elf_contents_opt = program.get_elf_contents(); let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); let log_chunk = 13; // Use default log_chunk for tests @@ -2470,19 +3388,18 @@ mod tests { // when the jolt-inlines-keccak256 crate is linked (see lib.rs) let mut program = host::Program::new("sha3-guest"); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let inputs = postcard::to_stdvec(&[5u8; 32]).unwrap(); let (_, _, _, io_device) = program.trace(&inputs, &[], &[]); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); let elf_contents_opt = program.get_elf_contents(); let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); let prover = RV64IMACProver::gen_from_elf( @@ -2531,19 +3448,18 @@ mod tests { // SHA2 inlines are automatically registered via #[ctor::ctor] // when the jolt-inlines-sha2 crate is linked (see lib.rs) let mut program = host::Program::new("sha2-guest"); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let inputs = postcard::to_stdvec(&[5u8; 32]).unwrap(); let (_, _, _, io_device) = program.trace(&inputs, &[], &[]); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); let elf_contents_opt = program.get_elf_contents(); let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); let prover = RV64IMACProver::gen_from_elf( @@ -2590,21 +3506,21 @@ mod tests { // - Trusted: commit in preprocessing-only context, reduce in Stage 6, batch in Stage 8 // - Untrusted: commit at prove time, reduce in Stage 6, batch in Stage 8 let mut program = host::Program::new("sha2-guest"); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let inputs = postcard::to_stdvec(&[5u8; 32]).unwrap(); let trusted_advice = postcard::to_stdvec(&[7u8; 32]).unwrap(); let untrusted_advice = postcard::to_stdvec(&[9u8; 32]).unwrap(); let (_, _, _, io_device) = program.trace(&inputs, &untrusted_advice, &trusted_advice); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); let elf_contents = program.get_elf_contents().expect("elf contents is None"); let (trusted_commitment, trusted_hint) = @@ -2656,18 +3572,18 @@ mod tests { let trusted_advice = vec![7u8; 4096]; let untrusted_advice = vec![9u8; 4096]; - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let (lazy_trace, trace, final_memory_state, io_device) = program.trace(&inputs, &untrusted_advice, &trusted_advice); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 4096, - e_entry, ); - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); tracing::info!( "preprocessing.memory_layout.max_trusted_advice_size: {}", shared_preprocessing.memory_layout.max_trusted_advice_size @@ -2712,7 +3628,7 @@ mod tests { DoryGlobals::reset(); // Tests a guest (merkle-tree) that actually consumes both trusted and untrusted advice. let mut program = host::Program::new("merkle-tree-guest"); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); // Merkle tree with 4 leaves: input=leaf1, trusted=[leaf2, leaf3], untrusted=leaf4 let inputs = postcard::to_stdvec(&[5u8; 32].as_slice()).unwrap(); @@ -2721,14 +3637,14 @@ mod tests { trusted_advice.extend(postcard::to_stdvec(&[7u8; 32]).unwrap()); let (_, _, _, io_device) = program.trace(&inputs, &untrusted_advice, &trusted_advice); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); let elf_contents = program.get_elf_contents().expect("elf contents is None"); let (trusted_commitment, trusted_hint) = @@ -2782,18 +3698,18 @@ mod tests { let trusted_advice = postcard::to_stdvec(&[7u8; 32]).unwrap(); let untrusted_advice = postcard::to_stdvec(&[9u8; 32]).unwrap(); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let (lazy_trace, trace, final_memory_state, io_device) = program.trace(&inputs, &untrusted_advice, &trusted_advice); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); let (trusted_commitment, trusted_hint) = commit_trusted_advice_preprocessing_only(&prover_preprocessing, &trusted_advice); @@ -2873,18 +3789,17 @@ mod tests { fn memory_ops_e2e_dory() { DoryGlobals::reset(); let mut program = host::Program::new("memory-ops-guest"); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let (_, _, _, io_device) = program.trace(&[], &[], &[]); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); let elf_contents_opt = program.get_elf_contents(); let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); let prover = RV64IMACProver::gen_from_elf( @@ -2917,19 +3832,18 @@ mod tests { fn btreemap_e2e_dory() { DoryGlobals::reset(); let mut program = host::Program::new("btreemap-guest"); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let inputs = postcard::to_stdvec(&50u32).unwrap(); let (_, _, _, io_device) = program.trace(&inputs, &[], &[]); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); let elf_contents_opt = program.get_elf_contents(); let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); let prover = RV64IMACProver::gen_from_elf( @@ -2962,19 +3876,62 @@ mod tests { fn muldiv_e2e_dory() { DoryGlobals::reset(); let mut program = host::Program::new("muldiv-guest"); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let inputs = postcard::to_stdvec(&[9u32, 5u32, 3u32]).unwrap(); let (_, _, _, io_device) = program.trace(&inputs, &[], &[]); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); + let elf_contents_opt = program.get_elf_contents(); + let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); + let prover = RV64IMACProver::gen_from_elf( + &prover_preprocessing, + elf_contents, + &inputs, + &[], + &[], + None, + None, + None, + ); + let io_device = prover.program_io.clone(); + let (jolt_proof, debug_info) = prover.prove(); - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let verifier_preprocessing = JoltVerifierPreprocessing::from(&prover_preprocessing); + let verifier = RV64IMACVerifier::new( + &verifier_preprocessing, + jolt_proof, + io_device, + None, + debug_info, + ) + .expect("Failed to create verifier"); + verifier.verify().expect("Failed to verify proof"); + } + + #[test] + #[serial] + fn muldiv_e2e_dory_committed_program_commitments() { + DoryGlobals::reset(); + let mut program = host::Program::new("muldiv-guest"); + let (bytecode, init_memory_state, _, _) = program.decode(); + let inputs = postcard::to_stdvec(&[9u32, 5u32, 3u32]).unwrap(); + let (_, _, _, io_device) = program.trace(&inputs, &[], &[]); + let (shared_preprocessing, program_data) = test_shared_preprocessing_committed( + bytecode, + init_memory_state, + io_device.memory_layout.clone(), + 1 << 16, + 1, + ); + let prover_preprocessing = + JoltProverPreprocessing::new_committed(shared_preprocessing.clone(), program_data); let elf_contents_opt = program.get_elf_contents(); let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); let prover = RV64IMACProver::gen_from_elf( @@ -3012,18 +3969,17 @@ mod tests { program.set_std(true); program.set_func("int_to_string"); let inputs = postcard::to_stdvec(&81i32).unwrap(); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let (_, _, _, io_device) = program.trace(&inputs, &[], &[]); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); let elf_contents_opt = program.get_elf_contents(); let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); let prover = RV64IMACProver::gen_from_elf( @@ -3163,18 +4119,16 @@ mod tests { // Run muldiv prover to get a real proof let mut program = host::Program::new("muldiv-guest"); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let inputs = postcard::to_stdvec(&[9u32, 5u32, 3u32]).unwrap(); let (_, _, _, io_device) = program.trace(&inputs, &[], &[]); - - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - let preprocessing = JoltProverPreprocessing::new(shared_preprocessing); + let preprocessing = JoltProverPreprocessing::new(shared_preprocessing, program_data); let elf_contents_opt = program.get_elf_contents(); let elf_contents = elf_contents_opt.as_deref().expect("elf contents is None"); let prover = RV64IMACProver::gen_from_elf( @@ -3283,22 +4237,21 @@ mod tests { #[should_panic] fn truncated_trace() { let mut program = host::Program::new("fibonacci-guest"); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let inputs = postcard::to_stdvec(&9u8).unwrap(); let (lazy_trace, mut trace, final_memory_state, mut program_io) = program.trace(&inputs, &[], &[]); trace.truncate(100); program_io.outputs[0] = 0; // change the output to 0 - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - program_io.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + program_io.memory_layout.clone(), 1 << 16, - e_entry, ); - - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); let prover = RV64IMACProver::gen_from_trace( &prover_preprocessing, @@ -3324,19 +4277,19 @@ mod tests { fn malicious_trace() { let mut program = host::Program::new("fibonacci-guest"); let inputs = postcard::to_stdvec(&1u8).unwrap(); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let (lazy_trace, trace, final_memory_state, mut program_io) = program.trace(&inputs, &[], &[]); // Since the preprocessing is done with the original memory layout, the verifier should fail - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - program_io.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + program_io.memory_layout.clone(), 1 << 16, - e_entry, ); - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); // change memory address of output & termination bit to the same address as input // changes here should not be able to spoof the verifier result @@ -3375,15 +4328,17 @@ mod tests { let inputs = postcard::to_stdvec(&9u8).unwrap(); let (bytecode, init_memory_state, _, e_entry) = program.decode(); let (lazy_trace, trace, final_memory_state, program_io) = program.trace(&inputs, &[], &[]); - + let original_program = Arc::new(ProgramPreprocessing::preprocess( + bytecode, + init_memory_state, + )); let shared = JoltSharedPreprocessing::new( - bytecode.clone(), + original_program.meta(), program_io.memory_layout.clone(), - init_memory_state, 1 << 16, - e_entry, ); - let prover_preprocessing = JoltProverPreprocessing::new(shared.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared.clone(), Arc::clone(&original_program)); let prover = RV64IMACProver::gen_from_trace( &prover_preprocessing, lazy_trace, @@ -3395,19 +4350,19 @@ mod tests { ); let (proof, _) = prover.prove(); - let original_entry_index = shared.bytecode.entry_bytecode_index(); + let original_entry_index = original_program.entry_bytecode_index(); // Tamper: give verifier a wrong entry_address so it computes a different // entry_bytecode_index and thus a different input_claim expectation. let mut tampered_shared = shared.clone(); - let mut tampered_bytecode = (*tampered_shared.bytecode).clone(); - tampered_bytecode.entry_address = e_entry.wrapping_add(4); - tampered_shared.bytecode = Arc::new(tampered_bytecode); - let tampered_entry_index = tampered_shared.bytecode.entry_bytecode_index(); + tampered_shared.program_meta.entry_address = e_entry.wrapping_add(4); + let tampered_entry_index = tampered_shared.program_meta.entry_address as usize + / common::constants::BYTES_PER_INSTRUCTION; assert_ne!( original_entry_index, tampered_entry_index, "tamper did not change entry_bytecode_index — test scenario is invalid" ); - let tampered_prover_preprocessing = JoltProverPreprocessing::new(tampered_shared); + let tampered_prover_preprocessing = + JoltProverPreprocessing::new(tampered_shared, original_program); let verifier_preprocessing = JoltVerifierPreprocessing::from(&tampered_prover_preprocessing); let verifier = @@ -3546,17 +4501,16 @@ mod tests { let mut program = host::Program::new("fibonacci-guest"); let inputs = postcard::to_stdvec(&50u32).unwrap(); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); let (_, _, _, io_device) = program.trace(&inputs, &[], &[]); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing); + let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing, program_data); let elf_contents = program.get_elf_contents().expect("elf contents is None"); let prover = RV64IMACProver::gen_from_elf( &prover_preprocessing, @@ -3588,7 +4542,7 @@ mod tests { // Tests a guest (merkle-tree) that actually consumes both trusted and untrusted advice. let mut program = host::Program::new("merkle-tree-guest"); - let (bytecode, init_memory_state, _, e_entry) = program.decode(); + let (bytecode, init_memory_state, _, _) = program.decode(); // Merkle tree with 4 leaves: input=leaf1, trusted=[leaf2, leaf3], untrusted=leaf4 let inputs = postcard::to_stdvec(&[5u8; 32].as_slice()).unwrap(); @@ -3597,14 +4551,14 @@ mod tests { trusted_advice.extend(postcard::to_stdvec(&[7u8; 32]).unwrap()); let (_, _, _, io_device) = program.trace(&inputs, &untrusted_advice, &trusted_advice); - let shared_preprocessing = JoltSharedPreprocessing::new( - bytecode.clone(), - io_device.memory_layout.clone(), + let (shared_preprocessing, program_data) = test_shared_preprocessing( + bytecode, init_memory_state, + io_device.memory_layout.clone(), 1 << 16, - e_entry, ); - let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing.clone()); + let prover_preprocessing = + JoltProverPreprocessing::new(shared_preprocessing.clone(), program_data); let elf_contents = program.get_elf_contents().expect("elf contents is None"); let (trusted_commitment, trusted_hint) = diff --git a/jolt-core/src/zkvm/ram/mod.rs b/jolt-core/src/zkvm/ram/mod.rs index 715714a559..4fb7c1ad1f 100644 --- a/jolt-core/src/zkvm/ram/mod.rs +++ b/jolt-core/src/zkvm/ram/mod.rs @@ -281,6 +281,60 @@ pub fn verifier_accumulate_advice( } } +/// Accumulates staged program-image scalar contribution claims into the prover accumulator. +/// +/// These are scalar inner products: +/// - `C_rw = Σ_j ProgramWord[j] * eq(r_address_rw, start_index + j)` +/// This is stored as a virtual opening under `SumcheckId::RamValCheck`. +pub fn prover_accumulate_program_image( + ram_K: usize, + ram_preprocessing: &RAMPreprocessing, + program_io: &JoltDevice, + opening_accumulator: &mut ProverOpeningAccumulator, +) { + let total_vars = ram_K.log_2(); + let bytecode_start = remap_address( + ram_preprocessing.min_bytecode_address, + &program_io.memory_layout, + ) + .unwrap() as usize; + + let (r_rw, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::RamVal, + SumcheckId::RamReadWriteChecking, + ); + let (r_address_rw, _) = r_rw.split_at(total_vars); + let c_rw = sparse_eval_u64_block::( + bytecode_start, + &ram_preprocessing.bytecode_words, + &r_address_rw.r, + ); + opening_accumulator.append_virtual( + VirtualPolynomial::ProgramImageInitContributionRw, + SumcheckId::RamValCheck, + r_address_rw, + c_rw, + ); +} + +/// Mirrors [`prover_accumulate_program_image`] on verifier side by caching opening points. +pub fn verifier_accumulate_program_image( + ram_K: usize, + opening_accumulator: &mut VerifierOpeningAccumulator, +) { + let total_vars = ram_K.log_2(); + let (r_rw, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::RamVal, + SumcheckId::RamReadWriteChecking, + ); + let (r_address_rw, _) = r_rw.split_at(total_vars); + opening_accumulator.append_virtual( + VirtualPolynomial::ProgramImageInitContributionRw, + SumcheckId::RamValCheck, + r_address_rw, + ); +} + /// Calculates how advice inputs contribute to the evaluation of initial_ram_state at a given random point. /// /// ## Example with Two Commitments: @@ -429,6 +483,34 @@ pub fn reconstruct_full_eval( eval } +/// Evaluate just the public input words at a random RAM address point. +/// +/// Inputs are packed into little-endian `u64` words and placed at +/// `memory_layout.input_start`. +pub fn eval_inputs_mle(program_io: &JoltDevice, r_address: &[F::Challenge]) -> F { + if program_io.inputs.is_empty() { + return F::zero(); + } + + let input_start = remap_address( + program_io.memory_layout.input_start, + &program_io.memory_layout, + ) + .unwrap() as usize; + let input_words: Vec = program_io + .inputs + .chunks(8) + .map(|chunk| { + let mut word = [0u8; 8]; + for (i, byte) in chunk.iter().enumerate() { + word[i] = *byte; + } + u64::from_le_bytes(word) + }) + .collect(); + sparse_eval_u64_block::(input_start, &input_words, r_address) +} + /// Evaluate a shifted slice of `u64` coefficients as a multilinear polynomial at `r`. /// /// Conceptually computes: @@ -496,25 +578,7 @@ pub fn eval_initial_ram_mle( sparse_eval_u64_block::(bytecode_start, &ram_preprocessing.bytecode_words, r_address); // Inputs region (packed into u64 words in little-endian) - if !program_io.inputs.is_empty() { - let input_start = remap_address( - program_io.memory_layout.input_start, - &program_io.memory_layout, - ) - .unwrap() as usize; - let input_words: Vec = program_io - .inputs - .chunks(8) - .map(|chunk| { - let mut word = [0u8; 8]; - for (i, byte) in chunk.iter().enumerate() { - word[i] = *byte; - } - u64::from_le_bytes(word) - }) - .collect(); - acc += sparse_eval_u64_block::(input_start, &input_words, r_address); - } + acc += eval_inputs_mle::(program_io, r_address); acc } diff --git a/jolt-core/src/zkvm/ram/val_check.rs b/jolt-core/src/zkvm/ram/val_check.rs index b1a4c792c2..ab6994bff8 100644 --- a/jolt-core/src/zkvm/ram/val_check.rs +++ b/jolt-core/src/zkvm/ram/val_check.rs @@ -81,12 +81,14 @@ pub struct RamValCheckSumcheckParams { /// Val_init(r_address) evaluation to subtract on both LHS terms. pub init_eval: F, - /// Public-only portion of init_eval (bytecode + inputs), used by BlindFold constraint. + /// Public constant portion of init_eval used by BlindFold constraints. + /// In committed-program mode this is inputs-only; program image is an opening. #[cfg(feature = "zk")] pub init_eval_public: F, /// Advice contributions decomposed for BlindFold: each is (-selector, opening_id). #[cfg(feature = "zk")] pub advice_contributions: Vec<(F, OpeningId)>, + pub include_program_image_claims: bool, } impl RamValCheckSumcheckParams { @@ -98,6 +100,8 @@ impl RamValCheckSumcheckParams { gamma: F, ram_preprocessing: &super::RAMPreprocessing, program_io: &JoltDevice, + _rw_config: &ReadWriteConfig, + include_program_image_claims: bool, ) -> Self { let K = one_hot_params.ram_k; @@ -124,8 +128,11 @@ impl RamValCheckSumcheckParams { let init_eval = val_init.evaluate(&r_address.r); #[cfg(feature = "zk")] - let init_eval_public = - super::eval_initial_ram_mle::(ram_preprocessing, program_io, &r_address.r); + let init_eval_public = if include_program_image_claims { + super::eval_inputs_mle::(program_io, &r_address.r) + } else { + super::eval_initial_ram_mle::(ram_preprocessing, program_io, &r_address.r) + }; #[cfg(feature = "zk")] let advice_contributions = super::compute_advice_init_contributions( @@ -151,6 +158,7 @@ impl RamValCheckSumcheckParams { init_eval_public, #[cfg(feature = "zk")] advice_contributions, + include_program_image_claims, } } @@ -163,6 +171,7 @@ impl RamValCheckSumcheckParams { _rw_config: &ReadWriteConfig, gamma: F, opening_accumulator: &VerifierOpeningAccumulator, + include_program_image_claims: bool, ) -> Self { // (r_address, r_cycle) from RamVal/RamReadWriteChecking. let (r, _) = opening_accumulator.get_virtual_polynomial_opening( @@ -183,8 +192,22 @@ impl RamValCheckSumcheckParams { } let n_memory_vars = ram_K.log_2(); - let init_eval_public = - super::eval_initial_ram_mle::(ram_preprocessing, program_io, &r_address.r); + let init_eval_public_base = if include_program_image_claims { + super::eval_inputs_mle::(program_io, &r_address.r) + } else { + super::eval_initial_ram_mle::(ram_preprocessing, program_io, &r_address.r) + }; + let program_image_contribution = if include_program_image_claims { + opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::ProgramImageInitContributionRw, + SumcheckId::RamValCheck, + ) + .1 + } else { + F::zero() + }; + let init_eval_public = init_eval_public_base + program_image_contribution; let advice_contributions = super::compute_advice_init_contributions( opening_accumulator, &program_io.memory_layout, @@ -199,7 +222,9 @@ impl RamValCheckSumcheckParams { ); #[cfg(not(feature = "zk"))] - let _ = (init_eval_public, advice_contributions); + let _ = (init_eval_public_base, advice_contributions); + #[cfg(feature = "zk")] + let init_eval_public = init_eval_public_base; Self { T: trace_len, @@ -212,6 +237,7 @@ impl RamValCheckSumcheckParams { init_eval_public, #[cfg(feature = "zk")] advice_contributions, + include_program_image_claims, } } } @@ -248,12 +274,17 @@ impl SumcheckInstanceParams for RamValCheckSumcheckParams { fn input_claim_constraint(&self) -> InputClaimConstraint { // input_claim = (val_rw - init_eval) + γ*(val_final - init_eval) // = val_rw + γ*val_final - (1+γ)*init_eval - // = val_rw + γ*val_final - (1+γ)*(init_eval_public + Σ(sel_i * advice_i)) + // where: + // - in full-program mode: + // init_eval = init_eval_public + Σ(sel_i * advice_i) + // - in committed-program mode: + // init_eval = init_eval_public + program_image_claim + Σ(sel_i * advice_i) // // Challenge layout: // Challenge(0) = γ // Challenge(1) = -(1+γ)*init_eval_public - // Challenge(2..) = -(1+γ)*selector_i (one per advice contribution) + // Challenge(2) = -(1+γ) (program-image claim; committed mode only) + // Challenge(next..) = -(1+γ)*selector_i (one per advice contribution) let val_rw = OpeningId::virt(VirtualPolynomial::RamVal, SumcheckId::RamReadWriteChecking); let val_final = OpeningId::virt(VirtualPolynomial::RamValFinal, SumcheckId::RamOutputCheck); @@ -265,11 +296,23 @@ impl SumcheckInstanceParams for RamValCheckSumcheckParams { ), ProductTerm::single(ValueSource::Challenge(1)), ]; - for (i, (_, advice_opening_id)) in self.advice_contributions.iter().enumerate() { + let mut challenge_idx = 2; + if self.include_program_image_claims { terms.push(ProductTerm::product(vec![ - ValueSource::Challenge(i + 2), + ValueSource::Challenge(challenge_idx), + ValueSource::Opening(OpeningId::virt( + VirtualPolynomial::ProgramImageInitContributionRw, + SumcheckId::RamValCheck, + )), + ])); + challenge_idx += 1; + } + for (_, advice_opening_id) in self.advice_contributions.iter() { + terms.push(ProductTerm::product(vec![ + ValueSource::Challenge(challenge_idx), ValueSource::Opening(*advice_opening_id), ])); + challenge_idx += 1; } InputClaimConstraint::sum_of_products(terms) } @@ -278,6 +321,9 @@ impl SumcheckInstanceParams for RamValCheckSumcheckParams { fn input_constraint_challenge_values(&self, _: &dyn OpeningAccumulator) -> Vec { let one_plus_gamma = F::one() + self.gamma; let mut values = vec![self.gamma, -one_plus_gamma * self.init_eval_public]; + if self.include_program_image_claims { + values.push(-one_plus_gamma); + } for (neg_selector, _) in &self.advice_contributions { // neg_selector is already negative (-selector_i), scale by (1+γ) values.push(one_plus_gamma * *neg_selector); @@ -474,6 +520,7 @@ impl RamValCheckSumcheckVerifier { rw_config: &ReadWriteConfig, gamma: F, opening_accumulator: &VerifierOpeningAccumulator, + include_program_image_claims: bool, ) -> Self { let params = RamValCheckSumcheckParams::new_from_verifier( initial_ram_state, @@ -484,6 +531,7 @@ impl RamValCheckSumcheckVerifier { rw_config, gamma, opening_accumulator, + include_program_image_claims, ); Self { params } } diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index 2feb3fe68f..3e51915895 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -8,10 +8,11 @@ use crate::curve::JoltCurve; use crate::poly::commitment::commitment_scheme::{CommitmentScheme, ZkEvalCommitment}; #[cfg(feature = "zk")] use crate::poly::commitment::dory::bind_opening_inputs_zk; -use crate::poly::commitment::dory::{bind_opening_inputs, DoryGlobals}; +use crate::poly::commitment::dory::{bind_opening_inputs, DoryGlobals, DoryLayout}; use crate::poly::commitment::pedersen::PedersenGenerators; #[cfg(feature = "zk")] use crate::poly::lagrange_poly::LagrangeHelper; +use crate::poly::multilinear_polynomial::{MultilinearPolynomial, PolynomialEvaluation}; #[cfg(feature = "zk")] use crate::subprotocols::blindfold::{ pedersen_generator_count_for_r1cs, BakedPublicInputs, BlindFoldVerifier, @@ -22,12 +23,18 @@ use crate::subprotocols::sumcheck::BatchedSumcheck; #[cfg(feature = "zk")] use crate::subprotocols::sumcheck::SumcheckInstanceProof; #[cfg(feature = "zk")] -use crate::subprotocols::sumcheck_verifier::SumcheckInstanceParams; -#[cfg(feature = "zk")] use crate::subprotocols::univariate_skip::UniSkipFirstRoundProofVariant; -use crate::zkvm::bytecode::BytecodePreprocessing; +use crate::zkvm::bytecode::chunks::DEFAULT_COMMITTED_BYTECODE_CHUNK_COUNT; +use crate::zkvm::bytecode::chunks::{ + build_committed_bytecode_chunk_polynomials, committed_lanes, + validate_committed_bytecode_chunk_count, +}; +use crate::zkvm::bytecode::TrustedBytecodeCommitments; use crate::zkvm::claim_reductions::RegistersClaimReductionSumcheckVerifier; -use crate::zkvm::config::OneHotParams; +use crate::zkvm::config::{OneHotParams, ProgramMode}; +use crate::zkvm::program::{ + ProgramMetadata, ProgramPreprocessing, TrustedProgramCommitments, VerifierProgram, +}; #[cfg(feature = "prover")] use crate::zkvm::prover::JoltProverPreprocessing; #[cfg(feature = "zk")] @@ -35,7 +42,6 @@ use crate::zkvm::r1cs::constraints::{ OUTER_FIRST_ROUND_POLY_NUM_COEFFS, OUTER_UNIVARIATE_SKIP_DOMAIN_SIZE, PRODUCT_VIRTUAL_FIRST_ROUND_POLY_NUM_COEFFS, PRODUCT_VIRTUAL_UNIVARIATE_SKIP_DOMAIN_SIZE, }; -use crate::zkvm::ram::RAMPreprocessing; use crate::zkvm::witness::all_committed_polynomials; use crate::zkvm::Serializable; use crate::zkvm::{ @@ -44,9 +50,11 @@ use crate::zkvm::{ BytecodeReadRafSumcheckParams, }, claim_reductions::{ - AdviceClaimReductionVerifier, AdviceKind, HammingWeightClaimReductionVerifier, + AdviceClaimReductionVerifier, AdviceKind, BytecodeClaimReductionParams, + BytecodeClaimReductionVerifier, HammingWeightClaimReductionVerifier, IncClaimReductionSumcheckVerifier, InstructionLookupsClaimReductionSumcheckVerifier, - PrecommittedClaimReduction, RamRaClaimReductionSumcheckVerifier, + PrecommittedClaimReduction, ProgramImageClaimReductionParams, + ProgramImageClaimReductionVerifier, RamRaClaimReductionSumcheckVerifier, }, fiat_shamir_preamble, instruction_lookups::{ @@ -60,7 +68,7 @@ use crate::zkvm::{ output_check::OutputSumcheckVerifier, ra_virtual::RamRaVirtualSumcheckVerifier, raf_evaluation::RafEvaluationSumcheckVerifier as RamRafEvaluationSumcheckVerifier, read_write_checking::RamReadWriteCheckingVerifier, val_check::RamValCheckSumcheckVerifier, - verifier_accumulate_advice, + verifier_accumulate_advice, verifier_accumulate_program_image, }, registers::{ read_write_checking::RegistersReadWriteCheckingVerifier, @@ -71,7 +79,7 @@ use crate::zkvm::{ product::ProductVirtualRemainderVerifier, shift::ShiftSumcheckVerifier, verify_stage1_uni_skip, verify_stage2_uni_skip, }, - stage8_opening_ids, ProverDebugInfo, + stage8_opening_ids, stage8_program_openings_from_env, ProverDebugInfo, }; use crate::{ field::JoltField, @@ -91,6 +99,7 @@ use crate::{ utils::{errors::ProofVerifyError, math::Math}, zkvm::witness::CommittedPolynomial, }; +use common::constants::BYTES_PER_INSTRUCTION; #[cfg(feature = "zk")] struct StageVerifyResult { @@ -195,7 +204,6 @@ fn scale_batching_coefficients( } use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use common::jolt_device::MemoryLayout; -use tracer::instruction::Instruction; use tracer::JoltDevice; pub struct JoltVerifier< @@ -217,6 +225,10 @@ pub struct JoltVerifier< /// The advice claim reduction sumcheck effectively spans two stages (6 and 7). /// Cache the verifier state here between stages. advice_reduction_verifier_untrusted: Option>, + /// Bytecode claim reduction spans stages 6b and 7 in committed mode. + bytecode_reduction_verifier: Option>, + /// Program-image claim reduction spans stages 6b and 7 in committed mode. + program_image_reduction_verifier: Option>, pub spartan_key: UniformSpartanKey, pub one_hot_params: OneHotParams, } @@ -250,6 +262,14 @@ impl< #[inline] fn precommitted_candidate_total_vars(&self) -> Vec { let mut candidates = Vec::new(); + if self.preprocessing.program.is_committed() { + let bytecode_t_full = self.preprocessing.shared.bytecode_size().log_2(); + let chunk_log = self.preprocessing.shared.bytecode_chunk_count.log_2(); + let chunk_cycle_log_t = bytecode_t_full.saturating_sub(chunk_log); + candidates.push(committed_lanes().log_2() + chunk_cycle_log_t); + let program_image_words = self.preprocessing.shared.program_image_len_words().max(1); + candidates.push(program_image_words.next_power_of_two().log_2()); + } if self.trusted_advice_commitment.is_some() { let (sigma, nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( self.program_io.memory_layout.max_trusted_advice_size as usize, @@ -266,6 +286,18 @@ impl< candidates } + #[inline] + fn include_bytecode_in_stage8(&self) -> bool { + self.preprocessing.program.is_committed() + && stage8_program_openings_from_env().includes_bytecode() + } + + #[inline] + fn include_program_image_in_stage8(&self) -> bool { + self.preprocessing.program.is_committed() + && stage8_program_openings_from_env().includes_program_image() + } + fn stage8_opening_point(&self) -> Result, ProofVerifyError> { let native_main_vars = self.proof.trace_length.log_2() + self.one_hot_params.log_k_chunk; let mut opening_candidates: Vec<(&str, OpeningPoint)> = Vec::new(); @@ -281,6 +313,23 @@ impl< { opening_candidates.push(("untrusted_advice", point)); } + if self.include_bytecode_in_stage8() { + for chunk_idx in 0..self.preprocessing.shared.bytecode_chunk_count { + let (point, _) = self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::BytecodeChunk(chunk_idx), + SumcheckId::BytecodeClaimReduction, + ); + opening_candidates.push(("bytecode_chunk", point)); + } + } + if self.include_program_image_in_stage8() { + let (program_image_point, _) = + self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReduction, + ); + opening_candidates.push(("program_image", program_image_point)); + } let max_len = opening_candidates .iter() @@ -305,13 +354,45 @@ impl< } Ok(OpeningPoint::::new(dominant.1.r.clone())) } else { - Ok(self + let (hamming_point, _) = self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::InstructionRa(0), + SumcheckId::HammingWeightClaimReduction, + ); + let r_address_stage7 = hamming_point.r[..self.one_hot_params.log_k_chunk].to_vec(); + let r_cycle_stage6 = self .opening_accumulator .get_committed_polynomial_opening( - CommittedPolynomial::InstructionRa(0), - SumcheckId::HammingWeightClaimReduction, + CommittedPolynomial::RamInc, + SumcheckId::IncClaimReduction, ) - .0) + .0 + .r; + + match self.proof.dory_layout { + DoryLayout::AddressMajor => Ok(OpeningPoint::::new( + [r_cycle_stage6.as_slice(), r_address_stage7.as_slice()].concat(), + )), + DoryLayout::CycleMajor => { + let native_cycle = &hamming_point.r[self.one_hot_params.log_k_chunk..]; + if r_cycle_stage6.len() < native_cycle.len() { + return Err(ProofVerifyError::DoryError( + "stage6 cycle challenges shorter than native cycle vars".to_string(), + )); + } + if r_cycle_stage6[..native_cycle.len()] != *native_cycle { + return Err(ProofVerifyError::DoryError(format!( + "cycle-major Stage-8 expects stage6 cycle prefix to equal native cycle vars \ + (cycle_full_len={}, native_len={})", + r_cycle_stage6.len(), + native_cycle.len() + ))); + } + let cycle_extra = &r_cycle_stage6[native_cycle.len()..]; + let cycle_extra_and_anchor = + [cycle_extra, r_address_stage7.as_slice(), native_cycle].concat(); + Ok(OpeningPoint::::new(cycle_extra_and_anchor)) + } + } } } @@ -384,10 +465,11 @@ impl< .validate() .map_err(ProofVerifyError::InvalidOneHotConfig)?; - let min_ram_K = compute_min_ram_K( - &preprocessing.shared.ram, - &preprocessing.shared.memory_layout, - ); + let ram_preprocessing = crate::zkvm::ram::RAMPreprocessing { + min_bytecode_address: preprocessing.shared.program_meta.min_bytecode_address, + bytecode_words: vec![0; preprocessing.shared.program_meta.program_image_len_words], + }; + let min_ram_K = compute_min_ram_K(&ram_preprocessing, &preprocessing.shared.memory_layout); if !proof.ram_K.is_power_of_two() || proof.ram_K < min_ram_K { return Err(ProofVerifyError::InvalidRamK(proof.ram_K, min_ram_K)); } @@ -398,7 +480,7 @@ impl< .map_err(ProofVerifyError::InvalidReadWriteConfig)?; // Construct full params from the validated config. - let bytecode_K = preprocessing.shared.bytecode.code_size; + let bytecode_K = preprocessing.shared.bytecode_size(); let one_hot_params = OneHotParams::from_config(&proof.one_hot_config, bytecode_K, proof.ram_K); @@ -411,6 +493,8 @@ impl< opening_accumulator, advice_reduction_verifier_trusted: None, advice_reduction_verifier_untrusted: None, + bytecode_reduction_verifier: None, + program_image_reduction_verifier: None, spartan_key, one_hot_params, }) @@ -426,7 +510,7 @@ impl< &self.program_io, self.proof.ram_K, self.proof.trace_length, - self.preprocessing.shared.bytecode.entry_address, + self.preprocessing.shared.program_meta.entry_address, &mut self.transcript, ); @@ -445,6 +529,19 @@ impl< self.transcript .append_serializable(b"trusted_advice", trusted_advice_commitment); } + if let Some(trusted_bytecode) = self.preprocessing.bytecode_commitments.as_ref() { + for commitment in &trusted_bytecode.commitments { + self.transcript + .append_serializable(b"bytecode_chunk_commit", commitment); + } + } + if self.preprocessing.program.is_committed() { + let trusted = self.preprocessing.program.as_committed()?; + self.transcript.append_serializable( + b"program_image_commitment", + &trusted.program_image_commitment, + ); + } let (stage1_result, uniskip_challenge1) = self .verify_stage1() @@ -849,23 +946,55 @@ impl< self.trusted_advice_commitment.is_some(), &mut self.opening_accumulator, ); + if self.preprocessing.program.is_committed() { + verifier_accumulate_program_image::(self.proof.ram_K, &mut self.opening_accumulator); + } // Domain-separate the batching challenge. self.transcript.append_bytes(b"ram_val_check_gamma", &[]); let ram_val_check_gamma: F = self.transcript.challenge_scalar::(); - let initial_ram_state = crate::zkvm::ram::gen_ram_initial_memory_state::( - self.proof.ram_K, - &self.preprocessing.shared.ram, - &self.program_io, - ); + let initial_ram_state = if self.preprocessing.program.is_full() { + crate::zkvm::ram::gen_ram_initial_memory_state::( + self.proof.ram_K, + &self + .preprocessing + .program + .as_full() + .expect("checked is_full") + .ram, + &self.program_io, + ) + } else { + vec![0u64; self.proof.ram_K] + }; + let ram_preprocessing = if self.preprocessing.program.is_full() { + self.preprocessing + .program + .as_full() + .expect("checked is_full") + .ram + .clone() + } else { + crate::zkvm::ram::RAMPreprocessing { + min_bytecode_address: self.preprocessing.shared.program_meta.min_bytecode_address, + bytecode_words: vec![ + 0; + self.preprocessing + .shared + .program_meta + .program_image_len_words + ], + } + }; let ram_val_check = RamValCheckSumcheckVerifier::new( &initial_ram_state, &self.program_io, - &self.preprocessing.shared.ram, + &ram_preprocessing, self.proof.trace_length, self.proof.ram_K, &self.proof.rw_config, ram_val_check_gamma, &self.opening_accumulator, + self.preprocessing.program.is_committed(), ); let instances: Vec<&dyn SumcheckInstanceVerifier> = @@ -996,7 +1125,9 @@ impl< self.main_total_vars(), Some(self.proof.dory_layout), ); + tracing::info!("stage6a"); let (bytecode_read_raf_params, booleanity_params) = self.verify_stage6a()?; + tracing::info!("stage6b"); self.verify_stage6b(bytecode_read_raf_params, booleanity_params) } @@ -1010,19 +1141,48 @@ impl< ProofVerifyError, > { let n_cycle_vars = self.proof.trace_length.log_2(); + let program_preprocessing = self.preprocessing.program.full().map(|p| p.as_ref()); + let entry_bytecode_index = self + .preprocessing + .shared + .program_meta + .entry_address + .saturating_sub(self.preprocessing.shared.program_meta.min_bytecode_address) + as usize + / BYTES_PER_INSTRUCTION + + 1; let bytecode_read_raf = BytecodeReadRafAddressSumcheckVerifier::new( - &self.preprocessing.shared.bytecode, + program_preprocessing, n_cycle_vars, &self.one_hot_params, &self.opening_accumulator, &mut self.transcript, - ); + if self.preprocessing.program.is_committed() { + ProgramMode::Committed + } else { + ProgramMode::Full + }, + entry_bytecode_index, + )?; let booleanity = BooleanityAddressSumcheckVerifier::new(BooleanitySumcheckParams::new( n_cycle_vars, &self.one_hot_params, &self.opening_accumulator, &mut self.transcript, )); + tracing::info!( + "Stage 6a verifier input claims: bytecode_read_raf={} booleanity={}", + as SumcheckInstanceVerifier< + F, + ProofTranscript, + >>::get_params(&bytecode_read_raf) + .input_claim(&self.opening_accumulator), + as SumcheckInstanceVerifier< + F, + ProofTranscript, + >>::get_params(&booleanity) + .input_claim(&self.opening_accumulator), + ); let instances: Vec<&dyn SumcheckInstanceVerifier> = vec![&bytecode_read_raf, &booleanity]; @@ -1031,7 +1191,11 @@ impl< instances, &mut self.opening_accumulator, &mut self.transcript, - )?; + ) + .map_err(|err| { + tracing::error!("Stage 6a: Sumcheck verification failed"); + err + })?; #[cfg(feature = "zk")] { // Stage 6a is proven in clear and excluded from BlindFold stage data. @@ -1048,6 +1212,7 @@ impl< bytecode_read_raf_params: BytecodeReadRafSumcheckParams, booleanity_params: BooleanitySumcheckParams, ) -> Result, ProofVerifyError> { + let bytecode_reduction_seed_params = bytecode_read_raf_params.clone(); let bytecode_read_raf = BytecodeReadRafCycleSumcheckVerifier::new( bytecode_read_raf_params, &self.opening_accumulator, @@ -1100,6 +1265,40 @@ impl< &self.opening_accumulator, )); } + if self.preprocessing.program.is_committed() { + let bytecode_chunk_count = self.preprocessing.shared.bytecode_chunk_count; + let bytecode_reduction_params = BytecodeClaimReductionParams::new( + &bytecode_reduction_seed_params, + self.preprocessing.shared.bytecode_size(), + bytecode_chunk_count, + precommitted_scheduling_reference, + &self.opening_accumulator, + &mut self.transcript, + ); + self.bytecode_reduction_verifier = Some(BytecodeClaimReductionVerifier::new( + bytecode_reduction_params, + )); + + let padded_len_words = self + .preprocessing + .shared + .program_meta + .program_image_len_words + .max(1) + .next_power_of_two(); + let program_image_reduction_params = ProgramImageClaimReductionParams::new( + &self.program_io, + self.preprocessing.shared.program_meta.min_bytecode_address, + padded_len_words, + self.proof.ram_K, + precommitted_scheduling_reference, + &self.opening_accumulator, + &mut self.transcript, + ); + self.program_image_reduction_verifier = Some(ProgramImageClaimReductionVerifier::new( + program_image_reduction_params, + )); + } let mut instances: Vec<&dyn SumcheckInstanceVerifier> = vec![ &bytecode_read_raf, @@ -1115,13 +1314,23 @@ impl< if let Some(ref advice) = self.advice_reduction_verifier_untrusted { instances.push(advice); } + if let Some(ref reduction) = self.bytecode_reduction_verifier { + instances.push(reduction); + } + if let Some(ref reduction) = self.program_image_reduction_verifier { + instances.push(reduction); + } let (batching_coefficients, r_stage6b) = BatchedSumcheck::verify( &self.proof.stage6b_sumcheck_proof, instances.clone(), &mut self.opening_accumulator, &mut self.transcript, - )?; + ) + .map_err(|err| { + tracing::error!("Stage 6b: Sumcheck verification failed"); + err + })?; #[cfg(feature = "zk")] { @@ -1450,6 +1659,22 @@ impl< instances.push(advice_reduction_verifier_untrusted); } } + if let Some(bytecode_reduction_verifier) = self.bytecode_reduction_verifier.as_mut() { + let mut params = bytecode_reduction_verifier.params.borrow_mut(); + if params.num_address_phase_rounds() > 0 { + params.transition_to_address_phase(); + instances.push(bytecode_reduction_verifier); + } + } + if let Some(program_image_reduction_verifier) = + self.program_image_reduction_verifier.as_mut() + { + let mut params = program_image_reduction_verifier.params.borrow_mut(); + if params.num_address_phase_rounds() > 0 { + params.transition_to_address_phase(); + instances.push(program_image_reduction_verifier); + } + } let (batching_coefficients, r_stage7) = BatchedSumcheck::verify( &self.proof.stage7_sumcheck_proof, @@ -1497,8 +1722,64 @@ impl< }) } + fn verify_omitted_program_openings(&self) -> Result<(), ProofVerifyError> { + if !self.preprocessing.program.is_committed() { + return Ok(()); + } + + if !self.include_bytecode_in_stage8() { + let program = self.preprocessing.program.as_full()?; + let bytecode_chunk_polys = build_committed_bytecode_chunk_polynomials::( + &program.bytecode.bytecode, + self.preprocessing.shared.bytecode_chunk_count, + ); + for (chunk_idx, poly) in bytecode_chunk_polys.into_iter().enumerate() { + let (point, claim) = self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::BytecodeChunk(chunk_idx), + SumcheckId::BytecodeClaimReduction, + ); + let eval = poly.evaluate(&point.r); + if eval != claim { + return Err(ProofVerifyError::DoryError(format!( + "omitted bytecode chunk opening mismatch for chunk {chunk_idx}" + ))); + } + } + } + + if !self.include_program_image_in_stage8() { + let mut program_image_words = self + .preprocessing + .program + .as_full()? + .ram + .bytecode_words + .clone(); + if program_image_words.is_empty() { + program_image_words.push(0); + } + let padded_len = program_image_words.len().next_power_of_two().max(2); + program_image_words.resize(padded_len, 0); + let program_image_poly = MultilinearPolynomial::from(program_image_words); + let (point, claim) = self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReduction, + ); + let eval = program_image_poly.evaluate(&point.r); + if eval != claim { + return Err(ProofVerifyError::DoryError( + "omitted program image opening mismatch".to_string(), + )); + } + } + + Ok(()) + } + /// Stage 8: Dory batch opening verification. fn verify_stage8(&mut self) -> Result, ProofVerifyError> { + self.verify_omitted_program_openings()?; + let opening_point = self.stage8_opening_point()?; // 1. Collect all (polynomial, claim) pairs @@ -1587,6 +1868,37 @@ impl< include_untrusted_advice = true; } + if self.include_bytecode_in_stage8() { + let chunk_count = self.preprocessing.shared.bytecode_chunk_count; + for chunk_idx in 0..chunk_count { + let (chunk_point, chunk_claim) = + self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::BytecodeChunk(chunk_idx), + SumcheckId::BytecodeClaimReduction, + ); + let lagrange_factor = + compute_lagrange_factor::(&opening_point.r, &chunk_point.r); + polynomial_claims.push(( + CommittedPolynomial::BytecodeChunk(chunk_idx), + chunk_claim * lagrange_factor, + )); + scaling_factors.push(lagrange_factor); + } + } + if self.include_program_image_in_stage8() { + let (program_point, program_claim) = + self.opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReduction, + ); + let lagrange_factor = compute_lagrange_factor::(&opening_point.r, &program_point.r); + polynomial_claims.push(( + CommittedPolynomial::ProgramImageInit, + program_claim * lagrange_factor, + )); + scaling_factors.push(lagrange_factor); + } + // 2. Sample gamma and compute powers for RLC let claims: Vec = polynomial_claims.iter().map(|(_, c)| *c).collect(); // In non-ZK mode, absorb claims before sampling gamma for Fiat-Shamir binding. @@ -1606,6 +1918,13 @@ impl< &self.one_hot_params, include_trusted_advice, include_untrusted_advice, + if self.preprocessing.program.is_committed() { + ProgramMode::Committed + } else { + ProgramMode::Full + }, + self.preprocessing.shared.bytecode_chunk_count, + stage8_program_openings_from_env(), ); let joint_claim: F = gamma_powers .iter() @@ -1655,6 +1974,32 @@ impl< commitments_map.insert(CommittedPolynomial::UntrustedAdvice, commitment.clone()); } } + if let Some(trusted_bytecode) = self.preprocessing.bytecode_commitments.as_ref() { + for (chunk_idx, commitment) in trusted_bytecode.commitments.iter().enumerate() { + if state + .polynomial_claims + .iter() + .any(|(p, _)| *p == CommittedPolynomial::BytecodeChunk(chunk_idx)) + { + commitments_map.insert( + CommittedPolynomial::BytecodeChunk(chunk_idx), + commitment.clone(), + ); + } + } + } + if let Some(trusted_program) = self.preprocessing.program.as_committed().ok() { + if state + .polynomial_claims + .iter() + .any(|(p, _)| *p == CommittedPolynomial::ProgramImageInit) + { + commitments_map.insert( + CommittedPolynomial::ProgramImageInit, + trusted_program.program_image_commitment.clone(), + ); + } + } let joint_commitment = self.compute_joint_commitment(&mut commitments_map, &state)?; @@ -1731,10 +2076,10 @@ impl< #[derive(Debug, Clone)] pub struct JoltSharedPreprocessing { - pub bytecode: Arc, - pub ram: RAMPreprocessing, + pub program_meta: ProgramMetadata, pub memory_layout: MemoryLayout, pub max_padded_trace_length: usize, + pub bytecode_chunk_count: usize, } impl CanonicalSerialize for JoltSharedPreprocessing { @@ -1743,22 +2088,22 @@ impl CanonicalSerialize for JoltSharedPreprocessing { mut writer: W, compress: ark_serialize::Compress, ) -> Result<(), ark_serialize::SerializationError> { - self.bytecode - .as_ref() + self.program_meta .serialize_with_mode(&mut writer, compress)?; - self.ram.serialize_with_mode(&mut writer, compress)?; self.memory_layout .serialize_with_mode(&mut writer, compress)?; self.max_padded_trace_length .serialize_with_mode(&mut writer, compress)?; + self.bytecode_chunk_count + .serialize_with_mode(&mut writer, compress)?; Ok(()) } fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { - self.bytecode.serialized_size(compress) - + self.ram.serialized_size(compress) + self.program_meta.serialized_size(compress) + self.memory_layout.serialized_size(compress) + self.max_padded_trace_length.serialized_size(compress) + + self.bytecode_chunk_count.serialized_size(compress) } } @@ -1768,25 +2113,23 @@ impl CanonicalDeserialize for JoltSharedPreprocessing { compress: ark_serialize::Compress, validate: ark_serialize::Validate, ) -> Result { - let bytecode = - BytecodePreprocessing::deserialize_with_mode(&mut reader, compress, validate)?; - let ram = RAMPreprocessing::deserialize_with_mode(&mut reader, compress, validate)?; + let program_meta = ProgramMetadata::deserialize_with_mode(&mut reader, compress, validate)?; let memory_layout = MemoryLayout::deserialize_with_mode(&mut reader, compress, validate)?; let max_padded_trace_length = usize::deserialize_with_mode(&mut reader, compress, validate)?; + let bytecode_chunk_count = usize::deserialize_with_mode(&mut reader, compress, validate)?; Ok(Self { - bytecode: Arc::new(bytecode), - ram, + program_meta, memory_layout, max_padded_trace_length, + bytecode_chunk_count, }) } } impl ark_serialize::Valid for JoltSharedPreprocessing { fn check(&self) -> Result<(), ark_serialize::SerializationError> { - self.bytecode.check()?; - self.ram.check()?; + self.program_meta.check()?; self.memory_layout.check()?; Ok(()) } @@ -1795,21 +2138,52 @@ impl ark_serialize::Valid for JoltSharedPreprocessing { impl JoltSharedPreprocessing { #[tracing::instrument(skip_all, name = "JoltSharedPreprocessing::new")] pub fn new( - bytecode: Vec, + program_meta: ProgramMetadata, + memory_layout: MemoryLayout, + max_padded_trace_length: usize, + ) -> JoltSharedPreprocessing { + Self { + program_meta, + memory_layout, + max_padded_trace_length, + bytecode_chunk_count: DEFAULT_COMMITTED_BYTECODE_CHUNK_COUNT, + } + } + + #[tracing::instrument(skip_all, name = "JoltSharedPreprocessing::new_committed")] + pub fn new_committed( + program_meta: ProgramMetadata, memory_layout: MemoryLayout, - memory_init: Vec<(u64, u8)>, max_padded_trace_length: usize, - entry_address: u64, + bytecode_chunk_count: usize, ) -> JoltSharedPreprocessing { - let bytecode = Arc::new(BytecodePreprocessing::preprocess(bytecode, entry_address)); - let ram = RAMPreprocessing::preprocess(memory_init); + validate_committed_bytecode_chunk_count(bytecode_chunk_count); + assert!( + program_meta + .bytecode_len + .is_multiple_of(bytecode_chunk_count), + "bytecode chunk count ({bytecode_chunk_count}) must divide bytecode size ({})", + program_meta.bytecode_len + ); Self { - bytecode, - ram, + program_meta, memory_layout, max_padded_trace_length, + bytecode_chunk_count, } } + + pub fn bytecode_size(&self) -> usize { + self.program_meta.bytecode_len + } + + pub fn min_bytecode_address(&self) -> u64 { + self.program_meta.min_bytecode_address + } + + pub fn program_image_len_words(&self) -> usize { + self.program_meta.program_image_len_words + } } /// Serializable wrapper around [`PedersenGenerators`] for ZK setup transfer. @@ -1836,8 +2210,11 @@ where C: JoltCurve, PCS: CommitmentScheme, { + _curve: std::marker::PhantomData, pub generators: PCS::VerifierSetup, pub shared: JoltSharedPreprocessing, + pub bytecode_commitments: Option>, + pub program: VerifierProgram, pub blindfold_setup: Option>, } @@ -1876,15 +2253,37 @@ where impl, PCS: CommitmentScheme> JoltVerifierPreprocessing { - #[tracing::instrument(skip_all, name = "JoltVerifierPreprocessing::new")] - pub fn new( + #[tracing::instrument(skip_all, name = "JoltVerifierPreprocessing::new_full")] + pub fn new_full( shared: JoltSharedPreprocessing, generators: PCS::VerifierSetup, + program: Arc, blindfold_setup: Option>, ) -> Self { Self { + _curve: std::marker::PhantomData, generators, shared, + bytecode_commitments: None, + program: VerifierProgram::Full(program), + blindfold_setup, + } + } + + #[tracing::instrument(skip_all, name = "JoltVerifierPreprocessing::new_committed")] + pub fn new_committed( + shared: JoltSharedPreprocessing, + generators: PCS::VerifierSetup, + bytecode_commitments: TrustedBytecodeCommitments, + program_commitments: TrustedProgramCommitments, + blindfold_setup: Option>, + ) -> Self { + Self { + _curve: std::marker::PhantomData, + generators, + shared, + bytecode_commitments: Some(bytecode_commitments), + program: VerifierProgram::Committed(program_commitments), blindfold_setup, } } @@ -1919,6 +2318,23 @@ impl, PCS: CommitmentScheme + ZkEva let blindfold_setup = None; #[cfg(feature = "zk")] let blindfold_setup = Some(prover_preprocessing.blindfold_setup()); - Self::new(shared, generators, blindfold_setup) + match ( + &prover_preprocessing.bytecode_commitments, + &prover_preprocessing.program_commitments, + ) { + (Some(bytecode_commitments), Some(program_commitments)) => Self::new_committed( + shared, + generators, + bytecode_commitments.clone(), + program_commitments.clone(), + blindfold_setup, + ), + _ => Self::new_full( + shared, + generators, + Arc::clone(&prover_preprocessing.program), + blindfold_setup, + ), + } } } diff --git a/jolt-core/src/zkvm/witness.rs b/jolt-core/src/zkvm/witness.rs index fd91c8aaf8..38e7fd9282 100644 --- a/jolt-core/src/zkvm/witness.rs +++ b/jolt-core/src/zkvm/witness.rs @@ -7,9 +7,9 @@ use rayon::prelude::*; use tracer::instruction::Cycle; use crate::poly::commitment::commitment_scheme::StreamingCommitmentScheme; -use crate::zkvm::bytecode::BytecodePreprocessing; -use crate::zkvm::config::OneHotParams; +use crate::zkvm::config::{OneHotParams, ProgramMode}; use crate::zkvm::instruction::InstructionFlags; +use crate::zkvm::program::ProgramPreprocessing; use crate::zkvm::verifier::JoltSharedPreprocessing; use crate::{ field::JoltField, @@ -31,6 +31,8 @@ pub enum CommittedPolynomial { InstructionRa(usize), /// One-hot ra polynomial for the bytecode instance of Shout BytecodeRa(usize), + /// Dense committed bytecode chunk polynomial for committed program mode. + BytecodeChunk(usize), /// One-hot ra/wa polynomial for the RAM instance of Twist /// Note that for RAM, ra and wa are the same polynomial because /// there is at most one load or store per cycle. @@ -41,6 +43,8 @@ pub enum CommittedPolynomial { /// Untrusted advice polynomial - committed during proving, commitment in proof. /// Length cannot exceed max_trace_length. UntrustedAdvice, + /// Program image (initial RAM image) polynomial for committed program mode. + ProgramImageInit, } /// Returns a list of symbols representing all committed polynomials. @@ -58,12 +62,29 @@ pub fn all_committed_polynomials(one_hot_params: &OneHotParams) -> Vec Vec { + let mut polynomials = all_committed_polynomials(one_hot_params); + if program_mode == ProgramMode::Committed { + for i in 0..bytecode_chunk_count { + polynomials.push(CommittedPolynomial::BytecodeChunk(i)); + } + polynomials.push(CommittedPolynomial::ProgramImageInit); + } + polynomials +} + impl CommittedPolynomial { /// Generate witness data and compute tier 1 commitment for a single row pub fn stream_witness_and_commit_rows( &self, setup: &PCS::ProverSetup, preprocessing: &JoltSharedPreprocessing, + program: &ProgramPreprocessing, row_cycles: &[tracer::instruction::Cycle], one_hot_params: &OneHotParams, ) -> ::ChunkState @@ -108,7 +129,7 @@ impl CommittedPolynomial { let row: Vec> = row_cycles .iter() .map(|cycle| { - let pc = preprocessing.bytecode.get_pc(cycle); + let pc = program.get_pc(cycle); Some(one_hot_params.bytecode_pc_chunk(pc, *idx) as usize) }) .collect(); @@ -127,8 +148,11 @@ impl CommittedPolynomial { .collect(); PCS::process_chunk_onehot(setup, one_hot_params.k_chunk, &row) } - CommittedPolynomial::TrustedAdvice | CommittedPolynomial::UntrustedAdvice => { - panic!("Advice polynomials should not use streaming witness generation") + CommittedPolynomial::TrustedAdvice + | CommittedPolynomial::UntrustedAdvice + | CommittedPolynomial::ProgramImageInit + | CommittedPolynomial::BytecodeChunk(_) => { + panic!("Precommitted polynomials should not use streaming witness generation") } } } @@ -136,7 +160,7 @@ impl CommittedPolynomial { #[tracing::instrument(skip_all, name = "CommittedPolynomial::generate_witness")] pub fn generate_witness( &self, - bytecode_preprocessing: &BytecodePreprocessing, + bytecode_preprocessing: &crate::zkvm::bytecode::BytecodePreprocessing, memory_layout: &MemoryLayout, trace: &[Cycle], one_hot_params: Option<&OneHotParams>, @@ -212,8 +236,11 @@ impl CommittedPolynomial { one_hot_params.k_chunk, )) } - CommittedPolynomial::TrustedAdvice | CommittedPolynomial::UntrustedAdvice => { - panic!("Advice polynomials should not use generate_witness") + CommittedPolynomial::TrustedAdvice + | CommittedPolynomial::UntrustedAdvice + | CommittedPolynomial::ProgramImageInit + | CommittedPolynomial::BytecodeChunk(_) => { + panic!("Precommitted polynomials should not use generate_witness") } } } @@ -269,6 +296,10 @@ pub enum VirtualPolynomial { OpFlags(CircuitFlags), InstructionFlags(InstructionFlags), LookupTableFlag(usize), + BytecodeValStage(usize), BytecodeReadRafAddrClaim, BooleanityAddrClaim, + BytecodeClaimReductionIntermediate, + ProgramImageInitContributionRw, + ProgramImageInitContributionRaf, } diff --git a/jolt-sdk/macros/src/lib.rs b/jolt-sdk/macros/src/lib.rs index 49797bd639..9bd69b1f86 100644 --- a/jolt-sdk/macros/src/lib.rs +++ b/jolt-sdk/macros/src/lib.rs @@ -77,8 +77,11 @@ impl MacroBuilder { let trace_to_file_fn = self.make_trace_to_file_func(); let compile_fn = self.make_compile_func(); let preprocess_shared_fn = self.make_preprocess_shared_func(); + let preprocess_shared_committed_fn = self.make_preprocess_shared_committed_func(); let preprocess_prover_fn = self.make_preprocess_prover_func(); + let preprocess_committed_prover_fn = self.make_preprocess_committed_prover_func(); let preprocess_verifier_fn = self.make_preprocess_verifier_func(); + let preprocess_committed_verifier_fn = self.make_preprocess_committed_verifier_func(); let verifier_preprocess_from_prover_fn = self.make_preprocess_from_prover_func(); let commit_trusted_advice_fn = self.make_commit_trusted_advice_func(); let prove_fn = self.make_prove_func(); @@ -111,8 +114,11 @@ impl MacroBuilder { #trace_to_file_fn #compile_fn #preprocess_shared_fn + #preprocess_shared_committed_fn #preprocess_prover_fn + #preprocess_committed_prover_fn #preprocess_verifier_fn + #preprocess_committed_verifier_fn #verifier_preprocess_from_prover_fn #commit_trusted_advice_fn #prove_fn @@ -466,11 +472,11 @@ impl MacroBuilder { quote! { #[cfg(all(not(target_arch = "wasm32"), not(feature = "guest")))] pub fn #preprocess_shared_fn_name(program: &mut jolt::host::Program) - -> jolt::JoltSharedPreprocessing + -> (jolt::JoltSharedPreprocessing, std::sync::Arc) { #imports - let (bytecode, memory_init, program_size, e_entry) = program.decode(); + let (bytecode, memory_init, program_size, _e_entry) = program.decode(); let memory_config = MemoryConfig { max_input_size: #max_input_size, max_output_size: #max_output_size, @@ -482,15 +488,46 @@ impl MacroBuilder { }; let memory_layout = MemoryLayout::new(&memory_config); + let program_data = std::sync::Arc::new( + jolt::ProgramPreprocessing::preprocess(bytecode, memory_init) + ); let preprocessing = JoltSharedPreprocessing::new( - bytecode, + program_data.meta(), memory_layout, - memory_init, #max_trace_length, - e_entry, ); - preprocessing + (preprocessing, program_data) + } + } + } + + fn make_preprocess_shared_committed_func(&self) -> TokenStream2 { + let imports = self.make_imports(); + + let fn_name = self.get_func_name(); + let preprocess_shared_fn_name = + Ident::new(&format!("preprocess_shared_{fn_name}"), fn_name.span()); + let preprocess_shared_committed_fn_name = + Ident::new(&format!("preprocess_shared_committed_{fn_name}"), fn_name.span()); + quote! { + #[cfg(all(not(target_arch = "wasm32"), not(feature = "guest")))] + pub fn #preprocess_shared_committed_fn_name( + program: &mut jolt::host::Program, + bytecode_chunk_count: usize, + ) -> (jolt::JoltSharedPreprocessing, std::sync::Arc) + { + #imports + + let (shared_preprocessing, program_data) = #preprocess_shared_fn_name(program); + let shared_preprocessing = JoltSharedPreprocessing::new_committed( + program_data.meta(), + shared_preprocessing.memory_layout, + shared_preprocessing.max_padded_trace_length, + bytecode_chunk_count, + ); + + (shared_preprocessing, program_data) } } } @@ -503,12 +540,16 @@ impl MacroBuilder { Ident::new(&format!("preprocess_prover_{fn_name}"), fn_name.span()); quote! { #[cfg(all(not(target_arch = "wasm32"), not(feature = "guest")))] - pub fn #preprocess_prover_fn_name(shared_preprocessing: jolt::JoltSharedPreprocessing) + pub fn #preprocess_prover_fn_name( + shared_preprocessing: (jolt::JoltSharedPreprocessing, std::sync::Arc) + ) -> jolt::JoltProverPreprocessing { #imports + let (shared_preprocessing, program_data) = shared_preprocessing; let prover_preprocessing = JoltProverPreprocessing::new( shared_preprocessing, + program_data, ); prover_preprocessing @@ -516,6 +557,30 @@ impl MacroBuilder { } } + fn make_preprocess_committed_prover_func(&self) -> TokenStream2 { + let imports = self.make_imports(); + + let fn_name = self.get_func_name(); + let preprocess_committed_fn_name = + Ident::new(&format!("preprocess_committed_{fn_name}"), fn_name.span()); + let preprocess_shared_committed_fn_name = + Ident::new(&format!("preprocess_shared_committed_{fn_name}"), fn_name.span()); + quote! { + #[cfg(all(not(target_arch = "wasm32"), not(feature = "guest")))] + pub fn #preprocess_committed_fn_name( + program: &mut jolt::host::Program, + bytecode_chunk_count: usize, + ) + -> jolt::JoltProverPreprocessing + { + #imports + let (shared_preprocessing, program_data) = + #preprocess_shared_committed_fn_name(program, bytecode_chunk_count); + JoltProverPreprocessing::new_committed(shared_preprocessing, program_data) + } + } + } + fn make_preprocess_verifier_func(&self) -> TokenStream2 { let fn_name = self.get_func_name(); let preprocess_verifier_fn_name = @@ -524,12 +589,45 @@ impl MacroBuilder { quote! { #[cfg(all(not(target_arch = "wasm32"), not(feature = "guest")))] pub fn #preprocess_verifier_fn_name( - shared_preprocess: jolt::JoltSharedPreprocessing, + shared_preprocess: (jolt::JoltSharedPreprocessing, std::sync::Arc), + generators: ::VerifierSetup, + blindfold_setup: Option>, + ) -> jolt::JoltVerifierPreprocessing + { + let (shared_preprocess, program_data) = shared_preprocess; + jolt::JoltVerifierPreprocessing::new_full( + shared_preprocess, + generators, + program_data, + blindfold_setup, + ) + } + } + } + + fn make_preprocess_committed_verifier_func(&self) -> TokenStream2 { + let fn_name = self.get_func_name(); + let preprocess_committed_verifier_fn_name = + Ident::new(&format!("preprocess_committed_verifier_{fn_name}"), fn_name.span()); + + quote! { + #[cfg(all(not(target_arch = "wasm32"), not(feature = "guest")))] + pub fn #preprocess_committed_verifier_fn_name( + shared_preprocess: (jolt::JoltSharedPreprocessing, std::sync::Arc), generators: ::VerifierSetup, + bytecode_commitments: jolt::TrustedBytecodeCommitments, + program_commitments: jolt::TrustedProgramCommitments, blindfold_setup: Option>, ) -> jolt::JoltVerifierPreprocessing { - jolt::JoltVerifierPreprocessing::new(shared_preprocess, generators, blindfold_setup) + let (shared_preprocess, _program_data) = shared_preprocess; + jolt::JoltVerifierPreprocessing::new_committed( + shared_preprocess, + generators, + bytecode_commitments, + program_commitments, + blindfold_setup, + ) } } } diff --git a/jolt-sdk/src/host_utils.rs b/jolt-sdk/src/host_utils.rs index 5da343ce19..8994f63c5c 100644 --- a/jolt-sdk/src/host_utils.rs +++ b/jolt-sdk/src/host_utils.rs @@ -13,8 +13,11 @@ pub use jolt_core::field::JoltField; pub use jolt_core::guest; pub use jolt_core::poly::commitment::dory::DoryCommitmentScheme as PCS; pub use jolt_core::zkvm::{ - proof_serialization::JoltProof, verifier::JoltSharedPreprocessing, - verifier::JoltVerifierPreprocessing, RV64IMACProof, RV64IMACVerifier, Serializable, + bytecode::TrustedBytecodeCommitments, + program::ProgramPreprocessing, proof_serialization::JoltProof, + program::TrustedProgramCommitments, + verifier::JoltSharedPreprocessing, verifier::JoltVerifierPreprocessing, RV64IMACProof, + RV64IMACVerifier, Serializable, }; pub use jolt_core::AdviceTape; From 97d16e41e12c1795653d4628e4fcf5dcb507f2e7 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Mon, 16 Mar 2026 21:00:18 -0700 Subject: [PATCH 04/20] feat(zkvm): add committed program/bytecode reduction plumbing with zk constraints Introduce committed program/bytecode helpers (`program.rs`, `bytecode/chunks.rs`) and wire bytecode/program-image claim reductions into the zk verifier constraint path. Also update Dory geometry/one-hot integration and related tests to keep committed-mode openings and challenge binding consistent. Co-authored-by: Quang Dao --- .../src/poly/commitment/dory/dory_globals.rs | 20 -- jolt-core/src/poly/commitment/dory/tests.rs | 20 +- jolt-core/src/poly/one_hot_polynomial.rs | 10 +- jolt-core/src/zkvm/bytecode/chunks.rs | 195 ++++++++++ .../src/zkvm/bytecode/read_raf_checking.rs | 74 +++- .../src/zkvm/claim_reductions/bytecode.rs | 75 ++++ .../zkvm/claim_reductions/program_image.rs | 58 +++ jolt-core/src/zkvm/program.rs | 334 ++++++++++++++++++ jolt-core/src/zkvm/verifier.rs | 2 + jolt-sdk/macros/src/lib.rs | 18 +- jolt-sdk/src/host_utils.rs | 5 +- 11 files changed, 761 insertions(+), 50 deletions(-) create mode 100644 jolt-core/src/zkvm/bytecode/chunks.rs create mode 100644 jolt-core/src/zkvm/program.rs diff --git a/jolt-core/src/poly/commitment/dory/dory_globals.rs b/jolt-core/src/poly/commitment/dory/dory_globals.rs index 6958a5741a..82b8987a15 100644 --- a/jolt-core/src/poly/commitment/dory/dory_globals.rs +++ b/jolt-core/src/poly/commitment/dory/dory_globals.rs @@ -368,26 +368,6 @@ impl DoryGlobals { (num_rows * num_cols) / t } - /// For `AddressMajor`, each Dory matrix row corresponds to this many cycles. - /// - /// Equivalent to `T / num_rows` and to `num_cols / dense_stride`. - pub fn address_major_cycles_per_row() -> usize { - let num_cols = Self::get_num_columns(); - let dense_stride = Self::dense_stride(); - assert!(dense_stride > 0, "Dense stride must be positive"); - assert_eq!( - num_cols % dense_stride, - 0, - "Expected num_cols to be divisible by dense stride" - ); - let cycles_per_row = num_cols / dense_stride; - assert!( - cycles_per_row > 0, - "AddressMajor row must contain at least one cycle" - ); - cycles_per_row - } - fn set_max_num_rows_for_context(max_num_rows: usize, context: DoryContext) { match context { DoryContext::Main => { diff --git a/jolt-core/src/poly/commitment/dory/tests.rs b/jolt-core/src/poly/commitment/dory/tests.rs index 06100570fe..69052a653d 100644 --- a/jolt-core/src/poly/commitment/dory/tests.rs +++ b/jolt-core/src/poly/commitment/dory/tests.rs @@ -983,16 +983,26 @@ mod tests { let vmp_result = rlc_poly.vector_matrix_product(&left_vec); let mut expected = vec![Fr::zero(); num_columns]; - let cycles_per_row = DoryGlobals::address_major_cycles_per_row(); + let dense_stride = DoryGlobals::dense_stride(); + let cycles_per_row = num_columns / dense_stride; // Dense contribution for AddressMajor layout: // Dense coefficients occupy evenly-spaced columns (every K-th column). // Coefficient i maps to: row = i / cycles_per_row, col = (i % cycles_per_row) * K for (i, &coeff) in rlc_dense.iter().enumerate() { - let row = i / cycles_per_row; - let col = (i % cycles_per_row) * K; - if row < num_rows && col < num_columns { - expected[col] += left_vec[row] * coeff; + if cycles_per_row == 0 { + let scaled_index = i * dense_stride; + let row = scaled_index / num_columns; + let col = scaled_index % num_columns; + if row < num_rows && col < num_columns { + expected[col] += left_vec[row] * coeff; + } + } else { + let row = i / cycles_per_row; + let col = (i % cycles_per_row) * K; + if row < num_rows && col < num_columns { + expected[col] += left_vec[row] * coeff; + } } } diff --git a/jolt-core/src/poly/one_hot_polynomial.rs b/jolt-core/src/poly/one_hot_polynomial.rs index 7134c04aac..4cef02ac7b 100644 --- a/jolt-core/src/poly/one_hot_polynomial.rs +++ b/jolt-core/src/poly/one_hot_polynomial.rs @@ -56,15 +56,9 @@ impl OneHotPolynomial { /// /// Note: the Dory matrix may be square or almost-square depending on `log2(K*T)`. pub fn num_rows(&self) -> usize { - let t = DoryGlobals::get_T(); match DoryGlobals::get_layout() { - DoryLayout::AddressMajor => { - if t == 0 { - return 0; - } - t.div_ceil(DoryGlobals::address_major_cycles_per_row()) - } - DoryLayout::CycleMajor => (t * self.K).div_ceil(DoryGlobals::get_num_columns()), + DoryLayout::AddressMajor => DoryGlobals::get_max_num_rows(), + DoryLayout::CycleMajor => (DoryGlobals::get_T() * self.K).div_ceil(DoryGlobals::get_num_columns()), } } diff --git a/jolt-core/src/zkvm/bytecode/chunks.rs b/jolt-core/src/zkvm/bytecode/chunks.rs new file mode 100644 index 0000000000..acdf22075e --- /dev/null +++ b/jolt-core/src/zkvm/bytecode/chunks.rs @@ -0,0 +1,195 @@ +use crate::field::JoltField; +use crate::poly::commitment::dory::DoryGlobals; +use crate::poly::multilinear_polynomial::MultilinearPolynomial; +use crate::utils::thread::unsafe_allocate_zero_vec; +use crate::zkvm::instruction::{ + Flags, InstructionLookup, InterleavedBitsMarker, NUM_CIRCUIT_FLAGS, NUM_INSTRUCTION_FLAGS, +}; +use crate::zkvm::lookup_table::LookupTables; +use common::constants::{REGISTER_COUNT, XLEN}; +use tracer::instruction::Instruction; + +/// Total number of lanes encoded by committed-bytecode rows. +pub const fn total_lanes() -> usize { + 3 * (REGISTER_COUNT as usize) + + 2 + + NUM_CIRCUIT_FLAGS + + NUM_INSTRUCTION_FLAGS + + as strum::EnumCount>::COUNT + + 1 +} + +/// Fixed lane capacity for committed bytecode rows. +pub const COMMITTED_BYTECODE_LANE_CAPACITY: usize = total_lanes().next_power_of_two(); + +#[inline(always)] +pub const fn committed_lanes() -> usize { + COMMITTED_BYTECODE_LANE_CAPACITY +} + +pub const DEFAULT_COMMITTED_BYTECODE_CHUNK_COUNT: usize = 1; + +#[inline] +pub fn validate_committed_bytecode_chunk_count(chunk_count: usize) { + assert!(chunk_count > 0, "bytecode chunk count must be non-zero"); + assert!( + chunk_count.is_power_of_two(), + "bytecode chunk count must be a power of two" + ); +} + +#[inline(always)] +pub fn validate_committed_bytecode_chunking_for_len(bytecode_len: usize, chunk_count: usize) { + validate_committed_bytecode_chunk_count(chunk_count); + assert!( + bytecode_len.is_multiple_of(chunk_count), + "bytecode length ({bytecode_len}) must be divisible by chunk count ({chunk_count})" + ); +} + +#[inline(always)] +pub fn committed_bytecode_chunk_cycle_len(bytecode_len: usize, chunk_count: usize) -> usize { + validate_committed_bytecode_chunking_for_len(bytecode_len, chunk_count); + bytecode_len / chunk_count +} + +#[derive(Clone, Copy, Debug)] +pub struct BytecodeLaneLayout { + pub rs1_start: usize, + pub rs2_start: usize, + pub rd_start: usize, + pub unexp_pc_idx: usize, + pub imm_idx: usize, + pub circuit_start: usize, + pub instr_start: usize, + pub lookup_start: usize, + pub raf_flag_idx: usize, +} + +impl BytecodeLaneLayout { + pub const fn new() -> Self { + let reg_count = REGISTER_COUNT as usize; + let rs1_start = 0usize; + let rs2_start = rs1_start + reg_count; + let rd_start = rs2_start + reg_count; + let unexp_pc_idx = rd_start + reg_count; + let imm_idx = unexp_pc_idx + 1; + let circuit_start = imm_idx + 1; + let instr_start = circuit_start + NUM_CIRCUIT_FLAGS; + let lookup_start = instr_start + NUM_INSTRUCTION_FLAGS; + let raf_flag_idx = lookup_start + as strum::EnumCount>::COUNT; + Self { + rs1_start, + rs2_start, + rd_start, + unexp_pc_idx, + imm_idx, + circuit_start, + instr_start, + lookup_start, + raf_flag_idx, + } + } +} + +pub const BYTECODE_LANE_LAYOUT: BytecodeLaneLayout = BytecodeLaneLayout::new(); + +#[derive(Clone, Copy, Debug)] +pub enum ActiveLaneValue { + One, + Scalar(F), +} + +#[inline(always)] +pub fn for_each_active_lane_value( + instr: &Instruction, + mut visit: impl FnMut(usize, ActiveLaneValue), +) { + let l = BYTECODE_LANE_LAYOUT; + + let normalized = instr.normalize(); + let circuit_flags = ::circuit_flags(instr); + let instr_flags = ::instruction_flags(instr); + let lookup_idx = >::lookup_table(instr) + .map(|t| LookupTables::::enum_index(&t)); + let raf_flag = !InterleavedBitsMarker::is_interleaved_operands(&circuit_flags); + + if let Some(r) = normalized.operands.rs1 { + visit(l.rs1_start + (r as usize), ActiveLaneValue::One); + } + if let Some(r) = normalized.operands.rs2 { + visit(l.rs2_start + (r as usize), ActiveLaneValue::One); + } + if let Some(r) = normalized.operands.rd { + visit(l.rd_start + (r as usize), ActiveLaneValue::One); + } + + let unexpanded_pc = F::from_u64(normalized.address as u64); + if !unexpanded_pc.is_zero() { + visit(l.unexp_pc_idx, ActiveLaneValue::Scalar(unexpanded_pc)); + } + let imm = F::from_i128(normalized.operands.imm); + if !imm.is_zero() { + visit(l.imm_idx, ActiveLaneValue::Scalar(imm)); + } + + for i in 0..NUM_CIRCUIT_FLAGS { + if circuit_flags[i] { + visit(l.circuit_start + i, ActiveLaneValue::One); + } + } + for i in 0..NUM_INSTRUCTION_FLAGS { + if instr_flags[i] { + visit(l.instr_start + i, ActiveLaneValue::One); + } + } + if let Some(t) = lookup_idx { + visit(l.lookup_start + t, ActiveLaneValue::One); + } + if raf_flag { + visit(l.raf_flag_idx, ActiveLaneValue::One); + } +} + +#[tracing::instrument( + skip_all, + name = "bytecode::build_committed_bytecode_chunk_polynomials" +)] +pub fn build_committed_bytecode_chunk_polynomials( + instructions: &[Instruction], + chunk_count: usize, +) -> Vec> { + let bytecode_len = instructions.len(); + validate_committed_bytecode_chunking_for_len(bytecode_len, chunk_count); + + let chunk_cycle_len = committed_bytecode_chunk_cycle_len(bytecode_len, chunk_count); + let lane_capacity = committed_lanes(); + let mut chunk_coeffs: Vec> = (0..chunk_count) + .map(|_| unsafe_allocate_zero_vec(lane_capacity * chunk_cycle_len)) + .collect(); + + for (cycle, instr) in instructions.iter().enumerate() { + let cycle_chunk_idx = cycle / chunk_cycle_len; + let chunk_cycle = cycle % chunk_cycle_len; + let coeffs = &mut chunk_coeffs[cycle_chunk_idx]; + + for_each_active_lane_value::(instr, |global_lane, lane_val| { + let idx = DoryGlobals::get_layout().address_cycle_to_index( + global_lane, + chunk_cycle, + lane_capacity, + chunk_cycle_len, + ); + let lane_value = match lane_val { + ActiveLaneValue::One => F::one(), + ActiveLaneValue::Scalar(v) => v, + }; + coeffs[idx] += lane_value; + }); + } + + chunk_coeffs + .into_iter() + .map(MultilinearPolynomial::from) + .collect() +} diff --git a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs index 8d3997ddd8..8ba8fc6df4 100644 --- a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs +++ b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs @@ -2313,19 +2313,38 @@ impl SumcheckInstanceParams for BytecodeReadRafSumcheckParams Option { - let factors: Vec = (0..self.d) + let ra_factors: Vec = (0..self.d) .map(|i| { - let opening = OpeningId::committed( + ValueSource::Opening(OpeningId::committed( CommittedPolynomial::BytecodeRa(i), SumcheckId::BytecodeReadRaf, - ); - ValueSource::Opening(opening) + )) }) .collect(); - let terms = vec![ProductTerm::scaled(ValueSource::Challenge(0), factors)]; - - Some(OutputClaimConstraint::sum_of_products(terms)) + if self.use_staged_val_claims { + // In committed mode, verifier does not materialize stage Val polynomials. + // Encode output as: + // ra_prod * (Σ_stage coeff_stage * ValStage(stage) + const_term) + // where coeff_stage / const_term are public challenge values. + let mut terms = Vec::with_capacity(N_STAGES + 1); + for stage in 0..N_STAGES { + let mut factors = ra_factors.clone(); + factors.push(ValueSource::Opening(OpeningId::virt( + VirtualPolynomial::BytecodeValStage(stage), + SumcheckId::BytecodeReadRafAddressPhase, + ))); + terms.push(ProductTerm::scaled(ValueSource::Challenge(stage), factors)); + } + terms.push(ProductTerm::scaled( + ValueSource::Challenge(N_STAGES), + ra_factors, + )); + Some(OutputClaimConstraint::sum_of_products(terms)) + } else { + let terms = vec![ProductTerm::scaled(ValueSource::Challenge(0), ra_factors)]; + Some(OutputClaimConstraint::sum_of_products(terms)) + } } #[cfg(feature = "zk")] @@ -2333,7 +2352,46 @@ impl SumcheckInstanceParams for BytecodeReadRafSumcheckParams = self + .r_cycles + .iter() + .map(|r_cycle| EqPolynomial::::mle(r_cycle, &r_cycle_prime.r)) + .collect(); + + let mut coeffs: Vec = (0..N_STAGES) + .map(|stage| self.gamma_powers[stage] * eq_cycles[stage]) + .collect(); + + let int_poly_contrib_by_stage = [ + int_poly * self.gamma_powers[5], // RAF for Stage1 + F::zero(), + int_poly * self.gamma_powers[4], // RAF for Stage3 + F::zero(), + F::zero(), + ]; + let int_contrib: F = (0..N_STAGES) + .map(|stage| { + int_poly_contrib_by_stage[stage] * eq_cycles[stage] * self.gamma_powers[stage] + }) + .sum(); + + let log_k = self.log_K; + let e = self.entry_bytecode_index; + let entry_bits: Vec = (0..log_k) + .map(|i| F::from_u64(((e >> (log_k - 1 - i)) & 1) as u64)) + .collect(); + let f_entry_at_r_addr = EqPolynomial::::mle(&entry_bits, &r_address_prime.r); + let zeros: Vec = vec![F::Challenge::default(); r_cycle_prime.r.len()]; + let eq_zero_at_r_cycle = EqPolynomial::::mle(&zeros, &r_cycle_prime.r); + let entry_contrib = self.entry_gamma * f_entry_at_r_addr * eq_zero_at_r_cycle; + + coeffs.push(int_contrib + entry_contrib); + return coeffs; + } + + // Prover stores bound values before clearing polys; verifier evaluates directly. let val: F = if let Some(bound_val_polys) = &self.bound_val_polys { bound_val_polys .iter() diff --git a/jolt-core/src/zkvm/claim_reductions/bytecode.rs b/jolt-core/src/zkvm/claim_reductions/bytecode.rs index 90f87c3875..bf954e6e4a 100644 --- a/jolt-core/src/zkvm/claim_reductions/bytecode.rs +++ b/jolt-core/src/zkvm/claim_reductions/bytecode.rs @@ -9,11 +9,15 @@ use crate::field::JoltField; use crate::poly::commitment::dory::{DoryGlobals, DoryLayout}; use crate::poly::eq_poly::EqPolynomial; use crate::poly::multilinear_polynomial::{BindingOrder, MultilinearPolynomial, PolynomialBinding}; +#[cfg(feature = "zk")] +use crate::poly::opening_proof::OpeningId; use crate::poly::opening_proof::{ OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, SumcheckId, VerifierOpeningAccumulator, BIG_ENDIAN, LITTLE_ENDIAN, }; use crate::poly::unipoly::UniPoly; +#[cfg(feature = "zk")] +use crate::subprotocols::blindfold::{InputClaimConstraint, OutputClaimConstraint, ValueSource}; use crate::subprotocols::sumcheck_prover::SumcheckInstanceProver; use crate::subprotocols::sumcheck_verifier::{SumcheckInstanceParams, SumcheckInstanceVerifier}; use crate::transcripts::Transcript; @@ -211,6 +215,77 @@ impl SumcheckInstanceParams for BytecodeClaimReductionParams self.dense_cycle_prefix_vars, ) } + + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + match self.phase { + PrecommittedPhase::CycleVariables => { + let openings: Vec = (0..NUM_VAL_STAGES) + .map(|stage| { + OpeningId::virt( + VirtualPolynomial::BytecodeValStage(stage), + SumcheckId::BytecodeReadRafAddressPhase, + ) + }) + .collect(); + InputClaimConstraint::all_weighted_openings(&openings) + } + PrecommittedPhase::AddressVariables => InputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BytecodeClaimReductionIntermediate, + SumcheckId::BytecodeClaimReductionCyclePhase, + )), + } + } + + #[cfg(feature = "zk")] + fn input_constraint_challenge_values(&self, _: &dyn OpeningAccumulator) -> Vec { + match self.phase { + PrecommittedPhase::CycleVariables => self.eta_powers.to_vec(), + PrecommittedPhase::AddressVariables => Vec::new(), + } + } + + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + match self.phase { + PrecommittedPhase::CycleVariables => { + Some(OutputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BytecodeClaimReductionIntermediate, + SumcheckId::BytecodeClaimReductionCyclePhase, + ))) + } + PrecommittedPhase::AddressVariables => { + let terms = (0..self.bytecode_chunk_count) + .map(|chunk_idx| { + let opening = OpeningId::committed( + CommittedPolynomial::BytecodeChunk(chunk_idx), + SumcheckId::BytecodeClaimReduction, + ); + ( + ValueSource::Challenge(chunk_idx), + ValueSource::Opening(opening), + ) + }) + .collect(); + Some(OutputClaimConstraint::linear(terms)) + } + } + } + + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { + match self.phase { + PrecommittedPhase::CycleVariables => vec![], + PrecommittedPhase::AddressVariables => { + let eq_combined = evaluate_bytecode_eq_combined(self, sumcheck_challenges); + let scale: F = precommitted_skip_round_scale(&self.precommitted); + self.chunk_rbc_weights + .iter() + .map(|w| *w * eq_combined * scale) + .collect() + } + } + } } #[derive(Allocative)] diff --git a/jolt-core/src/zkvm/claim_reductions/program_image.rs b/jolt-core/src/zkvm/claim_reductions/program_image.rs index 438cbec072..951afce066 100644 --- a/jolt-core/src/zkvm/claim_reductions/program_image.rs +++ b/jolt-core/src/zkvm/claim_reductions/program_image.rs @@ -11,11 +11,15 @@ use crate::field::JoltField; use crate::poly::commitment::dory::DoryGlobals; use crate::poly::eq_poly::EqPolynomial; use crate::poly::multilinear_polynomial::MultilinearPolynomial; +#[cfg(feature = "zk")] +use crate::poly::opening_proof::OpeningId; use crate::poly::opening_proof::{ OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, SumcheckId, VerifierOpeningAccumulator, BIG_ENDIAN, LITTLE_ENDIAN, }; use crate::poly::unipoly::UniPoly; +#[cfg(feature = "zk")] +use crate::subprotocols::blindfold::{InputClaimConstraint, OutputClaimConstraint, ValueSource}; use crate::subprotocols::sumcheck_prover::SumcheckInstanceProver; use crate::subprotocols::sumcheck_verifier::{SumcheckInstanceParams, SumcheckInstanceVerifier}; use crate::transcripts::Transcript; @@ -176,6 +180,60 @@ impl SumcheckInstanceParams for ProgramImageClaimReductionParam self.precommitted .normalize_opening_point(self.is_cycle_phase(), challenges, self.log_t) } + + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + match self.phase { + PrecommittedPhase::CycleVariables => InputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::ProgramImageInitContributionRw, + SumcheckId::RamValCheck, + )), + PrecommittedPhase::AddressVariables => { + InputClaimConstraint::direct(OpeningId::committed( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReductionCyclePhase, + )) + } + } + } + + #[cfg(feature = "zk")] + fn input_constraint_challenge_values(&self, _: &dyn OpeningAccumulator) -> Vec { + Vec::new() + } + + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + match self.phase { + PrecommittedPhase::CycleVariables => { + Some(OutputClaimConstraint::direct(OpeningId::committed( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReductionCyclePhase, + ))) + } + PrecommittedPhase::AddressVariables => Some(OutputClaimConstraint::linear(vec![( + ValueSource::Challenge(0), + ValueSource::Opening(OpeningId::committed( + CommittedPolynomial::ProgramImageInit, + SumcheckId::ProgramImageClaimReduction, + )), + )])), + } + } + + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { + match self.phase { + PrecommittedPhase::CycleVariables => vec![], + PrecommittedPhase::AddressVariables => { + let opening_point = self.normalize_opening_point(sumcheck_challenges); + let eq_combined = + self.selector_rw * EqPolynomial::mle(&opening_point.r, &self.r_addr_rw_reduced); + let scale: F = precommitted_skip_round_scale(&self.precommitted); + vec![eq_combined * scale] + } + } + } } impl PrecomittedParams for ProgramImageClaimReductionParams { diff --git a/jolt-core/src/zkvm/program.rs b/jolt-core/src/zkvm/program.rs new file mode 100644 index 0000000000..c10c18bd18 --- /dev/null +++ b/jolt-core/src/zkvm/program.rs @@ -0,0 +1,334 @@ +use std::io::{Read, Write}; +use std::sync::Arc; + +use ark_serialize::{ + CanonicalDeserialize, CanonicalSerialize, Compress, SerializationError, Valid, Validate, +}; + +use crate::poly::commitment::commitment_scheme::CommitmentScheme; +use crate::poly::commitment::dory::{DoryContext, DoryGlobals}; +use crate::poly::multilinear_polynomial::MultilinearPolynomial; +use crate::utils::errors::ProofVerifyError; +use crate::utils::math::Math; +use crate::zkvm::bytecode::BytecodePreprocessing; +use crate::zkvm::ram::RAMPreprocessing; +use tracer::instruction::{Cycle, Instruction}; + +#[derive(Debug, Clone, CanonicalSerialize, CanonicalDeserialize)] +pub struct ProgramPreprocessing { + pub bytecode: BytecodePreprocessing, + pub ram: RAMPreprocessing, +} + +impl Default for ProgramPreprocessing { + fn default() -> Self { + Self { + bytecode: BytecodePreprocessing::default(), + ram: RAMPreprocessing { + min_bytecode_address: 0, + bytecode_words: Vec::new(), + }, + } + } +} + +impl ProgramPreprocessing { + #[tracing::instrument(skip_all, name = "ProgramPreprocessing::preprocess")] + pub fn preprocess(instructions: Vec, memory_init: Vec<(u64, u8)>) -> Self { + let entry_address = instructions + .first() + .map(|instr| instr.normalize().address as u64) + .unwrap_or(0); + Self { + bytecode: BytecodePreprocessing::preprocess(instructions, entry_address), + ram: RAMPreprocessing::preprocess(memory_init), + } + } + + pub fn bytecode_len(&self) -> usize { + self.bytecode.code_size + } + + pub fn program_image_len_words(&self) -> usize { + self.ram.bytecode_words.len() + } + + pub fn program_image_len_words_padded(&self) -> usize { + self.program_image_len_words().next_power_of_two().max(2) + } + + pub fn meta(&self) -> ProgramMetadata { + ProgramMetadata { + entry_address: self.bytecode.entry_address, + min_bytecode_address: self.ram.min_bytecode_address, + program_image_len_words: self.program_image_len_words(), + bytecode_len: self.bytecode_len(), + } + } + + #[inline(always)] + pub fn get_pc(&self, cycle: &Cycle) -> usize { + self.bytecode.get_pc(cycle) + } + + #[inline(always)] + pub fn entry_bytecode_index(&self) -> usize { + self.bytecode.entry_bytecode_index() + } + + pub fn as_bytecode(&self) -> BytecodePreprocessing { + self.bytecode.clone() + } +} + +#[derive(Debug, Clone, CanonicalSerialize, CanonicalDeserialize)] +pub struct ProgramMetadata { + pub entry_address: u64, + pub min_bytecode_address: u64, + pub program_image_len_words: usize, + pub bytecode_len: usize, +} + +impl ProgramMetadata { + pub fn from_program(program: &ProgramPreprocessing) -> Self { + program.meta() + } + + pub fn program_image_len_words_padded(&self) -> usize { + self.program_image_len_words.next_power_of_two().max(2) + } +} + +#[derive(Clone, Debug, PartialEq, CanonicalSerialize, CanonicalDeserialize)] +pub struct TrustedProgramCommitments { + pub program_image_commitment: PCS::Commitment, + pub program_image_num_columns: usize, + pub program_image_num_words: usize, +} + +#[derive(Clone)] +pub struct TrustedProgramHints { + pub program_image_hint: PCS::OpeningProofHint, +} + +impl CanonicalSerialize for TrustedProgramHints +where + PCS::OpeningProofHint: CanonicalSerialize, +{ + fn serialize_with_mode( + &self, + mut writer: W, + compress: Compress, + ) -> Result<(), SerializationError> { + self.program_image_hint + .serialize_with_mode(&mut writer, compress)?; + Ok(()) + } + + fn serialized_size(&self, compress: Compress) -> usize { + self.program_image_hint.serialized_size(compress) + } +} + +impl Valid for TrustedProgramHints +where + PCS::OpeningProofHint: Valid, +{ + fn check(&self) -> Result<(), SerializationError> { + self.program_image_hint.check() + } +} + +impl CanonicalDeserialize for TrustedProgramHints +where + PCS::OpeningProofHint: CanonicalDeserialize, +{ + fn deserialize_with_mode( + mut reader: R, + compress: Compress, + validate: Validate, + ) -> Result { + Ok(Self { + program_image_hint: PCS::OpeningProofHint::deserialize_with_mode( + &mut reader, + compress, + validate, + )?, + }) + } +} + +impl TrustedProgramCommitments { + #[tracing::instrument(skip_all, name = "TrustedProgramCommitments::derive")] + pub fn derive( + program: &ProgramPreprocessing, + generators: &PCS::ProverSetup, + ) -> (Self, TrustedProgramHints) { + let program_image_num_words = program.program_image_len_words_padded(); + let (program_image_sigma, _) = + crate::poly::commitment::dory::DoryGlobals::balanced_sigma_nu( + program_image_num_words.log_2(), + ); + let program_image_num_columns = 1usize << program_image_sigma; + let program_image_poly = + build_program_image_polynomial_padded::(program, program_image_num_words); + let _program_image_guard = DoryGlobals::initialize_context( + 1, + program_image_num_words, + DoryContext::UntrustedAdvice, + None, + ); + let (program_image_commitment, program_image_hint) = { + let _ctx = DoryGlobals::with_context(DoryContext::UntrustedAdvice); + PCS::commit(&program_image_poly, generators) + }; + + ( + Self { + program_image_commitment, + program_image_num_columns, + program_image_num_words, + }, + TrustedProgramHints { program_image_hint }, + ) + } +} + +pub(crate) fn build_program_image_polynomial_padded( + program: &ProgramPreprocessing, + padded_len: usize, +) -> MultilinearPolynomial { + debug_assert!(padded_len.is_power_of_two()); + debug_assert!(padded_len >= program.ram.bytecode_words.len()); + let mut coeffs = vec![0u64; padded_len]; + for (i, &word) in program.ram.bytecode_words.iter().enumerate() { + coeffs[i] = word; + } + MultilinearPolynomial::from(coeffs) +} + +#[derive(Debug, Clone)] +pub enum VerifierProgram { + Full(Arc), + Committed(TrustedProgramCommitments), +} + +impl VerifierProgram { + pub fn as_full(&self) -> Result<&Arc, ProofVerifyError> { + match self { + VerifierProgram::Full(program) => Ok(program), + VerifierProgram::Committed(_) => Err(ProofVerifyError::BytecodeTypeMismatch( + "expected Full, got Committed".to_string(), + )), + } + } + + pub fn as_committed(&self) -> Result<&TrustedProgramCommitments, ProofVerifyError> { + match self { + VerifierProgram::Committed(program) => Ok(program), + VerifierProgram::Full(_) => Err(ProofVerifyError::BytecodeTypeMismatch( + "expected Committed, got Full".to_string(), + )), + } + } + + pub fn is_full(&self) -> bool { + matches!(self, VerifierProgram::Full(_)) + } + + pub fn is_committed(&self) -> bool { + matches!(self, VerifierProgram::Committed(_)) + } + + pub fn full(&self) -> Option<&Arc> { + match self { + VerifierProgram::Full(program) => Some(program), + VerifierProgram::Committed(_) => None, + } + } + + pub fn instructions(&self) -> Option<&[Instruction]> { + match self { + VerifierProgram::Full(program) => Some(&program.bytecode.bytecode), + VerifierProgram::Committed(_) => None, + } + } + + pub fn program_image_words(&self) -> Option<&[u64]> { + match self { + VerifierProgram::Full(program) => Some(&program.ram.bytecode_words), + VerifierProgram::Committed(_) => None, + } + } + + pub fn as_bytecode(&self) -> Option { + match self { + VerifierProgram::Full(program) => Some(program.as_bytecode()), + VerifierProgram::Committed(_) => None, + } + } +} + +impl CanonicalSerialize for VerifierProgram { + fn serialize_with_mode( + &self, + mut writer: W, + compress: Compress, + ) -> Result<(), SerializationError> { + match self { + VerifierProgram::Full(program) => { + 0u8.serialize_with_mode(&mut writer, compress)?; + program + .as_ref() + .serialize_with_mode(&mut writer, compress)?; + } + VerifierProgram::Committed(program) => { + 1u8.serialize_with_mode(&mut writer, compress)?; + program.serialize_with_mode(&mut writer, compress)?; + } + } + Ok(()) + } + + fn serialized_size(&self, compress: Compress) -> usize { + 1 + match self { + VerifierProgram::Full(program) => program.serialized_size(compress), + VerifierProgram::Committed(program) => program.serialized_size(compress), + } + } +} + +impl Valid for VerifierProgram { + fn check(&self) -> Result<(), SerializationError> { + match self { + VerifierProgram::Full(program) => program.check(), + VerifierProgram::Committed(program) => program.check(), + } + } +} + +impl CanonicalDeserialize for VerifierProgram { + fn deserialize_with_mode( + mut reader: R, + compress: Compress, + validate: Validate, + ) -> Result { + let tag = u8::deserialize_with_mode(&mut reader, compress, validate)?; + match tag { + 0 => { + let program = + ProgramPreprocessing::deserialize_with_mode(&mut reader, compress, validate)?; + Ok(VerifierProgram::Full(Arc::new(program))) + } + 1 => { + let program = TrustedProgramCommitments::::deserialize_with_mode( + &mut reader, + compress, + validate, + )?; + Ok(VerifierProgram::Committed(program)) + } + _ => Err(SerializationError::InvalidData), + } + } +} diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index 3e51915895..33e30d7d37 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -23,6 +23,8 @@ use crate::subprotocols::sumcheck::BatchedSumcheck; #[cfg(feature = "zk")] use crate::subprotocols::sumcheck::SumcheckInstanceProof; #[cfg(feature = "zk")] +use crate::subprotocols::sumcheck_verifier::SumcheckInstanceParams; +#[cfg(feature = "zk")] use crate::subprotocols::univariate_skip::UniSkipFirstRoundProofVariant; use crate::zkvm::bytecode::chunks::DEFAULT_COMMITTED_BYTECODE_CHUNK_COUNT; use crate::zkvm::bytecode::chunks::{ diff --git a/jolt-sdk/macros/src/lib.rs b/jolt-sdk/macros/src/lib.rs index 9bd69b1f86..b87d7a6985 100644 --- a/jolt-sdk/macros/src/lib.rs +++ b/jolt-sdk/macros/src/lib.rs @@ -508,8 +508,10 @@ impl MacroBuilder { let fn_name = self.get_func_name(); let preprocess_shared_fn_name = Ident::new(&format!("preprocess_shared_{fn_name}"), fn_name.span()); - let preprocess_shared_committed_fn_name = - Ident::new(&format!("preprocess_shared_committed_{fn_name}"), fn_name.span()); + let preprocess_shared_committed_fn_name = Ident::new( + &format!("preprocess_shared_committed_{fn_name}"), + fn_name.span(), + ); quote! { #[cfg(all(not(target_arch = "wasm32"), not(feature = "guest")))] pub fn #preprocess_shared_committed_fn_name( @@ -563,8 +565,10 @@ impl MacroBuilder { let fn_name = self.get_func_name(); let preprocess_committed_fn_name = Ident::new(&format!("preprocess_committed_{fn_name}"), fn_name.span()); - let preprocess_shared_committed_fn_name = - Ident::new(&format!("preprocess_shared_committed_{fn_name}"), fn_name.span()); + let preprocess_shared_committed_fn_name = Ident::new( + &format!("preprocess_shared_committed_{fn_name}"), + fn_name.span(), + ); quote! { #[cfg(all(not(target_arch = "wasm32"), not(feature = "guest")))] pub fn #preprocess_committed_fn_name( @@ -607,8 +611,10 @@ impl MacroBuilder { fn make_preprocess_committed_verifier_func(&self) -> TokenStream2 { let fn_name = self.get_func_name(); - let preprocess_committed_verifier_fn_name = - Ident::new(&format!("preprocess_committed_verifier_{fn_name}"), fn_name.span()); + let preprocess_committed_verifier_fn_name = Ident::new( + &format!("preprocess_committed_verifier_{fn_name}"), + fn_name.span(), + ); quote! { #[cfg(all(not(target_arch = "wasm32"), not(feature = "guest")))] diff --git a/jolt-sdk/src/host_utils.rs b/jolt-sdk/src/host_utils.rs index 8994f63c5c..f7f9b19b70 100644 --- a/jolt-sdk/src/host_utils.rs +++ b/jolt-sdk/src/host_utils.rs @@ -13,9 +13,8 @@ pub use jolt_core::field::JoltField; pub use jolt_core::guest; pub use jolt_core::poly::commitment::dory::DoryCommitmentScheme as PCS; pub use jolt_core::zkvm::{ - bytecode::TrustedBytecodeCommitments, - program::ProgramPreprocessing, proof_serialization::JoltProof, - program::TrustedProgramCommitments, + bytecode::TrustedBytecodeCommitments, program::ProgramPreprocessing, + program::TrustedProgramCommitments, proof_serialization::JoltProof, verifier::JoltSharedPreprocessing, verifier::JoltVerifierPreprocessing, RV64IMACProof, RV64IMACVerifier, Serializable, }; From f39608a99f5b1ef17904a39df752097b343b9b4a Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Mon, 16 Mar 2026 22:08:02 -0700 Subject: [PATCH 05/20] chore(zkvm): remove temporary stage6/stage8 debug instrumentation. --- jolt-core/src/poly/one_hot_polynomial.rs | 4 +- jolt-core/src/poly/rlc_polynomial.rs | 6 +- jolt-core/src/zkvm/prover.rs | 452 +---------------------- jolt-core/src/zkvm/verifier.rs | 16 - 4 files changed, 7 insertions(+), 471 deletions(-) diff --git a/jolt-core/src/poly/one_hot_polynomial.rs b/jolt-core/src/poly/one_hot_polynomial.rs index 4cef02ac7b..f4d6b107ca 100644 --- a/jolt-core/src/poly/one_hot_polynomial.rs +++ b/jolt-core/src/poly/one_hot_polynomial.rs @@ -58,7 +58,9 @@ impl OneHotPolynomial { pub fn num_rows(&self) -> usize { match DoryGlobals::get_layout() { DoryLayout::AddressMajor => DoryGlobals::get_max_num_rows(), - DoryLayout::CycleMajor => (DoryGlobals::get_T() * self.K).div_ceil(DoryGlobals::get_num_columns()), + DoryLayout::CycleMajor => { + (DoryGlobals::get_T() * self.K).div_ceil(DoryGlobals::get_num_columns()) + } } } diff --git a/jolt-core/src/poly/rlc_polynomial.rs b/jolt-core/src/poly/rlc_polynomial.rs index e681e0c199..56f3b8b61f 100644 --- a/jolt-core/src/poly/rlc_polynomial.rs +++ b/jolt-core/src/poly/rlc_polynomial.rs @@ -197,10 +197,8 @@ impl RLCPolynomial { | CommittedPolynomial::ProgramImageInit => { // Precommitted polynomials are passed in directly (not streamed from trace). if precommitted_poly_map.contains_key(poly_id) { - precommitted_polys.push(( - *coeff, - precommitted_poly_map.remove(poly_id).unwrap(), - )); + precommitted_polys + .push((*coeff, precommitted_poly_map.remove(poly_id).unwrap())); } } } diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 4a7ef109b9..80f308c245 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -40,7 +40,7 @@ use crate::{ commitment_scheme::{StreamingCommitmentScheme, ZkEvalCommitment}, dory::{DoryGlobals, DoryLayout}, }, - multilinear_polynomial::{MultilinearPolynomial, PolynomialEvaluation}, + multilinear_polynomial::MultilinearPolynomial, opening_proof::{ compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, SumcheckId, BIG_ENDIAN, @@ -56,7 +56,6 @@ use crate::{ streaming_schedule::LinearOnlySchedule, sumcheck::{BatchedSumcheck, SumcheckInstanceProof}, sumcheck_prover::SumcheckInstanceProver, - sumcheck_verifier::SumcheckInstanceParams, univariate_skip::UniSkipFirstRoundProofVariant, }, transcripts::Transcript, @@ -177,50 +176,6 @@ use crate::zkvm::r1cs::constraints::{ #[cfg(feature = "zk")] use crate::zkvm::verifier::BlindfoldSetup; -pub(crate) fn derive_poly_source_point_from_matrix_dims( - stage8_opening_point: &OpeningPoint, - poly_num_rows: usize, - poly_num_columns: usize, -) -> OpeningPoint { - assert!( - poly_num_rows.is_power_of_two() && poly_num_columns.is_power_of_two(), - "polynomial matrix dimensions must be powers of two (rows={poly_num_rows}, cols={poly_num_columns})" - ); - let nu_poly = poly_num_rows.log_2(); - let sigma_poly = poly_num_columns.log_2(); - let nu_full = DoryGlobals::get_max_num_rows().log_2(); - let sigma_full = DoryGlobals::get_num_columns().log_2(); - assert!( - sigma_poly <= sigma_full && nu_poly <= nu_full, - "top-left projection requires poly dims <= full dims (poly sigma/nu={sigma_poly}/{nu_poly}, full sigma/nu={sigma_full}/{nu_full})" - ); - - // Dimension-only projection: - // - Treat full point as [row_variables || column_variables] - // - For target dims (nu_poly rows, sigma_poly cols), take tails: - // [last nu_poly row vars || last sigma_poly col vars] - let row_be = &stage8_opening_point.r[..nu_full]; - let col_be = &stage8_opening_point.r[nu_full..nu_full + sigma_full]; - let row_tail = &row_be[nu_full - nu_poly..]; - let col_tail = &col_be[sigma_full - sigma_poly..]; - - let mut projected = Vec::with_capacity(nu_poly + sigma_poly); - projected.extend_from_slice(row_tail); - projected.extend_from_slice(col_tail); - OpeningPoint::::new(projected) -} - -#[inline] -pub(crate) fn derive_poly_source_point_from_dory_dims( - stage8_opening_point: &OpeningPoint, - poly_num_vars: usize, -) -> OpeningPoint { - let (sigma_poly, nu_poly) = DoryGlobals::balanced_sigma_nu(poly_num_vars); - let poly_num_rows = 1usize << nu_poly; - let poly_num_columns = 1usize << sigma_poly; - derive_poly_source_point_from_matrix_dims(stage8_opening_point, poly_num_rows, poly_num_columns) -} - /// Jolt CPU prover for RV64IMAC. pub struct JoltCpuProver< 'a, @@ -388,7 +343,6 @@ impl< fn stage8_opening_point(&self) -> OpeningPoint { let native_main_vars = self.trace.len().log_2() + self.one_hot_params.log_k_chunk; - let debug_stage8_point = Self::stage8_debug_enabled(); let mut opening_candidates: Vec<(String, OpeningPoint)> = Vec::new(); if let Some((point, _)) = self .opening_accumulator @@ -440,13 +394,6 @@ impl< dominant.0, name, max_len ); } - if debug_stage8_point { - tracing::info!( - "Stage8 opening point: dominant polynomial anchor = {} (len={})", - dominant.0, - max_len - ); - } OpeningPoint::::new(dominant.1.r.clone()) } else { let (hamming_point, _) = self.opening_accumulator.get_committed_polynomial_opening( @@ -488,362 +435,9 @@ impl< } }; - if debug_stage8_point { - if max_len <= native_main_vars { - tracing::info!( - "Stage8 opening point: no dominant precommitted polynomial (max_candidate_len={} <= native_main_vars={}); fallback anchor = hamming+stage6-cycle (with Dory layout ordering already encoded here)", - max_len, - native_main_vars - ); - } - tracing::info!("Stage8 final Dory opening point (BE): {:?}", final_point.r); - } - final_point } - #[inline] - fn stage8_debug_enabled() -> bool { - let parse_env_bool = |key: &str| { - std::env::var(key).is_ok_and(|v| { - matches!( - v.trim().to_ascii_lowercase().as_str(), - "1" | "true" | "yes" | "on" | "all" - ) - }) - }; - parse_env_bool("JOLT_DEBUG_STAGE8_POLY_CLAIMS") || parse_env_bool("JOLT_DEBUG_DORY_CLASSES") - } - - fn stage8_sumcheck_opening( - &self, - poly: CommittedPolynomial, - ) -> (OpeningPoint, F) { - match poly { - CommittedPolynomial::RdInc | CommittedPolynomial::RamInc => self - .opening_accumulator - .get_committed_polynomial_opening(poly, SumcheckId::IncClaimReduction), - CommittedPolynomial::InstructionRa(_) - | CommittedPolynomial::BytecodeRa(_) - | CommittedPolynomial::RamRa(_) => self - .opening_accumulator - .get_committed_polynomial_opening(poly, SumcheckId::HammingWeightClaimReduction), - CommittedPolynomial::TrustedAdvice => self - .opening_accumulator - .get_advice_opening(AdviceKind::Trusted, SumcheckId::AdviceClaimReduction) - .expect("missing trusted advice opening for Stage 8 debug"), - CommittedPolynomial::UntrustedAdvice => self - .opening_accumulator - .get_advice_opening(AdviceKind::Untrusted, SumcheckId::AdviceClaimReduction) - .expect("missing untrusted advice opening for Stage 8 debug"), - CommittedPolynomial::BytecodeChunk(_) => self - .opening_accumulator - .get_committed_polynomial_opening(poly, SumcheckId::BytecodeClaimReduction), - CommittedPolynomial::ProgramImageInit => self - .opening_accumulator - .get_committed_polynomial_opening(poly, SumcheckId::ProgramImageClaimReduction), - } - } - - fn debug_verify_stage8_polynomial_claims( - &self, - stage8_opening_point: &OpeningPoint, - polynomial_claims: &[(CommittedPolynomial, F)], - direct_polys: &HashMap>, - ) { - let mut generated_polys: HashMap> = - HashMap::new(); - - let eval_poly = |poly_id: CommittedPolynomial, - point: &[F::Challenge], - generated: &mut HashMap>| - -> F { - if let Some(poly) = direct_polys.get(&poly_id) { - return poly.evaluate(point); - } - let poly = generated.entry(poly_id).or_insert_with(|| { - poly_id.generate_witness( - &self.preprocessing.program.bytecode, - &self.preprocessing.shared.memory_layout, - self.trace.as_slice(), - Some(&self.one_hot_params), - ) - }); - poly.evaluate(point) - }; - - let poly_num_vars = - |poly_id: CommittedPolynomial, - generated: &mut HashMap>| - -> usize { - if let Some(poly) = direct_polys.get(&poly_id) { - return poly.get_num_vars(); - } - let poly = generated.entry(poly_id).or_insert_with(|| { - poly_id.generate_witness( - &self.preprocessing.program.bytecode, - &self.preprocessing.shared.memory_layout, - self.trace.as_slice(), - Some(&self.one_hot_params), - ) - }); - poly.get_num_vars() - }; - - let derive_from_prefix = |num_vars: usize| -> OpeningPoint { - assert!( - num_vars <= stage8_opening_point.r.len(), - "cannot derive source point of len {} from stage8 point len {}", - num_vars, - stage8_opening_point.r.len() - ); - OpeningPoint::::new(stage8_opening_point.r[..num_vars].to_vec()) - }; - let derive_from_suffix = |num_vars: usize| -> OpeningPoint { - assert!( - num_vars <= stage8_opening_point.r.len(), - "cannot derive source point of len {} from stage8 point len {}", - num_vars, - stage8_opening_point.r.len() - ); - let start = stage8_opening_point.r.len() - num_vars; - OpeningPoint::::new(stage8_opening_point.r[start..].to_vec()) - }; - let derive_ra_from_prefix_rotated = |num_vars: usize| -> OpeningPoint { - assert!( - num_vars <= stage8_opening_point.r.len(), - "cannot derive source point of len {} from stage8 point len {}", - num_vars, - stage8_opening_point.r.len() - ); - let k = self.one_hot_params.log_k_chunk; - assert!( - k <= num_vars, - "ra opening point shorter than log_k_chunk (len={}, log_k_chunk={})", - num_vars, - k - ); - let t = num_vars - k; - let prefix = &stage8_opening_point.r[..num_vars]; - let mut rotated = Vec::with_capacity(num_vars); - // Move the last k address variables to the front: [t | k] -> [k | t]. - rotated.extend_from_slice(&prefix[t..]); - rotated.extend_from_slice(&prefix[..t]); - OpeningPoint::::new(rotated) - }; - - for (poly_id, _) in polynomial_claims.iter() { - let (sumcheck_point, sumcheck_claim) = self.stage8_sumcheck_opening(*poly_id); - let num_vars = poly_num_vars(*poly_id, &mut generated_polys); - let projected_point = match poly_id { - CommittedPolynomial::RdInc | CommittedPolynomial::RamInc => { - match DoryGlobals::get_layout() { - DoryLayout::AddressMajor => derive_from_prefix(num_vars), - DoryLayout::CycleMajor => derive_from_suffix(num_vars), - } - } - CommittedPolynomial::InstructionRa(_) - | CommittedPolynomial::BytecodeRa(_) - | CommittedPolynomial::RamRa(_) => match DoryGlobals::get_layout() { - DoryLayout::AddressMajor => derive_ra_from_prefix_rotated(num_vars), - DoryLayout::CycleMajor => derive_from_suffix(num_vars), - }, - CommittedPolynomial::TrustedAdvice - | CommittedPolynomial::UntrustedAdvice - | CommittedPolynomial::BytecodeChunk(_) - | CommittedPolynomial::ProgramImageInit => { - derive_poly_source_point_from_dory_dims(stage8_opening_point, num_vars) - } - }; - - let eval_at_sumcheck = eval_poly(*poly_id, &sumcheck_point.r, &mut generated_polys); - assert_eq!( - eval_at_sumcheck, sumcheck_claim, - "Stage8 debug mismatch for {poly_id:?}: sumcheck claim != direct eval at sumcheck point; sumcheck_point={:?}; projected_point={:?}", - sumcheck_point.r, - projected_point.r, - ); - - let eval_at_projected = eval_poly(*poly_id, &projected_point.r, &mut generated_polys); - assert_eq!( - eval_at_projected, sumcheck_claim, - "Stage8 debug mismatch for {poly_id:?}: sumcheck claim != direct eval at projected Dory point; sumcheck_point={:?}; projected_point={:?}", - sumcheck_point.r, - projected_point.r, - ); - } - - tracing::info!( - "Stage8 debug validated {} polynomial claims", - polynomial_claims.len() - ); - } - - fn stage8_debug_joint_commitment( - &self, - commitments: &[PCS::Commitment], - untrusted_advice_commitment: Option<&PCS::Commitment>, - state: &DoryOpeningState, - ) -> PCS::Commitment { - let expected_polynomials = all_committed_polynomials(&self.one_hot_params); - assert_eq!( - expected_polynomials.len(), - commitments.len(), - "Stage8 debug: expected {} commitments but prover produced {}", - expected_polynomials.len(), - commitments.len() - ); - - let mut commitment_map: HashMap = - expected_polynomials - .into_iter() - .zip(commitments.iter().cloned()) - .collect(); - - if let Some(commitment) = self.advice.trusted_advice_commitment.as_ref() { - if state - .polynomial_claims - .iter() - .any(|(p, _)| *p == CommittedPolynomial::TrustedAdvice) - { - commitment_map.insert(CommittedPolynomial::TrustedAdvice, commitment.clone()); - } - } - if let Some(commitment) = untrusted_advice_commitment { - if state - .polynomial_claims - .iter() - .any(|(p, _)| *p == CommittedPolynomial::UntrustedAdvice) - { - commitment_map.insert(CommittedPolynomial::UntrustedAdvice, commitment.clone()); - } - } - if let Some(bytecode_commitments) = &self.preprocessing.bytecode_commitments { - for (chunk_idx, commitment) in bytecode_commitments.commitments.iter().enumerate() { - if state - .polynomial_claims - .iter() - .any(|(p, _)| *p == CommittedPolynomial::BytecodeChunk(chunk_idx)) - { - commitment_map.insert( - CommittedPolynomial::BytecodeChunk(chunk_idx), - commitment.clone(), - ); - } - } - } - if let Some(program_commitments) = &self.preprocessing.program_commitments { - if state - .polynomial_claims - .iter() - .any(|(p, _)| *p == CommittedPolynomial::ProgramImageInit) - { - commitment_map.insert( - CommittedPolynomial::ProgramImageInit, - program_commitments.program_image_commitment.clone(), - ); - } - } - - let mut rlc_map: HashMap = HashMap::new(); - for (gamma, (poly, _claim)) in state - .gamma_powers - .iter() - .zip(state.polynomial_claims.iter()) - { - *rlc_map.entry(*poly).or_insert(F::zero()) += *gamma; - } - - let (coeffs, commitments): (Vec, Vec) = rlc_map - .into_iter() - .map(|(poly, coeff)| { - let commitment = commitment_map.remove(&poly).unwrap_or_else(|| { - panic!("Stage8 debug: missing commitment for {poly:?} in joint commitment map") - }); - (coeff, commitment) - }) - .unzip(); - - PCS::combine_commitments(&commitments, &coeffs) - } - - fn debug_verify_stage8_dory_self( - &self, - proof: &PCS::Proof, - stage8_opening_point: &OpeningPoint, - state: &DoryOpeningState, - joint_claim: F, - commitments: &[PCS::Commitment], - untrusted_advice_commitment: Option<&PCS::Commitment>, - mut transcript: ProofTranscript, - ) { - let joint_commitment = - self.stage8_debug_joint_commitment(commitments, untrusted_advice_commitment, state); - - #[cfg(feature = "zk")] - let opening = F::zero(); - #[cfg(not(feature = "zk"))] - let opening = joint_claim; - let verifier_setup = PCS::setup_verifier(&self.preprocessing.generators); - - PCS::verify( - proof, - &verifier_setup, - &mut transcript, - &stage8_opening_point.r, - &opening, - &joint_commitment, - ) - .unwrap_or_else(|e| panic!("Stage8 debug Dory self-verification failed: {e:?}")); - - tracing::info!("Stage8 debug Dory self-verification passed"); - } - - fn debug_verify_omitted_program_openings(&self) { - if !self.preprocessing.is_committed_mode() { - return; - } - - if !self.include_bytecode_in_stage8() { - let bytecode_chunk_polys = build_committed_bytecode_chunk_polynomials::( - &self.preprocessing.program.bytecode.bytecode, - self.preprocessing.shared.bytecode_chunk_count, - ); - for (chunk_idx, poly) in bytecode_chunk_polys.into_iter().enumerate() { - let (point, claim) = self.opening_accumulator.get_committed_polynomial_opening( - CommittedPolynomial::BytecodeChunk(chunk_idx), - SumcheckId::BytecodeClaimReduction, - ); - let eval = poly.evaluate(&point.r); - assert_eq!( - eval, claim, - "Stage8 debug mismatch for omitted bytecode chunk {chunk_idx}: direct evaluation does not match the cached opening claim" - ); - } - tracing::info!("Stage8 debug validated omitted bytecode chunk openings directly"); - } - - if !self.include_program_image_in_stage8() { - let mut program_image_words = self.preprocessing.program.ram.bytecode_words.clone(); - if program_image_words.is_empty() { - program_image_words.push(0); - } - let padded_len = program_image_words.len().next_power_of_two().max(2); - program_image_words.resize(padded_len, 0); - let program_image_poly = MultilinearPolynomial::from(program_image_words); - let (point, claim) = self.opening_accumulator.get_committed_polynomial_opening( - CommittedPolynomial::ProgramImageInit, - SumcheckId::ProgramImageClaimReduction, - ); - let eval = program_image_poly.evaluate(&point.r); - assert_eq!( - eval, claim, - "Stage8 debug mismatch for omitted program image: direct evaluation does not match the cached opening claim" - ); - tracing::info!("Stage8 debug validated omitted program image opening directly"); - } - } - pub fn gen_from_trace( preprocessing: &'a JoltProverPreprocessing, lazy_trace: LazyTraceIterator, @@ -1035,11 +629,7 @@ impl< r_stage1, r_stage2, r_stage3, r_stage4, r_stage5, r_stage6, r_stage7, ]; - let joint_opening_proof = self.prove_stage8( - opening_proof_hints, - &commitments, - untrusted_advice_commitment.as_ref(), - ); + let joint_opening_proof = self.prove_stage8(opening_proof_hints); #[cfg(feature = "zk")] let blindfold_proof = self.prove_blindfold(&joint_opening_proof); @@ -1753,12 +1343,6 @@ impl< &self.opening_accumulator, &mut self.transcript, ); - tracing::info!( - "Stage 6a prover input claims: bytecode_read_raf={} booleanity={}", - bytecode_read_raf_params.input_claim(&self.opening_accumulator), - booleanity_params.input_claim(&self.opening_accumulator), - ); - let mut bytecode_read_raf = BytecodeReadRafAddressSumcheckProver::initialize( bytecode_read_raf_params.clone(), Arc::clone(&self.trace), @@ -2589,8 +2173,6 @@ impl< fn prove_stage8( &mut self, opening_proof_hints: HashMap, - commitments: &[PCS::Commitment], - untrusted_advice_commitment: Option<&PCS::Commitment>, ) -> PCS::Proof { tracing::info!("Stage 8 proving (Dory batch opening)"); @@ -2755,9 +2337,6 @@ impl< .zip(claims.iter()) .map(|(gamma, claim)| *gamma * claim) .sum(); - if Self::stage8_debug_enabled() { - tracing::info!("Stage8 final Dory claim (joint_claim): {}", joint_claim); - } #[cfg(feature = "zk")] let opening_ids = stage8_opening_ids( @@ -2794,13 +2373,6 @@ impl< precommitted_polys.insert(CommittedPolynomial::UntrustedAdvice, poly); } precommitted_polys.extend(extra_dense_polys); - let debug_stage8_polys = if Self::stage8_debug_enabled() { - Some(precommitted_polys.clone()) - } else { - None - }; - let mut debug_stage8_verify_transcript = - debug_stage8_polys.as_ref().map(|_| self.transcript.clone()); // Build streaming RLC polynomial directly (no witness poly regeneration!) // Use materialized trace (default, single pass) instead of lazy trace @@ -2836,26 +2408,6 @@ impl< { bind_opening_inputs::(&mut self.transcript, &opening_point.r, &joint_claim); } - if let Some(ref direct_polys) = debug_stage8_polys { - self.debug_verify_stage8_polynomial_claims( - &opening_point, - &state.polynomial_claims, - direct_polys, - ); - self.debug_verify_omitted_program_openings(); - let verify_transcript = debug_stage8_verify_transcript - .take() - .expect("Stage8 debug transcript clone should exist"); - self.debug_verify_stage8_dory_self( - &proof, - &opening_point, - &state, - joint_claim, - commitments, - untrusted_advice_commitment, - verify_transcript, - ); - } proof } diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index 33e30d7d37..73109fd18e 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -1127,9 +1127,7 @@ impl< self.main_total_vars(), Some(self.proof.dory_layout), ); - tracing::info!("stage6a"); let (bytecode_read_raf_params, booleanity_params) = self.verify_stage6a()?; - tracing::info!("stage6b"); self.verify_stage6b(bytecode_read_raf_params, booleanity_params) } @@ -1172,20 +1170,6 @@ impl< &self.opening_accumulator, &mut self.transcript, )); - tracing::info!( - "Stage 6a verifier input claims: bytecode_read_raf={} booleanity={}", - as SumcheckInstanceVerifier< - F, - ProofTranscript, - >>::get_params(&bytecode_read_raf) - .input_claim(&self.opening_accumulator), - as SumcheckInstanceVerifier< - F, - ProofTranscript, - >>::get_params(&booleanity) - .input_claim(&self.opening_accumulator), - ); - let instances: Vec<&dyn SumcheckInstanceVerifier> = vec![&bytecode_read_raf, &booleanity]; BatchedSumcheck::verify( From 95d0a6a1af651ead43ae91568c74cac26d9e1823 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Tue, 17 Mar 2026 15:57:41 -0700 Subject: [PATCH 06/20] fix(zkvm): enforce Stage 6a in BlindFold and decouple phase sumchecks Wire Stage 6a into global BlindFold stage verification so address-phase bridge constraints are enforced in ZK mode, and refactor Booleanity/BytecodeReadRaf into explicit address/cycle phase paths to remove legacy monolithic implementations and duplicated logic. Removed debugger artifacts from committed-mode Stage 8 opening handling, update wasm preprocessing wiring, and remove the obsolete fibonacci2 example workspace entries. Made-with: Cursor --- Cargo.lock | 17 - Cargo.toml | 2 - examples/fibonacci2/Cargo.toml | 13 - examples/fibonacci2/guest/Cargo.toml | 11 - examples/fibonacci2/guest/src/lib.rs | 75 -- examples/fibonacci2/guest/src/main.rs | 5 - examples/fibonacci2/src/main.rs | 158 ---- .../src/poly/commitment/dory/wrappers.rs | 133 ++-- jolt-core/src/poly/rlc_polynomial.rs | 20 +- jolt-core/src/subprotocols/booleanity.rs | 734 ++++++------------ jolt-core/src/subprotocols/mod.rs | 4 - jolt-core/src/subprotocols/sumcheck.rs | 111 +-- .../src/zkvm/bytecode/read_raf_checking.rs | 129 +-- .../src/zkvm/claim_reductions/precommitted.rs | 12 +- jolt-core/src/zkvm/mod.rs | 42 +- jolt-core/src/zkvm/prover.rs | 59 +- jolt-core/src/zkvm/ram/mod.rs | 1 + jolt-core/src/zkvm/verifier.rs | 221 +++--- src/build_wasm.rs | 31 +- 19 files changed, 506 insertions(+), 1272 deletions(-) delete mode 100644 examples/fibonacci2/Cargo.toml delete mode 100644 examples/fibonacci2/guest/Cargo.toml delete mode 100644 examples/fibonacci2/guest/src/lib.rs delete mode 100644 examples/fibonacci2/guest/src/main.rs delete mode 100644 examples/fibonacci2/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 98128170a2..a27d1c6dc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2013,23 +2013,6 @@ dependencies = [ "jolt-sdk", ] -[[package]] -name = "fibonacci2" -version = "0.1.0" -dependencies = [ - "fibonacci2-guest", - "jolt-sdk", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "fibonacci2-guest" -version = "0.1.0" -dependencies = [ - "jolt-sdk", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 937e85c4b4..67c8a9364a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,8 +43,6 @@ members = [ "examples/collatz/guest", "examples/fibonacci", "examples/fibonacci/guest", - "examples/fibonacci2", - "examples/fibonacci2/guest", "examples/secp256k1-ecdsa-verify", "examples/secp256k1-ecdsa-verify/guest", "examples/sha2-ex", diff --git a/examples/fibonacci2/Cargo.toml b/examples/fibonacci2/Cargo.toml deleted file mode 100644 index 93e851a3dc..0000000000 --- a/examples/fibonacci2/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "fibonacci2" -version = "0.1.0" -edition = "2021" - -[features] -zk = ["jolt-sdk/zk", "guest/zk"] - -[dependencies] -jolt-sdk = { workspace = true, features = ["host"] } -tracing-subscriber.workspace = true -tracing.workspace = true -guest = { package = "fibonacci2-guest", path = "./guest" } diff --git a/examples/fibonacci2/guest/Cargo.toml b/examples/fibonacci2/guest/Cargo.toml deleted file mode 100644 index f2a640a425..0000000000 --- a/examples/fibonacci2/guest/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "fibonacci2-guest" -version = "0.1.0" -edition = "2021" - -[features] -guest = [] -zk = [] - -[dependencies] -jolt = { package = "jolt-sdk", path = "../../../jolt-sdk" } diff --git a/examples/fibonacci2/guest/src/lib.rs b/examples/fibonacci2/guest/src/lib.rs deleted file mode 100644 index 5cdaa5788e..0000000000 --- a/examples/fibonacci2/guest/src/lib.rs +++ /dev/null @@ -1,75 +0,0 @@ -#![cfg_attr(feature = "guest", no_std)] -use jolt::{end_cycle_tracking, start_cycle_tracking}; - -#[jolt::provable(heap_size = 32768, max_trace_length = 65536)] -fn fib(n: u32) -> u128 { - let mut a: u128 = 0; - let mut b: u128 = 1; - let mut sum: u128; - - start_cycle_tracking("fib_loop"); - for _ in 1..n { - sum = a + b; - a = b; - b = sum; - } - end_cycle_tracking("fib_loop"); - b -} - -#[cfg(any(feature = "guest", feature = "zk"))] -#[jolt::provable(heap_size = 32768, max_trace_length = 65536)] -fn fib_with_private_input(n: u32, private_bump: jolt::PrivateInput) -> u128 { - let adjusted_n = n + (*private_bump % 3); - - let mut a: u128 = 0; - let mut b: u128 = 1; - let mut sum: u128; - - start_cycle_tracking("fib_loop_private"); - for _ in 1..adjusted_n { - sum = a + b; - a = b; - b = sum; - } - end_cycle_tracking("fib_loop_private"); - b -} - -#[jolt::provable( - heap_size = 32768, - max_trace_length = 65536, - max_untrusted_advice_size = 131072 -)] -fn fib_with_large_advice_input(n: u32, advice: jolt::UntrustedAdvice<&[u8]>) -> u128 { - let advice = *advice; - jolt::check_advice!(advice.len() >= 2, "advice must contain at least 2 entries"); - - let last_idx = advice.len() - 1; - jolt::check_advice_eq!( - advice[last_idx] as u64, - 7u64, - "expected fixed marker in last advice byte" - ); - - let sampled_idx = (n as usize) % advice.len(); - jolt::check_advice_eq!( - advice[sampled_idx] as u64, - 7u64, - "expected fixed marker in sampled advice byte" - ); - - let mut a: u128 = 0; - let mut b: u128 = 1; - let mut sum: u128; - - start_cycle_tracking("fib_loop_large_advice"); - for _ in 1..n { - sum = a + b; - a = b; - b = sum; - } - end_cycle_tracking("fib_loop_large_advice"); - - b -} diff --git a/examples/fibonacci2/guest/src/main.rs b/examples/fibonacci2/guest/src/main.rs deleted file mode 100644 index 82e3334ca7..0000000000 --- a/examples/fibonacci2/guest/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![cfg_attr(feature = "guest", no_std)] -#![no_main] - -#[allow(unused_imports)] -use fibonacci2_guest::*; diff --git a/examples/fibonacci2/src/main.rs b/examples/fibonacci2/src/main.rs deleted file mode 100644 index af4ce263a6..0000000000 --- a/examples/fibonacci2/src/main.rs +++ /dev/null @@ -1,158 +0,0 @@ -use jolt_sdk::serialize_and_print_size; -use jolt_sdk::DoryContext; -use jolt_sdk::DoryGlobals; -use jolt_sdk::DoryLayout; -#[cfg(feature = "zk")] -use jolt_sdk::PrivateInput; -use jolt_sdk::UntrustedAdvice; -use std::time::Instant; -use tracing::info; - -pub fn main() { - tracing_subscriber::fmt::init(); - let layout = match std::env::var("FIB2_DORY_LAYOUT") - .ok() - .map(|v| v.to_ascii_lowercase()) - .as_deref() - { - Some("cycle") | Some("cyclemajor") => DoryLayout::CycleMajor, - Some("address") | Some("addressmajor") | None => DoryLayout::AddressMajor, - Some(other) => panic!( - "invalid FIB2_DORY_LAYOUT='{other}', expected one of: cycle, cyclemajor, address, addressmajor" - ), - }; - - DoryGlobals::initialize_context(1, 1, DoryContext::Main, Some(layout)) - .expect("failed to set Dory layout"); - info!("Using Dory layout: {layout:?}"); - - let save_to_disk = std::env::args().any(|arg| arg == "--save"); - let target_dir = "/tmp/jolt-guest-targets"; - - let mut program = guest::compile_fib(target_dir); - let shared_preprocessing = guest::preprocess_shared_fib(&mut program); - let prover_preprocessing = guest::preprocess_prover_fib(shared_preprocessing.clone()); - let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); - #[cfg(feature = "zk")] - let blindfold_setup = Some(prover_preprocessing.blindfold_setup()); - #[cfg(not(feature = "zk"))] - let blindfold_setup = None; - let verifier_preprocessing = - guest::preprocess_verifier_fib(shared_preprocessing, verifier_setup, blindfold_setup); - - if save_to_disk { - serialize_and_print_size( - "Verifier Preprocessing", - "/tmp/jolt_fib2_verifier_preprocessing.dat", - &verifier_preprocessing, - ) - .expect("Could not serialize preprocessing."); - } - - let prove_fib = guest::build_prover_fib(program, prover_preprocessing); - let verify_fib = guest::build_verifier_fib(verifier_preprocessing); - - let now = Instant::now(); - let (output, proof, io_device) = prove_fib(50); - info!("Prover runtime: {} s", now.elapsed().as_secs_f64()); - - if save_to_disk { - serialize_and_print_size("Proof", "/tmp/jolt_fib2_proof.bin", &proof) - .expect("Could not serialize proof."); - serialize_and_print_size("io_device", "/tmp/jolt_fib2_io_device.bin", &io_device) - .expect("Could not serialize io_device."); - } - - let is_valid = verify_fib(50, output, io_device.panic, proof); - info!("output: {output}"); - info!("valid: {is_valid}"); - - #[cfg(feature = "zk")] - { - let mut private_program = guest::compile_fib_with_private_input(target_dir); - let private_shared_preprocessing = - guest::preprocess_shared_fib_with_private_input(&mut private_program); - let private_prover_preprocessing = - guest::preprocess_prover_fib_with_private_input(private_shared_preprocessing.clone()); - let private_verifier_setup = private_prover_preprocessing.generators.to_verifier_setup(); - let private_verifier_preprocessing = guest::preprocess_verifier_fib_with_private_input( - private_shared_preprocessing, - private_verifier_setup, - Some(private_prover_preprocessing.blindfold_setup()), - ); - - let prove_fib_with_private_input = guest::build_prover_fib_with_private_input( - private_program, - private_prover_preprocessing, - ); - let verify_fib_with_private_input = - guest::build_verifier_fib_with_private_input(private_verifier_preprocessing); - - let now = Instant::now(); - let (private_output, private_proof, private_io_device) = - prove_fib_with_private_input(50, PrivateInput::new(0u32)); - info!( - "Prover runtime with private input: {} s", - now.elapsed().as_secs_f64() - ); - - let private_valid = verify_fib_with_private_input( - 50, - private_output, - private_io_device.panic, - private_proof, - ); - info!("output with private input: {private_output}"); - info!("valid with private input: {private_valid}"); - } - - let mut advice_program = guest::compile_fib_with_large_advice_input(target_dir); - let advice_shared_preprocessing = - guest::preprocess_shared_fib_with_large_advice_input(&mut advice_program); - let advice_prover_preprocessing = - guest::preprocess_prover_fib_with_large_advice_input(advice_shared_preprocessing.clone()); - let advice_verifier_setup = advice_prover_preprocessing.generators.to_verifier_setup(); - #[cfg(feature = "zk")] - let advice_blindfold_setup = Some(advice_prover_preprocessing.blindfold_setup()); - #[cfg(not(feature = "zk"))] - let advice_blindfold_setup = None; - let advice_verifier_preprocessing = guest::preprocess_verifier_fib_with_large_advice_input( - advice_shared_preprocessing, - advice_verifier_setup, - advice_blindfold_setup, - ); - - let prove_fib_with_large_advice = guest::build_prover_fib_with_large_advice_input( - advice_program, - advice_prover_preprocessing, - ); - let verify_fib_with_large_advice = - guest::build_verifier_fib_with_large_advice_input(advice_verifier_preprocessing); - - let advice_payload = vec![7u8; 65536]; - let advice_input = UntrustedAdvice::new(advice_payload.as_slice()); - let advice_input_bytes = jolt_sdk::postcard::to_stdvec(&advice_input) - .expect("failed to serialize advice input") - .len(); - - let now = Instant::now(); - let (advice_output, advice_proof, advice_io_device) = - prove_fib_with_large_advice(50, advice_input); - info!( - "Prover runtime with large advice input: {} s", - now.elapsed().as_secs_f64() - ); - - let advice_trace_length = advice_proof.trace_length as usize; - assert!( - advice_input_bytes > advice_trace_length, - "expected advice input bytes ({advice_input_bytes}) to exceed trace length ({advice_trace_length})", - ); - - let advice_valid = - verify_fib_with_large_advice(50, advice_output, advice_io_device.panic, advice_proof); - info!("output with large advice input: {advice_output}"); - info!("valid with large advice input: {advice_valid}"); - info!("advice input bytes: {advice_input_bytes}"); - info!("trace length with large advice input: {advice_trace_length}"); -} diff --git a/jolt-core/src/poly/commitment/dory/wrappers.rs b/jolt-core/src/poly/commitment/dory/wrappers.rs index 32050be144..1ce7149ec3 100644 --- a/jolt-core/src/poly/commitment/dory/wrappers.rs +++ b/jolt-core/src/poly/commitment/dory/wrappers.rs @@ -215,76 +215,73 @@ where "Main+AddressMajor dense polynomial length exceeds trace T" ); - let (dense_affine_bases, dense_chunk_size, dense_sparse_row_terms): ( - Vec<_>, - usize, - Option>>, - ) = if is_trace_dense_addr_major { - let stride = DoryGlobals::dense_stride(); - let cycles_per_row = row_len / stride; - // This branch is taken when the AddressMajor trace-dense embedding stride exceeds - // the post-embedded Main row width (`row_len`), i.e. `row_len < stride`. - // - // With: - // - M = DoryGlobals::get_main_log_embedding() = total embedded Main vars - // - k = log2(main K) - // - t = log2(execution T) - // - e = embedding extra vars = M - (k + t) - // - // we have: - // - row_len = 2^sigma_main, where sigma_main = ceil(M/2) - // = 2^ceil((e + k + t)/2) - // - stride = 2^(main_embedding_extra_vars + k) = 2^(M - t) = 2^(e + k) - // - // so `cycles_per_row == 0` exactly when: - // ceil(M/2) < (M - t) <=> t < floor(M/2). - if cycles_per_row == 0 { - let dense_len = poly.original_len(); - let dense_affine_bases: Vec<_> = g1_slice - .par_iter() - .take(row_len) - .map(|g| g.0.into_affine()) - .collect(); - let num_rows = DoryGlobals::get_max_num_rows(); - let sparse_terms: Vec<(usize, usize, Fr)> = (0..dense_len) - .into_par_iter() - .filter_map(|cycle| { - let coeff = poly.get_coeff(cycle); - if coeff.is_zero() { - return None; - } - let scaled_index = cycle.saturating_mul(stride); - let row_index = scaled_index / row_len; - let col_index = scaled_index % row_len; - debug_assert!(row_index < num_rows); - Some((row_index, col_index, coeff)) - }) - .collect(); - let mut row_terms: Vec> = vec![Vec::new(); num_rows]; - for (row_index, col_index, coeff) in sparse_terms { - row_terms[row_index].push((col_index, coeff)); + let (dense_affine_bases, dense_chunk_size, dense_sparse_row_terms) = + if is_trace_dense_addr_major { + let stride = DoryGlobals::dense_stride(); + let cycles_per_row = row_len / stride; + // This branch is taken when the AddressMajor trace-dense embedding stride exceeds + // the post-embedded Main row width (`row_len`), i.e. `row_len < stride`. + // + // With: + // - M = DoryGlobals::get_main_log_embedding() = total embedded Main vars + // - k = log2(main K) + // - t = log2(execution T) + // - e = embedding extra vars = M - (k + t) + // + // we have: + // - row_len = 2^sigma_main, where sigma_main = ceil(M/2) + // = 2^ceil((e + k + t)/2) + // - stride = 2^(main_embedding_extra_vars + k) = 2^(M - t) = 2^(e + k) + // + // so `cycles_per_row == 0` exactly when: + // ceil(M/2) < (M - t) <=> t < floor(M/2). + if cycles_per_row == 0 { + let dense_len = poly.original_len(); + let dense_affine_bases: Vec<_> = g1_slice + .par_iter() + .take(row_len) + .map(|g| g.0.into_affine()) + .collect(); + let num_rows = DoryGlobals::get_max_num_rows(); + let sparse_terms: Vec<(usize, usize, Fr)> = (0..dense_len) + .into_par_iter() + .filter_map(|cycle| { + let coeff = poly.get_coeff(cycle); + if coeff.is_zero() { + return None; + } + let scaled_index = cycle.saturating_mul(stride); + let row_index = scaled_index / row_len; + let col_index = scaled_index % row_len; + debug_assert!(row_index < num_rows); + Some((row_index, col_index, coeff)) + }) + .collect(); + let mut row_terms: Vec> = vec![Vec::new(); num_rows]; + for (row_index, col_index, coeff) in sparse_terms { + row_terms[row_index].push((col_index, coeff)); + } + (dense_affine_bases, 1, Some(row_terms)) + } else { + let dense_affine_bases: Vec<_> = g1_slice + .par_iter() + .take(row_len) + .step_by(stride) + .map(|g| g.0.into_affine()) + .collect(); + (dense_affine_bases, cycles_per_row, None) } - (dense_affine_bases, 1, Some(row_terms)) } else { - let dense_affine_bases: Vec<_> = g1_slice - .par_iter() - .take(row_len) - .step_by(stride) - .map(|g| g.0.into_affine()) - .collect(); - (dense_affine_bases, cycles_per_row, None) - } - } else { - ( - g1_slice - .par_iter() - .take(row_len) - .map(|g| g.0.into_affine()) - .collect(), - row_len, - None, - ) - }; + ( + g1_slice + .par_iter() + .take(row_len) + .map(|g| g.0.into_affine()) + .collect(), + row_len, + None, + ) + }; if let Some(row_terms) = dense_sparse_row_terms { let result: Vec = row_terms diff --git a/jolt-core/src/poly/rlc_polynomial.rs b/jolt-core/src/poly/rlc_polynomial.rs index 56f3b8b61f..41b2f7a690 100644 --- a/jolt-core/src/poly/rlc_polynomial.rs +++ b/jolt-core/src/poly/rlc_polynomial.rs @@ -530,9 +530,8 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." let num_rows = T / num_columns; let trace_len = trace.len(); - let has_onehot = !ctx.onehot_polys.is_empty(); - let exact_onehot_prefix_mode = - DoryGlobals::get_layout() == DoryLayout::CycleMajor && has_onehot && trace_len < T; + let main_embedding_mode = + DoryGlobals::get_layout() == DoryLayout::CycleMajor && trace_len < T; // When the dominant Stage-8 matrix is larger than the trace-backed prefix, one-hot // witnesses still live on the exact trace prefix rather than the expanded matrix T. @@ -569,14 +568,14 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." // Process valid trace elements. for (col_idx, cycle) in row_cycles.iter().enumerate() { - if exact_onehot_prefix_mode { + if main_embedding_mode { setup.process_cycle_dense( cycle, scaled_rd_inc, scaled_ram_inc, &mut dense_accs[col_idx], ); - setup.process_cycle_onehot_prefix_exact( + setup.process_cycle_onehot_prefix( cycle, chunk_start + col_idx, trace_len, @@ -625,9 +624,8 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." ) -> Vec { let num_rows = T / num_columns; let trace_len = DoryGlobals::main_t(); - let has_onehot = !ctx.onehot_polys.is_empty(); - let exact_onehot_prefix_mode = - DoryGlobals::get_layout() == DoryLayout::CycleMajor && has_onehot && trace_len < T; + let main_embedding_mode = + DoryGlobals::get_layout() == DoryLayout::CycleMajor && trace_len < T; // Setup: precompute coefficients, row factors, and folded one-hot tables. let onehot_rows_per_k = trace_len.div_ceil(num_columns).min(num_rows); @@ -648,14 +646,14 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." // Process columns within chunk sequentially. for (col_idx, cycle) in chunk.iter().enumerate() { let cycle_idx = row_idx * num_columns + col_idx; - if exact_onehot_prefix_mode && cycle_idx < trace_len { + if main_embedding_mode && cycle_idx < trace_len { setup.process_cycle_dense( cycle, scaled_rd_inc, scaled_ram_inc, &mut dense_accs[col_idx], ); - setup.process_cycle_onehot_prefix_exact( + setup.process_cycle_onehot_prefix( cycle, cycle_idx, trace_len, @@ -816,7 +814,7 @@ impl<'a, F: JoltField> VmvSetup<'a, F> { #[allow(clippy::too_many_arguments)] #[inline(always)] - fn process_cycle_onehot_prefix_exact( + fn process_cycle_onehot_prefix( &self, cycle: &Cycle, cycle_idx: usize, diff --git a/jolt-core/src/subprotocols/booleanity.rs b/jolt-core/src/subprotocols/booleanity.rs index c74d689ba8..a5fb220a77 100644 --- a/jolt-core/src/subprotocols/booleanity.rs +++ b/jolt-core/src/subprotocols/booleanity.rs @@ -1,12 +1,11 @@ -//! Booleanity Sumcheck +//! Booleanity Sumcheck (split into address/cycle phases) //! -//! This module implements a single booleanity sumcheck that handles all three families: -//! - Instruction RA polynomials -//! - Bytecode RA polynomials -//! - RAM RA polynomials +//! This module implements Stage 6 booleanity as two explicit sumcheck instances: +//! - Address phase (`log_k_chunk` rounds) +//! - Cycle phase (`log_t` rounds) //! -//! By combining them into a single sumcheck, all families share the same `r_address` and `r_cycle`, -//! which is required by the HammingWeightClaimReduction sumcheck in Stage 7. +//! Both phases still batch all three families together (InstructionRA, BytecodeRA, RAMRA), +//! so they share the same `r_address` and `r_cycle`, matching what Stage 7 claim reductions expect. //! //! ## Sumcheck Relation //! @@ -42,7 +41,7 @@ use crate::{ OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, SumcheckId, VerifierOpeningAccumulator, BIG_ENDIAN, }, - shared_ra_polys::{compute_all_G_and_ra_indices, RaIndices, SharedRaPolynomials}, + shared_ra_polys::{compute_all_G, compute_ra_indices, SharedRaPolynomials}, split_eq_poly::GruenSplitEqPolynomial, unipoly::UniPoly, }, @@ -51,7 +50,7 @@ use crate::{ sumcheck_verifier::{SumcheckInstanceParams, SumcheckInstanceVerifier}, }, transcripts::Transcript, - utils::{expanding_table::ExpandingTable, thread::drop_in_background_thread}, + utils::expanding_table::ExpandingTable, zkvm::{ bytecode::BytecodePreprocessing, config::OneHotParams, @@ -113,18 +112,18 @@ impl SumcheckInstanceParams for BooleanitySumcheckParams { } #[cfg(feature = "zk")] - fn input_constraint_challenge_values(&self, _: &dyn OpeningAccumulator) -> Vec { + fn input_constraint_challenge_values( + &self, + _accumulator: &dyn OpeningAccumulator, + ) -> Vec { Vec::new() } #[cfg(feature = "zk")] fn output_claim_constraint(&self) -> Option { - let n = self.polynomial_types.len(); - - let mut terms = Vec::with_capacity(2 * n); + let mut terms = Vec::with_capacity(2 * self.polynomial_types.len()); for (i, poly_type) in self.polynomial_types.iter().enumerate() { let opening = OpeningId::committed(*poly_type, SumcheckId::Booleanity); - terms.push(ProductTerm::scaled( ValueSource::Challenge(2 * i), vec![ValueSource::Opening(opening), ValueSource::Opening(opening)], @@ -134,22 +133,12 @@ impl SumcheckInstanceParams for BooleanitySumcheckParams { vec![ValueSource::Opening(opening)], )); } - Some(OutputClaimConstraint::sum_of_products(terms)) } #[cfg(feature = "zk")] fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { - let combined_r: Vec = self - .r_address - .iter() - .cloned() - .rev() - .chain(self.r_cycle.iter().cloned().rev()) - .collect(); - - let eq_eval: F = EqPolynomial::::mle(sumcheck_challenges, &combined_r); - + let eq_eval: F = EqPolynomial::::mle(sumcheck_challenges, &self.combined_r_big_endian()); let mut challenges = Vec::with_capacity(2 * self.polynomial_types.len()); for gamma_2i in &self.gamma_powers_square { let coeff = eq_eval * *gamma_2i; @@ -191,19 +180,9 @@ impl BooleanitySumcheckParams { // NOTE: `stage5_point.r` is stored in BIG_ENDIAN format (each segment was reversed by // `normalize_opening_point`). For internal eq evaluations we want LowToHigh (LE) order // because `GruenSplitEqPolynomial` is instantiated with `BindingOrder::LowToHigh`. - debug_assert!( - stage5_point.r.len() == log_k_instruction + log_t, - "InstructionReadRaf opening point length mismatch: got {}, expected {} (= log_k_instruction {} + log_t {})", - stage5_point.r.len(), - log_k_instruction + log_t, - log_k_instruction, - log_t - ); - // Address segment: BE -> LE let mut stage5_addr = stage5_point.r[..log_k_instruction].to_vec(); stage5_addr.reverse(); - // Cycle segment: BE -> LE let mut r_cycle = stage5_point.r[log_k_instruction..].to_vec(); r_cycle.reverse(); @@ -261,105 +240,93 @@ impl BooleanitySumcheckParams { one_hot_params: one_hot_params.clone(), } } + + fn combined_r_big_endian(&self) -> Vec { + self.r_address + .iter() + .cloned() + .rev() + .chain(self.r_cycle.iter().cloned().rev()) + .collect() + } +} + +fn compute_gamma_powers(gamma: F::Challenge, count: usize) -> (Vec, Vec) { + let gamma_f: F = gamma.into(); + let mut powers = Vec::with_capacity(count); + let mut powers_inv = Vec::with_capacity(count); + let mut rho_i = F::one(); + for _ in 0..count { + powers.push(rho_i); + powers_inv.push(rho_i.inverse().expect("gamma powers are nonzero")); + rho_i *= gamma_f; + } + (powers, powers_inv) } -/// Booleanity Sumcheck Prover. +/// Booleanity address-phase prover. #[derive(Allocative)] -pub struct BooleanitySumcheckProver { - /// Per-polynomial powers γ^i (in the base field). - /// Used to pre-scale the address eq tables for phase 2. - gamma_powers: Vec, - /// Per-polynomial inverse powers γ^{-i} (in the base field). - /// Used to unscale cached committed-polynomial openings. - gamma_powers_inv: Vec, +pub struct BooleanityAddressSumcheckProver { /// B: split-eq over address-chunk variables (phase 1, LowToHigh). B: GruenSplitEqPolynomial, - /// D: split-eq over time/cycle variables (phase 2, LowToHigh). - D: GruenSplitEqPolynomial, - /// G[i][k] = Σ_j eq(r_cycle, j) · ra_i(k, j) for all RA polynomials + /// G[i][k] = Σ_j eq(r_cycle, j) · ra_i(k, j) for all RA polynomials. G: Vec>, - /// Shared H polynomials for phase 2 (initialized at transition) - H: Option>, - /// F: Expanding table for phase 1 + /// F: Expanding table over address bits for phase 1. F: ExpandingTable, - /// eq(r_address, r_address) at end of phase 1 - eq_r_r: F, - /// RA indices (non-transposed, one per cycle) - ra_indices: Vec, - pub params: BooleanitySumcheckParams, + /// Most recent round polynomial, used to cache the address-phase output claim. + last_round_poly: Option>, + /// Output claim after the final address round (input claim for cycle phase). + address_claim: Option, + /// Shared booleanity parameters across both phases. + params: BooleanitySumcheckParams, + /// Address-only `SumcheckInstanceParams` wrapper. + address_params: BooleanityAddressPhaseParams, } -impl BooleanitySumcheckProver { - /// Initialize a BooleanitySumcheckProver with all three families. +impl BooleanityAddressSumcheckProver { + /// Initialize the address-phase prover. /// - /// All heavy computation is done here: - /// - Compute G polynomials and RA indices in a single pass over the trace - /// - Initialize split-eq polynomials for address (B) and cycle (D) variables - /// - Initialize expanding table for phase 1 - #[tracing::instrument(skip_all, name = "BooleanitySumcheckProver::initialize")] + /// Heavy precomputation for this phase happens here: + /// - Compute all G-polynomial slices from the trace + /// - Initialize the address split-eq polynomial (`B`) + /// - Initialize the address expanding table (`F`) pub fn initialize( params: BooleanitySumcheckParams, trace: &[Cycle], bytecode: &BytecodePreprocessing, memory_layout: &MemoryLayout, ) -> Self { - // Compute G and RA indices in a single pass over the trace - let (G, ra_indices) = compute_all_G_and_ra_indices::( + let G = compute_all_G::( trace, bytecode, memory_layout, ¶ms.one_hot_params, ¶ms.r_cycle, ); - - // Initialize split-eq polynomials for address and cycle variables let B = GruenSplitEqPolynomial::new(¶ms.r_address, BindingOrder::LowToHigh); - let D = GruenSplitEqPolynomial::new(¶ms.r_cycle, BindingOrder::LowToHigh); - - // Initialize expanding table for phase 1 let k_chunk = 1 << params.log_k_chunk; let mut F_table = ExpandingTable::new(k_chunk, BindingOrder::LowToHigh); F_table.reset(F::one()); - // Compute prover-only fields: gamma_powers (γ^i) and gamma_powers_inv (γ^{-i}) - let num_polys = params.polynomial_types.len(); - let gamma_f: F = params.gamma.into(); - let mut gamma_powers = Vec::with_capacity(num_polys); - let mut gamma_powers_inv = Vec::with_capacity(num_polys); - let mut rho_i = F::one(); - for _ in 0..num_polys { - gamma_powers.push(rho_i); - gamma_powers_inv.push( - rho_i - .inverse() - .expect("gamma_powers[i] is nonzero (gamma != 0)"), - ); - rho_i *= gamma_f; - } - Self { - gamma_powers, - gamma_powers_inv, B, - D, G, - ra_indices, - H: None, F: F_table, - eq_r_r: F::zero(), + last_round_poly: None, + address_claim: None, + address_params: BooleanityAddressPhaseParams::new(params.clone()), params, } } - fn compute_phase1_message(&self, round: usize, previous_claim: F) -> UniPoly { + fn compute_message_impl(&self, round: usize, previous_claim: F) -> UniPoly { let m = round + 1; - let B = &self.B; - let N = self.params.polynomial_types.len(); - - // Compute quadratic coefficients via generic split-eq fold - let quadratic_coeffs: [F; DEGREE_BOUND - 1] = B + let n = self.params.polynomial_types.len(); + // Compute quadratic coefficients via split-eq folding over the unbound address suffix. + let quadratic_coeffs: [F; DEGREE_BOUND - 1] = self + .B .par_fold_out_in_unreduced::<{ DEGREE_BOUND - 1 }>(&|k_prime| { - let coeffs = (0..N) + (0..n) .into_par_iter() .map(|i| { let G_i = &self.G[i]; @@ -370,7 +337,6 @@ impl BooleanitySumcheckProver { let k_m = k >> (m - 1); let F_k = self.F[k & ((1 << (m - 1)) - 1)]; let G_times_F = G_k * F_k; - let eval_infty = G_times_F * F_k; let eval_0 = if k_m == 0 { eval_infty - G_times_F @@ -402,168 +368,11 @@ impl BooleanitySumcheckProver { .reduce( || [F::zero(); DEGREE_BOUND - 1], |running, new| [running[0] + new[0], running[1] + new[1]], - ); - coeffs + ) }); - B.gruen_poly_deg_3(quadratic_coeffs[0], quadratic_coeffs[1], previous_claim) - } - - fn compute_phase2_message(&self, _round: usize, previous_claim: F) -> UniPoly { - let D = &self.D; - let H = self.H.as_ref().expect("H should be initialized in phase 2"); - let num_polys = H.num_polys(); - - // Compute quadratic coefficients via generic split-eq fold (handles both E_in cases). - let quadratic_coeffs: [F; DEGREE_BOUND - 1] = D - .par_fold_out_in_unreduced::<{ DEGREE_BOUND - 1 }>(&|j_prime| { - // Accumulate in unreduced form to minimize per-term reductions - let mut acc_c = F::UnreducedProductAccum::zero(); - let mut acc_e = F::UnreducedProductAccum::zero(); - for i in 0..num_polys { - let h_0 = H.get_bound_coeff(i, 2 * j_prime); - let h_1 = H.get_bound_coeff(i, 2 * j_prime + 1); - let b = h_1 - h_0; - - // Phase-2 optimization: H is pre-scaled by rho_i = gamma^i, so gamma^{2i} - // factors are already accounted for: - // gamma^{2i}*h0*(h0-1) = (rho*h0) * (rho*h0 - rho) - // gamma^{2i}*b*b = (rho*b) * (rho*b) - let rho = self.gamma_powers[i]; - acc_c += h_0.mul_to_product_accum(h_0 - rho); - acc_e += b.mul_to_product_accum(b); - } - [ - F::reduce_product_accum(acc_c), - F::reduce_product_accum(acc_e), - ] - }); - - // previous_claim is s(0)+s(1) of the scaled polynomial; divide out eq_r_r to get inner claim - let adjusted_claim = previous_claim * self.eq_r_r.inverse().unwrap(); - let gruen_poly = - D.gruen_poly_deg_3(quadratic_coeffs[0], quadratic_coeffs[1], adjusted_claim); - - gruen_poly * self.eq_r_r - } - - fn ingest_address_challenge(&mut self, r_j: F::Challenge, round: usize) { - self.B.bind(r_j); - self.F.update(r_j); - - if round == self.params.log_k_chunk - 1 { - self.eq_r_r = self.B.get_current_scalar(); - - let F_table = std::mem::take(&mut self.F); - let ra_indices = std::mem::take(&mut self.ra_indices); - let base_eq = F_table.clone_values(); - let num_polys = self.params.polynomial_types.len(); - debug_assert!( - num_polys == self.gamma_powers.len(), - "gamma_powers length mismatch: got {}, expected {}", - self.gamma_powers.len(), - num_polys - ); - let tables: Vec> = (0..num_polys) - .into_par_iter() - .map(|i| { - let rho = self.gamma_powers[i]; - base_eq.iter().map(|v| rho * *v).collect() - }) - .collect(); - self.H = Some(SharedRaPolynomials::new( - tables, - ra_indices, - self.params.one_hot_params.clone(), - )); - - let g = std::mem::take(&mut self.G); - drop_in_background_thread(g); - } - } - - fn ingest_cycle_challenge(&mut self, r_j: F::Challenge) { - self.D.bind(r_j); - if let Some(ref mut h) = self.H { - h.bind_in_place(r_j, BindingOrder::LowToHigh); - } - } -} - -impl SumcheckInstanceProver for BooleanitySumcheckProver { - fn get_params(&self) -> &dyn SumcheckInstanceParams { - &self.params - } - - #[tracing::instrument(skip_all, name = "BooleanitySumcheckProver::compute_message")] - fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { - if round < self.params.log_k_chunk { - self.compute_phase1_message(round, previous_claim) - } else { - self.compute_phase2_message(round, previous_claim) - } - } - - #[tracing::instrument(skip_all, name = "BooleanitySumcheckProver::ingest_challenge")] - fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { - if round < self.params.log_k_chunk { - self.ingest_address_challenge(r_j, round); - } else { - self.ingest_cycle_challenge(r_j); - } - } - - fn cache_openings( - &self, - accumulator: &mut ProverOpeningAccumulator, - sumcheck_challenges: &[F::Challenge], - ) { - let opening_point = self.params.normalize_opening_point(sumcheck_challenges); - let H = self.H.as_ref().expect("H should be initialized"); - // H is scaled by rho_i; unscale so cached openings match the committed polynomials. - let claims: Vec = (0..H.num_polys()) - .map(|i| H.final_sumcheck_claim(i) * self.gamma_powers_inv[i]) - .collect(); - - // All polynomials share the same opening point (r_address, r_cycle) - // Use a single SumcheckId for all - accumulator.append_sparse( - self.params.polynomial_types.clone(), - SumcheckId::Booleanity, - opening_point.r[..self.params.log_k_chunk].to_vec(), - opening_point.r[self.params.log_k_chunk..].to_vec(), - claims, - ); - } - - #[cfg(feature = "allocative")] - fn update_flamegraph(&self, flamegraph: &mut FlameGraphBuilder) { - flamegraph.visit_root(self); - } -} - -#[derive(Allocative)] -pub struct BooleanityAddressSumcheckProver { - inner: BooleanitySumcheckProver, - address_params: BooleanityAddressPhaseParams, - last_round_poly: Option>, - address_claim: Option, -} - -impl BooleanityAddressSumcheckProver { - pub fn initialize( - params: BooleanitySumcheckParams, - trace: &[Cycle], - bytecode: &BytecodePreprocessing, - memory_layout: &MemoryLayout, - ) -> Self { - let address_params = BooleanityAddressPhaseParams::new(params.clone()); - Self { - inner: BooleanitySumcheckProver::initialize(params, trace, bytecode, memory_layout), - address_params, - last_round_poly: None, - address_claim: None, - } + self.B + .gruen_poly_deg_3(quadratic_coeffs[0], quadratic_coeffs[1], previous_claim) } } @@ -574,20 +383,8 @@ impl SumcheckInstanceProver &self.address_params } - fn degree(&self) -> usize { - self.inner.params.degree() - } - - fn num_rounds(&self) -> usize { - self.inner.params.log_k_chunk - } - - fn input_claim(&self, accumulator: &ProverOpeningAccumulator) -> F { - self.inner.params.input_claim(accumulator) - } - fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { - let poly = self.inner.compute_phase1_message(round, previous_claim); + let poly = self.compute_message_impl(round, previous_claim); self.last_round_poly = Some(poly.clone()); poly } @@ -595,11 +392,12 @@ impl SumcheckInstanceProver fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { if let Some(poly) = self.last_round_poly.take() { let claim = poly.evaluate(&r_j); - if round == self.inner.params.log_k_chunk - 1 { + if round == self.params.log_k_chunk - 1 { self.address_claim = Some(claim); } } - self.inner.ingest_address_challenge(r_j, round); + self.B.bind(r_j); + self.F.update(r_j); } fn cache_openings( @@ -607,6 +405,7 @@ impl SumcheckInstanceProver accumulator: &mut ProverOpeningAccumulator, sumcheck_challenges: &[F::Challenge], ) { + // Cache the intermediate address-phase claim used as input to cycle phase. let mut r_address = sumcheck_challenges.to_vec(); r_address.reverse(); accumulator.append_virtual( @@ -624,14 +423,27 @@ impl SumcheckInstanceProver } } +/// Booleanity cycle-phase prover. #[derive(Allocative)] pub struct BooleanityCycleSumcheckProver { - inner: BooleanitySumcheckProver, + /// D: split-eq over cycle variables (phase 2, LowToHigh). + D: GruenSplitEqPolynomial, + /// Shared RA polynomials, pre-scaled for batched cycle-phase accumulation. + H: SharedRaPolynomials, + /// eq(r_address, r_address), carried from address-phase binding. + eq_r_r: F, + /// Per-polynomial powers γ^i used for pre-scaling. + gamma_powers: Vec, + /// Per-polynomial inverse powers γ^{-i} used to unscale cached openings. + gamma_powers_inv: Vec, + /// Shared booleanity parameters across both phases. + params: BooleanitySumcheckParams, + /// Cycle-only `SumcheckInstanceParams` wrapper. cycle_params: BooleanityCyclePhaseParams, } impl BooleanityCycleSumcheckProver { - #[tracing::instrument(skip_all, name = "BooleanityCycleSumcheckProver::initialize")] + /// Initialize cycle-phase state from the cached address-phase opening. pub fn initialize( params: BooleanitySumcheckParams, trace: &[Cycle], @@ -648,17 +460,74 @@ impl BooleanityCycleSumcheckProver { let cycle_params = BooleanityCyclePhaseParams::new(params.clone(), r_address_low_to_high.clone()); - let mut inner = - BooleanitySumcheckProver::initialize(params, trace, bytecode, memory_layout); - for (round, r_j) in r_address_low_to_high.iter().cloned().enumerate() { - inner.ingest_address_challenge(r_j, round); + let mut B = GruenSplitEqPolynomial::new(¶ms.r_address, BindingOrder::LowToHigh); + for r_j in r_address_low_to_high.iter().copied() { + B.bind(r_j); } + let eq_r_r = B.get_current_scalar(); + + let k_chunk = 1 << params.log_k_chunk; + let mut F_table = ExpandingTable::new(k_chunk, BindingOrder::LowToHigh); + F_table.reset(F::one()); + for r_j in r_address_low_to_high.iter().copied() { + F_table.update(r_j); + } + let base_eq = F_table.clone_values(); + + let ra_indices = compute_ra_indices(trace, bytecode, memory_layout, ¶ms.one_hot_params); + let num_polys = params.polynomial_types.len(); + let (gamma_powers, gamma_powers_inv) = compute_gamma_powers(params.gamma, num_polys); + let tables: Vec> = (0..num_polys) + .into_par_iter() + .map(|i| { + let rho = gamma_powers[i]; + base_eq.iter().map(|v| rho * *v).collect() + }) + .collect(); Self { - inner, + D: GruenSplitEqPolynomial::new(¶ms.r_cycle, BindingOrder::LowToHigh), + H: SharedRaPolynomials::new(tables, ra_indices, params.one_hot_params.clone()), + eq_r_r, + gamma_powers, + gamma_powers_inv, cycle_params, + params, } } + + fn compute_message_impl(&self, previous_claim: F) -> UniPoly { + let num_polys = self.H.num_polys(); + let quadratic_coeffs: [F; DEGREE_BOUND - 1] = self + .D + .par_fold_out_in_unreduced::<{ DEGREE_BOUND - 1 }>(&|j_prime| { + // Accumulate in unreduced form to minimize per-term reductions. + let mut acc_c = F::UnreducedProductAccum::zero(); + let mut acc_e = F::UnreducedProductAccum::zero(); + for i in 0..num_polys { + let h_0 = self.H.get_bound_coeff(i, 2 * j_prime); + let h_1 = self.H.get_bound_coeff(i, 2 * j_prime + 1); + let b = h_1 - h_0; + // Phase-2 optimization: H is pre-scaled by rho_i = gamma^i, so gamma^{2i} + // factors are already accounted for: + // gamma^{2i}*h0*(h0-1) = (rho*h0) * (rho*h0 - rho) + // gamma^{2i}*b*b = (rho*b) * (rho*b) + let rho = self.gamma_powers[i]; + acc_c += h_0.mul_to_product_accum(h_0 - rho); + acc_e += b.mul_to_product_accum(b); + } + [ + F::reduce_product_accum(acc_c), + F::reduce_product_accum(acc_e), + ] + }); + // previous_claim is s(0)+s(1) of the scaled polynomial; divide out eq_r_r to get inner claim + let adjusted_claim = previous_claim * self.eq_r_r.inverse().unwrap(); + let gruen_poly = + self.D + .gruen_poly_deg_3(quadratic_coeffs[0], quadratic_coeffs[1], adjusted_claim); + gruen_poly * self.eq_r_r + } } impl SumcheckInstanceProver @@ -668,29 +537,13 @@ impl SumcheckInstanceProver &self.cycle_params } - fn degree(&self) -> usize { - self.inner.params.degree() - } - - fn num_rounds(&self) -> usize { - self.inner.params.log_t - } - - fn input_claim(&self, accumulator: &ProverOpeningAccumulator) -> F { - accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::BooleanityAddrClaim, - SumcheckId::BooleanityAddressPhase, - ) - .1 - } - - fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { - self.inner.compute_phase2_message(round, previous_claim) + fn compute_message(&mut self, _round: usize, previous_claim: F) -> UniPoly { + self.compute_message_impl(previous_claim) } fn ingest_challenge(&mut self, r_j: F::Challenge, _round: usize) { - self.inner.ingest_cycle_challenge(r_j); + self.D.bind(r_j); + self.H.bind_in_place(r_j, BindingOrder::LowToHigh); } fn cache_openings( @@ -698,12 +551,18 @@ impl SumcheckInstanceProver accumulator: &mut ProverOpeningAccumulator, sumcheck_challenges: &[F::Challenge], ) { - let mut full_challenges = self.cycle_params.r_address_low_to_high.clone(); - full_challenges.extend_from_slice(sumcheck_challenges); - as SumcheckInstanceProver>::cache_openings( - &self.inner, - accumulator, - &full_challenges, + let full_challenges = self.cycle_params.full_challenges(sumcheck_challenges); + let opening_point = self.params.normalize_opening_point(&full_challenges); + // H is scaled by rho_i; unscale so cached openings match the committed polynomials. + let claims: Vec = (0..self.H.num_polys()) + .map(|i| self.H.final_sumcheck_claim(i) * self.gamma_powers_inv[i]) + .collect(); + accumulator.append_sparse( + self.params.polynomial_types.clone(), + SumcheckId::Booleanity, + opening_point.r[..self.params.log_k_chunk].to_vec(), + opening_point.r[self.params.log_k_chunk..].to_vec(), + claims, ); } @@ -713,67 +572,7 @@ impl SumcheckInstanceProver } } -/// Booleanity Sumcheck Verifier. -pub struct BooleanitySumcheckVerifier { - params: BooleanitySumcheckParams, -} - -impl BooleanitySumcheckVerifier { - pub fn new(params: BooleanitySumcheckParams) -> Self { - Self { params } - } -} - -impl SumcheckInstanceVerifier for BooleanitySumcheckVerifier { - fn get_params(&self) -> &dyn SumcheckInstanceParams { - &self.params - } - - fn expected_output_claim( - &self, - accumulator: &VerifierOpeningAccumulator, - sumcheck_challenges: &[F::Challenge], - ) -> F { - let ra_claims: Vec = self - .params - .polynomial_types - .iter() - .map(|poly_type| { - accumulator - .get_committed_polynomial_opening(*poly_type, SumcheckId::Booleanity) - .1 - }) - .collect(); - - let combined_r: Vec = self - .params - .r_address - .iter() - .cloned() - .rev() - .chain(self.params.r_cycle.iter().cloned().rev()) - .collect(); - - EqPolynomial::::mle(sumcheck_challenges, &combined_r) - * zip(&self.params.gamma_powers_square, ra_claims) - .map(|(gamma_2i, ra)| (ra.square() - ra) * gamma_2i) - .sum::() - } - - fn cache_openings( - &self, - accumulator: &mut VerifierOpeningAccumulator, - sumcheck_challenges: &[F::Challenge], - ) { - let opening_point = self.params.normalize_opening_point(sumcheck_challenges); - accumulator.append_sparse( - self.params.polynomial_types.clone(), - SumcheckId::Booleanity, - opening_point.r, - ); - } -} - +/// Booleanity address-phase verifier. pub struct BooleanityAddressSumcheckVerifier { params: BooleanitySumcheckParams, address_params: BooleanityAddressPhaseParams, @@ -781,10 +580,9 @@ pub struct BooleanityAddressSumcheckVerifier { impl BooleanityAddressSumcheckVerifier { pub fn new(params: BooleanitySumcheckParams) -> Self { - let address_params = BooleanityAddressPhaseParams::new(params.clone()); Self { + address_params: BooleanityAddressPhaseParams::new(params.clone()), params, - address_params, } } @@ -800,18 +598,6 @@ impl SumcheckInstanceVerifier &self.address_params } - fn degree(&self) -> usize { - self.params.degree() - } - - fn num_rounds(&self) -> usize { - self.params.log_k_chunk - } - - fn input_claim(&self, accumulator: &VerifierOpeningAccumulator) -> F { - self.params.input_claim(accumulator) - } - fn expected_output_claim( &self, accumulator: &VerifierOpeningAccumulator, @@ -840,6 +626,7 @@ impl SumcheckInstanceVerifier } } +/// Booleanity cycle-phase verifier. pub struct BooleanityCycleSumcheckVerifier { params: BooleanitySumcheckParams, cycle_params: BooleanityCyclePhaseParams, @@ -856,10 +643,9 @@ impl BooleanityCycleSumcheckVerifier { ); let mut r_address_low_to_high = r_address_point.r; r_address_low_to_high.reverse(); - let cycle_params = BooleanityCyclePhaseParams::new(params.clone(), r_address_low_to_high); Self { + cycle_params: BooleanityCyclePhaseParams::new(params.clone(), r_address_low_to_high), params, - cycle_params, } } } @@ -871,39 +657,26 @@ impl SumcheckInstanceVerifier &self.cycle_params } - fn degree(&self) -> usize { - self.params.degree() - } - - fn num_rounds(&self) -> usize { - self.params.log_t - } - - fn input_claim(&self, accumulator: &VerifierOpeningAccumulator) -> F { - accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::BooleanityAddrClaim, - SumcheckId::BooleanityAddressPhase, - ) - .1 - } - fn expected_output_claim( &self, accumulator: &VerifierOpeningAccumulator, sumcheck_challenges: &[F::Challenge], ) -> F { - let mut full_challenges = self.cycle_params.r_address_low_to_high.clone(); - full_challenges.extend_from_slice(sumcheck_challenges); - - let inner = BooleanitySumcheckVerifier { - params: self.params.clone(), - }; - as SumcheckInstanceVerifier>::expected_output_claim( - &inner, - accumulator, - &full_challenges, - ) + let full_challenges = self.cycle_params.full_challenges(sumcheck_challenges); + let ra_claims: Vec = self + .params + .polynomial_types + .iter() + .map(|poly_type| { + accumulator + .get_committed_polynomial_opening(*poly_type, SumcheckId::Booleanity) + .1 + }) + .collect(); + EqPolynomial::::mle(&full_challenges, &self.params.combined_r_big_endian()) + * zip(&self.params.gamma_powers_square, ra_claims) + .map(|(gamma_2i, ra)| (ra.square() - ra) * gamma_2i) + .sum::() } fn cache_openings( @@ -911,20 +684,77 @@ impl SumcheckInstanceVerifier accumulator: &mut VerifierOpeningAccumulator, sumcheck_challenges: &[F::Challenge], ) { - let mut full_challenges = self.cycle_params.r_address_low_to_high.clone(); - full_challenges.extend_from_slice(sumcheck_challenges); - - let inner = BooleanitySumcheckVerifier { - params: self.params.clone(), - }; - as SumcheckInstanceVerifier>::cache_openings( - &inner, - accumulator, - &full_challenges, + let full_challenges = self.cycle_params.full_challenges(sumcheck_challenges); + let opening_point = self.params.normalize_opening_point(&full_challenges); + accumulator.append_sparse( + self.params.polynomial_types.clone(), + SumcheckId::Booleanity, + opening_point.r, ); } } +#[derive(Allocative, Clone)] +struct BooleanityAddressPhaseParams { + inner: BooleanitySumcheckParams, +} + +impl BooleanityAddressPhaseParams { + fn new(inner: BooleanitySumcheckParams) -> Self { + Self { inner } + } +} + +impl SumcheckInstanceParams for BooleanityAddressPhaseParams { + fn degree(&self) -> usize { + as SumcheckInstanceParams>::degree(&self.inner) + } + + fn num_rounds(&self) -> usize { + self.inner.log_k_chunk + } + + fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { + as SumcheckInstanceParams>::input_claim( + &self.inner, + accumulator, + ) + } + + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + let mut r = challenges.to_vec(); + r.reverse(); + OpeningPoint::new(r) + } + + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + as SumcheckInstanceParams>::input_claim_constraint( + &self.inner, + ) + } + + #[cfg(feature = "zk")] + fn input_constraint_challenge_values(&self, accumulator: &dyn OpeningAccumulator) -> Vec { + as SumcheckInstanceParams>::input_constraint_challenge_values( + &self.inner, + accumulator, + ) + } + + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + Some(OutputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ))) + } + + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, _sumcheck_challenges: &[F::Challenge]) -> Vec { + Vec::new() + } +} #[derive(Allocative, Clone)] struct BooleanityCyclePhaseParams { inner: BooleanitySumcheckParams, @@ -1004,65 +834,3 @@ impl SumcheckInstanceParams for BooleanityCyclePhaseParams { ) } } - -#[derive(Allocative, Clone)] -struct BooleanityAddressPhaseParams { - inner: BooleanitySumcheckParams, -} - -impl BooleanityAddressPhaseParams { - fn new(inner: BooleanitySumcheckParams) -> Self { - Self { inner } - } -} - -impl SumcheckInstanceParams for BooleanityAddressPhaseParams { - fn degree(&self) -> usize { - as SumcheckInstanceParams>::degree(&self.inner) - } - - fn num_rounds(&self) -> usize { - self.inner.log_k_chunk - } - - fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { - as SumcheckInstanceParams>::input_claim( - &self.inner, - accumulator, - ) - } - - fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { - let mut r = challenges.to_vec(); - r.reverse(); - OpeningPoint::new(r) - } - - #[cfg(feature = "zk")] - fn input_claim_constraint(&self) -> InputClaimConstraint { - as SumcheckInstanceParams>::input_claim_constraint( - &self.inner, - ) - } - - #[cfg(feature = "zk")] - fn input_constraint_challenge_values(&self, accumulator: &dyn OpeningAccumulator) -> Vec { - as SumcheckInstanceParams>::input_constraint_challenge_values( - &self.inner, - accumulator, - ) - } - - #[cfg(feature = "zk")] - fn output_claim_constraint(&self) -> Option { - Some(OutputClaimConstraint::direct(OpeningId::virt( - VirtualPolynomial::BooleanityAddrClaim, - SumcheckId::BooleanityAddressPhase, - ))) - } - - #[cfg(feature = "zk")] - fn output_constraint_challenge_values(&self, _sumcheck_challenges: &[F::Challenge]) -> Vec { - Vec::new() - } -} diff --git a/jolt-core/src/subprotocols/mod.rs b/jolt-core/src/subprotocols/mod.rs index b0c476e4d0..ecd4708549 100644 --- a/jolt-core/src/subprotocols/mod.rs +++ b/jolt-core/src/subprotocols/mod.rs @@ -10,7 +10,3 @@ pub mod sumcheck_claim; pub mod sumcheck_prover; pub mod sumcheck_verifier; pub mod univariate_skip; - -pub use booleanity::{ - BooleanitySumcheckParams, BooleanitySumcheckProver, BooleanitySumcheckVerifier, -}; diff --git a/jolt-core/src/subprotocols/sumcheck.rs b/jolt-core/src/subprotocols/sumcheck.rs index 49ac01cf55..803486ec19 100644 --- a/jolt-core/src/subprotocols/sumcheck.rs +++ b/jolt-core/src/subprotocols/sumcheck.rs @@ -30,24 +30,6 @@ pub use crate::subprotocols::univariate_skip::UniSkipFirstRoundProof; /// We do what they describe as "front-loaded" batch sumcheck. pub enum BatchedSumcheck {} impl BatchedSumcheck { - fn debug_final_claims_enabled() -> bool { - std::env::var("JOLT_DEBUG_BATCHED_SUMCHECK_FINAL_CLAIMS") - .map(|v| { - let value = v.trim().to_ascii_lowercase(); - !matches!(value.as_str(), "" | "0" | "false" | "off") - }) - .unwrap_or(false) - } - - fn debug_pending_ids_enabled() -> bool { - std::env::var("JOLT_DEBUG_PENDING_IDS") - .map(|v| { - let value = v.trim().to_ascii_lowercase(); - !matches!(value.as_str(), "" | "0" | "false" | "off") - }) - .unwrap_or(false) - } - /// Returns (proof, challenges, initial_batched_claim) /// For non-ZK mode - returns ClearSumcheckProof with polynomial coefficients visible. pub fn prove( @@ -61,15 +43,9 @@ impl BatchedSumcheck { .max() .unwrap(); - let input_claims: Vec = sumcheck_instances - .iter() - .map(|sumcheck| sumcheck.input_claim(opening_accumulator)) - .collect(); - if Self::debug_final_claims_enabled() { - tracing::info!("BatchedSumcheck::prove input claims: {:?}", input_claims); - } - input_claims.iter().for_each(|input_claim| { - transcript.append_scalar(b"sumcheck_claim", input_claim); + sumcheck_instances.iter().for_each(|sumcheck| { + let input_claim = sumcheck.input_claim(opening_accumulator); + transcript.append_scalar(b"sumcheck_claim", &input_claim); }); let batching_coeffs: Vec = transcript.challenge_vector(sumcheck_instances.len()); @@ -84,9 +60,9 @@ impl BatchedSumcheck { // = A * 2^N * claim_a + B * claim_b let mut individual_claims: Vec = sumcheck_instances .iter() - .zip(input_claims.iter()) - .map(|(sumcheck, input_claim)| { + .map(|sumcheck| { let num_rounds = sumcheck.num_rounds(); + let input_claim = sumcheck.input_claim(opening_accumulator); input_claim.mul_pow_2(max_num_rounds - num_rounds) }) .collect(); @@ -189,26 +165,6 @@ impl BatchedSumcheck { .max() .unwrap(); - if Self::debug_final_claims_enabled() { - let final_claims: Vec<(usize, usize, usize, F)> = sumcheck_instances - .iter() - .zip(individual_claims.iter()) - .enumerate() - .map(|(idx, (sumcheck, claim))| { - ( - idx, - sumcheck.round_offset(max_num_rounds), - sumcheck.num_rounds(), - *claim, - ) - }) - .collect(); - tracing::info!( - "BatchedSumcheck::prove final individual claims: {:?}", - final_claims - ); - } - for sumcheck in sumcheck_instances.iter() { // Instance-local slice can start at a custom global offset. let offset = sumcheck.round_offset(max_num_rounds); @@ -219,15 +175,6 @@ impl BatchedSumcheck { sumcheck.cache_openings(opening_accumulator, r_slice); } - if Self::debug_pending_ids_enabled() { - tracing::info!( - "BatchedSumcheck::prove pending_ids (instances={} rounds={}): {:?}", - sumcheck_instances.len(), - max_num_rounds, - opening_accumulator.pending_claim_ids_debug() - ); - } - opening_accumulator.flush_to_transcript(transcript); ( @@ -502,22 +449,12 @@ impl BatchedSumcheck { .sum(); let (output_claim, r_sumcheck) = - proof.verify(claim, max_num_rounds, max_degree, transcript) - .map_err(|err| { - tracing::error!( - "BatchedSumcheck::verify failed inside proof.verify: claim={} max_num_rounds={} max_degree={}", - claim, - max_num_rounds, - max_degree - ); - err - })?; - - let expected_output_terms: Vec<(usize, usize, F, F, F)> = sumcheck_instances + proof.verify(claim, max_num_rounds, max_degree, transcript)?; + + let expected_output_claim: F = sumcheck_instances .iter() .zip(batching_coeffs.iter()) - .enumerate() - .map(|(idx, (sumcheck, coeff))| { + .map(|(sumcheck, coeff)| { let offset = sumcheck.round_offset(max_num_rounds); let r_slice = &r_sumcheck[offset..offset + sumcheck.num_rounds()]; @@ -526,29 +463,10 @@ impl BatchedSumcheck { sumcheck.cache_openings(opening_accumulator, r_slice); let claim = sumcheck.expected_output_claim(opening_accumulator, r_slice); - (idx, sumcheck.num_rounds(), *coeff, claim, claim * coeff) + claim * coeff }) - .collect(); - if Self::debug_final_claims_enabled() { - tracing::info!( - "BatchedSumcheck::verify expected output terms: {:?}", - expected_output_terms - ); - } - let expected_output_claim: F = expected_output_terms - .iter() - .map(|(_, _, _, _, weighted_claim)| *weighted_claim) .sum(); - if Self::debug_pending_ids_enabled() { - tracing::info!( - "BatchedSumcheck::verify pending_ids (instances={} rounds={}): {:?}", - sumcheck_instances.len(), - max_num_rounds, - opening_accumulator.pending_claim_ids_debug() - ); - } - if !is_zk { opening_accumulator.flush_to_transcript(transcript); } else if let SumcheckInstanceProof::Zk(zk_proof) = proof { @@ -559,15 +477,6 @@ impl BatchedSumcheck { // In ZK mode, skip output claim verification — BlindFold proves this if !is_zk && output_claim != expected_output_claim { - tracing::error!( - "BatchedSumcheck::verify output-claim mismatch: output_claim={} expected_output_claim={}", - output_claim, - expected_output_claim - ); - tracing::error!( - "BatchedSumcheck::verify expected output terms: {:?}", - expected_output_terms - ); return Err(ProofVerifyError::SumcheckVerificationError); } diff --git a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs index 8ba8fc6df4..ffc21a15a6 100644 --- a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs +++ b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs @@ -389,7 +389,6 @@ impl BytecodeReadRafSumcheckProver { .unwrap(); self.bound_val_evals = Some(bound_val_evals); self.params.bound_val_polys = Some(bound_val_evals); - self.params.bound_int_poly = Some(int_poly); let bound_f_entry = self.f_entry_expected.final_sumcheck_claim(); self.bound_f_entry = Some(bound_f_entry); @@ -928,10 +927,11 @@ impl BytecodeReadRafSumcheckVerifier { ) -> Self { Self { params: BytecodeReadRafSumcheckParams::gen( - program, + Some(program), n_cycle_vars, one_hot_params, use_staged_val_claims, + None, opening_accumulator, transcript, ), @@ -957,16 +957,14 @@ impl SumcheckInstanceVerifier let int_poly = self.params.int_poly.evaluate(&r_address_prime.r); - let ra_claims: Vec = (0..self.params.d) - .map(|i| { - accumulator - .get_committed_polynomial_opening( - CommittedPolynomial::BytecodeRa(i), - SumcheckId::BytecodeReadRaf, - ) - .1 - }) - .collect(); + let ra_claims = (0..self.params.d).map(|i| { + accumulator + .get_committed_polynomial_opening( + CommittedPolynomial::BytecodeRa(i), + SumcheckId::BytecodeReadRaf, + ) + .1 + }); // We have a separate Val polynomial for each stage // Additionally, for stages 1 and 3 we have an Int polynomial for RAF @@ -1028,9 +1026,7 @@ impl SumcheckInstanceVerifier let eq_zero_at_r_cycle = EqPolynomial::::mle(&zeros, &r_cycle_prime.r); let entry_contrib = self.params.entry_gamma * entry_f_at_r_addr * eq_zero_at_r_cycle; - ra_claims - .iter() - .fold(val + entry_contrib, |running, ra_claim| running * *ra_claim) + ra_claims.fold(val + entry_contrib, |running, ra_claim| running * ra_claim) } fn cache_openings( @@ -1074,22 +1070,25 @@ impl BytecodeReadRafAddressSumcheckVerifier { entry_bytecode_index: usize, ) -> Result { let params = match program_mode { - ProgramMode::Committed => BytecodeReadRafSumcheckParams::gen_verifier( + ProgramMode::Committed => BytecodeReadRafSumcheckParams::gen( + None, n_cycle_vars, one_hot_params, - entry_bytecode_index, + true, + Some(entry_bytecode_index), opening_accumulator, transcript, ), ProgramMode::Full => BytecodeReadRafSumcheckParams::gen( - program.ok_or_else(|| { + Some(program.ok_or_else(|| { ProofVerifyError::BytecodeTypeMismatch( "expected Full program preprocessing, got Committed".to_string(), ) - })?, + })?), n_cycle_vars, one_hot_params, false, + None, opening_accumulator, transcript, ), @@ -1426,7 +1425,6 @@ pub struct BytecodeReadRafSumcheckParams { pub int_poly: IdentityPolynomial, pub r_cycles: [Vec; N_STAGES], /// Bound values after log_K rounds (set by prover for output_constraint_challenge_values) - pub bound_int_poly: Option, pub bound_val_polys: Option<[F; N_STAGES]>, /// γ_entry = gamma_powers[7]. Weights the entry-point constraint term. pub entry_gamma: F, @@ -1439,47 +1437,6 @@ pub struct BytecodeReadRafSumcheckParams { impl BytecodeReadRafSumcheckParams { #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckParams::gen")] pub fn gen( - program: &ProgramPreprocessing, - n_cycle_vars: usize, - one_hot_params: &OneHotParams, - use_staged_val_claims: bool, - opening_accumulator: &dyn OpeningAccumulator, - transcript: &mut impl Transcript, - ) -> Self { - Self::gen_impl( - Some(program), - n_cycle_vars, - one_hot_params, - use_staged_val_claims, - Some(program.entry_bytecode_index()), - opening_accumulator, - transcript, - true, - ) - } - - #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckParams::gen_verifier")] - pub fn gen_verifier( - n_cycle_vars: usize, - one_hot_params: &OneHotParams, - entry_bytecode_index: usize, - opening_accumulator: &dyn OpeningAccumulator, - transcript: &mut impl Transcript, - ) -> Self { - Self::gen_impl( - None, - n_cycle_vars, - one_hot_params, - true, - Some(entry_bytecode_index), - opening_accumulator, - transcript, - false, - ) - } - - #[allow(clippy::too_many_arguments)] - fn gen_impl( program: Option<&ProgramPreprocessing>, n_cycle_vars: usize, one_hot_params: &OneHotParams, @@ -1487,7 +1444,6 @@ impl BytecodeReadRafSumcheckParams { entry_bytecode_index: Option, opening_accumulator: &dyn OpeningAccumulator, transcript: &mut impl Transcript, - compute_val_polys: bool, ) -> Self { let gamma_powers = transcript.challenge_scalar_powers(8); @@ -1506,34 +1462,30 @@ impl BytecodeReadRafSumcheckParams { let rv_claim_5 = Self::compute_rv_claim_5(opening_accumulator, &stage5_gammas); let rv_claims = [rv_claim_1, rv_claim_2, rv_claim_3, rv_claim_4, rv_claim_5]; - // Pre-compute eq_r_register for stages 4 and 5 (they use different r_register points) - let r_register_4 = opening_accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::RdWa, - SumcheckId::RegistersReadWriteChecking, - ) - .0 - .r; - let eq_r_register_4 = - EqPolynomial::::evals(&r_register_4[..(REGISTER_COUNT as usize).log_2()]); - - let r_register_5 = opening_accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::RdWa, - SumcheckId::RegistersValEvaluation, - ) - .0 - .r; - let eq_r_register_5 = - EqPolynomial::::evals(&r_register_5[..(REGISTER_COUNT as usize).log_2()]); + // Fused pass: compute all val polynomials in a single parallel iteration in Full mode. + let val_polys = if let Some(program) = program { + let r_register_4 = opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::RdWa, + SumcheckId::RegistersReadWriteChecking, + ) + .0 + .r; + let eq_r_register_4 = + EqPolynomial::::evals(&r_register_4[..(REGISTER_COUNT as usize).log_2()]); + + let r_register_5 = opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::RdWa, + SumcheckId::RegistersValEvaluation, + ) + .0 + .r; + let eq_r_register_5 = + EqPolynomial::::evals(&r_register_5[..(REGISTER_COUNT as usize).log_2()]); - // Fused pass: compute all val polynomials in a single parallel iteration - let val_polys = if compute_val_polys { Self::compute_val_polys( - &program - .expect("compute_val_polys requires program preprocessing") - .bytecode - .bytecode, + &program.bytecode.bytecode, &eq_r_register_4, &eq_r_register_5, &stage1_gammas, @@ -1623,7 +1575,6 @@ impl BytecodeReadRafSumcheckParams { raf_shift_claim, int_poly, r_cycles, - bound_int_poly: None, bound_val_polys: None, bound_f_entry: None, } diff --git a/jolt-core/src/zkvm/claim_reductions/precommitted.rs b/jolt-core/src/zkvm/claim_reductions/precommitted.rs index 3f201d482f..571bab226c 100644 --- a/jolt-core/src/zkvm/claim_reductions/precommitted.rs +++ b/jolt-core/src/zkvm/claim_reductions/precommitted.rs @@ -214,16 +214,12 @@ impl PrecommittedClaimReduction { #[inline] pub fn is_cycle_phase_round(&self, round: usize) -> bool { - self.cycle_phase_rounds - .iter() - .any(|&scheduled| scheduled == round) + self.cycle_phase_rounds.contains(&round) } #[inline] pub fn is_address_phase_round(&self, round: usize) -> bool { - self.address_phase_rounds - .iter() - .any(|&scheduled| scheduled == round) + self.address_phase_rounds.contains(&round) } #[inline] @@ -374,13 +370,13 @@ where .collect() } -pub fn precommitted_eq_evals_with_scaling( +pub fn precommitted_eq_evals_with_scaling( challenges_be: &[F::Challenge], scaling_factor: Option, precommitted: &PrecommittedClaimReduction, ) -> Vec where - F: std::ops::Mul + std::ops::SubAssign, + F: JoltField + std::ops::Mul + std::ops::SubAssign, { let permuted_challenges = precommitted_permute_eq_challenges( challenges_be, diff --git a/jolt-core/src/zkvm/mod.rs b/jolt-core/src/zkvm/mod.rs index 2699581e11..90a7f79416 100644 --- a/jolt-core/src/zkvm/mod.rs +++ b/jolt-core/src/zkvm/mod.rs @@ -41,50 +41,12 @@ pub mod spartan; pub mod verifier; pub mod witness; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum Stage8ProgramOpenings { - Both, - Bytecode, - ProgramImage, - None, -} - -impl Stage8ProgramOpenings { - pub(crate) fn includes_bytecode(self) -> bool { - matches!(self, Self::Both | Self::Bytecode) - } - - pub(crate) fn includes_program_image(self) -> bool { - matches!(self, Self::Both | Self::ProgramImage) - } -} - -pub(crate) fn stage8_program_openings_from_env() -> Stage8ProgramOpenings { - let Ok(raw) = std::env::var("JOLT_STAGE8_PROGRAM_OPENINGS") else { - return Stage8ProgramOpenings::Both; - }; - - match raw.trim().to_ascii_lowercase().as_str() { - "" | "both" => Stage8ProgramOpenings::Both, - "bytecode" => Stage8ProgramOpenings::Bytecode, - "program_image" | "program-image" => Stage8ProgramOpenings::ProgramImage, - "none" => Stage8ProgramOpenings::None, - other => { - tracing::warn!( - "Unrecognized JOLT_STAGE8_PROGRAM_OPENINGS value `{other}`; defaulting to `both`" - ); - Stage8ProgramOpenings::Both - } - } -} - pub(crate) fn stage8_opening_ids( one_hot_params: &OneHotParams, include_trusted_advice: bool, include_untrusted_advice: bool, program_mode: ProgramMode, bytecode_chunk_count: usize, - stage8_program_openings: Stage8ProgramOpenings, ) -> Vec { let mut opening_ids = Vec::new(); @@ -122,7 +84,7 @@ pub(crate) fn stage8_opening_ids( if include_untrusted_advice { opening_ids.push(OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReduction)); } - if program_mode == ProgramMode::Committed && stage8_program_openings.includes_bytecode() { + if program_mode == ProgramMode::Committed { for i in 0..bytecode_chunk_count { opening_ids.push(OpeningId::committed( CommittedPolynomial::BytecodeChunk(i), @@ -130,7 +92,7 @@ pub(crate) fn stage8_opening_ids( )); } } - if program_mode == ProgramMode::Committed && stage8_program_openings.includes_program_image() { + if program_mode == ProgramMode::Committed { opening_ids.push(OpeningId::committed( CommittedPolynomial::ProgramImageInit, SumcheckId::ProgramImageClaimReduction, diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 80f308c245..d17cb31d08 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -2,7 +2,6 @@ use crate::poly::opening_proof::OpeningId; #[cfg(feature = "zk")] use crate::zkvm::stage8_opening_ids; -use crate::zkvm::stage8_program_openings_from_env; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; use std::{ @@ -329,18 +328,6 @@ impl< candidates } - #[inline] - fn include_bytecode_in_stage8(&self) -> bool { - self.preprocessing.is_committed_mode() - && stage8_program_openings_from_env().includes_bytecode() - } - - #[inline] - fn include_program_image_in_stage8(&self) -> bool { - self.preprocessing.is_committed_mode() - && stage8_program_openings_from_env().includes_program_image() - } - fn stage8_opening_point(&self) -> OpeningPoint { let native_main_vars = self.trace.len().log_2() + self.one_hot_params.log_k_chunk; let mut opening_candidates: Vec<(String, OpeningPoint)> = Vec::new(); @@ -356,7 +343,7 @@ impl< { opening_candidates.push(("untrusted_advice".to_string(), point)); } - if self.include_bytecode_in_stage8() { + if self.preprocessing.is_committed_mode() { for chunk_idx in 0..self.preprocessing.shared.bytecode_chunk_count { let (point, _) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::BytecodeChunk(chunk_idx), @@ -365,7 +352,7 @@ impl< opening_candidates.push((format!("bytecode_chunk[{chunk_idx}]"), point)); } } - if self.include_program_image_in_stage8() { + if self.preprocessing.is_committed_mode() { let (program_image_point, _) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::ProgramImageInit, @@ -1329,10 +1316,11 @@ impl< print_current_memory_usage("Stage 6a baseline"); let bytecode_read_raf_params = BytecodeReadRafSumcheckParams::gen( - &self.preprocessing.program, + Some(&self.preprocessing.program), self.trace.len().log_2(), &self.one_hot_params, self.preprocessing.is_committed_mode(), + None, &self.opening_accumulator, &mut self.transcript, ); @@ -1371,25 +1359,6 @@ impl< write_instance_flamegraph_svg(&instances, "stage6a_start_flamechart.svg"); tracing::info!("Stage 6a proving"); - #[cfg(feature = "zk")] - let (sumcheck_proof, _r_stage6a, _initial_claim) = { - // Stage 6a input claims depend on hidden prior-stage outputs in ZK mode, - // so we prove it with a ZK sumcheck proof. We keep a local blindfold - // accumulator so this split-internal phase does not add a new global - // BlindFold stage. - let mut rng = rand::thread_rng(); - let mut local_blindfold = - crate::subprotocols::blindfold::BlindFoldAccumulator::::new(); - BatchedSumcheck::prove_zk::( - instances.iter_mut().map(|v| &mut **v as _).collect(), - &mut self.opening_accumulator, - &mut local_blindfold, - &mut self.transcript, - &self.pedersen_generators, - &mut rng, - ) - }; - #[cfg(not(feature = "zk"))] let (sumcheck_proof, _r_stage6a, _initial_claim) = self.prove_batched_sumcheck(instances.iter_mut().map(|v| &mut **v as _).collect()); @@ -1650,8 +1619,8 @@ impl< let zk_stages = self.blindfold_accumulator.take_stage_data(); assert_eq!( zk_stages.len(), - 7, - "Expected 7 ZK stages, got {}", + 8, + "Expected 8 ZK stages, got {}", zk_stages.len() ); @@ -2180,7 +2149,7 @@ impl< let mut polynomial_claims = Vec::new(); let mut scaling_factors = Vec::new(); - let mut extra_dense_polys: HashMap> = + let mut precommitted_polys: HashMap> = HashMap::new(); let (ram_inc_point, ram_inc_claim) = @@ -2274,7 +2243,7 @@ impl< } } - if self.include_bytecode_in_stage8() { + if self.preprocessing.is_committed_mode() { let chunk_count = self.preprocessing.shared.bytecode_chunk_count; let bytecode_chunks = build_committed_bytecode_chunk_polynomials::( &self.preprocessing.program.bytecode.bytecode, @@ -2293,11 +2262,11 @@ impl< chunk_claim * lagrange_factor, )); scaling_factors.push(lagrange_factor); - extra_dense_polys.insert(CommittedPolynomial::BytecodeChunk(chunk_idx), poly); + precommitted_polys.insert(CommittedPolynomial::BytecodeChunk(chunk_idx), poly); } } - if self.include_program_image_in_stage8() { + if self.preprocessing.is_committed_mode() { let mut program_image_words = self.preprocessing.program.ram.bytecode_words.clone(); if program_image_words.is_empty() { program_image_words.push(0); @@ -2316,7 +2285,7 @@ impl< program_claim * lagrange_factor, )); scaling_factors.push(lagrange_factor); - extra_dense_polys.insert(CommittedPolynomial::ProgramImageInit, program_image_poly); + precommitted_polys.insert(CommittedPolynomial::ProgramImageInit, program_image_poly); } // 2. Sample gamma and compute powers for RLC @@ -2349,7 +2318,6 @@ impl< ProgramMode::Full }, self.preprocessing.shared.bytecode_chunk_count, - stage8_program_openings_from_env(), ); // Build DoryOpeningState @@ -2364,15 +2332,13 @@ impl< memory_layout: self.preprocessing.shared.memory_layout.clone(), }); - // Build precommitted polynomials map for RLC - let mut precommitted_polys = HashMap::new(); + // Add advice polynomials to precommitted polynomials map for RLC. if let Some(poly) = self.advice.trusted_advice_polynomial.take() { precommitted_polys.insert(CommittedPolynomial::TrustedAdvice, poly); } if let Some(poly) = self.advice.untrusted_advice_polynomial.take() { precommitted_polys.insert(CommittedPolynomial::UntrustedAdvice, poly); } - precommitted_polys.extend(extra_dense_polys); // Build streaming RLC polynomial directly (no witness poly regeneration!) // Use materialized trace (default, single pass) instead of lazy trace @@ -3577,7 +3543,6 @@ mod tests { }; use crate::subprotocols::sumcheck::SumcheckInstanceProof; use crate::transcripts::{KeccakTranscript, Transcript}; - use crate::zkvm::verifier::JoltSharedPreprocessing; /// Helper to process a single stage's sumcheck proof. /// Returns a list of (RoundWitness, degree) for each round. /// For ZK proofs, creates synthetic witnesses with correct degrees to test R1CS structure. diff --git a/jolt-core/src/zkvm/ram/mod.rs b/jolt-core/src/zkvm/ram/mod.rs index 4fb7c1ad1f..e5f27860bd 100644 --- a/jolt-core/src/zkvm/ram/mod.rs +++ b/jolt-core/src/zkvm/ram/mod.rs @@ -285,6 +285,7 @@ pub fn verifier_accumulate_advice( /// /// These are scalar inner products: /// - `C_rw = Σ_j ProgramWord[j] * eq(r_address_rw, start_index + j)` +/// /// This is stored as a virtual opening under `SumcheckId::RamValCheck`. pub fn prover_accumulate_program_image( ram_K: usize, diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index 73109fd18e..fa99456d24 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -12,7 +12,6 @@ use crate::poly::commitment::dory::{bind_opening_inputs, DoryGlobals, DoryLayout use crate::poly::commitment::pedersen::PedersenGenerators; #[cfg(feature = "zk")] use crate::poly::lagrange_poly::LagrangeHelper; -use crate::poly::multilinear_polynomial::{MultilinearPolynomial, PolynomialEvaluation}; #[cfg(feature = "zk")] use crate::subprotocols::blindfold::{ pedersen_generator_count_for_r1cs, BakedPublicInputs, BlindFoldVerifier, @@ -27,10 +26,7 @@ use crate::subprotocols::sumcheck_verifier::SumcheckInstanceParams; #[cfg(feature = "zk")] use crate::subprotocols::univariate_skip::UniSkipFirstRoundProofVariant; use crate::zkvm::bytecode::chunks::DEFAULT_COMMITTED_BYTECODE_CHUNK_COUNT; -use crate::zkvm::bytecode::chunks::{ - build_committed_bytecode_chunk_polynomials, committed_lanes, - validate_committed_bytecode_chunk_count, -}; +use crate::zkvm::bytecode::chunks::{committed_lanes, validate_committed_bytecode_chunk_count}; use crate::zkvm::bytecode::TrustedBytecodeCommitments; use crate::zkvm::claim_reductions::RegistersClaimReductionSumcheckVerifier; use crate::zkvm::config::{OneHotParams, ProgramMode}; @@ -81,7 +77,7 @@ use crate::zkvm::{ product::ProductVirtualRemainderVerifier, shift::ShiftSumcheckVerifier, verify_stage1_uni_skip, verify_stage2_uni_skip, }, - stage8_opening_ids, stage8_program_openings_from_env, ProverDebugInfo, + stage8_opening_ids, ProverDebugInfo, }; use crate::{ field::JoltField, @@ -121,6 +117,12 @@ struct StageVerifyResult { challenges: Vec, } +type Stage6aVerifyResult = ( + BytecodeReadRafSumcheckParams, + BooleanitySumcheckParams, + StageVerifyResult, +); + #[cfg(feature = "zk")] impl StageVerifyResult { fn new( @@ -288,18 +290,6 @@ impl< candidates } - #[inline] - fn include_bytecode_in_stage8(&self) -> bool { - self.preprocessing.program.is_committed() - && stage8_program_openings_from_env().includes_bytecode() - } - - #[inline] - fn include_program_image_in_stage8(&self) -> bool { - self.preprocessing.program.is_committed() - && stage8_program_openings_from_env().includes_program_image() - } - fn stage8_opening_point(&self) -> Result, ProofVerifyError> { let native_main_vars = self.proof.trace_length.log_2() + self.one_hot_params.log_k_chunk; let mut opening_candidates: Vec<(&str, OpeningPoint)> = Vec::new(); @@ -315,7 +305,7 @@ impl< { opening_candidates.push(("untrusted_advice", point)); } - if self.include_bytecode_in_stage8() { + if self.preprocessing.program.is_committed() { for chunk_idx in 0..self.preprocessing.shared.bytecode_chunk_count { let (point, _) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::BytecodeChunk(chunk_idx), @@ -324,7 +314,7 @@ impl< opening_candidates.push(("bytecode_chunk", point)); } } - if self.include_program_image_in_stage8() { + if self.preprocessing.program.is_committed() { let (program_image_point, _) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::ProgramImageInit, @@ -560,7 +550,7 @@ impl< let stage5_result = self .verify_stage5() .inspect_err(|e| tracing::error!("Stage 5: {e}"))?; - let stage6_result = self + let (stage6a_result, stage6b_result) = self .verify_stage6() .inspect_err(|e| tracing::error!("Stage 6: {e}"))?; let stage7_result = self @@ -579,7 +569,8 @@ impl< stage3_result.challenges.clone(), stage4_result.challenges.clone(), stage5_result.challenges.clone(), - stage6_result.challenges.clone(), + stage6a_result.challenges.clone(), + stage6b_result.challenges.clone(), stage7_result.challenges.clone(), ]; let uniskip_challenges = [uniskip_challenge1, uniskip_challenge2]; @@ -590,7 +581,8 @@ impl< stage3_result.batched_output_constraint, stage4_result.batched_output_constraint, stage5_result.batched_output_constraint, - stage6_result.batched_output_constraint, + stage6a_result.batched_output_constraint, + stage6b_result.batched_output_constraint, stage7_result.batched_output_constraint, ]; @@ -600,7 +592,8 @@ impl< stage3_result.batched_input_constraint.clone(), stage4_result.batched_input_constraint.clone(), stage5_result.batched_input_constraint.clone(), - stage6_result.batched_input_constraint.clone(), + stage6a_result.batched_input_constraint.clone(), + stage6b_result.batched_input_constraint.clone(), stage7_result.batched_input_constraint.clone(), ]; @@ -614,17 +607,19 @@ impl< stage3_result.input_constraint_challenge_values.clone(), stage4_result.input_constraint_challenge_values.clone(), stage5_result.input_constraint_challenge_values.clone(), - stage6_result.input_constraint_challenge_values.clone(), + stage6a_result.input_constraint_challenge_values.clone(), + stage6b_result.input_constraint_challenge_values.clone(), stage7_result.input_constraint_challenge_values.clone(), ]; - let output_constraint_challenge_values: [Vec; 7] = [ + let output_constraint_challenge_values: [Vec; 8] = [ stage1_result.output_constraint_challenge_values.clone(), stage2_result.output_constraint_challenge_values.clone(), stage3_result.output_constraint_challenge_values.clone(), stage4_result.output_constraint_challenge_values.clone(), stage5_result.output_constraint_challenge_values.clone(), - stage6_result.output_constraint_challenge_values.clone(), + stage6a_result.output_constraint_challenge_values.clone(), + stage6b_result.output_constraint_challenge_values.clone(), stage7_result.output_constraint_challenge_values.clone(), ]; @@ -634,7 +629,8 @@ impl< oc_blocks.extend(stage3_result.oc_block_ids); oc_blocks.extend(stage4_result.oc_block_ids); oc_blocks.extend(stage5_result.oc_block_ids); - oc_blocks.extend(stage6_result.oc_block_ids); + oc_blocks.extend(stage6a_result.oc_block_ids); + oc_blocks.extend(stage6b_result.oc_block_ids); oc_blocks.extend(stage7_result.oc_block_ids); self.verify_blindfold( @@ -1120,26 +1116,22 @@ impl< } #[cfg_attr(not(feature = "zk"), allow(unused_variables))] - fn verify_stage6(&mut self) -> Result, ProofVerifyError> { + fn verify_stage6( + &mut self, + ) -> Result<(StageVerifyResult, StageVerifyResult), ProofVerifyError> { let _ = DoryGlobals::initialize_main_with_log_embedding( self.one_hot_params.k_chunk, self.proof.trace_length, self.main_total_vars(), Some(self.proof.dory_layout), ); - let (bytecode_read_raf_params, booleanity_params) = self.verify_stage6a()?; - self.verify_stage6b(bytecode_read_raf_params, booleanity_params) + let (bytecode_read_raf_params, booleanity_params, stage6a_result) = + self.verify_stage6a()?; + let stage6b_result = self.verify_stage6b(bytecode_read_raf_params, booleanity_params)?; + Ok((stage6a_result, stage6b_result)) } - fn verify_stage6a( - &mut self, - ) -> Result< - ( - BytecodeReadRafSumcheckParams, - BooleanitySumcheckParams, - ), - ProofVerifyError, - > { + fn verify_stage6a(&mut self) -> Result, ProofVerifyError> { let n_cycle_vars = self.proof.trace_length.log_2(); let program_preprocessing = self.preprocessing.program.full().map(|p| p.as_ref()); let entry_bytecode_index = self @@ -1172,24 +1164,59 @@ impl< )); let instances: Vec<&dyn SumcheckInstanceVerifier> = vec![&bytecode_read_raf, &booleanity]; - BatchedSumcheck::verify( + let (_batching_coefficients, r_stage6a) = BatchedSumcheck::verify( &self.proof.stage6a_sumcheck_proof, - instances, + instances.clone(), &mut self.opening_accumulator, &mut self.transcript, ) - .map_err(|err| { - tracing::error!("Stage 6a: Sumcheck verification failed"); - err - })?; + .inspect_err(|err| tracing::error!("Stage 6a: {err}"))?; #[cfg(feature = "zk")] { - // Stage 6a is proven in clear and excluded from BlindFold stage data. - // Drop any pending OC IDs so Stage 6b OC blocks stay aligned. - let _ = self.opening_accumulator.take_pending_claim_ids(); + let regular_oc_ids = self.opening_accumulator.take_pending_claim_ids(); + let batched_output_constraint = batch_output_constraints(&instances); + let batched_input_constraint = batch_input_constraints(&instances); + let max_num_rounds = instances.iter().map(|i| i.num_rounds()).max().unwrap(); + let mut output_constraint_challenge_values: Vec = _batching_coefficients.clone(); + let mut input_constraint_challenge_values: Vec = + scale_batching_coefficients(&_batching_coefficients, &instances); + for instance in &instances { + let num_rounds = instance.num_rounds(); + let offset = instance.round_offset(max_num_rounds); + let r_slice = &r_stage6a[offset..offset + num_rounds]; + output_constraint_challenge_values.extend( + instance + .get_params() + .output_constraint_challenge_values(r_slice), + ); + input_constraint_challenge_values.extend( + instance + .get_params() + .input_constraint_challenge_values(&self.opening_accumulator), + ); + } + let stage_result = StageVerifyResult::new( + r_stage6a, + batched_output_constraint, + output_constraint_challenge_values, + batched_input_constraint, + input_constraint_challenge_values, + vec![regular_oc_ids], + ); + Ok(( + bytecode_read_raf.into_params(), + booleanity.into_params(), + stage_result, + )) } - - Ok((bytecode_read_raf.into_params(), booleanity.into_params())) + #[cfg(not(feature = "zk"))] + Ok(( + bytecode_read_raf.into_params(), + booleanity.into_params(), + StageVerifyResult { + challenges: r_stage6a, + }, + )) } #[cfg_attr(not(feature = "zk"), allow(unused_variables))] @@ -1313,10 +1340,7 @@ impl< &mut self.opening_accumulator, &mut self.transcript, ) - .map_err(|err| { - tracing::error!("Stage 6b: Sumcheck verification failed"); - err - })?; + .inspect_err(|err| tracing::error!("Stage 6b: {err}"))?; #[cfg(feature = "zk")] { @@ -1361,12 +1385,12 @@ impl< #[allow(clippy::too_many_arguments)] fn verify_blindfold( &mut self, - sumcheck_challenges: &[Vec; 7], + sumcheck_challenges: &[Vec; 8], uniskip_challenges: [F::Challenge; 2], - stage_output_constraints: &[Option; 7], - output_constraint_challenge_values: &[Vec; 7], - stage_input_constraints: &[InputClaimConstraint; 7], - input_constraint_challenge_values: &[Vec; 7], + stage_output_constraints: &[Option; 8], + output_constraint_challenge_values: &[Vec; 8], + stage_input_constraints: &[InputClaimConstraint; 8], + input_constraint_challenge_values: &[Vec; 8], // For stages 0-1: batched input constraint for regular rounds (different from uni-skip) stage1_batched_input: &InputClaimConstraint, stage2_batched_input: &InputClaimConstraint, @@ -1383,6 +1407,7 @@ impl< &self.proof.stage3_sumcheck_proof, &self.proof.stage4_sumcheck_proof, &self.proof.stage5_sumcheck_proof, + &self.proof.stage6a_sumcheck_proof, &self.proof.stage6b_sumcheck_proof, &self.proof.stage7_sumcheck_proof, ]; @@ -1400,7 +1425,7 @@ impl< let mut stage_configs = Vec::new(); // Track which stage_config index corresponds to uni-skip and regular first rounds let mut uniskip_indices: Vec = Vec::new(); // Only 2 elements for stages 0-1 - let mut regular_first_round_indices: Vec = Vec::new(); // 7 elements for all stages + let mut regular_first_round_indices: Vec = Vec::new(); // 8 elements for all stages let mut last_round_indices: Vec = Vec::new(); for (stage_idx, proof) in stage_proofs.iter().enumerate() { @@ -1482,7 +1507,7 @@ impl< Some(ClaimBindingConfig::with_constraint(constraint.clone())); } - // Add initial_input configurations for regular first rounds (all 7 stages) + // Add initial_input configurations for regular first rounds (all 8 stages) // These use the batched input constraints from the stage results let regular_constraints = [ stage1_batched_input.clone(), // Stage 0 regular @@ -1490,8 +1515,9 @@ impl< stage_input_constraints[2].clone(), // Stage 2 stage_input_constraints[3].clone(), // Stage 3 stage_input_constraints[4].clone(), // Stage 4 - stage_input_constraints[5].clone(), // Stage 5 - stage_input_constraints[6].clone(), // Stage 6 + stage_input_constraints[5].clone(), // Stage 5 (6a) + stage_input_constraints[6].clone(), // Stage 6 (6b) + stage_input_constraints[7].clone(), // Stage 7 ]; for (i, constraint) in regular_constraints.iter().enumerate() { let idx = regular_first_round_indices[i]; @@ -1519,7 +1545,7 @@ impl< } } - let all_input_challenge_values: [&[F]; 9] = [ + let all_input_challenge_values: [&[F]; 10] = [ &input_constraint_challenge_values[0], stage1_batched_input_values, &input_constraint_challenge_values[1], @@ -1529,6 +1555,7 @@ impl< &input_constraint_challenge_values[4], &input_constraint_challenge_values[5], &input_constraint_challenge_values[6], + &input_constraint_challenge_values[7], ]; let mut baked_input_challenges: Vec = Vec::new(); for expected_values in all_input_challenge_values.iter() { @@ -1708,64 +1735,7 @@ impl< }) } - fn verify_omitted_program_openings(&self) -> Result<(), ProofVerifyError> { - if !self.preprocessing.program.is_committed() { - return Ok(()); - } - - if !self.include_bytecode_in_stage8() { - let program = self.preprocessing.program.as_full()?; - let bytecode_chunk_polys = build_committed_bytecode_chunk_polynomials::( - &program.bytecode.bytecode, - self.preprocessing.shared.bytecode_chunk_count, - ); - for (chunk_idx, poly) in bytecode_chunk_polys.into_iter().enumerate() { - let (point, claim) = self.opening_accumulator.get_committed_polynomial_opening( - CommittedPolynomial::BytecodeChunk(chunk_idx), - SumcheckId::BytecodeClaimReduction, - ); - let eval = poly.evaluate(&point.r); - if eval != claim { - return Err(ProofVerifyError::DoryError(format!( - "omitted bytecode chunk opening mismatch for chunk {chunk_idx}" - ))); - } - } - } - - if !self.include_program_image_in_stage8() { - let mut program_image_words = self - .preprocessing - .program - .as_full()? - .ram - .bytecode_words - .clone(); - if program_image_words.is_empty() { - program_image_words.push(0); - } - let padded_len = program_image_words.len().next_power_of_two().max(2); - program_image_words.resize(padded_len, 0); - let program_image_poly = MultilinearPolynomial::from(program_image_words); - let (point, claim) = self.opening_accumulator.get_committed_polynomial_opening( - CommittedPolynomial::ProgramImageInit, - SumcheckId::ProgramImageClaimReduction, - ); - let eval = program_image_poly.evaluate(&point.r); - if eval != claim { - return Err(ProofVerifyError::DoryError( - "omitted program image opening mismatch".to_string(), - )); - } - } - - Ok(()) - } - - /// Stage 8: Dory batch opening verification. fn verify_stage8(&mut self) -> Result, ProofVerifyError> { - self.verify_omitted_program_openings()?; - let opening_point = self.stage8_opening_point()?; // 1. Collect all (polynomial, claim) pairs @@ -1854,7 +1824,7 @@ impl< include_untrusted_advice = true; } - if self.include_bytecode_in_stage8() { + if self.preprocessing.program.is_committed() { let chunk_count = self.preprocessing.shared.bytecode_chunk_count; for chunk_idx in 0..chunk_count { let (chunk_point, chunk_claim) = @@ -1871,7 +1841,7 @@ impl< scaling_factors.push(lagrange_factor); } } - if self.include_program_image_in_stage8() { + if self.preprocessing.program.is_committed() { let (program_point, program_claim) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::ProgramImageInit, @@ -1910,7 +1880,6 @@ impl< ProgramMode::Full }, self.preprocessing.shared.bytecode_chunk_count, - stage8_program_openings_from_env(), ); let joint_claim: F = gamma_powers .iter() @@ -1974,7 +1943,7 @@ impl< } } } - if let Some(trusted_program) = self.preprocessing.program.as_committed().ok() { + if let Ok(trusted_program) = self.preprocessing.program.as_committed() { if state .polynomial_claims .iter() diff --git a/src/build_wasm.rs b/src/build_wasm.rs index 9dc0878b71..1926301cf5 100644 --- a/src/build_wasm.rs +++ b/src/build_wasm.rs @@ -5,6 +5,7 @@ use std::{ fs::{self, File}, io::Write, path::Path, + sync::Arc, }; use ark_bn254::Fr; @@ -14,6 +15,7 @@ use jolt_core::{ host::Program, poly::commitment::dory::DoryCommitmentScheme, zkvm::{ + program::ProgramPreprocessing, prover::JoltProverPreprocessing, verifier::{JoltSharedPreprocessing, JoltVerifierPreprocessing}, Serializable, @@ -28,16 +30,16 @@ struct FunctionAttributes { } fn preprocess_and_save(func_name: &str, attributes: &Attributes, is_std: bool) -> Result<()> { - let mut program = Program::new("guest"); + let mut host_program = Program::new("guest"); - program.set_func(func_name); - program.set_std(is_std); - program.set_heap_size(attributes.heap_size); - program.set_stack_size(attributes.stack_size); - program.set_max_input_size(attributes.max_input_size); - program.set_max_output_size(attributes.max_output_size); + host_program.set_func(func_name); + host_program.set_std(is_std); + host_program.set_heap_size(attributes.heap_size); + host_program.set_stack_size(attributes.stack_size); + host_program.set_max_input_size(attributes.max_input_size); + host_program.set_max_output_size(attributes.max_output_size); - let (bytecode, memory_init, program_size, e_entry) = program.decode(); + let (bytecode, memory_init, program_size, _e_entry) = host_program.decode(); let memory_config = MemoryConfig { max_input_size: attributes.max_input_size, @@ -50,16 +52,17 @@ fn preprocess_and_save(func_name: &str, attributes: &Attributes, is_std: bool) - }; let memory_layout = MemoryLayout::new(&memory_config); + let preprocessed_program = Arc::new(ProgramPreprocessing::preprocess(bytecode, memory_init)); let shared = JoltSharedPreprocessing::new( - bytecode, + preprocessed_program.meta(), memory_layout, - memory_init, attributes.max_trace_length as usize, - e_entry, ); - let prover_preprocessing = - JoltProverPreprocessing::::new(shared); + let prover_preprocessing = JoltProverPreprocessing::::new( + shared, + preprocessed_program, + ); let verifier_preprocessing = JoltVerifierPreprocessing::from(&prover_preprocessing); let verifier_bytes = verifier_preprocessing.serialize_to_bytes()?; @@ -71,7 +74,7 @@ fn preprocess_and_save(func_name: &str, attributes: &Attributes, is_std: bool) - let mut file = File::create(verifier_path)?; file.write_all(&verifier_bytes)?; - let elf_bytes = program + let elf_bytes = host_program .get_elf_contents() .expect("ELF not found after decode"); let elf_path = target_dir.join(format!("{func_name}.elf")); From 46e367ebdbcfd20bddb1029e6b3d944168a3403a Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Tue, 17 Mar 2026 17:34:05 -0700 Subject: [PATCH 07/20] refactor(zkvm): share total-var sizing logic Move total-variable sizing into `JoltSharedPreprocessing` so prover and verifier use the same precommitted candidate and max-var calculations. --- jolt-core/src/zkvm/prover.rs | 91 ++++-------------------- jolt-core/src/zkvm/verifier.rs | 124 ++++++++++++++++++++++++--------- 2 files changed, 106 insertions(+), 109 deletions(-) diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index d17cb31d08..f1384f3fe6 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -294,38 +294,14 @@ impl< fn main_total_vars(&self) -> usize { let trace_log_t = self.trace.len().log_2(); let log_k_chunk = self.one_hot_params.log_k_chunk; - let mut max_total_vars = trace_log_t + log_k_chunk; - for total_vars in self.precommitted_candidate_total_vars() { - max_total_vars = max_total_vars.max(total_vars); - } - max_total_vars - } - - #[inline] - fn precommitted_candidate_total_vars(&self) -> Vec { - let mut candidates = Vec::new(); - if self.preprocessing.is_committed_mode() { - let bytecode_t_full = self.preprocessing.shared.bytecode_size().log_2(); - let chunk_log = self.preprocessing.shared.bytecode_chunk_count.log_2(); - let chunk_cycle_log_t = bytecode_t_full.saturating_sub(chunk_log); - candidates.push(committed_lanes().log_2() + chunk_cycle_log_t); - let program_image_words = self.preprocessing.shared.program_image_len_words().max(1); - candidates.push(program_image_words.next_power_of_two().log_2()); - } - if !self.program_io.trusted_advice.is_empty() { - let (sigma, nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( - self.program_io.memory_layout.max_trusted_advice_size as usize, - ); - candidates.push(sigma + nu); - } - - if !self.program_io.untrusted_advice.is_empty() { - let (sigma, nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( - self.program_io.memory_layout.max_untrusted_advice_size as usize, - ); - candidates.push(sigma + nu); - } - candidates + JoltSharedPreprocessing::max_total_vars_from_candidates( + trace_log_t + log_k_chunk, + self.preprocessing.shared.precommitted_candidate_total_vars( + self.preprocessing.is_committed_mode(), + !self.program_io.trusted_advice.is_empty(), + !self.program_io.untrusted_advice.is_empty(), + ), + ) } fn stage8_opening_point(&self) -> OpeningPoint { @@ -1400,7 +1376,11 @@ impl< ); let main_total_vars = self.trace.len().log_2() + self.one_hot_params.log_k_chunk; - let precommitted_candidates = self.precommitted_candidate_total_vars(); + let precommitted_candidates = self.preprocessing.shared.precommitted_candidate_total_vars( + self.preprocessing.is_committed_mode(), + self.advice.trusted_advice_polynomial.is_some(), + self.advice.untrusted_advice_polynomial.is_some(), + ); let precommitted_scheduling_reference = PrecommittedClaimReduction::::scheduling_reference( main_total_vars, @@ -2542,23 +2522,7 @@ where { #[tracing::instrument(skip_all, name = "JoltProverPreprocessing::new")] pub fn new(shared: JoltSharedPreprocessing, program: Arc) -> Self { - use common::constants::ONEHOT_CHUNK_THRESHOLD_LOG_T; - let max_T: usize = shared.max_padded_trace_length.next_power_of_two(); - let max_log_T = max_T.log_2(); - let max_log_k_chunk = if max_log_T < ONEHOT_CHUNK_THRESHOLD_LOG_T { - 4 - } else { - 8 - }; - let mut max_total_vars = max_log_k_chunk + max_log_T; - let (trusted_sigma, trusted_nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( - shared.memory_layout.max_trusted_advice_size as usize, - ); - max_total_vars = max_total_vars.max(trusted_sigma + trusted_nu); - let (untrusted_sigma, untrusted_nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( - shared.memory_layout.max_untrusted_advice_size as usize, - ); - max_total_vars = max_total_vars.max(untrusted_sigma + untrusted_nu); + let (max_total_vars, _) = shared.compute_max_total_vars(); let generators = PCS::setup_prover(max_total_vars); JoltProverPreprocessing { @@ -2578,32 +2542,7 @@ where shared: JoltSharedPreprocessing, program: Arc, ) -> Self { - use common::constants::ONEHOT_CHUNK_THRESHOLD_LOG_T; - let max_t_any = shared - .max_padded_trace_length - .max(shared.bytecode_size()) - .max(program.program_image_len_words_padded()) - .next_power_of_two(); - let max_log_t = max_t_any.log_2(); - let max_log_k_chunk = if max_log_t < ONEHOT_CHUNK_THRESHOLD_LOG_T { - 4 - } else { - 8 - }; - let mut max_total_vars = max_log_k_chunk + max_log_t; - let (trusted_sigma, trusted_nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( - shared.memory_layout.max_trusted_advice_size as usize, - ); - max_total_vars = max_total_vars.max(trusted_sigma + trusted_nu); - let (untrusted_sigma, untrusted_nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( - shared.memory_layout.max_untrusted_advice_size as usize, - ); - max_total_vars = max_total_vars.max(untrusted_sigma + untrusted_nu); - let chunk_cycle_log_t = (shared.bytecode_size() / shared.bytecode_chunk_count) - .next_power_of_two() - .log_2(); - max_total_vars = max_total_vars.max(committed_lanes().log_2() + chunk_cycle_log_t); - max_total_vars = max_total_vars.max(program.program_image_len_words_padded().log_2()); + let (max_total_vars, max_log_k_chunk) = shared.compute_max_total_vars(); let generators = PCS::setup_prover(max_total_vars); let (bytecode_commitments, bytecode_hints) = TrustedBytecodeCommitments::derive( &program.bytecode, diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index fa99456d24..80e3d33f55 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -256,38 +256,14 @@ impl< fn main_total_vars(&self) -> usize { let trace_log_t = self.proof.trace_length.log_2(); let log_k_chunk = self.one_hot_params.log_k_chunk; - let mut max_total_vars = trace_log_t + log_k_chunk; - for total_vars in self.precommitted_candidate_total_vars() { - max_total_vars = max_total_vars.max(total_vars); - } - max_total_vars - } - - #[inline] - fn precommitted_candidate_total_vars(&self) -> Vec { - let mut candidates = Vec::new(); - if self.preprocessing.program.is_committed() { - let bytecode_t_full = self.preprocessing.shared.bytecode_size().log_2(); - let chunk_log = self.preprocessing.shared.bytecode_chunk_count.log_2(); - let chunk_cycle_log_t = bytecode_t_full.saturating_sub(chunk_log); - candidates.push(committed_lanes().log_2() + chunk_cycle_log_t); - let program_image_words = self.preprocessing.shared.program_image_len_words().max(1); - candidates.push(program_image_words.next_power_of_two().log_2()); - } - if self.trusted_advice_commitment.is_some() { - let (sigma, nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( - self.program_io.memory_layout.max_trusted_advice_size as usize, - ); - candidates.push(sigma + nu); - } - - if self.proof.untrusted_advice_commitment.is_some() { - let (sigma, nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( - self.program_io.memory_layout.max_untrusted_advice_size as usize, - ); - candidates.push(sigma + nu); - } - candidates + JoltSharedPreprocessing::max_total_vars_from_candidates( + trace_log_t + log_k_chunk, + self.preprocessing.shared.precommitted_candidate_total_vars( + self.preprocessing.program.is_committed(), + self.trusted_advice_commitment.is_some(), + self.proof.untrusted_advice_commitment.is_some(), + ), + ) } fn stage8_opening_point(&self) -> Result, ProofVerifyError> { @@ -1252,7 +1228,11 @@ impl< ); let main_total_vars = self.proof.trace_length.log_2() + self.one_hot_params.log_k_chunk; - let precommitted_candidates = self.precommitted_candidate_total_vars(); + let precommitted_candidates = self.preprocessing.shared.precommitted_candidate_total_vars( + self.preprocessing.program.is_committed(), + self.trusted_advice_commitment.is_some(), + self.proof.untrusted_advice_commitment.is_some(), + ); let precommitted_scheduling_reference = PrecommittedClaimReduction::::scheduling_reference( main_total_vars, @@ -2139,6 +2119,84 @@ impl JoltSharedPreprocessing { pub fn program_image_len_words(&self) -> usize { self.program_meta.program_image_len_words } + + #[inline] + pub(crate) fn precommitted_candidate_total_vars( + &self, + include_committed: bool, + include_trusted_advice: bool, + include_untrusted_advice: bool, + ) -> Vec { + let mut candidates = Vec::with_capacity( + include_committed as usize * 2 + + include_trusted_advice as usize + + include_untrusted_advice as usize, + ); + + if include_trusted_advice { + let (trusted_sigma, trusted_nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( + self.memory_layout.max_trusted_advice_size as usize, + ); + candidates.push(trusted_sigma + trusted_nu); + } + + if include_untrusted_advice { + let (untrusted_sigma, untrusted_nu) = DoryGlobals::advice_sigma_nu_from_max_bytes( + self.memory_layout.max_untrusted_advice_size as usize, + ); + candidates.push(untrusted_sigma + untrusted_nu); + } + + if include_committed { + let chunk_cycle_log_t = (self.bytecode_size() / self.bytecode_chunk_count) + .next_power_of_two() + .log_2(); + candidates.push(committed_lanes().log_2() + chunk_cycle_log_t); + candidates.push( + self.program_image_len_words() + .max(1) + .next_power_of_two() + .log_2(), + ); + } + + candidates + } + + #[inline] + pub(crate) fn max_total_vars_from_candidates( + main_total_vars: usize, + candidates: impl IntoIterator, + ) -> usize { + let mut max_total_vars = main_total_vars; + for total_vars in candidates { + max_total_vars = max_total_vars.max(total_vars); + } + max_total_vars + } + + #[inline] + pub(crate) fn compute_max_total_vars(&self) -> (usize, usize) { + use common::constants::ONEHOT_CHUNK_THRESHOLD_LOG_T; + let max_t_any = self + .max_padded_trace_length + .max(self.bytecode_size()) + .max(self.program_image_len_words().max(1).next_power_of_two()) + .next_power_of_two(); + let max_log_t = max_t_any.log_2(); + let max_log_k_chunk = if max_log_t < ONEHOT_CHUNK_THRESHOLD_LOG_T { + 4 + } else { + 8 + }; + + let max_total_vars = Self::max_total_vars_from_candidates( + max_log_k_chunk + max_log_t, + self.precommitted_candidate_total_vars(true, true, true), + ); + + (max_total_vars, max_log_k_chunk) + } } /// Serializable wrapper around [`PedersenGenerators`] for ZK setup transfer. From 18cf9ddee4ad7e4e9b2e250aa432b1ddf1a677ee Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Tue, 17 Mar 2026 17:47:17 -0700 Subject: [PATCH 08/20] fix(zkvm): address clippy warning in prover --- jolt-core/src/zkvm/prover.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index f1384f3fe6..333069faf3 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -61,9 +61,8 @@ use crate::{ utils::{math::Math, thread::drop_in_background_thread}, zkvm::{ bytecode::{ - chunks::{build_committed_bytecode_chunk_polynomials, committed_lanes}, - read_raf_checking::BytecodeReadRafSumcheckParams, - TrustedBytecodeCommitments, + chunks::build_committed_bytecode_chunk_polynomials, + read_raf_checking::BytecodeReadRafSumcheckParams, TrustedBytecodeCommitments, }, claim_reductions::{ AdviceClaimReductionParams, AdviceClaimReductionProver, AdviceKind, From 77db90a57b693325840f3e3f72694f423a1b9586 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 18 Mar 2026 15:39:58 -0700 Subject: [PATCH 09/20] docs(zkvm): explain precommitted Dory embedding in Stage 8 Document how precommitted polynomials fit into the shared Dory geometry, how the final opening point is derived across layouts, and why the precommitted variable permutation is needed to preserve low-to-high binding. --- book/src/how/architecture/opening-proof.md | 358 +++++++++++++++++++++ 1 file changed, 358 insertions(+) diff --git a/book/src/how/architecture/opening-proof.md b/book/src/how/architecture/opening-proof.md index 59147c8a9f..7eec23e41c 100644 --- a/book/src/how/architecture/opening-proof.md +++ b/book/src/how/architecture/opening-proof.md @@ -23,6 +23,8 @@ The claim reduction sumchecks can be found in `jolt-core/src/zkvm/claim_reductio - **Increments** (`increments.rs`): Reduces claims related to increment checks. - **Hamming weight** (`hamming_weight.rs`): Reduces hamming weight-related claims. - **Advice** (`advice.rs`): Reduces claims from advice polynomials. +- **Bytecode** (`bytecode.rs`): Reduces committed bytecode openings into the shared Stage 8 Dory geometry. +- **Program image** (`program_image.rs`): Reduces the committed initial-memory image into the same final opening geometry. ### How claim reduction sumchecks work @@ -43,6 +45,362 @@ We apply the [Multiple polynomials, same point](../optimizations/batched-opening On the verifier side, this entails taking a linear combination of commitments. Since Dory is an additively homomorphic commitment scheme, the verifier is able to do so. +### Precommitted geometry and Dory embedding + +Some committed polynomials in Stage 8 do not naturally live in the "main" Dory geometry induced by the trace-domain witness polynomials. Examples include the bytecode chunks, the program image, and trusted or untrusted advice. In the implementation these are called **precommitted** polynomials. + +The goal of Stage 8 is still the same: every committed polynomial must be opened at one common Dory point so that a single random linear combination can be opened. The subtlety is that these precommitted polynomials may have a different number of variables from the main trace-domain polynomials. + +In this section we write: + +- $T$ for the **log** trace length +- $K$ for the **log** main address space size +- $B$ for the number of **extra** variables contributed by the largest precommitted polynomial beyond the main geometry + +With that notation, the final Dory opening point has length + +$$ +D = T + K + B. +$$ + +Equivalently, Stage 8 works in a joint Dory matrix of size $2^{\nu_D} \times 2^{\sigma_D}$ where + +$$ +\sigma_D = \left\lceil \frac{D}{2} \right\rceil, \qquad \nu_D = D - \sigma_D. +$$ + +Here $\nu_D$ is the number of **row variables** and $\sigma_D$ is the number of **column variables**. This matches the implementation in `DoryGlobals::balanced_sigma_nu()` and the split used by `PrecommittedClaimReduction::project_dory_round_permutation_for_poly()`. + + +Write: + +- the main geometry size as $T + K$ +- the joint geometry size as $D = T + K + B$ +- the joint Dory matrix as $2^{\nu_D} \times 2^{\sigma_D}$ with $\nu_D + \sigma_D = D$ + +The main design constraint is that we do not want to complicate the existing main sumchecks round scheduling. So Jolt does the following: + +- precommitted reductions are forward-loaded +- main reductions are backward-loaded +- Stage 6b always has exactly $T + B$ rounds +- Stage 7 always has exactly $K$ rounds + +This way: + +- the precommitted reductions see the full challenge set needed for the joint geometry +- the main sumchecks keep their old round scheduling +- Stage 8 only has to normalize already-produced opening points into the final Dory point + +If some precommitted polynomial already has $D$ variables, we call it a **dominant precommitted polynomial**. Otherwise there is **no dominant precommitted polynomial**, and the joint point is anchored by the ordinary main openings. + +#### How Main Polynomials Sit In The Joint Matrix + +The main polynomials are embedded the way they are because this lets Stage 8 reuse the old sumcheck outputs unchanged. + +As a concrete example, take $D = 5$. Since Dory uses a balanced split, this means: + +$$ +\sigma_D = 3, \qquad \nu_D = 2, +$$ + +so the joint matrix has $2^2 = 4$ rows and $2^3 = 8$ columns, for a total of $2^5 = 32$ slots. + +##### `CycleMajor` dense placement + +Take a dense polynomial with $T = 3$ variables and coefficients + +$$ +a_{000}, a_{001}, a_{010}, a_{011}, a_{100}, a_{101}, a_{110}, a_{111}. +$$ + +In `CycleMajor`, the dense polynomial is written across the top of the matrix, so only the lowest $T$ index bits vary: + +```text +Joint 4 x 8 matrix + + col000 col001 col010 col011 col100 col101 col110 col111 +row00 | a_000 | a_001 | a_010 | a_011 | a_100 | a_101 | a_110 | a_111 | +row01 | . | . | . | . | . | . | . | . | +row10 | . | . | . | . | . | . | . | . | +row11 | . | . | . | . | . | . | . | . | +``` + +so the first $2$ bits are fixed and only the last $3$ bits vary. + +##### `AddressMajor` dense placement + +Now take the same joint geometry $D = 5$, but the dense polynomial should now use the highest $T = 3$ bits. Then its coefficients are written into slots whose last $K+B = 2$ bits are zero: + +In the same $4 \times 8$ matrix this looks like: + +```text +Joint 4 x 8 matrix + + col000 col001 col010 col011 col100 col101 col110 col111 +row00 | a_000 | . | . | . | a_001 | . | . | . | +row01 | a_010 | . | . | . | a_011 | . | . | . | +row10 | a_100 | . | . | . | a_101 | . | . | . | +row11 | a_110 | . | . | . | a_111 | . | . | . | +``` + +This is exactly what "the dense polynomial gets the highest $T$ bits" means in practice: the coefficient slots are those where the trailing $K+B$ bits are zero. + +The same idea applies to one-hot polynomials: + +- in `CycleMajor`, they use the lowest $T+K$ bits +- in `AddressMajor`, they use the highest $T+K$ bits, so the trailing $B$ bits are zero + +Therefore, the extra $B$ variables must end up on opposite sides of the final Dory opening point in the two layouts. + +#### When Address-Major Dense Stride Exceeds The Row Width + +In `AddressMajor`, dense polynomials are embedded with stride $2^{K+B}$. Sometimes that stride is larger than the number of columns of the joint matrix. This is the special branch handled in `dory/wrappers.rs`. + +Take a real example: + +- joint geometry $D = 7$, so the balanced Dory matrix is $2^3 \times 2^4 = 8 \times 16$ +- dense polynomial has $T = 2$ variables, so it has 4 coefficients +- therefore $K+B = 5$, so the stride is $2^5 = 32$ + +Since the row width is only 16, consecutive coefficients jump by two whole rows: + +```text +coeff a_00 -> slot 0 -> row 0, col 0 +coeff a_01 -> slot 32 -> row 2, col 0 +coeff a_10 -> slot 64 -> row 4, col 0 +coeff a_11 -> slot 96 -> row 6, col 0 +``` + +and the matrix picture is: + +```text +8 x 16 joint matrix + +row0 | a_00 . . . . . . . . . . . . . . . | +row1 | . . . . . . . . . . . . . . . . | +row2 | a_01 . . . . . . . . . . . . . . . | +row3 | . . . . . . . . . . . . . . . . | +row4 | a_10 . . . . . . . . . . . . . . . | +row5 | . . . . . . . . . . . . . . . . | +row6 | a_11 . . . . . . . . . . . . . . . | +row7 | . . . . . . . . . . . . . . . . | +``` + +So the logical embedding is unchanged, but it is no longer a convenient row-local chunking. That is why the implementation switches to explicit sparse row/column placement in this case. + +#### Final Dory Opening Point + +In summary +- in `CycleMajor`, the main dense / one-hot geometry consumes the low bits of the final Dory point, so any extra precommitted variables must sit on the high side +- in `AddressMajor`, the main geometry consumes the high bits, so any extra precommitted variables must sit on the low side +- each block appears in reverse because Dory opening points are written in big-endian order, while the sumcheck challenges are accumulated round-by-round and then normalized into that order + +Now we study two cases: +If there **is** a dominant precommitted polynomial, let the raw Stage 6b challenges be + +$$ +[x_1, x_2, \dots, x_B, x_{B+1}, \dots, x_{B+T}] +$$ + +and the raw Stage 7 challenges be + +$$ +[y_1, y_2, \dots, y_K]. +$$ + +The final big-endian Dory opening point is obtained by normalizing these challenges into Dory order. + +For **AddressMajor**: + +$$ +[x_{B+T}, x_{B+T-1}, \dots, x_{B+1} \;\Vert\; y_K, y_{K-1}, \dots, y_1 \;\Vert\; x_B, x_{B-1}, \dots, x_1] +$$ + +For **CycleMajor**: + +$$ +[x_B, x_{B-1}, \dots, x_1 \;\Vert\; y_K, y_{K-1}, \dots, y_1 \;\Vert\; x_{B+T}, x_{B+T-1}, \dots, x_{B+1}] +$$ + +Each block is reversed, and the extra $B$ variables move to different sides depending on the layout. + +If there is **no dominant precommitted polynomial**, then the final point is anchored by the ordinary main openings: + +- in this case the joint geometry is just the main geometry, so $B = 0$ +- let $r_{\mathrm{inc}}$ be the Stage 6b opening point from `IncClaimReduction` +- let $r_{\mathrm{ham}}$ be the Stage 7 opening point from `HammingWeightClaimReduction` + +These are already normalized opening points. + +Then: + +For **AddressMajor**: + +$$ +r_{\mathrm{final}} = +\big[ +r_{\mathrm{inc}} +\;\Vert\; +r_{\mathrm{ham}} +\big] +$$ + +For **CycleMajor**: + +$$ +r_{\mathrm{final}} = +\big[ +r_{\mathrm{ham}} +\big]. +$$ + +This is exactly the logic implemented in `stage8_opening_point()` in `prover.rs`. + +#### Embedding Smaller Precommitted Polynomials + +Now suppose there is **no dominant precommitted polynomial**, and we want to batch a smaller precommitted polynomial with the rest of the Stage 8 openings. The verifier's commitment to a precommitted polynomial already treats that polynomial as occupying the first rows and first columns of its balanced Dory matrix. Therefore, when we place that polynomial inside the larger joint matrix, we must continue to place it in the top-left corner; otherwise the verifier would be checking the Dory proof against a different geometry than the one used by the commitment. + +This is why smaller precommitted polynomials are placed in the **top-left corner** of the joint Dory matrix: + +```text +Joint Dory matrix: 2^nu_D rows x 2^sigma_D columns +Smaller precommitted matrix: 2^nu_C rows x 2^sigma_C columns + + left 2^sigma_C cols remaining cols + +---------------------------+------------------+ +top 2^nu_C rows | smaller precommitted poly | not used by this | + | lives here | poly | + +---------------------------+------------------+ +remaining rows | not used by this poly | not used by this | + | | poly | + +---------------------------+------------------+ +``` + +Suppose the smaller precommitted polynomial has + +$$ +C = \nu_C + \sigma_C +$$ + +variables, while the joint point has + +$$ +D = \nu_D + \sigma_D. +$$ + +Split the joint point as + +$$ +r_{\mathrm{joint}} = +\big[ +r_{\mathrm{row}}^{\mathrm{hi}} +\;\Vert\; +r_{\mathrm{row}}^{\mathrm{lo}} +\;\Vert\; +r_{\mathrm{col}}^{\mathrm{hi}} +\;\Vert\; +r_{\mathrm{col}}^{\mathrm{lo}} +\big] +$$ + +where: + +- $r_{\mathrm{row}}^{\mathrm{hi}}$ has length $\nu_D - \nu_C$ +- $r_{\mathrm{row}}^{\mathrm{lo}}$ has length $\nu_C$ +- $r_{\mathrm{col}}^{\mathrm{hi}}$ has length $\sigma_D - \sigma_C$ +- $r_{\mathrm{col}}^{\mathrm{lo}}$ has length $\sigma_C$ + +Then the smaller polynomial is evaluated on + +$$ +r_{\mathrm{small}} = +\big[ +r_{\mathrm{row}}^{\mathrm{lo}} +\;\Vert\; +r_{\mathrm{col}}^{\mathrm{lo}} +\big]. +$$ + +The reason is that top-left embedding forces the missing high row bits and high column bits to be zero: + +```text +joint row variables : [row_hi | row_lo] +joint col variables : [col_hi | col_lo] + +top-left embedding forces: + row_hi = 0 + col_hi = 0 +``` + +So if $P$ is the smaller polynomial and $P_{\mathrm{emb}}$ is its embedding into the joint matrix, then + +$$ +P_{\mathrm{emb}}(r_{\mathrm{joint}}) += +\operatorname{eq}\!\left(r_{\mathrm{row}}^{\mathrm{hi}}, 0^{\nu_D - \nu_C}\right) +\cdot +\operatorname{eq}\!\left(r_{\mathrm{col}}^{\mathrm{hi}}, 0^{\sigma_D - \sigma_C}\right) +\cdot +P(r_{\mathrm{small}}). +$$ + +This selector is exactly why top-left embedding works inside one shared Dory proof. + +The same selector appears when a joint `RLCPolynomial` mixes a main polynomial with a smaller precommitted polynomial: + +$$ +\text{RLC coefficient} +\cdot +P(r_{\mathrm{small}}) +\cdot +\operatorname{eq}\!\left(r_{\mathrm{row}}^{\mathrm{hi}}, 0^{\nu_D - \nu_C}\right) +\cdot +\operatorname{eq}\!\left(r_{\mathrm{col}}^{\mathrm{hi}}, 0^{\sigma_D - \sigma_C}\right). +$$ + +#### Permuting Precommitted Polynomial Variables + +The precommitted sumchecks still bind variables low-to-high. But the final Dory point order is determined by the joint geometry, not by the order in which those rounds happen. + +So Jolt permutes the variables of each precommitted polynomial before running the sumcheck. This keeps the sumcheck code simple while ensuring the final claim corresponds to the original polynomial at the correct Stage 8 point. + +This is cheap: it is only a variable-position permutation, so on the coefficient table it is just a bit permutation of the $2^n$ Boolean-hypercube evaluations. + +Here is a concrete 3-variable example. Suppose the original polynomial is encoded by + +```text +point 000 001 010 011 100 101 110 111 +P(point) v0 v1 v2 v3 v4 v5 v6 v7 +``` + +Now suppose the Stage 8 geometry wants the variables in the order $(c,b,a)$ rather than $(a,b,c)$. Define + +$$ +P'(u,v,w) = P(w,v,u). +$$ + +Then the new coefficient table becomes + +```text +point 000 001 010 011 100 101 110 111 +P'(point) v0 v4 v2 v6 v1 v5 v3 v7 +``` + +because + +```text +P'(000) = P(000) +P'(001) = P(100) +P'(010) = P(010) +P'(011) = P(110) +P'(100) = P(001) +P'(101) = P(101) +P'(110) = P(011) +P'(111) = P(111) +``` + +After the sumcheck finishes, `normalize_opening_point()` converts the collected challenges back into the true opening point of the original, non-permuted polynomial. + ### `RLCPolynomial` Recall that all of the polynomials in Jolt fall into one of two categories: **one-hot** polynomials (the $\widetilde{\textsf{ra}}$ and $\widetilde{\textsf{wa}}$ arising in [Twist/Shout](../twist-shout.md)), and **dense** polynomials (we use this to mean anything that's not one-hot). From a0ca6128a3d9ee91b66976e3cf865a28cfc8df2d Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Thu, 19 Mar 2026 14:17:09 -0700 Subject: [PATCH 10/20] feat(zkvm): integrate committed bytecode handling across examples Add support for processing committed bytecode in various examples by introducing a command-line argument to specify the bytecode chunk. Minor bug fix for program-image in committed mode. --- examples/advice-demo/src/main.rs | 24 ++- examples/alloc/src/main.rs | 26 ++- examples/backtrace/src/main.rs | 68 ++++-- examples/btreemap/host/src/main.rs | 43 ++-- examples/collatz/src/main.rs | 57 +++-- examples/fibonacci/Cargo.toml | 3 - examples/fibonacci/src/main.rs | 23 ++- examples/hash-bench/src/main.rs | 22 +- examples/malloc/src/main.rs | 26 ++- examples/memory-ops/src/main.rs | 28 ++- examples/merkle-tree/src/main.rs | 32 ++- examples/modinv/src/main.rs | 22 +- examples/muldiv/src/main.rs | 26 ++- examples/multi-function/src/main.rs | 44 ++-- examples/overflow/src/main.rs | 53 +++-- examples/random/src/main.rs | 26 ++- examples/recover-ecdsa/src/main.rs | 26 ++- examples/recursion/src/main.rs | 98 ++++++++- examples/secp256k1-ecdsa-verify/src/main.rs | 31 ++- .../Cargo.toml | 14 ++ .../guest/Cargo.toml | 11 + .../guest/src/lib.rs | 13 ++ .../guest/src/main.rs | 5 + .../src/main.rs | 72 +++++++ examples/sha2-chain-huge-advice/Cargo.toml | 10 + .../sha2-chain-huge-advice/guest/Cargo.toml | 11 + .../sha2-chain-huge-advice/guest/src/lib.rs | 39 ++++ .../sha2-chain-huge-advice/guest/src/main.rs | 5 + examples/sha2-chain-huge-advice/src/main.rs | 100 +++++++++ examples/sha2-chain/src/main.rs | 28 ++- examples/sha2-ex/src/main.rs | 26 ++- examples/sha3-chain/src/main.rs | 28 ++- examples/sha3-ex/src/main.rs | 26 ++- examples/sig-recovery/host/src/main.rs | 24 ++- examples/stdlib/src/main.rs | 79 +++++-- jolt-core/src/poly/opening_proof.rs | 7 +- jolt-core/src/poly/rlc_polynomial.rs | 195 ++++++++++++++---- jolt-core/src/zkvm/bytecode/chunks.rs | 20 +- .../src/zkvm/bytecode/read_raf_checking.rs | 29 ++- .../src/zkvm/claim_reductions/bytecode.rs | 19 +- jolt-core/src/zkvm/claim_reductions/mod.rs | 3 +- .../src/zkvm/claim_reductions/precommitted.rs | 28 +++ .../zkvm/claim_reductions/program_image.rs | 30 +-- jolt-core/src/zkvm/program.rs | 57 ++++- jolt-core/src/zkvm/prover.rs | 88 +++++--- jolt-core/src/zkvm/verifier.rs | 17 +- jolt-sdk/Cargo.toml | 2 +- jolt-sdk/tests/fixtures/fib_io_device.bin | Bin 0 -> 199 bytes jolt-sdk/tests/fixtures/fib_proof.bin | Bin 0 -> 70500 bytes .../fixtures/jolt_verifier_preprocessing.dat | Bin 0 -> 873697 bytes 50 files changed, 1298 insertions(+), 366 deletions(-) create mode 100644 examples/sha2-chain-committed-16-untrusted-advice/Cargo.toml create mode 100644 examples/sha2-chain-committed-16-untrusted-advice/guest/Cargo.toml create mode 100644 examples/sha2-chain-committed-16-untrusted-advice/guest/src/lib.rs create mode 100644 examples/sha2-chain-committed-16-untrusted-advice/guest/src/main.rs create mode 100644 examples/sha2-chain-committed-16-untrusted-advice/src/main.rs create mode 100644 examples/sha2-chain-huge-advice/Cargo.toml create mode 100644 examples/sha2-chain-huge-advice/guest/Cargo.toml create mode 100644 examples/sha2-chain-huge-advice/guest/src/lib.rs create mode 100644 examples/sha2-chain-huge-advice/guest/src/main.rs create mode 100644 examples/sha2-chain-huge-advice/src/main.rs create mode 100644 jolt-sdk/tests/fixtures/fib_io_device.bin create mode 100644 jolt-sdk/tests/fixtures/fib_proof.bin create mode 100644 jolt-sdk/tests/fixtures/jolt_verifier_preprocessing.dat diff --git a/examples/advice-demo/src/main.rs b/examples/advice-demo/src/main.rs index acc38647d5..44108b9472 100644 --- a/examples/advice-demo/src/main.rs +++ b/examples/advice-demo/src/main.rs @@ -4,6 +4,10 @@ use tracing::info; // Demonstration of advice tape usage in a provable computation pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; @@ -13,11 +17,21 @@ pub fn main() { let b = vec![0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let mut program = guest::compile_advice_demo(target_dir); - let shared_preprocessing = guest::preprocess_shared_advice_demo(&mut program); - let prover_preprocessing = guest::preprocess_prover_advice_demo(shared_preprocessing.clone()); - let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); - let verifier_preprocessing = - guest::preprocess_verifier_advice_demo(shared_preprocessing, verifier_setup, None); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_advice_demo(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_advice_demo(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_advice_demo(&mut program); + let prover_preprocessing = + guest::preprocess_prover_advice_demo(shared_preprocessing.clone()); + let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); + let verifier_preprocessing = + guest::preprocess_verifier_advice_demo(shared_preprocessing, verifier_setup, None); + (prover_preprocessing, verifier_preprocessing) + }; let prove_advice_demo = guest::build_prover_advice_demo(program, prover_preprocessing); let verify_advice_demo = guest::build_verifier_advice_demo(verifier_preprocessing); diff --git a/examples/alloc/src/main.rs b/examples/alloc/src/main.rs index 50b26ac909..36536bf8e0 100644 --- a/examples/alloc/src/main.rs +++ b/examples/alloc/src/main.rs @@ -3,17 +3,29 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_alloc(target_dir); - let shared_preprocessing = guest::preprocess_shared_alloc(&mut program); - let prover_preprocessing = guest::preprocess_prover_alloc(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_alloc( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_alloc(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_alloc(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_alloc(&mut program); + let prover_preprocessing = guest::preprocess_prover_alloc(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_alloc( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove_alloc = guest::build_prover_alloc(program, prover_preprocessing); let verify_alloc = guest::build_verifier_alloc(verifier_preprocessing); diff --git a/examples/backtrace/src/main.rs b/examples/backtrace/src/main.rs index 0704c3a986..9802273efb 100644 --- a/examples/backtrace/src/main.rs +++ b/examples/backtrace/src/main.rs @@ -15,13 +15,18 @@ fn main() { #[cfg(any(feature = "nostd", feature = "std"))] let should_panic = env_flag("JOLT_BT_TRIGGER").unwrap_or(true); #[cfg(any(feature = "nostd", feature = "std"))] + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); + #[cfg(any(feature = "nostd", feature = "std"))] let target_dir = "/tmp/jolt-guest-targets"; #[cfg(feature = "nostd")] - run_nostd(target_dir, should_panic); + run_nostd(target_dir, should_panic, bytecode_chunk); #[cfg(feature = "std")] - run_std(target_dir, should_panic); + run_std(target_dir, should_panic, bytecode_chunk); #[cfg(not(any(feature = "nostd", feature = "std")))] { @@ -39,7 +44,7 @@ fn env_flag(key: &str) -> Option { } #[cfg(feature = "nostd")] -fn run_nostd(target_dir: &str, should_panic: bool) { +fn run_nostd(target_dir: &str, should_panic: bool, bytecode_chunk: Option) { info!("mode=nostd should_panic={}", should_panic); let trace_enabled = env_flag("JOLT_BACKTRACE").unwrap_or(false); @@ -47,14 +52,26 @@ fn run_nostd(target_dir: &str, should_panic: bool) { let mut program = guest_nostd::compile_panic_backtrace_nostd(target_dir); - let shared_preprocessing = guest_nostd::preprocess_shared_panic_backtrace_nostd(&mut program); - let prover_preprocessing = - guest_nostd::preprocess_prover_panic_backtrace_nostd(shared_preprocessing.clone()); - let verifier_preprocessing = guest_nostd::preprocess_verifier_panic_backtrace_nostd( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest_nostd::preprocess_committed_panic_backtrace_nostd(&mut program, chunk_count); + let verifier_preprocessing = + guest_nostd::verifier_preprocessing_from_prover_panic_backtrace_nostd( + &prover_preprocessing, + ); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = + guest_nostd::preprocess_shared_panic_backtrace_nostd(&mut program); + let prover_preprocessing = + guest_nostd::preprocess_prover_panic_backtrace_nostd(shared_preprocessing.clone()); + let verifier_preprocessing = guest_nostd::preprocess_verifier_panic_backtrace_nostd( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove = guest_nostd::build_prover_panic_backtrace_nostd(program, prover_preprocessing); let verify = guest_nostd::build_verifier_panic_backtrace_nostd(verifier_preprocessing); @@ -78,7 +95,7 @@ fn run_nostd(target_dir: &str, should_panic: bool) { } #[cfg(feature = "std")] -fn run_std(target_dir: &str, should_panic: bool) { +fn run_std(target_dir: &str, should_panic: bool, bytecode_chunk: Option) { info!("mode=std should_panic={}", should_panic); let trace_enabled = env_flag("JOLT_BACKTRACE").unwrap_or(false); @@ -86,14 +103,25 @@ fn run_std(target_dir: &str, should_panic: bool) { let mut program = guest_std::compile_panic_backtrace_std(target_dir); - let shared_preprocessing = guest_std::preprocess_shared_panic_backtrace_std(&mut program); - let prover_preprocessing = - guest_std::preprocess_prover_panic_backtrace_std(shared_preprocessing.clone()); - let verifier_preprocessing = guest_std::preprocess_verifier_panic_backtrace_std( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest_std::preprocess_committed_panic_backtrace_std(&mut program, chunk_count); + let verifier_preprocessing = + guest_std::verifier_preprocessing_from_prover_panic_backtrace_std( + &prover_preprocessing, + ); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest_std::preprocess_shared_panic_backtrace_std(&mut program); + let prover_preprocessing = + guest_std::preprocess_prover_panic_backtrace_std(shared_preprocessing.clone()); + let verifier_preprocessing = guest_std::preprocess_verifier_panic_backtrace_std( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove = guest_std::build_prover_panic_backtrace_std(program, prover_preprocessing); let verify = guest_std::build_verifier_panic_backtrace_std(verifier_preprocessing); diff --git a/examples/btreemap/host/src/main.rs b/examples/btreemap/host/src/main.rs index 2e83fe45ef..853798e01c 100644 --- a/examples/btreemap/host/src/main.rs +++ b/examples/btreemap/host/src/main.rs @@ -11,27 +11,40 @@ macro_rules! step { } pub fn btreemap() { + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = step!("Compiling guest code", { guest::compile_btreemap(target_dir) }); - let shared_preprocessing = step!("Preprocessing shared", { - guest::preprocess_shared_btreemap(&mut program) - }); - - let prover_preprocessing = step!("Preprocessing prover", { - guest::preprocess_prover_btreemap(shared_preprocessing.clone()) - }); - - let verifier_preprocessing = step!("Preprocessing verifier", { - guest::preprocess_verifier_btreemap( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ) - }); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = step!("Preprocessing prover", { + guest::preprocess_committed_btreemap(&mut program, chunk_count) + }); + let verifier_preprocessing = step!("Preprocessing verifier", { + guest::verifier_preprocessing_from_prover_btreemap(&prover_preprocessing) + }); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = step!("Preprocessing shared", { + guest::preprocess_shared_btreemap(&mut program) + }); + let prover_preprocessing = step!("Preprocessing prover", { + guest::preprocess_prover_btreemap(shared_preprocessing.clone()) + }); + let verifier_preprocessing = step!("Preprocessing verifier", { + guest::preprocess_verifier_btreemap( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ) + }); + (prover_preprocessing, verifier_preprocessing) + }; let prove = step!("Building prover", { guest::build_prover_btreemap(program, prover_preprocessing) diff --git a/examples/collatz/src/main.rs b/examples/collatz/src/main.rs index c45fd2bfb7..d0492f6867 100644 --- a/examples/collatz/src/main.rs +++ b/examples/collatz/src/main.rs @@ -3,17 +3,33 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); // Prove/verify convergence for a single number: let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_collatz_convergence(target_dir); - let shared_preprocessing = guest::preprocess_shared_collatz_convergence(&mut program); - let prover_preprocessing = - guest::preprocess_prover_collatz_convergence(shared_preprocessing.clone()); - let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); - let verifier_preprocessing = - guest::preprocess_verifier_collatz_convergence(shared_preprocessing, verifier_setup, None); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_collatz_convergence(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_collatz_convergence(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_collatz_convergence(&mut program); + let prover_preprocessing = + guest::preprocess_prover_collatz_convergence(shared_preprocessing.clone()); + let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); + let verifier_preprocessing = guest::preprocess_verifier_collatz_convergence( + shared_preprocessing, + verifier_setup, + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove_collatz_single = guest::build_prover_collatz_convergence(program, prover_preprocessing); @@ -31,15 +47,26 @@ pub fn main() { // Prove/verify convergence for a range of numbers: let mut program = guest::compile_collatz_convergence_range(target_dir); - let shared_preprocessing = guest::preprocess_shared_collatz_convergence_range(&mut program); - let prover_preprocessing = - guest::preprocess_prover_collatz_convergence_range(shared_preprocessing.clone()); - let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); - let verifier_preprocessing = guest::preprocess_verifier_collatz_convergence_range( - shared_preprocessing, - verifier_setup, - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_collatz_convergence_range(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_collatz_convergence_range( + &prover_preprocessing, + ); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_collatz_convergence_range(&mut program); + let prover_preprocessing = + guest::preprocess_prover_collatz_convergence_range(shared_preprocessing.clone()); + let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); + let verifier_preprocessing = guest::preprocess_verifier_collatz_convergence_range( + shared_preprocessing, + verifier_setup, + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove_collatz_convergence = guest::build_prover_collatz_convergence_range(program, prover_preprocessing); diff --git a/examples/fibonacci/Cargo.toml b/examples/fibonacci/Cargo.toml index f20b3bc444..d8d898a159 100644 --- a/examples/fibonacci/Cargo.toml +++ b/examples/fibonacci/Cargo.toml @@ -3,9 +3,6 @@ name = "fibonacci" version = "0.1.0" edition = "2021" -[features] -zk = ["jolt-sdk/zk"] - [dependencies] jolt-sdk = { workspace = true, features = ["host"] } tracing-subscriber.workspace = true diff --git a/examples/fibonacci/src/main.rs b/examples/fibonacci/src/main.rs index 5365b03398..d78a3e351c 100644 --- a/examples/fibonacci/src/main.rs +++ b/examples/fibonacci/src/main.rs @@ -4,18 +4,29 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let save_to_disk = std::env::args().any(|arg| arg == "--save"); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_fib(target_dir); - let shared_preprocessing = guest::preprocess_shared_fib(&mut program); - - let prover_preprocessing = guest::preprocess_prover_fib(shared_preprocessing.clone()); - let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); - let verifier_preprocessing = - guest::preprocess_verifier_fib(shared_preprocessing, verifier_setup, None); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_fib(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_fib(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_fib(&mut program); + let prover_preprocessing = guest::preprocess_prover_fib(shared_preprocessing.clone()); + let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); + let verifier_preprocessing = + guest::preprocess_verifier_fib(shared_preprocessing, verifier_setup, None); + (prover_preprocessing, verifier_preprocessing) + }; if save_to_disk { serialize_and_print_size( diff --git a/examples/hash-bench/src/main.rs b/examples/hash-bench/src/main.rs index b254ce2fec..83690f3a27 100644 --- a/examples/hash-bench/src/main.rs +++ b/examples/hash-bench/src/main.rs @@ -2,15 +2,27 @@ use std::time::Instant; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_hashbench(target_dir); - let shared_preprocessing = guest::preprocess_shared_hashbench(&mut program); - let prover_preprocessing = guest::preprocess_prover_hashbench(shared_preprocessing.clone()); - let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); - let verifier_preprocessing = - guest::preprocess_verifier_hashbench(shared_preprocessing, verifier_setup, None); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_hashbench(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_hashbench(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_hashbench(&mut program); + let prover_preprocessing = guest::preprocess_prover_hashbench(shared_preprocessing.clone()); + let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); + let verifier_preprocessing = + guest::preprocess_verifier_hashbench(shared_preprocessing, verifier_setup, None); + (prover_preprocessing, verifier_preprocessing) + }; let prove_hashbench = guest::build_prover_hashbench(program, prover_preprocessing); let verify_hashbench = guest::build_verifier_hashbench(verifier_preprocessing); diff --git a/examples/malloc/src/main.rs b/examples/malloc/src/main.rs index 2889edbbb7..61a86b2169 100644 --- a/examples/malloc/src/main.rs +++ b/examples/malloc/src/main.rs @@ -1,16 +1,28 @@ use std::time::Instant; pub fn main() { + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_alloc(target_dir); - let shared_preprocessing = guest::preprocess_shared_alloc(&mut program); - let prover_preprocessing = guest::preprocess_prover_alloc(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_alloc( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_alloc(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_alloc(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_alloc(&mut program); + let prover_preprocessing = guest::preprocess_prover_alloc(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_alloc( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove = guest::build_prover_alloc(program, prover_preprocessing); let verify = guest::build_verifier_alloc(verifier_preprocessing); diff --git a/examples/memory-ops/src/main.rs b/examples/memory-ops/src/main.rs index 165560cb9e..5ff436bafc 100644 --- a/examples/memory-ops/src/main.rs +++ b/examples/memory-ops/src/main.rs @@ -3,17 +3,31 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_memory_ops(target_dir); - let shared_preprocessing = guest::preprocess_shared_memory_ops(&mut program); - let prover_preprocessing = guest::preprocess_prover_memory_ops(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_memory_ops( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_memory_ops(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_memory_ops(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_memory_ops(&mut program); + let prover_preprocessing = + guest::preprocess_prover_memory_ops(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_memory_ops( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove = guest::build_prover_memory_ops(program, prover_preprocessing); let verify = guest::build_verifier_memory_ops(verifier_preprocessing); diff --git a/examples/merkle-tree/src/main.rs b/examples/merkle-tree/src/main.rs index 395e5b084b..b0f7e6b944 100644 --- a/examples/merkle-tree/src/main.rs +++ b/examples/merkle-tree/src/main.rs @@ -1,20 +1,36 @@ -use jolt_sdk::{TrustedAdvice, UntrustedAdvice}; +use jolt_sdk::{DoryContext, DoryGlobals, DoryLayout, TrustedAdvice, UntrustedAdvice}; use std::time::Instant; use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); + DoryGlobals::initialize_context(1, 1, DoryContext::Main, Some(DoryLayout::CycleMajor)) + .expect("failed to set Dory layout"); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_merkle_tree(target_dir); - let shared_preprocessing = guest::preprocess_shared_merkle_tree(&mut program); - let prover_preprocessing = guest::preprocess_prover_merkle_tree(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_merkle_tree( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_merkle_tree(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_merkle_tree(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_merkle_tree(&mut program); + let prover_preprocessing = + guest::preprocess_prover_merkle_tree(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_merkle_tree( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let leaf1: &[u8] = &[5u8; 32]; let leaf2 = [6u8; 32]; diff --git a/examples/modinv/src/main.rs b/examples/modinv/src/main.rs index 8a204552c2..abc7021acb 100644 --- a/examples/modinv/src/main.rs +++ b/examples/modinv/src/main.rs @@ -3,6 +3,10 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; @@ -15,11 +19,19 @@ pub fn main() { // Compile and preprocess the advice-based version let mut program = guest::compile_modinv(target_dir); - let shared_preprocessing = guest::preprocess_shared_modinv(&mut program); - let prover_preprocessing = guest::preprocess_prover_modinv(shared_preprocessing.clone()); - let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); - let verifier_preprocessing = - guest::preprocess_verifier_modinv(shared_preprocessing, verifier_setup, None); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_modinv(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_modinv(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_modinv(&mut program); + let prover_preprocessing = guest::preprocess_prover_modinv(shared_preprocessing.clone()); + let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); + let verifier_preprocessing = + guest::preprocess_verifier_modinv(shared_preprocessing, verifier_setup, None); + (prover_preprocessing, verifier_preprocessing) + }; let prove_modinv = guest::build_prover_modinv(program, prover_preprocessing); let verify_modinv = guest::build_verifier_modinv(verifier_preprocessing); diff --git a/examples/muldiv/src/main.rs b/examples/muldiv/src/main.rs index e58a2f576f..d24305dae5 100644 --- a/examples/muldiv/src/main.rs +++ b/examples/muldiv/src/main.rs @@ -3,17 +3,29 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_muldiv(target_dir); - let shared_preprocessing = guest::preprocess_shared_muldiv(&mut program); - let prover_preprocessing = guest::preprocess_prover_muldiv(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_muldiv( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_muldiv(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_muldiv(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_muldiv(&mut program); + let prover_preprocessing = guest::preprocess_prover_muldiv(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_muldiv( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove = guest::build_prover_muldiv(program, prover_preprocessing); let verify = guest::build_verifier_muldiv(verifier_preprocessing); diff --git a/examples/multi-function/src/main.rs b/examples/multi-function/src/main.rs index 91f7ed93fa..a90bc9a284 100644 --- a/examples/multi-function/src/main.rs +++ b/examples/multi-function/src/main.rs @@ -3,16 +3,28 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); // Prove addition. let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_add(target_dir); - let shared_preprocessing = guest::preprocess_shared_add(&mut program); - let prover_preprocessing = guest::preprocess_prover_add(shared_preprocessing.clone()); - let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); - let verifier_preprocessing = - guest::preprocess_verifier_add(shared_preprocessing, verifier_setup, None); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_add(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_add(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_add(&mut program); + let prover_preprocessing = guest::preprocess_prover_add(shared_preprocessing.clone()); + let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); + let verifier_preprocessing = + guest::preprocess_verifier_add(shared_preprocessing, verifier_setup, None); + (prover_preprocessing, verifier_preprocessing) + }; let prove_add = guest::build_prover_add(program, prover_preprocessing); let verify_add = guest::build_verifier_add(verifier_preprocessing); @@ -21,13 +33,21 @@ pub fn main() { let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_mul(target_dir); - let shared_preprocessing = guest::preprocess_shared_mul(&mut program); - let prover_preprocessing = guest::preprocess_prover_mul(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_mul( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_mul(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_mul(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_mul(&mut program); + let prover_preprocessing = guest::preprocess_prover_mul(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_mul( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove_mul = guest::build_prover_mul(program, prover_preprocessing); let verify_mul = guest::build_verifier_mul(verifier_preprocessing); diff --git a/examples/overflow/src/main.rs b/examples/overflow/src/main.rs index cfc5d9dd47..20df2a9446 100644 --- a/examples/overflow/src/main.rs +++ b/examples/overflow/src/main.rs @@ -5,13 +5,20 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); // An overflowing stack should fail to prove. let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_overflow_stack(target_dir); - let shared_preprocessing = guest::preprocess_shared_overflow_stack(&mut program); - let prover_preprocessing = - guest::preprocess_prover_overflow_stack(shared_preprocessing.clone()); + let prover_preprocessing = if let Some(chunk_count) = bytecode_chunk { + guest::preprocess_committed_overflow_stack(&mut program, chunk_count) + } else { + let shared_preprocessing = guest::preprocess_shared_overflow_stack(&mut program); + guest::preprocess_prover_overflow_stack(shared_preprocessing.clone()) + }; let prove_overflow_stack = guest::build_prover_overflow_stack(program, prover_preprocessing); let res = panic::catch_unwind(|| { @@ -23,8 +30,12 @@ pub fn main() { // now lets try to overflow the heap, should also panic let mut program = guest::compile_overflow_heap(target_dir); - let shared_preprocessing = guest::preprocess_shared_overflow_heap(&mut program); - let prover_preprocessing = guest::preprocess_prover_overflow_heap(shared_preprocessing.clone()); + let prover_preprocessing = if let Some(chunk_count) = bytecode_chunk { + guest::preprocess_committed_overflow_heap(&mut program, chunk_count) + } else { + let shared_preprocessing = guest::preprocess_shared_overflow_heap(&mut program); + guest::preprocess_prover_overflow_heap(shared_preprocessing.clone()) + }; let prove_overflow_heap = guest::build_prover_overflow_heap(program, prover_preprocessing); let res = panic::catch_unwind(|| { @@ -36,15 +47,29 @@ pub fn main() { // but with stack_size=8192 let mut program = guest::compile_allocate_stack_with_increased_size(target_dir); - let shared_preprocessing = - guest::preprocess_shared_allocate_stack_with_increased_size(&mut program); - let prover_preprocessing = - guest::preprocess_prover_allocate_stack_with_increased_size(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_allocate_stack_with_increased_size( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_allocate_stack_with_increased_size( + &mut program, + chunk_count, + ); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_allocate_stack_with_increased_size( + &prover_preprocessing, + ); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = + guest::preprocess_shared_allocate_stack_with_increased_size(&mut program); + let prover_preprocessing = guest::preprocess_prover_allocate_stack_with_increased_size( + shared_preprocessing.clone(), + ); + let verifier_preprocessing = guest::preprocess_verifier_allocate_stack_with_increased_size( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove_allocate_stack_with_increased_size = guest::build_prover_allocate_stack_with_increased_size(program, prover_preprocessing); diff --git a/examples/random/src/main.rs b/examples/random/src/main.rs index e0eff34bca..60cc06f31b 100644 --- a/examples/random/src/main.rs +++ b/examples/random/src/main.rs @@ -3,17 +3,29 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_rand(target_dir); - let shared_preprocessing = guest::preprocess_shared_rand(&mut program); - let prover_preprocessing = guest::preprocess_prover_rand(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_rand( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_rand(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_rand(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_rand(&mut program); + let prover_preprocessing = guest::preprocess_prover_rand(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_rand( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove = guest::build_prover_rand(program, prover_preprocessing); let verify = guest::build_verifier_rand(verifier_preprocessing); diff --git a/examples/recover-ecdsa/src/main.rs b/examples/recover-ecdsa/src/main.rs index 9f0a894180..cfa8cc3ac9 100644 --- a/examples/recover-ecdsa/src/main.rs +++ b/examples/recover-ecdsa/src/main.rs @@ -11,6 +11,10 @@ const SECRET_KEY: [u8; 32] = [ pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let secp = Secp256k1::new(); @@ -31,13 +35,21 @@ pub fn main() { let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_recover(target_dir); - let shared_preprocessing = guest::preprocess_shared_recover(&mut program); - let prover_preprocessing = guest::preprocess_prover_recover(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_recover( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_recover(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_recover(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_recover(&mut program); + let prover_preprocessing = guest::preprocess_prover_recover(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_recover( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; if save_to_disk { serialize_and_print_size( diff --git a/examples/recursion/src/main.rs b/examples/recursion/src/main.rs index 57b4132e39..162fc73b91 100644 --- a/examples/recursion/src/main.rs +++ b/examples/recursion/src/main.rs @@ -1,9 +1,13 @@ use ark_serialize::CanonicalDeserialize; use ark_serialize::CanonicalSerialize; use clap::{Parser, Subcommand}; -use jolt_sdk::{JoltDevice, MemoryConfig, RV64IMACProof, Serializable}; +use jolt_sdk::{ + JoltDevice, JoltProverPreprocessing, JoltSharedPreprocessing, MemoryConfig, MemoryLayout, + ProgramPreprocessing, RV64IMACProof, Serializable, +}; use std::cmp::PartialEq; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::time::Instant; use tracing::{error, info}; @@ -17,10 +21,67 @@ fn get_guest_src_dir() -> PathBuf { #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { + #[arg(long, global = true, default_value_t = false)] + committed_bytecode: bool, + #[arg( + long, + global = true, + value_name = "COUNT", + requires = "committed_bytecode", + value_parser = parse_bytecode_chunk + )] + bytecode_chunk: Option, #[command(subcommand)] command: Option, } +#[derive(Clone, Copy)] +struct BytecodeConfig { + committed_bytecode: bool, + bytecode_chunk: Option, +} + +impl BytecodeConfig { + fn chunk_count(self) -> usize { + self.bytecode_chunk.unwrap_or(1) + } +} + +fn parse_bytecode_chunk(value: &str) -> Result { + value + .parse::() + .map_err(|_| format!("invalid bytecode chunk count `{value}`")) +} + +fn preprocess_guest_program( + guest: &jolt_sdk::guest::program::Program, + max_trace_length: usize, + bytecode_config: BytecodeConfig, +) -> JoltProverPreprocessing { + let (bytecode, memory_init, program_size, _e_entry) = guest.decode(); + + let mut memory_config = guest.memory_config; + memory_config.program_size = Some(program_size); + let memory_layout = MemoryLayout::new(&memory_config); + let program = Arc::new(ProgramPreprocessing::preprocess(bytecode, memory_init)); + let shared_preprocessing = if bytecode_config.committed_bytecode { + JoltSharedPreprocessing::new_committed( + program.meta(), + memory_layout, + max_trace_length, + bytecode_config.chunk_count(), + ) + } else { + JoltSharedPreprocessing::new(program.meta(), memory_layout, max_trace_length) + }; + + if bytecode_config.committed_bytecode { + JoltProverPreprocessing::new_committed(shared_preprocessing, program) + } else { + JoltProverPreprocessing::new(shared_preprocessing, program) + } +} + #[derive(Subcommand)] enum Commands { /// Generate proofs for guest programs @@ -268,7 +329,12 @@ fn check_data_integrity(all_groups_data: &[u8]) -> (u32, u32) { (n, remaining_data.len() as u32) } -fn collect_guest_proofs(guest: GuestProgram, target_dir: &str, use_embed: bool) -> Vec { +fn collect_guest_proofs( + guest: GuestProgram, + target_dir: &str, + use_embed: bool, + bytecode_config: BytecodeConfig, +) -> Vec { info!("Starting collect_guest_proofs for {}", guest.name()); let max_trace_length = guest.get_max_trace_length(use_embed); @@ -293,7 +359,7 @@ fn collect_guest_proofs(guest: GuestProgram, target_dir: &str, use_embed: bool) info!("Preprocessing guest prover..."); let guest_prover_preprocessing = - jolt_sdk::guest::prover::preprocess(&guest_prog, max_trace_length); + preprocess_guest_program(&guest_prog, max_trace_length, bytecode_config); info!("Preprocessing guest verifier..."); let guest_verifier_preprocessing = jolt_sdk::JoltVerifierPreprocessing::from(&guest_prover_preprocessing); @@ -449,13 +515,13 @@ fn load_proof_data(guest: GuestProgram, workdir: &Path) -> Vec { proof_data } -fn generate_proofs(guest: GuestProgram, workdir: &Path) { +fn generate_proofs(guest: GuestProgram, workdir: &Path, bytecode_config: BytecodeConfig) { info!("Generating proofs for {} guest program...", guest.name()); let target_dir = "/tmp/jolt-guest-targets"; // Collect guest proofs - let all_groups_data = collect_guest_proofs(guest, target_dir, false); + let all_groups_data = collect_guest_proofs(guest, target_dir, false, bytecode_config); // Save proof data save_proof_data(guest, &all_groups_data, workdir); @@ -469,6 +535,7 @@ fn run_recursion_proof( input_bytes: Vec, memory_config: MemoryConfig, mut max_trace_length: usize, + bytecode_config: BytecodeConfig, ) { let target_dir = "/tmp/jolt-guest-targets"; @@ -486,7 +553,7 @@ fn run_recursion_proof( max_trace_length = 0; } let recursion_prover_preprocessing = - jolt_sdk::guest::prover::preprocess(&recursion, max_trace_length); + preprocess_guest_program(&recursion, max_trace_length, bytecode_config); let recursion_verifier_preprocessing = jolt_sdk::JoltVerifierPreprocessing::from(&recursion_prover_preprocessing); @@ -555,6 +622,7 @@ fn verify_proofs( workdir: &Path, output_dir: &Path, run_config: RunConfig, + bytecode_config: BytecodeConfig, ) { info!("Verifying proofs for {} guest program...", guest.name()); info!("Using embed mode: {use_embed}"); @@ -581,6 +649,7 @@ fn verify_proofs( input_bytes, memory_config, guest.get_max_trace_length(use_embed), + bytecode_config, ); } else { info!("Running {} recursion with input data...", guest.name()); @@ -610,6 +679,7 @@ fn verify_proofs( input_bytes, memory_config, guest.get_max_trace_length(use_embed), + bytecode_config, ); } } @@ -618,6 +688,10 @@ fn main() { tracing_subscriber::fmt::init(); let cli = Cli::parse(); + let bytecode_config = BytecodeConfig { + committed_bytecode: cli.committed_bytecode, + bytecode_chunk: cli.bytecode_chunk, + }; match &cli.command { Some(Commands::Generate { example, workdir }) => { @@ -628,7 +702,7 @@ fn main() { return; } }; - generate_proofs(guest, workdir); + generate_proofs(guest, workdir, bytecode_config); } Some(Commands::Verify { example, @@ -653,6 +727,7 @@ fn main() { workdir, &output_dir, RunConfig::Prove, + bytecode_config, ); } Some(Commands::Trace { @@ -678,7 +753,14 @@ fn main() { } else { RunConfig::Trace }; - verify_proofs(guest, embed.is_some(), workdir, &output_dir, run_config); + verify_proofs( + guest, + embed.is_some(), + workdir, + &output_dir, + run_config, + bytecode_config, + ); } None => { info!("No subcommand specified. Available commands:"); diff --git a/examples/secp256k1-ecdsa-verify/src/main.rs b/examples/secp256k1-ecdsa-verify/src/main.rs index 4aa828bd27..b99939fda3 100644 --- a/examples/secp256k1-ecdsa-verify/src/main.rs +++ b/examples/secp256k1-ecdsa-verify/src/main.rs @@ -3,19 +3,32 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_secp256k1_ecdsa_verify(target_dir); - let shared_preprocessing = guest::preprocess_shared_secp256k1_ecdsa_verify(&mut program); - let prover_preprocessing = - guest::preprocess_prover_secp256k1_ecdsa_verify(shared_preprocessing.clone()); - let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); - let verifier_preprocessing = guest::preprocess_verifier_secp256k1_ecdsa_verify( - shared_preprocessing, - verifier_setup, - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_secp256k1_ecdsa_verify(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_secp256k1_ecdsa_verify(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_secp256k1_ecdsa_verify(&mut program); + let prover_preprocessing = + guest::preprocess_prover_secp256k1_ecdsa_verify(shared_preprocessing.clone()); + let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); + let verifier_preprocessing = guest::preprocess_verifier_secp256k1_ecdsa_verify( + shared_preprocessing, + verifier_setup, + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove_secp256k1_ecdsa_verify = guest::build_prover_secp256k1_ecdsa_verify(program, prover_preprocessing); diff --git a/examples/sha2-chain-committed-16-untrusted-advice/Cargo.toml b/examples/sha2-chain-committed-16-untrusted-advice/Cargo.toml new file mode 100644 index 0000000000..d93d450478 --- /dev/null +++ b/examples/sha2-chain-committed-16-untrusted-advice/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "sha2-chain-committed-16-untrusted-advice" +version = "0.1.0" +edition = "2021" + +[dependencies] +jolt-sdk = { workspace = true, features = ["host"] } +jolt-core.workspace = true +tracing-subscriber.workspace = true +tracing.workspace = true +jolt-inlines-sha2 = { workspace = true, features = ["host"] } +guest = { package = "sha2-chain-committed-16-untrusted-advice-guest", path = "./guest" } + +hex.workspace = true diff --git a/examples/sha2-chain-committed-16-untrusted-advice/guest/Cargo.toml b/examples/sha2-chain-committed-16-untrusted-advice/guest/Cargo.toml new file mode 100644 index 0000000000..77f5c8f21a --- /dev/null +++ b/examples/sha2-chain-committed-16-untrusted-advice/guest/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "sha2-chain-committed-16-untrusted-advice-guest" +version = "0.1.0" +edition = "2021" + +[features] +guest = [] + +[dependencies] +jolt = { package = "jolt-sdk", path = "../../../jolt-sdk", features = [] } +jolt-inlines-sha2.workspace = true diff --git a/examples/sha2-chain-committed-16-untrusted-advice/guest/src/lib.rs b/examples/sha2-chain-committed-16-untrusted-advice/guest/src/lib.rs new file mode 100644 index 0000000000..4775d81279 --- /dev/null +++ b/examples/sha2-chain-committed-16-untrusted-advice/guest/src/lib.rs @@ -0,0 +1,13 @@ +#![cfg_attr(feature = "guest", no_std)] + +#[jolt::provable(max_trace_length = 4194304)] +fn sha2_chain( + input: jolt::UntrustedAdvice<[u8; 32]>, + num_iters: jolt::UntrustedAdvice, +) -> [u8; 32] { + let mut hash = *input; + for _ in 0..*num_iters { + hash = jolt_inlines_sha2::Sha256::digest(&hash); + } + hash +} diff --git a/examples/sha2-chain-committed-16-untrusted-advice/guest/src/main.rs b/examples/sha2-chain-committed-16-untrusted-advice/guest/src/main.rs new file mode 100644 index 0000000000..bd66a29930 --- /dev/null +++ b/examples/sha2-chain-committed-16-untrusted-advice/guest/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(feature = "guest", no_std)] +#![no_main] + +#[allow(unused_imports)] +use sha2_chain_committed_16_untrusted_advice_guest::*; diff --git a/examples/sha2-chain-committed-16-untrusted-advice/src/main.rs b/examples/sha2-chain-committed-16-untrusted-advice/src/main.rs new file mode 100644 index 0000000000..8f0bc49a3b --- /dev/null +++ b/examples/sha2-chain-committed-16-untrusted-advice/src/main.rs @@ -0,0 +1,72 @@ +use jolt_inlines_sha2 as _; +use jolt_sdk::{DoryContext, DoryGlobals, DoryLayout, UntrustedAdvice}; +use std::time::Instant; +use tracing::info; + +fn dory_layout_from_env() -> DoryLayout { + match std::env::var("JOLT_DORY_LAYOUT") + .unwrap_or_else(|_| "cycle".to_string()) + .to_ascii_lowercase() + .as_str() + { + "cycle" | "cyclemajor" => DoryLayout::CycleMajor, + "address" | "addressmajor" | "addr" => DoryLayout::AddressMajor, + other => panic!("invalid JOLT_DORY_LAYOUT={other}; expected cycle|address"), + } +} + +pub fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); + + let layout = dory_layout_from_env(); + DoryGlobals::initialize_context(1, 1, DoryContext::Main, Some(layout)) + .expect("failed to initialize Dory layout"); + info!("dory layout: {:?}", layout); + + let target_dir = "/tmp/jolt-guest-targets"; + let mut program = guest::compile_sha2_chain(target_dir); + + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + info!("bytecode_chunk_count: {}", chunk_count); + let prover_preprocessing = + guest::preprocess_committed_sha2_chain(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_sha2_chain(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_sha2_chain(&mut program); + let prover_preprocessing = + guest::preprocess_prover_sha2_chain(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_sha2_chain( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; + + let prove_sha2_chain = guest::build_prover_sha2_chain(program, prover_preprocessing); + let verify_sha2_chain = guest::build_verifier_sha2_chain(verifier_preprocessing); + + let input = [5u8; 32]; + let iters = 10; + + let native_output = guest::sha2_chain(UntrustedAdvice::new(input), UntrustedAdvice::new(iters)); + let now = Instant::now(); + let (output, proof, program_io) = + prove_sha2_chain(UntrustedAdvice::new(input), UntrustedAdvice::new(iters)); + info!("Prover runtime: {} s", now.elapsed().as_secs_f64()); + let is_valid = verify_sha2_chain(output, program_io.panic, proof); + + assert_eq!(output, native_output, "output mismatch"); + if !is_valid { + return Err(std::io::Error::other("verification failed").into()); + } + info!("output: {}", hex::encode(output)); + info!("valid: {is_valid}"); + Ok(()) +} diff --git a/examples/sha2-chain-huge-advice/Cargo.toml b/examples/sha2-chain-huge-advice/Cargo.toml new file mode 100644 index 0000000000..6dd8e1a706 --- /dev/null +++ b/examples/sha2-chain-huge-advice/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sha2-chain-huge-advice" +version = "0.1.0" +edition = "2021" + +[dependencies] +jolt-sdk = { workspace = true, features = ["host"] } +tracing-subscriber.workspace = true +tracing.workspace = true +guest = { package = "sha2-chain-huge-advice-guest", path = "./guest" } diff --git a/examples/sha2-chain-huge-advice/guest/Cargo.toml b/examples/sha2-chain-huge-advice/guest/Cargo.toml new file mode 100644 index 0000000000..b9f49e6d51 --- /dev/null +++ b/examples/sha2-chain-huge-advice/guest/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "sha2-chain-huge-advice-guest" +version = "0.1.0" +edition = "2021" + +[features] +guest = [] +compute_advice = [] + +[dependencies] +jolt = { package = "jolt-sdk", path = "../../../jolt-sdk", features = [] } diff --git a/examples/sha2-chain-huge-advice/guest/src/lib.rs b/examples/sha2-chain-huge-advice/guest/src/lib.rs new file mode 100644 index 0000000000..39e5f98e34 --- /dev/null +++ b/examples/sha2-chain-huge-advice/guest/src/lib.rs @@ -0,0 +1,39 @@ +#![cfg_attr(feature = "guest", no_std)] +use jolt::{end_cycle_tracking, start_cycle_tracking}; + +#[jolt::provable( + heap_size = 32768, + max_trace_length = 4194304, + // Keep advice capacity very large, but below the threshold that would place + // the untrusted-advice base at address 0 in the guest memory layout. + max_untrusted_advice_size = 16777216, + backtrace = "off" +)] +fn fib_huge_advice(n: u32, huge_advice: jolt::UntrustedAdvice<&[u8]>) -> u128 { + let advice = *huge_advice; + jolt::check_advice!(advice.len() >= 2, "advice must contain at least 2 bytes"); + let last_idx = advice.len() - 1; + let sampled_idx = (n as usize) % advice.len(); + + jolt::check_advice_eq!(advice[0] as u64, 7u64, "unexpected first advice byte"); + jolt::check_advice_eq!(advice[last_idx] as u64, 7u64, "unexpected last advice byte"); + jolt::check_advice_eq!( + advice[sampled_idx] as u64, + 7u64, + "unexpected sampled advice byte" + ); + + let mut a: u128 = 0; + let mut b: u128 = 1; + let mut sum: u128; + + start_cycle_tracking("fib_loop_huge_advice"); + for _ in 1..n { + sum = a + b; + a = b; + b = sum; + } + end_cycle_tracking("fib_loop_huge_advice"); + + b +} diff --git a/examples/sha2-chain-huge-advice/guest/src/main.rs b/examples/sha2-chain-huge-advice/guest/src/main.rs new file mode 100644 index 0000000000..c3d5d27220 --- /dev/null +++ b/examples/sha2-chain-huge-advice/guest/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(feature = "guest", no_std)] +#![no_main] + +#[allow(unused_imports)] +use sha2_chain_huge_advice_guest::*; diff --git a/examples/sha2-chain-huge-advice/src/main.rs b/examples/sha2-chain-huge-advice/src/main.rs new file mode 100644 index 0000000000..afb7c4d65d --- /dev/null +++ b/examples/sha2-chain-huge-advice/src/main.rs @@ -0,0 +1,100 @@ +use jolt_sdk::{DoryContext, DoryGlobals, DoryLayout, UntrustedAdvice}; +use std::time::Instant; +use tracing::info; + +const N: u32 = 1; +const ADVICE_BYTES: usize = 8388608; + +fn serialized_advice_size(payload_len: usize) -> usize { + let payload = vec![7u8; payload_len]; + jolt_sdk::postcard::to_stdvec(&UntrustedAdvice::new(payload.as_slice())) + .expect("failed to serialize advice input") + .len() +} + +pub fn main() { + tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); + DoryGlobals::initialize_context(1, 1, DoryContext::Main, Some(DoryLayout::AddressMajor)) + .expect("failed to set Dory layout"); + + let advice_bytes = ADVICE_BYTES; + let serialized_advice_bytes = serialized_advice_size(advice_bytes); + let max_untrusted_advice_bytes = serialized_advice_bytes.next_power_of_two(); + + let target_dir = "/tmp/jolt-guest-targets"; + let mut program = guest::compile_fib_huge_advice(target_dir); + program.set_max_untrusted_advice_size(max_untrusted_advice_bytes as u64); + + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_fib_huge_advice(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_fib_huge_advice(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_fib_huge_advice(&mut program); + let prover_preprocessing = + guest::preprocess_prover_fib_huge_advice(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_fib_huge_advice( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; + + let prove_fib_huge_advice = guest::build_prover_fib_huge_advice(program, prover_preprocessing); + let verify_fib_huge_advice = guest::build_verifier_fib_huge_advice(verifier_preprocessing); + + let analysis = guest::analyze_fib_huge_advice(N, UntrustedAdvice::new(&[7u8; 2][..])); + let execution_trace_length = analysis.trace_len(); + let padded_trace_length = execution_trace_length.next_power_of_two(); + + let huge_advice = vec![7u8; advice_bytes]; + let advice_input = UntrustedAdvice::new(huge_advice.as_slice()); + let native_output = guest::fib_huge_advice(N, advice_input); + + let now = Instant::now(); + let (output, proof, program_io) = prove_fib_huge_advice(N, advice_input); + let trace_length = proof.trace_length; + info!("Prover runtime: {} s", now.elapsed().as_secs_f64()); + let is_valid = verify_fib_huge_advice(N, output, program_io.panic, proof); + + info!("output: {output}"); + info!("native_output: {native_output}"); + info!( + "execution trace length: {} and padded trace length: {}", + execution_trace_length, padded_trace_length + ); + info!("padded proof trace length: {}", trace_length); + info!("advice payload bytes: {}", advice_bytes); + info!("serialized advice bytes: {}", serialized_advice_bytes); + info!( + "configured max_untrusted_advice bytes: {}", + max_untrusted_advice_bytes + ); + info!( + "advice_bytes / padded_trace_length = {:.2}", + advice_bytes as f64 / trace_length as f64 + ); + info!("valid: {is_valid}"); + + assert_eq!(output, native_output, "output mismatch"); + // assert_eq!( + // trace_length, padded_trace_length, + // "analysis and proof trace lengths diverged" + // ); + // assert_eq!( + // advice_bytes, ADVICE_BYTES, + // "advice length must match the fixed target" + // ); + // assert!( + // serialized_advice_bytes <= max_untrusted_advice_bytes, + // "serialized advice exceeds configured max_untrusted_advice_size" + // ); + assert!(is_valid, "proof verification failed"); +} diff --git a/examples/sha2-chain/src/main.rs b/examples/sha2-chain/src/main.rs index 3b578b8c23..40796cff23 100644 --- a/examples/sha2-chain/src/main.rs +++ b/examples/sha2-chain/src/main.rs @@ -3,17 +3,31 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_sha2_chain(target_dir); - let shared_preprocessing = guest::preprocess_shared_sha2_chain(&mut program); - let prover_preprocessing = guest::preprocess_prover_sha2_chain(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_sha2_chain( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_sha2_chain(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_sha2_chain(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_sha2_chain(&mut program); + let prover_preprocessing = + guest::preprocess_prover_sha2_chain(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_sha2_chain( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove_sha2_chain = guest::build_prover_sha2_chain(program, prover_preprocessing); let verify_sha2_chain = guest::build_verifier_sha2_chain(verifier_preprocessing); diff --git a/examples/sha2-ex/src/main.rs b/examples/sha2-ex/src/main.rs index 3984913d79..ecf2c424c8 100644 --- a/examples/sha2-ex/src/main.rs +++ b/examples/sha2-ex/src/main.rs @@ -3,17 +3,29 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_sha2(target_dir); - let shared_preprocessing = guest::preprocess_shared_sha2(&mut program); - let prover_preprocessing = guest::preprocess_prover_sha2(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_sha2( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_sha2(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_sha2(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_sha2(&mut program); + let prover_preprocessing = guest::preprocess_prover_sha2(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_sha2( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove_sha2 = guest::build_prover_sha2(program, prover_preprocessing); let verify_sha2 = guest::build_verifier_sha2(verifier_preprocessing); diff --git a/examples/sha3-chain/src/main.rs b/examples/sha3-chain/src/main.rs index c118e37096..5e34dd28cc 100644 --- a/examples/sha3-chain/src/main.rs +++ b/examples/sha3-chain/src/main.rs @@ -3,16 +3,30 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_sha3_chain(target_dir); - let shared_preprocessing = guest::preprocess_shared_sha3_chain(&mut program); - let prover_preprocessing = guest::preprocess_prover_sha3_chain(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_sha3_chain( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_sha3_chain(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_sha3_chain(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_sha3_chain(&mut program); + let prover_preprocessing = + guest::preprocess_prover_sha3_chain(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_sha3_chain( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove_sha3_chain = guest::build_prover_sha3_chain(program, prover_preprocessing); let verify_sha3_chain = guest::build_verifier_sha3_chain(verifier_preprocessing); diff --git a/examples/sha3-ex/src/main.rs b/examples/sha3-ex/src/main.rs index 4da880ab3d..45c5189f28 100644 --- a/examples/sha3-ex/src/main.rs +++ b/examples/sha3-ex/src/main.rs @@ -3,16 +3,28 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; let mut program = guest::compile_sha3(target_dir); - let shared_preprocessing = guest::preprocess_shared_sha3(&mut program); - let prover_preprocessing = guest::preprocess_prover_sha3(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_sha3( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = guest::preprocess_committed_sha3(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_sha3(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_sha3(&mut program); + let prover_preprocessing = guest::preprocess_prover_sha3(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_sha3( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove_sha3 = guest::build_prover_sha3(program, prover_preprocessing); let verify_sha3 = guest::build_verifier_sha3(verifier_preprocessing); diff --git a/examples/sig-recovery/host/src/main.rs b/examples/sig-recovery/host/src/main.rs index c34c360f52..51a36a5351 100644 --- a/examples/sig-recovery/host/src/main.rs +++ b/examples/sig-recovery/host/src/main.rs @@ -17,6 +17,10 @@ fn main() { .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); info!("sig-recovery: zkVM ECDSA Signature Recovery"); info!("=============================================\n"); @@ -42,11 +46,21 @@ fn main() { info!("\nPreprocessing..."); let start = Instant::now(); - let shared_preprocessing = guest::preprocess_shared_verify_txs(&mut program); - let prover_preprocessing = guest::preprocess_prover_verify_txs(shared_preprocessing.clone()); - let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); - let verifier_preprocessing = - guest::preprocess_verifier_verify_txs(shared_preprocessing, verifier_setup, None); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_verify_txs(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_verify_txs(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_verify_txs(&mut program); + let prover_preprocessing = + guest::preprocess_prover_verify_txs(shared_preprocessing.clone()); + let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); + let verifier_preprocessing = + guest::preprocess_verifier_verify_txs(shared_preprocessing, verifier_setup, None); + (prover_preprocessing, verifier_preprocessing) + }; info!("Preprocessing time: {:?}", start.elapsed()); let prove_verify_txs = guest::build_prover_verify_txs(program, prover_preprocessing); diff --git a/examples/stdlib/src/main.rs b/examples/stdlib/src/main.rs index 2657e8ffa6..bc12e0770f 100644 --- a/examples/stdlib/src/main.rs +++ b/examples/stdlib/src/main.rs @@ -3,6 +3,10 @@ use tracing::info; pub fn main() { tracing_subscriber::fmt::init(); + let bytecode_chunk = std::env::args() + .skip_while(|arg| arg != "--committed-bytecode") + .nth(1) + .map(|arg| arg.parse().unwrap()); let target_dir = "/tmp/jolt-guest-targets"; @@ -10,13 +14,23 @@ pub fn main() { info!("=== Int to String ==="); let mut program = guest::compile_int_to_string(target_dir); - let shared_preprocessing = guest::preprocess_shared_int_to_string(&mut program); - let prover_preprocessing = guest::preprocess_prover_int_to_string(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_int_to_string( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_int_to_string(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_int_to_string(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_int_to_string(&mut program); + let prover_preprocessing = + guest::preprocess_prover_int_to_string(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_int_to_string( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove = guest::build_prover_int_to_string(program, prover_preprocessing); let verify = guest::build_verifier_int_to_string(verifier_preprocessing); @@ -31,13 +45,23 @@ pub fn main() { info!("=== String Concat ==="); let mut program = guest::compile_string_concat(target_dir); - let shared_preprocessing = guest::preprocess_shared_string_concat(&mut program); - let prover_preprocessing = guest::preprocess_prover_string_concat(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_string_concat( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_string_concat(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_string_concat(&prover_preprocessing); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_string_concat(&mut program); + let prover_preprocessing = + guest::preprocess_prover_string_concat(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_string_concat( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove = guest::build_prover_string_concat(program, prover_preprocessing); let verify = guest::build_verifier_string_concat(verifier_preprocessing); @@ -56,14 +80,25 @@ pub fn main() { info!("=== Parallel Sum of Squares (rayon) ==="); let mut program = guest::compile_parallel_sum_of_squares(target_dir); - let shared_preprocessing = guest::preprocess_shared_parallel_sum_of_squares(&mut program); - let prover_preprocessing = - guest::preprocess_prover_parallel_sum_of_squares(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_parallel_sum_of_squares( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); + let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { + let prover_preprocessing = + guest::preprocess_committed_parallel_sum_of_squares(&mut program, chunk_count); + let verifier_preprocessing = + guest::verifier_preprocessing_from_prover_parallel_sum_of_squares( + &prover_preprocessing, + ); + (prover_preprocessing, verifier_preprocessing) + } else { + let shared_preprocessing = guest::preprocess_shared_parallel_sum_of_squares(&mut program); + let prover_preprocessing = + guest::preprocess_prover_parallel_sum_of_squares(shared_preprocessing.clone()); + let verifier_preprocessing = guest::preprocess_verifier_parallel_sum_of_squares( + shared_preprocessing, + prover_preprocessing.generators.to_verifier_setup(), + None, + ); + (prover_preprocessing, verifier_preprocessing) + }; let prove = guest::build_prover_parallel_sum_of_squares(program, prover_preprocessing); let verify = guest::build_verifier_parallel_sum_of_squares(verifier_preprocessing); diff --git a/jolt-core/src/poly/opening_proof.rs b/jolt-core/src/poly/opening_proof.rs index c2d8a0e563..f12f241f4c 100644 --- a/jolt-core/src/poly/opening_proof.rs +++ b/jolt-core/src/poly/opening_proof.rs @@ -7,7 +7,10 @@ use crate::{ poly::rlc_polynomial::{RLCPolynomial, RLCStreamingData, TraceSource}, - zkvm::{claim_reductions::AdviceKind, config::OneHotParams}, + zkvm::{ + claim_reductions::{AdviceKind, PrecommittedPolynomial}, + config::OneHotParams, + }, }; use allocative::Allocative; use num_derive::FromPrimitive; @@ -297,7 +300,7 @@ impl DoryOpeningState { trace_source: TraceSource, rlc_streaming_data: Arc, mut opening_hints: HashMap, - precommitted_polys: HashMap>, + precommitted_polys: HashMap>, ) -> (MultilinearPolynomial, PCS::OpeningProofHint) { // Accumulate gamma coefficients per polynomial let mut rlc_map = BTreeMap::new(); diff --git a/jolt-core/src/poly/rlc_polynomial.rs b/jolt-core/src/poly/rlc_polynomial.rs index 41b2f7a690..43878b19b7 100644 --- a/jolt-core/src/poly/rlc_polynomial.rs +++ b/jolt-core/src/poly/rlc_polynomial.rs @@ -4,10 +4,17 @@ use crate::poly::multilinear_polynomial::MultilinearPolynomial; use crate::utils::accumulation::MedAccumS; use crate::utils::math::{s64_from_diff_u64s, Math}; use crate::utils::thread::unsafe_allocate_zero_vec; +use crate::zkvm::claim_reductions::PrecommittedPolynomial; use crate::zkvm::config::OneHotParams; use crate::zkvm::instruction::LookupQuery; use crate::zkvm::ram::remap_address; -use crate::zkvm::{bytecode::BytecodePreprocessing, witness::CommittedPolynomial}; +use crate::zkvm::{ + bytecode::{ + chunks::{committed_lanes, for_each_active_lane_value, ActiveLaneValue}, + BytecodePreprocessing, + }, + witness::CommittedPolynomial, +}; use allocative::Allocative; use common::constants::XLEN; use common::jolt_device::MemoryLayout; @@ -58,7 +65,7 @@ pub struct StreamingRLCContext { pub onehot_polys: Vec<(CommittedPolynomial, F)>, /// Precommitted polynomials with their RLC coefficients. /// These are NOT streamed from trace - they're passed in directly. - pub precommitted_polys: Vec<(F, MultilinearPolynomial)>, + pub precommitted_polys: Vec<(F, PrecommittedPolynomial)>, pub trace_source: TraceSource, pub preprocessing: Arc, pub one_hot_params: OneHotParams, @@ -173,7 +180,7 @@ impl RLCPolynomial { trace_source: TraceSource, poly_ids: Vec, coefficients: &[F], - mut precommitted_poly_map: HashMap>, + mut precommitted_poly_map: HashMap>, ) -> Self { debug_assert_eq!(poly_ids.len(), coefficients.len()); @@ -369,47 +376,151 @@ impl RLCPolynomial { .iter() .filter(|(_, precommitted_poly)| precommitted_poly.original_len() > 0) .for_each(|(coeff, precommitted_poly)| { - let precommitted_len = precommitted_poly.original_len(); - let precommitted_vars = precommitted_len.log_2(); - let (sigma_a, nu_a) = DoryGlobals::balanced_sigma_nu(precommitted_vars); - let precommitted_cols = 1usize << sigma_a; - let precommitted_rows = 1usize << nu_a; - - debug_assert!( - precommitted_cols <= num_columns, - "Precommitted columns (2^{{sigma_a}}={precommitted_cols}) must fit in main num_columns={num_columns}; \ + match precommitted_poly { + PrecommittedPolynomial::Dense(poly) => { + let precommitted_len = poly.original_len(); + let precommitted_vars = precommitted_len.log_2(); + let (sigma_a, nu_a) = DoryGlobals::balanced_sigma_nu(precommitted_vars); + let precommitted_cols = 1usize << sigma_a; + let precommitted_rows = 1usize << nu_a; + + debug_assert!( + precommitted_cols <= num_columns, + "Precommitted columns (2^{{sigma_a}}={precommitted_cols}) must fit in main num_columns={num_columns}; \ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." - ); - - // Only the top-left block contributes: rows [0..precommitted_rows), cols [0..precommitted_cols) - let effective_rows = precommitted_rows.min(left_vec.len()); - - // Compute column contributions: for each column, sum contributions from all rows - // Note: precommitted_len is always precommitted_cols * precommitted_rows (size must be power of 2) - let column_contributions: Vec = (0..precommitted_cols) - .into_par_iter() - .map(|col_idx| { - // For this column, sum contributions from all non-zero rows - left_vec[..effective_rows] - .iter() - .enumerate() - .filter(|(_, &left)| !left.is_zero()) - .map(|(row_idx, &left)| { - let coeff_idx = row_idx * precommitted_cols + col_idx; - let precommitted_val = precommitted_poly.get_coeff(coeff_idx); - left * *coeff * precommitted_val + ); + + let effective_rows = precommitted_rows.min(left_vec.len()); + let column_contributions: Vec = (0..precommitted_cols) + .into_par_iter() + .map(|col_idx| { + left_vec[..effective_rows] + .iter() + .enumerate() + .filter(|(_, &left)| !left.is_zero()) + .map(|(row_idx, &left)| { + let coeff_idx = row_idx * precommitted_cols + col_idx; + let precommitted_val = poly.get_coeff(coeff_idx); + left * *coeff * precommitted_val + }) + .sum() }) - .sum() - }) - .collect(); - - // Add column contributions to result in parallel - result[..precommitted_cols] - .par_iter_mut() - .zip(column_contributions.par_iter()) - .for_each(|(res, &contrib)| { - *res += contrib; - }); + .collect(); + + result[..precommitted_cols] + .par_iter_mut() + .zip(column_contributions.par_iter()) + .for_each(|(res, &contrib)| { + *res += contrib; + }); + } + PrecommittedPolynomial::BytecodeChunk { + chunk_index, + chunk_cycle_len, + } => { + let precommitted_len = committed_lanes() * *chunk_cycle_len; + let precommitted_vars = precommitted_len.log_2(); + let (sigma_a, nu_a) = DoryGlobals::balanced_sigma_nu(precommitted_vars); + let precommitted_cols = 1usize << sigma_a; + let effective_rows = (1usize << nu_a).min(left_vec.len()); + let chunk_start = chunk_index * chunk_cycle_len; + let chunk_end = chunk_start + chunk_cycle_len; + let layout = DoryGlobals::get_layout(); + let column_contributions = ctx.preprocessing.bytecode.bytecode + [chunk_start..chunk_end] + .par_iter() + .enumerate() + .fold( + || unsafe_allocate_zero_vec(precommitted_cols), + |mut acc, (chunk_cycle, instr)| { + for_each_active_lane_value::(instr, |global_lane, lane_val| { + let coeff_idx = layout.address_cycle_to_index( + global_lane, + chunk_cycle, + committed_lanes(), + *chunk_cycle_len, + ); + let row_idx = coeff_idx / precommitted_cols; + if row_idx >= effective_rows { + return; + } + let left = left_vec[row_idx]; + if left.is_zero() { + return; + } + let lane_value = match lane_val { + ActiveLaneValue::One => F::one(), + ActiveLaneValue::Scalar(v) => v, + }; + let col_idx = coeff_idx % precommitted_cols; + acc[col_idx] += left * *coeff * lane_value; + }); + acc + }, + ) + .reduce( + || unsafe_allocate_zero_vec(precommitted_cols), + |mut a, b| { + a.iter_mut().zip(b.iter()).for_each(|(x, y)| *x += *y); + a + }, + ); + + result[..precommitted_cols] + .par_iter_mut() + .zip(column_contributions.par_iter()) + .for_each(|(res, &contrib)| { + *res += contrib; + }); + } + PrecommittedPolynomial::ProgramImage { + words, + start_index, + padded_len, + } => { + let precommitted_vars = padded_len.log_2(); + let (sigma_a, nu_a) = DoryGlobals::balanced_sigma_nu(precommitted_vars); + let precommitted_cols = 1usize << sigma_a; + let effective_rows = (1usize << nu_a).min(left_vec.len()); + let column_contributions = words + .par_iter() + .enumerate() + .fold( + || unsafe_allocate_zero_vec(precommitted_cols), + |mut acc, (offset, &word)| { + if word == 0 { + return acc; + } + let coeff_idx = start_index + offset; + let row_idx = coeff_idx / precommitted_cols; + if row_idx >= effective_rows { + return acc; + } + let left = left_vec[row_idx]; + if left.is_zero() { + return acc; + } + let col_idx = coeff_idx % precommitted_cols; + acc[col_idx] += left * coeff.mul_u64(word); + acc + }, + ) + .reduce( + || unsafe_allocate_zero_vec(precommitted_cols), + |mut a, b| { + a.iter_mut().zip(b.iter()).for_each(|(x, y)| *x += *y); + a + }, + ); + + result[..precommitted_cols] + .par_iter_mut() + .zip(column_contributions.par_iter()) + .for_each(|(res, &contrib)| { + *res += contrib; + }); + } + } }); } diff --git a/jolt-core/src/zkvm/bytecode/chunks.rs b/jolt-core/src/zkvm/bytecode/chunks.rs index acdf22075e..47f820bd55 100644 --- a/jolt-core/src/zkvm/bytecode/chunks.rs +++ b/jolt-core/src/zkvm/bytecode/chunks.rs @@ -151,14 +151,11 @@ pub fn for_each_active_lane_value( } } -#[tracing::instrument( - skip_all, - name = "bytecode::build_committed_bytecode_chunk_polynomials" -)] -pub fn build_committed_bytecode_chunk_polynomials( +#[tracing::instrument(skip_all, name = "bytecode::build_committed_bytecode_chunk_coeffs")] +pub fn build_committed_bytecode_chunk_coeffs( instructions: &[Instruction], chunk_count: usize, -) -> Vec> { +) -> Vec> { let bytecode_len = instructions.len(); validate_committed_bytecode_chunking_for_len(bytecode_len, chunk_count); @@ -189,6 +186,17 @@ pub fn build_committed_bytecode_chunk_polynomials( } chunk_coeffs +} + +#[tracing::instrument( + skip_all, + name = "bytecode::build_committed_bytecode_chunk_polynomials" +)] +pub fn build_committed_bytecode_chunk_polynomials( + instructions: &[Instruction], + chunk_count: usize, +) -> Vec> { + build_committed_bytecode_chunk_coeffs::(instructions, chunk_count) .into_iter() .map(MultilinearPolynomial::from) .collect() diff --git a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs index ffc21a15a6..1d580227ba 100644 --- a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs +++ b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs @@ -361,6 +361,14 @@ impl BytecodeReadRafSumcheckProver { fn init_log_t_rounds(&mut self) { let int_poly = self.params.int_poly.final_sumcheck_claim(); + let staged_val_claims: [F; N_STAGES] = self + .params + .val_polys + .iter() + .map(MultilinearPolynomial::final_sumcheck_claim) + .collect::>() + .try_into() + .unwrap(); // We have a separate Val polynomial for each stage // Additionally, for stages 1 and 3 we have an Int polynomial for RAF @@ -389,6 +397,7 @@ impl BytecodeReadRafSumcheckProver { .unwrap(); self.bound_val_evals = Some(bound_val_evals); self.params.bound_val_polys = Some(bound_val_evals); + self.params.staged_val_claims = Some(staged_val_claims); let bound_f_entry = self.f_entry_expected.final_sumcheck_claim(); self.bound_f_entry = Some(bound_f_entry); @@ -704,7 +713,6 @@ impl SumcheckInstanceProver pub struct BytecodeReadRafAddressSumcheckProver { inner: BytecodeReadRafSumcheckProver, address_params: BytecodeReadRafAddressPhaseParams, - staged_val_polys: Option<[MultilinearPolynomial; N_STAGES]>, } impl BytecodeReadRafAddressSumcheckProver { @@ -713,16 +721,10 @@ impl BytecodeReadRafAddressSumcheckProver { trace: Arc>, bytecode_preprocessing: Arc, ) -> Self { - let staged_val_polys = if params.use_staged_val_claims { - Some(params.val_polys.clone()) - } else { - None - }; let address_params = BytecodeReadRafAddressPhaseParams::new(params.clone()); Self { inner: BytecodeReadRafSumcheckProver::initialize(params, trace, bytecode_preprocessing), address_params, - staged_val_polys, } } } @@ -786,16 +788,18 @@ impl SumcheckInstanceProver address_claim, ); if self.inner.params.use_staged_val_claims { - let staged_val_polys = self - .staged_val_polys + let staged_val_claims = self + .inner + .params + .staged_val_claims .as_ref() - .expect("staged val polynomials must be present in committed mode"); + .expect("staged val claims must be present in committed mode"); for stage in 0..N_STAGES { accumulator.append_virtual( VirtualPolynomial::BytecodeValStage(stage), SumcheckId::BytecodeReadRafAddressPhase, opening_point.clone(), - staged_val_polys[stage].evaluate(&opening_point.r), + staged_val_claims[stage], ); } } @@ -1426,6 +1430,8 @@ pub struct BytecodeReadRafSumcheckParams { pub r_cycles: [Vec; N_STAGES], /// Bound values after log_K rounds (set by prover for output_constraint_challenge_values) pub bound_val_polys: Option<[F; N_STAGES]>, + /// Val-only claims cached after Stage 6a address binding in committed mode. + pub staged_val_claims: Option<[F; N_STAGES]>, /// γ_entry = gamma_powers[7]. Weights the entry-point constraint term. pub entry_gamma: F, /// Bytecode table index of the ELF entry point. @@ -1576,6 +1582,7 @@ impl BytecodeReadRafSumcheckParams { int_poly, r_cycles, bound_val_polys: None, + staged_val_claims: None, bound_f_entry: None, } } diff --git a/jolt-core/src/zkvm/claim_reductions/bytecode.rs b/jolt-core/src/zkvm/claim_reductions/bytecode.rs index bf954e6e4a..f5d57121ea 100644 --- a/jolt-core/src/zkvm/claim_reductions/bytecode.rs +++ b/jolt-core/src/zkvm/claim_reductions/bytecode.rs @@ -310,34 +310,31 @@ impl BytecodeClaimReductionProver { pub fn initialize( params: BytecodeClaimReductionParams, - raw_chunk_polys: &[MultilinearPolynomial], + raw_chunk_coeffs: &[Vec], ) -> Self { let eq_cycle = EqPolynomial::::evals(¶ms.r_bc.r); - let eq_coeffs_template: Vec = (0..raw_chunk_polys[0].len()) + let eq_coeffs_template: Vec = (0..raw_chunk_coeffs[0].len()) .map(|idx| { let (lane, cycle) = native_index_to_lane_cycle(¶ms, idx); params.lane_weights[lane] * eq_cycle[cycle] }) .collect(); - let raw_value_coeffs: Vec = (0..raw_chunk_polys[0].len()) + let raw_value_coeffs: Vec = (0..raw_chunk_coeffs[0].len()) .into_par_iter() .map(|idx| { - raw_chunk_polys + raw_chunk_coeffs .iter() .zip(params.chunk_rbc_weights.iter()) - .map(|(poly, weight)| poly.get_coeff(idx) * *weight) + .map(|(coeffs, weight)| coeffs[idx] * *weight) .sum::() }) .collect(); - let mut coeffs_by_poly = Vec::with_capacity(2 + raw_chunk_polys.len()); + let mut coeffs_by_poly = Vec::with_capacity(2 + raw_chunk_coeffs.len()); coeffs_by_poly.push(raw_value_coeffs); coeffs_by_poly.push(eq_coeffs_template); - for raw_chunk_poly in raw_chunk_polys.iter() { - let raw_chunk_coeffs: Vec = (0..raw_chunk_poly.len()) - .map(|idx| raw_chunk_poly.get_coeff(idx)) - .collect(); - coeffs_by_poly.push(raw_chunk_coeffs); + for coeffs in raw_chunk_coeffs.iter() { + coeffs_by_poly.push(coeffs.clone()); } let mut permuted_polys = permute_precommitted_polys(coeffs_by_poly, ¶ms.precommitted).into_iter(); diff --git a/jolt-core/src/zkvm/claim_reductions/mod.rs b/jolt-core/src/zkvm/claim_reductions/mod.rs index 787f5d6efa..15055ba7a4 100644 --- a/jolt-core/src/zkvm/claim_reductions/mod.rs +++ b/jolt-core/src/zkvm/claim_reductions/mod.rs @@ -30,7 +30,8 @@ pub use instruction_lookups::{ pub use precommitted::{ permute_precommitted_polys, precommitted_eq_evals_with_scaling, precommitted_skip_round_scale, PrecomittedParams, PrecomittedProver, PrecommittedClaimReduction, PrecommittedEmbeddingMode, - PrecommittedPhase, PrecommittedSchedulingReference, TWO_PHASE_DEGREE_BOUND, + PrecommittedPhase, PrecommittedPolynomial, PrecommittedSchedulingReference, + TWO_PHASE_DEGREE_BOUND, }; pub use program_image::{ ProgramImageClaimReductionParams, ProgramImageClaimReductionProver, diff --git a/jolt-core/src/zkvm/claim_reductions/precommitted.rs b/jolt-core/src/zkvm/claim_reductions/precommitted.rs index 571bab226c..aa935859ff 100644 --- a/jolt-core/src/zkvm/claim_reductions/precommitted.rs +++ b/jolt-core/src/zkvm/claim_reductions/precommitted.rs @@ -1,5 +1,6 @@ use allocative::Allocative; use rayon::prelude::*; +use std::sync::Arc; use crate::field::JoltField; use crate::poly::commitment::dory::{DoryGlobals, DoryLayout}; @@ -9,6 +10,33 @@ use crate::poly::opening_proof::{OpeningPoint, BIG_ENDIAN, LITTLE_ENDIAN}; use crate::poly::unipoly::UniPoly; use crate::subprotocols::sumcheck_verifier::SumcheckInstanceParams; use crate::utils::math::Math; +use crate::zkvm::bytecode::chunks::committed_lanes; + +#[derive(Clone, Debug)] +pub enum PrecommittedPolynomial { + Dense(MultilinearPolynomial), + BytecodeChunk { + chunk_index: usize, + chunk_cycle_len: usize, + }, + ProgramImage { + words: Arc>, + start_index: usize, + padded_len: usize, + }, +} + +impl PrecommittedPolynomial { + pub(crate) fn original_len(&self) -> usize { + match self { + Self::Dense(poly) => poly.original_len(), + Self::BytecodeChunk { + chunk_cycle_len, .. + } => committed_lanes() * *chunk_cycle_len, + Self::ProgramImage { padded_len, .. } => *padded_len, + } + } +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Allocative)] pub enum PrecommittedEmbeddingMode { diff --git a/jolt-core/src/zkvm/claim_reductions/program_image.rs b/jolt-core/src/zkvm/claim_reductions/program_image.rs index 951afce066..9db19cd67e 100644 --- a/jolt-core/src/zkvm/claim_reductions/program_image.rs +++ b/jolt-core/src/zkvm/claim_reductions/program_image.rs @@ -276,33 +276,23 @@ fn top_left_program_image_point_and_selector( padded_len_words.is_power_of_two() && padded_len_words > 0, "padded_len_words must be a non-zero power of two" ); - assert_eq!( - start_index % padded_len_words, - 0, - "program-image block must be aligned to padded_len_words for top-left embedding" - ); - let m = padded_len_words.log_2(); assert!( m <= r_addr.len(), "program-image variable count exceeds RAM address variable count" ); + assert!( + start_index < padded_len_words, + "committed program-image domain must cover the bytecode start index" + ); let prefix_len = r_addr.len() - m; - let start_prefix = start_index / padded_len_words; - - let mut selector = F::one(); - for (i, r_i) in r_addr[..prefix_len].iter().enumerate() { - let bit_index = prefix_len - 1 - i; - let prefix_bit = (start_prefix >> bit_index) & 1; - let r_i_f: F = (*r_i).into(); - selector *= if prefix_bit == 1 { - r_i_f - } else { - F::one() - r_i_f - }; - } - (r_addr[prefix_len..].to_vec(), selector) + // The committed program-image polynomial is shifted into the RAM-relative domain + // before commitment, so it is always embedded at the top-left corner of RAM. + ( + r_addr[prefix_len..].to_vec(), + EqPolynomial::zero_selector(&r_addr[..prefix_len]), + ) } impl ProgramImageClaimReductionProver { diff --git a/jolt-core/src/zkvm/program.rs b/jolt-core/src/zkvm/program.rs index c10c18bd18..efe0665263 100644 --- a/jolt-core/src/zkvm/program.rs +++ b/jolt-core/src/zkvm/program.rs @@ -11,7 +11,8 @@ use crate::poly::multilinear_polynomial::MultilinearPolynomial; use crate::utils::errors::ProofVerifyError; use crate::utils::math::Math; use crate::zkvm::bytecode::BytecodePreprocessing; -use crate::zkvm::ram::RAMPreprocessing; +use crate::zkvm::ram::{remap_address, RAMPreprocessing}; +use common::jolt_device::MemoryLayout; use tracer::instruction::{Cycle, Instruction}; #[derive(Debug, Clone, CanonicalSerialize, CanonicalDeserialize)] @@ -57,6 +58,15 @@ impl ProgramPreprocessing { self.program_image_len_words().next_power_of_two().max(2) } + pub fn committed_program_image_start_index(&self, memory_layout: &MemoryLayout) -> usize { + self.meta() + .committed_program_image_start_index(memory_layout) + } + + pub fn committed_program_image_num_words(&self, memory_layout: &MemoryLayout) -> usize { + self.meta().committed_program_image_num_words(memory_layout) + } + pub fn meta(&self) -> ProgramMetadata { ProgramMetadata { entry_address: self.bytecode.entry_address, @@ -97,6 +107,17 @@ impl ProgramMetadata { pub fn program_image_len_words_padded(&self) -> usize { self.program_image_len_words.next_power_of_two().max(2) } + + pub fn committed_program_image_start_index(&self, memory_layout: &MemoryLayout) -> usize { + remap_address(self.min_bytecode_address, memory_layout).unwrap_or(0) as usize + } + + pub fn committed_program_image_num_words(&self, memory_layout: &MemoryLayout) -> usize { + let start_index = self.committed_program_image_start_index(memory_layout); + (start_index + self.program_image_len_words.max(1)) + .next_power_of_two() + .max(2) + } } #[derive(Clone, Debug, PartialEq, CanonicalSerialize, CanonicalDeserialize)] @@ -162,16 +183,20 @@ impl TrustedProgramCommitments { #[tracing::instrument(skip_all, name = "TrustedProgramCommitments::derive")] pub fn derive( program: &ProgramPreprocessing, + memory_layout: &MemoryLayout, generators: &PCS::ProverSetup, ) -> (Self, TrustedProgramHints) { - let program_image_num_words = program.program_image_len_words_padded(); + let program_image_num_words = program.committed_program_image_num_words(memory_layout); let (program_image_sigma, _) = crate::poly::commitment::dory::DoryGlobals::balanced_sigma_nu( program_image_num_words.log_2(), ); let program_image_num_columns = 1usize << program_image_sigma; - let program_image_poly = - build_program_image_polynomial_padded::(program, program_image_num_words); + let program_image_poly = build_program_image_polynomial_padded::( + program, + memory_layout, + program_image_num_words, + ); let _program_image_guard = DoryGlobals::initialize_context( 1, program_image_num_words, @@ -194,17 +219,31 @@ impl TrustedProgramCommitments { } } -pub(crate) fn build_program_image_polynomial_padded( +pub(crate) fn build_program_image_words_padded( program: &ProgramPreprocessing, + memory_layout: &MemoryLayout, padded_len: usize, -) -> MultilinearPolynomial { +) -> Vec { debug_assert!(padded_len.is_power_of_two()); - debug_assert!(padded_len >= program.ram.bytecode_words.len()); + let start_index = program.committed_program_image_start_index(memory_layout); + debug_assert!(padded_len >= start_index + program.ram.bytecode_words.len().max(1)); let mut coeffs = vec![0u64; padded_len]; for (i, &word) in program.ram.bytecode_words.iter().enumerate() { - coeffs[i] = word; + coeffs[start_index + i] = word; } - MultilinearPolynomial::from(coeffs) + coeffs +} + +pub(crate) fn build_program_image_polynomial_padded( + program: &ProgramPreprocessing, + memory_layout: &MemoryLayout, + padded_len: usize, +) -> MultilinearPolynomial { + MultilinearPolynomial::from(build_program_image_words_padded( + program, + memory_layout, + padded_len, + )) } #[derive(Debug, Clone)] diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 333069faf3..fae263ef8f 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -22,7 +22,10 @@ use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; #[cfg(feature = "zk")] use crate::zkvm::config::ProgramMode; use crate::zkvm::config::ReadWriteConfig; -use crate::zkvm::program::{ProgramPreprocessing, TrustedProgramCommitments, TrustedProgramHints}; +use crate::zkvm::program::{ + build_program_image_words_padded, ProgramPreprocessing, TrustedProgramCommitments, + TrustedProgramHints, +}; use crate::zkvm::ram::remap_address; use crate::zkvm::verifier::JoltSharedPreprocessing; use crate::zkvm::Serializable; @@ -44,7 +47,8 @@ use crate::{ compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, SumcheckId, BIG_ENDIAN, }, - rlc_polynomial::{RLCStreamingData, TraceSource}, + rlc_polynomial::{RLCStreamingData, TraceSource, + }, }, pprof_scope, subprotocols::{ @@ -61,8 +65,9 @@ use crate::{ utils::{math::Math, thread::drop_in_background_thread}, zkvm::{ bytecode::{ - chunks::build_committed_bytecode_chunk_polynomials, - read_raf_checking::BytecodeReadRafSumcheckParams, TrustedBytecodeCommitments, + chunks::{build_committed_bytecode_chunk_coeffs, committed_bytecode_chunk_cycle_len}, + read_raf_checking::BytecodeReadRafSumcheckParams, + TrustedBytecodeCommitments, }, claim_reductions::{ AdviceClaimReductionParams, AdviceClaimReductionProver, AdviceKind, @@ -71,9 +76,9 @@ use crate::{ IncClaimReductionSumcheckParams, IncClaimReductionSumcheckProver, InstructionLookupsClaimReductionSumcheckParams, InstructionLookupsClaimReductionSumcheckProver, PrecommittedClaimReduction, - ProgramImageClaimReductionParams, ProgramImageClaimReductionProver, RaReductionParams, - RamRaClaimReductionSumcheckProver, RegistersClaimReductionSumcheckParams, - RegistersClaimReductionSumcheckProver, + PrecommittedPolynomial, ProgramImageClaimReductionParams, + ProgramImageClaimReductionProver, RaReductionParams, RamRaClaimReductionSumcheckProver, + RegistersClaimReductionSumcheckParams, RegistersClaimReductionSumcheckProver, }, config::OneHotParams, instruction_lookups::{ @@ -1443,25 +1448,24 @@ impl< &self.opening_accumulator, &mut self.transcript, ); - let bytecode_chunk_polys = build_committed_bytecode_chunk_polynomials( + let bytecode_chunk_coeffs = build_committed_bytecode_chunk_coeffs( &self.preprocessing.program.bytecode.bytecode, bytecode_chunk_count, ); self.bytecode_reduction_prover = Some(BytecodeClaimReductionProver::initialize( bytecode_reduction_params, - &bytecode_chunk_polys, + &bytecode_chunk_coeffs, )); let padded_len_words = self .preprocessing .program - .ram - .bytecode_words - .len() - .max(1) - .next_power_of_two(); - let mut program_image_words = self.preprocessing.program.ram.bytecode_words.clone(); - program_image_words.resize(padded_len_words, 0); + .committed_program_image_num_words(&self.program_io.memory_layout); + let program_image_words = build_program_image_words_padded( + &self.preprocessing.program, + &self.program_io.memory_layout, + padded_len_words, + ); let program_image_reduction_params = ProgramImageClaimReductionParams::new( &self.program_io, self.preprocessing.shared.program_meta.min_bytecode_address, @@ -2128,7 +2132,7 @@ impl< let mut polynomial_claims = Vec::new(); let mut scaling_factors = Vec::new(); - let mut precommitted_polys: HashMap> = + let mut precommitted_polys: HashMap> = HashMap::new(); let (ram_inc_point, ram_inc_claim) = @@ -2224,11 +2228,11 @@ impl< if self.preprocessing.is_committed_mode() { let chunk_count = self.preprocessing.shared.bytecode_chunk_count; - let bytecode_chunks = build_committed_bytecode_chunk_polynomials::( - &self.preprocessing.program.bytecode.bytecode, + let chunk_cycle_len = committed_bytecode_chunk_cycle_len( + self.preprocessing.program.bytecode.bytecode.len(), chunk_count, ); - for (chunk_idx, poly) in bytecode_chunks.into_iter().enumerate() { + for chunk_idx in 0..chunk_count { let (chunk_point, chunk_claim) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::BytecodeChunk(chunk_idx), @@ -2241,18 +2245,25 @@ impl< chunk_claim * lagrange_factor, )); scaling_factors.push(lagrange_factor); - precommitted_polys.insert(CommittedPolynomial::BytecodeChunk(chunk_idx), poly); + precommitted_polys.insert( + CommittedPolynomial::BytecodeChunk(chunk_idx), + PrecommittedPolynomial::BytecodeChunk { + chunk_index: chunk_idx, + chunk_cycle_len, + }, + ); } } if self.preprocessing.is_committed_mode() { - let mut program_image_words = self.preprocessing.program.ram.bytecode_words.clone(); - if program_image_words.is_empty() { - program_image_words.push(0); - } - let padded_len = program_image_words.len().next_power_of_two().max(2); - program_image_words.resize(padded_len, 0); - let program_image_poly = MultilinearPolynomial::from(program_image_words); + let padded_len = self + .preprocessing + .program + .committed_program_image_num_words(&self.program_io.memory_layout); + let start_index = self + .preprocessing + .program + .committed_program_image_start_index(&self.program_io.memory_layout); let (program_point, program_claim) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::ProgramImageInit, @@ -2264,7 +2275,14 @@ impl< program_claim * lagrange_factor, )); scaling_factors.push(lagrange_factor); - precommitted_polys.insert(CommittedPolynomial::ProgramImageInit, program_image_poly); + precommitted_polys.insert( + CommittedPolynomial::ProgramImageInit, + PrecommittedPolynomial::ProgramImage { + words: Arc::new(self.preprocessing.program.ram.bytecode_words.clone()), + start_index, + padded_len, + }, + ); } // 2. Sample gamma and compute powers for RLC @@ -2313,10 +2331,16 @@ impl< // Add advice polynomials to precommitted polynomials map for RLC. if let Some(poly) = self.advice.trusted_advice_polynomial.take() { - precommitted_polys.insert(CommittedPolynomial::TrustedAdvice, poly); + precommitted_polys.insert( + CommittedPolynomial::TrustedAdvice, + PrecommittedPolynomial::Dense(poly), + ); } if let Some(poly) = self.advice.untrusted_advice_polynomial.take() { - precommitted_polys.insert(CommittedPolynomial::UntrustedAdvice, poly); + precommitted_polys.insert( + CommittedPolynomial::UntrustedAdvice, + PrecommittedPolynomial::Dense(poly), + ); } // Build streaming RLC polynomial directly (no witness poly regeneration!) @@ -2550,7 +2574,7 @@ where shared.bytecode_chunk_count, ); let (program_commitments, program_hints) = - TrustedProgramCommitments::derive(&program, &generators); + TrustedProgramCommitments::derive(&program, &shared.memory_layout, &generators); JoltProverPreprocessing { generators, diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index 80e3d33f55..b0951f373a 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -1276,9 +1276,7 @@ impl< .preprocessing .shared .program_meta - .program_image_len_words - .max(1) - .next_power_of_two(); + .committed_program_image_num_words(&self.program_io.memory_layout); let program_image_reduction_params = ProgramImageClaimReductionParams::new( &self.program_io, self.preprocessing.shared.program_meta.min_bytecode_address, @@ -2120,6 +2118,12 @@ impl JoltSharedPreprocessing { self.program_meta.program_image_len_words } + #[inline] + pub fn committed_program_image_num_words(&self) -> usize { + self.program_meta + .committed_program_image_num_words(&self.memory_layout) + } + #[inline] pub(crate) fn precommitted_candidate_total_vars( &self, @@ -2152,12 +2156,7 @@ impl JoltSharedPreprocessing { .next_power_of_two() .log_2(); candidates.push(committed_lanes().log_2() + chunk_cycle_log_t); - candidates.push( - self.program_image_len_words() - .max(1) - .next_power_of_two() - .log_2(), - ); + candidates.push(self.committed_program_image_num_words().log_2()); } candidates diff --git a/jolt-sdk/Cargo.toml b/jolt-sdk/Cargo.toml index 181fe4a6e3..82dde4aa7b 100644 --- a/jolt-sdk/Cargo.toml +++ b/jolt-sdk/Cargo.toml @@ -100,4 +100,4 @@ zeroos = { workspace = true, default-features = false, features = ["arch-riscv", zeroos = { workspace = true, default-features = false, features = ["runtime-nostd", "panic"]} [target.'cfg(all(target_arch = "riscv64", target_os = "linux"))'.dependencies] -zeroos = { workspace = true, default-features = false, features = ["os-linux"]} \ No newline at end of file +zeroos = { workspace = true, default-features = false, features = ["os-linux"]} diff --git a/jolt-sdk/tests/fixtures/fib_io_device.bin b/jolt-sdk/tests/fixtures/fib_io_device.bin new file mode 100644 index 0000000000000000000000000000000000000000..e8980c42e543865df8a03142f7a16fd62eeb6bf6 GIT binary patch literal 199 zcmZQ%fB+*X4X0S4tcRb@eAHv`P=j&>pmf9kdN6AOl!l4J_zTd)Ve&BX4N!R)eE?NG X(7pz+`4E!f#5O1&s*VFn3qWZAVf-KC literal 0 HcmV?d00001 diff --git a/jolt-sdk/tests/fixtures/fib_proof.bin b/jolt-sdk/tests/fixtures/fib_proof.bin new file mode 100644 index 0000000000000000000000000000000000000000..1adb0b928e3b3e14bb946f0c30ea5486b8ae3d76 GIT binary patch literal 70500 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

?q=w^9CgwxPN!ksCMA97z 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%0A8MJ!T!F`5i z)*+~}wo#R8`{-R2`ZtmRm#3ARoS81lH-aWv zm)Wk9*@P;AYAla1?SzwpPrg~n~Q{AKj(WEk2dK*E1upXcZd5Ery}Y`7qru6HEw@4 zbksX};&l`jF_~o#pWOrIH`L8N@=bwM=B_+p&i8lXHMWtsH|m8AEm53QFm*MlP!qFz zqe?`T_>pM$WXQY_nffTmIEKKt9|wczt0&hThJ>u5{eGYQ#Ygqhv-Doo?sEuuukq4% z*DfkTb7Lp}xvwCsAf?%K0Yjy5^PIu>$5EHh%9;ce8}_ycm=GtEKVMKYh_rT}$jc%3 z-^EB`x-+yz*G5*g=#JAPDr`BC=LL?AZJ(S|?;A9;GjWPX9>2SPvQYPc4M*L}7ED$K z(FH%Y>{7LZwmV|)T%$0*FW}pukfGijNL=9bW`eeV^bV^LpN^3JsNnq|su>RX0X_-q z)<;d^1vxunhph?eNSJlnVbOyNUsQC=wCb!g$g)g{b9iHFE;c;W z7;^R*b!X0Kvmmc($y*3pU{5$dcJI>&pKrzJ6;7ev8|Tj+D)S~(9^<_x$s;>=?w#Yo zX!X4xWZ5k-PZXbHu9E)7BYA-mhwEGP#JQuPr(GLgSs2Bq(25Yp^Vud{m5w;nmYU6@ z@^!AGe3WKXZ;N8LmtdAo&t+>u7lOK2xm{OY)np#KskT+t$VAhWPoPCzjFd%jj~j8W zP-`8xi_hulDonq``B{1&Y#p_C?S9-P+$?x%v0_IbalSe!L?EZke6#ZkKKE&1)<}%- z$i@1azM;I~L}@zQ!Yw_qLIc64^=^U|xgtd@6q&FdnugyQ^e2L(qm2U-86vsr!v!{n+rfg37ye?$ZAdU4?V{~GDsAX}f z@K6;PM09utH!#g@5AF23z6a0U6onbi7AY2l2RASu-+q6NJSNd$@1T7MiRqz5))#y) zaFC0|8Th0+X5=wig;!A8{ z-x^}Tm$0Ao_z{Wb%+P22OayT|>u2^8CtR@pZ0FwTHxfNU*brFcS4(*OHm%>uY~t}J z!grobeFfc$^K)%2exsFhzM^y2&+V`(hdB&gDG#{wJ4hXOnv#^bXk7I9hh6DKc*&d4Z)81B33thr;z%thuy zoJ)q3(HRhF)o0r)mF%t+z-i zXhUR;i;wQ_Q>xgP5i`AOgky-ckg-uoMvs^E)hMPoE!i!7Qd_h0=GD4fG@&A`u@@9d zs1F3}cF3`GC@WIoT42%E2{0#cQn(nAs`8ma>sSv38IHMk2SzHZns8*(wmP=v*9_yi zC#$6opUtz_^WktZjdEPdT#tZ&_gRcWS?O}>dovafu#ZvoeV`0bi^*838zE}f9}^ns z-Cf?{i-A`s7+}lAWcY^r{5H?~vnp6A6&5lRX%fi_w;I`E1l-c9$OfY>XYGuxBoxa$ zl~}pGc`i4y8wZr*BA;@ru{7`DZcbuW$3Q8r8_W%cVpA`2p6Jrg?E=+n?r39k4q?4{ z_2|}&VlFaMvHp_4>!5ggem9lf;nbZ|(JJAFUZ328Z-2u5R|kSsVm9{m-#pO2d7yvu zK>y}}{>=mZn+N(g5A<&y=-)ihzj>g4^FaURf&R?{{hJ5+HxKl09_ZgZ(8kth{;R(o z=ju7he%rHekB%l`y2}}K2)gvp6h1QDt9fMR{ zSMjr+o8Hjoiwd5*)YyC(G8kGMyq>+1^=>wb zOo81Xn>J8{ue&ygrB^&~yP);s#BbgK@;T-gjCau6MVwaE7U41FCdz1KQN@_XZ+mpw zPt`$dQd-$I`@RYPoH%NK8%l zcei|guID!?U%Ak{Mb6-SHbJF~*ex*2QY=4a1Wf|F1LgVM!dDqSUWl|vjAD(DpT=mo z)ci!H;!Tk0jS;FgW~)~N>a1n9OI0=$v<{=C3iqznsu135534F{nqeSjRrJEVzaQl5 zhc_`?X&27ctdht^o)FyoiYPR2$#{4>_!z~bm@PA4Te~e(MQ>tw+6L|$D&yjZX)9{X z-j;CSO-lLDNLhJ%=vJ9|_lux|-bu$7E|a?#=Tmuikew$i+OqS-;cJ583AOUl(q*c1 z2`?!)9C29_&R)MLtZP6p7A4q}XltohQFiay_LmJGc*F;u*(6tC2uWm)U4N)7(PD_# zQ6edg@zinTPS?rlk&lPdsU&F}?$yM+d4L~%Hl~1`dGBlm3+GtkHo51;A=m}l4Oc`( zLI$P6FPx8(zfC`~o2TY=_TmvIVcXI1orhBH+F&vU)$z`dxa|F0%qK~FfROBy49%dN z%1D}|XUW4uH9LA$>qB26I&Zc4LgcwE4} zM?!EXnAkawc-KKE4yTT`!Zx}2uSThKx^B!|Nfz~0H}|nIJ{X@XVp+UDcm4K^@36_~ zD19B)oA;wLZ^wz!V^=R`Wl9x4wh~Of_e|TUkj5{LWgnAOMrlCYog|}hswLldHSb0C z?o_4n^>|tI@(D}Jo6y>&;{vJ5-7ZU1Asj_dPKvrvyccDn9>O4XPts-esV{Hs&7yqC z-*RR5Vaz8iM4oqXPx+qcBFlfu(CgXc)43}uu|=K=KgdZhOazM$H@kMq4TjW1gdSK0AngOnuJG8S7es zLGZzCmY1x*y|%*4o}4&z*ngHT2KRo+Cm9}4C?S~Y(V1)|%TMlH!x9w$VX`T#N#nPX{k7!WBmwWIo7dcreTF z+Ow+1jOLh{LdLr-TEMQ!rrdk{>V|W0UJ#L8YPx>rpkc=T65>ang${m`DG{nu-=*&@ zA1{b`q@O;ck~Y|&Md{f~lTg}{z8bZo447nrbXIowk~pgW?jyHPvh9D z_Y)q_63VaV{&ar+q0Yd0AJMit*7RoI3cjxM^zqa9A{HJph3=2K$Lyx6Tx$8Z^K}Rv zC5tDU*JQ|gQDaX?QL8?TnNZk~nx9H2zTnQl9dZ2L@vVk0o@y60m3V*jW%$%^Tf=I2 zXUsjF4=mdcr3h5EhBdi7_P(d>%{zMhc`W}+E^%rzo!_9d;O@;BB#D<2&P7M z249@Cp1iQY>9U3cGYXbQuj}7?3b$%8y;K{d87`5*A26m4DQoR_%;lO3a}F&Vka)kf zhO_zp+kq*PdOwT=lVFS;+YZ}1&0^daoF%FC#ZBLNZ?t?P1DmFHxGoXdP};%y z4{GOlrtPn4({d38lz5^29BhV@X&IAc%h_Il8n>pQktUGOy2CRNiczBDGAxh1`4l=gX+%oX};wD)nX<~{ha zqdw8R+!Jrga!)Ug;no$6&y|$Ew{lHsW0Lb34Z;;H!e8k1a>0QT*xmy~V=y zBQL6Rx1YnkmCteh@%dWgY4N8Ao_+y!;we0|QGWe?asPt$!3Lfy-StkKla!n%Z3_+! z4RU>ZWrius>}|Vklu7JV z%W!Z`&rr87;>$haO!_3UQ<&Yt4Uq>8m>swYD&8mN<@%(ljnn0(V5VDOkKpSb&(wZo zPUz(JQpBfonEwe~nlvD}VgB1i6DLgMyUe&%9H^6?Od^Acr?NPWxT-ozU zw(_LT;aBwx`JwaADr8yPW@#lI7UflkdF0Y7Zj0V|s8>gAW@Np9F=3#@bF1AX^p;lk z9%u2iy)L_+*fsZG9g;p1a5?f`CAkqzK~I)DCq4yk@$FfK;-dc`9q{5>DO+|$+6$3$Oa$Ve){>z zM29L2Qia$E*1pT8Y&?!sdI5zr*2KqI4l`|_|m28?oqMZXxA7EqFEuM#m*OB4|)$$X}C5)6aOefj$>w?0ETS5-BW;8$`Qynm?qTi)7z*!NgMKL;|S7 z$fp4OjZ$Xh4$JV?ot#I?Wtf97KMIEu#6;4hYn6Bi?V9vD>p21z9;W8YV9KeK*+O)5 z_I0?2b^hnVfIjn@#IG?g4&>mCy`CXvy0ueWDJqUFHAsP+dj3Voz>fPu(?wNX?BndT zoxaUfsVdCcbEf;g6?drxIW{))G%yYxWd<(=hIF@_R#Lu8{&g!uO7ls4h*V`S7>8XxoXb0 zedioifQ1|;_~wl>BI->pp^XN^TggNC4qa_i&rdU=rD3^rF+x^ZIJe@Y_M!P?#~dt9 zef!h>{-O5XGab4D*r#@e<>E{p;Ida_7i7-l5`R_xNr^;!&nf$sH_4p)MTo`3&)!Oi zV?K2jJGrJf{d2v*+196K?V(3H-i+o<^%Jq?EE)UsifKEjsw<6$o!j#w|5~0;3I0{n zFRvLex18g{wJEqd*0F_ncwnGwG0f9Ik+`ktRH_9539D{_SkH(OMa+%n zMayj`54Ku)RqE6Sx0Myg1cb!i7s#nqGC8jr)AprXx{Op%lB2=d^6J%T$1YkO)@xzS zHHrE%5+hfhdb?{Jp42ks(6I`0Xn5MVO)Tc{gS#5eUtA;ZraF9iSZ$o{?W3;TyEVDd z*HZm$;85-n=Zku3R-Cu629HJGu%mr`PDR0=Y?e-ED$iW0OOoG|Li#l?Gg0kQYI#8| zmG485`?qiQo+;)@h)*UhA|CpD?nAK;^AP)6#iqLbgu>m96~VP4jXjoaaH zR35l0fR!a7#XB~1!{*bBGmGEV=Er-ZSq_B9c0N4%lxmp9OeRQU)|YCx#Kp9jLi4Pu zkHvE>f~=CWt(XCZAN0p5EgZJf-tX|aQ@wbjyj%>_pT^(jk%IN1mhcsIpyazg3A zd?+nVFz_jJT-IAGG;8{<#;laM$@?)cv+#T?gEPYTLgiAlLR5CiM5BG*D(x*xdTtN# zM+k3M-Vw4p`Irr18}|Dx-}D(ca2Y=1edyjlL^k$}ua}AXYkcgHRu{p@`bXSJ>MgJF zedwP|l)t`3N!VI5MLP0GYBB1gN&X3jR;(PldC@9CGM{!kO5%xwu2S{)4Z8*?TmAc| z(jj9Tau;LB6C}!w#(Gk*`e!*m9jj?j_`K76M6KwXNG8Lb!GfrV540yDy zM>YCm)8yU>dYRomH+3G!Sag4!Y{vANYRP2eN_jZQ4bpt;mw}OSDIAZ4*w3n6qx;nC zrNg9y=4$j3+PYjfB1%M9wHP(ou+&Ds<&+zR9lNJLLRDJQCg4nBZ_x zBrsFiKOq&bOlXvBlS4n@r+bpchg8P}{~J-5LaC^}PBPW3B*As==-Sq#Fud6Nc!%Gs z3D7JJGKYI~UN&hCy*Inq*r|KPs_=tcy6}Z@)#tk%6u9HL9^#3UrME~YONR1Nne;mE zNJ%}jXclL8<^mUp(T`A|#f*tYQc+w)f2N<-IMl52qw$Vgu1Q^O3RG@5`QN7QO+R7V z?$LtjbT}Uv!l+GWwr&>%tp41X2JQIm*aAl3vVPot) zg9~nREF9gU3Nm{01T>~TDw(R(^O)iw8&+OwycJ$jJJt4tg{)?*)4ltf35PCsd}?uK zIVWLf4GG_3XZb?MM4Fb7t9!mB4EVqNNJb0B!c}Q6yJKjWdydzYLP0r-zY7m^_R&nB ze>T$=wo4S05*{<=7GLN_8@IAK+++{<6#7=kD(EhLK;M%*8P*opTx?n`xKo z9lEjgod+Hfe>cH&W@>*)teYA2vE&3lke^1ggN@!;^HC|;)2+<>B->a@vy5lH8EtF5 zYbs-14mm|)rze+%{i^LNeVJUFF3CxMMV6O!uJ5C<-7J$l6NK$oY%0DhukSSIQBg@z`Guhm33W2jQKU?P1Va}5tjKwj>jK&5Q(=ob5z!q zjhkpp`E4(~vekzrvm1PA^peZOHGwxFU!$cOrWilwP;wQ7b8r>v^U8)O~%D|8f zP$Osh5P0W`w*^g6;7G*e4MG(wOz!^H>nEN(vBAm@lCFAdyJY^}r24&(lkqLKi3?q# z*3MfV9vrH;i$lg+q_nSq)vs0|`rPM+ZPA<*7RnLeLoz#3qBJ><$UaivNfB&qPCyTTC=h`xvI)IOj+;Z##=ffb-(V zCI_N|YVC_Pwbgt}T+#im2Z|UT3BTo^r>Cc&{GcsxY9!Y;ik0gmak-Bo!ZXmi)M@JZ$wPKq`a8~qU7?Gfc9_BVs86QLA;HEi z#rEM^;Q5(;W{}}ECP{ib>qx6uaq4Q-viDg>$g?D)Yme@e>fkSJ5wwuc(va&I`Dh@^ zwj1A2Mf3<6K~S9fM07=gA=cs1;$RC`&KD8CQebyXL&0S_gD%F1R)d!>ju42wlBSLQRD6_Q zAZ+L2`9T|@mIEl0?965CN2?yO6 ztv}LgczID@A8bb;vfn?rkeamKcIalDmCcR4qwEF~sw{$w2`9dNJ@H(8zZwVQgkOyh z<*xb%${&^Ahs2FVW-O!$yQ7 z601AM#bSHp^R;V?)6(n)C)tw>eJ@;1-Aj6w$KY1d*warXpN|TJL)z<2NrpDN-S&3% z?k;K(zv>$_A&OVcHs9GeidW1+yE-rx_WkYhYaY(`uh>>zRe8RDP4WFTZ0YdwyOi&* zu+L#%pIh@e?EA3K3%`FK_FD7v&qMycu;o90<^LE>W#7O4{&NK1UnyX}hp}=))(`h& zu`T~T2FCYSyzgIfXt`^9a2)^sRS<`wp_+~ohfENMwVj=py|*`q3>YLPDJCT4%RN-_RHV2b$9iE{=N2)KYjVrkdSMu z;CAm;1H>i{2E^v&4f+A2l$e-=q?nk9q^P8bw79s4l#~d%Ag{-s;u46MA^!~Y+w(MP zdrF7^Vq3N+9HS)ozi_!05|>Metpnm3mxClk(B|^=RjVQmAqjvG0)2P66i6D3uI0EI zL355K(0~ANw4)TMtMv@Fjf&pAC3d0C7=DPF&bTibSI3L4(D~2v{Z~`72kl+Q(^CU$9 z!b21mA5g65^X*AEIwYYh1`rtt)LI7=*s$ z93?_2pfG`{msNydwCyCQ5dy`DB!ZXGR~h97KgsN(3NMpy|mPwvj+vvVYiVZVXb>=oN1#EFDB1)A>I$Y5P#zC&JwVL6 zt`ZzXru}|-6?K6S7~FUNLJNiK0taCvU~s=2Q(0Ym$jX<1=-}ncbvH*Kl>P_sR*i?O zsU#%V_KM+8BZ0gBQ?Y-bJ4%MCo3jLlN+N9fl8Wnh+&CpmGen zW(k9bHc7au`H`$Xpko_QA^7w1L!Tl<4GR3@N`MHJ4=6wc4n&_AL=6xiNLG7@Fc+gw3$ChF zBs7Ci`+!;yUe$*IM57c$jSwhGAZGnJjM1nBQ6mHj6L@7GZbS45L6pl7@NfnK6@t(( zma?`$gRH7A2tg3LEr-d5;}a7> zD9=EM20lTEZhWYZvp@g>BufyQL6cvi*M~3{ShCmHjEAtv0i*@$?->g@bWquWY{|K0aoPT>knqY zjf)S2A*mnj2QRIoArV-w{eBf6=q#+S7vHbq18Usm07gS9x?X(0iVtW)wTur9x%@iu zAq<%PwEiH@U&e=qV#s>&{c8V!rc*2U&@T^>>~9F;Z$H@&CL+*Qi(Ac4kSYe~2*uiB zD`ZhFt5;Zj{1^EN1R%iKSTPZx@jQ67YdH;oDh1KMKJdME0V2Z(SgeK_5RMOoCVZXa1HCYlhuSr^!>Yj+$dqb~#gkKn zIv@faord?Q;1JPwi0lV98v?IFz_ZhEWD+0|2@qHkeS`n&vEkYT8rnvKg!eLkL#v_7 zD-jI|)^|6jz)2EPM+PJtptIBPjRkF)^ZNH`pnKO=X4s*nSXc^;a@+=LYCx`rQp)o8 z0Pdz)rD|l!0m6VYw0i;D0ax-s2xp_=Zlv%2UEQORP~rMIeCT3o)bQ38=zi?}Kn=MZ zAE;E=pKc_p0gR+Rg3#Q9?!?R4GsLU#`lb*XXZcV;1R?a#euM}wVGE(L4~`n*S||Sq z5z2e2~>S`Yz3 z_ve)gD2yRd5jdJkC&1`nOAsz25W;4oN+&4ruGgCADxCnsgE<>+&J8Y|tP>@wO&iUn z6JV5^rE~&}4jwqmZ#F^c1Q6!Nl}=W-L?n6~1kF#^()o|mM9al`@ZawytQ00z^93ZO z9T38u9ndWip|pcCoB93y20;j+sE>jWTB}fH4x=In;$_Vy2^JBT6qkmy!6c=@lF}$s zZOHHck69RCApnHX$AqTYuxNt!7yiE6zgzVOziSu(W!WHvHX)!CLc<^tf|x`WIe>jQ zn7@}_ms1`z2_e*xEd~_XFc-t|{e2g&MD}VTi`0H=i?%;zvM_woXg43yTmhj?2*n4J zksF^YAe_1Z4v_yM-9qMJz2^wl{u%lUeUB zh;U`gkGmjztO$J#j@60iHfe8sTjZGB6b;S1!5ZTxS2nZ&8fBEm{Cf3830L5HHzC^VpqKz+s z(81jf+iwtr+Q#waFY#_XUjo6~e0;eslpD^MKqxm4Ujm_n2hQf z%5(~>D1d5s9bJ40hz{=d&BvF&#=GHs2?%e~@#XqZZZuy4qTDom35X6JIGctqfndTP z&9XW95}?M5s4r37W{x(#1VRUQ`zGYeU*g?(z664|`S@~OC^wuhflzK9z63%C51h@z zmjE!~Pm22Q`EuPOydZ>Ic7JRo!waool*fpn6t?~u-jyorYGYAb53n=)>jBy2c!zX# zK&Xlae_91Tx3(OE?7$KT=pr2Wb_qWyDIqS7vSR%wcD%TLoWEM8(Dk}pW(7$|N=l-B zHp!nrU9J2hbNR0lwUvm1x*S#B^ZPDGXq^B91Or_;1J5K^-arAODpmhJz_krLNGlX@ z-~tTvjAz7w3-Q0+?p}9y4Dw(efJva`a9)|XSzd*xS|`76K?HzEhGW49v%7%Zr~lyU z2c$km7~fqp(Xg&|$?scmZTk+{$#DpIti!{o#Q5*yL-a84F+MQDAOi&2lUo1>TL$XMo|s2ywIY7uSh$gYy|+ zl$#}=0Y(Q8oXwKY0K(kV{Y5as^e!M@qLO83<4b`35SkYLd{@Ax=`R9X6OI@1W?cdU zUGo^g@HQV`0y!N4<%aVm5X#NNmq6%#T!_o31~z&g18_mY3jmwA{saEyQGyMw|3Dt? z36MoV=95->pv!O2nQN~WE0E@4zdYOkSLuI0m0fot26RUod?SY)6+uNoeY5_F`3$|9#7?J3$0GbOBEgH+ldS42TZS+YqO3vIOzhcsE?| z0K!9%5*s{#3hZ$^wC^wDZ0rWIHaF*Xd_Ph-jJU}qv`^tYWcu)_Zf`JZM zA@b!$71RLH!QH+I`4XUnznai)JYNFALtvr}=gW1W+;F}GLb-YP5(phUa5fKL0>Ff~ z4gY(-T=#$g80bP%IMJgyL0nD1kThd|dF*oqB{V@qMe2Vczd@QIBAlm$)FolJG{9B* zf6&f`00f$`03)2Hgy4hwcbPFE7DU70_mw;c<=Jhp#Cpz_qw8Sr>VyhX^8wVSpl)3* zQNYdj_ZQ%b`Bw2EJ6jHx03x3M>TLNBmoFg6{}B$N0_Oi9v~^-vEr`s+2uD#N0RkVV zSa}195Ol3Tlt;_KKu5vg1qy_Yf|vxlic#y3;F18r0+-DBGZGQsBy3D=K<>+k5$_TDv&-yL;Iwx;Qzy zx!Sw=08IgjA{-Net^g?JWrL!U7?92DH|+Vn$gF9i59UQ|Shu0x%`@L)6vHz%sthF)R=coESry}j<{!a7l+ ziWqbkF#scH(};3Fbnw7gegk33jXvW5=xR8j|Ih#v6O(|9Rf$VWNs5A{#6+Y~Y7M0O z&tF=j&|pC-%2ywph$@tj!UWtVztj2;dI)&pR21;&2qbJ*PIWHN#-gVK_8sBs#smCZ zUQwX481Uj1G^bb>PyVT(LLKAJK)~=#eZBe(dg|m4(4B=F6vfjCf4vgmIt~>-E!U2I-|wqgkhUJ+^?1PW*0#P8 zy$(WkVIDM*|9!mce&olmaf`uHH8^kyX)%zPxRe;AfP>W8 z4r@C*FMDrq=#adaq!Hleh97l*ceG27;pu`;6X2VSn)BcB?yhN{{wfu=*K4vGEQuJec1gKsk_)yT-D z91!AV+JPDQKvyXVLzlMnc$iklkx7im*hXq8`3E z!t9Dl%Ke!#ItCY<8iq)e9FP%VprDs+qmpcBS3W=hA)hS=5?r#OigUCnA0TwFFkgvx zD1)Nfw9&47fMEW%5z(f6fY8Ar1U}A<(uj~hb?n{0g14%CP)Q54DW6}$TQ(zf z38IP+v?(7TcyN}6D<4$WLznVdi|)TvKC2a>KT$pq;{8cr{G@!OQSB$_6FzHkt+R%O z67Zib(Deu(AjF$m_y7Thht%c~K0q*Gv-6j-1 zKrnyXh-ec&K? zMLM}E25CW;E4H44QoJ@FrtJrI=YKe$a<}S=-`2~{ARiFg`(>ZU9o_)^GhSHe_R0w=1M*E zdw*_K1^pMplRu0lBDr%w^p{T?g{Jb*?Gb&)G*%HJJL!UOX#;ehS&ngt!wgDb6@X-`WQArVLadD8iII2kheYLh$ZveBom0UvDhf#XBfa`;nx zs~sgIfdkD25#W7~8wko3Qb(J6QNsf`Y6pqt<*qbf7HF-@-#7HNua=*+6ltm&>(oz?xDoGR%C6ZnMX`LZfAPoB)mc91Ee@0y2; zK8eZ^aACg5E2r4-fNQWtm+&mw=y$llHPTwI*>&A^QrXr6uE7Vsu$~*WG2vxTWXW?c zB6mw<8Laxs?QqehiYoMpG`Ve2NXSVtMtcP5K{@ucQ4$OZ|R!dsW?TVaZ zh3|avI^Lw-mE_|all(O;;C~k^$izAj1FzV(V11ZIqtCcK(^6M_f2M4o42VX6Z|F^& z7EZ6PLI7DVm<@PFW;SbyoSWamhkBV?c-=ZPXP}3AYc_RmM@F-UdVP^m^lcY@o3{wc z?Z{~MhL9D6e#j{L>`X^wG@IIIIwe)O5Q27O6nzw@BQlD{vO6N9BS;z@kx}$ToQ}w7 z_C*|ajh@IT8gEz^YJOxialIlLEqxIul2KD~8226aBlu_!Xa*PWjg=?=IfLnaiApteMV%hix4k+FYv3)E16# zvEPP21E2F-95xhvb68}}B<5Gc^BNt+_(VKX)jS`GIav)74)xHWn=^6N=%|5L_?JZw zy+%hdyx=r+8`YdvaAcaX-*(lbqm?MF6LuaQ#V92ZsV%LervKo8EmJ?DFgl9SspEtn z9gL1bnEHNZxp$KllKM?o9J==3(Whk|coD^bmbmML{pIwypNH4y#(5$yHD?R!g$7Q9 z3;BV`kKD=lgV~Q)cC^ixZE)x8En)F|KHof=WZ6Jt;px$ELDfqLk8Cw|Rc7B_lOJg+ zxHKFvz|j=(YRT5-_Ne_$+S!8tux3A{QPu%Xfh;a!q&?5d=lwSDc3oqZz3A7F2*s-A zZ`ZI(OK=9XtvF5K>ODO@9>e)OUha4EWA*#+&URkh1Xx9Iepx%Z_FZCcGF(~1nul*f zLipL2Kg9@$PzX=PMa>qWgAsaVXR1@Y*u2(gY z!GhI<3D2#XfcPYQDrM`Zn%KrG;mEq7Zq)>YN1h!@aW$uvF1@RXI+U%e35Zgdo7&Pk zYNjarS+8A9Ky>Oj;YSbE1cIsk^kp^ij)!RJy*4GuZ(x?pCZB>HO)e2vsEZ)|7FLX( zfzZn>Ks96>S`{qoX6{dua~^-HDL#iLL<2lBoTczA_udaLX7Po@hT*Mw5SL^UB*Q<+ zb1q(bZ*uLD-is!9v;r4=UT^U$%m;3t77_yQ;#JVv_|OQCRt$>o^^!pC^+OXptVJ*6 za+8rAb&s^z^V4sB^poF&4uVE_YN^eBTv>%x8Z8>KQ5bgI*>Hl7hPk~etn~4X!W-cs zTS6j5K*$|*_d@+b?_a$jrr=UY9dOeJ(a~`EWSGOPnQ1$3HqG(Y*lgp=ra2g&+(8L0 zZeO0Sz(;&@twLnM+3hQM=Jk7^)H~lk5%bP&vCoDFI@Ofzf^#T5w^2459>~1Hs}!Ew zB%1|q%qBV5H`eBJ8)UQLA=g-&&uxy)h6h|jyZmyx4&q#-EjB#h8vVQ(pk4BL+*9FD ztY=Eu`^ihC^|-uPhvk0W42+y1GI&Bs#d>ZdZ06m|CN(Iq^z&w5JR{ejf44{*ZUbz# z^^j|jpu2$QM){sPq=kw6k)vF8`&Rh;=GPLyQr$749xe-j!GayUuN_ej_nGy%woRTI zPhb2VswYPl{1yisBsF2n^v{6r>Q)scgayfEL|t6f3}PN(O~?(QN$f}1zLgLyro4Em z8cFgiweCjLs7+2nNUJVkvC+hdHc07Us_yC4Phq_nMkjTfiuv92nGC!lc3Ah(tIvet zNkmk0-VK?9FU9`eNcGfOB}z+(cAi?pD2a51?4q``j+$B8ep@E;P?%c7=+tq-kGJ-j zK7~`W5T>q_d+IY?@3(OpZ}-P|r)iPvXcknQo%Y)`k&0rxn`MuNq2xF!CwCwxGUtoaiS}1aGCgpHV?Y)#;xExRFb z5Z&`djGy1NVkO_${XD38V>LYLhJ!+>$oTBHzJP;5V>aayR1Ha#j6JycCufI_%$zCH z_txjPZ>2Hn5zdsz9pwgQz&hL>cbYCbUnjE`Jg&FlVMTyLCtm$N zVa#SHxE@lp^dsC*x|-MFG)yNdGp5UVyxvWgw-0xu$rXeNe$YjR@Z_R;m2dTq4+lOZ z{Hx{oKJ<>6$TR5;+w~o%E}tqR{C;B0K2Z>2 zQ#kTaZO9cpTO|2#Wt5^*VFGE~{z`#*(eU~qscOeTpDEBxFOKti6GnV#{H?GI^p2(& zln{G7C5*t5a)2kd?k76DwO*u6P@SsvtIAVIECEDCxdcQ7`1*sL^P-6l_TZ%ts`#v8 z;;`@jN6p)XFRI^Bn_DAvs%+^=>p0!ccZZF`xhe6R+xzC5TY9rM(KklS#C+J4DGU}~ z-QH2hR>$O?{dE z#O4^IVxw?ew9t{2_n-p;1-^L{Tr*-WM=K}m_B=F|vBc6KJ`TSbKH=qx&zim3UwqZt z56+Fq-BWz8FTJYqQOL^dC)q>sS!%eW{np;WLb&8!#b-IkQG7`tE4~jUh5#&B>9qTC z&BazK`b%@#cl1paaSWl_m;382HzTxN7k;)x5X#Xy$j87n;1tLmHH(?mhdO}%8TsnV zr>7#mbixI7`<{KcgcFC_O^?{2`4$nT#9J((v#v0CuRJmn1HS$St_@gdUf$8SOZ0r- z(7gOhtw>8EDEvN%7;YR$TXed8f2YujXPY&r>1@*zJprlZoP0cDH0#j)L2-THC7z0w z4d}DIc=ANK%&D{e0Oy$aBs%>V(;4(V4!x4Wb6f#Fwfzc%9(VYKi)1?qJ0_ccfe{NT z6YNC`<+^Yg&V1T$YN8@8W)0q_xX3b05JH?>J0+H)Yg_O71HN~N@g>#|U(Scr_ml$P zP?Z$PSJru^0~Tr%)YY1jZoiAH17Ts2=soK?FQ|jW?oZraP$gbnXgp8bjpykt1qoz@ z6KXx{>o_spTkxTRctYPlcpfN-eJV>0JbLv~pQ^xGr|kO&q1z)Oj84?wUe9diB%B&6 zMjv3}td`Zrl1TJX%4K4i_;IH+4;T&A5PQoiTIO)yoqC%(qPsySuilEqRtK^J95Ou2^TDggw3@Dy-qk) zDTNTdDg1Lsm>IU`yh}oe8#^?1vC74P=>#9)BA#2Bq0Vz-IyTEj+O?+NvzxlKxO|Ys zqKBc%V>CQ_@q@%-3rCx2>wL6y>1J~MhTCV03=`7V-zC1*Ggc(B>oTY?>JywK3^82? z`lJDE7#c2*+lwdjGvZYUJ4n&i$F^KB1fPC=RIJ7a`ejU5120s3*6h;$@^8jxO4>9F zyNZfL95fcTm9!_Q56gRb1=*9NJ;4W9AretUiV-s;o0p4;^MR*Qsvx>g@Zpuv2$i%4 zFMzhBJ!OxrLZa3zBV^(oeSt#Mf$x^2TG4v6rKCL}%~-OHx1??JiR`r!mn(7!<)j_z zc|f)y-+_Ymx}17LIbSSC`xdmPT0#5ui8-FIsdPOy1?~6f+34mypq$;0XFQOeoe2$> zW&>W2OwUF_Qo9cTJkTy!l`=K*HS1SApvd0SfEPX8${DK&G_Q{*Gp|bOLMbFa(56*M z8G?^m5?;j0Ou7%Kq69`Iq@Q|xoq2orNIXoQbUt>pH|J}@_9i+ZJi>`8KaAYDCv z(v~DQJ#dHKcCF+_+iO5fl(Z+3n;$@1a(iT>*GtJw1f${gfhRL!AvdGZYaI|!a??_B zdmy=5BDYwu*~_8_RHo$;hJSKc^x|HLIh42|9JO9oCk-XH2NceQ*-%aD%4|~JE6k8S z;2^l1D~G{=?-PgA3`A8p!SKk0{b%+d2)#TcQLjRTfphBf`O5Kn?CXf;v*CeGBIVwM-bM5 zMJGH2p4(iW5%&llat)nP5Iy1bytyjw5j^A?^zRmT&w(}9!UI`D$7AGh-AHMYi-?pW z$}*goIAynQWj?>peZD;n*Yjp@IdAR z{*fjw*Q>gQ$*Vqk`bt#sDk5K5U&RdZ`1;whwQs|r%;A)^n^;qz+Zu;5-xnE6@Yas= z43A75F6J8|V+r2cZ=T_an&1{0lVg|Qt^MX1o{E{~GQN+qh7!KDvn=q5{hIkk)(Crs z$ByF~7Srh#!!~M#kx3V{!Hsk+@PR3r;RIik2)>a0NX$BW1?viYYxj6Am=vG2fa|Dt zRNz~?2P-jBZz?`7DduT0C0yYKA&%nP1y9>084Fz4#nmuM)S23oDR{s zPAw;d=zL`qfDhU$yUhSo*@Wd@`bxq&{n80%7w|2-6$TjC2V1MrZfaX{;3DsbT`!^i z-hK9juXw!Z0%rE}I$!(xZMNhj0Qx^SEk&5@Sqt+6fu#ZF^*!uJ?H9+3>*B0o$N@S4 zd@Ad_@`p9uJDkuLg|w`5BlT93hH$OjBJe&rSo&v(%YMuAli~rj%Hf21nEI(wO&ori zZE^KrWUlKIdPSBBeCs1-&j@AjvkQAw9#7Qjb7;P-O~~-p#NNw#Hafrx$exR`!_9qd zbZg#6wiC`mz@zVa6&)$36JY92OS7naMh(JsTyY>Q zYZV+vW%|Bm-ayI*51K_~dXEC&t*zH8Dxc9~sg9!Zxu!el7L`-hC`Dxw$@0@pQQ28# zwp&`Ho~JOC>QV( zhZ{H}Gy2MRhSp{}aghhC! zmA_C6;;#-3XVjbEo@_ANbbeT+=+QB<3S>ZEI?ESbT#OcPU4`SKh6Kwu;uev5IY>!6 z?i^J8Wp>BKd^5PGegq!CyC6|rKe0gZZn`>bBc9Qv)5U4y0!3@`)s+Y`Ieaf=hM3f( zQs%fIZCLJ~dhdBM_vY^R-W?c$nj~+R)Q?G}ef>JO9T+J=tY6^h0$p*IGi@Avd4`)Y zrW98^YbY5iNHTJaHskpEiQmkS^hxF4{sxVHT%a;1Tqe>kAF428{0Gf++n-GtE>Noz zr24gmRpET2O(51KV8(V_mBnAHCb%dKYCHJehCi_t|xKpxspawAZx^ zw71J?xLH$f`*6qmkOa2UeSp$mb{`y6{n@ZjRN%+>!SDr2fO-?8YP+2LKB#7=10(`! zI*32q(!t*xNCzffk8}|4nBrE$OATe_R*YzSK zuP;_9XaiW929nTZ8IR?i-6SwwQ~E3goN&EBbQ0ScW;$(6p~G;d9BNP#;t&|{CJjuQ zVL^o0#w#h`vL;&{T5}UzYR>=9zx&k=bCMNljb@{39r>6J{0Q z1t@>=%Ri>H2g(t6)KWgFEv=*Gf?z+&bkrqM1ktJEgdgNj@giakN{OBTH0+?D?bQv$RMlV$uDPD>mW_h?*yoBiV z*!zo@+jyJDO9-#Kcv*+Cb-aWqyNQ<&ojOi8@e;x0j&&?}#9w;imuTc9kC(0ZB}Avk z-eJ51y#h{?%k#Igj~l;)=Ik$CA|qxfo5xFpvY&W~&^=$opaV9q$I*pUx$^z|@^4j0 zDC(DJ4o>4Gv1Kd}4(3F75Wk`H0rlXy7nJ(MMM)C^)|>Ew5E?pj5OJmEQP4CYV0A1) zUe#y`&qMa3t5*#nOc@X18xdeyQHowU9}v9$Oz&x7T=N3a38>3pb*uRzzrc7!F%!a5 zp2SjW%@?|gRodnSw8cz_?l}R);Ayp=DFpv_V8oA-jgle^jVS&RHCI+REF&Hh|I-;$ zZvx8coACJISv+HFR_>VUpWxvSqz~AC#d^2!gRN&VN&0&nxPl4ZF6(KGuC^Wr^Zj8a zspj}WOl~|L_K!3?h-#JLg~R23bTu3TbRlXugw-hX;}8gN*Bk-?*8ABHhd?mZ%Ozbm z;}8g)fa-ulAb9leVy(@AxhaQ0=sM&O2!05W+5E$O>^yk#e!05W;5E$leBRX*ij86Q6qg`D?i2gnY z1Z7i=--g$mLm2Q{a|jGiW3(Qw)d}KYzQ0>w^)hmF@8l5ascC7k(luLy!uWV?M{o&U ztaLLQeTP?!q(@T4#Y$JS2Sr~NdoH=L*2rAZ_Gx{Qq7cvH!)o>U+HcKd=XaYyV)wM) zUs{t6SGe|s69KFL<1jd^2#T*>)A}81f#Yo_SN=x$dT&fRT?u_>YrOLC*;c=ErAMn< zNQJ2`AK~-MNAvrZOw?dpgmX>NwB}NVD_qVZl|ZyGRrPM$PEt-T4B%YRCS~1Sb(GRqblQ^hLirX}aL}Fz<6Z0FoB8+Z*ricBdi|YW$%}jz;%R%;?UwdgR9#6| z&|vUVC&2XrWPWtLSR+}7LH~QTwDCIs3Kxh730Lg2Ho|Hm9P#;@aEiXy8R<8XjCWM6%zAN0?`)tryAz5PT;b8StT7L5xnt`OfT6uzvJ8?NkiBFjeSom(# zGrTz@9azC*}Ung z6+RNl5Qkja!3v)1gt+xH+~D_K7a@6lkypdfXYz{B^j68i(i@6`HDAaO5x`!uM^o|x19^k}$($H2?6v1SbMqsnz@1rLTNhppzcZsRfKy81CqB}$7Sk(1#> zgd_W$i87R>S1TB$P&>7yj?+uLgfO)U@P2-2KcBy4BWKSm9x3|cf_E@K9}?QKQ2N-CH0>2U z7_i=i4}{Pl)*IM0*Qcf#)FgiKx=FjIbJgPOj zc>0FXJ*UaKiEHN-JQ(I~BYw0vD=5Mko%l!8TpK=p+lJSCY~6qt%*fXBmir4UcrZMT z(cuTfYcsZ85&u%ybgeftyIfsDg z`r!}|-tP2QR4z72tl1E~L3I6b2nchx5xqDBL?`|c`w%TT#3sD<9AX<@a}ELFX^d{o zAu@EmaR_qK_2Zrz4q?5iaMK}pgg*n^HHSce^?vrlArMT7k?lDILe~+8K=A0_^`S#R zm+6p0yu^rZ90H*e|7gC(?*`9U;B~+uwyf8D6)?7*#^~l8Vr{(MI0Wcc*Ki2yp_3nn zK!CgE5D2i|&we-rf~i4B(sip|#u=d#Q0;pe5FUAcCmf<+yofH4iJB7HyO;4ABf4=2 zWJK`~eXC|}8KFyo<=INDsiek3;|aVDIK-CqI^Ylpp2q0r9Ab^`ogCt61<#j1{RTbb zyk#3u7+-n~&sUxYZm(A>d+dLF7`*0(A1YSx+%9M_f}~&16*4v8;5reycfoL;;Qm~f zGZYqy+|VKs4zhft)7czq*?*?-LFr%Ez|esTG8dagWNEoc>4oDBt~VjLA>#^dIiIZ7 z?S9+M{_r$l^9}swJgs`jr8ph+yx)e?c(G){!|0L$3*$dGbh5(n^?~Slc(?@=6Czqw za#e`px#wYcgB6R{a99_gG^i%GP#onvnGdlJA9Px&QiizBZ3K~^1xHCMcFs3zfd4Q? zdk+x3=?1<+|1N?gl2sV^yIalr!=S5I>fE-sfyh0rh?X3+;G`d%D~OS@#a>zZnyPmH~~hrNLL|NHQf3%oX|yhW>f*;rQO!2f$g=P0!Usj zVX2#~Pd45_|EyZB7~-+;7h}A+^=Ux8!-fBZ<9;qE_S3gAqI0`ZOR# zb#YA->+fLe({P~+W_y*_MM#1}snQ!VmGJr1l$Lsy8x?0Y`9g*g<XW8;> z#0+I=>k~#vIdo}D%W=3I-H$RIb)m?>=+to}o8PP@oqS{W!>iu<1Ys(1d;crbns^B^ zrcP#PytJxfw|I%r>9I4}>1ylKfVw~ZY<4FR1VY03( zs}MNCZ%LZ=)+Y>DZ^8#cD4!EXyeS)|oP@`|Fkp2o{y$p7;Q7_=0d$!jS5yt*MUa^a zz7YYYb?i=CpRf@HRP&b-7#_uf$dhE2$|6NYRcrGC+F~X=S~8wWt)ZJ{j&o4${E8b` zRD>bS-A4RqaaK}_($wRwB!(*@c0iSPqeZi{e?b6 zaSC649Jb*#=MWH{#^`G6IdO;#U2hx$bml5J#KU5YejEY;?wUg&z#K`74 z1bV}#pgQ0X$auTcpBslj=sM&O2a#FL@y2j(TRV=esxO@u?ep|huDVKoI^l(8lzithz#93IfT9Si8h<89-J{gT`9ox zaG?)=Ia}G|@N$8Q#Y)3~p2JG|typ_;7+l5X1A6VaG9f*Qiw#mhEh0hS#tCa)aTHoS0I67|RGXNB-EzZ@EF*Yf&=ng#4OH!iYBjC)VJ%C7>oS z!sQ@zA%f$021aYO*08-5L}K^(Hn=60ztgZ&e9a6OgHYUwM=Sv-e=mG~{XVeh?zUOi zngn~hI+2__nf3Ju=Xb-^i+08nit5753J&kK6{I4Qk8q6%*)&`SB}IOlY*I`r^aMxr z(NZ0_jiiHPlJ#U}e>R#Azj~F;n0>8zfo(Us|DusURI zH-7f*x4-)C`|pXH(-ytkG#GdZct`grWp~+~j-RNVX$q4st=&6epJ6dQm zq6HkO+7QGcWp%L7g!YdDX8&wp*Nc$czC-~=_@tcNB2ob1DV85GRoLzJY3~k&d$`*I zqa#oOohrLIK{2<15{Wlv6C@US;+SN$}h z5~YcPFg)miJc>e>cig<~h z$nt3~0om^uWXsubRc)5+YdAx-1MA=Qegy__9 zx`~$vrq;iG#!EneQ@rH#SMwawEnXsYdh8v>%T2uP<0XREU%aeB**sn%l>Nj@gial& zpLhvicD>MKgzrwpg_3A&h=gwO67;arf{+g5Uq;w3`Y zVY~z|Rb72QzqB7CkJ&>fY8NQKq`wU=c!$aA4~9?+m6@$Ri@neU1J;}Hfe`An&;$ck z$EuB?-V04I%+R-@F(i$pR+MsDXoAtnVK;wBgy9i)iaI+>sUND+HicnZ%-rWCJr3qD=WRN0QqA!~liYak90IQ45MZdNhC^6Gg?=0Y0q&YZAi#P*`{57> zrUoHN*UdNtLMNa);1CGj?(|nwE;d6@1&2WB$ap$Q5c7*w7SR97fe5*SLm-&Djp)W9 z5IXUX<`0QR_*!4!b-*FEtk?YW@z{DAqnmSxwedRR5ZObgN*%&_+2O_^AmE-k1O%-2 zvl|WpVQLVPblr+WKy(7CJ%@nscBelt4gt~i#~~oh-A44{5D=aCNAray<0icJ9AX<@ zFk?#{0>aZ6-I_yW=z8N2=f<(wa(YJT#ub>wrUS zS+Bhg0j;MoS`XJ+OUuFBp_h@Ndnbpm7n+ROsu0GP-tMwTax3@Z_wIc0IubwYf-6JB zL8KbBdTIL~@y0jiHTZys=qqz?eCefj@zEY!Xd=8z@SU%IFSXM<{US!V(?r}@F`iz3 z%Ary4!w=~(d8s1~H;0H7<6CX2rTmOo|Ck)E4xu|5NJE(W$)EMyE~mll=JPqgAh|W< zWLp-l4w19h8sA}Q%zXCsI~w2u5xoPPLx5!`dM{<&+kKXek?F~+N)KWuG@zH^9j$kL z`(x*9*{5FrH=n_W`42khbrBmW8LxvPX)bUx^X%F5yKm-^MFQJ+=`T|?`cypb&9>zt z6=7?x+a6SF>4P8bxLE?g{^O1pGQH5A7c)>O#Tos9?xlLXSh1r;0{0!;SvpuGaEH5D zo(&w_%IhK|w=d=U`w4I+6|Xc$Sz)s%-ESqNbWKZ(1TZ>@o2saEvq-?eD|yMX=11%A z(joy2k7A&z7$Im{w~=lPj`5p8GaIHT1*J-qRx6I`l2@QCjg4ZI;@NsNt)u1;3-jfs zEt8f}Ajue=I!^e(4F`*G@QvLM?;o?pKwshf17UXk(CdBz88eTUjTQ-Dbb9Q}c5+_a zaFKw4mv0~AwVcQ=UTz(>`grqrS%tEByhJGbiI)hSI!^e}MZ5$swXS&oL$3;c$u;D< zu1xWg(_f8;d+|$%PLI96c)5+YdAx-1y5pC1C|k!%h_ah_3DK$JbQ3QT%rj{-vEUJZ zIZxO7ZJfs2{W0FpNj@giakN{OEySq6^6}v{AgY9|7LcDqM}1#FlZvJD35U|JFFd zZ%LZ=A^{9oZ^8#cXb@{Zl9TXqR17$~6IKlMUL=5F%5Z|#>QqLi%F%@Lv~qDV|4hz@Y)RD<;HvG@Ldgu*y@39a0sgk^WzW*aMv6H0oMCT z$gA>;KZigtH3&(%ZpI-HIsw%Ihd}V?-^JR2tXOr%MWwegLKf2>LPy5a)_2|PIhAp^ zD%gaV^}dT;W%S{R+ioMeaR_8Y@edtqYR4hA;B~+uHsQ7B5D1>e=;j<^jjlHi0XlOv zI)piaL59Z>=c76V0^Bu+K!Ejr_QN3%Oo@^0bqIv6BMyP!k>_{9ArQI_IRt{a+lX!) z0-+QCh`qTQF^B#>I4o`wu?ihx3tk5tViR6_4uRlljBd^$*62Fp5LYr#)?JnOLfA{a z3{%#j|6S}XALI}aaL*h90@nN44Tpd*B}O*q5D;BI90J1Io&LOZ2#8L6yq{hMgt^;@ zUK|3V6aR?)>Xsa06JC1`u??>|hk)=jMu#5_dl?zJcXEh7*M}9q^XZr9;o<#?=1e3u zpUv+;AANE7ZxvYap&9x@PWCweVl~i?0yLj&-Z}QJ$(Mf9G6ZP4br(zfe+fSLR`bq* zBLgU8OOE(qd|~J)zO>!ovoC-84ZM0RQ!ntbXQ(L5yDS2Ky;`d?_d^UN-w!|7-f>$O z0M9Fs&L?YtU_UbE*FXGF;T_qIGk!2k=`#o)a=32pyYLQt`-aYMA6)RFw~rk;+`fz3 z?faJ+0!y(u+wt;z`yLH1--tqAcKIx&`lxp-E+336*4~-8n!tL7^%8v8ImUJ9dw%XO z-rF!VXNzYRp_#WkL}}smcW@5PwsWApGpW*j%iY6gdq;t9?Hmnh!R9)gH{QM9`U2B} z`MsmN&@EZBSka42_6dix8_C0~An zZ{>s{j`;EI^srzZ4Tev3qLVrwcs+ ze>|-4zR!sw=}t@cO4ne5yJX1B;ijgnNbdgfs%o}v4amZ|N04#>&exM=#KX7KTpTu` zr3jIGKbL@KH+r!su#opmEDA1)Qw~ZLFM}0>>r-`qw!RE?*o4(SG{F}*(1*tR?RL6f z@27{u&ba79_Q{_?MXd`wQu+1zbTm6qdQtf`UTij9(8;S-ENZ8DH#vIk&f|Zhaeby9 zU;q3Me(}Yh{QA3JeEa*q{1sinKmCoLef#aNzWe@rl9rfNOoNR#1bj;;D@M#+^U#7uYg0v{uRf3$xD{?ifuM%z1iv0 zJuNK`n;<#~qgq#H9FI0c8mXSH5sm}cv1Y`H;NeGh240~~mNhAn9RRl&ih-OS!=noj zuA`dMqDjPL4#3NXr%Q{&Dp6V|%oQn&^qVJ~p)8GfW0d0BYD?>=*}?XsOh;XqlgH@P zal#MozFB0GZ|r_})r-R*Oie&NF2b3vfXCOQF`)bw- z`Nd0Q&i>+M70TxE5~1uTULthQ7cssoY922E%s41RZ0-H=^6#`TD)URkfJLO^)@UIj znU&m8PMmTlVTPp}hx0XQ7sTP8J{m(cWk(YPnu5fl7V!ww9;Ws!%KjwE7Ivgh5S@C` z2T~Br5C_Oes(}ie9Rk+CRGWjmAGKn;O69A3(~9y=i!m@dIqc?-S}{C|rLpkgQlW-t z6j|G5Ns$6hiLApL_)Igpc+~nLBXWR(amT}m;vcc5;EKl*ujx{J2wq{e12&$(;|mba z*iwS{s(!?Jq_p6iM+nwAFMf=QiABK4*0Zt-{jYoo-Y)BDjIOrcM>ELRmBxGLFzJ(u zR%qo;@XPEHUs8jNMN@4?@H%O;k>W=pfXvJEb}3XH(EZ#<1OluVv>y_IU}_YSeBF#h zAanw%0}_GY^+O^MI&tz?9@OMN`d>K^!RuvH7>jlr(TzkPBZ_~-Lc7L@ZX{w0UI!#% z6JC1~f#7M3ZcZZB=-x>p%;DV$PPU6@gfK&n;qbM-sEVXc>t|p59we~YU^|;nzq~0) z#BG3~0w6kHH5^$ol%Q9OVIP&f(#sEPC;-j3Yfnap0xTtzEe;D6C@;lRPVLp={2YD! zFE3OAspA6PbAGO3#%ptaTtS{`F}cy5-haq*gF0QqlDIh{Jyw`CV{AT=P#PhRGZR9k zTXonVI(Hu!Rc95%*F_FQ(MJR+Iu&i*VE(|8T$H5sDeMeSo!hQg2?NB8Z-315|n zcWG(pX4K?h#a1i&?sF{s>FNfluBOQm{y#zwgUoFv9ufRWHFr_ln6lx_Wuf`xmCx_i$(}VDK%TZp9L5Plwr!sgSjX?-= zw-GA*voYUaNE z@S-c5=(x04o0G0)V26V4Y6&Vw(b<0T;8JppPuxWC-K@go&ga-PHrVxK&jlQ)>nXsL@}N09J_A@U5*}$DqR0 z$Y1*{SqTq4WsR~SAG+K||7ck9=4>M-OR~X>37>lNTUZz54(Q?CvorhlEo)d8TG~kw zJdgx8)|L2vG0m~J7So&neEr3nBaA(t`p8yJ($!n@ggnx?Eg4Ob2AB1ZY=$k&nn=Q% zPkeg4Mc0w01aDoQJj0Ws+5B?u#{*#jUN%Ye43A~#(DC)BttEJC()tXKDNn#R`qQow zzBOrG;1m0q3(bxmZ=$Uwd~4GBjE@;vESkK$!^XGe9T#cvx)pUMzHGp0VLvIG-cZ&R z&NtiQxxhz!E1d78nSW$MSPOjQJOQ8E4qh{Pz$Km`+mS4Ik#@b8d)V+c-9y;I>*-8& zQH%yO2=hYpaNC=hythQ7;hs^7$^oC!Xs_({*ILfaQ=XsFVyIvJnzvMa`iHc%>W{wr z#jhc|&W1?XYqTmcy@k^v?7;J}hCh2=Q#CWgg)wmjpTQz#m=qdGCMLXN{n;nxT6paI zfcQkIE?3E|_7fR+CHYz2{n1WsX&Mm23r<6~QO#+k7LM`oGKPCsx{XP%F$Sb7QCbn4 zGvy0Vmi7~2lu}ixEv=(wzOf%=$})wfJVvLE6Mk^t){2vSWB0?Wo(6<4bqTJH0BLPH zK5pmRFx{?ZZOWl*KkyVW3k!uG_jUT72kRo~K3&G)N@%^t&(osG!aL(f))Alah4iSO z4Ln=tzQ1}wIR|D!*b^6mp$Xhe2TO~Y3lCi$SxY=*Y9Wx467PO{WV3CUu80{qSI`#jC3bShleMgwVQ)QB?gH_Z*Gq^62ii0=Xin*U9qGf zbf^huuSsuTKEwOr2_cdt&Vl&yxC003Fox_a)7m`4N=*YUfkn;f1N{RbpPpoY$ z*B(#Iw--WfxQJpyIp*8-9q}9PMk0voV_wAX5GMJ!U*A+ihYY-#5P~z-=WF4t8K4zv zjvjLA*%y5obtO|xBLa$b3yUSqRxC6tn(+kDNu*KQsEj8puI614=9D&v`~8f^c;95< zm9S%3)2X5|oh=Cpd`z7OQl#yPnn*Ab9Py78lwJ*!@5~UovYq4aIcve{~t;$J^vLLmLCpeCfgZ8Qv7v~Q1IO_K+SBpOav3xng$kx((`Okj*1qQf2 z_Q2M{#bjWtoG|14#8fBubB&n{rJ)2oyfdCS!z>sjT}{n8H@7b({;fLALWchBpZ(!K z`(s0jCd_2H1?FH^4liuZi{&&#|8N}IB1TDADTbj8)@;fchN|Hed0RNsfE`HX6~qnV z6NlC_?}xZ;<0ar##h4p$gYf8-!*Q!QElt3ixYeO-P23>LZipL1r;ZbTyp^~?F%5*p zXO{6q++O$Plg)@^s~K=gD)J(-rv%Ka!xvNM^q5!>1$CIhvr#0rsZvXSH6t0Zn0B%c z;xa@ED+|f$;xKM88?go=Qd}s{Eg|{|f2rG4I_GR=g^ZZ>By+8R$O%U!D-Cr`bWJwk%l6`^_}GURJ}#?D06gS zKfu|olO$dw4XE+T0WoA`f?5@?i|Fab=4`U7Ff%otAWBI$YD=q`#4&0=$`rWq%v1rLI?nqG)k22>8Bs~k zQ=$5%4nrznXm$b8Z#@4&%2IezCIRV_izW6Pj_65iE_?`cJi`Hy0Fmc(Sp^H?=UlLAVt=a?u(iyW99e4Mvo_m6hp}TR+V{5y(>ZJ~OXf2;f|8$yrPG zb~B>&B6vKbZ(D0{%((b)e?8P#Sl5_xecZ^DR!8BmU)!e4UVsz64O5yEa{HlHn-YF2 zdVMRcJxlxX&EcoQl;EhhO!@j#kETooM4>`Fqt>f-HdP??n}PjEckZ0EWtZJKiyP4) zVQ#clR3ssz7&9|ivm=FyN8hoST2Qg(3D+=SKh&Me&BpkYy9vk0f@iCu$H3dV=)v%^ zqQ@d^&fU36l#PoXjIx)a2cuKRd4JJ^yK@kxO88Gj&zrh)PXpUu{`4DE^_;Kke93`4 z&DC%b%O%Ct;X|x*PB+I_XzxwVgf*uRZmXkG3NPjxw6a3aTek#Qs>@+I^dz0*r-|eF zwmx5`zT(#W1z;LENZbI z;e5sm6GAUR-A)}^2plLNb()K`A2^&**OQ}m5D zUiuL(q-9N32tT4pm*$AEIpwf5S6dOsn~6gsW+<)9;@p0~wv?2uwzQ6#Q~O7by3l^W z=$7&+*dTQ%ouc=(XCp1inGCKP{gB7c2HCvu^uW(j`aaE6l@ z@nSMCDIpvmI4sBz9I-k-D2ivz5{XKZ&8?8RpRGk3CVYiq5!zk7g2#L^Bpx!M^m49N z;yH@%>8m{W+u-9FZi5#$5&HpM^7T_|Ym-vEg#0&a*HSbRz_Gw((|Y;!Kdhg&eEk)c zgf4Y0+uXFK;KHW@Gbm)n(X3CB#*d*{FQ@fQT?F}1MP9P1*!H0kP z;EM)ag#r4cFFb&&=IG?j7(TnN;nl}7@b6NWxu(!wZ>*GLV|HabZ@8(#ns{g&9Uk@iHhHYD?>= znJ|6is0)P|MyHMweo$mI2U7XQ?uYl^@_dL3GYC_p$sX&n3v=*{T1vu6WzWl*Moc(~ zyOkcEU(g_qSX8)W7B3yEvXmo>Q3SD9JoG1l z8W&}k>~jo3pceWgIoz=(0}fvxz-Z3pBKI?m+O1^+d*)#u0nYd(-knEFF3{Oa&*VH# zx69pcWkaHGMR=g0C+B^pEggi=%0ThXxW&3lfo(Kn2Acnf|c?8)lcK>I+g4_F9$IAff z#8ilho_f1HZnWIyV(yAQ6m1qK4$H-C&pQ=koE^1G7R$xg%YU;=XRD$MYaGW*))QrA zK-@>}rms0uy;BkgP*?QHf1)wH(supbM2#r5rG_G3V)z zkpLsKub(`hoyUBWd_nC8trQ*J!q=))n+tgP6I#Jj+%jRhdxaUBOV)MZxG1bK{-C?( zHh+--mS$`&u;oD9F+^_4Jwf+*y>HA{ZvhZy7AWaU4Z~kJ2}kUfSn3Oe2Vk@s4E0ki zSp|C?EuMQQWt)b*LWx`wp*F_c4EqESw??i=A&*5XQ#3jGH^l9NRCR?Kxm?Wz&@KE_ zcWd^an@I!)>3a5W*OsovMK^Ge;~nrT%*w+K;_ z!Z|x^m^MDS1@ERP1eFj*n(NhKN+v9o z+Sg2;;#xtJay6mf7oE*Q;fho#O4=cLiv=&QZy#rVQkcuXX5Zml+(MnYuv#LHO2D=7 zp-VAgjEsI~oUWmv1YDVLuY$<2`&5%?j@auG#vwXHO<027H&3-%IIK1r~;DuGxD5b`|kq^^{NCUd<+irFe}r zv#YQOt+xE0dM@rEDi*F#ER$kJY99ELCTF=3QW00}&k%iXbISI95J_o)P@uf#prFP{NF20kBuh?Q8 z9fmgN>OW2E&bhBNes)Dlqj*jcx!oMzmky($>#HvK^+d5ZSJ;tTtXqaR^JB|LU?=zg z-LGJ9f->nd+Cay>xMaokMKY<}0ghAHnvU1E{w5&(G=^{ z1L#6dRE%kOeUTI_##DNj$%9>7U#Jml>ZKpp^;%!wY+Rj4zgWDt>kDP>XA`%MzGzTg zjH#m$U0&bY!P)i3*Y}15da-zC*OxxNu|zag7uju+BVEo`R8k>J@vC! ztVw!p;cHa%QP7nM7yUKX90TM0vkJIvOw^b^*zExWql?rR+D+#+8UT_?0 zA*ng7Sjm}bKfG)od1>;t5~bB&VA=EqC`((@F-jqkYD?>==|4DtG97hcxU43o}S0Oe}Q1(i|@DW#R3QNG-|}>86vuA+Wl#KE)f@b5_Z{d zr~%7Mmlv}N*45k(F!GF4e-(yZZ)l-hs%`>Kx}4Y71Dq{bJ-B{Di{(<4qr()#^mtrv zgE{=c2^Gd9wp{o#kS43U2Q-nlV#Acp6I#hWv;&i72!`oIy46IlDX(|46Tc64HQj_y zEY{c5tGM_twJ&RKupLTgA4!s40{Uq5=|R)77utznypBC$&s0#-rwCAEj=o&II{v^h z#hv&fMETv#uTnu-m|eMLolb@r2nad6B6QF-z@3CPM9t0T$Qe`(H*w8;U~xmeO(h#| zm}w}Mj+@^ssH54F6-;VWDG*}W<|Ze^jK07Pn3|Z!{vUbO|Any@O*Lp$#`d>_(cy}F4{Zs zGixQj0rkmP#$JjYR_@*(qh@^OYw`g#pDyy$)bllY8mcLI%z)moFt%Vk1)0g29%Bj7 zjZ&FGPu5T_A5f=^C-oy_;YF8syj&_FT+7!?*vU}>!qCFyE{_|)Cw;w8?hd^BdC{W? zc{4y{XcB`Q7~AYMiBsaJGoX(Nsf57p9nz&=7sbvt19~2kHRzqF(LybNZUWm-atZaM zU;nbRl9CVT>55jhiq9)sC;R=*r(b>r4T!pAfmu>*qs-C^UkZbTnrnbPGO-&+o8Tay zIJj^;VY8^AEh(!YLt#*h8k5`ZPskCM8Cg){PA4rivN#NCXN0EE5F1cKO!J2v@ui*X zW?e7NZ{iw)@BDhkRU2A1U;8LNuW6);(2}XmY(QbG+~MG=+AFjqe5L!BJw<}&`xbmq z2q%Rzk1Wp)671)tFs1-g7+)ANAsotKZhJE-s0(^1CDcVyhu8V0)K+LD3{ajAeoLuW z=Tk0jqK;kCK}nvw`RurjOFAgO6Si0n3&^fYIw-}+`Oica(ZNh6Mw zbbbJzK7m#$=`x9XQPT0aPmGF#+a9457z;JsrkJeEt7%PF_2i{lO$Rk(35-H)8WKbF z#w8<$ZWT!)HpQ9_DcVZBh*Y=|uax*oHC^`5W<^OfT@pI08OJZ*HKAeO|e=)7o`jgFu!G(lqhl7j`^{q>wH7IlfU;@mO9M?7oe zjjzvbweZM7YvbvNtys@Z^RTT46lrZa2|Tw?klOHoW9a0N!t?SB8y@fs?Sl$D2ij73 z_c;8n@18d31fGMQRSSKE=)|e+EeNV3>UG71Ni1ju7Q_06 z7i~`Jfn`M4^U_%lx(g0Q%-SLa28h~8dKKA-PQ_cU5f#?nj~rOm&ayb>D(fAMhH0-y z>7mF78%vUk)koeU4bE1ro`N1>Qz!H947<-O{vEIZcVx;D6OedR2HR(gN=le`3DCIN z`H9ZPfy%4QN(xCqhow2V+{H8qC_o2^X}+)aNEJ>W;PK)P`sfO9cRl#<>MgGF{CM)|As1s`8W9ckAixpckwE$iww9>vKuBFqqiPpUk%F%cP9qm%0l% zEV`?yT$g&-gy;2e9pjT5slhxL zav;ZQPU{s6lj!}ntA4m%iPBOQn54V_W$EEMMk$F>ZD}1f{Ran7CI!8egtBq3LC0sM1p`Lbnp!+sDRT3HQsHeU5 z47ZtuBq)|<7VhX>X%a!?;kZ@FeVmV$4TuO;AW~F&&_7-IvbmY_j=Jqy0B70^?GSl+ zVHDZR(;~z|$Cy5|2$IMuNN0$L>Gh>tM4U>cuRllC<{dsd4Zgz^e?ED)QpAS0ZoP3a zEb!>khKuPIwilHMe7JT;Z%@~R&@Cx1@WJC{FUd+x`Nmvua6V=)LHj?gS9g8-{rBJf z>es*j%U^x_=^y_54}S4S-~Hm(81~wW7Gk#L-bJyPeS+@H_x}Vaujwilqa+%(NMXD- zOdb=4pF&UJlX!OsPxxy&T-s9hF}2u|vJJcvd#uBD)`~I8;5ihD)T$OWJEj$9yQseY z=Rf~vzy9jyfAG(L{n^id{YO9h{(q_S9+qZ9(0qn$8J7No1Ila?IMWxv>Otyl4L|(! z<7}tk*A$Ju_>*6M_ls{iI8_7IFTbmjY+03~#D2Z|X|A(GJm54HUAs^oN^E*OUN4Hx znsIXXPrv!mPks}c5Dp^=HvT&G%X!-jE)AU)gM0SGo9kD}Y>x^Dn4t+=CGeSHR>5Z14J!s&Iy|SD&H5K=))G?qTv?z_XLFp#a88DxEkfe?J9?sv&lf}ah_pyXGt8q(=lJ+PAElZDGP+t7^ zE!R61>N@3tiXDsgEKg@r6HgcF;yz3|SF|;}8qu-L!Qu&Zp%!rCx8hzeR?iWudgJnh zIzH(Uj%0vD;puzhkK062*WpVH4ZAziaz&Zxv3iFKxG$&SX4%~5LZHSAxDaj}^@+C> zRL?1EU=aUHH-Xhb=F*pwU*T@&1JoqYd=P)Q<%7S`0!{=uQ|g*bydL==2C0<&iK!wH zX@V^cA=B~XSq!lh+5LP!0!%`@@$_~=di>(lgB6F7Y3hTvzh2H)lMEhl^ZInPxdv2u zK!~axJlB|@(z(^G+-^eh`%+Ogp|zl?VNs7`sYUWe>QKq9V6`;_D>Nw#CrJ&YtAD|JHy=lklJaI8U zjW_x(7ESS8uJjDf(4mT1Xw!u%;w5?v&ZiwkD2bPr+~^fAAv!&FHVRYCe&Xde-sbTV z!jr>RA8#Em>rl3imk?z)@e-m_$LS_sBA8lA_8BjUX|3gi6fbK%+$~-rbb9O^#>-8- zwbpdwmuL-%9(F(R5*aaz>doUNLfKEeMCjCU!jB&KCAyIMjGYdw$v&vN}3R`-h>Z?(2yMVYe`A-C}^4x zusT+43=Q+C`_a{_h7hKVvMBeoq7)CP?nn7&+QBLeN0N4h%b?i$8>~qXzR3GwWyULt znGha9j>52v*I1%kk#%f}Znl^S(LJZh(DXMO&_r4PJ22u$i?fm<42>xM5j9t4>{b{5 znE0R0n0gbiXSxY5J`G`b)@a``)jz>2>;T37E7rS(9}Lg(kYZA)^pIWF(-<9o)L4&$ z`Tj7IG6;UbnH!HE1u&b4e#=H-S=url=f?f$YB&TKd8y$L7BTnZ5D0MB90CE>``HhN zKrq$Ig_||w5D1-sy0qsI2wp!N0-@`WLm-&Djp)W95IXS>9c*gHA-3Rkz#%r_wdW8B zp2q0r9Ab^GGY)}PGS_ejE19@)2ne`m4gmq{{p^NAK$sFEo9hq|T|XQG!rPtxymSbN zu0IX|VeU4f7l(l8#6Mzhj!V^6I>aWt_8ejxUULos;c1L+%^@;$y>STCRG0Wd{09&3 z4d%uSKMsKacg-OXV7;IHa0mobVq|*`fzWltArQRX>CcTrAavs6v0$i4fAqg{KntyJ z2;C9P-9~id5D1<4hd!%m$04@hb-*Du;kD-w2%g61<{V;;?wuSWJvJ>ZV4AWI1Tj8t zzK33mt~I)hZc81~`^Zu=e4;?Io(b0dRp;C7d>*bRhmD@0$@aq!>7$j!sgh}}Ht~~P ziP8z`YOLgRyIcLq{ck#F(A8_LrU$tQvTl5#B6-?ruT^}3nl>L%H@dKTWOJJfW#9Oa z-|7h-w&&ARKD_y8f-78vBHpei`=$P2aP|#-pl>ATq*fROf-ikeSGOB#E^m0itte71 z=vEYmU9YnbRX;O+u#0Ctq)6ow=sT+MaXa5=Me^1BN*KEz>|w*Hvgl1IpcZC39$esU z!VS)34H+H2u4#Ot5b+;q^?s;I!{!58D>Yml9zoB?o|fzx@8KWjc9B?tpKjA|w>C-~ zIq9cKif=wcV=@o4=RUc;a5vM1_y*Ts!`Y4p_VyTf#gVKiU$kXF7XB_^&(MF?sXX)C z*QAcq{c}AL&fO_x&el!K?ut3{vAyg>Pv4E&Rd2s}z?~;*Q3_W;%c8|Xb`&UH1sFhQ z+uErK?h}!EH>D=a=?{m{ztp>03ZYy#W-*>D8VorOgToren0_kA2;9m+#ebxn0#ehZ zOks}>AN8J3y#A)5m-5N?`MO0SMHR_-q0Mr`NEVw;a*de!T{w1EiYyUpl~DKe?3Qu0PV~QpNc(Gz6j7d^+TEUzy_p;i8tKa z7iZS&wDAU{N45r<6KZza4wNqfOq*q_%)j~IMa!s;HXuFF*6PFng;9lA+rb7TT1gGk zR_0c2HzE0bN$Yu_4cj`oqeY74NX32u-^da}P4~3)TnM8R`)MmEPp#%QVWPaa1lZ)% zPqNA9ut0&XjF*4`r+8WG;a>a_eSsuC&3pb5S3mr68*i;O-S{Ph*I&Hcx{}J= zn#W6ovY&W~(1kF$eB5fNgzoVY!0dWs+yi}aD36!T_$8PO={jC+;%y%<5xoB5WfjWi z@e-lzCtf0S9mY!lQ{~n7^Gkbs!2^8)P2(l~Z9u0rY(O%Zd{LR%k~HlNNEoo*gb$SH zL9gFmd?d{SZ$QF;)v;=0Xc!yZk1o^WimD;J2-0(}W_3;*kgyTuu$yl$!0?DWMPXQ4 z*p`@CtkO0mS6j@4NBf*6qnK$mCy#0V*$pfz!VuRvEL^lqB(20MQ>G)6b)5NmXuaR~GY@fr?c6)j-| z3v#j#atH{xXAS`Y>;3G8LqM1kBb)0G5M4hU0>ay!{=7H@MAsjOfG~F((ThVsbmAY) zw)D7Ohd_Y4<`4+5-p_tG1cE6s zvOR}D=sMyM2p;{rK6)8w@q_w!{83XvQ#Euzsjcs2K$yFY=*A(?5sQDseszrz>3`*b z5$#?EG@iigfJ1Cquld&;9?*IkqnmSxwejA`A^wdH3K!pV!M!cyhaviDkT%Bh_CMri zG2(|GOnmp#i7>1=j!xtWzK0XOv}NG4FMs+Cc)>;Z=z=Zq@%d_5!cU{)M2Xv5_HYk+@8EFN_gf zye{7YAA0$AKQU2XTt0p{OaW)%yk7X8oa48?z|O%~7Wfv z)%Me7G9|i;;f&dz#f>i$ZQr>)?&JM@8VAd#IXvx_V*f(@8Sl2ZpGsS8LZG>w%#Zc$ zw`PpiX-`?vqc7_N=L<>UXK^9Vr`NmmeBOVj#w$Za2Q74AV*t<%bL|p~`{|0@UGFI0 zHrZk}jOXcmzfQCy<}^JX*6`xKiyCi3+OAkMHC$^W<$f4H40y?OAD0MS?=)%%$~FM7M2 zh8Hh~iT5)debKNe66o~|7UrY3qWodQKFvYF{blzP+*%_9B<)$UUU#rgwL1^(-?CDH zH(UTKk`|*;aGCITdEQ?Zk2@`gurfW|t@CWu0J5JYCS+FRR~h0?mY5J+5DfZxm7VSp*=P#swvsC?rE`Y$T+fVI>-BTPYX*-?k9*&;*u6_y*x6t@N)3sG;dk+ zquO6#iAe#Ta&?8S=7Mpe&567f=d$R$E#}P5;3G zlu1V|CG>5_2_D2Uuf>Lo>2mX^YaiufBQ|_ARTD2kS(nF4i-x$xON6fHc)5wUeY`~Q z`iqxUD4WMigtDJ_iO{LzgdbhRO8_$#KlAQ@xvae2ZzFv@`~DzxZoFK_$;v}J3ynFa zS7=NlV!$G7^H4%b;88g3VYa1!j*J)4g&&-MtdVz8M_zFQi$xE-fxC_P(fYPj^gwjtAFXFw z3TG_vTFEjp#-q5IXS>Z8Vxpj6*7HG*+H*F%53P>wrXT!fQ_= z5Il|1%}K->-8)IdV#W^YBH0&0dHO;;BZL`>E^af5Nw@jzi{Hce==>JbA#6ULtP4$6 zNg{4D3>5&;`KsZ_l7WOde7`gbOKD8WnhV(eim3uMMwOWIz!13A6yE=>I2!9h8l&XB zpvu(9VSGU#+|7ucPQ5f%Wqww*`3wMjHjvq`RZk0|dxe@v-gw}^jSrdJPuqBF>wu5a zo8k-rgeQlsB4X1vv@UY~*!y}(&;yjhS z4X9z5QV6MCt>om?hr{S37d4mAFudLBkCTLk(UI}`>ce4}yN&o!j#&43ro$kwwG^G8`_L4d^YG)9LX46jWeE=TuH32i}XU?1+xw?E@F;WQY?|Jmdy((OsUua{fVzN18Yfe#uGKJz75@|HF}`?EBQm*6YdkFHyG1tlno{j7+66yJYJFMYrH z(NBI8TM+pOuKI5G;qrQ@^TcR+I87@WzJgG%=q}D@EhWI2#zkL#8)t}V)&qXk<9>#3 zd1ruh=Ds4njSieN#qHoKg3jR~XRMhQu8ds;y)qV05qwumlRD}u319L0as9S-%RGsMIaC@lvIesXsB zwEP@l{z?OOVNcC@C|tsVN_VrghNJ$HMCL*)gwL|YowfDW4j#IUWZU2@3eVr0OV$Ii zn)TxdPokk)LpU+lVl+e#m{T-l0imYDarFAxgN^iPAP?7a|HOcq!Z}>M0%UR+JSa-9 zaaMT4kOCh$Rn0g_k=0le`HZB&vNea-N6Fedn$7eq z?x?u=Ck}zA8sVlF(M^af*CEr?-^dGWN{jW}Zsgx9{VqlE3v4};t1OAx+QCoPu_{zQt+M-CtX%a&fts?$v1)(GWHlz2 zl6wU5V|u+3G+$9$D!3p~O^#av`O2!pd2nEv@d8-2_$AZUIBW*MPf~$H8B_~zT~0j1 z6V~pKxohFA1JW}*p1oq>${}q>cq#heI{cCeYtQhwVI1%|B<+YVNq>da$`>T8sbYHJ z-On@}>Q_FbK36d-j_V6ZAtz~*LMtprUYsOjL(wsO|4*JbZ06DC$N0td1)`IP$S}hJ zXYs~ff$IF)N#TO%75>Bw(u$JV?&raQ2+jC z3+WRCrDduYxQdHBBI*O;;pdCxMEeq#cM(9Vh(g zA?+ZTeWxARE0bf_j-zARsi$wH4%tffHz$KQ%tC4eYl?Nn_`6@h+&HZZT2@#zeTN^c z3%tH57V#ySvXs2DXZT#w=E?i?dW18-s7T&f;||u07DBJ&4bf=~>Mwb3<87Y2A-wLA zcOA;s$s3}SDBiqggy__9x=G#$X5Y#Cfs&3K`#X~N|D(-%LwmWxVFHCpKce6^UMFjO z%_DtZv#(>mH+`ozav^Mna5cw1+0 z3{MVQGma%zIV--EC>v*Qj8bBw+R{>4IEKQqCh+g@hoDztD!v$7|R6y|X#11ysr2m($~ZHaqez`5QjseP8))=WjR%7VjtN0+N32mXkY5 z=htiy3|8M3J@d-n5S@l0F<>_xTWn3mOFshPwa(j1I>+$3%ir+OGnB3KH$*9cym|hH z=$ES>-KjJn^wAvVWpa#59N4*;{p7!@Mpc@AiAb7$(Hy*Q^_ys3M#YU;w z9l_ypKe{yTTzv6$y|sGrRRb=2{K0ydq+QSFXjR})S5WeWC%E(piy{#$y<1rJh-hh` zB{S_O{CU|__#E36Jr2l)S^F)3D@NoVnmXjPM~77^vmH}`Ts-V-UU>GBaM%)IOxAzc zihrJne`)K&6%OoG#=gVAQbkhQ>~n=$oGk9m{lSl zJz0C%2E`O`u;iAjjJ!1I@FTVJbUw`+DZ-4)nzas1X>2Lh( z+i!pM-S^+q@)TZMG2JWw23F|gaIiy}1i2f719$W}tb5(jS|5U&enb}v&gA3c^98ko zB--<%E<}eG4ETCt5$%^mtIvK6-?-m0WtXXsz*6S(i9?|f5&oGvOkw-@Z2&h=d#SIN zO@U*;l?$ZhY@`?OF2~E#`E$D+i8&^o-v!( zC+2wfD&T>uC&WR0*Nb>n$PsY2hcYj@f@?YAeD?dkXT_C->b+zVxsqOFagQ8vF9gIz zHEq#_!>lo-lp-a&ught6?r<+Dw-9@bmDcM+_nea##2d%%36%!IYvrsR_PSIj7bwq* z2MNH}pK-+ZuQVr@lM8(R|AyRR_mXZ!w@Zqa?e&pp`FnGC^ncIi#;VTDE%J&l8UF(JH%)c&N)d78MS{RZZ`O3-8 zufC}(CvrK{(|O9i*EAe-S6coo|dMCF+9mdT8U+0 z$(|Q+nij@@#mDt&h2`R*n+v06i0h@WnaygJKHQCA3gJ|{T1612h5aZ~*GOeeDG0fdYJT@(yqIO|g7^4&a5NXe}Ay=FG{Ub%~ z!no@;y5_^e2E1TJzJXQ{EasHW7xeNRc)OFG8)HD|I%Et8=58apF$RQA{6m{-?HI!rybc({CcO5H z0m0K4-JCJ3(Y0X=g$^X=hHMmF9Yc=ROAz~SwF3wP9HtJF=u|f)+2&1hliAFN!@mkQ z{2-mt;l*`q?IwIhq6oJ-Nbgy5v(p>5B!rff)&hr|=al2(hxe`E2< zP$}$0CDBi-t@j={a4WPMi?#IKO8WMdRVeU5>=tX&+X+jo-5kbMub=LZ@${h0@t2GF zd9Fi%lRFin=(vvY&qKpXFV0?ruk{xJ&cAA0qQ6DFv(I7F@gkUZg_ExeSHoz-37O52 zSB(Ld8@bin5S}=`)`cBS$Uwl#<+`c2Axxo7YS&g1G7z2Q%=QXBgtxm?^s2WZIx=3I z-w8i>0Kjr-j-@a&Zq6rUGg_s6dIN;H+lUlG51>psWJ#fi=)^zTEA*T2+Sl9L@T4ef zHX;MzX^aj(*h9>=^r5?Fj>u%_+SJ>{!VA4Y;fxXDi!uglMX(=Z00S~{8{yob%o*Zi z7|!ixYN0*#Hul39kXsEVD2`e8)HD|#6RN7 zpc*66-{(VwrZiLwqm01ofH7=YkB83VM1(cj#OY^0X-#x+m-RG8haW8&!`gW7WDH4X zEJ>l+lTavyUPrg7+61vAg~B(NA~Ue__9KPDH<;qcND95t>%j9!1jo?AIa@;C*&4FU zwz3r84k@2B&=pPH667_g?yUdkE_QygCH z+DHh+=(;IuFg)?<)=vd7y6(yv40E>;oyr=FPW(gn)R}5B{e3XG*i@5K*e($BZFtSg z8UvotiB>`=hNm&Qaaoh2duLgbu3nKbFxg&@9_5U|s%pH1&=JDxnK3}XdKRLPxk=K!*t=NYiIx4W;hzlFlz6nv4RTgu;1h!oX(Yj)5f&or3?|i`8+6w*G*9akCxI#U-O_ZGNM;e1C1#D z(Ow4Kgx9{P*@oA=sDbb_Mz=0%GIZ}OYAR$|b zfG{N@HfIbFoz#Tw83TmZ4P$`l`eO_b=58Z;F$RcE{G<6i=(q`w|1b_*T5lleFNC#{ z%DyhG#zu=c4S3BN1GJvT=xXb6Fo#OhruD2BB)Re4$rzHxSdu}r-2x0Rmq8uA6cEh% z!w(5!Ne0bc@3X8Vq@k;M&=J1k*El07FDfF}$9Nyc@p`(Q22*6>{}f)%t$EP@xL$}+ zng<=>lm1Kwz0ou%!8OR^A*iNU2OsaNWhDl6Fbj&lsg+d4r3mVO1#G*n$JG~_t%{&l z>NzSdSClk&x=TnZ5vz-D&xq5ySKSIEI8~b4l=-4G9wr30oT}O-o!J~f{b23>mw0Es%i*Aeoq3e(_ zAeg(2=*AcjI`NMF(ietB!Xu97@!Dx znO;8%RxN^N@AelWD4lONkK#x8eqXNQau)PFozIsm$)M-!$w^uK57iM8`)XHl@eU-=$L3(=LIQom1+eUT-+sLx(s$cA&^!8?GZWh7mM`m8 zdKEMnUdIwB1}vV`&6jgAOl3q;J6Ol=GzW^&38>~0D25lDnODy>E@&`1@#yHa|v`CUh{&+fEUcjSI{c?DQq6Nf=R_IEEB-+G)4>C zX|xwKNB7QxCK<2v9R6OnK*#zLhaY1A46$bk6x&m8V=odYhA9!TwFHXMb;KCZrQDtD zoFveq@%n0-Vwk&)=*Ae(5sQB`p996l6L=jkhArzgmq4-gG)6aP3~S@PlQH}sDuNXx z&^vss9|{uIKFERR=Y7`n-_b#^B+?efK<`XO)?M8`$3XAA@-c%de*C0>Uhg+jOKaenX;Q|2Er7F zZ(hwnbR7A3DNB3(6T%a(ZmxeqbY#5Fsu>7#w-LRn8Hi5&qrLvQ39o%Mvkgz^L^J&p z!qXVtx|+$*y|bFB&_Az$foF{2V$p3k{S(6LnlT`I>TT?YF(8-{5!*8cgsvmTfZ**; zc3%1?beRqr!%K|l#uyMf@ehu6@u9gT^3bKgGQ?)vIfd;~&kvrl!0Ui9Y+0|p{t2z8 zFBEn2Pf@(Eo3(BYyqWmp?)aBS_T*P$gEY{`t6Eu2<3=N%tIX_xmut_Wq2 zy7UPCenU^8g_NmmXUzc9UlY5}mPBi6bOSnk#TI_>J+%n!QFZkYilOC4MX>u0)q2s!i6pyA?7Jp4>|%-2J?M zzeB(hMf(~3gfNu}ag5<$-)i&|q7zWG1$`QxE4XM^l2`BJgS1&(r31Ch^0>nfL(0>y`x<@u^!>Qs$8@Mm3F$_ zXl29gB;Q0C1{Ynif-Yve51_!*j4ncWJ<~-9SVK`abP>YTd?U=P6Y zy}+~yuRUGdhS!`fLUaN^i~+;kZA2%=fYFJ6G@p6ihS!`i z81Py%1`JPQbYsSlqkAV~Fa@URL_WIEL>2;*B)Ie4+FRx(FimJ5Z<=6YrsDi~vsz%9 z7}mm=tcA%<#1l-@z+dmMBnM8G4g%A}+X>fzJEDsaUe9z90+tKV%@a9%?2-z>mxVbJL%%n*?wM7aL%CJmoo;dVdgc#G(mV>GX`W&y^X@Z zEq=%`>A-EQej!e282%hqxl4r;l9;5 z&y6u`S+DsMIczJK$4fo-Cy-t($8mY)1J~qKLWy@{}a7T0z!V`Nct?6e66Sk*@BDt&0 z=_0hNq(il?M`-Xi~*tRkTD>byN&3^7!W$~ zkM^g1SUyv=bKe+`Uy;Y{}%{Z zwUGcAm`XPjOmO|Y+{9d98ph%B7^lnaYRPw;EFszcpujY&Prom^2;uci7a?G|0R2ob zL719v__8%z@=Y^+xx*`mZN7sE!`sbtyy#-dczu1}@I^-SqKnXo;vek=rcHS5>EbrL zV8)gL6NINRI{auTFlFf8Nf#>wCiIz=s9V#TNitr@92aZe-A!PE@VaIUlsVSywnKpl z+EpT=CTFWi@M=jxbV6e~U<~Mt`(X?S9T~4T#(-e%HliD2KG1vmrupYWu2u#8l&R1*b*-v1~zN@kjm{bDmUYy}+~yuRUGdh8N7(QecAcG)9LX4F#qQ-8<=G zg}`(t17(7U#25OrC@|fP597KCOb}kri~$1H+t>|bfG{N@HW!#6x_%e~gtt4{c`*iv zPCPnl#~cr>5{2d{aEY?{WH@`I`IBG%G5ufr)8F{nx8MHiyYIgz(fWL&FRP@%_Ob}kri~$1H+t`i31Yt@b;I*cU7@o%H#&j`9_fEQK3rvsGdg2{2hKJEG^Aeaa zysjAovZvn0ei#FSDG{+fV?gLSVhjl0?qui27!bM+83Tg3+lX$A0ihHBh%*M#}^wF2-v803rqSDI>rU&|jNi0VgD$kGxsMqN_kgjwZhRbw0kJtOz zvM%RJ4}B@y@ZJ|&gz$Q1ix99zB5|hX`Vxex=|+f|B`Z2DRCqvi0;>57ISg+%(eYx7 zCFAwAP~k;J^kR$9h~gjZ+2SU=_H1z*UN9qbZ7T{=*u**SgIpWJ(-<9ov>RZ`9pjyB zF%>GM9p8`3df*)~hO2e9fwt`?H2+n}?597*-?+o@x@HW>o_ZVmVGIbSM8xL$Qt490 z=bD4Hah?ja1CgfsaTmUn{V)b}wEgKz@QimG(Ty=6BZ_}CA7H{aP~dgI7`Cj}USEP& zPh)g*#;`WtI~jwmFI_2=l=USk>+V*u>8CHDPtv5JAj!av^Gg-13vQX07)=*2B)WF2 z3&QJ}bwR-D(Q@V{i{;x3S?p$sk`agh@tZGy1hrWRQ^U6Sj^#RDIt@f8pxWy+5Z-RW z;HA?*bmGxWsDJf+O+$)e|0`}_X)nP8!rW~{FV+RoiGQ?bU7PUQv#xD;!Hg|+8VFBg zbZgd?p?fFms$dKpLKd_O>NMw8b3hPY&x`>A*4x+(V}LLvBJv*wrr3%xKy(7CJ!63I zb|*V8#sJX;qCm}M{=+^P1BAKTh+d2Vq7(mUKRLV!uRUYfhS!`iKzJIXTQi0X-8&gW z(rJpFof|pnnlbP|OXwU+kUm?J!$)j5vKTAAUl2Gcr<&$e6kQJ0wsM!Q)W$5C7rb{ohe%?oEuWFEy=VK?9pdfx`G_m(ww zTMNGEzF9plN4o)cNOrR(-Hzg_up3;pzYcI-=W5@#5x9fe&3u9CX*b{qS$wmWoE^ni zVK?9o$!=cpqvpN=N674Em12H&yHoC5hu>tkhIt`d`H13MJ05Ze?x1$Ndc}_l0+;Qg zM0PuS9jxuBXcJ{JoL6F~Fx&(@A#>ETrIq=4D%y*whI9Bb41d@6haYMT2V5aHoY%?P zH!j--_Dp>*Ua6tRa5bsn+^%NGmZB^S=M_RK4EF#eDZg=ECu`rhY+cK<;m!?{1KiTG zCco!)Rl@^#gvL1Yeaz9=Yu>o&1b4<{hVwevKU4Vu%OZpic!V0xi(S^(ttvNol?@ek z+bkQL)S4Q*0Y}Iqod3ao`3;&$nNBQ{ugeDhcR@}HpEDS*=kfLU!br^zKctOW1wD=s zqMIkZMe>$_)FHXf2p5$hP%;T1I${DQN^5B3oL_Ik$m{-^yUbpAoZ$$%wdM-p5C8IW=tIz+5`cQyvtO{QG;7tamJxl%9}DQjSy~S>6yT8`#fP9|#gGQAQqE_X zp@2?2I=XiI@y7zp-A42oW+pa8hB^Jzb($=vaO@uZ) zpuL_c4lm(ZG3O|}|4SXFn5PH8AJUspxqtV;O;xJEh~|aZHKGya8q&9Ay~MhNaSl9% zcZ}0%MCUEhJIFGes4bwyC9 z4Bx4wF6a>1pbF~{0WinJ8L*XDjuHtQ9U)oJuxE5+>0nMT+tn~wG_ImDgeQZo0tfCQ zM{aj|{ePyIQ}B~Bnz|skF(yhm`tWn*8i40wAy96n|?pLj*s zIiqb)?~aM3u#+uQB8O4^SRBSmC{FV68G@73R`q>#+(ShhoC4X0YWfcjpiDFi4}2ytAwn2ON4nEUa?^2&tA zu8|Qm)fk2USl)dUo+X}~QQw6b+meR6josV$kN_YdTu=86;!dGOd4}y3Ri7jUzJBs= zc8^p*7hM?jSol_kBd%pUUXy=kh)yH7_{Rat6Y8`588I2O5N{x0i8mUnsbgN{iVwDhlH!vYEWs)*ViR{1|C=MMx zo5_w|Z(WZp@Nv9geD~KEOqKYu9WD!el7r2G zrH@x6_;Y#_%}cE+CI%SN8-)~A-7V&F6yN`*os0kBiy!~y7clybOBeA{uK!A*6ke{< zBf*uaW-&8dv79rzO*qtu!f*&fuiM$-^N4wgRO)}9OCt`!kle+yGTHv?HlzD@ddS5B z4vi0R{jjRhuMUez>OrCn8!Bz7*OCJEhHlx{-+TuB7A><#b3mdJ`k663bS$2A){WiTD+gZ?-(*Ie7@Y*N5i$8YaE+v)}sS8+gRok_HAC4anI;>4BR++dbP0 z+=mc2Gh-DiT5XBL5$Eqx#KGSHn=Yqig8PkM@hwz@V#GYbn9niygO02wc}~H%FQ+vv zWXPf;#UJ4UygXS)fcDkAoYu*Dc{RD%2+YteK-xxRM=6Wd;R3J`U++B8h<|V2%*|{bu7YQR5odmNve1xLn|qfc}mfSUTW!=y_%pDLZ-@FexG>-gAMI4{`+weee(qU$_ZS zu;KVZ|0Cc36Ck*zA)KNELv-T6DyAREhaqA?#DVgZASZ+3CIvR9(8z9@6d0!P47ID} zI8IFpj843{d6NReqev9W!q!a+j7}W7n43rz})PW&Snlk%V0=%K&P z0U0~hf(4a*8(s4rg#k~mDsgQs^u4RrINknAPG#oQtwf4iK`a57W&GSTswj1K=xx#BH=4N@4ui`G0FvNsmoFOrNb?*0J;pQHzvHG z@f+n!Hjgwn?jnPB+?1SJcEy{w8uW>MP~ikx$6XyhAd+>w%NS8)5%d?`e0}_z5&oytS1e5cUEmgxx$Ia8`?vdWd3`v{tXYgVg=HOa9B798xLV`i z`vcC5uqNWby&Zbcj!s<#4tb2kY0q$25pl8s8;*u~#PRdy8jL8NWD)1)Aa7+pQi_UK zIAgPpI1WS1^%3U^hitlxIM-}+<%zwD<2dddu(wQHSxH+cn$LbCf#>Y-ZMpftZJJ1* z^vEH;jV301MVEn7DiV6cgs;!hwKV&Rrc~Z$zltlEO{t(Oc=6lCl8(5<9Ij|F$JV7)3r5+D^y%HRMQV%=dB3fLMCqxCq$RIM;qvF2?KR%Z0NOpqGtu9HLX32Ksn`5($l~}TK(vUpof4*h=0q5w)e%X6pY(FpWP3K#* zAL>ZF&3;KUD%`(p{Wi9rTWqc|AjlnWGhix|N(NlJN5Oy&J?=GgDP6%EYUaGnf`4r{ zk8fy~xpJx%$9L@>KfRzWld`A%j;#XtS~9w05;~D;k@kz*rogpwx5JW zNGb2A{a2LAH`ITk(DeVacb);46vf^@ZBCeT7IRpA&U3PnU04An31UPPB}x=l!JGv} z1r-BHl#BtzoO8mQuL)5RbGYVs^;Y#vSJj!WK0Pzf^fTwfydPeE_qfV`tGhy1hp}yt zluKoPZy-3l3%7Qh#-bm=qb$v*#e7P}TkWvD?N3QJ;;yVMNQPGXxOvINhMgYhhu+=~ z26`)4wA$Cr<{WLh`^;bMaBiqt;Eb zDkyHgqO;8DbM{N`8-d=sas9(#B1&^XHd%|@>$!Vx;fzNNYN4Aql*P_{9CCH4WNhE z|4hF8lerDoHG$aeZ4F2}x;4paR9oFWJ&xTWeZ{&#>SODrJLVjniQdOBr9zsC2_9?C zXJS;K*WFeHraNvQorCIwmah838rv?6WA0IX3YIkB$m`8Y1JcSf!OA!n7~6jw1&-Oz z?e8UPd&Jc(Sa?E(leX&-SEsBy?cB>+M^;^eq0PcGSN{68_Xs1Lx9W7Oc4xcE1_VfJ-m@GW= z0EZ1&^pW*6Ag2$AfZ74@k=N75HM{lTtgVneb=LSQv*^Ep?_i9)yBz2s6kq@nRenW% zf$ZyD-H_HpGg^5GO8ZP72k4;viupseUxLO~(SSk!7_{jhe5x?`aKuRM>${;%xo2ET zoD!btz*$Oh@k17)l9vy>Ll)`2f>J`X_Rb33Mnq}PHy9JQv9h+`ax_udO z-YI(&=SuCA;!3!;*A-S1c40z}`U%~;glM%v>{h8{SzmO5x}$td(vEDSR(G7hPSr`W z54@C#NnT?XC&O~aB(FuANwvmwlE=ejyFpxN1`1)XL#z5#x^03I3$a<7g(jyRy%R82 zTgcifWk_ZzK1dC?~Nv}*00Al4Y!)E#S$WlDdPvt!PT zHm|y)TcSi8Yi(s5=;ZZdyxJLS9b1}&B(hlTti-p+YmYi-zd#plteu0N8||zi-h6(} zT!-6yf!OHvNzt)gF-Eho`6W#zkL?#)bB(n}Z#Xi#3MZ974vhi(E$os~pRdJzjjDX(L-OFXa;ywZ<5P$xq3NU5tr+CP^`EN7;dIz*s`$3;6 zQE^?ufLiB!-3-Y3f{PRH5V?OrZK%vjTG1!BVTqQ5y20WG znU=@i#2ojV?}hnR-DNxN;LcdxUm?vj{;A$Z6f<*|&APy1$v8JL-LMZVUiF~K|O{y&X@U@_z>!|xaZj_x3*JWa6l7%zvqF<#Od#}-;P2q!O*mGY<(mZV9 z?HQq^rP}-$ZlX2C>15VWa+-KMMsqO$XN5KB>U15_lfLmb@%Ch_L>RElcur#T3k$ER z3kbw2eXc5=6X;?3ZME&MMx@bhz83P07})-uct?e; z5v`}d_Ah?U6W@AYP4JSx7pns^QEf9*J?UII*>P(w%7fuhYcq4rK-JkgM8nWLRF4h2 ziAeLX`FxxOn-ae4^>XKZ*;R=NngaPWWM2+dhH9_WRfg7hn%3&*@8EbFsdVlc>!|&q(|c|p z1AFaItOZ@O2OWv;B%`wvSst{<9M}dAOw27PdmugRWL@o5SZSRL;7Hv`F#(k&xM-i zBeZ9~K%`lGExmC~C65>9F{uR3)qh%jHC0=xqWsHzxB2Xmjc*Cp^^TCw*9fkTNq3-x=b#>-r|Lm+n?JBb zaBGlML(K95F5N4tO!WZPNFbr{0-%v>)km^KV0fE=Q)Y zgax%2ZQGSnX(|iO=~LI3Q?DzHLo9-fUCgB_0OP4Qbf(?PO7Ngx*>Tl6<+Nc#GqCwi zd1-yxeuxWvq%YOD^1KVgWv`8ov(C!GxZHl>JjL>Et~fw}HZr3?Ye=k4n;+nO=^?Uj zU_S}o%qU2-+1JY+={}@5xBSqf%fd!}!oxMW75S4J8v{pGrrMEbihoog_{VFa=RKMZED+rwudc^u z;{t)&d*cQrQl04fgb8+=vS}ENgWfL0+$cMJ&6f(9y!b3!cmO+F^x2_qe4okNh6oI> zw+%5jz%&I@l`DBX4*ChEu~yCZhnA^$u5vgX6kl{oq`Y!X)302l4z@I=Ixh27^GdM1grRR(Rt== zlVEhVRX$dMF1VzM&avb%Q#VPh*GhTmy&~YX`WA^xzRoF{mzPJqsQJ_l5-ShQ=k%o- zU0%b5oYvbRnX3()9oONLgn{w(c1Y%(Z=Yx0i6FJx2I7WvS7+y9rMek4^X_rXRcNlj zNY*44ZWP$!o2eTqwsgcA*M%Z)=u_NAFS=21E$wZt%vqH*r`qBxF*)_^6?^4UHa<7Z zns;?CO@<=8x`RP$R|}{$l@)n;6WC&zX;&z?P$Pk<@_2Olpzp9yJO_bE_xPffq_)38 zDV)xcG(Q82ejFTMWq*bBksXso$LIj7pLWge#;Zj(Q}IDN?fsdf|heJFQw(g6FYj_WVWI{ z{Nffb7-H=3dPPk?!DW=(qGd!cT==_T_eCdo6u5$DEr#3FjOC-74!-CFK}joow5ogt zUxim%EcRpsh<0H!+1iHhj)uOe1`4&gkgT4_EyvGo1&&03E^WBo8yLD<4QG=Cy}n#> zJW#>br8e3w2BZvBrNn7b)cJZ!aU}wh*U8OuBfWT{M$r{mnK#}QC?Am2v*$1a=Fg24 z1JcSY-AXLlCYY_LC%#=kqN$p!r5gVh6l}0-UKH^7$!IlM=aQ`%3{q$#1n6S$;BEP<8)n9b`Nz|76cY+A8hQ}A(o5oWaU|0UvUb~ z>EqnpcoNLqH?FTZ1LyR)DL70OUZVQEzT=!eXTN0E*Ul*s{?yK+%fp4*A?epxc`q>- za29v@SakE_?d7|GLwg66=LX!Z5ffa5F6l68o6ie43Er*G^p#mAXnj8FW29UrloD4A zI9Sa%KTavG#2Q9zp@CCG)-WrB=3kB~zlQOSETci~gv6$bvuq&_jaFvq9(IgZW_`8i z23oD9UDipKwM#5v`raXCG^<+=%pajj8r0)Un7(>i*v(oeIo&7KOL%=B!;e=-x^$D$ z>r06%){x5+lKN^7Egq7l>0V%t$I-spL@N=J2FZFs>_wr{sPDaik7*;eVR zJvQgy>ll+bz=zr@eYLYzh)L<%`q7W)%=%gv%}r64fQm`8KT6OD%IZ4apV#YR*lj9A z(7C!ep9BfEq{_+zo6QAyDGy3M+x<h)?+r_}6VG^>s)%v5K8*m?nZ3mc@g@M*pgnOX0AY7s2{iuDYx zV$*OAokuT+*#_+<=d#lm2Wt8Bd}gW~(*a!k#-lRJEsm z{Lso2_Dpcj*lP*xw5PfS-}jn;`Gm~26k#np8_v>wrk<;vVD0D~?vtMFW@G{j7UTD> zyQ?FtcKzHu!~mYuqc14>%f_RQ+V99MJM44F{zo5vOp6B z^+a^z(4Hx&93il*eye8S;}9&zM;U@`bN zODN54B{_7>{n#kuSH!BTs6Z@rC(cb?YGh|WW( zN{bp1CdHcMJtC9}H5YO6@Ysq?g~M2z2dC;GzxywDrUT3nY}P6pFlYTdSdF>dOL@C&0s~vQbyf3 z5P3{PNkiQ&bP2aVhGpn#f+I3{DZ}yt{;D3iw?mrmZY3>UdCG;Xr@pv^(E(@LbTEa- z5t|>{+;}AxChF&7ox{??BvuyGoqSwK2S!)jyL90$yS)YBuAi#$#rlGUzW3Y`IBm9K zg8AnnO7kO}E$sO8NnV#`E2oFYR!ytk_NrVpMZFo%lk4#rW!e8sS(U_%>C4I^pe?Z6 zswplH-K%=~h_9M-eJULpQCaXu86;#QVe1 zbU28%O%g8yb%WC~&{;fIrzKhj>gwEdK(l7$WNfcG@iI`CS=oq*mVvrD=kk=<+gb)L zmdDG$*c7aniMZ$uECVC*tQm0`7+a;*%Q`GCung4YQQEOkW|dkm?eK{neHrMhTbE^E zRr^<){&BwRA2JPQ>+xwEPsC3CSabVCy^?WKEuUR+YpUnQBXs)x&5B&7Y{ z1W);B>o9HU5A9E7=a&>(z;#Z{pX;u}xQ-01b6|<@E2Iq#yC5jjItixaMGFUUSSL8b znz|+m^Lj~|y&G47SI=2~+zgm1lYTsYXY&WSyx0n@g|s=@1XLpOdcDr0N! zdMVmd8QRj&hnjL>*;;n3nL4ngp{tVj4V4s+m)t-e4kzmm}=B)wco>)8HuFd2@g zSiY`5hsHQ z+{lKt?Mkfj4H{OB^jUE<*RRyg3MQO8MTp%t=S;MDl&l}fm2WUlYNv!bPof)_iR|Hi zkZQiX&M48rQst4qFBtDw7)`YA4zM?719<$E{f>=~Sn)^s)qC@o7b$_I0iD$~`) zlrdvQtk)3You$AgglU=cmei+;MVw%5xp0Wtr9&xFRR_jfu3dk9 zj4K;)ttDk8BRc=cGC6hxVm&kX`_xF+JjS7hg*wJg9=Cg9>#usXB+Co6dlt*HI?MD^ z)>=eXvYyMz<*iq*h~%|vpxxscZ(Z6wc}gzPQc*AMIP1~M+AqP<^((}YVD;_c*{XMY z2W@I&BDN`9X4#PJ!o*yqsILwezbuY{1PifLJI8YZ)lycwUql7Ehpy+O{A~GsL-XvqJq%(%GtZYX?2rmJaA9c6FWL z?f6_F_o2ew=FsIB>mDt4o4r@L<>3knGlkGNgvEcX(5~Jn%`G~&^4HzQyM7|qsLpLt zxwWVz^qXX>>We+XtL>C*-hP==m$+EJs$Pn5;d*|PJ?X`IUQLHCv95YIV{oXQd+UY9a5UOn&ai4!XN=6g^;5-Fvdp=Ta@u7FS=&$?b{7D~9vv+17OGAaWPe z4J?gqFn8(_+$t+;JEc(qBRZ!pSv^)FEP97;!B#Vm9&qxJ**?U(A^l{@ZpM0?% zMBTZ^jvMd{?QAeNK5^xQzL6?y$K@%mk<~ZYMt1U2S&wI5$a;!z(Ds4twW9Tsc}JNt zEOd@}8OkvA*(1txZgdM}*a)7TiM4a8yGve%l~}!~M~7X&ab;MtodOmZ3^>}!VGcOh zh{H*l%4fu;df?sjfpb4t9fWYKyK9Ge<=pH#&N?-mVYQ{R`;U0J%Y zcyb}M<2q13X6npMHQj7=bugH$uE0@{(Qq;uO{3Crq|_53I8*I^CO3+U(>Krw94v0~ zTiLdAp2)w%N5(>_$@`EghJb8|0m^VzWHBImIiUJ{&BIiQGI^kO^RAcpPl5vC;g(*H4}5=T~O~ z@6o2Y)Jda0XkKF3*rRpPF?w1HLb%H|A(Bl(E{lU>thUUlrgPIUL0>W-Wj;X@xSKuD zn$qa+)<04e!?Cu@zQtRH zq|=Z+Ikd>rI=Ze>&6KW}C+neRsP;$Qc@$E(+Wga!yGQh ze{?RT(0RQL(X*_z*U9WEfr&W8WqYxzp_D?GtJ8%W9_K(r9C{ae)XH#_s&rt1p?Em- zTB2BbIbWe+PStOWH}8gOLzEg|pwzBSymmwUF_PL7lM`Xks#?d9tqSTh(amR>JOY() zaIdi#aPV}yn_PG6$9Uj{j)H%qM7wN(1mjkv$?7qdr!NbJT9eK#T5$DZ$2gfhE*AsV z^ZImlfL*1X+?NSB$g+r|~ zNBzP{D!<^!9I{*SAe6LQ^2o>X0*OOwcP&xk(7Q=(Bo2KWf9SQ6aCrU1kzg+(4=g;w zfeGmKlH>$T9+!FL6gUVm^g2n_g0__;N~;9tRLe$Y?>$iznC*uNfz1b)Twm-6H+*NI zP@3Yh$=VNey81p<-r!|jsI>A<1)}-f@S;wec$2L=I}ylz z1-DPv{OIPBkB2wVJ{o!F>!kVUpw&8@`qabU$ZKy|@1?@U&A((8(Sd;G^IOUKd8FQHw`LRhE!F5o*GD6@HR(o`>gNr^ z^btB(AgGt_odw~aiCSyL3=-B?oPv;nT>^Fyu+0L->z#tWbt_;>oB&9TzT=NR3fqZ{XxakPX3c|F)%TV_BfvM}iI&E|Bc zOu4TZbRexU%W2(-jupPx<12qVDj#og*Enz!ZKSGm%YNE8^Gq5)%mV3H7i_iJ|4`oN z@@8nD6S`Qh^ZS@JaY3$HbD=n1B8DFD*3Ic^U@~Xy@DiROsxGheqp2lT#=;OLtOitu z9`M#xg1=aACtJpb8Xc^O}U+&E*a%PakWw<7MI!KxKIP6NLy1LRqQqh?s`S!S)FLTn1`&j+Noqc ziqRHOUhJLjdeshR5PfsOHy&;-tk?IYxrY)s`!ZG!AK35$HlsFREOS&pRGf9jpHF8d zAGmp!Z8vMTabYz#xBHf=xH^-h1deZxu_~I|280zpmHK|o(&8)4W9_^FRn{)^Cfm^G zSr_mL^`cMKB86G!^ff=}@;dr78tP5kZ&u&r8tZ%dF4R#IchvlrR<4nE-Ga6L;%hU` zim~;4b=S=midVTp<-T`o276+zC_u2)v+p|5_jsy{&f*O(@xZETwDI=(QL|OKG1;$F5x&ll1j(Qj&i#}*Nw`fXm;60z2Jjy{M&gYb^k1S5iRL`Ge z(@?aNL#~5~rI>6ef|Cu`#dSQZLY=6-az6GB0u<+__4no@Qd#F-cnTofSHu~b%6Fo9 z(RiTw6pLYsiPM^y-$?`V)btrm!SEz6T+N_QH#gx8NMQ^c` z3hZc)N6JxvC2G*A@?82`Q0zuCwhdX;7pJ_u3N+|c!L;~uxh6K?$s2U4OgAyft})L^ z)u2<=Id^WqL8r=dWn|l*f?Mp-nQKE*iT0LKH|Ux%V!Z|(dvT~ir>b-Q;SylfhN=2< z3)QvXpIa=?Wxdw=bE-TF(QWtVJay~YpW})q&?)f_=h=$O+02FLbce9w0>S6z)+uuW z>4;vk*ZkrFiv^c1FxfX+;sT3n(RqR3zP9L(y6@Vn{$Qs`yMpZ(t{%5+(|^{Q1>=R#upRjVp*X{uILoh`I*d#ZHBmAYzGWmYy~y{eU0^P#F$ zRpHLU-(a_Bhu*n1WQI&oYwByMDu zI7>cO?+&=^k@75g8FAWo=cwtQQk>YC2z4;0#fl$$tS)m-d8)vNAYbcqe#g|F|@R86L^#08hJ_jC%{Ro~Z5dclRyo`xdQM17|u zKjjyY4A(OfylRn8fD&@YQ@$&CV#q)oJZYrw#&c(gOe| z(ZRTld+xZ~w%P%y$v}zvPFKpHe?Dsq%|z{)3|ezIjm4?BG8@GPJ9xAo#}tO-sK+X| zVT9)dj>}9`y)JZ8d#xxKd?zNjURQ>5zP{HU(d%3%mW7qK>GB(s0{;R8u_T6-Kv)n_jkAb}vS3MS7-mnm}u(e{C=tMdX>FpR1)-BhIj6$_(@4pix*?gqQ{ zttnkzKXk{K>iW`p&8F8)EP+)6HSLghLPQtdeHMLk?>swP1 zdDe_rV5F{p0yiFYc@(29puBc&Jo@U|*_sO6c$|1=#8I9NFJLoU2VjDWWsd3{m8=0! z;>P2oa3n2Nb1!=1apIjm%d4$GPbb-p$5(J+80<@EALA`styCY(ykae}?cjax7W<>o zj^3#GJ1FIgY;SLQ*^6vduro3hgG*FotAZ(jmz}oBi)>Y}^_nazu;b#2J8V$hz~+$MuXf^M3%k1F_=auhto=lLGS*Ey zCojQ$kIZU8&A(siyK4zB&_H%TGaqcl*dCUOJ1I;L~=QyOh~4*WLwo zhHH7hTz#+B)qqZ8an-akTZx@I3pR$)YN5WcaP823xvJOY6;SL$-_!HRku~n`S)Jp>>L? zjvU7V_KVxzFV|$!>mAFuU*7V2!14X^>B9Gb*AU`U?U(CEvS@Al<+?hV$CqNiT$fqdh-hpCEeRo(Ix*{aOSMyyw4^JYS*$X3-k z|FFiyGAwrV3H~~3>XHRcRZbVGYrn=`EU)z%Ta`y4y6qae;WjUk;z!Zv0eorkFP~^d0Hxpy(@dN7~sl!FvN`;JKj<2;mn&` zp-Oq!-Nm!-(R&Sb<#DqiFu0gygYs}?X!Rv~t23?-YLBo!XTJn59c8mGu5XcZ;7c9Z z?n!I|zwY?Pr{5y)SaseT+l8nrFM6J2>RmlU_i%+NohL~a{7MMa9E1cy5oiZE!Mi}k z2^jbS!_>Q7gXzT8G9aX>aJeS?^otb+LmhkxtUpfhwXD1;CLY<@WjpQ7 zjj47Ihrgp_t&4}nnzw4MkmFA1*y40tjEpeu=rD8705vZ#8+Nwfrd`yJ$W6UlICGAT zD@sg!s|g;`$rSQMp|Ayuu@Nl@%H}NSA8tx8FEhy((xt119dzWDC+~9ztm@S#EvDXK zWT_ng3RzHET^$VIxv0Syg2ZGpg0_96hJ@*(FZ5BM{mxLCBW*Jb1y zGjy;~m*;X3nX~1CjU`parX&V~>PH4kA8gcBy8Zy!GB(ukU#OvyhdR_yRvn!j#}!6x zA8b^ONX5|-9&DU?$NuMXB<5u-mzqzJdB`lW=c*m6Zz9Ae+jCU~(^Mss7CUY6$KFyM zY*Yo4=a&ug$qzQFGVN{%6ipPMVqrAZ!A4b`8|?NEHmdU6nxpM6t4&pPGLJ9E!A4bP zWh1V&8VaRIRh{z>T2!>Q>AFx}`Mu-_ zy#y3*@XoX=FH~$VXM-26R2KG?u-A=QSL})fPA1O0lYUTfHWK6Q-%g#Yn8EElmK)1?$`ln|U>{FcOmo$xPQS#9 z?t~}m@#=!f^~*+in{QQnTGNFL>TN=W4jAY%T@Q;QR4k4r>hbF8++erf4)T@HL85nxsr6D7(wHylccy)EoKU(kcE|%ANk2fN(?H;c# zk7Bfi_IkX&y7lbwinh?^hiE4!XBa`|dMro z+UI{LyZTj2vL-JtPQ%TKp>|plS4vp5w%nW;fm6;+y1ByDi7Xm)WfE=o1e5Thx>Z!# zp}JKS+(g}4FD>)<2bvR$^_DGL*P4xk%?VZB(lsZ_sf-oXIGYLm^E*X+_HEM8h~PH0y$vgHc4Pu39Q zlQkz)#WYu)Cl#|&i9^PzniHyEa((m}%=IzXPu`qRWx@oobCt@cSR75&oKV%d!EV1f zp~_pD=7g$FCi0~?fHqTQRyN{V%b`$nLRIJdqi=(HnGO#I{pvZA8&H8GRJ!q;y!M+D z3$2GIG_xrfi-Hg~*Ux$sqb;=9obZgdp3Mn9QxiCVHuLUG&NU}ol@rSxDO(eTYm@D@ zCbTPtlw19pC0T2t#Fyx1>IK7+)k^ij^meC0U+$Pqv_odDrq1;hO}R4Y$6D{TC9{Ks z?!?ZtkBzL`p|ca|rY4wF?Jla?k&%`cPrItHjx9!q}>``oLyA z&l38ynFtNNd3~u4XqC{XH5XmoXa5OFE!hDwu2}+)EX=fH-`?=X4t=K^-WlY5Z~D2F zCYdjlzA_=&JNk`>S(hM0s~;4Ig=jc;nJO{IPU6A2aP7D?HA2>K*E&rfBqZ3)&IL6^ zbdM-WA0#B0YV5MpHfmjn9LV?SP*K$@U zq}R>GwJe{&2WwO|i_Z#0^_E>d)hh})wdiMsLh@YbA#=8Rk*T!G*xC-tv>GE^@{o2& zrAsqp%UBqP|3VFwJk;twXh6~Z~76mZ$16&n=jGVLD~3dwXW0J3W=jHYUl zsYZlUWNp`ei%gYQxw{iAi`Lj8Q`N~lz63{wLNlQ6JSVUH9@#?cwR}`4WId|REVOl0NK?05Ju*xVUW>S;GX=zD@r zUaZ=yJN4#P$jZ0RAgpLtKtR@<<0e?vWsb}Gf(y~|_huFQ`xP4!;AkF7wa7=>zuDZi3*FcK+m2s*_b^P6nx$L6Xh8@*SGO)B4$WKwU#2 z*H>JfuO$5BUUV+@wKw=_XP$f|RT_@=UHhS%AhxtBnzBEd#MW_ZkJ_QA*k5TgD#qJg zeeXytlr+w(UDER{x{}_a+IPEWZ~(O!Cb9Ilf^mDoc~dfdu_C9BZc;2@DNIuPFqP}X z<$4@=|4)bLfy_{-M{6M;*1_ky{>q_>uc=JmjDQ4m)(e!?tM2r|%u>#Zf1@ z(V8*XM#AC&=FCx#RbJtp3zKR|=F0--QF_I(&Tzdpl5lSIwKZ@S&upmAEk)PT8Lrn- z;;jLFeHGE`ygTMR^*TykR}PL2)EITLYRreg>1rGu;0>YNgZ4P^p#8V(e$W92ZrOCu zmP7YB>fmzzgA>7pd6FD6Earpi2Le6A?yj zv{eJ0qrQ403Jf~enTci1x=Y|!Qr-+6_vo)`iw-Oy%N&)iE-dfN@SUyDji~;rHtitA z&Gl5HtBcwSIf;jDH-HJ|E!fy#m)wlK-4*K=1VZXYb+=+P0vlx+^*J~QQyIGS+9Ok zmi^D_EzLbyvY>RT_LQ)sP|)E=B#&9 zr}SMtee|7f`g}VHAJ2yU;b=M>MB^0%y&ALC>nZ53xtCMD3in~0z%*sZr*EFnYMCbBZKc>ZlXu9#7JU)mPn}YQ+5nB&3k=n#k9fagr zKgJq-JW4!C6URE4t27UvM5U{DWeI`ezBqYROH5gxTMtd zEpV)3NDkYtG7?{z;YTjfP(!~^$IZ<(}rdI`b(AwS7P;J zQjNvI#U3xk6_>2Xvsf`vTjQxE?5OpjT^UIuJ!^hLDIeir-x|Qgg;O!v zHH^5v;vmoMG{Apo^+lJg)yFtn7smB1+zG)G9%#R0cYxyhiW6|oe$Gjf-Jpu<^KMYh z>4V8>?Y9nBF#=5q@2fC#`kehH%{y?(`RCoDn$w4UE}VbU=Hql(eGC1=Ela1($LLHS zH=V+}!jqzlU>td%dS@7sy3KXuYzEjxkrp<}uu^S6r4(^&%5*yoB{BUXA-5*?P(e zjKv-6{%X4$vmHzo_3lNy+YLkLo3D|(4K|OeitX5jB*zX==z?98EPsFk4PRZI*O&_( zpwMNyIEmw*RcDdsqw1YUIDw*M$gR~;ccNN<^+q=yOw_D3>`L31&jb(IFPwnv(t*P6%EH`s0!8k z)jKE;1q~&P*oamy{j=3FPdWWg;i`K0ocz_Lj`rj!Wel&KoV--MhPg6T9aQy*yvrFb zWhDDRX$gISPKc|bv!-w?+|`|ZWtMf?34Yh4E-vd*9^iiq6<5J!op*#k_h4(xfaXzu zxEj0QT;05R(207tdT8EFPkV|tP1C7l@bS)v;Tq57lO>P`QoJRo7aLOrpXlIv=fKc< zG`N^#+a26upBUv)7sSa+WxXZx^qQlw|0ljnh|1Ie4WcRq+$g-s(fp zZmGwnjWiqTTC{{dEkMG`a=yAq)uiJEL8u+#J%pJ@NIMfptK-37-2C9?xtX0h*g{la z!lVEWkm;PpZnsn);9TR}2 z7_yeJ^Oyc~0Q+h1QHjZHJn4^D2jl4J4UDYSg~#vouZE6BSjAKXCCfNG^1+C%osS?V zFRd`y#+oXxiIARZV@(xIj_&4Y%+c*?y!wpv;!$v8O_fQmwg zIc0Vy;qOx;vG5p&wuV&WIeG1GtSy#jb+);&rplujZK1`DHBa4oZmgA9qpAlN-Q06Q zI@KDLpJUFi@$PW=QCoK0Rc(uw-$+ZDYU1Tll zLvnEzG^eLZc1cUCgOpmMc8niZ^^Jz|8d+20-%T~>!X=H^S=dgC>AGO&QRNS-VrZzY zuJmhPx=a^0smB+cmFREPWkz57(&c$|n-`a&xx8Kzpmf)7v2I#zQxUQ>uYKvlp#sU; zl`gEt3eZGt0$rV(4omYigD$hO5$m-HI4}j;1iCus^7fxCb_o`%<1RsL2G&bNB2Vg% zeMRJv^LneP7wEC*@+j+ADAUBKmvwmWP(Oa_t6P^YK~|&lX8We?H}9#ZY_N}4 zn9+fB+upWI$8|VC6E4%B#W|-D>L6O(;;cf2&3{8IuX#DR1O;kuX;TBk35?bg_y^T` zVo{Y_qKs74HBv^_a~U?MzTFPbfBNH{ML}3JlrJOUe?_sxm%TA2{Trt8=1S-7dc&TbGbq0d*s3Kg7t(k_5Sg+>dyd0{z zR3ke7pcE55`G~DRH7qW4W0n^x8y3prqAOO_q$;`=%d=)IQ`H2MN($@2zbx|*3+-*q z_>QqIMOR)h@K;9zcb*2TFsF+J4fP%1V}OZHuAWPGIU?_&548I`RM6NH85o_1tFs{- z-C3Q*df>rHxTvxz-+0i}@^K@V^RmXnV6-}!jQiseXkUrt=6dxFcX)C$$3F_D8l3(L z2&K{?{#C1oO3`-?9M~Law~?stO!HwJiFPvS6^H7Ey@*>pPSkB9hjMkKDSpSvymE7&t~Wvb75?#BDgfvr(^ zrKOM_=C(#%Fo?bxUESNVt^x1e1{b{Q9V=J8W3)%}CCFpBXK};|9El!icbKx06F0<3 zVY>GSd6645;=;>IPGU4g;U83E{-WGkqHCb4Tjs8TDj2MjRiqeO%J{VHc&YYWSTL9G z8mKaDIgiZ^m#1RGo~mo0s&k^+?;5D`T->28rda))s%xOC1LG}O*Fcq7*@&sS2C6#e zAMJMy7RqbCYp__JHDjAy163ZyXbUZN4Lo)0*)=FrFev*Vv`oM{025qnA0*Wckb!s6 zxv|qK_0|P!(7-l>DwrnY@)Qi(sJnpJe!-xrTcUzNlUF&{B`X*-bxT+UIC)D@Ff6hjpDv1()R=v$ubOIFk7BfiHVcNL z@z%3oIFY*fp=}23oeWAClbA!)Z69%SCiKm*f%h#2Jka4rmAW(O+R5cRv*Y3jV^QNG zG&R1LWg|pWO9y3WPt4+(SccHMYQ6hQ!T%l(p2CB@;MoiT-P}MpA zXn&Vsp}h8+28-ocGvXACRbrTZs#odprhzJtVzh-8n+Be`^=uk&ehU^1+G7VKbD}%bODhHv9<~ZRo(Iw463}!xh_?~psHKGfC4ku)B!Ze#M}vTcV0VlUF&}C94=TbxT+= zXfi7sFi?+sG z5i`G0zB-)^qnE@mhnFuCv%SXn#{LikvbUAz!Xa69UhfFLVwUduAV2#Vxa2g%sp)BU zy)ZhQ;%QFt>7LU2k;BmU9_^kft^vwyOr_e7)YUnYQjAhe8taVN;ZW!8!t;gPuhsV> zb-|Q~oS#J(UqP>0sLtCB3cZ!0%XIEVc8wV^(SD?^4o!k{vGx5(U0&rnH_>82R|m#h zu3JyK%*sYgv{=y9Isa&VKXS3W))xyAdDe_A?nmnKC`PxuSn$=Y=VF2LTd*kCKE;d~ zW3FImYd>}Gz*8?=Flen~7hk}5?e0gycklhKO*60hWAaYWVo(LsY+Rm-L9;7`9G9Hi zYB8urbfVgCF{tt?C%a@7gEq9zqnD^+&}3FNVzP=sQ|J7n^%G2bSakB1pki2Ly(Op^ zGXxix(4M#SS`7WzMYCj^5~{pq?lGu>X*MoR#h}WhkmHhbTNQ(<&WUQj zVo>E(PIjp(236hiRSc@k%0^68F{tXCf3$vrX}C~c`xV1tdDe_=Zc3=~C`PwkF?j0M zvto#UdUL40HVu9_wpQ+5f0l>8Dgqg>6^r{gCD{C zyXerp__S)^!FYJ>+aKXZnq2U|TcgLtU+D@QqgkxCY{k0PL=4?@{WP5_&p8Z^y6tIB zBJKi(MGf1PKjc$RWh^Q}W^6)ba9>AN=^UGE8FL)?FVs-U-RrIR7|}=IpGAcf(Q&@vX|$-qWL*eVo--i%%@Q4rR0WfxqlYZR&Fi5m z)2*_|uCb_@>S&~@&Ly1oyAZ0p$_waV6}0du0lRc4n5*i*cuRIPQk7ZRh-)o=DjCuF zNBbuoRO2~$?ROy-%d_VvWC`MaAd9e>m)hl;+m8E~X*ALB0)`b{pmu%eJb3r=Q z8uiTLU7#HfKWfX4yJ}w<9)@0`wSmwHjg^MAYp)X3;aI7?vSZiyy&w#2w)l!Huxcxu z>qqDo)P~wMoNP(~KZ$NpRTr4`Bet?Ay@fbuLwy;oKFI0v zak^=k*|3a0rEB7XA^bC&gRze-)*WNk2k%Mmu1-Sl$e(;*E**aXFvWE^WyDp3PC;F9$w$j*^6J=Pzph1j#9>XU>(j!bvnc6Hm0`GRw{g!M zciUE1sJM*1Ylra~pSDgFvP;}4oq>hC(RC|k5?NR=58-=>%@26qg_s+0-G^q%kh>9w zf2NH=XH!iP9&zt-I|L!@3flxd@7%CWlP=$IP7KA%QRt$(Eqh4wWF^@oNxs)xqhQ{vfXgq^mcdwZTTKOsl`(KOZj2HcL&S2J)~ zy`UFMG%FZNM=-YapalMsCBwKp7}Gg(N_p7cvJT$7t7P(cLWcUYBkkt8Ox9@yo=%~0 z(@qcFQ_F=T@00Pi26rQjQZtDw$?^+l>^=6}q-!irLfn}aOV%Tg!gWk6?gZESIGFI) zJnFGeIQ-Bfj@<94qxOUU{oJK%e2!wIeW{(~l0{>3acQxpO^v`d>u6Ppk$vzZn_x&@ zY+tZ;kepjstHV7|V&s5IAgUmn_v(DiIZ1EYezWd1-dPFjHBK6e%>rXGjdMkrYDG+Y zbD(%m~|@p1#UXU40ss)~5xXb!(n*n=zPL)IDiC1!emw7G{Fm z7>lB!D0D(mMpfsWn0BZN>zGkL{*eZ(;;P3*PBQvoj?rSh&Y@%0TBAf)RmSRXtU0QWu%|L~m_t`-D;{thvSn4Ioz)rhnP6RH7YX*Spez^Jr9qU@QsHn|19UBDl`iA9An1Aa9KSFmS{L?pN{sMPpNj_$_UijPi|eE@YF=M z?&t1P;84QIJ9dHtYy&imFO`WK`mqRIwcrHJdQj}iI=I${Qf;rZ!jN-&XD4jm)|wEMxf(XXXU_9MF40<^f||KO zMu}`ZSHG^tGED7p=K3Zy&gvNj7s6erjJ>W{U0)W#Szcg`=Y9GHUE4KKD8V#uzDypw zabQQ)J1a3izGc^-)y?O}nZS`K_{Mu-3W5b`1BNy?V$09!3X2vDwMF3cl`jxSHcNrH z>cHroN??3fb?ki+Pb3Z2>?j)p`EHDTmbp-f z3S}K(hq?KLS1c6+ad3=UJbkXykR?7eVX~cbkxc^I`(xbLos(!2o?GKlIh(FpjDFc; z?43Y>QKlZ(YAPp=ZlI36Ckt`*SZ=;S?b*`8pFYz}mqTjlQxKN`3 z5!*(M=q+na%;43#BU~AVwo!F?c0OVk2cT3&!FgL%879cmby^X5Mq*FS}xQ#mYjy|bHnIDl_+(sP_lm!jP zcvp?brmAfVsUEc`$iJD+Ub)xlRS!JySP$lb8jJAOC+Sx|+jfX44<=I9ZFFc)$T>!gK zXm1;JtUfj8Y6iY1*!WgUu{RdjX&HOxX>ovUfQDH|8*J&eQF-OBZxf7%O7Mf9YzrBE z(l#byp|5&-HnU$;opxB!?eRKw37U49y1Cw%dO~fi9bpt8aw3 z`k-uqT6c8I*ss`^#Z{C$KI@9WSn5crPw@`gVR8Cmm2+Kv>>c_Z+E_iKVW%Gim(=Qb z<05eWVXPg{fbpi?sl>-0N=!fPgofKcbMD*x==xhzp`9P~c!m>*txJ14qw(aH3*^#t z*WJ~vGwn?fr_TNpb6m0mI6MJE$=5rGF_(Nnp|xMKTg5Ce*z(X`-Jtm52DthB;myF1 z1*fp6@%GJZ{=lJi7M!K356W0j%OGwNItwHVnRZfy+&JM$ z5zmhUsd(ZY!~pZ*JMlIb(;~QphRZeW%hIT~f6FgqRdB9>Gca~WL3RyU%jGFWe^Lv8m1wgq+E>WwSkP~eegTiImLMPR!K3yFRJFb!JCPwkG&-e>iiB9$Dg2{`@2F%o_ z`gEE0xQ%r7SV+QbTHV=CpzcoKD~h^0H`uM8>eJ;x60yCVwomox>SRt_ic@{M%*sYw zYh@8kH^l`YR=3VSXbIicsXpC!PG0M$`XchI&NA(hwbl}WQ+>KTiqRHOUOT7ye08|? zTdz}n+*=B+uhe4&Vr`#gsah6s|5{qAMAgo>{jvOCFnDrs=Qe;zgra>@nBP8 z;WD~JS3;H7M3tSWE3sH_*&=(bwL`Egp~{mLm<>U(_Gjv@L^+kQB>*II-D4Kc$?{zZ zRVC$WwlisrAbTjcOS0`i{kkqHRgHrs_smVhoO8n#LTQjct5_OwnP zv#33)qMCG31}agtd9rF{pm3jO;Y^KR-<+uW$djC(*JzBVYV@drDKMc&m+6crZ}g}# zX{G4w8jJGv8a@0Gq)?+rRp&&FN25V(2Qyf(Eu4?#(~l~!a+jiB;TBInsyZ-V)`+rz z=_hu8wO*CB-bSofu5kxEl*3gcIuEf~vWyMuRck!Npfi zvyWPU=@}(EmLN*nP+Lm-dLlJdTw5v@scZLC_(W}>Ej9I?1Jv76{pfoc)iruH)RzTQ z?HoFpFnwQ&zmb`*x3Xmc&c}s4scGt(1l35Es$hzx)DudQEa$ovS|*>az_QNU!m*_| zY=zOMDPn8Gx{2tVV(n2j&U;s8Fol-ClMwX%MS*_Kn7Yq+yf3M&#s8 zyj^#%FfHA>J3Q*qC-Ca^sxB&;6(W5N-E{hgrt5e%><{7N(}U;(t%-YC`n089ma}*) z4g|~5g<*i}(LftUSLdcfcJ!u9AEsVC>gD;+DOp`+Wh2(BL2DY(xqO!1%NQ}3iAs)f zpqZr`&&lIvR%{B^D?izKkcrf?BN{NXlXJ0iLG`i@%d2EPN;?*CaAVfP{8XXAk>_zv ziu#o)e{9#YnH61~Y1f5aL5bsqONiL2r*1vzU3m;=Y=~ary4N*{VmdLBs{_2Kj^uMo z!G?kM#yl+<=+<6LU3DwwGmv|1fgAQy?J;+{)JH)w47>?7RqqD7z+#&U)`ZQcxonVy z3kSBkrX#I=SF~XB0ou2qegb>y-6fpc>T>;?1kVp-7MzPQYK9vt8P3H(`%mzdXdk5d z6wKstEgg)}3-1hqz*0E`|6JuVwZz7eD$dP5x)V(OnaNioDkaw6s$e_u?0=}7 z4rg-)x`~VRy4e_uh->XAgbp)LRe3htK)FTcj1@HXItpA-=Q~Ehl~XyDvBX!evEZo; zHjh-5w1Ri(IX2X|UadY4IqHNuaH=|T91GZAh^0Q9vDM|3@4sp?DL>QBRew5wS6rs! z;bb7CfQ*&F$iR%i#VT2kVzdR67qecyQj9Z5pvmMJZ#}nqSY0T;E$TktfTd|5W{YQDpH@2KroH6O zHBQ+Tb#S;Bl{nt+ROpDCrjfRPTn8`eG4+bR(mxeFwldad3O#V9^x7}8)BCRV*#NPEG0IqMmtNjRh~(?1wPWH+7Z8A zF((qnnYyxG*I_V>EtsoI%|`qzQksYDl82^e-aGkd<+2q|aD}?4;^7K1)N}Gh8s2SK*gECRr1v)bQAT@`!=sC!;Mml(6?Ao9Dt+@rmsvTK*4$!Xmk-=L z(dF5?4typ&>e2O3z0HQwJD-bnFY(P2T{y%XYWSC$w!va=p-*v?3ar(2b#6K=&CL^C zW@RJREAeU?(YZWrBe%63p&QT1XmT3ywJNvzB(*@vMqR9SeDF+82BqB#cbKjt*mej1U_0^p;I}D|58lZ&9{m& zeerhhO#7l*)&m4HCO_}Woua_QBs1@OWjMUfC8!y=VG>(F)a`5t69&40+Jygp8?B^ofQye8HS z^;$+8Ap^^X#d^yw8`fGngpQc{b5M>pwJ~A`+JkG@h2^2w#t8O@R3q9rgz3fw^4LF?#LMx=mF9U$8mSdRyDxj+jR z894unMFs?CyTHQ1#2OI>7iLFHdZpSd zL0?~EcJx%qcv)IqJs;;zb+D_Z$|EOtA*fDQ4KtNmZY&J(JU;{JQ56j4$=Vb6FD*6P z_&2Mr&I|3q1fbbRooKz(wPPjgQIxiT@?zFoYYquCp*-WQOA{*Z0C0v27l=?76l*Z*LLu1b7we$} zjkl|DK5#9wWEY%8%WRj;>Y<=fvE`_@%#wX# zDt>IqyTqQCuyz)DrQs;ShpxTB-flbfy@#24o83iiSKuVOEY8QXgXh$|FLB!zo(qWf z-A1fz`YW-#SAB_<#~*G$1&(LWv>WZ&DL>iFjrnt}@Z&82<4=h%mP;0m$*mZEa$~Z4 z2Ul2QEto^^&=!j%uld8BzQRC9T)kB4YHi%t&syTKmR{`%$%n`{1zsYVsh{CvX;=eV zuZgXvYHOU^I#R>s=KUll zFnpK?h1g=e*45#eDi2~Dt@-q9SmO9mFi=_#4oT)RGSDbd1yc!*|I#u`=$hwx$~Q_> znQmT|?$p<7ltix%>!XgT0&Um&)nQ$p3*B_cDCRS%+9c}GI*-m=yUYr|-aeZ*1B1IT zs?5qpOw}e))j9uYzfH1GUi)p5#qz8f+ia7l@+d~TRY6;A5>MTFwn^SXP3&NsM7#Uw z_Z_?i>Nwf<)!gY5eSzwIMhbf+d(9Y^aUQdM< zr|O5JoXcdF=D?#9Eb;U8)LVI8hOYB@HzMc87_`nQ538r0 z%x&hfj#dtVLKw=qppGVVYcG4#4ad3p=u`CTK))UY7X4b!Ivbi#jNU8^beLhQ07?qa zh_WQI&eD8`IWr=cY!DpPu;XpF&JD6HGBE7K_A{$b!`y@=iVJTG(+)F9gBh9CY{`Z> z+92((Xm4FFO@lNbZ+RP}0m05~=-_B9I3{k824uR*yyXUIK;5!5NCWa*erdTu8c?@v z4bp(j%0|R$LtVbuC~f~kWoL%tqJEAnpw9V6+YQo)ytW&pF?lWlwsejxAdh0S1t=Zb zX^<-FuxdztnK7?wq772er&ZU^dUmH2tLtTIkb>r7!HvZtxidgkh(&rO)oaD#l0|Qj z_E*#cJT_+A8aCILtU=0aK6qqmceUL1zL)_PrWZYcOhf^RJNCW3D&_-2A{F8CILZz=dzf^RMO zHiB;}_;!M~7JPfbcMyC>!FLjTXTf(7d{@DD6MT2U_Yk~6@I3|JOYn-|zTj2C1HnVV zBf(?Albm4+ntSu_J#!!aKg@jv-%s%U1wVk;Y{R&buxB<2-d6B-f;S6(py2HV?;vh5uxEA`yocb23f@!j!vsHE@FN62Qt+b$KbqL=#rPP; zy%`_N_&CCz*+=l>iOmxj_hsCVaeu}G7!PDTi1CSx2QwbRcqrpxjE6HG!T2P`BME!v zC}OjPuxFl}Gekf0O!51(h|RMJd*(TUpDXxzf}bz=1;plsggx^j!N(Gt;}~B|*fTE? ze7xY73Vs=}c{yRvoIq?&%n|>luj8LjChVD01fMGSG{L76o7Xd*!T1KoHxl;DnZ)KS z#bcj`-KPNbvgwe_ZgzIl~|SJmVJ_zsUF{!k+muvH1#N&wN$z z*NDy68NWf;Gv6dO-^vknUn2N&{v$tQ{5j(<2z%z2#O7Cwzs?c=r~f4QU;Ib@P1rO4 zC-^_a=D&l(44mde!So##Gii>u{n|uVji*C!uVvyr{svrpG`kQm?JjNWqclC z&pco73y94N8DGSBEaP#EFJ^oRauV#D=<7*jT z$9OX1DU7Ewp2m1O*Ig%6K{B&lrEs_zT8gGX9G3*Nnem{4L|}7=O?Bzl?ui{3GL^82?PzGk+2MS7P(G z95I^zmw)bE!DS;@^a{QLvAH5)&s>SvTscP^*=y6!up||HUBTBAe0{+;AT~E-ybN_M~q{} z3FDNoXJ&%$O>FMNcwff*G2WjLawf6ahH)bytoQ_PD|kD>n~BW>343OHVzUF|j*Jgt zd@$oqj5`zd%r1g=6}%gzK*0wQn4ZJ=3}W+4#%D1;oAEh>J@Z__&m%U^XM6!+ z&%99Ziv%Al_&8$oV#b#+9?$qv#+Na^obd$46B%E@_)5lCF`mTuYR1-Q^ z-_Cdu<2xAN$@nhDcN6x^dj!9i*u0PN{fr-A{2=3p7(dMT5yp=)evI+sjGtiqB;%(T zKh5|V#?LZ-j`3o~&oh33@r#UKV*E1WR~Wxa*fU=f{B>gU4aRRWev9$jggx^=g1;m9 zyTs;uggx_p!9O52moWa2@lwVg5%$cF1^2z%y_IYUWj{w#j~3$ghtVbAvLm3ZaJe=`}9C0*`5&Rs%#|b_$XB>^w1;0t~dj)?gXB>@B3;vAY&k~!@FW8n{N|BbQ1g>!QU19J!12H!k+no z;7f9b(Yus>Ha}wgG2>4dFUt`}@7IF=B=}!~uXv@*4RQ3YEBID|?c9*+?VlwggtYA!4D9;jo^)fHxZj{388mKY&H}2%mW2)FL;NXF%Uk8em4(h+=+2# z#$9qm`#wzY-h%fRd|1wC-=_;cPVlP*pP4h-_bg&_Hsd*jJ#(($^8}wS_yWOiA~tU( z?3uUZjK;W-em8Gpd^_VsjPJ-1jqw4&pAh_c!C%iAjd7{q-w6JP;Hz9Yo1E~+ugZ8e z#;Y@4gRp0=No=mgxE15I384c)Y_7|AJ;v)Z-hdEx#EH#~7;nsY6T+UksoWPBFmvl*Ym_*};4F+QKLXI?;TUdZ?&#$y?eV|+0o z>|Y8#p4hyU@nwuJXFP%NM8;PzzLN1(j3+U^n(;M^uVs84i! zOT_O#6nv@R9|``k;GYni%LrjTMQko7?3teto1YWHdManM;vdBCe|;yH0Gb!+6~sQQuAZ=bI7s%*_Sgg4o=WuxD;1_|}4NBlxz&=5~ZVv$f#c3%-Nk zI|{y&;5!qWyAbxwU5U-z2z%!4#O5A^J+ndZJq6!O@QUER;8kKXAnchTu^BOr343ND zcuH($gm6-e*xZM(XYMQbeuD2$Y#u<^GusfGjfBv(CpOzMZpXNp@qvun6G9#*Hajvt zi1ERMJ+l+B*_m+{#$5?}W;bH<5XRjZ_aKBlcw)0B+%ty}o5Klv<_N)0A~r`d9>utY5IXC` z<|&LvGakeER6=O13x2xbXAqlb686lq1V3BwbBN7z8J|Z8hZO|BK=2C%zlhiz%Xl2) ziy2?Scs%1v347*c#OCFUCorDK_zK2XGQNuOB*s@0_RMRD&1)H7N7yqb3qD2gse(@< zHm5Vbp79LEH!!}D@l3|E7|&)rhw)s-^BB)(ynyjdjBjRq3*%cEFJych0zKgJD-Yxh&#OA$(J@Y1g zUds3*#ve2Ogz+-QpE6#~_%p_zGya0{myEy45$lvc($8?1PVk=v|3&a$1^-R(--*pX z82?GwGyf9&Z^8d3_&-z&wY4eJq=<1UQ5GVaFs5XRjJdu9*84;8$p;D-r*xZp z34XNTy#zl-@ZN$SEBJAO_YwSf!A}spFR|H=5Zar942OE zKaw*>jgQgq=HrA=j0*lFvH27sY_AIb46*qvVb6Sy*j&u`dBUFg0u=G|=G%m@mr88D!}wjs?=gO#5H?bY%_WRKWW1CRQWdfJF=5aA zMDS(A=BJF86GEmE{Byy-5d2GG^DD+*GyaD0w>jd(`zQSjl`66MHz6Fi5d0s(|IHcy zXRdYiESVwSEN8%*6GHJvY;MVTE5e?+wcy(bzOCTf5u2?Ed*=4U<_;)5>%{k(~X?MYor9aX$j}yF);KvJo0Z7W^4v^I1ZOJ;dhX9PwZDZNZoEANeU^&sy$InjlHk3?A3s>|G4yB5QwgCJDER5(j~^%a)%0h~YX~7T z3x1vWS~v%$*qT%y<{ZyE5L5@$QWGVBElXPsV#Ot}yl) zR~ZM4L&g!~m~p~5Wt=hIoAEx3_hq~vRgmAY&@D9Xg zM?yH>Bly9BcOo`B=ZJ&)DEb+W{N;>AB(XV)aSP*<38C*H_-Mh$2!5*IrwM+#;AaSarr>Agj5d6c`2AR7 za~$J~8DGM9JR$T!a>lVbnSM8?FrLbI8sq7VuP5x8Gl43A@u76Uq|qDiOuyGug`b`#v3x;i1EgZH(|UfbDk zgmBVN@NJ3B?Fiv;h2Yy0n>!G~oqS?*C&oJy_RL)b-&OG41m9ioJ&4T)#(Of}i*bdq z&$!AsU>q`z7{`ng#wp{B@!pL0VZ1Nn{TT002un`E+X&t$c$4651#c&Kv)~5`-d^wy zf_D`BAi)n7yc4n6nQ<4!T^V;{dX-~j&UEx$1^^GabL#$824v9fbl>=xEw)jo=Dg;2NRn^7!PGUjId`8CpJeA zLiI&#jwFOW0I}J^_+&z;zKG4yjK>i6%u@wFP4LqNKSS^{iOsVZpG^omP=cQ;_<4e# zPi$U52zMcg&5Hf%FPbBP_R|tM3v3V6CbVi8H zs|n%A2C;c9Aw0(;_+(;p3gfAar!k(+_H68vVtZy`2sWxSBEXWm9^-p+Ut<2xAN$@nfp=n)Z{_b|Sf@qL7FOjhs*h|LEX zKSbCwA0{>*A?%rt5}S`Pew^_WjGtuu6yv8EKSKz|Z;8$47%yi0JmVJ_zsUF{#xFB| zg%FN{5SyO>{0qUqBsRYygj-F5eo8uI@p_EcXS@O94H<7l*fTdKHa8)J_M70F3BI}D zTM(ODGTtgj_=mYW|9lTZI4(?V?nwymRuP*O#y;a}j_|g}M*0;F;tJkY@OH#zbB@?3 z*_nUdg>hHL-ExFUVje?3_srhJ=CO>AW88=F@r2Ob61=bA{RHn%Yz|;NkPs>?!A~SM z2QwbRcqrpxjE6HG!T2P`BN>k(go{0bpG<6?!gw^}F^o?ogcdllc{(9HGa>ky#O7Iy z&t`lMA#C>$o98h;pAfG02!5g97YROA@NvZE#e`5?2|k|Kyp-`}j4x+Af$>DfR}l8h zD+Rww@JYnx)r3%85u4W%LUkqhWWlEpn^OsU<}|^l6PwpFohXU-RV0kL@#A-p|7Y~Df$H;4pZDEMuH-!Awf!S4|KPGa*e#&0 z5u5iBLa{|`KEU`v#t$)mnDHZwA0>niqTr7U{sgi4B;%(D;Yb3p`3&P{89&E(G2`b6 z;Xs1mFADyW;4cgQir}vjo39Z<#U=O~g1;&FTRCHRzC`@~!<;b^|C)X`zai|I-x8bO zG5(&gXZ~049|ZrA*!+p{&y0T|gxgER=5K^hmk^tOF#ePAUxYpLZ({R*IpSaJ8t@FF zt3a+vY_7$)72~yY#Q)+g_~%;^!T~wKw-$UG!M7EBJHcBEzP;c(2)?7>I|;tC;JXOE ztKhp4o4YgKgK+~Pyg!jM&Wf@4eUdZUU_1KVY$k-O>BMGxLbz`uc*mUaf8}BHyLmX{ zBN!ja_$bClGw#Ls7{1>yLk#>&m2u`jv<8Zh2W>ykA!e5fY|(* z5Y}pf|4MBB#`t%}e=z=&@n4MpX8b?Gp81d9|K^NRuXP=Ylv^=gn-GrX6PxQYUXStm zggtWu!8gnqEpk@ufBoI)cXM~bp1B9H*}!;D#(NR=%!=T?;MJV*-}3=_|P1$+B17G z?#q8*KgRtDA$|%zQ1C&-=8234Gaiy7tRSDxzdnP1J)VAr-EhG#6Z~?)ClH$x8DGKp zO2$_)p2YZS#@8^umJlj@!6yqoMewPDPa`&`6GB)Ne1_mR5SusVh?DR_`Wf1n#OCdc z7cstr@tuTl0!i?@iOqW$-^=(u#`iORfUsviDELFf=EIC1Vf-lL#|U9lUhpS~%_nn2 z_x}R@+%sRy87=Z%`rUkw5MnH``2pi4j6Y<&l<`N5KW6+1Aq4E4VY2>F{QM`uf6f`@ znd?$;yh4ufB$&A}{R~|y!B-W3<{I?7xh5fmTftl9jQ^&a3%)b`8FLpxxOb5=D!7OE zeS`RYg?=}E!k$?bJjfaUt=kCRS@5Ii&znax?nMYq9AdLKAyj07AD1)!iw+h1G{MIT zJ}GDTz(xj}e=XGk${clZ>Atghg!5csl>H;`h%9 zzF6?*iOm-nzsUHd9MSgQ;-9~rBbxp){`sejmoxs15Dvl${)OOQ3jUSgUkm959o)CbmU`C=Yr0njvW@s+)EM#7cuE>Y#|j zc+d4yTEM(ijG*L9oMRF(nS7a`yv&Trn2aDM2zOO?(cOAZ&i!tEH0!UmUu*BZ_F8-I zuG%d%91-rdEeaV*4HxcXNBAJ^gONTM<%7{asP@4aADrQX5BlIkUISnG#%UX1B|(BOkfUI@oxcro^b45dz9CS$2*U+_g)>4or^zZYXKc_BRK zAY*tV0$c6Je^th?+kxfD7~V)J@QYID#n?tKgfIPN3^T*6UN*K(hOi{S96t7TFNAlX z{O}GxT;qppy%?+W!JR%>?}NL%5MiPhV{iJP(+k1=`{70}#&&xlJXh((*jqmMwhX0y zX!fxW`Cy9-rQT`tv0XBh6t3M5cY341%lv-)cfEYsY z*c~7I(FcF>LfDLxvD9vpe*7svj3TD39?Y)17{h&#kfGEmVLo=a4@USP?S=4`gCCCa zVl3JV;e>rZ9OH*)cro^%7s76!AAZ;m&-BBOcro^<55{^S!e)6fHrorqIQro^GM4I` z;N@e9KFIo@#s{@NsPjTN#>WpEd>A3C-1u%febQoM0zzU1_K6xOjbV9o5jNEri_nNQ zWtSSx-poNCqQbB<5gs0@mnLB&R%?VOY*_@>gwR37y2+9lQN-JipFk&W8OEEWQ6xk*Yjvv1D(anoDKDpjE_}BhdC)SQ< z-&%hYUeR>KImFRuv~WAOlZ@tLkE*M*-AT~LoVS@ zsM@^K!f2M?VGMKbFpCe}5y`4?yn%}*BZVOTTp-G{akYvF+F3=2fzgP2hD0w4eZo8& z{x%m$MjU?Y`_tU-OUVI8)lc+jB>KA$)kPTRA>J5`c#{qhX(lwAe7@!^wN~k}TtU4x zVqhksN9UkfS1}Vkx*R2R6WtFYz9$gk_0~E9>Cq|(d#7nSm)WFieLE5rIw<$BkO}1) zzd8KYzD_9>;ZBwAPPH5v?^ZyLRVbj>9d=&I(UmuZq&yW^)VJ^-59r=SU2Nd{Y)Gm< zYyT5caWOgG`agDJoO$HjIP>Ud_ZkAxLJEAzE{x6~#sG!VI{XR%l8JRHD!|c|5P|BMaw{0%wDP^tjHz=d* zvZHnFeKIytQohb_rIOYPyjF2%g{`t7c&K7I0p+k{OKNIv`3%?Axsc-=jN7V}<(|&* z=k>zf3o!D#JGT~5w`Sj*cjKAsD+XWeM={9`-S(r7PBI*EK^=(i2*8h2wFy!U=Lvhd zNF&+Ag#;qL(}8^=JYze!5ih-#wKVr&F23IA*2eod*3@unaCSdZ*+_x(=!q2;5Y&y? z*HIV~e`u1?oS39Z#ble{vDs_19*Iy4v4lh-wQ)(Dz>W1VjZp|Wa)foXDurr{Mo8bd z8}aQ!MKrH05-U%piTvSBqUv^zOLq$YKb0;a8-Id2l_Ew_L~p%I$iT%bB-UNw0uw0S z9(f~p|Ncf(bn3208u)G{;z92SIG~L%MI(kfG1*Mrnq-uNlg-L=licKmhorLk_Lf{V zl-rf#YVW`W$jO51)!*QR?(jOXc4UfHUimgo$?7iR;>(Aznp&5TiPL>rtha&%Ep&}u z#_IR)clJbg?b_c!z@e0UcNCOMfn9AaTboD?}$n zc=Soc&)t>Uy0@OTpG>2v?3FHI2zOQgfVwMKJ4x)>FZG+Qfj4bV9Z$S z-LegQ6Lov_=}2>!B~IuNJl>c`GmC6D4qiVz_)-6gi5JH+Z@qH!)f;QCXAiFLw}!Pj zh;@?9MltNB3ne z!@%yI$(2X=3=75yWQ+^qz@bGUq6&q6R265dNDprn?UBe;rAaN1b~zeTPE|$qaKdLe zZ&#p!PnDD6?c1T;$Oz)Dt1;2O9DQ+Gw9i3Z@Lz+z>16A=Q;1CP7x{mQh&n`5Tao!K zSJeW{Redf+ytM^hGvtEjNmAL}QcYX`MoFp)k5_s3`adc`|TO$t#_XtYy zkYYmYi9{XFf!(Vn#lIO)rwYS5@hKRWqAey&to$KVtX3nj5j@8BF(fwNc*icWL4|kh zX6uH~+$NTK9l2@?Qs~Vra*e)%#J=swHI@lEx`ZgAt3K`z+ze#|R~tel*3uLT+Qf5% z7ZFXMAdzNK#WVC6@p{JJT%i|Hv0cFh<%Df`xKQl5HMPgOud=&VJU2SEr(&PAyGC3- zHZ`MipVbJw+f#dZ;MKXtOjuuoYZUyM9a41TAcG{wbm#tu~9oB4Zqh1|q7bR*FjNYGzK5f#hnS5D%R|05O) z$n9gw-~cyeB$;ob6a`cj(fb80N#WZ*5B(%%C!OarUOs1#*}q9i^{#W zb?LT)+wk9B+psk6U>=@7SDHbZ#Ig@VYst9%VzeId+7L7=gg!5|#7zE+8mYclqvINe z#$08YXjUb=DHFAMTU)B_z=prv46>-qd-L5hho3JAW;-pXr@hC~YCg{6A=EJP=#Z8u z?rvYGD2Cvj5uKMDIV9({n+>}WZQE5QRw|2Vr7MjV#&YmzSI38V`!30zy(e5up&Ljd zeHTj&M*Y5Gx3&DiNtl%2A=F^8Wot=+wWJ2YOMt~zg0(EU9IiJG zXV9ecBp$`KH{+dfsG%r0zjO&5c!k>o={wS}7peDzjuf$h#MzD>T+9SpYNv>P& zg`_2`W|3-6Q(2Sg^`g+8#wy1UOSa?23KvNdHEwJ619(1<_qn6?SgGdFE0afT9Q+!4 z#l?r5(Wn^8m+Y8GW=)=QGS(Kf0PI-Sq{oh#1h8Z`r=~`%YhJmwW@#1)dawuN8CY{= zyzF!s^yb3_mG;#|w!wq_Ru!#0hBNVCr#0s$L{jmS8cur?m5K?*aOg*7@!*eYIO~_D zY{zMmyvrCM-r{o;Ogwla&7nzm?~)c0(%seBJY}Z*E~$u`m_0u4)-yNtEmLpsB#G$T zqe|{eSl^IQh#nhNK0>Xm<35${=eqBcrTZ^oJw$5V_nDq>1wk4Smj>y}Mnk)dTuwV4 zWo)JnAgNr*rZ<9Tpvms1X&X6DwDtT#BrhR(CiBBXQQcU2ofNYZUf!5x63LctXkoZZ zF#QHrrxhkLoh$hU$H2dps|90R73h7{74WIus9fJ+ciGON*|z-K;Z1^3@~5**x~ij9 zBAGMfaHprqt0>)6Vf4(5&=>UOQbb3;Nu<}#uAXUKxucn?N1RIW$UK40AywkP4Iwdl zKG95FMU1iwfi+rn9bK*qNat+Ihv^(jC1zw`bo5F5c?fwKyHv+0ucBtDT6)z5ETgVjc^omSF2n^WBSj0PqBFI)sQw&V4dDiSWgVkBj+#}F{sCo# zCw&WqufwGDtvIBIa3%oK2P?xo>7xiyJ&vUGF$AO!fb;>EQ2AdxsRanFMpEi(0#ZXL z6WE2yKBTiLRe3#FLRj6=tHz1v>m+`2;3ZUao(8$;%yn>g05K07N2@Q=L(mo(pTpe& z)cgRnLoe-?{b{TjI84xS^!!;*oRb(61aX48&?Py}e7qSrOz?5^YdOwtoC$_F!Ck23 zV^2ORq8T_$$Z_;|k0+lk1QP;rLb}lLk0n{@jO&DS$Alh7N!=pIBcbNd1L*nmil*Rn zZEJY?+}5b{sMZO=$WJgY&Ofbi;R&*yw5XTo^Ge(uf&aDGDtsUAC5#Cryk-fwGPLHytgG~G!OfeWDsBTDh?O+z3E7`W7tMD0u&o`CU=K!lmr>1Cf?w61= zg(B3nvr98=3bNVR2;t1as_A z7{m%=PQvGe24mizgZWld(oFF&seI}4ZKX9rz`Ey~$u9r4F@6p8zbcX&aZlSc&G)s9 zaFw=o4~XP{Dm-Da^VgMETJ!E!TAzlTqfA_O2VP4`vtU>??{9PNFjormEtS^!W$?*| zPZ4}_;j>lRF{-o@S|LpOPli`oR~L1exPcy&Y7%XuQ{UEdja0-;>TagE#Wpn=T~o{b zGdi4!kHVy0ZUkKd9gE(bAN(K0Ndf0J4=G%D6vh>-3foyEQWSQ?&TYAjT(+|*k=Gav zh1%?Uzui3|q#2vuETU}`#{IS%H#u45<&@pmm=hGLE@N%Oh|4*QYg;3877jW->acTb zE-T&yyu?2G;}UyC+T2!?{fJG984c+VgYVgduD;!+EAko$HA<5UnI2UyuCEnojE~``{4-Yxgl7$(qk!xeJCE3Evx`wc;F++FN-+U^~ z-9aieqh|m+Kba`pRo}f`);Un7{E4PE!|XSe8hGQv_}PX%wUJl-%bM;ZkDoDK13%Lb zer7Nz%;RNZcTC5C@z+P&EMJJ^F}a7Wf%8!#_|7nuMkNXsa^cOmTZ!Xw6CUmCexRfO z$e=Iv=w8r|&mmv8-$p85$lObi?`2=Uh@95sxT>mLm6>j+!Ve4F3luIom*X7#U_vK} z0@6+0HOzzO(5xo7!@6raDJy!?^l|NDDDr|-8__mgO>Z7Wx<)=ti4yJM6>%`e$3Vkw zPgzDuWt-Dz6m3z6RSu;{vWVo6Ex)1Fn{o+1AlFnkwxo`;!)k(L0kgzZ3ak7YJJm4o_u&bbFq~4; zJ5xz-vcqbh!Up<>Czuli;@j*nx|N8tO(}vvV>1zREC=lzVNU!D?PYCtgBa zOeoBTRA{y(xEHs8Y$(pjoM=Vc1j*JdvAre8`JDI=4FrWwEF281X5v4`W?ORaU`Bs? z=+PPSG>q-{MVqbbF(kL$I?qVX?w8iA^a$)RpkY+eH6x9Ai`+xWB#Weh>8Nl+PA7UG-^{a0+DV z^4a44uVr5oDfy_uJ~10)QhwHl$uh~hKENmyebZLolVWC<8q)BlL!Y|q5j^NXjFUM50BK z=)}dDvR9S%1UxCxC20&WHuCJXjY~@pmf~Blmu8&o#r=y1^Gz$rm)LxQ`?&Lfg|Qura?Bg>G`` z&E#9A@r4s@U_m;rAH5-!D$UwyvWNoveVC@kUWyEB?3Y#mvE}j{1bOBl-d&7;O- z=9bDV%^IrBJ1pt}H)mT^PilCg{it1^@dD)NoI{#;*pCv)@7?oJ&+J)HmaFq`r(%+~ zNeu`8t-{Z7(1*0L9(K%*qYs+i?rTT%=qLfJhVh5`EU5nPwDJIgnP!j_-mEx){?tTo z8Y3Qpy+813<8SwMBkZ3v%xYo28RCfpvo!Jm3U1nh#IZ4=w?ed0<1k~J@`PT3&@R*r zF&3l$Nmfx%qoekLH-{!Y<1;n9%@$|#SgRH>74y`)dHwXh4i-N9#=Ps`HMu_OJ*p{% z9^Zsqp(MtYk77%~QysPQ7nG=6%~+!`ffAJYKe?(%4A!No^k&->Jzfg_@t=r`QYp9} zzeb1cJ2>XpX>=g;fO6m;Ur)M(K{RtaYwv-*nPHUBdc?*myAii-2=tUh<k%}|7_x& zCjLCkg{(gy;b|%FuRe9zBQai7B9}b^V|H#vj`>n)m%fzvdzUrrW}3#)y(JF}T^awe zcCzvj9(E1+S`@wi)_l{0s5$TT8KM^$sTN1akaB%KNMD zpFa)#mgVa=Rtf#KbqJk>eq*d1uwoXdp(&}a#7(xTcN@5pD>%JdAUf4yVY`%~T^Yt2 z#$lbL!d)Xu?BRh~%7KC_lX8FE9){NGfIbo^%Vee1({lqkLz9X-6qrRGL4UqPl!*9XX7>T+IU^OKHdmEX!(GEvsYotbt9^#A^~Xi5ga; z(P%X~jb3BWBx&Qd3ED(0tJP?=TAfy}HE5G`@wxNR?;UZ>aV4f-TQydlAmXkZN*gVvxk=nV!#QWA)m1jQ#o)=7{oNn#}=vKp;UZ%8t) z<+3c<@mtQ|$+2h9qKpztamgZcer|ExhINbLICF7M;i6(oPHstQ@zzBp#oVIYf-K9H zIIh@SYAK1!Eqpuy^5wD(oNldltyZhgUX+(xu)&h`S{@{M&77B4$eBwEiyx1VONfut zx^q}pm}iMAE zxZFiKWtNgscQ$#sYo%O*OvM&RTTqx~iJgCD#)4RLUaq;sQW9&~0utmF3G*A6ZZF6!<7jf$>+=jxk(vn5nEX9R|C66bu&=uLxd5L;1Nt<9; z#IA+TTQx{QJi1%;anmRq2kJh3whEL%2O zxKc}&u^`Kj54FrR7v~m~-pgBBSeSP&XW2S)@x7dk(rm-MoL_B}s&p^USWuRKKZh&K zf(E~ryR4XldSqGP-?Flj(!zZA-ExcPW?5lg9!UREX|b`mxUg6n)6xZxqqwhWx-i@` zPDdW`P(}_{0F~mHOG+%ol8mw0=G;6>R&wkta{;%mu=ur&rNwc%C9f4gbIxCYOpkif z7s%<$fP$k(8%m4K8(+&VHs@PjGiPNLC&%8$h|MjLo=dnty%o|*XMDmF8xllIWV%$Q zPs{WA%VJWtsjjnXZxPE}8C@ zX`YKboZazbyApo^%7(D0GhWj^F)!H9Qb4Jz;b|eoV@sHU@w`!;QoA`g?uD=|7$+bTsZ|` z2LJ!^dqK|cIrCPqKKBd>Z(B%Ux;_4LhNnHygC2TJrt*80?lzXD$`+^PRUdq>yi7HGJR5}zmln0 Zre!j<%XFVikIM9YnfA(57UTa-{x5Cf9;^TW From db760f356f5749687deb1e5c42db370ad21df473 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 25 Mar 2026 00:09:48 -0700 Subject: [PATCH 15/20] Shrinked the program image polynomial size and added optimized verifier evaluation path. Removed leftover debugging instruments. Updated wasm-pack installation URL. --- .github/workflows/rust.yml | 2 +- jolt-core/src/poly/rlc_polynomial.rs | 3 +- jolt-core/src/zkvm/claim_reductions/advice.rs | 23 +- .../src/zkvm/claim_reductions/bytecode.rs | 14 +- jolt-core/src/zkvm/claim_reductions/mod.rs | 5 +- .../src/zkvm/claim_reductions/precommitted.rs | 44 +--- .../zkvm/claim_reductions/program_image.rs | 209 +++++++++++++----- jolt-core/src/zkvm/program.rs | 79 +------ jolt-core/src/zkvm/prover.rs | 9 - jolt-core/src/zkvm/verifier.rs | 2 - 10 files changed, 172 insertions(+), 218 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1c6e080f65..6ab20555d6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -141,7 +141,7 @@ jobs: - name: Install wasm-pack and wasm32 target run: | - curl https://drager.github.io/wasm-pack/installer/init.sh -sSf | bash + curl -sSf https://rustwasm.github.io/wasm-pack/installer/init.sh | sh rustup target add wasm32-unknown-unknown - name: Generate sample project diff --git a/jolt-core/src/poly/rlc_polynomial.rs b/jolt-core/src/poly/rlc_polynomial.rs index 43878b19b7..a1fe5aaa33 100644 --- a/jolt-core/src/poly/rlc_polynomial.rs +++ b/jolt-core/src/poly/rlc_polynomial.rs @@ -475,7 +475,6 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." } PrecommittedPolynomial::ProgramImage { words, - start_index, padded_len, } => { let precommitted_vars = padded_len.log_2(); @@ -491,7 +490,7 @@ guardrail in gen_from_trace should ensure sigma_main >= sigma_a." if word == 0 { return acc; } - let coeff_idx = start_index + offset; + let coeff_idx = offset; let row_idx = coeff_idx / precommitted_cols; if row_idx >= effective_rows { return acc; diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index 16d5da4adb..a251ba150b 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -18,7 +18,6 @@ use crate::subprotocols::blindfold::{InputClaimConstraint, OutputClaimConstraint use crate::subprotocols::sumcheck_prover::SumcheckInstanceProver; use crate::subprotocols::sumcheck_verifier::{SumcheckInstanceParams, SumcheckInstanceVerifier}; use crate::transcripts::Transcript; -use crate::utils::math::Math; use crate::zkvm::claim_reductions::{ permute_precommitted_polys, precommitted_eq_evals_with_scaling, precommitted_skip_round_scale, PrecomittedParams, PrecomittedProver, PrecommittedClaimReduction, PrecommittedPhase, @@ -37,7 +36,6 @@ pub struct AdviceClaimReductionParams { pub kind: AdviceKind, pub phase: PrecommittedPhase, pub precommitted: PrecommittedClaimReduction, - pub log_t: usize, pub advice_col_vars: usize, pub advice_row_vars: usize, pub r_val: OpeningPoint, @@ -47,11 +45,9 @@ impl AdviceClaimReductionParams { pub fn new( kind: AdviceKind, advice_size_bytes: usize, - trace_len: usize, scheduling_reference: PrecommittedSchedulingReference, accumulator: &dyn OpeningAccumulator, ) -> Self { - let log_t = trace_len.log_2(); let r_val = accumulator .get_advice_opening(kind, SumcheckId::RamValCheck) .map(|(p, _)| p) @@ -59,13 +55,8 @@ impl AdviceClaimReductionParams { let (advice_col_vars, advice_row_vars) = DoryGlobals::advice_sigma_nu_from_max_bytes(advice_size_bytes); - let total_vars = advice_row_vars + advice_col_vars; - let precommitted = PrecommittedClaimReduction::new( - total_vars, - advice_row_vars, - advice_col_vars, - scheduling_reference, - ); + let precommitted = + PrecommittedClaimReduction::new(advice_row_vars, advice_col_vars, scheduling_reference); Self { kind, @@ -73,7 +64,6 @@ impl AdviceClaimReductionParams { precommitted, advice_col_vars, advice_row_vars, - log_t, r_val, } } @@ -122,11 +112,8 @@ impl SumcheckInstanceParams for AdviceClaimReductionParams { } fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { - self.precommitted.normalize_opening_point( - self.phase == PrecommittedPhase::CycleVariables, - challenges, - self.log_t, - ) + self.precommitted + .normalize_opening_point(self.phase == PrecommittedPhase::CycleVariables, challenges) } #[cfg(feature = "zk")] @@ -334,14 +321,12 @@ impl AdviceClaimReductionVerifier { pub fn new( kind: AdviceKind, advice_size_bytes: usize, - trace_len: usize, scheduling_reference: PrecommittedSchedulingReference, accumulator: &VerifierOpeningAccumulator, ) -> Self { let params = AdviceClaimReductionParams::new( kind, advice_size_bytes, - trace_len, scheduling_reference, accumulator, ); diff --git a/jolt-core/src/zkvm/claim_reductions/bytecode.rs b/jolt-core/src/zkvm/claim_reductions/bytecode.rs index 256491146e..fea0531ce5 100644 --- a/jolt-core/src/zkvm/claim_reductions/bytecode.rs +++ b/jolt-core/src/zkvm/claim_reductions/bytecode.rs @@ -47,9 +47,6 @@ pub struct BytecodeClaimReductionParams { /// Eq weights over high bytecode address bits (one per committed chunk). pub chunk_rbc_weights: Vec, pub bytecode_T: usize, - pub log_t: usize, - /// Number of initial cycle rounds that must follow IncClaimReduction ordering. - pub dense_cycle_prefix_vars: usize, pub bytecode_chunk_count: usize, pub bytecode_col_vars: usize, pub bytecode_row_vars: usize, @@ -66,7 +63,6 @@ impl BytecodeClaimReductionParams { accumulator: &dyn OpeningAccumulator, transcript: &mut impl Transcript, ) -> Self { - let log_t = DoryGlobals::main_t().log_2(); assert!( full_bytecode_len.is_multiple_of(bytecode_chunk_count), "bytecode chunk count ({bytecode_chunk_count}) must divide bytecode_len ({full_bytecode_len})" @@ -103,7 +99,6 @@ impl BytecodeClaimReductionParams { // In Stage 8 it is embedded as a top-left block in Joint. let (bytecode_col_vars, bytecode_row_vars) = DoryGlobals::balanced_sigma_nu(total_vars); let precommitted = PrecommittedClaimReduction::new( - total_vars, bytecode_row_vars, bytecode_col_vars, scheduling_reference, @@ -117,8 +112,6 @@ impl BytecodeClaimReductionParams { eta_powers, chunk_rbc_weights, bytecode_T: bytecode_t, - log_t, - dense_cycle_prefix_vars: log_t, bytecode_chunk_count, bytecode_col_vars, bytecode_row_vars, @@ -200,11 +193,8 @@ impl SumcheckInstanceParams for BytecodeClaimReductionParams } fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { - self.precommitted.normalize_opening_point( - self.is_cycle_phase(), - challenges, - self.dense_cycle_prefix_vars, - ) + self.precommitted + .normalize_opening_point(self.is_cycle_phase(), challenges) } #[cfg(feature = "zk")] diff --git a/jolt-core/src/zkvm/claim_reductions/mod.rs b/jolt-core/src/zkvm/claim_reductions/mod.rs index 15055ba7a4..4282718298 100644 --- a/jolt-core/src/zkvm/claim_reductions/mod.rs +++ b/jolt-core/src/zkvm/claim_reductions/mod.rs @@ -29,9 +29,8 @@ pub use instruction_lookups::{ }; pub use precommitted::{ permute_precommitted_polys, precommitted_eq_evals_with_scaling, precommitted_skip_round_scale, - PrecomittedParams, PrecomittedProver, PrecommittedClaimReduction, PrecommittedEmbeddingMode, - PrecommittedPhase, PrecommittedPolynomial, PrecommittedSchedulingReference, - TWO_PHASE_DEGREE_BOUND, + PrecomittedParams, PrecomittedProver, PrecommittedClaimReduction, PrecommittedPhase, + PrecommittedPolynomial, PrecommittedSchedulingReference, TWO_PHASE_DEGREE_BOUND, }; pub use program_image::{ ProgramImageClaimReductionParams, ProgramImageClaimReductionProver, diff --git a/jolt-core/src/zkvm/claim_reductions/precommitted.rs b/jolt-core/src/zkvm/claim_reductions/precommitted.rs index aa935859ff..a3c4013161 100644 --- a/jolt-core/src/zkvm/claim_reductions/precommitted.rs +++ b/jolt-core/src/zkvm/claim_reductions/precommitted.rs @@ -21,7 +21,6 @@ pub enum PrecommittedPolynomial { }, ProgramImage { words: Arc>, - start_index: usize, padded_len: usize, }, } @@ -38,12 +37,6 @@ impl PrecommittedPolynomial { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Allocative)] -pub enum PrecommittedEmbeddingMode { - DominantPrecommitted, - EmbeddedPrecommitted, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Allocative)] pub enum PrecommittedPhase { CycleVariables, @@ -62,9 +55,7 @@ pub struct PrecommittedSchedulingReference { #[derive(Debug, Clone, Allocative)] pub struct PrecommittedClaimReduction { pub scheduling_reference: PrecommittedSchedulingReference, - pub embedding_mode: PrecommittedEmbeddingMode, pub cycle_var_challenges: Vec, - dory_opening_round_permutation_be: Vec, poly_opening_round_permutation_be: Vec, cycle_phase_rounds: Vec, cycle_phase_total_rounds: usize, @@ -100,14 +91,12 @@ impl PrecommittedClaimReduction { #[inline] pub fn new( - poly_total_vars: usize, poly_row_vars: usize, poly_col_vars: usize, scheduling_reference: PrecommittedSchedulingReference, ) -> Self { let has_precommitted_dominance = scheduling_reference.reference_total_vars > scheduling_reference.main_total_vars; - let embedding_mode = Self::embedding_mode_for_poly(poly_total_vars, &scheduling_reference); let dory_opening_round_permutation_be = Self::reference_dory_opening_round_permutation_be( &scheduling_reference, has_precommitted_dominance, @@ -125,9 +114,7 @@ impl PrecommittedClaimReduction { ); Self { scheduling_reference, - embedding_mode, cycle_var_challenges: vec![], - dory_opening_round_permutation_be, poly_opening_round_permutation_be, cycle_phase_rounds, cycle_phase_total_rounds: scheduling_reference.cycle_alignment_rounds, @@ -136,24 +123,6 @@ impl PrecommittedClaimReduction { } } - #[inline] - fn embedding_mode_for_poly( - poly_total_vars: usize, - reference: &PrecommittedSchedulingReference, - ) -> PrecommittedEmbeddingMode { - let has_precommitted_dominance = reference.reference_total_vars > reference.main_total_vars; - let embedding_mode = - if has_precommitted_dominance && poly_total_vars == reference.reference_total_vars { - PrecommittedEmbeddingMode::DominantPrecommitted - } else { - PrecommittedEmbeddingMode::EmbeddedPrecommitted - }; - if embedding_mode == PrecommittedEmbeddingMode::DominantPrecommitted { - assert_eq!(poly_total_vars, reference.reference_total_vars); - } - embedding_mode - } - fn reference_dory_opening_round_permutation_be( reference: &PrecommittedSchedulingReference, has_precommitted_dominance: bool, @@ -298,9 +267,7 @@ impl PrecommittedClaimReduction { &self, is_cycle_phase: bool, challenges: &[F::Challenge], - dense_cycle_prefix_rounds: usize, ) -> OpeningPoint { - let _ = dense_cycle_prefix_rounds; if is_cycle_phase { let local_cycle_challenges: Vec = self .cycle_phase_rounds @@ -319,10 +286,6 @@ impl PrecommittedClaimReduction { .match_endianness(); } - debug_assert_eq!( - self.dory_opening_round_permutation_be.len(), - self.scheduling_reference.reference_total_vars - ); let cycle_round_limit = self.cycle_alignment_rounds(); let opening_rounds = &self.poly_opening_round_permutation_be; let mut opening_point_be = Vec::with_capacity(opening_rounds.len()); @@ -398,13 +361,14 @@ where .collect() } -pub fn precommitted_eq_evals_with_scaling( - challenges_be: &[F::Challenge], +pub fn precommitted_eq_evals_with_scaling( + challenges_be: &[C], scaling_factor: Option, precommitted: &PrecommittedClaimReduction, ) -> Vec where - F: JoltField + std::ops::Mul + std::ops::SubAssign, + C: Copy + Send + Sync + Into, + F: JoltField + std::ops::Mul + std::ops::SubAssign, { let permuted_challenges = precommitted_permute_eq_challenges( challenges_be, diff --git a/jolt-core/src/zkvm/claim_reductions/program_image.rs b/jolt-core/src/zkvm/claim_reductions/program_image.rs index 5ac3527616..859be87149 100644 --- a/jolt-core/src/zkvm/claim_reductions/program_image.rs +++ b/jolt-core/src/zkvm/claim_reductions/program_image.rs @@ -10,7 +10,7 @@ use std::cell::RefCell; use crate::field::JoltField; use crate::poly::commitment::dory::DoryGlobals; use crate::poly::eq_poly::EqPolynomial; -use crate::poly::multilinear_polynomial::MultilinearPolynomial; +use crate::poly::multilinear_polynomial::{MultilinearPolynomial, PolynomialEvaluation}; #[cfg(feature = "zk")] use crate::poly::opening_proof::OpeningId; use crate::poly::opening_proof::{ @@ -25,8 +25,8 @@ use crate::subprotocols::sumcheck_verifier::{SumcheckInstanceParams, SumcheckIns use crate::transcripts::Transcript; use crate::utils::math::Math; use crate::zkvm::claim_reductions::{ - permute_precommitted_polys, precommitted_eq_evals_with_scaling, precommitted_skip_round_scale, - PrecomittedParams, PrecomittedProver, PrecommittedClaimReduction, PrecommittedPhase, + permute_precommitted_polys, precommitted_skip_round_scale, PrecomittedParams, + PrecomittedProver, PrecommittedClaimReduction, PrecommittedPhase, PrecommittedSchedulingReference, TWO_PHASE_DEGREE_BOUND, }; use crate::zkvm::ram::remap_address; @@ -37,7 +37,6 @@ use tracer::JoltDevice; pub struct ProgramImageClaimReductionParams { pub phase: PrecommittedPhase, pub precommitted: PrecommittedClaimReduction, - pub log_t: usize, pub prog_col_vars: usize, pub prog_row_vars: usize, pub ram_num_vars: usize, @@ -45,8 +44,7 @@ pub struct ProgramImageClaimReductionParams { pub padded_len_words: usize, pub m: usize, pub r_addr_rw: Vec, - pub r_addr_rw_reduced: Vec, - pub selector_rw: F, + pub shifted_eq_coeffs: Vec, } impl ProgramImageClaimReductionParams { @@ -71,30 +69,20 @@ impl ProgramImageClaimReductionParams { debug_assert!(padded_len_words.is_power_of_two()); debug_assert!(padded_len_words > 0); let (prog_col_vars, prog_row_vars) = DoryGlobals::balanced_sigma_nu(m); - let log_t = DoryGlobals::main_t().log_2(); - let total_vars = prog_row_vars + prog_col_vars; - let precommitted = PrecommittedClaimReduction::new( - total_vars, - prog_row_vars, - prog_col_vars, - scheduling_reference, - ); + let precommitted = + PrecommittedClaimReduction::new(prog_row_vars, prog_col_vars, scheduling_reference); let (r_rw, _) = accumulator.get_virtual_polynomial_opening( VirtualPolynomial::RamVal, SumcheckId::RamReadWriteChecking, ); let (r_addr_rw, _) = r_rw.split_at(ram_num_vars); - let (r_addr_rw_reduced, selector_rw) = top_left_program_image_point_and_selector::( - &r_addr_rw.r, - start_index, - padded_len_words, - ); + let shifted_eq_coeffs = + shifted_program_image_eq_slice::(&r_addr_rw.r, start_index, padded_len_words); Self { phase: PrecommittedPhase::CycleVariables, precommitted, - log_t, prog_col_vars, prog_row_vars, ram_num_vars, @@ -102,8 +90,7 @@ impl ProgramImageClaimReductionParams { padded_len_words, m, r_addr_rw: r_addr_rw.r, - r_addr_rw_reduced, - selector_rw, + shifted_eq_coeffs, } } } @@ -157,7 +144,7 @@ impl SumcheckInstanceParams for ProgramImageClaimReductionParam fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { self.precommitted - .normalize_opening_point(self.is_cycle_phase(), challenges, self.log_t) + .normalize_opening_point(self.is_cycle_phase(), challenges) } #[cfg(feature = "zk")] @@ -206,8 +193,16 @@ impl SumcheckInstanceParams for ProgramImageClaimReductionParam PrecommittedPhase::CycleVariables => vec![], PrecommittedPhase::AddressVariables => { let opening_point = self.normalize_opening_point(sumcheck_challenges); - let eq_combined = - self.selector_rw * EqPolynomial::mle(&opening_point.r, &self.r_addr_rw_reduced); + let eq_combined = eval_shifted_eq_poly_at_opening_point::( + &self.r_addr_rw, + self.start_index, + &opening_point.r, + ); + debug_assert_eq!( + eq_combined, + evaluate_shifted_eq_poly::(&self.shifted_eq_coeffs, &opening_point.r), + "program_image eq_slice optimized evaluation mismatch" + ); let scale: F = precommitted_skip_round_scale(&self.precommitted); vec![eq_combined * scale] } @@ -246,32 +241,35 @@ pub struct ProgramImageClaimReductionProver { core: PrecomittedProver>, } -fn top_left_program_image_point_and_selector( +fn shifted_program_image_eq_slice( r_addr: &[F::Challenge], start_index: usize, padded_len_words: usize, -) -> (Vec, F) { - assert!( - padded_len_words.is_power_of_two() && padded_len_words > 0, - "padded_len_words must be a non-zero power of two" - ); - let m = padded_len_words.log_2(); - assert!( - m <= r_addr.len(), - "program-image variable count exceeds RAM address variable count" - ); - assert!( - start_index < padded_len_words, - "committed program-image domain must cover the bytecode start index" - ); - let prefix_len = r_addr.len() - m; - - // The committed program-image polynomial is shifted into the RAM-relative domain - // before commitment, so it is always embedded at the top-left corner of RAM. - ( - r_addr[prefix_len..].to_vec(), - EqPolynomial::zero_selector(&r_addr[..prefix_len]), - ) +) -> Vec +where + F: JoltField + std::ops::Mul + std::ops::SubAssign, +{ + let mut eq_slice = Vec::with_capacity(padded_len_words); + let mut idx = start_index; + let mut remaining = padded_len_words; + + while remaining > 0 { + let (block_size, block_evals) = + EqPolynomial::::evals_for_max_aligned_block(r_addr, idx, remaining); + eq_slice.extend(block_evals); + idx += block_size; + remaining -= block_size; + } + + eq_slice +} + +fn evaluate_shifted_eq_poly(shifted_eq_coeffs: &[F], opening_point: &[C]) -> F +where + C: Copy + Send + Sync + Into + crate::field::ChallengeFieldOps, + F: JoltField + crate::field::FieldChallengeOps, +{ + MultilinearPolynomial::from(shifted_eq_coeffs.to_vec()).evaluate(opening_point) } impl ProgramImageClaimReductionProver { @@ -291,23 +289,23 @@ impl ProgramImageClaimReductionProver { debug_assert_eq!(program_image_words_padded.len(), params.padded_len_words); debug_assert_eq!(params.padded_len_words, 1usize << params.m); - let eq_evals = precommitted_eq_evals_with_scaling( - ¶ms.r_addr_rw_reduced, - Some(params.selector_rw), + let eq_slice = permute_precommitted_polys( + vec![params.shifted_eq_coeffs.clone()], ¶ms.precommitted, - ); + ) + .into_iter() + .next() + .expect("expected one permuted shifted eq polynomial"); // Permute ProgramWord and eq_slice so low-to-high binding follows the two-phase // schedule while preserving top-left projection semantics against the joint point. - let (program_word, eq_slice): (MultilinearPolynomial, MultilinearPolynomial) = { + let program_word: MultilinearPolynomial = { let mut permuted = permute_precommitted_polys(vec![program_image_words_padded], ¶ms.precommitted) .into_iter(); - let program_word = permuted + permuted .next() - .expect("expected one permuted program image polynomial"); - let eq_slice = eq_evals.into(); - (program_word, eq_slice) + .expect("expected one permuted program image polynomial") }; Self { @@ -417,8 +415,16 @@ impl SumcheckInstanceVerifier SumcheckId::ProgramImageClaimReduction, ) .1; - let eq_combined = params.selector_rw - * EqPolynomial::mle(&opening_point.r, ¶ms.r_addr_rw_reduced); + let eq_combined = eval_shifted_eq_poly_at_opening_point::( + ¶ms.r_addr_rw, + params.start_index, + &opening_point.r, + ); + debug_assert_eq!( + eq_combined, + evaluate_shifted_eq_poly::(¶ms.shifted_eq_coeffs, &opening_point.r), + "program_image eq_slice optimized evaluation mismatch" + ); let scale: F = precommitted_skip_round_scale(¶ms.precommitted); pw_eval * eq_combined * scale } @@ -456,3 +462,88 @@ impl SumcheckInstanceVerifier } } } + +fn eval_shifted_eq_poly_at_opening_point( + r_addr_be: &[F::Challenge], + start_index: usize, + opening_point_be: &[F::Challenge], +) -> F +where + F: JoltField, +{ + let ell = r_addr_be.len(); + let m = opening_point_be.len(); + debug_assert!(m <= ell); + + let challenge_for_old_lsb = |old_lsb: usize| -> F { + debug_assert!(old_lsb < m); + opening_point_be[m - 1 - old_lsb].into() + }; + + // Match the current verifier path exactly: `opening_point_be` is already arranged in the + // variable order expected by `evaluate_shifted_eq_poly`. + let mut dp0 = F::one(); + let mut dp1 = F::zero(); + + for old_lsb in 0..ell { + let start_bit = ((start_index >> old_lsb) & 1) as u8; + let r_addr_bit: F = r_addr_be[ell - 1 - old_lsb].into(); + let k0 = F::one() - r_addr_bit; + let k1 = r_addr_bit; + let y_var = old_lsb < m; + let r_y = if y_var { + challenge_for_old_lsb(old_lsb) + } else { + F::zero() + }; + + let mut next_dp0 = F::zero(); + let mut next_dp1 = F::zero(); + + let update_state = |weight: F, carry: u8, next_dp0: &mut F, next_dp1: &mut F| { + if weight.is_zero() { + return; + } + + if y_var { + let sum0 = start_bit + carry; + let k_bit0 = sum0 & 1; + let carry0 = (sum0 >> 1) & 1; + let addr_factor0 = if k_bit0 == 1 { k1 } else { k0 }; + let y_factor0 = F::one() - r_y; + if carry0 == 0 { + *next_dp0 += weight * addr_factor0 * y_factor0; + } else { + *next_dp1 += weight * addr_factor0 * y_factor0; + } + + let sum1 = start_bit + carry + 1; + let k_bit1 = sum1 & 1; + let carry1 = (sum1 >> 1) & 1; + let addr_factor1 = if k_bit1 == 1 { k1 } else { k0 }; + if carry1 == 0 { + *next_dp0 += weight * addr_factor1 * r_y; + } else { + *next_dp1 += weight * addr_factor1 * r_y; + } + } else { + let sum0 = start_bit + carry; + let k_bit0 = sum0 & 1; + let carry0 = (sum0 >> 1) & 1; + let addr_factor0 = if k_bit0 == 1 { k1 } else { k0 }; + if carry0 == 0 { + *next_dp0 += weight * addr_factor0; + } else { + *next_dp1 += weight * addr_factor0; + } + } + }; + + update_state(dp0, 0, &mut next_dp0, &mut next_dp1); + update_state(dp1, 1, &mut next_dp0, &mut next_dp1); + dp0 = next_dp0; + dp1 = next_dp1; + } + + dp0 + dp1 +} diff --git a/jolt-core/src/zkvm/program.rs b/jolt-core/src/zkvm/program.rs index 07dcce9b84..225277b892 100644 --- a/jolt-core/src/zkvm/program.rs +++ b/jolt-core/src/zkvm/program.rs @@ -12,7 +12,7 @@ use crate::utils::math::Math; use crate::zkvm::bytecode::{ BytecodePreprocessing, PreprocessingError, TrustedBytecodeCommitments, TrustedBytecodeHints, }; -use crate::zkvm::ram::{remap_address, RAMPreprocessing}; +use crate::zkvm::ram::RAMPreprocessing; use common::jolt_device::MemoryLayout; use tracer::instruction::{Cycle, Instruction}; @@ -62,11 +62,6 @@ impl FullProgramPreprocessing { self.program_image_len_words().next_power_of_two().max(2) } - pub fn committed_program_image_start_index(&self, memory_layout: &MemoryLayout) -> usize { - self.meta() - .committed_program_image_start_index(memory_layout) - } - pub fn committed_program_image_num_words(&self, memory_layout: &MemoryLayout) -> usize { self.meta().committed_program_image_num_words(memory_layout) } @@ -89,10 +84,6 @@ impl FullProgramPreprocessing { pub fn entry_bytecode_index(&self) -> usize { self.bytecode.entry_bytecode_index() } - - pub fn as_bytecode(&self) -> BytecodePreprocessing { - self.bytecode.clone() - } } #[derive(Debug, Clone)] @@ -384,11 +375,6 @@ impl ProgramPreprocessing { self.program_image_len_words().next_power_of_two().max(2) } - pub fn committed_program_image_start_index(&self, memory_layout: &MemoryLayout) -> usize { - self.meta() - .committed_program_image_start_index(memory_layout) - } - pub fn committed_program_image_num_words(&self, memory_layout: &MemoryLayout) -> usize { self.meta().committed_program_image_num_words(memory_layout) } @@ -414,12 +400,6 @@ impl ProgramPreprocessing { .entry_bytecode_index() } - pub fn as_bytecode(&self) -> BytecodePreprocessing { - self.as_full() - .expect("full program preprocessing required to materialize bytecode") - .as_bytecode() - } - pub fn to_verifier_program(&self) -> Self { match self { Self::Full(full) => Self::Full(full.clone()), @@ -443,23 +423,12 @@ pub struct ProgramMetadata { } impl ProgramMetadata { - pub fn from_program(program: &ProgramPreprocessing) -> Self { - program.meta() - } - pub fn program_image_len_words_padded(&self) -> usize { self.program_image_len_words.next_power_of_two().max(2) } - pub fn committed_program_image_start_index(&self, memory_layout: &MemoryLayout) -> usize { - remap_address(self.min_bytecode_address, memory_layout).unwrap_or(0) as usize - } - - pub fn committed_program_image_num_words(&self, memory_layout: &MemoryLayout) -> usize { - let start_index = self.committed_program_image_start_index(memory_layout); - (start_index + self.program_image_len_words.max(1)) - .next_power_of_two() - .max(2) + pub fn committed_program_image_num_words(&self, _memory_layout: &MemoryLayout) -> usize { + self.program_image_len_words_padded() } } @@ -488,11 +457,10 @@ impl TrustedProgramCommitments { program_image_num_words.log_2(), ); let program_image_num_columns = 1usize << program_image_sigma; - let program_image_poly = build_program_image_polynomial_padded::( + let program_image_poly = MultilinearPolynomial::from(build_program_image_words_padded( program, - memory_layout, program_image_num_words, - ); + )); let _program_image_guard = DoryGlobals::initialize_context( 1, program_image_num_words, @@ -516,43 +484,12 @@ impl TrustedProgramCommitments { } pub(crate) fn build_program_image_words_padded( - program: &impl ProgramImageSource, - memory_layout: &MemoryLayout, + program: &FullProgramPreprocessing, padded_len: usize, ) -> Vec { debug_assert!(padded_len.is_power_of_two()); - let start_index = program.committed_program_image_start_index(memory_layout); - debug_assert!(padded_len >= start_index + program.program_image_words().len().max(1)); + debug_assert!(padded_len >= program.ram.bytecode_words.len().max(1)); let mut coeffs = vec![0u64; padded_len]; - for (i, &word) in program.program_image_words().iter().enumerate() { - coeffs[start_index + i] = word; - } + coeffs[..program.ram.bytecode_words.len()].copy_from_slice(&program.ram.bytecode_words); coeffs } - -pub(crate) fn build_program_image_polynomial_padded( - program: &impl ProgramImageSource, - memory_layout: &MemoryLayout, - padded_len: usize, -) -> MultilinearPolynomial { - MultilinearPolynomial::from(build_program_image_words_padded( - program, - memory_layout, - padded_len, - )) -} - -pub trait ProgramImageSource { - fn program_image_words(&self) -> &[u64]; - fn committed_program_image_start_index(&self, memory_layout: &MemoryLayout) -> usize; -} - -impl ProgramImageSource for FullProgramPreprocessing { - fn program_image_words(&self) -> &[u64] { - &self.ram.bytecode_words - } - - fn committed_program_image_start_index(&self, memory_layout: &MemoryLayout) -> usize { - self.committed_program_image_start_index(memory_layout) - } -} diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 582e213074..7d09cde0cc 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -1390,7 +1390,6 @@ impl< let trusted_advice_params = AdviceClaimReductionParams::new( AdviceKind::Trusted, self.program_io.memory_layout.max_trusted_advice_size as usize, - self.trace.len(), precommitted_scheduling_reference, &self.opening_accumulator, ); @@ -1413,7 +1412,6 @@ impl< let untrusted_advice_params = AdviceClaimReductionParams::new( AdviceKind::Untrusted, self.program_io.memory_layout.max_untrusted_advice_size as usize, - self.trace.len(), precommitted_scheduling_reference, &self.opening_accumulator, ); @@ -1458,7 +1456,6 @@ impl< .committed_program_image_num_words(&self.program_io.memory_layout); let program_image_words = build_program_image_words_padded( self.preprocessing.materialized_program(), - &self.program_io.memory_layout, padded_len_words, ); let program_image_reduction_params = ProgramImageClaimReductionParams::new( @@ -2259,11 +2256,6 @@ impl< .shared .program .committed_program_image_num_words(&self.program_io.memory_layout); - let start_index = self - .preprocessing - .shared - .program - .committed_program_image_start_index(&self.program_io.memory_layout); let (program_point, program_claim) = self.opening_accumulator.get_committed_polynomial_opening( CommittedPolynomial::ProgramImageInit, @@ -2285,7 +2277,6 @@ impl< .bytecode_words .clone(), ), - start_index, padded_len, }, ); diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index 3bd04abdec..3a143297fb 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -1229,7 +1229,6 @@ impl< self.advice_reduction_verifier_trusted = Some(AdviceClaimReductionVerifier::new( AdviceKind::Trusted, self.program_io.memory_layout.max_trusted_advice_size as usize, - self.proof.trace_length, precommitted_scheduling_reference, &self.opening_accumulator, )); @@ -1238,7 +1237,6 @@ impl< self.advice_reduction_verifier_untrusted = Some(AdviceClaimReductionVerifier::new( AdviceKind::Untrusted, self.program_io.memory_layout.max_untrusted_advice_size as usize, - self.proof.trace_length, precommitted_scheduling_reference, &self.opening_accumulator, )); From d17b186aee2261136df48169a37d3e50f0743b38 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 25 Mar 2026 00:23:45 -0700 Subject: [PATCH 16/20] Remove binary test file `fib_io_device.bin`. --- jolt-sdk/tests/fixtures/fib_io_device.bin | Bin 199 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 jolt-sdk/tests/fixtures/fib_io_device.bin diff --git a/jolt-sdk/tests/fixtures/fib_io_device.bin b/jolt-sdk/tests/fixtures/fib_io_device.bin deleted file mode 100644 index e8980c42e543865df8a03142f7a16fd62eeb6bf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 199 zcmZQ%fB+*X4X0S4tcRb@eAHv`P=j&>pmf9kdN6AOl!l4J_zTd)Ve&BX4N!R)eE?NG X(7pz+`4E!f#5O1&s*VFn3qWZAVf-KC From c5b9247333a4228f204a15aa344e4a5444d92b25 Mon Sep 17 00:00:00 2001 From: Quang Dao Date: Thu, 26 Mar 2026 08:33:53 -0600 Subject: [PATCH 17/20] fix(zkvm): expose Dory hint helpers for C++ integration Expose Dory opening-hint accessors and a preprocessing constructor that accepts precomputed generators. This lets jolt-cpp serialize Dory hints and reuse loaded prover setup without carrying ad hoc local patches in the submodule consumer. Made-with: Cursor --- .../poly/commitment/dory/commitment_scheme.rs | 8 ++++++++ jolt-core/src/poly/commitment/dory/mod.rs | 2 +- jolt-core/src/zkvm/prover.rs | 17 ++++++++++++----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/jolt-core/src/poly/commitment/dory/commitment_scheme.rs b/jolt-core/src/poly/commitment/dory/commitment_scheme.rs index 57514e2bc8..eb7a27f6e6 100644 --- a/jolt-core/src/poly/commitment/dory/commitment_scheme.rs +++ b/jolt-core/src/poly/commitment/dory/commitment_scheme.rs @@ -47,6 +47,14 @@ impl DoryOpeningProofHint { Self(row_commitments) } + pub fn empty() -> Self { + Self(Vec::new()) + } + + pub fn rows(&self) -> &[ArkG1] { + &self.0 + } + fn into_rows(self) -> Vec { self.0 } diff --git a/jolt-core/src/poly/commitment/dory/mod.rs b/jolt-core/src/poly/commitment/dory/mod.rs index 4204949e88..11c2444f1d 100644 --- a/jolt-core/src/poly/commitment/dory/mod.rs +++ b/jolt-core/src/poly/commitment/dory/mod.rs @@ -13,7 +13,7 @@ mod tests; #[cfg(feature = "zk")] pub use commitment_scheme::bind_opening_inputs_zk; -pub use commitment_scheme::{bind_opening_inputs, DoryCommitmentScheme}; +pub use commitment_scheme::{bind_opening_inputs, DoryCommitmentScheme, DoryOpeningProofHint}; pub use dory_globals::{DoryContext, DoryGlobals, DoryLayout}; pub use jolt_dory_routines::{JoltG1Routines, JoltG2Routines}; pub use wrappers::{ diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 7d09cde0cc..7a2182231b 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -2487,17 +2487,24 @@ where C: JoltCurve, PCS: CommitmentScheme, { + pub fn new_with_generators( + shared: JoltSharedPreprocessing, + generators: PCS::ProverSetup, + ) -> Self { + Self { + generators, + shared, + _curve: std::marker::PhantomData, + } + } + #[tracing::instrument(skip_all, name = "JoltProverPreprocessing::new")] pub fn new(shared: JoltSharedPreprocessing) -> Self { let committed_mode = shared.program.is_committed(); let (max_total_vars, _) = shared.compute_max_total_vars(committed_mode); let generators = PCS::setup_prover(max_total_vars); - JoltProverPreprocessing { - generators, - shared, - _curve: std::marker::PhantomData, - } + Self::new_with_generators(shared, generators) } #[cfg(feature = "zk")] From fb5f4c904abdcf668be23f8514ed6bc445820844 Mon Sep 17 00:00:00 2001 From: Quang Dao Date: Sat, 28 Mar 2026 08:23:23 -0400 Subject: [PATCH 18/20] fix: make VirtualRegisterAllocator::allocate() public Required by the gen_inlines tool to pre-allocate Reg{40} before generating inline sequences, matching INLINE::trace()'s rd=0 remapping at runtime. Made-with: Cursor --- tracer/src/utils/virtual_registers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracer/src/utils/virtual_registers.rs b/tracer/src/utils/virtual_registers.rs index e93bb6b9f3..05392c09b9 100644 --- a/tracer/src/utils/virtual_registers.rs +++ b/tracer/src/utils/virtual_registers.rs @@ -131,7 +131,7 @@ impl VirtualRegisterAllocator { /// Allocate virtual register that can be used in the inline sequence of /// an instruction. Skips reserved registers (32-39) and uses registers 40-47. - pub(crate) fn allocate(&self) -> VirtualRegisterGuard { + pub fn allocate(&self) -> VirtualRegisterGuard { for (i, allocated) in self .allocated .lock() From 3f3837b63ce2d1e93f0b0fdb59f6caaceff5026b Mon Sep 17 00:00:00 2001 From: Quang Dao Date: Sun, 29 Mar 2026 22:02:45 -0400 Subject: [PATCH 19/20] fix(sumcheck): scale polynomial evals for phase1 gap rounds in RAF and output check When phase1_num_rounds < log_T, the input_claim for RafEvaluation and OutputCheck sumchecks is pre-scaled by 2^(phase3_cycle_rounds) to pre-compensate for the gap-round halvings. However, the raw polynomial evaluations computed from the witness data sum to the unscaled claim. This mismatch causes UniPoly reconstruction to produce an incorrect polynomial, failing the verifier's poly(0) + poly(1) == previous_claim check. Scale the raw evaluations by 2^(phase3_cycle_rounds) before polynomial reconstruction so they are consistent with the pre-scaled previous_claim. Made-with: Cursor --- jolt-core/src/zkvm/ram/output_check.rs | 15 ++++++++++++++- jolt-core/src/zkvm/ram/raf_evaluation.rs | 12 +++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/jolt-core/src/zkvm/ram/output_check.rs b/jolt-core/src/zkvm/ram/output_check.rs index 2e059686c4..f1fce1c017 100644 --- a/jolt-core/src/zkvm/ram/output_check.rs +++ b/jolt-core/src/zkvm/ram/output_check.rs @@ -308,7 +308,20 @@ impl SumcheckInstanceProver for OutputSumchec [c0, e] }); - eq_r_address.gruen_poly_deg_3(q_constant, q_quadratic, previous_claim) + // When phase1_num_rounds < log_T, the input_claim is pre-scaled by + // 2^(phase3_cycle_rounds) to compensate for the gap-round halvings that follow. + // The raw q_constant and q_quadratic sum to the unscaled claim, so we must + // scale them by the same factor to satisfy poly(0) + poly(1) == previous_claim. + let gap = self.params.phase3_cycle_rounds(); + if gap > 0 { + eq_r_address.gruen_poly_deg_3( + q_constant.mul_pow_2(gap), + q_quadratic.mul_pow_2(gap), + previous_claim, + ) + } else { + eq_r_address.gruen_poly_deg_3(q_constant, q_quadratic, previous_claim) + } } #[tracing::instrument(skip_all, name = "OutputSumcheckProver::ingest_challenge")] diff --git a/jolt-core/src/zkvm/ram/raf_evaluation.rs b/jolt-core/src/zkvm/ram/raf_evaluation.rs index c72f8c579f..3296b5ae54 100644 --- a/jolt-core/src/zkvm/ram/raf_evaluation.rs +++ b/jolt-core/src/zkvm/ram/raf_evaluation.rs @@ -306,7 +306,17 @@ impl SumcheckInstanceProver for RafEvaluation ) .map(F::reduce_product_accum); - UniPoly::from_evals_and_hint(previous_claim, &evals) + // When phase1_num_rounds < log_T, the input_claim is pre-scaled by + // 2^(phase3_cycle_rounds) to compensate for the gap-round halvings that follow. + // The raw polynomial evals sum to the unscaled claim, so we must scale them + // by the same factor to satisfy poly(0) + poly(1) == previous_claim. + let gap = self.params.phase3_cycle_rounds(); + if gap > 0 { + let scaled: Vec = evals.iter().map(|e| e.mul_pow_2(gap)).collect(); + UniPoly::from_evals_and_hint(previous_claim, &scaled) + } else { + UniPoly::from_evals_and_hint(previous_claim, &evals) + } } #[tracing::instrument(skip_all, name = "RamRafEvaluationSumcheckProver::ingest_challenge")] From 08091ced44f81f504d7a6a8de7ee5c568eb232ec Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 8 Apr 2026 15:59:11 -0700 Subject: [PATCH 20/20] Fix clippy warnings and formatting --- jolt-core/src/zkvm/transpilable_verifier.rs | 33 ++++--- transpiler/src/main.rs | 96 ++++++++++----------- 2 files changed, 67 insertions(+), 62 deletions(-) diff --git a/jolt-core/src/zkvm/transpilable_verifier.rs b/jolt-core/src/zkvm/transpilable_verifier.rs index 1c77b46846..fc3a91c411 100644 --- a/jolt-core/src/zkvm/transpilable_verifier.rs +++ b/jolt-core/src/zkvm/transpilable_verifier.rs @@ -57,11 +57,11 @@ use crate::zkvm::{ BytecodeReadRafAddressSumcheckVerifier, BytecodeReadRafCycleSumcheckVerifier, BytecodeReadRafSumcheckParams, }, - config::ProgramMode, claim_reductions::{ IncClaimReductionSumcheckVerifier, InstructionLookupsClaimReductionSumcheckVerifier, RamRaClaimReductionSumcheckVerifier, }, + config::ProgramMode, fiat_shamir_preamble, instruction_lookups::{ ra_virtual::RaSumcheckVerifier as LookupsRaSumcheckVerifier, @@ -606,8 +606,13 @@ impl< fn verify_stage6a( &mut self, - ) -> Result<(BytecodeReadRafSumcheckParams, BooleanitySumcheckParams), ProofVerifyError> - { + ) -> Result< + ( + BytecodeReadRafSumcheckParams, + BooleanitySumcheckParams, + ), + ProofVerifyError, + > { let n_cycle_vars = self.proof.trace_length.log_2(); let program_mode = if self.preprocessing.shared.program.is_committed() { ProgramMode::Committed @@ -685,12 +690,11 @@ impl< ); let main_total_vars = self.proof.trace_length.log_2() + self.one_hot_params.log_k_chunk; - let precommitted_candidates = - self.preprocessing.shared.precommitted_candidate_total_vars( - self.preprocessing.shared.program.is_committed(), - self.trusted_advice_commitment.is_some(), - self.proof.untrusted_advice_commitment.is_some(), - ); + let precommitted_candidates = self.preprocessing.shared.precommitted_candidate_total_vars( + self.preprocessing.shared.program.is_committed(), + self.trusted_advice_commitment.is_some(), + self.proof.untrusted_advice_commitment.is_some(), + ); let precommitted_scheduling_reference = crate::zkvm::claim_reductions::PrecommittedClaimReduction::::scheduling_reference( main_total_vars, @@ -724,8 +728,9 @@ impl< &self.opening_accumulator, &mut self.transcript, ); - self.bytecode_reduction_verifier = - Some(BytecodeClaimReductionVerifier::new(bytecode_reduction_params)); + self.bytecode_reduction_verifier = Some(BytecodeClaimReductionVerifier::new( + bytecode_reduction_params, + )); let padded_len_words = self .preprocessing @@ -741,9 +746,9 @@ impl< &self.opening_accumulator, &mut self.transcript, ); - self.program_image_reduction_verifier = Some( - ProgramImageClaimReductionVerifier::new(program_image_reduction_params), - ); + self.program_image_reduction_verifier = Some(ProgramImageClaimReductionVerifier::new( + program_image_reduction_params, + )); } let mut instances: Vec<&dyn SumcheckInstanceVerifier> = vec![ diff --git a/transpiler/src/main.rs b/transpiler/src/main.rs index c1e933b4f6..fe3dcebce2 100644 --- a/transpiler/src/main.rs +++ b/transpiler/src/main.rs @@ -252,55 +252,55 @@ fn main() { CommittedProgramPreprocessing, ProgramPreprocessing, TrustedProgramCommitments, }; - let symbolic_program: ProgramPreprocessing = - match &real_preprocessing.shared.program { - ProgramPreprocessing::Full(full) => { - println!(" Mode: Full"); - ProgramPreprocessing::Full(full.clone()) - } - ProgramPreprocessing::Committed(committed) => { - println!(" Mode: Committed (symbolizing trusted commitments)"); - let bytecode_commitments = TrustedBytecodeCommitments { - commitments: committed - .bytecode_commitments - .commitments - .iter() - .enumerate() - .map(|(i, c)| { - let chunks = var_alloc - .alloc_commitment(c, &format!("trusted_bytecode_{i}")); - AstCommitment::new(chunks) - }) - .collect(), - num_columns: committed.bytecode_commitments.num_columns, - log_k_chunk: committed.bytecode_commitments.log_k_chunk, - bytecode_chunk_count: committed.bytecode_commitments.bytecode_chunk_count, - bytecode_len: committed.bytecode_commitments.bytecode_len, - bytecode_T: committed.bytecode_commitments.bytecode_T, - }; - let program_commitments = TrustedProgramCommitments { - program_image_commitment: { - let chunks = var_alloc.alloc_commitment( - &committed.program_commitments.program_image_commitment, - "trusted_program_image", - ); + let symbolic_program: ProgramPreprocessing = match &real_preprocessing + .shared + .program + { + ProgramPreprocessing::Full(full) => { + println!(" Mode: Full"); + ProgramPreprocessing::Full(full.clone()) + } + ProgramPreprocessing::Committed(committed) => { + println!(" Mode: Committed (symbolizing trusted commitments)"); + let bytecode_commitments = TrustedBytecodeCommitments { + commitments: committed + .bytecode_commitments + .commitments + .iter() + .enumerate() + .map(|(i, c)| { + let chunks = + var_alloc.alloc_commitment(c, &format!("trusted_bytecode_{i}")); AstCommitment::new(chunks) - }, - program_image_num_columns: committed - .program_commitments - .program_image_num_columns, - program_image_num_words: committed - .program_commitments - .program_image_num_words, - }; - ProgramPreprocessing::Committed(CommittedProgramPreprocessing { - meta: committed.meta.clone(), - bytecode_commitments, - program_commitments, - prover_data: None, - }) - } - }; + }) + .collect(), + num_columns: committed.bytecode_commitments.num_columns, + log_k_chunk: committed.bytecode_commitments.log_k_chunk, + bytecode_chunk_count: committed.bytecode_commitments.bytecode_chunk_count, + bytecode_len: committed.bytecode_commitments.bytecode_len, + bytecode_T: committed.bytecode_commitments.bytecode_T, + }; + let program_commitments = TrustedProgramCommitments { + program_image_commitment: { + let chunks = var_alloc.alloc_commitment( + &committed.program_commitments.program_image_commitment, + "trusted_program_image", + ); + AstCommitment::new(chunks) + }, + program_image_num_columns: committed + .program_commitments + .program_image_num_columns, + program_image_num_words: committed.program_commitments.program_image_num_words, + }; + ProgramPreprocessing::Committed(CommittedProgramPreprocessing { + meta: committed.meta.clone(), + bytecode_commitments, + program_commitments, + prover_data: None, + }) + } + }; // Construct directly — don't use new_committed() because it calls // PCS::setup_prover + program.commit, which panics for AstCommitmentScheme.

?q=w^9CgwxPN!ksCMA97z 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%0A8MJ!T!F`5i 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 Zre!j<%XFVikIM9YnfA(57UTa-{x5Cf9;^TW literal 0 HcmV?d00001 From 6335f1452e3fa294c4c6a0002faaece688e9d609 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Thu, 19 Mar 2026 14:30:22 -0700 Subject: [PATCH 11/20] remove extra examples from git --- .../Cargo.toml | 14 --- .../guest/Cargo.toml | 11 -- .../guest/src/lib.rs | 13 --- .../guest/src/main.rs | 5 - .../src/main.rs | 72 ------------- examples/sha2-chain-huge-advice/Cargo.toml | 10 -- .../sha2-chain-huge-advice/guest/Cargo.toml | 11 -- .../sha2-chain-huge-advice/guest/src/lib.rs | 39 ------- .../sha2-chain-huge-advice/guest/src/main.rs | 5 - examples/sha2-chain-huge-advice/src/main.rs | 100 ------------------ jolt-sdk/Cargo.toml | 2 +- 11 files changed, 1 insertion(+), 281 deletions(-) delete mode 100644 examples/sha2-chain-committed-16-untrusted-advice/Cargo.toml delete mode 100644 examples/sha2-chain-committed-16-untrusted-advice/guest/Cargo.toml delete mode 100644 examples/sha2-chain-committed-16-untrusted-advice/guest/src/lib.rs delete mode 100644 examples/sha2-chain-committed-16-untrusted-advice/guest/src/main.rs delete mode 100644 examples/sha2-chain-committed-16-untrusted-advice/src/main.rs delete mode 100644 examples/sha2-chain-huge-advice/Cargo.toml delete mode 100644 examples/sha2-chain-huge-advice/guest/Cargo.toml delete mode 100644 examples/sha2-chain-huge-advice/guest/src/lib.rs delete mode 100644 examples/sha2-chain-huge-advice/guest/src/main.rs delete mode 100644 examples/sha2-chain-huge-advice/src/main.rs diff --git a/examples/sha2-chain-committed-16-untrusted-advice/Cargo.toml b/examples/sha2-chain-committed-16-untrusted-advice/Cargo.toml deleted file mode 100644 index d93d450478..0000000000 --- a/examples/sha2-chain-committed-16-untrusted-advice/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "sha2-chain-committed-16-untrusted-advice" -version = "0.1.0" -edition = "2021" - -[dependencies] -jolt-sdk = { workspace = true, features = ["host"] } -jolt-core.workspace = true -tracing-subscriber.workspace = true -tracing.workspace = true -jolt-inlines-sha2 = { workspace = true, features = ["host"] } -guest = { package = "sha2-chain-committed-16-untrusted-advice-guest", path = "./guest" } - -hex.workspace = true diff --git a/examples/sha2-chain-committed-16-untrusted-advice/guest/Cargo.toml b/examples/sha2-chain-committed-16-untrusted-advice/guest/Cargo.toml deleted file mode 100644 index 77f5c8f21a..0000000000 --- a/examples/sha2-chain-committed-16-untrusted-advice/guest/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "sha2-chain-committed-16-untrusted-advice-guest" -version = "0.1.0" -edition = "2021" - -[features] -guest = [] - -[dependencies] -jolt = { package = "jolt-sdk", path = "../../../jolt-sdk", features = [] } -jolt-inlines-sha2.workspace = true diff --git a/examples/sha2-chain-committed-16-untrusted-advice/guest/src/lib.rs b/examples/sha2-chain-committed-16-untrusted-advice/guest/src/lib.rs deleted file mode 100644 index 4775d81279..0000000000 --- a/examples/sha2-chain-committed-16-untrusted-advice/guest/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -#![cfg_attr(feature = "guest", no_std)] - -#[jolt::provable(max_trace_length = 4194304)] -fn sha2_chain( - input: jolt::UntrustedAdvice<[u8; 32]>, - num_iters: jolt::UntrustedAdvice, -) -> [u8; 32] { - let mut hash = *input; - for _ in 0..*num_iters { - hash = jolt_inlines_sha2::Sha256::digest(&hash); - } - hash -} diff --git a/examples/sha2-chain-committed-16-untrusted-advice/guest/src/main.rs b/examples/sha2-chain-committed-16-untrusted-advice/guest/src/main.rs deleted file mode 100644 index bd66a29930..0000000000 --- a/examples/sha2-chain-committed-16-untrusted-advice/guest/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![cfg_attr(feature = "guest", no_std)] -#![no_main] - -#[allow(unused_imports)] -use sha2_chain_committed_16_untrusted_advice_guest::*; diff --git a/examples/sha2-chain-committed-16-untrusted-advice/src/main.rs b/examples/sha2-chain-committed-16-untrusted-advice/src/main.rs deleted file mode 100644 index 8f0bc49a3b..0000000000 --- a/examples/sha2-chain-committed-16-untrusted-advice/src/main.rs +++ /dev/null @@ -1,72 +0,0 @@ -use jolt_inlines_sha2 as _; -use jolt_sdk::{DoryContext, DoryGlobals, DoryLayout, UntrustedAdvice}; -use std::time::Instant; -use tracing::info; - -fn dory_layout_from_env() -> DoryLayout { - match std::env::var("JOLT_DORY_LAYOUT") - .unwrap_or_else(|_| "cycle".to_string()) - .to_ascii_lowercase() - .as_str() - { - "cycle" | "cyclemajor" => DoryLayout::CycleMajor, - "address" | "addressmajor" | "addr" => DoryLayout::AddressMajor, - other => panic!("invalid JOLT_DORY_LAYOUT={other}; expected cycle|address"), - } -} - -pub fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let bytecode_chunk = std::env::args() - .skip_while(|arg| arg != "--committed-bytecode") - .nth(1) - .map(|arg| arg.parse().unwrap()); - - let layout = dory_layout_from_env(); - DoryGlobals::initialize_context(1, 1, DoryContext::Main, Some(layout)) - .expect("failed to initialize Dory layout"); - info!("dory layout: {:?}", layout); - - let target_dir = "/tmp/jolt-guest-targets"; - let mut program = guest::compile_sha2_chain(target_dir); - - let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { - info!("bytecode_chunk_count: {}", chunk_count); - let prover_preprocessing = - guest::preprocess_committed_sha2_chain(&mut program, chunk_count); - let verifier_preprocessing = - guest::verifier_preprocessing_from_prover_sha2_chain(&prover_preprocessing); - (prover_preprocessing, verifier_preprocessing) - } else { - let shared_preprocessing = guest::preprocess_shared_sha2_chain(&mut program); - let prover_preprocessing = - guest::preprocess_prover_sha2_chain(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_sha2_chain( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); - (prover_preprocessing, verifier_preprocessing) - }; - - let prove_sha2_chain = guest::build_prover_sha2_chain(program, prover_preprocessing); - let verify_sha2_chain = guest::build_verifier_sha2_chain(verifier_preprocessing); - - let input = [5u8; 32]; - let iters = 10; - - let native_output = guest::sha2_chain(UntrustedAdvice::new(input), UntrustedAdvice::new(iters)); - let now = Instant::now(); - let (output, proof, program_io) = - prove_sha2_chain(UntrustedAdvice::new(input), UntrustedAdvice::new(iters)); - info!("Prover runtime: {} s", now.elapsed().as_secs_f64()); - let is_valid = verify_sha2_chain(output, program_io.panic, proof); - - assert_eq!(output, native_output, "output mismatch"); - if !is_valid { - return Err(std::io::Error::other("verification failed").into()); - } - info!("output: {}", hex::encode(output)); - info!("valid: {is_valid}"); - Ok(()) -} diff --git a/examples/sha2-chain-huge-advice/Cargo.toml b/examples/sha2-chain-huge-advice/Cargo.toml deleted file mode 100644 index 6dd8e1a706..0000000000 --- a/examples/sha2-chain-huge-advice/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "sha2-chain-huge-advice" -version = "0.1.0" -edition = "2021" - -[dependencies] -jolt-sdk = { workspace = true, features = ["host"] } -tracing-subscriber.workspace = true -tracing.workspace = true -guest = { package = "sha2-chain-huge-advice-guest", path = "./guest" } diff --git a/examples/sha2-chain-huge-advice/guest/Cargo.toml b/examples/sha2-chain-huge-advice/guest/Cargo.toml deleted file mode 100644 index b9f49e6d51..0000000000 --- a/examples/sha2-chain-huge-advice/guest/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "sha2-chain-huge-advice-guest" -version = "0.1.0" -edition = "2021" - -[features] -guest = [] -compute_advice = [] - -[dependencies] -jolt = { package = "jolt-sdk", path = "../../../jolt-sdk", features = [] } diff --git a/examples/sha2-chain-huge-advice/guest/src/lib.rs b/examples/sha2-chain-huge-advice/guest/src/lib.rs deleted file mode 100644 index 39e5f98e34..0000000000 --- a/examples/sha2-chain-huge-advice/guest/src/lib.rs +++ /dev/null @@ -1,39 +0,0 @@ -#![cfg_attr(feature = "guest", no_std)] -use jolt::{end_cycle_tracking, start_cycle_tracking}; - -#[jolt::provable( - heap_size = 32768, - max_trace_length = 4194304, - // Keep advice capacity very large, but below the threshold that would place - // the untrusted-advice base at address 0 in the guest memory layout. - max_untrusted_advice_size = 16777216, - backtrace = "off" -)] -fn fib_huge_advice(n: u32, huge_advice: jolt::UntrustedAdvice<&[u8]>) -> u128 { - let advice = *huge_advice; - jolt::check_advice!(advice.len() >= 2, "advice must contain at least 2 bytes"); - let last_idx = advice.len() - 1; - let sampled_idx = (n as usize) % advice.len(); - - jolt::check_advice_eq!(advice[0] as u64, 7u64, "unexpected first advice byte"); - jolt::check_advice_eq!(advice[last_idx] as u64, 7u64, "unexpected last advice byte"); - jolt::check_advice_eq!( - advice[sampled_idx] as u64, - 7u64, - "unexpected sampled advice byte" - ); - - let mut a: u128 = 0; - let mut b: u128 = 1; - let mut sum: u128; - - start_cycle_tracking("fib_loop_huge_advice"); - for _ in 1..n { - sum = a + b; - a = b; - b = sum; - } - end_cycle_tracking("fib_loop_huge_advice"); - - b -} diff --git a/examples/sha2-chain-huge-advice/guest/src/main.rs b/examples/sha2-chain-huge-advice/guest/src/main.rs deleted file mode 100644 index c3d5d27220..0000000000 --- a/examples/sha2-chain-huge-advice/guest/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![cfg_attr(feature = "guest", no_std)] -#![no_main] - -#[allow(unused_imports)] -use sha2_chain_huge_advice_guest::*; diff --git a/examples/sha2-chain-huge-advice/src/main.rs b/examples/sha2-chain-huge-advice/src/main.rs deleted file mode 100644 index afb7c4d65d..0000000000 --- a/examples/sha2-chain-huge-advice/src/main.rs +++ /dev/null @@ -1,100 +0,0 @@ -use jolt_sdk::{DoryContext, DoryGlobals, DoryLayout, UntrustedAdvice}; -use std::time::Instant; -use tracing::info; - -const N: u32 = 1; -const ADVICE_BYTES: usize = 8388608; - -fn serialized_advice_size(payload_len: usize) -> usize { - let payload = vec![7u8; payload_len]; - jolt_sdk::postcard::to_stdvec(&UntrustedAdvice::new(payload.as_slice())) - .expect("failed to serialize advice input") - .len() -} - -pub fn main() { - tracing_subscriber::fmt::init(); - let bytecode_chunk = std::env::args() - .skip_while(|arg| arg != "--committed-bytecode") - .nth(1) - .map(|arg| arg.parse().unwrap()); - DoryGlobals::initialize_context(1, 1, DoryContext::Main, Some(DoryLayout::AddressMajor)) - .expect("failed to set Dory layout"); - - let advice_bytes = ADVICE_BYTES; - let serialized_advice_bytes = serialized_advice_size(advice_bytes); - let max_untrusted_advice_bytes = serialized_advice_bytes.next_power_of_two(); - - let target_dir = "/tmp/jolt-guest-targets"; - let mut program = guest::compile_fib_huge_advice(target_dir); - program.set_max_untrusted_advice_size(max_untrusted_advice_bytes as u64); - - let (prover_preprocessing, verifier_preprocessing) = if let Some(chunk_count) = bytecode_chunk { - let prover_preprocessing = - guest::preprocess_committed_fib_huge_advice(&mut program, chunk_count); - let verifier_preprocessing = - guest::verifier_preprocessing_from_prover_fib_huge_advice(&prover_preprocessing); - (prover_preprocessing, verifier_preprocessing) - } else { - let shared_preprocessing = guest::preprocess_shared_fib_huge_advice(&mut program); - let prover_preprocessing = - guest::preprocess_prover_fib_huge_advice(shared_preprocessing.clone()); - let verifier_preprocessing = guest::preprocess_verifier_fib_huge_advice( - shared_preprocessing, - prover_preprocessing.generators.to_verifier_setup(), - None, - ); - (prover_preprocessing, verifier_preprocessing) - }; - - let prove_fib_huge_advice = guest::build_prover_fib_huge_advice(program, prover_preprocessing); - let verify_fib_huge_advice = guest::build_verifier_fib_huge_advice(verifier_preprocessing); - - let analysis = guest::analyze_fib_huge_advice(N, UntrustedAdvice::new(&[7u8; 2][..])); - let execution_trace_length = analysis.trace_len(); - let padded_trace_length = execution_trace_length.next_power_of_two(); - - let huge_advice = vec![7u8; advice_bytes]; - let advice_input = UntrustedAdvice::new(huge_advice.as_slice()); - let native_output = guest::fib_huge_advice(N, advice_input); - - let now = Instant::now(); - let (output, proof, program_io) = prove_fib_huge_advice(N, advice_input); - let trace_length = proof.trace_length; - info!("Prover runtime: {} s", now.elapsed().as_secs_f64()); - let is_valid = verify_fib_huge_advice(N, output, program_io.panic, proof); - - info!("output: {output}"); - info!("native_output: {native_output}"); - info!( - "execution trace length: {} and padded trace length: {}", - execution_trace_length, padded_trace_length - ); - info!("padded proof trace length: {}", trace_length); - info!("advice payload bytes: {}", advice_bytes); - info!("serialized advice bytes: {}", serialized_advice_bytes); - info!( - "configured max_untrusted_advice bytes: {}", - max_untrusted_advice_bytes - ); - info!( - "advice_bytes / padded_trace_length = {:.2}", - advice_bytes as f64 / trace_length as f64 - ); - info!("valid: {is_valid}"); - - assert_eq!(output, native_output, "output mismatch"); - // assert_eq!( - // trace_length, padded_trace_length, - // "analysis and proof trace lengths diverged" - // ); - // assert_eq!( - // advice_bytes, ADVICE_BYTES, - // "advice length must match the fixed target" - // ); - // assert!( - // serialized_advice_bytes <= max_untrusted_advice_bytes, - // "serialized advice exceeds configured max_untrusted_advice_size" - // ); - assert!(is_valid, "proof verification failed"); -} diff --git a/jolt-sdk/Cargo.toml b/jolt-sdk/Cargo.toml index 82dde4aa7b..181fe4a6e3 100644 --- a/jolt-sdk/Cargo.toml +++ b/jolt-sdk/Cargo.toml @@ -100,4 +100,4 @@ zeroos = { workspace = true, default-features = false, features = ["arch-riscv", zeroos = { workspace = true, default-features = false, features = ["runtime-nostd", "panic"]} [target.'cfg(all(target_arch = "riscv64", target_os = "linux"))'.dependencies] -zeroos = { workspace = true, default-features = false, features = ["os-linux"]} +zeroos = { workspace = true, default-features = false, features = ["os-linux"]} \ No newline at end of file From 0a5b1e8d2b2ad997974ef7461738a5eb0792e701 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Fri, 20 Mar 2026 09:42:55 -0700 Subject: [PATCH 12/20] fix(zkvm): avoid Dory setup cache mismatches across runs Canonicalize odd/even Dory setup sizes that map to the same generator bucket so repeated proofs reuse a consistent setup instead of mixing cached prepared points from different URS files. Also stop full-mode preprocessing from sizing prover setups against committed bytecode and program-image dimensions. --- .../poly/commitment/dory/commitment_scheme.rs | 38 +++++++++++++++++-- jolt-core/src/zkvm/prover.rs | 7 ++-- jolt-core/src/zkvm/verifier.rs | 10 ++--- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/jolt-core/src/poly/commitment/dory/commitment_scheme.rs b/jolt-core/src/poly/commitment/dory/commitment_scheme.rs index f7a00e8ee4..6337b6b909 100644 --- a/jolt-core/src/poly/commitment/dory/commitment_scheme.rs +++ b/jolt-core/src/poly/commitment/dory/commitment_scheme.rs @@ -27,6 +27,15 @@ use rayon::prelude::*; use std::borrow::Borrow; use tracing::trace_span; +fn debug_disable_dory_setup_cache() -> bool { + std::env::var("JOLT_DEBUG_DISABLE_DORY_SETUP_CACHE") + .map(|v| { + let value = v.trim().to_ascii_lowercase(); + !matches!(value.as_str(), "" | "0" | "false" | "off") + }) + .unwrap_or(false) +} + #[derive(Clone)] pub struct DoryCommitmentScheme; @@ -43,6 +52,18 @@ impl DoryOpeningProofHint { } } +#[inline] +fn canonical_setup_log_n(max_num_vars: usize) -> usize { + // Dory's generator count depends on ceil(max_log_n / 2), so odd/even pairs like + // 23 and 24 share the same generator bucket. Canonicalizing to the even bucket + // representative keeps those runs on a single URS file. + if max_num_vars.is_multiple_of(2) { + max_num_vars + } else { + max_num_vars + 1 + } +} + pub fn bind_opening_inputs( transcript: &mut ProofTranscript, opening_point: &[F::Challenge], @@ -85,10 +106,11 @@ impl CommitmentScheme for DoryCommitmentScheme { fn setup_prover(max_num_vars: usize) -> Self::ProverSetup { let _span = trace_span!("DoryCommitmentScheme::setup_prover").entered(); + let canonical_max_num_vars = canonical_setup_log_n(max_num_vars); #[cfg(not(target_arch = "wasm32"))] - let setup = ArkworksProverSetup::new_from_urs(max_num_vars); + let setup = ArkworksProverSetup::new_from_urs(canonical_max_num_vars); #[cfg(target_arch = "wasm32")] - let setup = ArkworksProverSetup::new(max_num_vars); + let setup = ArkworksProverSetup::new(canonical_max_num_vars); // The prepared-point cache in dory-pcs is global and can only be initialized once. // In unit tests, multiple setups with different sizes are created, so initializing the @@ -383,7 +405,11 @@ impl StreamingCommitmentScheme for DoryCommitmentScheme { } let g2_bases = &setup.g2_vec[..num_rows]; - let tier_2 = ::multi_pair_g2_setup(&row_commitments, g2_bases); + let tier_2 = if debug_disable_dory_setup_cache() { + ::multi_pair(&row_commitments, g2_bases) + } else { + ::multi_pair_g2_setup(&row_commitments, g2_bases) + }; (tier_2, DoryOpeningProofHint::new(row_commitments)) } else { @@ -391,7 +417,11 @@ impl StreamingCommitmentScheme for DoryCommitmentScheme { chunks.iter().flat_map(|chunk| chunk.clone()).collect(); let g2_bases = &setup.g2_vec[..row_commitments.len()]; - let tier_2 = ::multi_pair_g2_setup(&row_commitments, g2_bases); + let tier_2 = if debug_disable_dory_setup_cache() { + ::multi_pair(&row_commitments, g2_bases) + } else { + ::multi_pair_g2_setup(&row_commitments, g2_bases) + }; (tier_2, DoryOpeningProofHint::new(row_commitments)) } diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index fae263ef8f..cdf6c3817d 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -47,8 +47,7 @@ use crate::{ compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, SumcheckId, BIG_ENDIAN, }, - rlc_polynomial::{RLCStreamingData, TraceSource, - }, + rlc_polynomial::{RLCStreamingData, TraceSource}, }, pprof_scope, subprotocols::{ @@ -2545,7 +2544,7 @@ where { #[tracing::instrument(skip_all, name = "JoltProverPreprocessing::new")] pub fn new(shared: JoltSharedPreprocessing, program: Arc) -> Self { - let (max_total_vars, _) = shared.compute_max_total_vars(); + let (max_total_vars, _) = shared.compute_max_total_vars(false); let generators = PCS::setup_prover(max_total_vars); JoltProverPreprocessing { @@ -2565,7 +2564,7 @@ where shared: JoltSharedPreprocessing, program: Arc, ) -> Self { - let (max_total_vars, max_log_k_chunk) = shared.compute_max_total_vars(); + let (max_total_vars, max_log_k_chunk) = shared.compute_max_total_vars(true); let generators = PCS::setup_prover(max_total_vars); let (bytecode_commitments, bytecode_hints) = TrustedBytecodeCommitments::derive( &program.bytecode, diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index b0951f373a..6f97640b09 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -2175,13 +2175,9 @@ impl JoltSharedPreprocessing { } #[inline] - pub(crate) fn compute_max_total_vars(&self) -> (usize, usize) { + pub(crate) fn compute_max_total_vars(&self, include_committed: bool) -> (usize, usize) { use common::constants::ONEHOT_CHUNK_THRESHOLD_LOG_T; - let max_t_any = self - .max_padded_trace_length - .max(self.bytecode_size()) - .max(self.program_image_len_words().max(1).next_power_of_two()) - .next_power_of_two(); + let max_t_any = self.max_padded_trace_length.next_power_of_two(); let max_log_t = max_t_any.log_2(); let max_log_k_chunk = if max_log_t < ONEHOT_CHUNK_THRESHOLD_LOG_T { 4 @@ -2191,7 +2187,7 @@ impl JoltSharedPreprocessing { let max_total_vars = Self::max_total_vars_from_candidates( max_log_k_chunk + max_log_t, - self.precommitted_candidate_total_vars(true, true, true), + self.precommitted_candidate_total_vars(include_committed, true, true), ); (max_total_vars, max_log_k_chunk) From 2f17616cfa815a1664c7e9c6fd0e233672fe093c Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Fri, 20 Mar 2026 10:17:57 -0700 Subject: [PATCH 13/20] refactor(docs): clarify embedding of precommitted polynomials in Dory geometry --- book/src/how/architecture/opening-proof.md | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/book/src/how/architecture/opening-proof.md b/book/src/how/architecture/opening-proof.md index 7eec23e41c..3e54dd3bf3 100644 --- a/book/src/how/architecture/opening-proof.md +++ b/book/src/how/architecture/opening-proof.md @@ -95,9 +95,7 @@ If some precommitted polynomial already has $D$ variables, we call it a **domina #### How Main Polynomials Sit In The Joint Matrix -The main polynomials are embedded the way they are because this lets Stage 8 reuse the old sumcheck outputs unchanged. - -As a concrete example, take $D = 5$. Since Dory uses a balanced split, this means: +The main polynomials are embedded depending on the dory layout. As a concrete example, take $D = 5$. Since Dory uses a balanced split, this means: $$ \sigma_D = 3, \qquad \nu_D = 2, @@ -143,8 +141,6 @@ row10 | a_100 | . | . | . | a_101 | . | . | . row11 | a_110 | . | . | . | a_111 | . | . | . | ``` -This is exactly what "the dense polynomial gets the highest $T$ bits" means in practice: the coefficient slots are those where the trailing $K+B$ bits are zero. - The same idea applies to one-hot polynomials: - in `CycleMajor`, they use the lowest $T+K$ bits @@ -186,14 +182,14 @@ row6 | a_11 . . . . . . . . . . . . . . . | row7 | . . . . . . . . . . . . . . . . | ``` -So the logical embedding is unchanged, but it is no longer a convenient row-local chunking. That is why the implementation switches to explicit sparse row/column placement in this case. +So the logical embedding is unchanged, but it is no longer a convenient row-local chunking. That is why the implementation switches to explicit sparse row/column placement in this case. Because polynomial lengths are powers of two, the placement still stays aligned: either the stride is a multiple of the row width, so the polynomial occupies the same column range in every row it touches, or the stride divides the row width, so it stays in a fixed column but appears only in every few rows, as in the example above. #### Final Dory Opening Point In summary - in `CycleMajor`, the main dense / one-hot geometry consumes the low bits of the final Dory point, so any extra precommitted variables must sit on the high side - in `AddressMajor`, the main geometry consumes the high bits, so any extra precommitted variables must sit on the low side -- each block appears in reverse because Dory opening points are written in big-endian order, while the sumcheck challenges are accumulated round-by-round and then normalized into that order +- each block appears in reverse because we always bind polynomials during claim reduction sumchecks from low to high bits Now we study two cases: If there **is** a dominant precommitted polynomial, let the raw Stage 6b challenges be @@ -256,11 +252,9 @@ $$ This is exactly the logic implemented in `stage8_opening_point()` in `prover.rs`. -#### Embedding Smaller Precommitted Polynomials +#### Embedding Precommitted Polynomials -Now suppose there is **no dominant precommitted polynomial**, and we want to batch a smaller precommitted polynomial with the rest of the Stage 8 openings. The verifier's commitment to a precommitted polynomial already treats that polynomial as occupying the first rows and first columns of its balanced Dory matrix. Therefore, when we place that polynomial inside the larger joint matrix, we must continue to place it in the top-left corner; otherwise the verifier would be checking the Dory proof against a different geometry than the one used by the commitment. - -This is why smaller precommitted polynomials are placed in the **top-left corner** of the joint Dory matrix: +The verifier already has the commitment to the precommitted polynomial. That commitment is computed under the convention that the polynomial occupies the top-left block of its balanced Dory matrix, meaning the earliest rows and earliest columns. So when we embed that polynomial into the larger joint matrix, we must preserve that same top-left placement; otherwise the verifier would be checking the Dory proof against a different geometry from the one encoded in the commitment. ```text Joint Dory matrix: 2^nu_D rows x 2^sigma_D columns @@ -362,9 +356,7 @@ $$ The precommitted sumchecks still bind variables low-to-high. But the final Dory point order is determined by the joint geometry, not by the order in which those rounds happen. -So Jolt permutes the variables of each precommitted polynomial before running the sumcheck. This keeps the sumcheck code simple while ensuring the final claim corresponds to the original polynomial at the correct Stage 8 point. - -This is cheap: it is only a variable-position permutation, so on the coefficient table it is just a bit permutation of the $2^n$ Boolean-hypercube evaluations. +So Jolt permutes the variables of each precommitted polynomial before running the sumcheck. This keeps the sumcheck code simple while ensuring the final claim corresponds to the original polynomial at the correct Stage 8 point. This permutation is cheap because it is only a variable-position movement, so on the coefficient table it is just a bit permutation of the $2^n$ Boolean-hypercube evaluations. Here is a concrete 3-variable example. Suppose the original polynomial is encoded by From f0e350fd3d9e94420dc8a1b1a3a9855ed003357d Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Tue, 24 Mar 2026 14:38:44 -0700 Subject: [PATCH 14/20] Removing debug functions and simplifying state management. Delete unused binary test files to clean up the repository. --- .../src/zkvm/claim_reductions/bytecode.rs | 52 +----------------- .../zkvm/claim_reductions/program_image.rs | 46 +--------------- jolt-sdk/tests/fixtures/fib_proof.bin | Bin 70500 -> 0 bytes .../fixtures/jolt_verifier_preprocessing.dat | Bin 873697 -> 0 bytes 4 files changed, 6 insertions(+), 92 deletions(-) delete mode 100644 jolt-sdk/tests/fixtures/fib_proof.bin delete mode 100644 jolt-sdk/tests/fixtures/jolt_verifier_preprocessing.dat diff --git a/jolt-core/src/zkvm/claim_reductions/bytecode.rs b/jolt-core/src/zkvm/claim_reductions/bytecode.rs index f5d57121ea..256491146e 100644 --- a/jolt-core/src/zkvm/claim_reductions/bytecode.rs +++ b/jolt-core/src/zkvm/claim_reductions/bytecode.rs @@ -38,15 +38,6 @@ use strum::EnumCount; const NUM_VAL_STAGES: usize = 5; -fn debug_bytecode_reduction_enabled() -> bool { - std::env::var("JOLT_DEBUG_BYTECODE_REDUCTION") - .map(|v| { - let value = v.trim().to_ascii_lowercase(); - !matches!(value.as_str(), "" | "0" | "false" | "off") - }) - .unwrap_or(false) -} - #[derive(Clone, Allocative)] pub struct BytecodeClaimReductionParams { pub phase: PrecommittedPhase, @@ -295,8 +286,6 @@ pub struct BytecodeClaimReductionProver { eq_poly: MultilinearPolynomial, scale: F, chunk_value_polys: Vec>, - pending_round_poly: Option>, - running_claim: Option, } impl BytecodeClaimReductionProver { @@ -346,26 +335,12 @@ impl BytecodeClaimReductionProver { .expect("expected permuted bytecode eq polynomial"); let chunk_value_polys: Vec> = permuted_polys.collect(); - if debug_bytecode_reduction_enabled() { - let initial_true_claim: F = (0..value_poly.len()) - .map(|i| value_poly.get_bound_coeff(i) * eq_poly.get_bound_coeff(i)) - .sum(); - tracing::info!( - "BytecodeClaimReduction initialize value_len={} eq_len={} initial_true_claim={}", - value_poly.len(), - eq_poly.len(), - initial_true_claim - ); - } - Self { params, value_poly, eq_poly, scale: F::one(), chunk_value_polys, - pending_round_poly: None, - running_claim: None, } } @@ -437,10 +412,7 @@ impl SumcheckInstanceProver for BytecodeClaim self.params.is_address_phase_round(round) }; if !is_active_round { - let round_poly = - UniPoly::from_coeff(vec![previous_claim * F::from_u64(2).inverse().unwrap()]); - self.pending_round_poly = Some(round_poly.clone()); - return round_poly; + return UniPoly::from_coeff(vec![previous_claim * F::from_u64(2).inverse().unwrap()]); } let trailing_cap = if self.params.is_cycle_phase() { @@ -453,15 +425,10 @@ impl SumcheckInstanceProver for BytecodeClaim let scaling_factor = self.scale * F::one().mul_pow_2(num_trailing_variables); let prev_unscaled = previous_claim * scaling_factor.inverse().unwrap(); let poly_unscaled = self.compute_message_unscaled(prev_unscaled); - let round_poly = poly_unscaled * scaling_factor; - self.pending_round_poly = Some(round_poly.clone()); - round_poly + poly_unscaled * scaling_factor } fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { - if let Some(round_poly) = self.pending_round_poly.take() { - self.running_claim = Some(round_poly.evaluate(&r_j)); - } let is_active_round = if self.params.is_cycle_phase() { self.params.is_cycle_phase_round(round) } else { @@ -489,24 +456,11 @@ impl SumcheckInstanceProver for BytecodeClaim let opening_point = params.normalize_opening_point(sumcheck_challenges); if params.phase == PrecommittedPhase::CycleVariables { - let c_mid = self.cycle_intermediate_claim(); - let synced_cycle_claim = self.running_claim.unwrap_or(c_mid); - if debug_bytecode_reduction_enabled() { - tracing::info!( - "BytecodeClaimReduction cache cycle len={} bound_value={} bound_eq={} scale={} cycle_claim={} synced_cycle_claim={}", - self.value_poly.len(), - self.value_poly.get_bound_coeff(0), - self.eq_poly.get_bound_coeff(0), - self.scale, - c_mid, - synced_cycle_claim, - ); - } accumulator.append_virtual( VirtualPolynomial::BytecodeClaimReductionIntermediate, SumcheckId::BytecodeClaimReductionCyclePhase, opening_point.clone(), - synced_cycle_claim, + self.cycle_intermediate_claim(), ); } diff --git a/jolt-core/src/zkvm/claim_reductions/program_image.rs b/jolt-core/src/zkvm/claim_reductions/program_image.rs index 9db19cd67e..5ac3527616 100644 --- a/jolt-core/src/zkvm/claim_reductions/program_image.rs +++ b/jolt-core/src/zkvm/claim_reductions/program_image.rs @@ -33,15 +33,6 @@ use crate::zkvm::ram::remap_address; use crate::zkvm::witness::{CommittedPolynomial, VirtualPolynomial}; use tracer::JoltDevice; -fn debug_program_image_reduction_enabled() -> bool { - std::env::var("JOLT_DEBUG_PROGRAM_IMAGE_REDUCTION") - .map(|v| { - let value = v.trim().to_ascii_lowercase(); - !matches!(value.as_str(), "" | "0" | "false" | "off") - }) - .unwrap_or(false) -} - #[derive(Clone, Allocative)] pub struct ProgramImageClaimReductionParams { pub phase: PrecommittedPhase, @@ -134,7 +125,7 @@ impl ProgramImageClaimReductionParams { impl SumcheckInstanceParams for ProgramImageClaimReductionParams { fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { - let claim = match self.phase { + match self.phase { PrecommittedPhase::CycleVariables => { // Scalar claims were staged in Stage 4 as virtual openings. accumulator @@ -152,19 +143,7 @@ impl SumcheckInstanceParams for ProgramImageClaimReductionParam ) .1 } - }; - if debug_program_image_reduction_enabled() { - tracing::info!( - "ProgramImageClaimReduction input_claim phase={} claim={}", - if self.phase == PrecommittedPhase::CycleVariables { - "cycle" - } else { - "address" - }, - claim - ); } - claim } fn degree(&self) -> usize { @@ -364,20 +343,13 @@ impl SumcheckInstanceProver let params = self.core.params(); let opening_point = params.normalize_opening_point(sumcheck_challenges); if params.phase == PrecommittedPhase::CycleVariables { - let c_mid = self.core.cycle_intermediate_claim(); - if debug_program_image_reduction_enabled() { - tracing::info!( - "ProgramImageClaimReduction prover cycle output claim={}", - c_mid - ); - } accumulator.append_dense( CommittedPolynomial::ProgramImageInit, SumcheckId::ProgramImageClaimReductionCyclePhase, // This is a phase-boundary intermediate claim, not a real program-image opening. // Keep a sentinel point so it cannot alias with the final opening claim. vec![], - c_mid, + self.core.cycle_intermediate_claim(), ); } @@ -427,7 +399,7 @@ impl SumcheckInstanceVerifier sumcheck_challenges: &[F::Challenge], ) -> F { let params = self.params.borrow(); - let claim = match params.phase { + match params.phase { PrecommittedPhase::CycleVariables => { accumulator .get_committed_polynomial_opening( @@ -450,19 +422,7 @@ impl SumcheckInstanceVerifier let scale: F = precommitted_skip_round_scale(¶ms.precommitted); pw_eval * eq_combined * scale } - }; - if debug_program_image_reduction_enabled() { - tracing::info!( - "ProgramImageClaimReduction verifier expected_output phase={} claim={}", - if params.phase == PrecommittedPhase::CycleVariables { - "cycle" - } else { - "address" - }, - claim - ); } - claim } fn cache_openings( diff --git a/jolt-sdk/tests/fixtures/fib_proof.bin b/jolt-sdk/tests/fixtures/fib_proof.bin deleted file mode 100644 index 1adb0b928e3b3e14bb946f0c30ea5486b8ae3d76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70500 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