From 1a93df7ee25be725286ceb045a8709857d78efab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 16:20:50 -0400 Subject: [PATCH 01/24] feat(blindfold): add jolt-blindfold crate (#1548) * feat: add jolt-blindfold crate * refactor(blindfold): move helpers onto owning types * style(blindfold): standardize commitment generic name --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Andrew Tretyakov <42178850+0xAndoroid@users.noreply.github.com> --- Cargo.lock | 17 + Cargo.toml | 2 + crates/jolt-blindfold/Cargo.toml | 25 + crates/jolt-blindfold/src/builder.rs | 185 ++ crates/jolt-blindfold/src/error.rs | 125 ++ crates/jolt-blindfold/src/lib.rs | 22 + crates/jolt-blindfold/src/proof.rs | 29 + crates/jolt-blindfold/src/protocol.rs | 1365 +++++++++++++ crates/jolt-blindfold/src/r1cs.rs | 756 +++++++ crates/jolt-blindfold/src/relaxed.rs | 397 ++++ crates/jolt-blindfold/src/statements.rs | 169 ++ crates/jolt-blindfold/src/verify.rs | 1150 +++++++++++ .../tests/jolt_claims_pipeline.rs | 296 +++ crates/jolt-blindfold/tests/proof_pipeline.rs | 278 +++ crates/jolt-blindfold/tests/r1cs_pipeline.rs | 23 + .../jolt-blindfold/tests/sumcheck_pipeline.rs | 112 ++ crates/jolt-blindfold/tests/support/mod.rs | 1742 +++++++++++++++++ crates/jolt-transcript/src/transcript.rs | 13 +- 18 files changed, 6705 insertions(+), 1 deletion(-) create mode 100644 crates/jolt-blindfold/Cargo.toml create mode 100644 crates/jolt-blindfold/src/builder.rs create mode 100644 crates/jolt-blindfold/src/error.rs create mode 100644 crates/jolt-blindfold/src/lib.rs create mode 100644 crates/jolt-blindfold/src/proof.rs create mode 100644 crates/jolt-blindfold/src/protocol.rs create mode 100644 crates/jolt-blindfold/src/r1cs.rs create mode 100644 crates/jolt-blindfold/src/relaxed.rs create mode 100644 crates/jolt-blindfold/src/statements.rs create mode 100644 crates/jolt-blindfold/src/verify.rs create mode 100644 crates/jolt-blindfold/tests/jolt_claims_pipeline.rs create mode 100644 crates/jolt-blindfold/tests/proof_pipeline.rs create mode 100644 crates/jolt-blindfold/tests/r1cs_pipeline.rs create mode 100644 crates/jolt-blindfold/tests/sumcheck_pipeline.rs create mode 100644 crates/jolt-blindfold/tests/support/mod.rs diff --git a/Cargo.lock b/Cargo.lock index dad95ed252..529e5cd5d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2745,6 +2745,23 @@ dependencies = [ "zeroos-build", ] +[[package]] +name = "jolt-blindfold" +version = "0.1.0" +dependencies = [ + "jolt-claims", + "jolt-crypto", + "jolt-field", + "jolt-poly", + "jolt-r1cs", + "jolt-sumcheck", + "jolt-transcript", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "jolt-claims" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d643be1a5a..1f40d519a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ keywords = ["SNARK", "cryptography", "proofs"] [workspace] members = [ "crates/jolt-claims", + "crates/jolt-blindfold", "crates/jolt-crypto", "crates/jolt-program", "crates/jolt-poly", @@ -377,6 +378,7 @@ secp256k1 = { version = "0.31", features = ["recovery", "rand"] } common = { path = "./common", default-features = false } tracer = { path = "./tracer", default-features = false } jolt-core = { path = "./jolt-core", default-features = false } +jolt-blindfold = { path = "./crates/jolt-blindfold" } jolt-claims = { path = "./crates/jolt-claims" } jolt-crypto = { path = "./crates/jolt-crypto" } jolt-field = { path = "./crates/jolt-field" } diff --git a/crates/jolt-blindfold/Cargo.toml b/crates/jolt-blindfold/Cargo.toml new file mode 100644 index 0000000000..d0d9f572de --- /dev/null +++ b/crates/jolt-blindfold/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "jolt-blindfold" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Generic BlindFold verifier infrastructure for the Jolt zkVM" + +[lints] +workspace = true + +[dependencies] +jolt-claims.workspace = true +jolt-crypto.workspace = true +jolt-field.workspace = true +jolt-poly.workspace = true +jolt-r1cs.workspace = true +jolt-sumcheck = { workspace = true, features = ["r1cs"] } +jolt-transcript.workspace = true +serde = { workspace = true, features = ["derive"] } +thiserror.workspace = true + +[dev-dependencies] +jolt-field = { workspace = true, features = ["bn254"] } +rand_chacha.workspace = true +rand_core.workspace = true diff --git a/crates/jolt-blindfold/src/builder.rs b/crates/jolt-blindfold/src/builder.rs new file mode 100644 index 0000000000..636b71f3dc --- /dev/null +++ b/crates/jolt-blindfold/src/builder.rs @@ -0,0 +1,185 @@ +use jolt_claims::Expr; +use jolt_field::Field; +use jolt_sumcheck::{ + CommittedOutputClaims, CommittedSumcheckConsistency, SumcheckDomainSpec, SumcheckStatement, +}; + +use crate::{ + BlindFoldProtocol, BlindFoldStage, BlindFoldStatement, CommittedClaimRows, Error, + FinalOpeningBinding, OpeningAlias, VerificationError, +}; + +#[derive(Clone, Debug)] +pub struct BlindFoldProtocolBuilder { + stages: Vec>, + final_openings: Vec>, + publics: Vec<(P, F)>, + challenges: Vec<(Ch, F)>, +} + +impl BlindFoldProtocolBuilder { + pub fn new() -> Self { + Self { + stages: Vec::new(), + final_openings: Vec::new(), + publics: Vec::new(), + challenges: Vec::new(), + } + } + + pub fn public(mut self, id: P, value: F) -> Self { + self.publics.push((id, value)); + self + } + + pub fn challenge(mut self, id: Ch, value: F) -> Self { + self.challenges.push((id, value)); + self + } + + pub fn stage(self, name: impl Into) -> BlindFoldStageBuilder { + BlindFoldStageBuilder::new(self, name.into()) + } + + pub fn final_opening( + mut self, + opening_ids: Vec, + coefficients: Vec, + evaluation_commitment: Com, + ) -> Self { + self.final_openings.push(FinalOpeningBinding::new( + opening_ids, + coefficients, + evaluation_commitment, + )); + self + } +} + +impl Default for BlindFoldProtocolBuilder { + fn default() -> Self { + Self::new() + } +} + +impl BlindFoldProtocolBuilder +where + F: Field + Clone, + O: Clone + PartialEq, + Com: Clone, + P: Clone + PartialEq, + Ch: Clone + PartialEq, +{ + pub fn build(self) -> Result, VerificationError> { + let statement = BlindFoldStatement::new(self.stages, self.final_openings); + BlindFoldProtocol::from_parts(&statement, &self.publics, &self.challenges) + } +} + +#[derive(Clone, Debug)] +pub struct BlindFoldStageBuilder { + parent: BlindFoldProtocolBuilder, + name: String, + statement: Option, + domain: Option, + consistency: Option>, + output_claim_rows: Option>, + input_claim: Option>, + output_claim: Option>, +} + +impl BlindFoldStageBuilder { + fn new(parent: BlindFoldProtocolBuilder, name: String) -> Self { + Self { + parent, + name, + statement: None, + domain: None, + consistency: None, + output_claim_rows: None, + input_claim: None, + output_claim: None, + } + } + + pub fn sumcheck(mut self, statement: SumcheckStatement) -> Self { + self.statement = Some(statement); + self + } + + pub fn domain(mut self, domain: SumcheckDomainSpec) -> Self { + self.domain = Some(domain); + self + } + + pub fn consistency(mut self, consistency: CommittedSumcheckConsistency) -> Self { + self.consistency = Some(consistency); + self + } + + pub fn output_claim_rows( + mut self, + opening_ids: Vec, + row_len: usize, + commitments: CommittedOutputClaims, + ) -> Self { + self.output_claim_rows = Some(CommittedClaimRows::new(opening_ids, row_len, commitments)); + self + } + + pub fn output_claim_aliases( + mut self, + aliases: impl IntoIterator>, + ) -> Self { + let rows = self + .output_claim_rows + .take() + .unwrap_or_else(CommittedClaimRows::empty) + .with_aliases(aliases); + self.output_claim_rows = Some(rows); + self + } + + pub fn input_claim(mut self, input_claim: Expr) -> Self { + self.input_claim = Some(input_claim); + self + } + + pub fn output_claim(mut self, output_claim: Expr) -> Self { + self.output_claim = Some(output_claim); + self + } + + pub fn finish_stage(mut self) -> Result, Error> { + let stage = BlindFoldStage::new( + self.name.clone(), + self.statement + .take() + .ok_or_else(|| self.missing("sumcheck"))?, + self.domain + .take() + .unwrap_or(SumcheckDomainSpec::BooleanHypercube), + self.consistency + .take() + .ok_or_else(|| self.missing("consistency"))?, + self.output_claim_rows + .take() + .unwrap_or_else(CommittedClaimRows::empty), + self.input_claim + .take() + .ok_or_else(|| self.missing("input claim"))?, + self.output_claim + .take() + .ok_or_else(|| self.missing("output claim"))?, + ); + self.parent.stages.push(stage); + Ok(self.parent) + } + + fn missing(&self, component: &'static str) -> Error { + Error::MissingStageComponent { + stage: self.name.clone(), + component, + } + } +} diff --git a/crates/jolt-blindfold/src/error.rs b/crates/jolt-blindfold/src/error.rs new file mode 100644 index 0000000000..1ff7a645df --- /dev/null +++ b/crates/jolt-blindfold/src/error.rs @@ -0,0 +1,125 @@ +use jolt_crypto::VectorOpeningError; +use jolt_field::FieldCore; +use jolt_r1cs::{ClaimLoweringError, ConstraintMatrixEvalError}; +use jolt_sumcheck::{SumcheckError, SumcheckR1csError}; +use thiserror::Error as ThisError; + +#[derive(Clone, Debug, ThisError, PartialEq, Eq)] +pub enum Error { + #[error(transparent)] + Layout(#[from] LayoutError), + #[error(transparent)] + Claim(#[from] ClaimLoweringError), + #[error("stage {stage}: missing {component}")] + MissingStageComponent { + stage: String, + component: &'static str, + }, + #[error("{name} must be non-zero when committed rows are present")] + MissingRowLength { name: &'static str }, + #[error("{name} has {ids} opening ids but only {slots} committed row slots")] + OpeningRowCapacityExceeded { + name: &'static str, + ids: usize, + slots: usize, + }, + #[error("final opening binding must reference at least one opening")] + EmptyFinalOpeningBinding, + #[error("{name} row count mismatch: expected {expected}, got {actual}")] + CommittedRowCountMismatch { + name: &'static str, + expected: usize, + actual: usize, + }, + #[error("opening id appears more than once")] + DuplicateOpeningSource, + #[error("opening alias refers to an unknown source")] + MissingOpeningAliasSource, + #[error("stage {stage_index}: {source}")] + Sumcheck { + stage_index: usize, + source: SumcheckR1csError, + }, + #[error("layout has {layout_stages} stages but statement has {statement_stages}")] + LayoutStageCountMismatch { + statement_stages: usize, + layout_stages: usize, + }, +} + +#[derive(Clone, Debug, ThisError, PartialEq, Eq)] +pub enum LayoutError { + #[error("stage {stage_index}: {source}")] + Sumcheck { + stage_index: usize, + source: SumcheckR1csError, + }, + #[error("{name} dimension {value} cannot be represented as a power-of-two size")] + DimensionOverflow { name: &'static str, value: usize }, + #[error("{name} must be non-zero when committed rows are present")] + MissingRowLength { name: &'static str }, + #[error("{name} has {ids} opening ids but only {slots} committed row slots")] + OpeningRowCapacityExceeded { + name: &'static str, + ids: usize, + slots: usize, + }, +} + +#[derive(Clone, Debug, ThisError, PartialEq, Eq)] +pub enum RelaxedError { + #[error("{name} length mismatch: expected {expected}, got {actual}")] + LengthMismatch { + name: &'static str, + expected: usize, + actual: usize, + }, + #[error("{name} dimension {value} cannot be represented as a power-of-two size")] + DimensionOverflow { name: &'static str, value: usize }, + #[error("{name} dimensions are inconsistent: total {total} is smaller than used {used}")] + InconsistentDimensions { + name: &'static str, + total: usize, + used: usize, + }, +} + +#[derive(Debug, ThisError)] +pub enum VerificationError { + #[error("claims have {claim_stages} stages but proof has {proof_stages}")] + StageCountMismatch { + claim_stages: usize, + proof_stages: usize, + }, + #[error("stage {stage_index}: {source}")] + Sumcheck { + stage_index: usize, + source: SumcheckError, + }, + #[error("outer folded R1CS sumcheck: {source}")] + OuterSumcheck { source: SumcheckError }, + #[error("inner folded R1CS sumcheck: {source}")] + InnerSumcheck { source: SumcheckError }, + #[error(transparent)] + R1cs(#[from] Error), + #[error(transparent)] + R1csMatrix(#[from] ConstraintMatrixEvalError), + #[error(transparent)] + Relaxed(#[from] RelaxedError), + #[error(transparent)] + VectorOpening(#[from] VectorOpeningError), + #[error("{name} must be a non-zero power of two, got {value}")] + InvalidPowerOfTwo { name: &'static str, value: usize }, + #[error("{name} must have at least one sumcheck round")] + DegenerateSumcheck { name: &'static str }, + #[error("folded eval commitment {index} does not match opened value and blinding")] + EvalCommitmentMismatch { index: usize }, + #[error("folded eval witness {kind} {index} does not match opened witness coordinate")] + EvalWitnessMismatch { kind: &'static str, index: usize }, + #[error("folded eval witness {kind} {index} opening has a non-zero value outside the dedicated slot")] + EvalWitnessRowNotDedicated { kind: &'static str, index: usize }, + #[error("outer final claim mismatch: expected {expected}, got {actual}")] + OuterFinalClaimMismatch { expected: F, actual: F }, + #[error("inner final claim mismatch: expected {expected}, got {actual}")] + InnerFinalClaimMismatch { expected: F, actual: F }, +} diff --git a/crates/jolt-blindfold/src/lib.rs b/crates/jolt-blindfold/src/lib.rs new file mode 100644 index 0000000000..0f728887c6 --- /dev/null +++ b/crates/jolt-blindfold/src/lib.rs @@ -0,0 +1,22 @@ +//! Generic BlindFold claim, protocol, layout, and verifier-equation types. + +mod builder; +mod error; +mod proof; +pub mod protocol; +pub mod r1cs; +mod relaxed; +mod statements; +mod verify; + +pub use builder::{BlindFoldProtocolBuilder, BlindFoldStageBuilder}; +pub use error::{Error, LayoutError, RelaxedError, VerificationError}; +pub use proof::BlindFoldProof; +pub use protocol::{ + BlindFoldDimensions, BlindFoldProtocol, FinalOpeningWitnessCoordinates, RowDimensions, + WitnessCoordinate, WitnessRowLayout, +}; +pub use relaxed::{RelaxedInstance, RelaxedWitness}; +pub use statements::{ + BlindFoldStage, BlindFoldStatement, CommittedClaimRows, FinalOpeningBinding, OpeningAlias, +}; diff --git a/crates/jolt-blindfold/src/proof.rs b/crates/jolt-blindfold/src/proof.rs new file mode 100644 index 0000000000..301292811c --- /dev/null +++ b/crates/jolt-blindfold/src/proof.rs @@ -0,0 +1,29 @@ +use jolt_crypto::VectorCommitmentOpening; +use jolt_field::Field; +use jolt_sumcheck::CompressedSumcheckProof; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound(serialize = "Com: Serialize", deserialize = "Com: DeserializeOwned"))] +pub struct BlindFoldProof { + pub auxiliary_row_commitments: Vec, + pub random_round_commitments: Vec, + pub random_output_claim_row_commitments: Vec, + pub random_auxiliary_row_commitments: Vec, + pub random_error_row_commitments: Vec, + pub random_eval_commitments: Vec, + pub random_u: F, + pub cross_term_error_row_commitments: Vec, + pub outer_sumcheck: CompressedSumcheckProof, + pub az_rx: F, + pub bz_rx: F, + pub cz_rx: F, + pub inner_sumcheck: CompressedSumcheckProof, + pub witness_opening: VectorCommitmentOpening, + pub error_opening: VectorCommitmentOpening, + pub folded_eval_outputs: Vec, + pub folded_eval_blindings: Vec, + pub folded_eval_output_openings: Vec>, + pub folded_eval_blinding_openings: Vec>, +} diff --git a/crates/jolt-blindfold/src/protocol.rs b/crates/jolt-blindfold/src/protocol.rs new file mode 100644 index 0000000000..6f206b2d07 --- /dev/null +++ b/crates/jolt-blindfold/src/protocol.rs @@ -0,0 +1,1365 @@ +use std::ops::Range; + +use jolt_crypto::HomomorphicCommitment; +use jolt_field::Field; +use jolt_r1cs::{ConstraintMatrices, R1csBuilder, Variable}; +use jolt_sumcheck::{CommittedOutputClaims, CommittedSumcheckConsistency}; + +use crate::{ + r1cs::Layout, BlindFoldProtocolBuilder, BlindFoldStatement, Error, RelaxedError, + RelaxedInstance, VerificationError, +}; + +#[derive(Clone, Debug)] +pub struct BlindFoldProtocol { + pub sumcheck_consistency: Vec>, + pub committed_output_claims: Vec>, + pub r1cs: ConstraintMatrices, + pub layout: Layout, + pub dimensions: BlindFoldDimensions, + pub eval_commitments: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RowDimensions { + pub row_len: usize, + pub row_count: usize, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BlindFoldDimensions { + pub witness: RowDimensions, + pub error: RowDimensions, + pub witness_rows: WitnessRowLayout, + pub coefficient_rows: usize, + pub output_claim_rows: usize, + pub auxiliary_rows: usize, + pub coefficient_values: usize, + pub auxiliary_values: usize, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WitnessRowLayout { + pub coefficients: Range, + pub output_claims: Range, + pub auxiliary: Range, + pub padding: Range, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WitnessCoordinate { + pub row: usize, + pub column: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FinalOpeningWitnessCoordinates { + pub evaluation: Option, + pub blinding: Option, +} + +impl BlindFoldProtocol +where + F: Field, +{ + pub fn builder() -> BlindFoldProtocolBuilder { + BlindFoldProtocolBuilder::new() + } +} + +impl BlindFoldProtocol +where + F: Field + Clone, + Com: Clone, +{ + pub(crate) fn from_parts( + statement: &BlindFoldStatement, + publics: &[(P, F)], + challenges: &[(Ch, F)], + ) -> Result> + where + O: Clone + PartialEq, + P: Clone + PartialEq, + Ch: Clone + PartialEq, + { + statement.validate()?; + + let sumcheck_consistency = statement.sumcheck_consistency(); + let committed_output_claims = statement.committed_output_claims(); + let (r1cs, layout) = statement.build_constraints(publics, challenges)?; + let dimensions = + layout.dimensions(&r1cs, &sumcheck_consistency, &committed_output_claims)?; + + Ok(Self { + sumcheck_consistency, + committed_output_claims, + r1cs, + layout, + dimensions, + eval_commitments: statement.final_opening_commitments(), + }) + } +} + +impl BlindFoldProtocol +where + F: Field, + Com: Clone + HomomorphicCommitment, +{ + pub fn committed_relaxed_instance( + &self, + auxiliary_row_commitments: &[Com], + ) -> Result, RelaxedError> { + if auxiliary_row_commitments.len() != self.dimensions.auxiliary_rows { + return Err(RelaxedError::LengthMismatch { + name: "auxiliary row commitments", + expected: self.dimensions.auxiliary_rows, + actual: auxiliary_row_commitments.len(), + }); + } + + let mut witness_row_commitments = Vec::with_capacity(self.dimensions.witness.row_count); + + witness_row_commitments.extend( + self.sumcheck_consistency + .iter() + .flat_map(|consistency| consistency.rounds.iter()) + .map(|round| round.commitment.clone()), + ); + pad_rows::( + &mut witness_row_commitments, + self.dimensions.witness_rows.coefficients.end, + "coefficient row commitments", + )?; + + witness_row_commitments.extend( + self.committed_output_claims + .iter() + .flat_map(|claims| claims.commitments.iter().cloned()), + ); + pad_rows::( + &mut witness_row_commitments, + self.dimensions.witness_rows.output_claims.end, + "output claim row commitments", + )?; + + witness_row_commitments.extend_from_slice(auxiliary_row_commitments); + pad_rows::( + &mut witness_row_commitments, + self.dimensions.witness_rows.auxiliary.end, + "auxiliary row commitments", + )?; + pad_rows::( + &mut witness_row_commitments, + self.dimensions.witness.row_count, + "witness row commitments", + )?; + + Ok(RelaxedInstance::new( + F::one(), + witness_row_commitments, + vec![Com::default(); self.dimensions.error.row_count], + self.eval_commitments.clone(), + )) + } +} + +impl BlindFoldProtocol +where + F: Field, +{ + pub fn validate_cross_term_error_rows( + &self, + cross_term_error_row_commitments: &[Com], + ) -> Result<(), RelaxedError> { + ensure_len( + "cross-term error row commitments", + self.dimensions.error.row_count, + cross_term_error_row_commitments.len(), + ) + } + + pub fn final_opening_witness_coordinates( + &self, + ) -> Result, RelaxedError> { + self.layout + .final_openings + .iter() + .map(|layout| { + Ok(FinalOpeningWitnessCoordinates { + evaluation: layout + .evaluation + .map(|variable| self.witness_coordinate(variable)) + .transpose()?, + blinding: layout + .blinding + .map(|variable| self.witness_coordinate(variable)) + .transpose()?, + }) + }) + .collect() + } + + fn witness_coordinate(&self, variable: Variable) -> Result { + let witness_index = + variable + .index() + .checked_sub(1) + .ok_or(RelaxedError::InconsistentDimensions { + name: "witness variable", + total: self.dimensions.witness.row_count * self.dimensions.witness.row_len, + used: 0, + })?; + let witness_values = self + .dimensions + .witness + .row_count + .checked_mul(self.dimensions.witness.row_len) + .ok_or(RelaxedError::DimensionOverflow { + name: "witness values", + value: self.dimensions.witness.row_count, + })?; + if witness_index >= witness_values { + return Err(RelaxedError::InconsistentDimensions { + name: "witness variable", + total: witness_values, + used: witness_index + 1, + }); + } + + Ok(WitnessCoordinate { + row: witness_index / self.dimensions.witness.row_len, + column: witness_index % self.dimensions.witness.row_len, + }) + } +} + +impl BlindFoldProtocol +where + F: Field, + Com: Clone + HomomorphicCommitment, +{ + pub fn random_relaxed_instance( + &self, + round_commitments: &[Com], + output_claim_row_commitments: &[Com], + auxiliary_row_commitments: &[Com], + error_row_commitments: &[Com], + eval_commitments: &[Com], + u: F, + ) -> Result, RelaxedError> { + ensure_len( + "random round commitments", + self.dimensions.coefficient_rows, + round_commitments.len(), + )?; + ensure_len( + "random output claim row commitments", + self.dimensions.output_claim_rows, + output_claim_row_commitments.len(), + )?; + ensure_len( + "random auxiliary row commitments", + self.dimensions.auxiliary_rows, + auxiliary_row_commitments.len(), + )?; + ensure_len( + "random error row commitments", + self.dimensions.error.row_count, + error_row_commitments.len(), + )?; + ensure_len( + "random eval commitments", + self.eval_commitments.len(), + eval_commitments.len(), + )?; + + let mut witness_row_commitments = Vec::with_capacity(self.dimensions.witness.row_count); + witness_row_commitments.extend_from_slice(round_commitments); + pad_rows::( + &mut witness_row_commitments, + self.dimensions.witness_rows.coefficients.end, + "random coefficient row commitments", + )?; + witness_row_commitments.extend_from_slice(output_claim_row_commitments); + pad_rows::( + &mut witness_row_commitments, + self.dimensions.witness_rows.output_claims.end, + "random output claim row commitments", + )?; + witness_row_commitments.extend_from_slice(auxiliary_row_commitments); + pad_rows::( + &mut witness_row_commitments, + self.dimensions.witness_rows.auxiliary.end, + "random auxiliary row commitments", + )?; + pad_rows::( + &mut witness_row_commitments, + self.dimensions.witness.row_count, + "random witness row commitments", + )?; + + Ok(RelaxedInstance::new( + u, + witness_row_commitments, + error_row_commitments.to_vec(), + eval_commitments.to_vec(), + )) + } +} + +impl BlindFoldStatement +where + F: Field + Clone, + O: Clone + PartialEq, + Com: Clone, + P: Clone + PartialEq, + Ch: Clone + PartialEq, +{ + fn validate(&self) -> Result<(), VerificationError> { + self.validate_unique_openings()?; + self.validate_output_claim_rows()?; + for binding in &self.final_openings { + if binding.opening_ids.is_empty() { + return Err(Error::EmptyFinalOpeningBinding.into()); + } + if binding.opening_ids.len() != binding.coefficients.len() { + return Err(RelaxedError::LengthMismatch { + name: "final opening coefficients", + expected: binding.opening_ids.len(), + actual: binding.coefficients.len(), + } + .into()); + } + } + Ok(()) + } + + fn validate_unique_openings(&self) -> Result<(), Error> { + let mut openings = Vec::new(); + for stage in &self.stages { + for opening_id in &stage.output_claim_rows.opening_ids { + if openings.contains(opening_id) { + return Err(Error::DuplicateOpeningSource); + } + openings.push(opening_id.clone()); + } + for alias in &stage.output_claim_rows.opening_aliases { + if openings.contains(&alias.alias) { + return Err(Error::DuplicateOpeningSource); + } + if !openings.contains(&alias.source) { + return Err(Error::MissingOpeningAliasSource); + } + openings.push(alias.alias.clone()); + } + } + Ok(()) + } + + fn validate_output_claim_rows(&self) -> Result<(), VerificationError> { + for stage in &self.stages { + let rows = &stage.output_claim_rows; + let row_count = rows.commitments.commitments.len(); + if row_count == 0 { + if !rows.opening_ids.is_empty() { + return Err(Error::OpeningRowCapacityExceeded { + name: "output claim rows", + ids: rows.opening_ids.len(), + slots: 0, + } + .into()); + } + continue; + } + if rows.opening_ids.is_empty() { + if row_count != 0 { + return Err(Error::CommittedRowCountMismatch { + name: "output claim rows", + expected: 0, + actual: row_count, + } + .into()); + } + continue; + } + if rows.row_len == 0 { + return Err(Error::MissingRowLength { + name: "output claim rows", + } + .into()); + } + let slots = + row_count + .checked_mul(rows.row_len) + .ok_or(RelaxedError::DimensionOverflow { + name: "output claim row slots", + value: row_count, + })?; + if rows.opening_ids.len() > slots { + return Err(Error::OpeningRowCapacityExceeded { + name: "output claim rows", + ids: rows.opening_ids.len(), + slots, + } + .into()); + } + let expected_rows = rows.opening_ids.len().div_ceil(rows.row_len); + if row_count != expected_rows { + return Err(Error::CommittedRowCountMismatch { + name: "output claim rows", + expected: expected_rows, + actual: row_count, + } + .into()); + } + } + Ok(()) + } + + fn sumcheck_consistency(&self) -> Vec> { + self.stages + .iter() + .map(|stage| stage.consistency.clone()) + .collect() + } + + fn committed_output_claims(&self) -> Vec> { + self.stages + .iter() + .map(|stage| stage.output_claim_rows.commitments.clone()) + .collect() + } + + fn build_constraints( + &self, + publics: &[(P, F)], + challenges: &[(Ch, F)], + ) -> Result<(ConstraintMatrices, Layout), VerificationError> { + let mut r1cs = R1csBuilder::new(); + let layout = self.build_with_sources(&mut r1cs, publics, challenges)?; + Ok((r1cs.into_matrices(), layout)) + } +} + +impl Layout { + fn dimensions( + &self, + r1cs: &ConstraintMatrices, + sumcheck_consistency: &[CommittedSumcheckConsistency], + output_claims: &[CommittedOutputClaims], + ) -> Result { + let total_rounds = checked_sum( + "total rounds", + sumcheck_consistency + .iter() + .map(|consistency| consistency.rounds.len()), + )?; + let witness_row_len = self.witness_row_len; + let coefficient_values = + total_rounds + .checked_mul(witness_row_len) + .ok_or(RelaxedError::DimensionOverflow { + name: "coefficient values", + value: total_rounds, + })?; + let coefficient_rows = total_rounds; + + let output_claim_rows = checked_sum( + "output claim rows", + output_claims.iter().map(|claims| claims.commitments.len()), + )?; + let output_claim_values = output_claim_rows.checked_mul(witness_row_len).ok_or( + RelaxedError::DimensionOverflow { + name: "output claim values", + value: output_claim_rows, + }, + )?; + + let r1cs_witness_values = r1cs.num_vars.saturating_sub(1); + let used_values = checked_sum( + "committed witness values", + [coefficient_values, output_claim_values], + )?; + let auxiliary_values = r1cs_witness_values.checked_sub(used_values).ok_or( + RelaxedError::InconsistentDimensions { + name: "auxiliary values", + total: r1cs_witness_values, + used: used_values, + }, + )?; + let auxiliary_rows = auxiliary_values.div_ceil(witness_row_len); + let occupied_witness_rows = checked_sum( + "occupied witness rows", + [coefficient_rows, output_claim_rows, auxiliary_rows], + )?; + let witness_row_count = + checked_next_power_of_two("witness rows", occupied_witness_rows.max(1))?; + let witness_rows = WitnessRowLayout::from_counts( + coefficient_rows, + output_claim_rows, + auxiliary_rows, + witness_row_count, + )?; + + let padded_constraints = + checked_next_power_of_two("error values", r1cs.num_constraints.max(1))?; + let error_row_len = witness_row_len.min(padded_constraints); + let error_row_count = padded_constraints / error_row_len; + + Ok(BlindFoldDimensions { + witness: RowDimensions { + row_len: witness_row_len, + row_count: witness_row_count, + }, + error: RowDimensions { + row_len: error_row_len, + row_count: error_row_count, + }, + witness_rows, + coefficient_rows, + output_claim_rows, + auxiliary_rows, + coefficient_values, + auxiliary_values, + }) + } +} + +impl WitnessRowLayout { + fn from_counts( + coefficient_rows: usize, + output_claim_rows: usize, + auxiliary_rows: usize, + witness_row_count: usize, + ) -> Result { + let output_claim_start = coefficient_rows; + let auxiliary_start = + checked_sum("witness row layout", [coefficient_rows, output_claim_rows])?; + let padding_start = checked_sum( + "witness row layout", + [coefficient_rows, auxiliary_rows, output_claim_rows], + )?; + if padding_start > witness_row_count { + return Err(RelaxedError::InconsistentDimensions { + name: "witness row layout", + total: witness_row_count, + used: padding_start, + }); + } + + Ok(Self { + coefficients: 0..coefficient_rows, + output_claims: output_claim_start..auxiliary_start, + auxiliary: auxiliary_start..padding_start, + padding: padding_start..witness_row_count, + }) + } +} + +fn pad_rows( + rows: &mut Vec, + target_len: usize, + name: &'static str, +) -> Result<(), RelaxedError> +where + F: Field, + Com: Clone + HomomorphicCommitment, +{ + if rows.len() > target_len { + return Err(RelaxedError::InconsistentDimensions { + name, + total: target_len, + used: rows.len(), + }); + } + rows.resize_with(target_len, Com::default); + Ok(()) +} + +fn ensure_len(name: &'static str, expected: usize, actual: usize) -> Result<(), RelaxedError> { + if expected != actual { + return Err(RelaxedError::LengthMismatch { + name, + expected, + actual, + }); + } + Ok(()) +} + +fn checked_next_power_of_two(name: &'static str, value: usize) -> Result { + value + .checked_next_power_of_two() + .ok_or(RelaxedError::DimensionOverflow { name, value }) +} + +fn checked_sum( + name: &'static str, + values: impl IntoIterator, +) -> Result { + values.into_iter().try_fold(0usize, |sum, value| { + sum.checked_add(value) + .ok_or(RelaxedError::DimensionOverflow { name, value: sum }) + }) +} + +#[cfg(test)] +#[expect(clippy::expect_used, reason = "tests may panic on assertion failures")] +mod tests { + use super::*; + use crate::{ + BlindFoldStage, BlindFoldStatement, CommittedClaimRows, FinalOpeningBinding, OpeningAlias, + }; + use jolt_claims::{constant, opening, Expr}; + use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment}; + use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_sumcheck::{ + CommittedOutputClaims, CommittedRound, CommittedSumcheckProof, SumcheckDomainSpec, + SumcheckError, SumcheckStatement, + }; + use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; + + #[derive(Clone, Debug)] + struct TestStage { + name: String, + statement: SumcheckStatement, + input_claim: Expr, + output_claim: Expr, + } + + fn stage(num_vars: usize, degree: usize) -> TestStage { + let claim: Expr = constant(Fr::from_u64(0)); + TestStage { + name: "stage".to_string(), + statement: SumcheckStatement::new(num_vars, degree), + input_claim: claim.clone(), + output_claim: claim, + } + } + + fn proof(rounds: &[(u64, usize)], output_claims: &[u64]) -> CommittedSumcheckProof { + CommittedSumcheckProof { + rounds: rounds + .iter() + .map(|&(commitment, degree)| CommittedRound { + commitment: Fr::from_u64(commitment), + degree, + }) + .collect(), + output_claims: CommittedOutputClaims { + commitments: output_claims + .iter() + .map(|&commitment| Fr::from_u64(commitment)) + .collect(), + }, + } + } + + fn commitment_proof( + setup: &PedersenSetup, + rounds: &[(u64, usize)], + output_claims: &[u64], + ) -> CommittedSumcheckProof { + CommittedSumcheckProof { + rounds: rounds + .iter() + .map(|&(commitment, degree)| CommittedRound { + commitment: pedersen_commitment(setup, commitment), + degree, + }) + .collect(), + output_claims: CommittedOutputClaims { + commitments: output_claims + .iter() + .map(|&commitment| pedersen_commitment(setup, commitment)) + .collect(), + }, + } + } + + fn pedersen_setup() -> PedersenSetup { + let generator = Bn254::g1_generator(); + let message_generators = (1..=4) + .map(|i| generator.scalar_mul(&Fr::from_u64(i))) + .collect(); + PedersenSetup::new(message_generators, generator.scalar_mul(&Fr::from_u64(99))) + } + + fn pedersen_commitment(setup: &PedersenSetup, value: u64) -> Bn254G1 { + Pedersen::::commit(setup, &[Fr::from_u64(value)], &Fr::from_u64(value + 1000)) + } + + fn try_statement_from_proofs( + stages: &[TestStage], + proofs: &[CommittedSumcheckProof], + final_openings: Vec>, + ) -> Result, VerificationError> + where + Com: Clone + AppendToTranscript, + { + if stages.len() != proofs.len() { + return Err(VerificationError::StageCountMismatch { + claim_stages: stages.len(), + proof_stages: proofs.len(), + }); + } + + let mut transcript = Blake2bTranscript::::new(b"blindfold"); + let mut next_opening_id = 0usize; + let stages = stages + .iter() + .zip(proofs) + .enumerate() + .map(|(stage_index, (stage, proof))| { + let consistency = proof + .verify_committed_consistency(stage.statement, &mut transcript) + .map_err(|source| VerificationError::Sumcheck { + stage_index, + source, + })?; + let row_len = stage.statement.degree + 1; + let output_opening_count = proof.output_claims.commitments.len() * row_len; + let opening_ids = + (next_opening_id..next_opening_id + output_opening_count).collect(); + next_opening_id += output_opening_count; + Ok::, VerificationError>(BlindFoldStage::new( + stage.name.clone(), + stage.statement, + SumcheckDomainSpec::BooleanHypercube, + consistency, + CommittedClaimRows::new(opening_ids, row_len, proof.output_claims.clone()), + stage.input_claim.clone(), + stage.output_claim.clone(), + )) + }) + .collect::, _>>()?; + + Ok(BlindFoldStatement::new(stages, final_openings)) + } + + fn protocol_from_proofs( + stages: &[TestStage], + proofs: &[CommittedSumcheckProof], + final_openings: Vec>, + ) -> BlindFoldProtocol + where + Com: Clone + AppendToTranscript, + { + let statement = try_statement_from_proofs(stages, proofs, final_openings) + .expect("statement builds from committed proofs"); + protocol_from_statement(&statement) + } + + fn protocol_from_statement( + statement: &BlindFoldStatement, + ) -> BlindFoldProtocol + where + Com: Clone, + { + let mut builder = BlindFoldProtocol::::builder::(); + for stage in &statement.stages { + builder = builder + .stage(stage.name.clone()) + .sumcheck(stage.statement) + .domain(stage.domain) + .consistency(stage.consistency.clone()) + .output_claim_rows( + stage.output_claim_rows.opening_ids.clone(), + stage.output_claim_rows.row_len, + stage.output_claim_rows.commitments.clone(), + ) + .input_claim(stage.input_claim.clone()) + .output_claim(stage.output_claim.clone()) + .finish_stage() + .expect("test stage statement is complete"); + } + for binding in &statement.final_openings { + builder = builder.final_opening( + binding.opening_ids.clone(), + binding.coefficients.clone(), + binding.evaluation_commitment.clone(), + ); + } + builder.build().expect("BlindFold protocol builds") + } + + #[test] + fn verifies_committed_stages_in_claim_order() { + let stages = vec![stage(2, 2), stage(1, 1)]; + let proofs = vec![ + proof(&[(11, 1), (12, 2)], &[21]), + proof(&[(13, 1)], &[34, 55]), + ]; + + let verified = + try_statement_from_proofs(&stages, &proofs, Vec::new()).expect("proofs verify"); + + assert_eq!(verified.stage_count(), 2); + assert_eq!(verified.stages[0].consistency.rounds.len(), 2); + assert_eq!(verified.stages[1].consistency.rounds.len(), 1); + let round_commitments = verified + .stages + .iter() + .flat_map(|stage| { + stage + .consistency + .rounds + .iter() + .map(|round| round.commitment) + }) + .collect::>(); + let output_claim_commitments = verified + .stages + .iter() + .flat_map(|stage| { + stage + .output_claim_rows + .commitments + .commitments + .iter() + .copied() + }) + .collect::>(); + assert_eq!( + round_commitments, + vec![Fr::from_u64(11), Fr::from_u64(12), Fr::from_u64(13)] + ); + assert_eq!( + output_claim_commitments, + vec![Fr::from_u64(21), Fr::from_u64(34), Fr::from_u64(55)] + ); + } + + #[test] + fn rejects_stage_count_mismatch() { + let stages = vec![stage(1, 1), stage(1, 1)]; + let proofs = vec![proof(&[(11, 1)], &[])]; + let error = + try_statement_from_proofs(&stages, &proofs, Vec::new()).expect_err("counts differ"); + + assert!(matches!( + error, + VerificationError::StageCountMismatch { + claim_stages: 2, + proof_stages: 1, + } + )); + } + + #[test] + fn reports_sumcheck_error_with_stage_index() { + let stages = vec![stage(1, 1), stage(1, 1)]; + let proofs = vec![proof(&[(11, 1)], &[]), proof(&[(12, 2)], &[])]; + let error = + try_statement_from_proofs(&stages, &proofs, Vec::new()).expect_err("degree fails"); + + assert!(matches!( + error, + VerificationError::Sumcheck { + stage_index: 1, + source: SumcheckError::DegreeBoundExceeded { got: 2, max: 1 }, + } + )); + } + + #[test] + fn blindfold_protocol_builder_constructs_protocol() { + let stages = vec![stage(1, 1)]; + let proofs = vec![proof(&[(11, 1)], &[21])]; + let protocol = protocol_from_proofs(&stages, &proofs, Vec::new()); + + assert_eq!(protocol.sumcheck_consistency.len(), 1); + assert_eq!(protocol.committed_output_claims.len(), 1); + assert_eq!(protocol.layout.stage_count(), 1); + assert!(protocol.eval_commitments.is_empty()); + assert!(protocol.r1cs.num_vars > 1); + assert_eq!( + protocol.dimensions, + BlindFoldDimensions { + witness: RowDimensions { + row_len: 2, + row_count: 4, + }, + error: RowDimensions { + row_len: 2, + row_count: 2, + }, + witness_rows: WitnessRowLayout { + coefficients: 0..1, + output_claims: 1..2, + auxiliary: 2..3, + padding: 3..4, + }, + coefficient_rows: 1, + output_claim_rows: 1, + auxiliary_rows: 1, + coefficient_values: 2, + auxiliary_values: 2, + } + ); + } + + #[test] + fn blindfold_protocol_builder_resolves_output_claim_aliases() { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum Opening { + Source, + Alias, + } + + let statement = SumcheckStatement::new(1, 1); + let proof = proof(&[(11, 1)], &[21]); + let mut transcript = Blake2bTranscript::::new(b"blindfold-alias"); + let consistency = proof + .verify_committed_consistency(statement, &mut transcript) + .expect("committed proof is consistent"); + let protocol = BlindFoldProtocol::::builder::() + .stage("alias") + .sumcheck(statement) + .domain(SumcheckDomainSpec::BooleanHypercube) + .consistency(consistency) + .output_claim_rows(vec![Opening::Source], 1, proof.output_claims.clone()) + .output_claim_aliases([OpeningAlias::new(Opening::Alias, Opening::Source)]) + .input_claim(constant(Fr::from_u64(0))) + .output_claim(opening(Opening::Alias)) + .finish_stage() + .expect("stage is complete") + .build() + .expect("alias resolves to a committed output claim"); + + assert_eq!(protocol.dimensions.output_claim_rows, 1); + } + + #[test] + fn blindfold_protocol_builder_rejects_missing_output_claim_alias_source() { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum Opening { + Source, + Alias, + Other, + } + + let statement = SumcheckStatement::new(1, 1); + let proof = proof(&[(11, 1)], &[21]); + let mut transcript = Blake2bTranscript::::new(b"blindfold-alias"); + let consistency = proof + .verify_committed_consistency(statement, &mut transcript) + .expect("committed proof is consistent"); + let error = BlindFoldProtocol::::builder::() + .stage("alias") + .sumcheck(statement) + .domain(SumcheckDomainSpec::BooleanHypercube) + .consistency(consistency) + .output_claim_rows(vec![Opening::Other], 1, proof.output_claims.clone()) + .output_claim_aliases([OpeningAlias::new(Opening::Alias, Opening::Source)]) + .input_claim(constant(Fr::from_u64(0))) + .output_claim(opening(Opening::Alias)) + .finish_stage() + .expect("stage is complete") + .build() + .expect_err("alias source is absent"); + + assert!(matches!( + error, + VerificationError::R1cs(Error::MissingOpeningAliasSource) + )); + } + + #[test] + fn rejects_malformed_final_opening_binding() { + let stages = vec![stage(1, 1)]; + let proofs = vec![proof(&[(11, 1)], &[21])]; + let statement = try_statement_from_proofs( + &stages, + &proofs, + vec![FinalOpeningBinding::new( + vec![0], + Vec::new(), + Fr::from_u64(99), + )], + ) + .expect("statement construction verifies committed proof"); + + let error = BlindFoldProtocol::::builder::() + .stage("stage") + .sumcheck(stages[0].statement) + .domain(SumcheckDomainSpec::BooleanHypercube) + .consistency(statement.stages[0].consistency.clone()) + .output_claim_rows( + vec![0, 1], + stages[0].statement.degree + 1, + proofs[0].output_claims.clone(), + ) + .input_claim(stages[0].input_claim.clone()) + .output_claim(stages[0].output_claim.clone()) + .finish_stage() + .expect("stage is complete") + .final_opening(vec![0], Vec::new(), Fr::from_u64(99)) + .build() + .expect_err("final opening binding is malformed"); + + assert!(matches!( + error, + VerificationError::Relaxed(RelaxedError::LengthMismatch { + name: "final opening coefficients", + expected: 1, + actual: 0, + }) + )); + } + + #[test] + fn rejects_empty_final_opening_binding() { + let stages = vec![stage(1, 1)]; + let proofs = vec![proof(&[(11, 1)], &[21])]; + let error = BlindFoldProtocol::::builder::() + .stage("stage") + .sumcheck(stages[0].statement) + .domain(SumcheckDomainSpec::BooleanHypercube) + .consistency( + try_statement_from_proofs(&stages, &proofs, Vec::new()) + .expect("statement construction verifies committed proof") + .stages[0] + .consistency + .clone(), + ) + .output_claim_rows( + vec![0, 1], + stages[0].statement.degree + 1, + proofs[0].output_claims.clone(), + ) + .input_claim(stages[0].input_claim.clone()) + .output_claim(stages[0].output_claim.clone()) + .finish_stage() + .expect("stage is complete") + .final_opening(Vec::new(), Vec::new(), Fr::from_u64(99)) + .build() + .expect_err("empty final opening binding is malformed"); + + assert!(matches!( + error, + VerificationError::R1cs(Error::EmptyFinalOpeningBinding) + )); + } + + #[test] + fn rejects_extra_output_claim_rows_without_typed_openings() { + let statement = SumcheckStatement::new(1, 1); + let proof = proof(&[(11, 1)], &[21, 22]); + let mut transcript = Blake2bTranscript::::new(b"blindfold-output-row-count"); + let consistency = proof + .verify_committed_consistency(statement, &mut transcript) + .expect("committed proof is consistent"); + let error = BlindFoldProtocol::::builder::() + .stage("row-count") + .sumcheck(statement) + .domain(SumcheckDomainSpec::BooleanHypercube) + .consistency(consistency) + .output_claim_rows(vec![0, 1], 2, proof.output_claims.clone()) + .input_claim(constant(Fr::from_u64(0))) + .output_claim(constant(Fr::from_u64(0))) + .finish_stage() + .expect("stage is complete") + .build() + .expect_err("anonymous extra output row is rejected"); + + assert!(matches!( + error, + VerificationError::R1cs(Error::CommittedRowCountMismatch { + name: "output claim rows", + expected: 1, + actual: 2, + }) + )); + } + + #[test] + fn dimensions_account_for_multiple_stages_and_padding() { + let stages = vec![stage(2, 2), stage(1, 1)]; + let proofs = vec![ + proof(&[(11, 1), (12, 2)], &[21, 22, 23]), + proof(&[(13, 1)], &[34, 55]), + ]; + let protocol = protocol_from_proofs(&stages, &proofs, Vec::new()); + + assert_eq!( + protocol.dimensions, + BlindFoldDimensions { + witness: RowDimensions { + row_len: 4, + row_count: 16, + }, + error: RowDimensions { + row_len: 4, + row_count: 4, + }, + witness_rows: WitnessRowLayout { + coefficients: 0..3, + output_claims: 3..8, + auxiliary: 8..10, + padding: 10..16, + }, + coefficient_rows: 3, + output_claim_rows: 5, + auxiliary_rows: 2, + coefficient_values: 12, + auxiliary_values: 5, + } + ); + } + + #[test] + fn committed_relaxed_instance_assembles_witness_rows_in_layout_order() { + let setup = pedersen_setup(); + let stages = vec![stage(2, 2), stage(1, 1)]; + let proofs = vec![ + commitment_proof(&setup, &[(11, 1), (12, 2)], &[21, 22]), + commitment_proof(&setup, &[(13, 1)], &[34]), + ]; + let protocol = protocol_from_proofs(&stages, &proofs, Vec::new()); + + let auxiliary_rows = vec![ + pedersen_commitment(&setup, 41), + pedersen_commitment(&setup, 42), + ]; + let relaxed = protocol + .committed_relaxed_instance(&auxiliary_rows) + .expect("relaxed instance builds"); + + assert_eq!(relaxed.u, Fr::from_u64(1)); + assert_eq!( + protocol.dimensions.witness_rows, + WitnessRowLayout { + coefficients: 0..3, + output_claims: 3..6, + auxiliary: 6..8, + padding: 8..8, + } + ); + let identity = ::identity(); + assert_eq!( + relaxed.witness_row_commitments, + vec![ + pedersen_commitment(&setup, 11), + pedersen_commitment(&setup, 12), + pedersen_commitment(&setup, 13), + pedersen_commitment(&setup, 21), + pedersen_commitment(&setup, 22), + pedersen_commitment(&setup, 34), + pedersen_commitment(&setup, 41), + pedersen_commitment(&setup, 42), + ] + ); + assert_eq!( + relaxed.error_row_commitments, + vec![identity; protocol.dimensions.error.row_count] + ); + assert!(relaxed.eval_commitments.is_empty()); + } + + #[test] + fn committed_relaxed_instance_rejects_auxiliary_row_count_mismatch() { + let setup = pedersen_setup(); + let stages = vec![stage(1, 1)]; + let proofs = vec![commitment_proof(&setup, &[(11, 1)], &[21])]; + let protocol = protocol_from_proofs(&stages, &proofs, Vec::new()); + + let error = protocol + .committed_relaxed_instance(&[]) + .expect_err("auxiliary row is missing"); + + assert_eq!( + error, + RelaxedError::LengthMismatch { + name: "auxiliary row commitments", + expected: 1, + actual: 0, + } + ); + } + + #[test] + fn random_relaxed_instance_accepts_exact_dimensions() { + let setup = pedersen_setup(); + let stages = vec![stage(1, 1)]; + let proofs = vec![commitment_proof(&setup, &[(11, 1)], &[21])]; + let protocol = protocol_from_proofs(&stages, &proofs, Vec::new()); + + let round_rows = vec![pedersen_commitment(&setup, 7); protocol.dimensions.coefficient_rows]; + let output_claim_rows = + vec![pedersen_commitment(&setup, 71); protocol.dimensions.output_claim_rows]; + let auxiliary_rows = + vec![pedersen_commitment(&setup, 72); protocol.dimensions.auxiliary_rows]; + let error_rows = vec![pedersen_commitment(&setup, 8); protocol.dimensions.error.row_count]; + let random_eval_commitments = Vec::new(); + let random = protocol + .random_relaxed_instance( + &round_rows, + &output_claim_rows, + &auxiliary_rows, + &error_rows, + &random_eval_commitments, + Fr::from_u64(3), + ) + .expect("random instance dimensions match"); + + assert_eq!(random.u, Fr::from_u64(3)); + let identity = ::identity(); + let mut expected_witness_rows = Vec::new(); + expected_witness_rows.extend_from_slice(&round_rows); + expected_witness_rows.resize(protocol.dimensions.witness_rows.coefficients.end, identity); + expected_witness_rows.extend_from_slice(&output_claim_rows); + expected_witness_rows.resize(protocol.dimensions.witness_rows.output_claims.end, identity); + expected_witness_rows.extend_from_slice(&auxiliary_rows); + expected_witness_rows.resize(protocol.dimensions.witness_rows.auxiliary.end, identity); + expected_witness_rows.resize(protocol.dimensions.witness.row_count, identity); + assert_eq!(random.witness_row_commitments, expected_witness_rows); + assert_eq!(random.error_row_commitments, error_rows); + assert_eq!(random.eval_commitments, random_eval_commitments); + } + + #[test] + fn random_relaxed_instance_rejects_round_row_count_mismatch() { + let setup = pedersen_setup(); + let protocol = one_stage_protocol(&setup); + let round_rows = vec![ + pedersen_commitment(&setup, 7); + protocol.dimensions.coefficient_rows.saturating_sub(1) + ]; + let output_claim_rows = + vec![pedersen_commitment(&setup, 71); protocol.dimensions.output_claim_rows]; + let auxiliary_rows = + vec![pedersen_commitment(&setup, 72); protocol.dimensions.auxiliary_rows]; + let error_rows = vec![pedersen_commitment(&setup, 8); protocol.dimensions.error.row_count]; + + let error = protocol + .random_relaxed_instance( + &round_rows, + &output_claim_rows, + &auxiliary_rows, + &error_rows, + &[], + Fr::from_u64(3), + ) + .expect_err("witness row count differs"); + + assert_eq!( + error, + RelaxedError::LengthMismatch { + name: "random round commitments", + expected: protocol.dimensions.coefficient_rows, + actual: protocol.dimensions.coefficient_rows - 1, + } + ); + } + + #[test] + fn random_relaxed_instance_rejects_error_row_count_mismatch() { + let setup = pedersen_setup(); + let protocol = one_stage_protocol(&setup); + let round_rows = vec![pedersen_commitment(&setup, 7); protocol.dimensions.coefficient_rows]; + let output_claim_rows = + vec![pedersen_commitment(&setup, 71); protocol.dimensions.output_claim_rows]; + let auxiliary_rows = + vec![pedersen_commitment(&setup, 72); protocol.dimensions.auxiliary_rows]; + let error_rows = vec![ + pedersen_commitment(&setup, 8); + protocol.dimensions.error.row_count.saturating_sub(1) + ]; + + let error = protocol + .random_relaxed_instance( + &round_rows, + &output_claim_rows, + &auxiliary_rows, + &error_rows, + &[], + Fr::from_u64(3), + ) + .expect_err("error row count differs"); + + assert_eq!( + error, + RelaxedError::LengthMismatch { + name: "random error row commitments", + expected: protocol.dimensions.error.row_count, + actual: protocol.dimensions.error.row_count - 1, + } + ); + } + + #[test] + fn random_relaxed_instance_rejects_eval_commitment_count_mismatch() { + let setup = pedersen_setup(); + let protocol = one_stage_protocol(&setup); + let round_rows = vec![pedersen_commitment(&setup, 7); protocol.dimensions.coefficient_rows]; + let output_claim_rows = + vec![pedersen_commitment(&setup, 71); protocol.dimensions.output_claim_rows]; + let auxiliary_rows = + vec![pedersen_commitment(&setup, 72); protocol.dimensions.auxiliary_rows]; + let error_rows = vec![pedersen_commitment(&setup, 8); protocol.dimensions.error.row_count]; + + let error = protocol + .random_relaxed_instance( + &round_rows, + &output_claim_rows, + &auxiliary_rows, + &error_rows, + &[pedersen_commitment(&setup, 100)], + Fr::from_u64(3), + ) + .expect_err("eval commitment count differs"); + + assert_eq!( + error, + RelaxedError::LengthMismatch { + name: "random eval commitments", + expected: 0, + actual: 1, + } + ); + } + + #[test] + fn validate_cross_term_error_rows_accepts_exact_count() { + let setup = pedersen_setup(); + let protocol = one_stage_protocol(&setup); + let cross_term_rows = + vec![pedersen_commitment(&setup, 9); protocol.dimensions.error.row_count]; + + protocol + .validate_cross_term_error_rows(&cross_term_rows) + .expect("cross-term row count matches"); + } + + #[test] + fn validate_cross_term_error_rows_rejects_count_mismatch() { + let setup = pedersen_setup(); + let protocol = one_stage_protocol(&setup); + let cross_term_rows = vec![ + pedersen_commitment(&setup, 9); + protocol.dimensions.error.row_count.saturating_sub(1) + ]; + + let error = protocol + .validate_cross_term_error_rows(&cross_term_rows) + .expect_err("cross-term row count differs"); + + assert_eq!( + error, + RelaxedError::LengthMismatch { + name: "cross-term error row commitments", + expected: protocol.dimensions.error.row_count, + actual: protocol.dimensions.error.row_count - 1, + } + ); + } + + fn one_stage_protocol(setup: &PedersenSetup) -> BlindFoldProtocol { + let stages = vec![stage(1, 1)]; + let proofs = vec![commitment_proof(setup, &[(11, 1)], &[21])]; + + protocol_from_proofs(&stages, &proofs, Vec::new()) + } +} diff --git a/crates/jolt-blindfold/src/r1cs.rs b/crates/jolt-blindfold/src/r1cs.rs new file mode 100644 index 0000000000..b0db17b694 --- /dev/null +++ b/crates/jolt-blindfold/src/r1cs.rs @@ -0,0 +1,756 @@ +use jolt_field::Field; +use jolt_r1cs::{ + assert_claim_expr_eq, ClaimSourceTable, ClaimSources, LinearCombination, R1csBuilder, Variable, +}; +use jolt_sumcheck::{ + append_sumcheck_r1cs_constraints_for_domain, SumcheckR1csError, SumcheckR1csLayout, + SumcheckR1csRoundLayout, +}; + +use crate::{BlindFoldStage, BlindFoldStatement, Error, LayoutError}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Layout { + pub witness_row_len: usize, + pub stages: Vec, + pub final_openings: Vec, +} + +impl Layout { + pub fn stage_count(&self) -> usize { + self.stages.len() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StageLayout { + pub sumcheck: SumcheckR1csLayout, + pub output_claim_rows: Vec, +} + +impl StageLayout { + pub fn input_claim(&self) -> Variable { + self.sumcheck.input_claim + } + + pub fn round_count(&self) -> usize { + self.sumcheck.round_count() + } + + pub fn output_claim(&self) -> Variable { + self.sumcheck.output_claim + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OutputClaimRowLayout { + pub variables: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FinalOpeningLayout { + pub evaluation: Option, + pub blinding: Option, +} + +impl BlindFoldStatement +where + F: Field, +{ + pub fn build( + &self, + builder: &mut R1csBuilder, + claim_sources: &mut R, + ) -> Result + where + R: ClaimSources, + { + let layout = self.allocate_layout(builder)?; + self.append(builder, &layout, claim_sources)?; + Ok(layout) + } + + pub fn append( + &self, + builder: &mut R1csBuilder, + layout: &Layout, + claim_sources: &mut R, + ) -> Result<(), Error> + where + R: ClaimSources, + { + self.validate_layout_counts(layout)?; + + for (stage_index, (stage, stage_layout)) in + self.stages.iter().zip(&layout.stages).enumerate() + { + assert_claim_expr_eq( + builder, + &stage.input_claim, + stage_layout.sumcheck.input_claim, + claim_sources, + )?; + + append_sumcheck_r1cs_constraints_for_domain( + builder, + stage.statement, + &stage.consistency.rounds, + &stage_layout.sumcheck, + stage.domain, + ) + .map_err(|source| Error::Sumcheck { + stage_index, + source, + })?; + + assert_claim_expr_eq( + builder, + &stage.output_claim, + stage_layout.sumcheck.output_claim, + claim_sources, + )?; + } + + for (binding, binding_layout) in self.final_openings.iter().zip(&layout.final_openings) { + let Some(evaluation) = binding_layout.evaluation else { + continue; + }; + let mut combined = LinearCombination::zero(); + for (opening_id, &coefficient) in binding.opening_ids.iter().zip(&binding.coefficients) + { + combined = combined + + claim_sources + .opening(opening_id)? + .into_linear_combination() + .scale(coefficient); + } + builder.assert_equal(combined, evaluation); + } + + Ok(()) + } + + pub fn allocate_layout(&self, builder: &mut R1csBuilder) -> Result { + let witness_row_len = self.witness_row_len()?; + + let coefficients = self + .stages + .iter() + .enumerate() + .map(|(stage_index, stage)| { + stage + .validate_sumcheck_statement() + .map_err(|source| LayoutError::Sumcheck { + stage_index, + source, + })?; + Ok(stage + .consistency + .rounds + .iter() + .map(|round| { + let coefficients = (0..=round.degree) + .map(|_| builder.alloc_unknown()) + .collect::>(); + for _ in coefficients.len()..witness_row_len { + let _ = builder.alloc(F::zero()); + } + coefficients + }) + .collect::>()) + }) + .collect::, _>>()?; + + let output_claim_rows = self + .stages + .iter() + .map(|stage| stage.allocate_output_claim_rows(builder, witness_row_len)) + .collect::>(); + + let stages = self + .stages + .iter() + .zip(coefficients) + .zip(output_claim_rows) + .map(|((stage, stage_coefficients), output_claim_rows)| { + let input_claim = builder.alloc_unknown(); + let mut claim_in = input_claim; + let mut rounds = Vec::with_capacity(stage.consistency.rounds.len()); + + for coefficients in stage_coefficients { + let claim_out = builder.alloc_unknown(); + rounds.push(SumcheckR1csRoundLayout { + claim_in, + coefficients, + claim_out, + }); + claim_in = claim_out; + } + + Ok(StageLayout { + output_claim_rows, + sumcheck: SumcheckR1csLayout { + input_claim, + rounds, + output_claim: claim_in, + }, + }) + }) + .collect::, LayoutError>>()?; + + let final_openings = self + .final_openings + .iter() + .map(|binding| FinalOpeningLayout { + evaluation: (!binding.opening_ids.is_empty()) + .then(|| allocate_private_row_scalar(builder, witness_row_len)), + blinding: (!binding.opening_ids.is_empty()) + .then(|| allocate_private_row_scalar(builder, witness_row_len)), + }) + .collect(); + + Ok(Layout { + witness_row_len, + stages, + final_openings, + }) + } + + fn witness_row_len(&self) -> Result { + let round_coefficients = self + .stages + .iter() + .flat_map(|stage| &stage.consistency.rounds) + .map(|round| round.degree.saturating_add(1)) + .max() + .unwrap_or(1); + let output_claim_row_len = self + .stages + .iter() + .map(|stage| stage.output_claim_rows.row_len) + .max() + .unwrap_or(0); + let row_len = round_coefficients.max(output_claim_row_len).max(1); + row_len + .checked_next_power_of_two() + .ok_or(LayoutError::DimensionOverflow { + name: "witness row length", + value: row_len, + }) + } + + fn validate_layout_counts(&self, layout: &Layout) -> Result<(), Error> { + if self.stages.len() != layout.stages.len() { + return Err(Error::LayoutStageCountMismatch { + statement_stages: self.stages.len(), + layout_stages: layout.stages.len(), + }); + } + + if self.final_openings.len() != layout.final_openings.len() { + return Err(Error::LayoutStageCountMismatch { + statement_stages: self.final_openings.len(), + layout_stages: layout.final_openings.len(), + }); + } + + Ok(()) + } +} + +impl BlindFoldStatement +where + F: Field, + O: Clone + PartialEq, + P: Clone + PartialEq, + Ch: Clone + PartialEq, +{ + pub fn build_with_sources( + &self, + builder: &mut R1csBuilder, + publics: &[(P, F)], + challenges: &[(Ch, F)], + ) -> Result { + let layout = self.allocate_layout(builder)?; + let mut claim_sources = ClaimSourceTable::::new(); + self.insert_output_claim_sources(&layout, &mut claim_sources)?; + for (id, value) in publics { + claim_sources.insert_public(id.clone(), *value); + } + for (id, value) in challenges { + claim_sources.insert_challenge(id.clone(), *value); + } + self.append(builder, &layout, &mut claim_sources)?; + Ok(layout) + } + + fn insert_output_claim_sources( + &self, + layout: &Layout, + claim_sources: &mut ClaimSourceTable, + ) -> Result<(), Error> { + let mut inserted = Vec::<(O, Variable)>::new(); + for (stage, stage_layout) in self.stages.iter().zip(&layout.stages) { + let row_len = stage.output_claim_rows.row_len; + let variables = stage_layout + .output_claim_rows + .iter() + .flat_map(|row| row.variables.iter().take(row_len)); + for (opening_id, &variable) in stage.output_claim_rows.opening_ids.iter().zip(variables) + { + claim_sources.insert_opening(opening_id.clone(), variable); + inserted.push((opening_id.clone(), variable)); + } + for alias in &stage.output_claim_rows.opening_aliases { + let variable = inserted + .iter() + .find_map(|(opening_id, variable)| { + (opening_id == &alias.source).then_some(*variable) + }) + .ok_or(Error::MissingOpeningAliasSource)?; + claim_sources.insert_opening(alias.alias.clone(), variable); + inserted.push((alias.alias.clone(), variable)); + } + } + Ok(()) + } +} + +fn allocate_private_row_scalar( + builder: &mut R1csBuilder, + witness_row_len: usize, +) -> Variable { + // Final evaluation bindings are opened at fixed coordinates. Keep them in + // dedicated rows so those openings cannot reveal unrelated witness values. + pad_to_witness_row_boundary(builder, witness_row_len); + let variable = builder.alloc_unknown(); + pad_to_witness_row_boundary(builder, witness_row_len); + variable +} + +fn pad_to_witness_row_boundary(builder: &mut R1csBuilder, witness_row_len: usize) { + while !(builder.num_vars() - 1).is_multiple_of(witness_row_len) { + let _ = builder.alloc(F::zero()); + } +} + +impl BlindFoldStage { + fn validate_sumcheck_statement(&self) -> Result<(), SumcheckR1csError> { + if self.statement.num_vars != self.consistency.rounds.len() { + return Err(SumcheckR1csError::WrongNumberOfRounds { + expected: self.statement.num_vars, + actual: self.consistency.rounds.len(), + }); + } + + for (round_index, round) in self.consistency.rounds.iter().enumerate() { + if round.degree > self.statement.degree { + return Err(SumcheckR1csError::DegreeBoundExceeded { + round_index, + bound: self.statement.degree, + actual: round.degree, + }); + } + } + + Ok(()) + } +} + +impl BlindFoldStage +where + F: Field, +{ + fn allocate_output_claim_rows( + &self, + builder: &mut R1csBuilder, + witness_row_len: usize, + ) -> Vec { + let row_count = self.output_claim_rows.commitments.commitments.len(); + let row_len = self.output_claim_rows.row_len; + let mut remaining_openings = self.output_claim_rows.opening_ids.len(); + let mut rows = Vec::with_capacity(row_count); + + for _ in 0..row_count { + let opening_slots = remaining_openings.min(row_len); + let mut variables = Vec::with_capacity(witness_row_len); + for slot in 0..witness_row_len { + let variable = if slot < opening_slots { + builder.alloc_unknown() + } else { + builder.alloc(F::zero()) + }; + variables.push(variable); + } + remaining_openings -= opening_slots; + rows.push(OutputClaimRowLayout { variables }); + } + + rows + } +} + +#[cfg(test)] +#[expect(clippy::expect_used, reason = "tests may panic on assertion failures")] +mod tests { + use super::*; + use crate::{BlindFoldStage, BlindFoldStatement, CommittedClaimRows, OpeningAlias}; + use jolt_claims::{challenge, constant, opening, public, Expr}; + use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_r1cs::{ClaimLoweringError, ClaimSourceTable, R1csBuilderError}; + use jolt_sumcheck::{ + CommittedOutputClaims, CommittedSumcheckConsistency, SumcheckDomainSpec, SumcheckR1csError, + SumcheckR1csRoundLayout, SumcheckStatement, VerifiedCommittedRound, + }; + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum Opening { + Input, + Output, + Alias, + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum Public { + Scale, + } + + fn consistency(degrees: &[usize]) -> CommittedSumcheckConsistency { + CommittedSumcheckConsistency { + rounds: degrees + .iter() + .enumerate() + .map(|(index, °ree)| VerifiedCommittedRound { + commitment: (), + degree, + challenge: Fr::from_u64(index as u64 + 11), + }) + .collect(), + } + } + + fn committed_consistency(rounds: &[(usize, u64)]) -> CommittedSumcheckConsistency { + CommittedSumcheckConsistency { + rounds: rounds + .iter() + .map(|&(degree, challenge)| VerifiedCommittedRound { + commitment: (), + degree, + challenge: Fr::from_u64(challenge), + }) + .collect(), + } + } + + fn output_claim_rows() -> CommittedClaimRows<(), ()> { + CommittedClaimRows::empty() + } + + fn empty_stage( + num_vars: usize, + degree: usize, + round_degrees: &[usize], + ) -> BlindFoldStage { + let claim: Expr = constant(Fr::from_u64(0)); + BlindFoldStage::new( + "stage", + SumcheckStatement::new(num_vars, degree), + SumcheckDomainSpec::BooleanHypercube, + consistency(round_degrees), + output_claim_rows(), + claim.clone(), + claim, + ) + } + + fn claim_stage( + num_vars: usize, + degree: usize, + rounds: &[(usize, u64)], + input_claim: Expr, + output_claim: Expr, + ) -> BlindFoldStage { + BlindFoldStage::new( + "stage", + SumcheckStatement::new(num_vars, degree), + SumcheckDomainSpec::BooleanHypercube, + committed_consistency(rounds), + CommittedClaimRows::empty(), + input_claim, + output_claim, + ) + } + + fn assign(builder: &mut R1csBuilder, variable: Variable, value: u64) { + builder + .assign(variable, Fr::from_u64(value)) + .expect("assignment succeeds"); + } + + fn assign_round( + builder: &mut R1csBuilder, + round: &SumcheckR1csRoundLayout, + coefficients: &[u64], + claim_out: u64, + ) { + for (&variable, &coefficient) in round.coefficients.iter().zip(coefficients) { + assign(builder, variable, coefficient); + } + assign(builder, round.claim_out, claim_out); + } + + #[test] + fn allocates_claim_chain_and_coefficients() { + let statement = BlindFoldStatement::new(vec![empty_stage(2, 3, &[1, 3])], Vec::new()); + let mut builder = R1csBuilder::::new(); + + let layout = statement + .allocate_layout(&mut builder) + .expect("layout allocates"); + + assert_eq!(layout.stage_count(), 1); + let stage = &layout.stages[0]; + assert_eq!(stage.round_count(), 2); + assert_eq!(stage.sumcheck.rounds[0].degree(), 1); + assert_eq!(stage.sumcheck.rounds[1].degree(), 3); + assert_eq!(stage.input_claim(), stage.sumcheck.rounds[0].claim_in); + assert_eq!( + stage.sumcheck.rounds[0].claim_out, + stage.sumcheck.rounds[1].claim_in + ); + assert_eq!(stage.output_claim(), stage.sumcheck.rounds[1].claim_out); + assert_eq!( + builder.witness().expect_err("layout is witness-only"), + R1csBuilderError::MissingWitnessValue { + variable: Variable::new(1), + }, + ); + } + + #[test] + fn append_rejects_layout_stage_count_mismatch() { + let statement = BlindFoldStatement::new(vec![empty_stage(1, 2, &[2])], Vec::new()); + let mut builder = R1csBuilder::::new(); + let mut sources = ClaimSourceTable::::new(); + let layout = Layout { + witness_row_len: 1, + stages: Vec::new(), + final_openings: Vec::new(), + }; + + let error = statement + .append(&mut builder, &layout, &mut sources) + .expect_err("stage counts differ"); + + assert_eq!( + error, + Error::LayoutStageCountMismatch { + statement_stages: 1, + layout_stages: 0, + } + ); + } + + #[test] + fn rejects_round_count_mismatch() { + let statement = BlindFoldStatement::new(vec![empty_stage(2, 2, &[2])], Vec::new()); + let mut builder = R1csBuilder::::new(); + + let error = statement + .allocate_layout(&mut builder) + .expect_err("round counts differ"); + + assert_eq!( + error, + LayoutError::Sumcheck { + stage_index: 0, + source: SumcheckR1csError::WrongNumberOfRounds { + expected: 2, + actual: 1, + }, + } + ); + } + + #[test] + fn rejects_degree_above_stage_bound() { + let statement = BlindFoldStatement::new(vec![empty_stage(1, 2, &[3])], Vec::new()); + let mut builder = R1csBuilder::::new(); + + let error = statement + .allocate_layout(&mut builder) + .expect_err("degree exceeds bound"); + + assert_eq!( + error, + LayoutError::Sumcheck { + stage_index: 0, + source: SumcheckR1csError::DegreeBoundExceeded { + round_index: 0, + bound: 2, + actual: 3, + }, + } + ); + } + + #[test] + fn lowers_claim_sources_and_sumcheck_constraints() { + let mut builder = R1csBuilder::::new(); + let input = builder.alloc(Fr::from_u64(3)); + let mut sources = ClaimSourceTable::::new(); + sources.insert_opening(Opening::Input, input); + sources.insert_challenge(0, Fr::from_u64(4)); + sources.insert_public(Public::Scale, Fr::from_u64(2)); + + let statement = BlindFoldStatement::new( + vec![claim_stage( + 1, + 1, + &[(1, 2)], + opening(Opening::Input) * public(Public::Scale) + challenge(0), + constant(Fr::from_u64(11)), + )], + Vec::new(), + ); + + let layout = statement + .build(&mut builder, &mut sources) + .expect("constraints should build"); + + let stage_layout = &layout.stages[0].sumcheck; + assign(&mut builder, stage_layout.input_claim, 10); + assign_round(&mut builder, &stage_layout.rounds[0], &[3, 4], 11); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); + } + + #[test] + fn lowers_output_claim_alias_to_source_variable() { + let input_claim: Expr = constant(Fr::from_u64(10)); + let output_claim: Expr = opening(Opening::Alias); + let statement = BlindFoldStatement::new( + vec![BlindFoldStage::new( + "alias", + SumcheckStatement::new(1, 1), + SumcheckDomainSpec::BooleanHypercube, + committed_consistency(&[(1, 2)]), + CommittedClaimRows::new( + vec![Opening::Output], + 1, + CommittedOutputClaims { + commitments: vec![()], + }, + ) + .with_aliases([OpeningAlias::new(Opening::Alias, Opening::Output)]), + input_claim, + output_claim, + )], + Vec::new(), + ); + let mut builder = R1csBuilder::::new(); + + let layout = statement + .build_with_sources(&mut builder, &[], &[]) + .expect("constraints should build"); + let stage_layout = &layout.stages[0].sumcheck; + assign(&mut builder, stage_layout.input_claim, 10); + assign_round(&mut builder, &stage_layout.rounds[0], &[3, 4], 11); + assign( + &mut builder, + layout.stages[0].output_claim_rows[0].variables[0], + 11, + ); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); + } + + #[test] + fn lowers_centered_integer_domain_round_sums() { + let mut builder = R1csBuilder::::new(); + let statement = BlindFoldStatement::new( + vec![BlindFoldStage::new( + "centered", + SumcheckStatement::new(1, 2), + SumcheckDomainSpec::centered_integer(4), + committed_consistency(&[(2, 5)]), + CommittedClaimRows::empty(), + constant(Fr::from_u64(26)), + constant(Fr::from_u64(86)), + )], + Vec::new(), + ); + let mut sources = ClaimSourceTable::::new(); + + let layout = statement + .build(&mut builder, &mut sources) + .expect("constraints should build for centered domain"); + let stage_layout = &layout.stages[0].sumcheck; + assign(&mut builder, stage_layout.input_claim, 26); + assign_round(&mut builder, &stage_layout.rounds[0], &[1, 2, 3], 86); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); + } + + #[test] + fn reports_missing_claim_source() { + let statement = BlindFoldStatement::new( + vec![claim_stage( + 0, + 1, + &[], + opening(Opening::Input), + constant(Fr::from_u64(0)), + )], + Vec::new(), + ); + let mut builder = R1csBuilder::::new(); + let mut sources = ClaimSourceTable::::new(); + + let error = statement + .build(&mut builder, &mut sources) + .expect_err("opening is missing"); + + assert_eq!(error, Error::Claim(ClaimLoweringError::MissingOpening)); + } + + #[test] + fn reports_sumcheck_layout_errors_with_stage_index() { + let statement = BlindFoldStatement::new( + vec![claim_stage( + 1, + 1, + &[(1, 2)], + constant(Fr::from_u64(10)), + constant(Fr::from_u64(11)), + )], + Vec::new(), + ); + let mut builder = R1csBuilder::::new(); + let mut layout = statement + .allocate_layout(&mut builder) + .expect("layout allocates"); + layout.stages[0].sumcheck.rounds[0].claim_in = + layout.stages[0].sumcheck.rounds[0].claim_out; + let mut sources = ClaimSourceTable::::new(); + + let error = statement + .append(&mut builder, &layout, &mut sources) + .expect_err("layout claim chain is broken"); + + assert_eq!( + error, + Error::Sumcheck { + stage_index: 0, + source: SumcheckR1csError::RoundClaimLinkMismatch { + round_index: 0, + expected: layout.stages[0].sumcheck.input_claim, + actual: layout.stages[0].sumcheck.rounds[0].claim_in, + }, + } + ); + } +} diff --git a/crates/jolt-blindfold/src/relaxed.rs b/crates/jolt-blindfold/src/relaxed.rs new file mode 100644 index 0000000000..7398247d32 --- /dev/null +++ b/crates/jolt-blindfold/src/relaxed.rs @@ -0,0 +1,397 @@ +use jolt_crypto::HomomorphicCommitment; +use jolt_field::Field; + +use crate::RelaxedError; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RelaxedInstance { + pub u: F, + pub witness_row_commitments: Vec, + pub error_row_commitments: Vec, + pub eval_commitments: Vec, +} + +impl RelaxedInstance { + pub fn new( + u: F, + witness_row_commitments: Vec, + error_row_commitments: Vec, + eval_commitments: Vec, + ) -> Self { + Self { + u, + witness_row_commitments, + error_row_commitments, + eval_commitments, + } + } +} + +impl RelaxedInstance +where + F: Field, + Com: HomomorphicCommitment, +{ + pub fn fold( + &self, + random: &Self, + cross_term_error_row_commitments: &[Com], + folding_challenge: F, + ) -> Result { + ensure_len( + "random witness row commitments", + self.witness_row_commitments.len(), + random.witness_row_commitments.len(), + )?; + ensure_len( + "random error row commitments", + self.error_row_commitments.len(), + random.error_row_commitments.len(), + )?; + ensure_len( + "cross-term error row commitments", + self.error_row_commitments.len(), + cross_term_error_row_commitments.len(), + )?; + ensure_len( + "random eval commitments", + self.eval_commitments.len(), + random.eval_commitments.len(), + )?; + + let r_squared = folding_challenge * folding_challenge; + let u = self.u + folding_challenge * random.u; + + let witness_row_commitments = self + .witness_row_commitments + .iter() + .zip(&random.witness_row_commitments) + .map(|(real, random)| Com::linear_combine(real, random, &folding_challenge)) + .collect(); + let error_row_commitments = self + .error_row_commitments + .iter() + .zip(cross_term_error_row_commitments) + .zip(&random.error_row_commitments) + .map(|((real, cross_term), random)| { + let with_cross = Com::linear_combine(real, cross_term, &folding_challenge); + Com::linear_combine(&with_cross, random, &r_squared) + }) + .collect(); + let eval_commitments = self + .eval_commitments + .iter() + .zip(&random.eval_commitments) + .map(|(real, random)| Com::linear_combine(real, random, &folding_challenge)) + .collect(); + + Ok(Self { + u, + witness_row_commitments, + error_row_commitments, + eval_commitments, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RelaxedWitness { + pub witness_rows: Vec, + pub witness_row_blindings: Vec, + pub error_rows: Vec, + pub error_row_blindings: Vec, +} + +impl RelaxedWitness { + pub fn new( + witness_rows: Vec, + witness_row_blindings: Vec, + error_rows: Vec, + error_row_blindings: Vec, + ) -> Self { + Self { + witness_rows, + witness_row_blindings, + error_rows, + error_row_blindings, + } + } +} + +impl RelaxedWitness { + pub fn fold( + &self, + random: &Self, + cross_term_error_rows: &[F], + cross_term_error_row_blindings: &[F], + folding_challenge: F, + ) -> Result { + ensure_len( + "random witness rows", + self.witness_rows.len(), + random.witness_rows.len(), + )?; + ensure_len( + "random witness row blindings", + self.witness_row_blindings.len(), + random.witness_row_blindings.len(), + )?; + ensure_len( + "random error rows", + self.error_rows.len(), + random.error_rows.len(), + )?; + ensure_len( + "cross-term error rows", + self.error_rows.len(), + cross_term_error_rows.len(), + )?; + ensure_len( + "random error row blindings", + self.error_row_blindings.len(), + random.error_row_blindings.len(), + )?; + ensure_len( + "cross-term error row blindings", + self.error_row_blindings.len(), + cross_term_error_row_blindings.len(), + )?; + + let r_squared = folding_challenge * folding_challenge; + let witness_rows = self + .witness_rows + .iter() + .zip(&random.witness_rows) + .map(|(&real, &random)| real + folding_challenge * random) + .collect(); + let witness_row_blindings = self + .witness_row_blindings + .iter() + .zip(&random.witness_row_blindings) + .map(|(&real, &random)| real + folding_challenge * random) + .collect(); + let error_rows = self + .error_rows + .iter() + .zip(cross_term_error_rows) + .zip(&random.error_rows) + .map(|((&real, &cross_term), &random)| { + real + folding_challenge * cross_term + r_squared * random + }) + .collect(); + let error_row_blindings = self + .error_row_blindings + .iter() + .zip(cross_term_error_row_blindings) + .zip(&random.error_row_blindings) + .map(|((&real, &cross_term), &random)| { + real + folding_challenge * cross_term + r_squared * random + }) + .collect(); + + Ok(Self { + witness_rows, + witness_row_blindings, + error_rows, + error_row_blindings, + }) + } +} + +fn ensure_len(name: &'static str, expected: usize, actual: usize) -> Result<(), RelaxedError> { + if expected != actual { + return Err(RelaxedError::LengthMismatch { + name, + expected, + actual, + }); + } + Ok(()) +} + +#[cfg(test)] +#[expect( + clippy::expect_used, + clippy::unwrap_used, + reason = "tests should fail loudly" +)] +mod tests { + use super::*; + use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment}; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn f(value: u64) -> Fr { + Fr::from_u64(value) + } + + fn pedersen_setup() -> PedersenSetup { + let generator = Bn254::g1_generator(); + PedersenSetup::new(vec![generator], generator.scalar_mul(&f(99))) + } + + fn c(setup: &PedersenSetup, value: Fr, blinding: Fr) -> Bn254G1 { + Pedersen::::commit(setup, &[value], &blinding) + } + + #[test] + fn folds_relaxed_instance() { + let setup = pedersen_setup(); + let real = RelaxedInstance::new( + f(2), + vec![c(&setup, f(10), f(100)), c(&setup, f(20), f(200))], + vec![c(&setup, f(30), f(300)), c(&setup, f(40), f(400))], + vec![c(&setup, f(50), f(500))], + ); + let random = RelaxedInstance::new( + f(3), + vec![c(&setup, f(1), f(10)), c(&setup, f(2), f(20))], + vec![c(&setup, f(3), f(30)), c(&setup, f(4), f(40))], + vec![c(&setup, f(5), f(50))], + ); + let cross_terms = [c(&setup, f(7), f(70)), c(&setup, f(8), f(80))]; + let folded = real + .fold(&random, &cross_terms, f(11)) + .expect("fold dimensions match"); + + assert_eq!(folded.u, f(2) + f(11) * f(3)); + assert_eq!( + folded.witness_row_commitments, + vec![ + c(&setup, f(10) + f(11), f(100) + f(110)), + c(&setup, f(20) + f(22), f(200) + f(220)), + ] + ); + assert_eq!( + folded.error_row_commitments, + vec![ + c( + &setup, + f(30) + f(11) * f(7) + f(121) * f(3), + f(300) + f(11) * f(70) + f(121) * f(30), + ), + c( + &setup, + f(40) + f(11) * f(8) + f(121) * f(4), + f(400) + f(11) * f(80) + f(121) * f(40), + ), + ] + ); + assert_eq!( + folded.eval_commitments, + vec![c(&setup, f(50) + f(55), f(500) + f(550))] + ); + } + + #[test] + fn folds_relaxed_witness() { + let real = RelaxedWitness::new( + vec![f(10), f(20)], + vec![f(30), f(40)], + vec![f(50), f(60)], + vec![f(70), f(80)], + ); + let random = RelaxedWitness::new( + vec![f(1), f(2)], + vec![f(3), f(4)], + vec![f(5), f(6)], + vec![f(7), f(8)], + ); + let folded = real + .fold(&random, &[f(9), f(10)], &[f(11), f(12)], f(13)) + .expect("fold dimensions match"); + + assert_eq!(folded.witness_rows, vec![f(10 + 13), f(20 + 26)]); + assert_eq!(folded.witness_row_blindings, vec![f(30 + 39), f(40 + 52)]); + assert_eq!( + folded.error_rows, + vec![ + f(50) + f(13) * f(9) + f(169) * f(5), + f(60) + f(13) * f(10) + f(169) * f(6), + ] + ); + assert_eq!( + folded.error_row_blindings, + vec![ + f(70) + f(13) * f(11) + f(169) * f(7), + f(80) + f(13) * f(12) + f(169) * f(8), + ] + ); + } + + #[test] + fn rejects_instance_length_mismatch() { + let setup = pedersen_setup(); + let real = RelaxedInstance::new( + f(1), + vec![c(&setup, f(1), f(10)), c(&setup, f(2), f(20))], + vec![c(&setup, f(3), f(30))], + vec![c(&setup, f(4), f(40))], + ); + let random = RelaxedInstance::new( + f(1), + vec![c(&setup, f(1), f(10))], + vec![c(&setup, f(3), f(30))], + vec![c(&setup, f(4), f(40))], + ); + + let error = real + .fold(&random, &[c(&setup, f(5), f(50))], f(2)) + .unwrap_err(); + + assert_eq!( + error, + RelaxedError::LengthMismatch { + name: "random witness row commitments", + expected: 2, + actual: 1, + } + ); + } + + #[test] + fn rejects_cross_term_length_mismatch() { + let setup = pedersen_setup(); + let real = RelaxedInstance::new( + f(1), + vec![c(&setup, f(1), f(10))], + vec![c(&setup, f(2), f(20)), c(&setup, f(3), f(30))], + vec![], + ); + let random = RelaxedInstance::new( + f(1), + vec![c(&setup, f(1), f(10))], + vec![c(&setup, f(2), f(20)), c(&setup, f(3), f(30))], + vec![], + ); + + let error = real + .fold(&random, &[c(&setup, f(5), f(50))], f(2)) + .unwrap_err(); + + assert_eq!( + error, + RelaxedError::LengthMismatch { + name: "cross-term error row commitments", + expected: 2, + actual: 1, + } + ); + } + + #[test] + fn rejects_witness_length_mismatch() { + let real = RelaxedWitness::new(vec![f(1)], vec![f(2)], vec![f(3), f(4)], vec![f(5)]); + let random = RelaxedWitness::new(vec![f(1)], vec![f(2)], vec![f(3), f(4)], vec![f(5)]); + + let error = real.fold(&random, &[f(6)], &[f(7)], f(2)).unwrap_err(); + + assert_eq!( + error, + RelaxedError::LengthMismatch { + name: "cross-term error rows", + expected: 2, + actual: 1, + } + ); + } +} diff --git a/crates/jolt-blindfold/src/statements.rs b/crates/jolt-blindfold/src/statements.rs new file mode 100644 index 0000000000..8a57abd86f --- /dev/null +++ b/crates/jolt-blindfold/src/statements.rs @@ -0,0 +1,169 @@ +use jolt_claims::Expr; +use jolt_sumcheck::{ + CommittedOutputClaims, CommittedSumcheckConsistency, SumcheckDomainSpec, SumcheckStatement, +}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BlindFoldStatement { + pub stages: Vec>, + pub final_openings: Vec>, +} + +impl BlindFoldStatement { + pub fn new( + stages: Vec>, + final_openings: Vec>, + ) -> Self { + Self { + stages, + final_openings, + } + } + + pub fn stage_count(&self) -> usize { + self.stages.len() + } +} + +impl BlindFoldStatement { + pub fn final_opening_commitments(&self) -> Vec { + self.final_openings + .iter() + .map(|binding| binding.evaluation_commitment.clone()) + .collect() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BlindFoldStage { + pub name: String, + pub statement: SumcheckStatement, + pub domain: SumcheckDomainSpec, + pub consistency: CommittedSumcheckConsistency, + pub output_claim_rows: CommittedClaimRows, + pub input_claim: Expr, + pub output_claim: Expr, +} + +impl BlindFoldStage { + pub fn new( + name: impl Into, + statement: SumcheckStatement, + domain: SumcheckDomainSpec, + consistency: CommittedSumcheckConsistency, + output_claim_rows: CommittedClaimRows, + input_claim: Expr, + output_claim: Expr, + ) -> Self { + Self { + name: name.into(), + statement, + domain, + consistency, + output_claim_rows, + input_claim, + output_claim, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CommittedClaimRows { + pub opening_ids: Vec, + pub opening_aliases: Vec>, + pub row_len: usize, + pub commitments: CommittedOutputClaims, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OpeningAlias { + pub alias: O, + pub source: O, +} + +impl OpeningAlias { + pub fn new(alias: O, source: O) -> Self { + Self { alias, source } + } +} + +impl CommittedClaimRows { + pub fn new( + opening_ids: Vec, + row_len: usize, + commitments: CommittedOutputClaims, + ) -> Self { + Self { + opening_ids, + opening_aliases: Vec::new(), + row_len, + commitments, + } + } + + pub fn with_aliases(mut self, aliases: impl IntoIterator>) -> Self { + self.opening_aliases.extend(aliases); + self + } + + pub fn empty() -> Self { + Self { + opening_ids: Vec::new(), + opening_aliases: Vec::new(), + row_len: 0, + commitments: CommittedOutputClaims { + commitments: Vec::new(), + }, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FinalOpeningBinding { + pub opening_ids: Vec, + pub coefficients: Vec, + pub evaluation_commitment: Com, +} + +impl FinalOpeningBinding { + pub fn new(opening_ids: Vec, coefficients: Vec, evaluation_commitment: Com) -> Self { + Self { + opening_ids, + coefficients, + evaluation_commitment, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_claims::{opening, Expr}; + use jolt_field::Fr; + use jolt_sumcheck::{CommittedSumcheckConsistency, SumcheckDomainSpec}; + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum Opening { + A, + } + + #[test] + fn blindfold_statement_groups_stages() { + let claim: Expr = opening(Opening::A); + let stage = BlindFoldStage::new( + "stage", + SumcheckStatement::new(2, 2), + SumcheckDomainSpec::BooleanHypercube, + CommittedSumcheckConsistency:: { rounds: Vec::new() }, + CommittedClaimRows::empty(), + claim.clone(), + claim, + ); + let statement = BlindFoldStatement::new(vec![stage], Vec::new()); + + assert_eq!(statement.stages.len(), 1); + assert_eq!(statement.stage_count(), 1); + assert_eq!(statement.stages[0].name, "stage"); + assert_eq!(statement.stages[0].statement, SumcheckStatement::new(2, 2)); + } +} diff --git a/crates/jolt-blindfold/src/verify.rs b/crates/jolt-blindfold/src/verify.rs new file mode 100644 index 0000000000..d6a81baf51 --- /dev/null +++ b/crates/jolt-blindfold/src/verify.rs @@ -0,0 +1,1150 @@ +use jolt_crypto::{HomomorphicCommitment, VectorCommitment, VectorCommitmentOpening}; +use jolt_field::{Field, FieldCore, RingAccumulator, WithAccumulator}; +use jolt_poly::EqPolynomial; +use jolt_r1cs::{ConstraintMatrices, MatrixColumnContributions}; +use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SUMCHECK_ROUND_TRANSCRIPT_LABEL}; +use jolt_transcript::{AppendToTranscript, Label, Transcript}; + +use crate::{ + BlindFoldProof, BlindFoldProtocol, RelaxedError, RelaxedInstance, VerificationError, + WitnessCoordinate, +}; + +const OUTER_SUMCHECK_DEGREE: usize = 3; +const INNER_SUMCHECK_DEGREE: usize = 2; +const INNER_SUMCHECK_LABEL: &[u8] = b"inner_sumcheck_poly"; + +impl BlindFoldProtocol +where + F: Field + AppendToTranscript, + Com: Copy + HomomorphicCommitment + AppendToTranscript, + ::Accumulator: RingAccumulator, +{ + pub fn verify( + &self, + proof: &BlindFoldProof, + vc_setup: &VC::Setup, + transcript: &mut T, + ) -> Result<(), VerificationError> + where + VC: VectorCommitment, + T: Transcript, + { + let folded = self.folded_instance_from_proof(proof, transcript)?; + ensure_len( + "folded eval outputs", + folded.eval_commitments.len(), + proof.folded_eval_outputs.len(), + )?; + ensure_len( + "folded eval blindings", + folded.eval_commitments.len(), + proof.folded_eval_blindings.len(), + )?; + proof.verify_folded_eval_commitments::(vc_setup, &folded)?; + self.verify_folded_eval_witness_bindings::(proof, vc_setup, &folded, transcript)?; + let outer = self.verify_outer_folded_r1cs::(proof, vc_setup, &folded, transcript)?; + self.verify_inner_folded_r1cs::(proof, vc_setup, &folded, &outer, transcript)?; + Ok(()) + } +} + +impl BlindFoldProtocol +where + F: Field + AppendToTranscript, + Com: Clone + HomomorphicCommitment + AppendToTranscript, +{ + fn folded_instance_from_proof( + &self, + proof: &BlindFoldProof, + transcript: &mut T, + ) -> Result, VerificationError> + where + T: Transcript, + { + let committed = self.committed_relaxed_instance(&proof.auxiliary_row_commitments)?; + committed.append_to_transcript( + transcript, + b"bf_committed_u", + b"bf_committed_w", + b"bf_committed_e", + b"bf_committed_eval", + ); + + let random = self.random_relaxed_instance( + &proof.random_round_commitments, + &proof.random_output_claim_row_commitments, + &proof.random_auxiliary_row_commitments, + &proof.random_error_row_commitments, + &proof.random_eval_commitments, + proof.random_u, + )?; + random.append_to_transcript( + transcript, + b"bf_random_u", + b"bf_random_w", + b"bf_random_e", + b"bf_random_eval", + ); + + self.validate_cross_term_error_rows(&proof.cross_term_error_row_commitments)?; + transcript.append_values(b"bf_cross_e", &proof.cross_term_error_row_commitments); + + let folding_challenge = transcript.challenge(); + Ok(committed.fold( + &random, + &proof.cross_term_error_row_commitments, + folding_challenge, + )?) + } +} + +impl RelaxedInstance +where + F: AppendToTranscript, + Com: AppendToTranscript, +{ + fn append_to_transcript( + &self, + transcript: &mut T, + u_label: &'static [u8], + witness_label: &'static [u8], + error_label: &'static [u8], + eval_label: &'static [u8], + ) where + T: Transcript, + { + transcript.append(&Label(u_label)); + self.u.append_to_transcript(transcript); + transcript.append_values(witness_label, &self.witness_row_commitments); + transcript.append_values(error_label, &self.error_row_commitments); + transcript.append_values(eval_label, &self.eval_commitments); + } +} + +impl BlindFoldProtocol +where + F: Field + AppendToTranscript, + Com: Copy + HomomorphicCommitment + AppendToTranscript, + ::Accumulator: RingAccumulator, +{ + fn verify_outer_folded_r1cs( + &self, + proof: &BlindFoldProof, + vc_setup: &VC::Setup, + folded: &RelaxedInstance, + transcript: &mut T, + ) -> Result, VerificationError> + where + VC: VectorCommitment, + T: Transcript, + { + let error_row_count = self.dimensions.error.row_count; + if error_row_count == 0 || !error_row_count.is_power_of_two() { + return Err(VerificationError::InvalidPowerOfTwo { + name: "error row count", + value: error_row_count, + }); + } + let row_vars = error_row_count.trailing_zeros() as usize; + + let error_row_len = self.dimensions.error.row_len; + if error_row_len == 0 || !error_row_len.is_power_of_two() { + return Err(VerificationError::InvalidPowerOfTwo { + name: "error row length", + value: error_row_len, + }); + } + let entry_vars = error_row_len.trailing_zeros() as usize; + let num_vars = + row_vars + .checked_add(entry_vars) + .ok_or(VerificationError::InvalidPowerOfTwo { + name: "outer sumcheck dimension", + value: usize::MAX, + })?; + if num_vars == 0 { + return Err(VerificationError::DegenerateSumcheck { + name: "outer folded R1CS sumcheck", + }); + } + + transcript.append(&Label(b"bf_spartan")); + let tau = transcript.challenge_vector(num_vars); + let claim = SumcheckClaim::new(num_vars, OUTER_SUMCHECK_DEGREE, F::zero()); + let outer = proof + .outer_sumcheck + .verify( + &claim, + BooleanHypercube, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + transcript, + ) + .map_err(|source| VerificationError::OuterSumcheck { source })?; + + let (row_point, entry_point) = outer.point.split_at(row_vars); + let e_rx = VC::verify_committed_rows( + vc_setup, + &folded.error_row_commitments, + row_point, + entry_point, + &proof.error_opening, + )?; + + let eq_tau_rx = EqPolynomial::::mle(&tau, &outer.point); + let expected = eq_tau_rx * (proof.az_rx * proof.bz_rx - folded.u * proof.cz_rx - e_rx); + if outer.value != expected { + return Err(VerificationError::OuterFinalClaimMismatch { + expected, + actual: outer.value, + }); + } + + transcript.append_values(b"bf_az_bz_cz", &[proof.az_rx, proof.bz_rx, proof.cz_rx]); + append_vector_opening( + transcript, + b"bf_error_opening", + b"bf_error_blind", + &proof.error_opening, + ); + + Ok(OuterCheck { + point: outer.point.into_vec(), + }) + } +} + +impl BlindFoldProof +where + F: Field, + Com: Copy + AppendToTranscript, +{ + fn verify_folded_eval_commitments( + &self, + vc_setup: &VC::Setup, + folded: &RelaxedInstance, + ) -> Result<(), VerificationError> + where + VC: VectorCommitment, + { + for (index, ((commitment, &output), &blinding)) in folded + .eval_commitments + .iter() + .zip(&self.folded_eval_outputs) + .zip(&self.folded_eval_blindings) + .enumerate() + { + if !VC::verify(vc_setup, commitment, &[output], &blinding) { + return Err(VerificationError::EvalCommitmentMismatch { index }); + } + } + + Ok(()) + } +} + +impl BlindFoldProtocol +where + F: Field + AppendToTranscript, + Com: Copy + HomomorphicCommitment + AppendToTranscript, + ::Accumulator: RingAccumulator, +{ + fn verify_folded_eval_witness_bindings( + &self, + proof: &BlindFoldProof, + vc_setup: &VC::Setup, + folded: &RelaxedInstance, + transcript: &mut T, + ) -> Result<(), VerificationError> + where + VC: VectorCommitment, + T: Transcript, + { + let coordinates = self.final_opening_witness_coordinates()?; + ensure_len( + "final opening bindings", + coordinates.len(), + folded.eval_commitments.len(), + )?; + + let expected_outputs = coordinates + .iter() + .filter(|coordinates| coordinates.evaluation.is_some()) + .count(); + ensure_len( + "folded eval output witness openings", + expected_outputs, + proof.folded_eval_output_openings.len(), + )?; + let expected_blindings = coordinates + .iter() + .filter(|coordinates| coordinates.blinding.is_some()) + .count(); + ensure_len( + "folded eval blinding witness openings", + expected_blindings, + proof.folded_eval_blinding_openings.len(), + )?; + + let mut output_openings = proof.folded_eval_output_openings.iter(); + let mut blinding_openings = proof.folded_eval_blinding_openings.iter(); + for (index, coordinates) in coordinates.iter().enumerate() { + if let Some(coordinate) = coordinates.evaluation { + let opening = output_openings.next().ok_or(RelaxedError::LengthMismatch { + name: "folded eval output witness openings", + expected: expected_outputs, + actual: proof.folded_eval_output_openings.len(), + })?; + let opened = coordinate.verify_opening::(vc_setup, folded, opening)?; + if opened != proof.folded_eval_outputs[index] { + return Err(VerificationError::EvalWitnessMismatch { + kind: "output", + index, + }); + } + coordinate.require_dedicated_row(opening, "output", index)?; + append_vector_opening( + transcript, + b"bf_eval_out_open", + b"bf_eval_out_blind", + opening, + ); + } + + if let Some(coordinate) = coordinates.blinding { + let opening = blinding_openings + .next() + .ok_or(RelaxedError::LengthMismatch { + name: "folded eval blinding witness openings", + expected: expected_blindings, + actual: proof.folded_eval_blinding_openings.len(), + })?; + let opened = coordinate.verify_opening::(vc_setup, folded, opening)?; + if opened != proof.folded_eval_blindings[index] { + return Err(VerificationError::EvalWitnessMismatch { + kind: "blinding", + index, + }); + } + coordinate.require_dedicated_row(opening, "blinding", index)?; + append_vector_opening( + transcript, + b"bf_eval_blind_open", + b"bf_eval_blind_bl", + opening, + ); + } + } + + Ok(()) + } +} + +impl WitnessCoordinate { + fn require_dedicated_row( + self, + opening: &VectorCommitmentOpening, + kind: &'static str, + index: usize, + ) -> Result<(), VerificationError> { + for (slot, value) in opening.combined_vector.iter().enumerate() { + if slot != self.column && !value.is_zero() { + return Err(VerificationError::EvalWitnessRowNotDedicated { kind, index }); + } + } + Ok(()) + } + + fn verify_opening( + self, + vc_setup: &VC::Setup, + folded: &RelaxedInstance, + opening: &VectorCommitmentOpening, + ) -> Result> + where + F: Field, + VC: VectorCommitment, + VC::Output: Copy + HomomorphicCommitment, + ::Accumulator: RingAccumulator, + { + let witness_row_count = folded.witness_row_commitments.len(); + if witness_row_count == 0 || !witness_row_count.is_power_of_two() { + return Err(VerificationError::InvalidPowerOfTwo { + name: "witness row count", + value: witness_row_count, + }); + } + let row_vars = witness_row_count.trailing_zeros() as usize; + + let witness_row_len = opening.combined_vector.len(); + if witness_row_len == 0 || !witness_row_len.is_power_of_two() { + return Err(VerificationError::InvalidPowerOfTwo { + name: "witness row length", + value: witness_row_len, + }); + } + let entry_vars = witness_row_len.trailing_zeros() as usize; + let row_point = boolean_point::(self.row, row_vars)?; + let entry_point = boolean_point::(self.column, entry_vars)?; + Ok(VC::verify_committed_rows( + vc_setup, + &folded.witness_row_commitments, + &row_point, + &entry_point, + opening, + )?) + } +} + +impl BlindFoldProtocol +where + F: Field + AppendToTranscript, + Com: Copy + HomomorphicCommitment + AppendToTranscript, + ::Accumulator: RingAccumulator, +{ + fn verify_inner_folded_r1cs( + &self, + proof: &BlindFoldProof, + vc_setup: &VC::Setup, + folded: &RelaxedInstance, + outer: &OuterCheck, + transcript: &mut T, + ) -> Result<(), VerificationError> + where + VC: VectorCommitment, + T: Transcript, + { + let ra = transcript.challenge(); + let rb = transcript.challenge(); + let rc = transcript.challenge(); + let public = public_contributions(&self.r1cs, &outer.point, folded.u)?; + let claim = ra * (proof.az_rx - public.a) + + rb * (proof.bz_rx - public.b) + + rc * (proof.cz_rx - public.c); + + let witness_row_count = self.dimensions.witness.row_count; + if witness_row_count == 0 || !witness_row_count.is_power_of_two() { + return Err(VerificationError::InvalidPowerOfTwo { + name: "witness row count", + value: witness_row_count, + }); + } + let row_vars = witness_row_count.trailing_zeros() as usize; + + let witness_row_len = self.dimensions.witness.row_len; + if witness_row_len == 0 || !witness_row_len.is_power_of_two() { + return Err(VerificationError::InvalidPowerOfTwo { + name: "witness row length", + value: witness_row_len, + }); + } + let entry_vars = witness_row_len.trailing_zeros() as usize; + let num_vars = + row_vars + .checked_add(entry_vars) + .ok_or(VerificationError::InvalidPowerOfTwo { + name: "inner sumcheck dimension", + value: usize::MAX, + })?; + if num_vars == 0 { + return Err(VerificationError::DegenerateSumcheck { + name: "inner folded R1CS sumcheck", + }); + } + let inner_claim = SumcheckClaim::new(num_vars, INNER_SUMCHECK_DEGREE, claim); + let inner = proof + .inner_sumcheck + .verify( + &inner_claim, + BooleanHypercube, + INNER_SUMCHECK_LABEL, + transcript, + ) + .map_err(|source| VerificationError::InnerSumcheck { source })?; + + let (row_point, entry_point) = inner.point.split_at(row_vars); + let w_ry = VC::verify_committed_rows( + vc_setup, + &folded.witness_row_commitments, + row_point, + entry_point, + &proof.witness_opening, + )?; + + let l_w_at_ry = compute_l_w_at_ry(&self.r1cs, &outer.point, &inner.point, ra, rb, rc)?; + let expected = l_w_at_ry * w_ry; + if inner.value != expected { + return Err(VerificationError::InnerFinalClaimMismatch { + expected, + actual: inner.value, + }); + } + + append_vector_opening( + transcript, + b"bf_witness_opening", + b"bf_witness_blind", + &proof.witness_opening, + ); + + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct OuterCheck { + point: Vec, +} + +fn append_vector_opening( + transcript: &mut T, + row_label: &'static [u8], + blinding_label: &'static [u8], + opening: &VectorCommitmentOpening, +) where + F: AppendToTranscript, + T: Transcript, +{ + transcript.append_values(row_label, &opening.combined_vector); + transcript.append(&Label(blinding_label)); + opening.combined_blinding.append_to_transcript(transcript); +} + +fn public_contributions( + r1cs: &ConstraintMatrices, + rx: &[F], + u: F, +) -> Result, VerificationError> +where + F: Field, +{ + let eq_rx = EqPolynomial::::evals(rx, None); + Ok(r1cs.public_column_contributions(&eq_rx, 0, u)?) +} + +fn compute_l_w_at_ry( + r1cs: &ConstraintMatrices, + rx: &[F], + ry: &[F], + ra: F, + rb: F, + rc: F, +) -> Result> +where + F: Field, +{ + let eq_rx = EqPolynomial::::evals(rx, None); + let eq_ry = EqPolynomial::::evals(ry, None); + let w_len = power_of_two_len::("inner point dimension", ry.len())?; + Ok(r1cs.linear_form_bilinear_eval(&eq_rx, &eq_ry, 1, w_len, [ra, rb, rc])?) +} + +fn power_of_two_len(name: &'static str, num_vars: usize) -> Result> +where + F: FieldCore, +{ + if num_vars >= usize::BITS as usize { + return Err(VerificationError::InvalidPowerOfTwo { + name, + value: num_vars, + }); + } + Ok(1usize << num_vars) +} + +fn boolean_point(index: usize, num_vars: usize) -> Result, VerificationError> +where + F: Field, +{ + let len = power_of_two_len::("boolean point dimension", num_vars)?; + if index >= len { + return Err(VerificationError::InvalidPowerOfTwo { + name: "boolean point index", + value: index, + }); + } + Ok((0..num_vars) + .map(|bit| { + let shift = num_vars - bit - 1; + if ((index >> shift) & 1) == 1 { + F::one() + } else { + F::zero() + } + }) + .collect()) +} + +fn ensure_len(name: &'static str, expected: usize, actual: usize) -> Result<(), RelaxedError> { + if expected != actual { + return Err(RelaxedError::LengthMismatch { + name, + expected, + actual, + }); + } + Ok(()) +} + +#[cfg(test)] +#[expect(clippy::expect_used, reason = "tests should fail loudly")] +mod tests { + use super::*; + use crate::{ + r1cs::{FinalOpeningLayout, Layout}, + BlindFoldDimensions, RowDimensions, WitnessRowLayout, + }; + use jolt_crypto::{ + Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment, VectorOpeningError, + }; + use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_poly::CompressedPoly; + use jolt_r1cs::ConstraintMatrices; + use jolt_sumcheck::CompressedSumcheckProof; + use jolt_transcript::Blake2bTranscript; + + fn f(value: u64) -> Fr { + Fr::from_u64(value) + } + + fn setup() -> PedersenSetup { + let generator = Bn254::g1_generator(); + let message_generators = (1..=4).map(|i| generator.scalar_mul(&f(i))).collect(); + PedersenSetup::new(message_generators, generator.scalar_mul(&f(99))) + } + + fn commitment(setup: &PedersenSetup, value: u64) -> Bn254G1 { + Pedersen::::commit(setup, &[f(value)], &f(value + 1000)) + } + + fn commit_value(setup: &PedersenSetup, value: Fr, blinding: Fr) -> Bn254G1 { + Pedersen::::commit(setup, &[value], &blinding) + } + + fn identity() -> Bn254G1 { + ::identity() + } + + fn protocol(setup: &PedersenSetup) -> BlindFoldProtocol { + let _ = setup; + empty_protocol(Vec::new()) + } + + fn protocol_with_eval(setup: &PedersenSetup) -> BlindFoldProtocol { + empty_protocol(vec![commit_value(setup, f(7), f(70))]) + } + + fn empty_protocol(eval_commitments: Vec) -> BlindFoldProtocol { + BlindFoldProtocol { + sumcheck_consistency: Vec::new(), + committed_output_claims: Vec::new(), + r1cs: ConstraintMatrices::new(0, 1, Vec::new(), Vec::new(), Vec::new()), + layout: Layout { + witness_row_len: 1, + stages: Vec::new(), + final_openings: vec![ + FinalOpeningLayout { + evaluation: None, + blinding: None, + }; + eval_commitments.len() + ], + }, + dimensions: BlindFoldDimensions { + witness: RowDimensions { + row_len: 1, + row_count: 1, + }, + error: RowDimensions { + row_len: 1, + row_count: 1, + }, + witness_rows: WitnessRowLayout { + coefficients: 0..0, + auxiliary: 0..0, + output_claims: 0..0, + padding: 0..1, + }, + coefficient_rows: 0, + output_claim_rows: 0, + auxiliary_rows: 0, + coefficient_values: 0, + auxiliary_values: 0, + }, + eval_commitments, + } + } + + fn witness_protocol() -> BlindFoldProtocol { + BlindFoldProtocol { + sumcheck_consistency: Vec::new(), + committed_output_claims: Vec::new(), + r1cs: ConstraintMatrices::new( + 1, + 2, + vec![vec![(1, f(1))]], + vec![Vec::new()], + vec![Vec::new()], + ), + layout: Layout { + witness_row_len: 1, + stages: Vec::new(), + final_openings: Vec::new(), + }, + dimensions: BlindFoldDimensions { + witness: RowDimensions { + row_len: 1, + row_count: 1, + }, + error: RowDimensions { + row_len: 1, + row_count: 1, + }, + witness_rows: WitnessRowLayout { + coefficients: 0..0, + output_claims: 0..0, + auxiliary: 0..1, + padding: 1..1, + }, + coefficient_rows: 0, + output_claim_rows: 0, + auxiliary_rows: 1, + coefficient_values: 0, + auxiliary_values: 1, + }, + eval_commitments: Vec::new(), + } + } + + fn inner_round_protocol() -> BlindFoldProtocol { + let mut protocol = witness_protocol(); + protocol.dimensions.witness = RowDimensions { + row_len: 1, + row_count: 2, + }; + protocol.dimensions.error = RowDimensions { + row_len: 1, + row_count: 2, + }; + protocol.dimensions.witness_rows.auxiliary = 0..2; + protocol.dimensions.witness_rows.padding = 2..2; + protocol.dimensions.auxiliary_rows = 2; + protocol.dimensions.auxiliary_values = 2; + protocol + } + + fn add_zero_inner_round(proof: &mut BlindFoldProof) { + proof.inner_sumcheck.round_polynomials = vec![CompressedPoly::new(vec![f(0)])]; + } + + fn outer_round_protocol() -> BlindFoldProtocol { + let mut protocol = empty_protocol(Vec::new()); + protocol.dimensions.error = RowDimensions { + row_len: 1, + row_count: 2, + }; + protocol + } + + fn coefficient_row_protocol() -> BlindFoldProtocol { + let mut protocol = empty_protocol(Vec::new()); + protocol.dimensions.witness_rows = WitnessRowLayout { + coefficients: 0..1, + output_claims: 1..1, + auxiliary: 1..1, + padding: 1..1, + }; + protocol.dimensions.coefficient_rows = 1; + protocol.dimensions.coefficient_values = 1; + protocol + } + + fn opening(row_len: usize) -> VectorCommitmentOpening { + VectorCommitmentOpening { + combined_vector: vec![f(0); row_len], + combined_blinding: f(0), + } + } + + fn zero_outer_sumcheck( + protocol: &BlindFoldProtocol, + ) -> CompressedSumcheckProof { + let num_vars = protocol.dimensions.error.row_count.trailing_zeros() as usize + + protocol.dimensions.error.row_len.trailing_zeros() as usize; + CompressedSumcheckProof { + round_polynomials: vec![CompressedPoly::new(vec![f(0)]); num_vars], + } + } + + fn proof( + setup: &PedersenSetup, + protocol: &BlindFoldProtocol, + ) -> BlindFoldProof { + BlindFoldProof { + auxiliary_row_commitments: vec![ + commitment(setup, 41); + protocol.dimensions.auxiliary_rows + ], + random_round_commitments: vec![identity(); protocol.dimensions.coefficient_rows], + random_output_claim_row_commitments: vec![ + identity(); + protocol.dimensions.output_claim_rows + ], + random_auxiliary_row_commitments: vec![identity(); protocol.dimensions.auxiliary_rows], + random_error_row_commitments: vec![identity(); protocol.dimensions.error.row_count], + random_eval_commitments: vec![ + commit_value(setup, f(11), f(110)); + protocol.eval_commitments.len() + ], + random_u: f(3), + cross_term_error_row_commitments: vec![identity(); protocol.dimensions.error.row_count], + outer_sumcheck: zero_outer_sumcheck(protocol), + az_rx: f(0), + bz_rx: f(0), + cz_rx: f(0), + inner_sumcheck: CompressedSumcheckProof::default(), + witness_opening: opening(protocol.dimensions.witness.row_len), + error_opening: opening(protocol.dimensions.error.row_len), + folded_eval_outputs: vec![f(0); protocol.eval_commitments.len()], + folded_eval_blindings: vec![f(0); protocol.eval_commitments.len()], + folded_eval_output_openings: Vec::new(), + folded_eval_blinding_openings: Vec::new(), + } + } + + fn folding_challenge( + protocol: &BlindFoldProtocol, + proof: &BlindFoldProof, + ) -> Fr { + let committed = protocol + .committed_relaxed_instance(&proof.auxiliary_row_commitments) + .expect("committed instance builds"); + let random = protocol + .random_relaxed_instance( + &proof.random_round_commitments, + &proof.random_output_claim_row_commitments, + &proof.random_auxiliary_row_commitments, + &proof.random_error_row_commitments, + &proof.random_eval_commitments, + proof.random_u, + ) + .expect("random instance builds"); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + committed.append_to_transcript( + &mut transcript, + b"bf_committed_u", + b"bf_committed_w", + b"bf_committed_e", + b"bf_committed_eval", + ); + random.append_to_transcript( + &mut transcript, + b"bf_random_u", + b"bf_random_w", + b"bf_random_e", + b"bf_random_eval", + ); + transcript.append_values(b"bf_cross_e", &proof.cross_term_error_row_commitments); + transcript.challenge() + } + + fn proof_with_valid_eval_opening( + setup: &PedersenSetup, + protocol: &BlindFoldProtocol, + ) -> BlindFoldProof { + let mut proof = proof(setup, protocol); + let folding_challenge = folding_challenge(protocol, &proof); + proof.folded_eval_outputs = vec![f(7) + folding_challenge * f(11)]; + proof.folded_eval_blindings = vec![f(70) + folding_challenge * f(110)]; + proof + } + + #[test] + fn verify_rejects_degenerate_outer_sumcheck() { + let setup = setup(); + let protocol = protocol(&setup); + let proof = proof(&setup, &protocol); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + + let error = protocol + .verify::, _>(&proof, &setup, &mut transcript) + .expect_err("degenerate outer sumcheck is rejected"); + + assert!(matches!( + error, + VerificationError::DegenerateSumcheck { + name: "outer folded R1CS sumcheck" + } + )); + } + + #[test] + fn folded_instance_uses_transcript_derived_challenge() { + let setup = setup(); + let protocol = protocol(&setup); + let proof = proof(&setup, &protocol); + + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + let folded = protocol + .folded_instance_from_proof(&proof, &mut transcript) + .expect("fold inputs are well-shaped"); + + let committed = protocol + .committed_relaxed_instance(&proof.auxiliary_row_commitments) + .expect("committed instance builds"); + let random = protocol + .random_relaxed_instance( + &proof.random_round_commitments, + &proof.random_output_claim_row_commitments, + &proof.random_auxiliary_row_commitments, + &proof.random_error_row_commitments, + &proof.random_eval_commitments, + proof.random_u, + ) + .expect("random instance builds"); + let mut manual_transcript = Blake2bTranscript::::new(b"blindfold-verify"); + committed.append_to_transcript( + &mut manual_transcript, + b"bf_committed_u", + b"bf_committed_w", + b"bf_committed_e", + b"bf_committed_eval", + ); + random.append_to_transcript( + &mut manual_transcript, + b"bf_random_u", + b"bf_random_w", + b"bf_random_e", + b"bf_random_eval", + ); + manual_transcript.append_values(b"bf_cross_e", &proof.cross_term_error_row_commitments); + let folding_challenge = manual_transcript.challenge(); + let expected = committed + .fold( + &random, + &proof.cross_term_error_row_commitments, + folding_challenge, + ) + .expect("fold dimensions match"); + + assert_eq!(folded, expected); + assert_eq!(transcript.state(), manual_transcript.state()); + } + + #[test] + fn verify_rejects_random_round_count_mismatch() { + let setup = setup(); + let protocol = coefficient_row_protocol(); + let mut proof = proof(&setup, &protocol); + let _ = proof.random_round_commitments.pop(); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + + let error = protocol + .verify::, _>(&proof, &setup, &mut transcript) + .expect_err("random rows are missing"); + + assert!(matches!( + error, + VerificationError::Relaxed(RelaxedError::LengthMismatch { + name: "random round commitments", + .. + }) + )); + } + + #[test] + fn verify_rejects_folded_eval_output_count_mismatch() { + let setup = setup(); + let protocol = protocol(&setup); + let mut proof = proof(&setup, &protocol); + proof.folded_eval_outputs.push(f(7)); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + + let error = protocol + .verify::, _>(&proof, &setup, &mut transcript) + .expect_err("folded eval count differs"); + + assert_eq!( + error.to_string(), + "folded eval outputs length mismatch: expected 0, got 1" + ); + } + + #[test] + fn verify_accepts_folded_eval_commitment_opening() { + let setup = setup(); + let protocol = protocol_with_eval(&setup); + let proof = proof_with_valid_eval_opening(&setup, &protocol); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + let folded = protocol + .folded_instance_from_proof(&proof, &mut transcript) + .expect("folded instance builds"); + + proof + .verify_folded_eval_commitments::>(&setup, &folded) + .expect("folded eval commitment opens"); + } + + #[test] + fn verify_rejects_bad_folded_eval_commitment_opening() { + let setup = setup(); + let protocol = protocol_with_eval(&setup); + let mut proof = proof_with_valid_eval_opening(&setup, &protocol); + proof.folded_eval_outputs[0] += f(1); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + + let error = protocol + .verify::, _>(&proof, &setup, &mut transcript) + .expect_err("folded eval commitment opening is wrong"); + + assert!(matches!( + error, + VerificationError::EvalCommitmentMismatch { index: 0 } + )); + } + + #[test] + fn verify_rejects_outer_sumcheck_round_count_mismatch() { + let setup = setup(); + let protocol = outer_round_protocol(); + let mut proof = proof(&setup, &protocol); + let _ = proof.outer_sumcheck.round_polynomials.pop(); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + + let error = protocol + .verify::, _>(&proof, &setup, &mut transcript) + .expect_err("outer sumcheck has wrong length"); + + assert!(matches!( + error, + VerificationError::OuterSumcheck { + source: jolt_sumcheck::SumcheckError::WrongNumberOfRounds { .. }, + } + )); + } + + #[test] + fn verify_rejects_outer_sumcheck_degree_bound() { + let setup = setup(); + let protocol = outer_round_protocol(); + let mut proof = proof(&setup, &protocol); + proof.outer_sumcheck.round_polynomials[0] = + CompressedPoly::new(vec![f(0), f(0), f(0), f(0)]); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + + let error = protocol + .verify::, _>(&proof, &setup, &mut transcript) + .expect_err("outer sumcheck degree is too high"); + + assert!(matches!( + error, + VerificationError::OuterSumcheck { + source: jolt_sumcheck::SumcheckError::DegreeBoundExceeded { got: 4, max: 3 }, + } + )); + } + + #[test] + fn verify_rejects_bad_error_opening() { + let setup = setup(); + let protocol = outer_round_protocol(); + let mut proof = proof(&setup, &protocol); + proof.error_opening.combined_blinding = f(1); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + + let error = protocol + .verify::, _>(&proof, &setup, &mut transcript) + .expect_err("error opening is not binding to folded rows"); + + assert!(matches!( + error, + VerificationError::VectorOpening(VectorOpeningError::CommitmentMismatch) + )); + } + + #[test] + fn verify_rejects_outer_final_claim_mismatch() { + let setup = setup(); + let protocol = outer_round_protocol(); + let mut proof = proof(&setup, &protocol); + proof.az_rx = f(1); + proof.bz_rx = f(1); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + + let error = protocol + .verify::, _>(&proof, &setup, &mut transcript) + .expect_err("outer final claim does not match opened error row"); + + assert!(matches!( + error, + VerificationError::OuterFinalClaimMismatch { .. } + )); + } + + #[test] + fn verify_rejects_inner_sumcheck_round_count_mismatch() { + let setup = setup(); + let protocol = inner_round_protocol(); + let proof = proof(&setup, &protocol); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + + let error = protocol + .verify::, _>(&proof, &setup, &mut transcript) + .expect_err("inner sumcheck has wrong length"); + + assert!(matches!( + error, + VerificationError::InnerSumcheck { + source: jolt_sumcheck::SumcheckError::WrongNumberOfRounds { + expected: 1, + got: 0, + }, + } + )); + } + + #[test] + fn verify_rejects_bad_witness_opening() { + let setup = setup(); + let protocol = inner_round_protocol(); + let mut proof = proof(&setup, &protocol); + add_zero_inner_round(&mut proof); + proof.witness_opening.combined_blinding = f(1); + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + + let error = protocol + .verify::, _>(&proof, &setup, &mut transcript) + .expect_err("witness opening is not binding to folded rows"); + + assert!(matches!( + error, + VerificationError::VectorOpening(VectorOpeningError::CommitmentMismatch) + )); + } + + #[test] + fn verify_rejects_inner_final_claim_mismatch() { + let setup = setup(); + let protocol = inner_round_protocol(); + let mut proof = proof(&setup, &protocol); + add_zero_inner_round(&mut proof); + proof.auxiliary_row_commitments = vec![ + commit_value(&setup, f(5), f(50)), + commit_value(&setup, f(5), f(50)), + ]; + proof.witness_opening = VectorCommitmentOpening { + combined_vector: vec![f(5)], + combined_blinding: f(50), + }; + let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + + let error = protocol + .verify::, _>(&proof, &setup, &mut transcript) + .expect_err("inner final claim does not match opened witness row"); + + assert!(matches!( + error, + VerificationError::InnerFinalClaimMismatch { .. } + )); + } +} diff --git a/crates/jolt-blindfold/tests/jolt_claims_pipeline.rs b/crates/jolt-blindfold/tests/jolt_claims_pipeline.rs new file mode 100644 index 0000000000..f99de295d4 --- /dev/null +++ b/crates/jolt-blindfold/tests/jolt_claims_pipeline.rs @@ -0,0 +1,296 @@ +#![expect(clippy::expect_used, reason = "integration tests should fail loudly")] + +mod support; + +use jolt_blindfold::{BlindFoldStage, BlindFoldStatement, CommittedClaimRows}; +use jolt_claims::protocols::jolt::{ + formulas::{ + booleanity::{booleanity, BooleanityDimensions}, + claim_reductions::increments, + ra::JoltRaPolynomialLayout, + ram::{self, RamValCheckAdviceContribution, RamValCheckInit}, + registers, + }, + JoltChallengeId, JoltExpr, JoltOpeningId, JoltPublicId, JoltRelationClaims, + ReadWriteDimensions, TraceDimensions, +}; +use jolt_r1cs::{ClaimSourceTable, R1csBuilder}; +use jolt_sumcheck::{SumcheckDomainSpec, SumcheckStatement}; +use rand_chacha::ChaCha20Rng; +use rand_core::SeedableRng; +use support::*; + +#[derive(Clone, Debug)] +struct JoltSourceValues { + openings: Vec<(JoltOpeningId, F)>, + publics: Vec<(JoltPublicId, F)>, + challenges: Vec<(JoltChallengeId, F)>, +} + +impl JoltSourceValues { + fn seeded(stage: &JoltRelationClaims, seed: u64) -> Self { + let openings = stage + .required_openings() + .into_iter() + .enumerate() + .map(|(index, id)| (id, f(seed + 11 + index as u64))) + .collect(); + let publics = stage + .required_publics() + .into_iter() + .enumerate() + .map(|(index, id)| (id, f(seed + 101 + index as u64))) + .collect(); + let challenges = stage + .required_challenges() + .into_iter() + .enumerate() + .map(|(index, id)| (id, f(seed + 211 + index as u64))) + .collect(); + Self { + openings, + publics, + challenges, + } + } + + fn opening(&self, id: &JoltOpeningId) -> F { + self.openings + .iter() + .find_map(|(candidate, value)| (candidate == id).then_some(*value)) + .expect("opening exists") + } + + fn public(&self, id: &JoltPublicId) -> F { + self.publics + .iter() + .find_map(|(candidate, value)| (candidate == id).then_some(*value)) + .expect("public exists") + } + + fn challenge(&self, id: &JoltChallengeId) -> F { + self.challenges + .iter() + .find_map(|(candidate, value)| (candidate == id).then_some(*value)) + .expect("challenge exists") + } + + fn set_opening(&mut self, id: JoltOpeningId, value: F) { + let (_, existing) = self + .openings + .iter_mut() + .find(|(candidate, _)| *candidate == id) + .expect("opening exists"); + *existing = value; + } + + fn evaluate(&self, expression: &JoltExpr) -> F { + expression.evaluate( + |id| self.opening(id), + |id| self.challenge(id), + |id| self.public(id), + ) + } +} + +fn solve_linear_output_opening( + stage: &JoltRelationClaims, + values: &mut JoltSourceValues, + target: F, +) -> JoltOpeningId { + stage + .output + .required_openings + .iter() + .find_map(|candidate| { + if stage.input.required_openings.contains(candidate) { + return None; + } + let original = values.opening(candidate); + values.set_opening(*candidate, f(0)); + let base = values.evaluate(stage.output.expression()); + values.set_opening(*candidate, f(1)); + let shifted = values.evaluate(stage.output.expression()); + let delta = shifted - base; + values.set_opening(*candidate, original); + if delta != f(0) { + let solved = (target - base) * inverse(delta); + values.set_opening(*candidate, solved); + Some(*candidate) + } else { + None + } + }) + .expect("stage output has a linearly solvable opening") +} + +fn build_jolt_stage_relation( + stage: &JoltRelationClaims, + generated: &GeneratedStage, + values: &JoltSourceValues, +) -> Result<(), usize> { + let statement = generated.statement; + let statement = BlindFoldStatement::new( + vec![BlindFoldStage::new( + "jolt-stage", + statement, + SumcheckDomainSpec::BooleanHypercube, + stage_consistency(statement, &generated.proof), + CommittedClaimRows::new( + Vec::new(), + statement.degree + 1, + generated.proof.output_claims.clone(), + ), + stage.input.expression().clone(), + stage.output.expression().clone(), + )], + Vec::new(), + ); + + let mut builder = R1csBuilder::::new(); + let mut sources = ClaimSourceTable::::new(); + for &(id, value) in &values.openings { + sources.insert_opening(id, builder.alloc(value)); + } + for &(id, value) in &values.challenges { + sources.insert_challenge(id, value); + } + for &(id, value) in &values.publics { + sources.insert_public(id, value); + } + + let layout = statement + .allocate_layout(&mut builder) + .expect("layout allocates"); + statement + .append(&mut builder, &layout, &mut sources) + .expect("constraints append"); + assign_generated_stage(&mut builder, &layout.stages[0].sumcheck, generated); + + let witness = builder.witness().expect("all witnesses assigned"); + builder.into_matrices().check_witness(&witness) +} + +fn generated_jolt_stage( + stage: &JoltRelationClaims, + seed: u64, +) -> (GeneratedStage, JoltSourceValues, JoltOpeningId) { + let setup = pedersen_setup(stage.sumcheck.degree + 1); + let statement = SumcheckStatement::new(stage.sumcheck.rounds, stage.sumcheck.degree); + let mut values = JoltSourceValues::seeded(stage, seed); + let input_claim = values.evaluate(stage.input.expression()); + let mut prover = SumcheckTestProver::new(ChaCha20Rng::from_seed([seed as u8; 32])); + let generated = prover.prove_stage_with_fresh_transcript( + &setup, + b"blindfold-r1cs-e2e", + statement, + input_claim, + ); + let final_claim = *generated + .claim_outs + .last() + .expect("stage has at least one round"); + let solved = solve_linear_output_opening(stage, &mut values, final_claim); + (generated, values, solved) +} + +#[test] +fn jolt_claims_pipeline_lowers_registers_read_write_relation() { + let stage = registers::read_write_checking::(ReadWriteDimensions::new(2, 2, 1, 1)); + let (generated, values, _) = generated_jolt_stage(&stage, 5); + + assert!(build_jolt_stage_relation(&stage, &generated, &values).is_ok()); +} + +#[test] +fn jolt_claims_pipeline_lowers_ram_val_check_with_decomposed_advice() { + let init = RamValCheckInit::decomposed( + f(17), + [ + RamValCheckAdviceContribution::trusted(f(3)), + RamValCheckAdviceContribution::untrusted(f(7)), + ], + ); + let stage = ram::val_check::(TraceDimensions::new(3), init); + let (generated, values, _) = generated_jolt_stage(&stage, 7); + + assert!(build_jolt_stage_relation(&stage, &generated, &values).is_ok()); +} + +#[test] +fn jolt_claims_pipeline_lowers_increment_claim_reduction() { + let stage = increments::claim_reduction::(TraceDimensions::new(3)); + let (generated, values, _) = generated_jolt_stage(&stage, 9); + + assert!(build_jolt_stage_relation(&stage, &generated, &values).is_ok()); +} + +#[test] +fn jolt_claims_pipeline_rejects_tampered_public_source() { + let stage = increments::claim_reduction::(TraceDimensions::new(3)); + let (generated, mut values, _) = generated_jolt_stage(&stage, 11); + values.publics[0].1 += f(1); + + assert!(build_jolt_stage_relation(&stage, &generated, &values).is_err()); +} + +#[test] +fn jolt_claims_pipeline_rejects_tampered_opening_source() { + let stage = registers::read_write_checking::(ReadWriteDimensions::new(2, 2, 1, 1)); + let (generated, mut values, solved) = generated_jolt_stage(&stage, 13); + values.set_opening(solved, values.opening(&solved) + f(1)); + + assert!(build_jolt_stage_relation(&stage, &generated, &values).is_err()); +} + +#[test] +fn jolt_claims_pipeline_lowers_booleanity_relation() { + let setup = pedersen_setup(1); + let layout = JoltRaPolynomialLayout::new(1, 1, 1).expect("valid RA layout"); + let jolt_stage = booleanity::(BooleanityDimensions { + layout, + log_t: 1, + log_k_chunk: 1, + }); + let statement = SumcheckStatement::new(jolt_stage.sumcheck.rounds, jolt_stage.sumcheck.degree); + let generated = generate_zero_stage(&setup, statement.num_vars); + let statement = BlindFoldStatement::new( + vec![BlindFoldStage::new( + "jolt-booleanity", + statement, + SumcheckDomainSpec::BooleanHypercube, + stage_consistency(statement, &generated.proof), + CommittedClaimRows::new( + Vec::new(), + statement.degree + 1, + generated.proof.output_claims.clone(), + ), + jolt_stage.input.expression().clone(), + jolt_stage.output.expression().clone(), + )], + Vec::new(), + ); + + let mut builder = R1csBuilder::::new(); + let mut sources = ClaimSourceTable::::new(); + for opening_id in jolt_stage.required_openings() { + sources.insert_opening(opening_id, builder.alloc(f(0))); + } + for challenge_id in jolt_stage.required_challenges() { + sources.insert_challenge(challenge_id, f(7)); + } + for public_id in jolt_stage.required_publics() { + sources.insert_public(public_id, f(11)); + } + + let r1cs_layout = statement + .allocate_layout(&mut builder) + .expect("layout allocates"); + statement + .append(&mut builder, &r1cs_layout, &mut sources) + .expect("constraints append"); + assign_generated_stage(&mut builder, &r1cs_layout.stages[0].sumcheck, &generated); + + let witness = builder.witness().expect("all witnesses assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); +} diff --git a/crates/jolt-blindfold/tests/proof_pipeline.rs b/crates/jolt-blindfold/tests/proof_pipeline.rs new file mode 100644 index 0000000000..af1e3e6058 --- /dev/null +++ b/crates/jolt-blindfold/tests/proof_pipeline.rs @@ -0,0 +1,278 @@ +#![expect(clippy::expect_used, reason = "integration tests should fail loudly")] + +mod support; + +use jolt_blindfold::VerificationError; +use jolt_poly::CompressedPoly; +use jolt_transcript::{Blake2bTranscript, Transcript}; +use rand_chacha::ChaCha20Rng; +use rand_core::SeedableRng; +use support::*; + +fn verify_blindfold_protocol_pipeline( + full: &BlindFoldTestProof, +) -> Result<(), VerificationError> { + let mut transcript = Blake2bTranscript::::new(b"protocol-backed-blindfold-proof"); + append_protocol_transcript_prefix(&full.protocol, &mut transcript); + full.protocol + .verify::(&full.proof, &full.setup, &mut transcript) +} + +#[test] +fn blindfold_protocol_pipeline_verifies_committed_sumcheck_outputs_and_eval_commitments() { + let mut rng = ChaCha20Rng::from_seed([81; 32]); + let full = prove_blindfold_protocol_pipeline(&mut rng); + + assert!(full.protocol.dimensions.coefficient_rows > 0); + assert!(full.protocol.dimensions.output_claim_rows > 0); + assert!(full.protocol.dimensions.auxiliary_rows > 0); + assert!(!full.protocol.eval_commitments.is_empty()); + verify_blindfold_protocol_pipeline(&full).expect("protocol-backed BlindFold proof verifies"); +} + +#[test] +fn blindfold_protocol_pipeline_randomness_is_empirically_independent() { + const SAMPLES: usize = 128; + let mut rng = ChaCha20Rng::from_seed([61; 32]); + let mut projections = [ + StatisticalProjection::new("random_u", SAMPLES), + StatisticalProjection::new("auxiliary_commitment", SAMPLES), + StatisticalProjection::new("random_round_commitment", SAMPLES), + StatisticalProjection::new("random_error_commitment", SAMPLES), + StatisticalProjection::new("cross_term_commitment", SAMPLES), + StatisticalProjection::new("outer_sumcheck", SAMPLES), + StatisticalProjection::new("inner_sumcheck", SAMPLES), + StatisticalProjection::new("witness_opening", SAMPLES), + StatisticalProjection::new("error_opening", SAMPLES), + ]; + + for _ in 0..SAMPLES { + let full = prove_blindfold_protocol_pipeline(&mut rng); + + verify_blindfold_protocol_pipeline(&full).expect("sample proof verifies"); + + let values = [ + field_low_u64(full.proof.random_u), + transcript_projection( + b"auxiliary_commitment", + &full.proof.auxiliary_row_commitments[0], + ), + transcript_projection( + b"random_round_commitment", + &full.proof.random_round_commitments[0], + ), + transcript_projection( + b"random_error_commitment", + &full.proof.random_error_row_commitments[0], + ), + transcript_projection( + b"cross_term_commitment", + &full.proof.cross_term_error_row_commitments[0], + ), + compressed_sumcheck_projection(b"outer_sumcheck", &full.proof.outer_sumcheck), + compressed_sumcheck_projection(b"inner_sumcheck", &full.proof.inner_sumcheck), + opening_projection(b"witness_opening", &full.proof.witness_opening), + opening_projection(b"error_opening", &full.proof.error_opening), + ]; + + for (projection, value) in projections.iter_mut().zip(values) { + projection.push(value); + } + } + + for projection in &projections { + assert_empirical_distribution(projection); + } + assert_empirical_pairwise_independence(&projections[0], &projections[1]); + assert_empirical_pairwise_independence(&projections[0], &projections[7]); + assert_empirical_pairwise_independence(&projections[1], &projections[2]); + assert_empirical_pairwise_independence(&projections[3], &projections[4]); + assert_empirical_pairwise_independence(&projections[5], &projections[6]); + assert_empirical_pairwise_independence(&projections[7], &projections[8]); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_random_u() { + let mut rng = ChaCha20Rng::from_seed([71; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + full.proof.random_u += f(1); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_outer_sumcheck() { + let mut rng = ChaCha20Rng::from_seed([72; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + let mut coefficients = full.proof.outer_sumcheck.round_polynomials[0] + .coeffs_except_linear_term() + .to_vec(); + coefficients[0] += f(1); + full.proof.outer_sumcheck.round_polynomials[0] = CompressedPoly::new(coefficients); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_folded_matrix_eval() { + let mut rng = ChaCha20Rng::from_seed([73; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + full.proof.az_rx += f(1); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_witness_opening() { + let mut rng = ChaCha20Rng::from_seed([74; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + full.proof.witness_opening.combined_vector[0] += f(1); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_error_opening_blinding() { + let mut rng = ChaCha20Rng::from_seed([75; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + full.proof.error_opening.combined_blinding += f(1); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_wrong_transcript() { + let mut rng = ChaCha20Rng::from_seed([76; 32]); + let full = prove_blindfold_protocol_pipeline(&mut rng); + let mut transcript = Blake2bTranscript::::new(b"wrong-transcript"); + append_protocol_transcript_prefix(&full.protocol, &mut transcript); + + assert!(full + .protocol + .verify::(&full.proof, &full.setup, &mut transcript) + .is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_random_commitment_row() { + let mut rng = ChaCha20Rng::from_seed([77; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + full.proof.random_round_commitments.swap(0, 1); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_inner_sumcheck() { + let mut rng = ChaCha20Rng::from_seed([78; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + let mut coefficients = full.proof.inner_sumcheck.round_polynomials[1] + .coeffs_except_linear_term() + .to_vec(); + coefficients[0] += f(1); + full.proof.inner_sumcheck.round_polynomials[1] = CompressedPoly::new(coefficients); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_truncated_error_rows_before_opening_checks() { + let mut rng = ChaCha20Rng::from_seed([79; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + let _ = full.proof.random_error_row_commitments.pop(); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_folded_eval_opening() { + let mut rng = ChaCha20Rng::from_seed([82; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + full.proof.folded_eval_outputs[0] += f(1); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_folded_eval_blinding() { + let mut rng = ChaCha20Rng::from_seed([87; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + full.proof.folded_eval_blindings[0] += f(1); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_folded_eval_witness_opening() { + let mut rng = ChaCha20Rng::from_seed([85; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + full.proof.folded_eval_output_openings[0].combined_vector[0] += f(1); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_folded_eval_blinding_witness_opening() { + let mut rng = ChaCha20Rng::from_seed([86; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + full.proof.folded_eval_blinding_openings[0].combined_vector[0] += f(1); + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_final_eval_openings_use_dedicated_rows() { + let mut rng = ChaCha20Rng::from_seed([88; 32]); + let full = prove_blindfold_protocol_pipeline(&mut rng); + let coordinates = full + .protocol + .final_opening_witness_coordinates() + .expect("final opening coordinates are valid"); + let eval = coordinates[0] + .evaluation + .expect("final opening has an evaluation coordinate"); + let blinding = coordinates[0] + .blinding + .expect("final opening has a blinding coordinate"); + + assert_eq!(eval.column, 0); + assert_eq!(blinding.column, 0); + assert_ne!(eval.row, blinding.row); + assert_eq!( + full.proof.folded_eval_output_openings[0].combined_vector[0], + full.proof.folded_eval_outputs[0] + ); + assert_eq!( + full.proof.folded_eval_blinding_openings[0].combined_vector[0], + full.proof.folded_eval_blindings[0] + ); + assert!( + full.proof.folded_eval_output_openings[0].combined_vector[1..] + .iter() + .all(|value| *value == f(0)) + ); + assert!( + full.proof.folded_eval_blinding_openings[0].combined_vector[1..] + .iter() + .all(|value| *value == f(0)) + ); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_auxiliary_commitment() { + let mut rng = ChaCha20Rng::from_seed([83; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + full.proof.auxiliary_row_commitments[0] = full.proof.random_round_commitments[0]; + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} + +#[test] +fn blindfold_protocol_pipeline_rejects_tampered_output_claim_row_commitment() { + let mut rng = ChaCha20Rng::from_seed([84; 32]); + let mut full = prove_blindfold_protocol_pipeline(&mut rng); + full.protocol.committed_output_claims[0].commitments[0] = + full.proof.random_round_commitments[0]; + + assert!(verify_blindfold_protocol_pipeline(&full).is_err()); +} diff --git a/crates/jolt-blindfold/tests/r1cs_pipeline.rs b/crates/jolt-blindfold/tests/r1cs_pipeline.rs new file mode 100644 index 0000000000..328da211c8 --- /dev/null +++ b/crates/jolt-blindfold/tests/r1cs_pipeline.rs @@ -0,0 +1,23 @@ +mod support; + +use rand_chacha::ChaCha20Rng; +use rand_core::SeedableRng; +use support::*; + +#[test] +fn r1cs_pipeline_satisfies_three_stage_claim_chain() { + let mut prover = SumcheckTestProver::new(ChaCha20Rng::from_seed([5; 32])); + let (stage1, stage2, stage3, values) = generated_deep_triple(&mut prover); + + assert!(build_deep_relation(&stage1, &stage2, &stage3, &values).is_ok()); +} + +#[test] +fn r1cs_pipeline_rejects_late_stage_mismatch() { + let mut prover = SumcheckTestProver::new(ChaCha20Rng::from_seed([6; 32])); + let (stage1, stage2, stage3, values) = generated_deep_triple(&mut prover); + let mut tampered_stage3 = stage3.clone(); + tampered_stage3.proof = stage2.proof.clone(); + + assert!(build_deep_relation(&stage1, &stage2, &tampered_stage3, &values).is_err()); +} diff --git a/crates/jolt-blindfold/tests/sumcheck_pipeline.rs b/crates/jolt-blindfold/tests/sumcheck_pipeline.rs new file mode 100644 index 0000000000..5baa60f38f --- /dev/null +++ b/crates/jolt-blindfold/tests/sumcheck_pipeline.rs @@ -0,0 +1,112 @@ +#![expect(clippy::expect_used, reason = "integration tests should fail loudly")] + +mod support; + +use jolt_crypto::VectorCommitment; +use jolt_sumcheck::RoundMessage; +use jolt_transcript::{Blake2bTranscript, Transcript}; +use rand_chacha::ChaCha20Rng; +use rand_core::SeedableRng; +use support::*; + +#[test] +fn committed_sumcheck_pipeline_satisfies_deep_r1cs_and_randomness_checks() { + const SAMPLES: usize = 256; + let mut rng = ChaCha20Rng::from_seed([21; 32]); + let mut projections = [ + StatisticalProjection::new("stage1_round_commitment", SAMPLES), + StatisticalProjection::new("stage2_round_commitment", SAMPLES), + StatisticalProjection::new("stage3_round_commitment", SAMPLES), + StatisticalProjection::new("stage1_round_blinding", SAMPLES), + StatisticalProjection::new("stage2_round_blinding", SAMPLES), + StatisticalProjection::new("stage3_round_blinding", SAMPLES), + StatisticalProjection::new("stage1_output_claim", SAMPLES), + StatisticalProjection::new("stage2_output_claim", SAMPLES), + StatisticalProjection::new("stage3_output_claim", SAMPLES), + ]; + + for _ in 0..SAMPLES { + let mut prover = SumcheckTestProver::new(&mut rng); + let (stage1, stage2, stage3, values) = generated_deep_triple(&mut prover); + + assert!(build_deep_relation(&stage1, &stage2, &stage3, &values).is_ok()); + let sample = [ + transcript_projection( + b"stage1_round_commitment", + &stage1.proof.rounds[0].commitment, + ), + transcript_projection( + b"stage2_round_commitment", + &stage2.proof.rounds[0].commitment, + ), + transcript_projection( + b"stage3_round_commitment", + &stage3.proof.rounds[0].commitment, + ), + field_low_u64(stage1.blindings[0]), + field_low_u64(stage2.blindings[0]), + field_low_u64(stage3.blindings[0]), + field_low_u64(*stage1.claim_outs.last().expect("stage 1 output exists")), + field_low_u64(*stage2.claim_outs.last().expect("stage 2 output exists")), + field_low_u64(*stage3.claim_outs.last().expect("stage 3 output exists")), + ]; + + for (projection, value) in projections.iter_mut().zip(sample) { + projection.push(value); + } + } + + for projection in &projections { + assert_empirical_distribution(projection); + } + assert_empirical_pairwise_independence(&projections[0], &projections[3]); + assert_empirical_pairwise_independence(&projections[1], &projections[4]); + assert_empirical_pairwise_independence(&projections[2], &projections[5]); + assert_empirical_pairwise_independence(&projections[3], &projections[6]); + assert_empirical_pairwise_independence(&projections[4], &projections[7]); + assert_empirical_pairwise_independence(&projections[5], &projections[8]); +} + +#[test] +fn committed_round_blinding_is_empirically_independent_from_commitments_and_challenges() { + const SAMPLES: usize = 512; + let setup = pedersen_setup(4); + let coefficients = vec![f(34), f(55), f(89), f(144)]; + let mut rng = ChaCha20Rng::from_seed([41; 32]); + let mut blindings = StatisticalProjection::new("round_blinding", SAMPLES); + let mut commitments = StatisticalProjection::new("round_commitment", SAMPLES); + let mut challenges = StatisticalProjection::new("round_transcript_challenge", SAMPLES); + + for _ in 0..SAMPLES { + let blinding = rng_field(&mut rng); + let round = commit_round_with_blinding(&setup, coefficients.clone(), blinding); + let mut transcript = Blake2bTranscript::::new(b"blindfold-r1cs-independence"); + round.append_to_transcript(&mut transcript); + let challenge = transcript.challenge(); + + assert!(VC::verify( + &setup, + &round.commitment, + &coefficients, + &blinding + )); + assert!(!VC::verify( + &setup, + &round.commitment, + &coefficients, + &(blinding + f(1)) + )); + blindings.push(field_low_u64(blinding)); + commitments.push(transcript_projection( + b"round_commitment", + &round.commitment, + )); + challenges.push(field_low_u64(challenge)); + } + + assert_empirical_distribution(&blindings); + assert_empirical_distribution(&commitments); + assert_empirical_distribution(&challenges); + assert_empirical_pairwise_independence(&blindings, &commitments); + assert_empirical_pairwise_independence(&commitments, &challenges); +} diff --git a/crates/jolt-blindfold/tests/support/mod.rs b/crates/jolt-blindfold/tests/support/mod.rs new file mode 100644 index 0000000000..a7a0886951 --- /dev/null +++ b/crates/jolt-blindfold/tests/support/mod.rs @@ -0,0 +1,1742 @@ +#![expect( + dead_code, + clippy::expect_used, + reason = "shared integration-test harness is intentionally broader than each test file" +)] + +use jolt_blindfold::{ + BlindFoldProof, BlindFoldProtocol, BlindFoldStage, BlindFoldStatement, CommittedClaimRows, + FinalOpeningBinding, WitnessCoordinate, +}; +use jolt_claims::{challenge, constant, opening, public, Expr}; +use jolt_crypto::{ + Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment, VectorCommitmentOpening, +}; +use jolt_field::{FixedBytes, Fr, FromPrimitiveInt, Invertible}; +use jolt_poly::{CompressedPoly, EqPolynomial}; +use jolt_r1cs::{ClaimSourceTable, ConstraintMatrices, R1csBuilder}; +use jolt_sumcheck::{ + CommittedOutputClaims, CommittedRound, CommittedRoundWitness, CommittedSumcheckConsistency, + CommittedSumcheckProof, CompressedSumcheckProof, RoundMessage, SumcheckDomainSpec, + SumcheckR1csLayout, SumcheckStatement, SUMCHECK_ROUND_TRANSCRIPT_LABEL, +}; +use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Label, Transcript}; +use rand_core::RngCore; + +pub type F = Fr; +pub type VC = Pedersen; +pub type TestExpr = Expr; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Opening { + Start, + Final, + Aux, + Link, + Mid, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Public { + Offset, + Multiplier, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Challenge { + Scale, + Bias, + Mix, +} + +#[derive(Clone, Debug)] +pub struct GeneratedStage { + pub statement: SumcheckStatement, + pub proof: CommittedSumcheckProof, + pub coefficients: Vec>, + pub blindings: Vec, + pub output_claim_rows: Vec>, + pub output_claim_blindings: Vec, + pub input_claim: F, + pub claim_outs: Vec, +} + +#[derive(Clone, Debug)] +pub struct DeepValues { + pub start: F, + pub aux: F, + pub link: F, + pub mid: F, + pub final_value: F, + pub scale: F, + pub bias: F, + pub mix: F, + pub offset: F, + pub multiplier: F, +} + +#[derive(Clone, Debug)] +pub struct TestStageRelation { + pub name: String, + pub statement: SumcheckStatement, + pub domain: SumcheckDomainSpec, + pub input_claim: Expr, + pub output_claim: Expr, +} + +impl TestStageRelation { + pub fn new( + name: impl Into, + statement: SumcheckStatement, + input_claim: Expr, + output_claim: Expr, + ) -> Self { + Self { + name: name.into(), + statement, + domain: SumcheckDomainSpec::BooleanHypercube, + input_claim, + output_claim, + } + } +} + +pub fn f(value: u64) -> F { + F::from_u64(value) +} + +pub fn rng_field(rng: &mut impl RngCore) -> F { + let mut bytes = [0u8; 32]; + rng.fill_bytes(&mut bytes); + F::from_bytes_array(&bytes) +} + +pub fn inverse(value: F) -> F { + value.inverse().expect("test values are nonzero") +} + +pub fn eval_poly(coefficients: &[F], point: F) -> F { + let mut result = f(0); + let mut power = f(1); + for coefficient in coefficients { + result += *coefficient * power; + power *= point; + } + result +} + +#[derive(Clone, Debug)] +pub struct StatisticalProjection { + pub label: &'static str, + pub values: Vec, +} + +impl StatisticalProjection { + pub fn new(label: &'static str, capacity: usize) -> Self { + Self { + label, + values: Vec::with_capacity(capacity), + } + } + + pub fn push(&mut self, value: u64) { + self.values.push(value); + } +} + +pub fn field_low_u64(value: F) -> u64 { + let bytes = value.to_bytes_array(); + u64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]) +} + +pub fn transcript_projection(label: &'static [u8], value: &A) -> u64 { + let mut transcript = Blake2bTranscript::::new(b"blindfold-statistical-projection"); + transcript.append(&Label(label)); + value.append_to_transcript(&mut transcript); + field_low_u64(transcript.challenge()) +} + +pub fn field_slice_projection(label: &'static [u8], values: &[F]) -> u64 { + let mut transcript = Blake2bTranscript::::new(b"blindfold-statistical-projection"); + transcript.append_values(label, values); + field_low_u64(transcript.challenge()) +} + +pub fn compressed_sumcheck_projection( + label: &'static [u8], + proof: &CompressedSumcheckProof, +) -> u64 { + let mut values = Vec::new(); + for round in &proof.round_polynomials { + values.extend_from_slice(round.coeffs_except_linear_term()); + } + field_slice_projection(label, &values) +} + +pub fn opening_projection( + label: &'static [u8], + opening: &jolt_crypto::VectorCommitmentOpening, +) -> u64 { + let mut values = opening.combined_vector.clone(); + values.push(opening.combined_blinding); + field_slice_projection(label, &values) +} + +pub fn assert_empirical_distribution(projection: &StatisticalProjection) { + assert!( + projection.values.len() >= 128, + "{} needs enough samples for empirical checks", + projection.label + ); + assert_high_unique_ratio(projection); + assert_low_bit_balance(projection); + assert_low_bucket_chi_square(projection); + assert_lag_one_correlation(projection); + assert_runs_around_median(projection); +} + +pub fn assert_empirical_pairwise_independence( + lhs: &StatisticalProjection, + rhs: &StatisticalProjection, +) { + let correlation = pearson_correlation(&lhs.values, &rhs.values); + assert!( + correlation.abs() < 0.25, + "{} and {} have suspicious pairwise correlation: {correlation}", + lhs.label, + rhs.label + ); +} + +fn assert_high_unique_ratio(projection: &StatisticalProjection) { + let mut sorted = projection.values.clone(); + sorted.sort_unstable(); + sorted.dedup(); + let minimum_unique = projection.values.len() * 99 / 100; + assert!( + sorted.len() >= minimum_unique, + "{} reused too many projected samples: {} unique out of {}", + projection.label, + sorted.len(), + projection.values.len() + ); +} + +fn assert_low_bit_balance(projection: &StatisticalProjection) { + let ones = projection + .values + .iter() + .map(|value| value.count_ones() as u64) + .sum::(); + let bit_count = (projection.values.len() * u64::BITS as usize) as f64; + let expected = bit_count / 2.0; + let sigma = (bit_count / 4.0).sqrt(); + let z_score = ((ones as f64) - expected).abs() / sigma; + assert!( + z_score < 6.0, + "{} low-bit balance failed: ones={ones}, z={z_score}", + projection.label + ); +} + +fn assert_low_bucket_chi_square(projection: &StatisticalProjection) { + const BUCKETS: usize = 64; + let mut buckets = [0usize; BUCKETS]; + for &value in &projection.values { + buckets[(value & (BUCKETS as u64 - 1)) as usize] += 1; + } + let expected = projection.values.len() as f64 / BUCKETS as f64; + let chi_square = buckets + .iter() + .map(|&count| { + let delta = count as f64 - expected; + delta * delta / expected + }) + .sum::(); + assert!( + chi_square < 155.0, + "{} bucket chi-square too high: {chi_square}", + projection.label + ); +} + +fn assert_lag_one_correlation(projection: &StatisticalProjection) { + let correlation = pearson_correlation( + &projection.values[..projection.values.len() - 1], + &projection.values[1..], + ); + assert!( + correlation.abs() < 0.25, + "{} has suspicious lag-one correlation: {correlation}", + projection.label + ); +} + +fn assert_runs_around_median(projection: &StatisticalProjection) { + let mut sorted = projection.values.clone(); + sorted.sort_unstable(); + let median = sorted[sorted.len() / 2]; + let signs = projection + .values + .iter() + .map(|&value| value > median) + .collect::>(); + let high_count = signs.iter().filter(|&&sign| sign).count(); + let low_count = signs.len() - high_count; + assert!( + high_count > 0 && low_count > 0, + "{} did not cross its sample median", + projection.label + ); + + let runs = 1 + signs + .windows(2) + .filter(|window| window[0] != window[1]) + .count(); + let n = signs.len() as f64; + let high = high_count as f64; + let low = low_count as f64; + let expected = 1.0 + 2.0 * high * low / n; + let variance = 2.0 * high * low * (2.0 * high * low - n) / (n * n * (n - 1.0)); + let z_score = (runs as f64 - expected).abs() / variance.sqrt(); + assert!( + z_score < 6.0, + "{} median-runs test failed: runs={runs}, z={z_score}", + projection.label + ); +} + +fn pearson_correlation(lhs: &[u64], rhs: &[u64]) -> f64 { + assert_eq!(lhs.len(), rhs.len()); + assert!(lhs.len() >= 2); + let lhs_values = lhs.iter().map(|&value| value as f64).collect::>(); + let rhs_values = rhs.iter().map(|&value| value as f64).collect::>(); + let lhs_mean = lhs_values.iter().sum::() / lhs_values.len() as f64; + let rhs_mean = rhs_values.iter().sum::() / rhs_values.len() as f64; + let mut numerator = 0.0; + let mut lhs_variance = 0.0; + let mut rhs_variance = 0.0; + for (&lhs_value, &rhs_value) in lhs_values.iter().zip(&rhs_values) { + let lhs_delta = lhs_value - lhs_mean; + let rhs_delta = rhs_value - rhs_mean; + numerator += lhs_delta * rhs_delta; + lhs_variance += lhs_delta * lhs_delta; + rhs_variance += rhs_delta * rhs_delta; + } + let denominator = (lhs_variance * rhs_variance).sqrt(); + assert!(denominator > 0.0); + numerator / denominator +} + +pub fn pedersen_setup(capacity: usize) -> PedersenSetup { + let generator = Bn254::g1_generator(); + let message_generators = (1..=capacity) + .map(|i| generator.scalar_mul(&F::from_u64(i as u64))) + .collect(); + PedersenSetup::new(message_generators, generator.scalar_mul(&f(99))) +} + +pub fn commit_round_with_blinding( + setup: &PedersenSetup, + coefficients: Vec, + blinding: F, +) -> CommittedRound { + CommittedRoundWitness { + coefficients, + blinding, + } + .commit::(setup) + .expect("round witness commits") +} + +pub fn commit_round( + setup: &PedersenSetup, + coefficients: Vec, + round: usize, +) -> CommittedRound { + commit_round_with_blinding(setup, coefficients, f(round as u64 + 17)) +} + +pub fn coefficients_for_claim_with_rng(claim: F, degree: usize, rng: &mut impl RngCore) -> Vec { + let mut coefficients = vec![f(0); degree + 1]; + let mut nonconstant_sum = f(0); + for coefficient in coefficients.iter_mut().skip(1) { + *coefficient = rng_field(rng); + nonconstant_sum += *coefficient; + } + coefficients[0] = (claim - nonconstant_sum) * inverse(f(2)); + coefficients +} + +#[derive(Debug)] +pub struct SumcheckTestProver { + rng: R, +} + +impl SumcheckTestProver { + pub fn new(rng: R) -> Self { + Self { rng } + } + + pub fn prove_stage( + &mut self, + setup: &PedersenSetup, + transcript: &mut Blake2bTranscript, + statement: SumcheckStatement, + input_claim: F, + ) -> GeneratedStage { + self.prove_stage_with_output_claims(setup, transcript, statement, input_claim, 0) + } + + pub fn prove_stage_with_output_claims( + &mut self, + setup: &PedersenSetup, + transcript: &mut Blake2bTranscript, + statement: SumcheckStatement, + input_claim: F, + output_claim_count: usize, + ) -> GeneratedStage { + let mut claim = input_claim; + let mut rounds = Vec::with_capacity(statement.num_vars); + let mut coefficients = Vec::with_capacity(statement.num_vars); + let mut blindings = Vec::with_capacity(statement.num_vars); + let mut claim_outs = Vec::with_capacity(statement.num_vars); + + for _ in 0..statement.num_vars { + let round_coefficients = + coefficients_for_claim_with_rng(claim, statement.degree, &mut self.rng); + let blinding = rng_field(&mut self.rng); + let round = commit_round_with_blinding(setup, round_coefficients.clone(), blinding); + round.append_to_transcript(transcript); + let challenge = transcript.challenge(); + claim = eval_poly(&round_coefficients, challenge); + + rounds.push(round); + coefficients.push(round_coefficients); + blindings.push(blinding); + claim_outs.push(claim); + } + let mut output_claim_rows = Vec::with_capacity(output_claim_count); + let mut output_claim_blindings = Vec::with_capacity(output_claim_count); + let mut output_commitments = Vec::with_capacity(output_claim_count); + for _ in 0..output_claim_count { + let row = (0..=statement.degree) + .map(|_| rng_field(&mut self.rng)) + .collect::>(); + let blinding = rng_field(&mut self.rng); + output_commitments.push(VC::commit(setup, &row, &blinding)); + output_claim_rows.push(row); + output_claim_blindings.push(blinding); + } + let output_claims = CommittedOutputClaims { + commitments: output_commitments, + }; + output_claims.append_to_transcript(transcript); + + GeneratedStage { + statement, + proof: CommittedSumcheckProof { + rounds, + output_claims, + }, + coefficients, + blindings, + output_claim_rows, + output_claim_blindings, + input_claim, + claim_outs, + } + } + + pub fn prove_stage_with_fresh_transcript( + &mut self, + setup: &PedersenSetup, + transcript_label: &'static [u8], + statement: SumcheckStatement, + input_claim: F, + ) -> GeneratedStage { + let mut transcript = Blake2bTranscript::::new(transcript_label); + self.prove_stage(setup, &mut transcript, statement, input_claim) + } +} + +pub fn generate_zero_stage(setup: &PedersenSetup, num_vars: usize) -> GeneratedStage { + let rounds = (0..num_vars) + .map(|round| commit_round(setup, vec![f(0)], round)) + .collect(); + GeneratedStage { + statement: SumcheckStatement::new(num_vars, 1), + proof: CommittedSumcheckProof { + rounds, + output_claims: CommittedOutputClaims::default(), + }, + coefficients: vec![vec![f(0)]; num_vars], + blindings: (0..num_vars).map(|round| f(round as u64 + 17)).collect(), + output_claim_rows: Vec::new(), + output_claim_blindings: Vec::new(), + input_claim: f(0), + claim_outs: vec![f(0); num_vars], + } +} + +pub fn stage_consistency( + statement: SumcheckStatement, + proof: &CommittedSumcheckProof, +) -> CommittedSumcheckConsistency { + let mut transcript = Blake2bTranscript::::new(b"blindfold-r1cs-e2e"); + proof + .verify_committed_consistency(statement, &mut transcript) + .expect("committed proof transcript verifies") +} + +pub fn stage_consistency_for_transcript( + stages: &[&GeneratedStage], +) -> Vec> { + stage_consistency_for_transcript_label(b"blindfold-r1cs-e2e", stages) +} + +pub fn stage_consistency_for_transcript_label( + transcript_label: &'static [u8], + stages: &[&GeneratedStage], +) -> Vec> { + let mut transcript = Blake2bTranscript::::new(transcript_label); + stages + .iter() + .map(|stage| { + stage + .proof + .verify_committed_consistency(stage.statement, &mut transcript) + .expect("committed proof transcript verifies") + }) + .collect() +} + +pub fn blindfold_statement_for_transcript_label( + transcript_label: &'static [u8], + relations: &[TestStageRelation], + stages: &[&GeneratedStage], + final_openings: Vec>, +) -> BlindFoldStatement +where + O: Clone, + P: Clone, + Ch: Clone, +{ + assert_eq!( + relations.len(), + stages.len(), + "relations and generated stages must align" + ); + let mut transcript = Blake2bTranscript::::new(transcript_label); + let stages = relations + .iter() + .zip(stages) + .map(|(relation, generated)| { + let consistency = generated + .proof + .verify_committed_consistency(generated.statement, &mut transcript) + .expect("committed proof transcript verifies"); + BlindFoldStage::new( + relation.name.clone(), + relation.statement, + relation.domain, + consistency, + CommittedClaimRows::new( + Vec::new(), + relation.statement.degree + 1, + generated.proof.output_claims.clone(), + ), + relation.input_claim.clone(), + relation.output_claim.clone(), + ) + }) + .collect(); + BlindFoldStatement::new(stages, final_openings) +} + +pub fn assign_generated_stage( + builder: &mut R1csBuilder, + layout: &SumcheckR1csLayout, + generated: &GeneratedStage, +) { + builder + .assign(layout.input_claim, generated.input_claim) + .expect("input claim assigns"); + for (round_layout, (round_coefficients, &claim_out)) in layout + .rounds + .iter() + .zip(generated.coefficients.iter().zip(&generated.claim_outs)) + { + for (&variable, &coefficient) in round_layout.coefficients.iter().zip(round_coefficients) { + builder + .assign(variable, coefficient) + .expect("coefficient assigns"); + } + builder + .assign(round_layout.claim_out, claim_out) + .expect("claim out assigns"); + } +} + +pub fn deep_stage1_input(values: &DeepValues) -> F { + values.start * values.aux * values.scale + values.offset - values.bias +} + +pub fn deep_stage2_input(values: &DeepValues) -> F { + values.link * values.mix + values.multiplier +} + +pub fn deep_stage3_input(values: &DeepValues) -> F { + values.mid + values.start * values.bias +} + +pub fn deep_values_without_links() -> DeepValues { + DeepValues { + start: f(6), + aux: f(10), + link: f(0), + mid: f(0), + final_value: f(0), + scale: f(4), + bias: f(12), + mix: f(8), + offset: f(18), + multiplier: f(30), + } +} + +pub fn deep_values( + stage1_final_claim: F, + stage2_final_claim: F, + stage3_final_claim: F, +) -> DeepValues { + let mut values = deep_values_without_links(); + values.link = stage1_final_claim; + values.mid = (stage2_final_claim - values.bias) * inverse(values.aux); + values.final_value = + (stage3_final_claim - values.mix * values.offset - values.link * values.mid) + * inverse(values.aux * values.start); + values +} + +pub fn deep_claims() -> (TestExpr, TestExpr, TestExpr, TestExpr, TestExpr, TestExpr) { + let stage1_input = + opening(Opening::Start) * opening(Opening::Aux) * challenge(Challenge::Scale) + + public(Public::Offset) + - challenge(Challenge::Bias); + let stage1_output = opening(Opening::Link); + let stage2_input = + opening(Opening::Link) * challenge(Challenge::Mix) + public(Public::Multiplier); + let stage2_output = opening(Opening::Mid) * opening(Opening::Aux) + challenge(Challenge::Bias); + let stage3_input = opening(Opening::Mid) + opening(Opening::Start) * challenge(Challenge::Bias); + let stage3_output = opening(Opening::Final) * opening(Opening::Aux) * opening(Opening::Start) + + challenge(Challenge::Mix) * public(Public::Offset) + + opening(Opening::Link) * opening(Opening::Mid); + ( + stage1_input, + stage1_output, + stage2_input, + stage2_output, + stage3_input, + stage3_output, + ) +} + +pub fn build_deep_relation( + stage1: &GeneratedStage, + stage2: &GeneratedStage, + stage3: &GeneratedStage, + values: &DeepValues, +) -> Result<(), usize> { + let (stage1_input, stage1_output, stage2_input, stage2_output, stage3_input, stage3_output) = + deep_claims(); + let relations = vec![ + TestStageRelation::new( + "deep-stage-1", + stage1.statement, + stage1_input, + stage1_output, + ), + TestStageRelation::new( + "deep-stage-2", + stage2.statement, + stage2_input, + stage2_output, + ), + TestStageRelation::new( + "deep-stage-3", + stage3.statement, + stage3_input, + stage3_output, + ), + ]; + let statement = blindfold_statement_for_transcript_label( + b"blindfold-r1cs-e2e", + &relations, + &[stage1, stage2, stage3], + Vec::new(), + ); + + let mut builder = R1csBuilder::::new(); + let mut sources = ClaimSourceTable::::new(); + sources.insert_opening(Opening::Start, builder.alloc(values.start)); + sources.insert_opening(Opening::Aux, builder.alloc(values.aux)); + sources.insert_opening(Opening::Link, builder.alloc(values.link)); + sources.insert_opening(Opening::Mid, builder.alloc(values.mid)); + sources.insert_opening(Opening::Final, builder.alloc(values.final_value)); + sources.insert_challenge(Challenge::Scale, values.scale); + sources.insert_challenge(Challenge::Bias, values.bias); + sources.insert_challenge(Challenge::Mix, values.mix); + sources.insert_public(Public::Offset, values.offset); + sources.insert_public(Public::Multiplier, values.multiplier); + + let layout = statement + .allocate_layout(&mut builder) + .expect("layout allocates"); + statement + .append(&mut builder, &layout, &mut sources) + .expect("constraints append"); + assign_generated_stage(&mut builder, &layout.stages[0].sumcheck, stage1); + assign_generated_stage(&mut builder, &layout.stages[1].sumcheck, stage2); + assign_generated_stage(&mut builder, &layout.stages[2].sumcheck, stage3); + + let witness = builder.witness().expect("all witnesses assigned"); + builder.into_matrices().check_witness(&witness) +} + +pub fn generated_deep_triple( + prover: &mut SumcheckTestProver, +) -> (GeneratedStage, GeneratedStage, GeneratedStage, DeepValues) { + let setup = pedersen_setup(4); + let statement = SumcheckStatement::new(4, 3); + let mut values = deep_values_without_links(); + let mut transcript = Blake2bTranscript::::new(b"blindfold-r1cs-e2e"); + let stage1 = prover.prove_stage( + &setup, + &mut transcript, + statement, + deep_stage1_input(&values), + ); + values.link = *stage1 + .claim_outs + .last() + .expect("stage has at least one round"); + let stage2 = prover.prove_stage( + &setup, + &mut transcript, + statement, + deep_stage2_input(&values), + ); + let stage2_final_claim = *stage2 + .claim_outs + .last() + .expect("stage has at least one round"); + values.mid = (stage2_final_claim - values.bias) * inverse(values.aux); + let stage3 = prover.prove_stage( + &setup, + &mut transcript, + statement, + deep_stage3_input(&values), + ); + let stage3_final_claim = *stage3 + .claim_outs + .last() + .expect("stage has at least one round"); + values = deep_values(values.link, stage2_final_claim, stage3_final_claim); + (stage1, stage2, stage3, values) +} + +#[derive(Clone, Debug)] +pub struct BlindFoldTestProof { + pub protocol: BlindFoldProtocol, + pub proof: BlindFoldProof, + pub setup: PedersenSetup, +} + +#[derive(Clone, Debug)] +struct SumcheckTrace { + proof: CompressedSumcheckProof, + point: Vec, +} + +pub fn prove_blindfold_protocol_pipeline(rng: &mut R) -> BlindFoldTestProof { + let setup = pedersen_setup(4); + let transcript_label = b"protocol-backed-blindfold-proof"; + let statement1 = SumcheckStatement::new(3, 3); + let statement2 = SumcheckStatement::new(2, 3); + let input1 = f(37); + let input2 = f(89); + + let (stage1, stage2) = { + let mut prover = SumcheckTestProver::new(&mut *rng); + let mut transcript = Blake2bTranscript::::new(transcript_label); + let stage1 = + prover.prove_stage_with_output_claims(&setup, &mut transcript, statement1, input1, 2); + let stage2 = + prover.prove_stage_with_output_claims(&setup, &mut transcript, statement2, input2, 1); + (stage1, stage2) + }; + let stage1_output = *stage1 + .claim_outs + .last() + .expect("stage has at least one round"); + let stage2_output = *stage2 + .claim_outs + .last() + .expect("stage has at least one round"); + let real_eval_outputs = vec![stage1.output_claim_rows[0][0]]; + let real_eval_blindings = vec![rng_field(rng)]; + let eval_commitments = real_eval_outputs + .iter() + .zip(&real_eval_blindings) + .map(|(&output, blinding)| VC::commit(&setup, &[output], blinding)) + .collect::>(); + + let mut transcript = Blake2bTranscript::::new(transcript_label); + let stage1_consistency = stage1 + .proof + .verify_committed_consistency(stage1.statement, &mut transcript) + .expect("stage 1 committed proof transcript verifies"); + let stage2_consistency = stage2 + .proof + .verify_committed_consistency(stage2.statement, &mut transcript) + .expect("stage 2 committed proof transcript verifies"); + let stages = vec![ + BlindFoldStage::new( + "protocol-backed-stage-1", + statement1, + SumcheckDomainSpec::BooleanHypercube, + stage1_consistency, + CommittedClaimRows::new( + (0..stage1.proof.output_claims.commitments.len() * (statement1.degree + 1)) + .collect(), + statement1.degree + 1, + stage1.proof.output_claims.clone(), + ), + constant(input1), + constant(stage1_output), + ), + BlindFoldStage::new( + "protocol-backed-stage-2", + statement2, + SumcheckDomainSpec::BooleanHypercube, + stage2_consistency, + CommittedClaimRows::new( + (100..100 + stage2.proof.output_claims.commitments.len() * (statement2.degree + 1)) + .collect(), + statement2.degree + 1, + stage2.proof.output_claims.clone(), + ), + constant(input2), + constant(stage2_output), + ), + ]; + let statement = BlindFoldStatement::new( + stages, + vec![FinalOpeningBinding::new( + vec![0usize], + vec![f(1)], + eval_commitments[0], + )], + ); + let protocol = blindfold_protocol_from_statement(&statement) + .expect("protocol builds from committed statement"); + let mut transcript = Blake2bTranscript::::new(transcript_label); + append_protocol_transcript_prefix(&protocol, &mut transcript); + let (real_witness_rows, real_witness_blindings) = protocol_backed_witness( + &protocol, + &statement, + &[&stage1, &stage2], + &real_eval_outputs, + &real_eval_blindings, + rng, + ); + let witness = ProtocolWitness { + rows: &real_witness_rows, + blindings: &real_witness_blindings, + eval_outputs: &real_eval_outputs, + eval_blindings: &real_eval_blindings, + }; + let proof = prove_from_protocol_witness(&setup, &protocol, &mut transcript, witness, rng); + + BlindFoldTestProof { + protocol, + proof, + setup, + } +} + +pub fn blindfold_protocol_from_statement( + statement: &BlindFoldStatement, +) -> Result, jolt_blindfold::VerificationError> +where + O: Clone + PartialEq, + P: Clone + PartialEq, + Ch: Clone + PartialEq, +{ + let mut builder = BlindFoldProtocol::::builder::(); + for stage in &statement.stages { + builder = builder + .stage(stage.name.clone()) + .sumcheck(stage.statement) + .domain(stage.domain) + .consistency(stage.consistency.clone()) + .output_claim_rows( + stage.output_claim_rows.opening_ids.clone(), + stage.output_claim_rows.row_len, + stage.output_claim_rows.commitments.clone(), + ) + .input_claim(stage.input_claim.clone()) + .output_claim(stage.output_claim.clone()) + .finish_stage() + .expect("test stage statement is complete"); + } + for binding in &statement.final_openings { + builder = builder.final_opening( + binding.opening_ids.clone(), + binding.coefficients.clone(), + binding.evaluation_commitment, + ); + } + builder.build() +} + +pub fn append_protocol_transcript_prefix( + protocol: &BlindFoldProtocol, + transcript: &mut Blake2bTranscript, +) { + for (stage, output_claims) in protocol + .sumcheck_consistency + .iter() + .zip(&protocol.committed_output_claims) + { + for round in &stage.rounds { + CommittedRound { + commitment: round.commitment, + degree: round.degree, + } + .append_to_transcript(transcript); + let _ = transcript.challenge(); + } + output_claims.append_to_transcript(transcript); + } +} + +fn protocol_backed_witness( + protocol: &BlindFoldProtocol, + statement: &BlindFoldStatement, + stages: &[&GeneratedStage], + eval_outputs: &[F], + eval_blindings: &[F], + rng: &mut R, +) -> (Vec>, Vec) { + let mut builder = R1csBuilder::::new(); + let mut sources = ClaimSourceTable::::new(); + let layout = statement + .allocate_layout(&mut builder) + .expect("layout allocates"); + for (stage, stage_layout) in statement.stages.iter().zip(&layout.stages) { + let variables = stage_layout + .output_claim_rows + .iter() + .flat_map(|row| row.variables.iter().take(stage.output_claim_rows.row_len)); + for (opening_id, &variable) in stage.output_claim_rows.opening_ids.iter().zip(variables) { + sources.insert_opening(*opening_id, variable); + } + } + statement + .append(&mut builder, &layout, &mut sources) + .expect("constraints append"); + for (stage, (stage_layout, generated)) in statement + .stages + .iter() + .zip(layout.stages.iter().zip(stages)) + { + assign_generated_stage(&mut builder, &stage_layout.sumcheck, generated); + let variables = stage_layout + .output_claim_rows + .iter() + .flat_map(|row| row.variables.iter().take(stage.output_claim_rows.row_len)); + let values = generated + .output_claim_rows + .iter() + .flat_map(|row| row.iter().copied()); + for (&variable, value) in variables + .zip(values) + .take(stage.output_claim_rows.opening_ids.len()) + { + builder + .assign(variable, value) + .expect("output claim opening assigns"); + } + } + for (index, final_opening) in layout.final_openings.iter().enumerate() { + if let Some(evaluation) = final_opening.evaluation { + builder + .assign(evaluation, eval_outputs[index]) + .expect("final opening evaluation assigns"); + } + if let Some(blinding) = final_opening.blinding { + builder + .assign(blinding, eval_blindings[index]) + .expect("final opening blinding assigns"); + } + } + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); + + let row_len = protocol.dimensions.witness.row_len; + let mut rows = witness[1..=protocol.dimensions.coefficient_values] + .chunks(row_len) + .map(<[F]>::to_vec) + .collect::>(); + assert_eq!(rows.len(), protocol.dimensions.coefficient_rows); + + for row in stages + .iter() + .flat_map(|stage| stage.output_claim_rows.iter()) + { + let mut row = row.clone(); + row.resize(row_len, f(0)); + rows.push(row); + } + assert_eq!( + rows.len(), + protocol.dimensions.witness_rows.output_claims.end + ); + + let output_claim_values = protocol + .dimensions + .output_claim_rows + .checked_mul(row_len) + .expect("output claim row value count fits"); + let auxiliary_values = + &witness[1 + protocol.dimensions.coefficient_values + output_claim_values..]; + let mut auxiliary_rows = auxiliary_values + .chunks(row_len) + .map(|chunk| { + let mut row = chunk.to_vec(); + row.resize(row_len, f(0)); + row + }) + .collect::>(); + auxiliary_rows.resize(protocol.dimensions.auxiliary_rows, vec![f(0); row_len]); + rows.extend(auxiliary_rows); + assert_eq!(rows.len(), protocol.dimensions.witness_rows.auxiliary.end); + + rows.resize(protocol.dimensions.witness.row_count, vec![f(0); row_len]); + + let mut blindings = stages + .iter() + .flat_map(|stage| stage.blindings.iter().copied()) + .collect::>(); + blindings.extend( + stages + .iter() + .flat_map(|stage| stage.output_claim_blindings.iter().copied()), + ); + blindings.extend((0..protocol.dimensions.auxiliary_rows).map(|_| rng_field(rng))); + blindings.resize(protocol.dimensions.witness.row_count, f(0)); + + assert_eq!(rows.len(), protocol.dimensions.witness.row_count); + assert_eq!(blindings.len(), protocol.dimensions.witness.row_count); + (rows, blindings) +} + +#[derive(Clone, Copy, Debug)] +struct ProtocolWitness<'a> { + rows: &'a [Vec], + blindings: &'a [F], + eval_outputs: &'a [F], + eval_blindings: &'a [F], +} + +fn prove_from_protocol_witness( + setup: &PedersenSetup, + protocol: &BlindFoldProtocol, + transcript: &mut Blake2bTranscript, + witness: ProtocolWitness<'_>, + rng: &mut R, +) -> BlindFoldProof { + let auxiliary_range = protocol.dimensions.witness_rows.auxiliary.clone(); + let auxiliary_row_commitments = commit_rows( + setup, + &witness.rows[auxiliary_range.clone()], + &witness.blindings[auxiliary_range], + ); + let committed = protocol + .committed_relaxed_instance(&auxiliary_row_commitments) + .expect("committed relaxed instance builds"); + assert_eq!( + committed.witness_row_commitments, + commit_rows(setup, witness.rows, witness.blindings) + ); + for ((commitment, &output), &blinding) in protocol + .eval_commitments + .iter() + .zip(witness.eval_outputs) + .zip(witness.eval_blindings) + { + assert!(VC::verify(setup, commitment, &[output], &blinding)); + } + + let random_u = rng_field(rng); + let random_witness_rows = random_rows( + protocol.dimensions.witness.row_count, + protocol.dimensions.witness.row_len, + rng, + ); + let mut random_witness_rows = random_witness_rows; + let mut random_witness_blindings = (0..protocol.dimensions.witness.row_count) + .map(|_| rng_field(rng)) + .collect::>(); + for row in protocol.dimensions.witness_rows.padding.clone() { + random_witness_rows[row].fill(f(0)); + random_witness_blindings[row] = f(0); + } + let random_eval_outputs = (0..protocol.eval_commitments.len()) + .map(|_| rng_field(rng)) + .collect::>(); + let random_eval_blindings = (0..protocol.eval_commitments.len()) + .map(|_| rng_field(rng)) + .collect::>(); + let final_coordinates = protocol + .final_opening_witness_coordinates() + .expect("final opening coordinates are in witness layout"); + let mut dedicated_rows = Vec::new(); + for coordinates in &final_coordinates { + if let Some(coordinate) = coordinates.evaluation { + dedicated_rows.push(coordinate.row); + } + if let Some(coordinate) = coordinates.blinding { + dedicated_rows.push(coordinate.row); + } + } + dedicated_rows.sort_unstable(); + dedicated_rows.dedup(); + for row in dedicated_rows { + random_witness_rows[row].fill(f(0)); + } + for (index, coordinates) in final_coordinates.iter().enumerate() { + if let Some(coordinate) = coordinates.evaluation { + random_witness_rows[coordinate.row][coordinate.column] = random_eval_outputs[index]; + } + if let Some(coordinate) = coordinates.blinding { + random_witness_rows[coordinate.row][coordinate.column] = random_eval_blindings[index]; + } + } + let random_error_rows = error_rows_for( + &protocol.r1cs, + random_u, + &flatten(&random_witness_rows), + protocol.dimensions.error.row_len, + ); + let random_error_blindings = (0..protocol.dimensions.error.row_count) + .map(|_| rng_field(rng)) + .collect::>(); + let coefficient_range = protocol.dimensions.witness_rows.coefficients.clone(); + let output_claim_range = protocol.dimensions.witness_rows.output_claims.clone(); + let auxiliary_range = protocol.dimensions.witness_rows.auxiliary.clone(); + let random_round_commitments = commit_rows( + setup, + &random_witness_rows[coefficient_range.clone()], + &random_witness_blindings[coefficient_range], + ); + let random_output_claim_row_commitments = commit_rows( + setup, + &random_witness_rows[output_claim_range.clone()], + &random_witness_blindings[output_claim_range], + ); + let random_auxiliary_row_commitments = commit_rows( + setup, + &random_witness_rows[auxiliary_range.clone()], + &random_witness_blindings[auxiliary_range], + ); + let random_error_row_commitments = + commit_rows(setup, &random_error_rows, &random_error_blindings); + let random_eval_commitments = random_eval_outputs + .iter() + .zip(&random_eval_blindings) + .map(|(&output, blinding)| VC::commit(setup, &[output], blinding)) + .collect::>(); + let random_instance = protocol + .random_relaxed_instance( + &random_round_commitments, + &random_output_claim_row_commitments, + &random_auxiliary_row_commitments, + &random_error_row_commitments, + &random_eval_commitments, + random_u, + ) + .expect("random relaxed instance builds"); + assert_eq!( + random_instance.witness_row_commitments, + commit_rows(setup, &random_witness_rows, &random_witness_blindings) + ); + + let cross_term_error_rows = cross_term_error_rows_for( + &protocol.r1cs, + f(1), + &flatten(witness.rows), + random_u, + &flatten(&random_witness_rows), + protocol.dimensions.error.row_len, + ); + let cross_term_error_blindings = (0..protocol.dimensions.error.row_count) + .map(|_| rng_field(rng)) + .collect::>(); + let cross_term_error_row_commitments = + commit_rows(setup, &cross_term_error_rows, &cross_term_error_blindings); + + append_relaxed_instance_from_parts( + transcript, + RelaxedInstanceLabels { + u: b"bf_committed_u", + witness: b"bf_committed_w", + error: b"bf_committed_e", + eval: b"bf_committed_eval", + }, + committed.u, + &committed.witness_row_commitments, + &committed.error_row_commitments, + &committed.eval_commitments, + ); + append_relaxed_instance_from_parts( + transcript, + RelaxedInstanceLabels { + u: b"bf_random_u", + witness: b"bf_random_w", + error: b"bf_random_e", + eval: b"bf_random_eval", + }, + random_u, + &random_instance.witness_row_commitments, + &random_instance.error_row_commitments, + &random_instance.eval_commitments, + ); + transcript.append_values(b"bf_cross_e", &cross_term_error_row_commitments); + let folding_challenge = transcript.challenge(); + + let folded_u = f(1) + folding_challenge * random_u; + let folded_witness_rows = fold_rows(witness.rows, &random_witness_rows, folding_challenge); + let folded_witness_blindings = fold_scalars( + witness.blindings, + &random_witness_blindings, + folding_challenge, + ); + let folded_error_rows = fold_error_rows( + &zero_rows( + protocol.dimensions.error.row_count, + protocol.dimensions.error.row_len, + ), + &cross_term_error_rows, + &random_error_rows, + folding_challenge, + ); + let folded_error_blindings = fold_error_scalars( + &vec![f(0); protocol.dimensions.error.row_count], + &cross_term_error_blindings, + &random_error_blindings, + folding_challenge, + ); + let folded_eval_outputs = fold_scalars( + witness.eval_outputs, + &random_eval_outputs, + folding_challenge, + ); + let folded_eval_blindings = fold_scalars( + witness.eval_blindings, + &random_eval_blindings, + folding_challenge, + ); + let final_coordinates = protocol + .final_opening_witness_coordinates() + .expect("final opening coordinates are in witness layout"); + let mut folded_eval_output_openings = Vec::new(); + let mut folded_eval_blinding_openings = Vec::new(); + for (index, coordinates) in final_coordinates.iter().enumerate() { + if let Some(coordinate) = coordinates.evaluation { + let (opening, opened) = open_witness_coordinate( + &folded_witness_rows, + &folded_witness_blindings, + coordinate, + ); + assert_eq!(opened, folded_eval_outputs[index]); + folded_eval_output_openings.push(opening); + } + if let Some(coordinate) = coordinates.blinding { + let (opening, opened) = open_witness_coordinate( + &folded_witness_rows, + &folded_witness_blindings, + coordinate, + ); + assert_eq!(opened, folded_eval_blindings[index]); + folded_eval_blinding_openings.push(opening); + } + } + for opening in &folded_eval_output_openings { + append_vector_opening( + transcript, + b"bf_eval_out_open", + b"bf_eval_out_blind", + opening, + ); + } + for opening in &folded_eval_blinding_openings { + append_vector_opening( + transcript, + b"bf_eval_blind_open", + b"bf_eval_blind_bl", + opening, + ); + } + + transcript.append(&Label(b"bf_spartan")); + let outer_num_vars = + log2(protocol.dimensions.error.row_count) + log2(protocol.dimensions.error.row_len); + let tau = transcript.challenge_vector(outer_num_vars); + let outer_trace = prove_slow_sumcheck( + outer_num_vars, + 3, + f(0), + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + transcript, + |point| { + outer_function( + &protocol.r1cs, + folded_u, + &flatten(&folded_witness_rows), + &folded_error_rows, + &tau, + point, + ) + }, + ); + + let (az_rx, bz_rx, cz_rx) = abc_at_point( + &protocol.r1cs, + folded_u, + &flatten(&folded_witness_rows), + &outer_trace.point, + ); + let (error_row_point, error_entry_point) = outer_trace + .point + .split_at(log2(protocol.dimensions.error.row_count)); + let (error_opening, _) = VC::open_committed_rows( + &flatten(&folded_error_rows), + &folded_error_blindings, + protocol.dimensions.error.row_len, + error_row_point, + error_entry_point, + ) + .expect("folded error rows open"); + + transcript.append_values(b"bf_az_bz_cz", &[az_rx, bz_rx, cz_rx]); + append_vector_opening( + transcript, + b"bf_error_opening", + b"bf_error_blind", + &error_opening, + ); + + let ra = transcript.challenge(); + let rb = transcript.challenge(); + let rc = transcript.challenge(); + let inner_num_vars = + log2(protocol.dimensions.witness.row_count) + log2(protocol.dimensions.witness.row_len); + let row_weights = EqPolynomial::::evals(&outer_trace.point, None); + let public = protocol + .r1cs + .public_column_contributions(&row_weights, 0, folded_u) + .expect("public column contributions evaluate"); + let inner_claim = ra * (az_rx - public.a) + rb * (bz_rx - public.b) + rc * (cz_rx - public.c); + let inner_trace = prove_slow_sumcheck( + inner_num_vars, + 2, + inner_claim, + b"inner_sumcheck_poly", + transcript, + |point| { + inner_function( + &protocol.r1cs, + &outer_trace.point, + &folded_witness_rows, + ra, + rb, + rc, + point, + ) + }, + ); + let (witness_row_point, witness_entry_point) = inner_trace + .point + .split_at(log2(protocol.dimensions.witness.row_count)); + let (witness_opening, _) = VC::open_committed_rows( + &flatten(&folded_witness_rows), + &folded_witness_blindings, + protocol.dimensions.witness.row_len, + witness_row_point, + witness_entry_point, + ) + .expect("folded witness rows open"); + + BlindFoldProof { + auxiliary_row_commitments, + random_round_commitments, + random_output_claim_row_commitments, + random_auxiliary_row_commitments, + random_error_row_commitments, + random_eval_commitments, + random_u, + cross_term_error_row_commitments, + outer_sumcheck: outer_trace.proof, + az_rx, + bz_rx, + cz_rx, + inner_sumcheck: inner_trace.proof, + witness_opening, + error_opening, + folded_eval_outputs, + folded_eval_blindings, + folded_eval_output_openings, + folded_eval_blinding_openings, + } +} + +fn commit_rows(setup: &PedersenSetup, rows: &[Vec], blindings: &[F]) -> Vec { + rows.iter() + .zip(blindings) + .map(|(row, blinding)| VC::commit(setup, row, blinding)) + .collect() +} + +fn open_witness_coordinate( + witness_rows: &[Vec], + witness_blindings: &[F], + coordinate: WitnessCoordinate, +) -> (VectorCommitmentOpening, F) { + let row_vars = log2(witness_rows.len()); + let entry_vars = log2(witness_rows[0].len()); + VC::open_committed_rows( + &flatten(witness_rows), + witness_blindings, + witness_rows[0].len(), + &boolean_point(coordinate.row, row_vars), + &boolean_point(coordinate.column, entry_vars), + ) + .expect("folded witness coordinate opens") +} + +fn boolean_point(index: usize, num_vars: usize) -> Vec { + (0..num_vars) + .map(|bit| { + let shift = num_vars - bit - 1; + f(((index >> shift) & 1) as u64) + }) + .collect() +} + +#[derive(Clone, Copy, Debug)] +struct RelaxedInstanceLabels { + u: &'static [u8], + witness: &'static [u8], + error: &'static [u8], + eval: &'static [u8], +} + +fn append_relaxed_instance_from_parts( + transcript: &mut Blake2bTranscript, + labels: RelaxedInstanceLabels, + u: F, + witness_commitments: &[Bn254G1], + error_commitments: &[Bn254G1], + eval_commitments: &[Bn254G1], +) { + transcript.append(&Label(labels.u)); + u.append_to_transcript(transcript); + transcript.append_values(labels.witness, witness_commitments); + transcript.append_values(labels.error, error_commitments); + transcript.append_values(labels.eval, eval_commitments); +} + +fn append_vector_opening( + transcript: &mut Blake2bTranscript, + row_label: &'static [u8], + blinding_label: &'static [u8], + opening: &jolt_crypto::VectorCommitmentOpening, +) { + transcript.append_values(row_label, &opening.combined_vector); + transcript.append(&Label(blinding_label)); + opening.combined_blinding.append_to_transcript(transcript); +} + +fn zero_rows(row_count: usize, row_len: usize) -> Vec> { + vec![vec![f(0); row_len]; row_count] +} + +fn random_rows(row_count: usize, row_len: usize, rng: &mut R) -> Vec> { + (0..row_count) + .map(|_| (0..row_len).map(|_| rng_field(rng)).collect()) + .collect() +} + +fn fold_rows(real: &[Vec], random: &[Vec], challenge: F) -> Vec> { + real.iter() + .zip(random) + .map(|(real_row, random_row)| { + real_row + .iter() + .zip(random_row) + .map(|(&real, &random)| real + challenge * random) + .collect() + }) + .collect() +} + +fn fold_scalars(real: &[F], random: &[F], challenge: F) -> Vec { + real.iter() + .zip(random) + .map(|(&real, &random)| real + challenge * random) + .collect() +} + +fn fold_error_rows( + real: &[Vec], + cross: &[Vec], + random: &[Vec], + challenge: F, +) -> Vec> { + let challenge_squared = challenge * challenge; + real.iter() + .zip(cross) + .zip(random) + .map(|((real_row, cross_row), random_row)| { + real_row + .iter() + .zip(cross_row) + .zip(random_row) + .map(|((&real, &cross), &random)| { + real + challenge * cross + challenge_squared * random + }) + .collect() + }) + .collect() +} + +fn fold_error_scalars(real: &[F], cross: &[F], random: &[F], challenge: F) -> Vec { + let challenge_squared = challenge * challenge; + real.iter() + .zip(cross) + .zip(random) + .map(|((&real, &cross), &random)| real + challenge * cross + challenge_squared * random) + .collect() +} + +fn error_rows_for( + r1cs: &ConstraintMatrices, + u: F, + witness: &[F], + row_len: usize, +) -> Vec> { + let z = z_vector(u, witness); + let mut errors = (0..r1cs.num_constraints) + .map(|row_index| { + dot(&r1cs.a[row_index], &z) * dot(&r1cs.b[row_index], &z) + - u * dot(&r1cs.c[row_index], &z) + }) + .collect::>(); + pad_to_multiple(&mut errors, row_len); + errors.chunks(row_len).map(<[F]>::to_vec).collect() +} + +fn cross_term_error_rows_for( + r1cs: &ConstraintMatrices, + real_u: F, + real_witness: &[F], + random_u: F, + random_witness: &[F], + row_len: usize, +) -> Vec> { + let real_z = z_vector(real_u, real_witness); + let random_z = z_vector(random_u, random_witness); + let mut errors = (0..r1cs.num_constraints) + .map(|row_index| { + dot(&r1cs.a[row_index], &real_z) * dot(&r1cs.b[row_index], &random_z) + + dot(&r1cs.a[row_index], &random_z) * dot(&r1cs.b[row_index], &real_z) + - real_u * dot(&r1cs.c[row_index], &random_z) + - random_u * dot(&r1cs.c[row_index], &real_z) + }) + .collect::>(); + pad_to_multiple(&mut errors, row_len); + errors.chunks(row_len).map(<[F]>::to_vec).collect() +} + +fn pad_to_multiple(values: &mut Vec, row_len: usize) { + let remainder = values.len() % row_len; + if remainder != 0 { + values.resize(values.len() + row_len - remainder, f(0)); + } +} + +fn z_vector(u: F, witness: &[F]) -> Vec { + let mut z = Vec::with_capacity(witness.len() + 1); + z.push(u); + z.extend_from_slice(witness); + z +} + +fn dot(row: &[(usize, F)], witness: &[F]) -> F { + row.iter() + .map(|&(column, coefficient)| coefficient * witness[column]) + .sum() +} + +fn flatten(rows: &[Vec]) -> Vec { + rows.iter().flat_map(|row| row.iter().copied()).collect() +} + +fn abc_at_point(r1cs: &ConstraintMatrices, u: F, witness: &[F], point: &[F]) -> (F, F, F) { + let row_weights = EqPolynomial::::evals(point, None); + let z = z_vector(u, witness); + let mut az = f(0); + let mut bz = f(0); + let mut cz = f(0); + for (row_index, &row_weight) in row_weights.iter().enumerate().take(r1cs.num_constraints) { + az += row_weight * dot(&r1cs.a[row_index], &z); + bz += row_weight * dot(&r1cs.b[row_index], &z); + cz += row_weight * dot(&r1cs.c[row_index], &z); + } + (az, bz, cz) +} + +fn outer_function( + r1cs: &ConstraintMatrices, + u: F, + witness: &[F], + error_rows: &[Vec], + tau: &[F], + point: &[F], +) -> F { + let (az, bz, cz) = abc_at_point(r1cs, u, witness, point); + let error = mle_eval(&flatten(error_rows), point); + EqPolynomial::::mle(tau, point) * (az * bz - u * cz - error) +} + +fn inner_function( + r1cs: &ConstraintMatrices, + outer_point: &[F], + witness_rows: &[Vec], + ra: F, + rb: F, + rc: F, + point: &[F], +) -> F { + let row_weights = EqPolynomial::::evals(outer_point, None); + let column_weights = EqPolynomial::::evals(point, None); + let l_w = r1cs + .linear_form_bilinear_eval( + &row_weights, + &column_weights, + 1, + column_weights.len(), + [ra, rb, rc], + ) + .expect("inner linear form dimensions match"); + l_w * mle_eval(&flatten(witness_rows), point) +} + +fn mle_eval(values: &[F], point: &[F]) -> F { + EqPolynomial::::evals(point, None) + .iter() + .zip(values) + .map(|(&weight, &value)| weight * value) + .sum() +} + +fn prove_slow_sumcheck( + num_vars: usize, + degree: usize, + claim: F, + label: &'static [u8], + transcript: &mut Blake2bTranscript, + eval: impl Fn(&[F]) -> F, +) -> SumcheckTrace { + let mut running_sum = claim; + let mut prefix = Vec::with_capacity(num_vars); + let mut rounds = Vec::with_capacity(num_vars); + + for round in 0..num_vars { + let remaining = num_vars - round - 1; + let values = (0..=degree) + .map(|point| { + let mut sum = f(0); + for suffix in 0..(1usize << remaining) { + let mut evaluation_point = prefix.clone(); + evaluation_point.push(f(point as u64)); + for bit in 0..remaining { + evaluation_point.push(f(((suffix >> bit) & 1) as u64)); + } + sum += eval(&evaluation_point); + } + sum + }) + .collect::>(); + let coefficients = interpolate_zero_to_degree(&values); + let round_sum = coefficients[0] + coefficients.iter().copied().sum::(); + assert_eq!(round_sum, running_sum); + let mut compressed = Vec::with_capacity(degree); + compressed.push(coefficients[0]); + compressed.extend_from_slice(&coefficients[2..]); + transcript.append_values(label, &compressed); + let challenge = transcript.challenge(); + running_sum = eval_poly(&coefficients, challenge); + prefix.push(challenge); + rounds.push(CompressedPoly::new(compressed)); + } + + SumcheckTrace { + proof: CompressedSumcheckProof { + round_polynomials: rounds, + }, + point: prefix, + } +} + +fn interpolate_zero_to_degree(values: &[F]) -> Vec { + let degree = values.len() - 1; + let mut result = vec![f(0); degree + 1]; + for (j, &value) in values.iter().enumerate() { + let x_j = f(j as u64); + let mut basis = vec![f(1)]; + let mut denominator = f(1); + for m in 0..=degree { + if m == j { + continue; + } + let x_m = f(m as u64); + basis = multiply_by_linear(&basis, -x_m, f(1)); + denominator *= x_j - x_m; + } + let scale = value * inverse(denominator); + for (coefficient, basis_coefficient) in result.iter_mut().zip(basis) { + *coefficient += scale * basis_coefficient; + } + } + result +} + +fn multiply_by_linear(poly: &[F], constant: F, linear: F) -> Vec { + let mut result = vec![f(0); poly.len() + 1]; + for (index, &coefficient) in poly.iter().enumerate() { + result[index] += coefficient * constant; + result[index + 1] += coefficient * linear; + } + result +} + +fn log2(value: usize) -> usize { + assert!(value.is_power_of_two()); + value.trailing_zeros() as usize +} diff --git a/crates/jolt-transcript/src/transcript.rs b/crates/jolt-transcript/src/transcript.rs index 49eefffa42..043bd0cecd 100644 --- a/crates/jolt-transcript/src/transcript.rs +++ b/crates/jolt-transcript/src/transcript.rs @@ -3,7 +3,7 @@ //! This module provides the [`Transcript`] trait for building Fiat-Shamir transcripts //! and the [`AppendToTranscript`] trait for types that can be absorbed into a transcript. -use crate::domain::Label; +use crate::domain::{Label, LabelWithCount}; use jolt_field::{Field, FromPrimitiveInt}; /// Fiat-Shamir transcript for non-interactive proofs. @@ -57,6 +57,17 @@ pub trait Transcript: Default + Clone + Sync + Send + 'static { self.append(value); } + /// Absorbs a domain label with a count followed by each value in order. + /// + /// Use this for transcript payloads whose domain separation must include + /// the number of appended items. + fn append_values(&mut self, label: &'static [u8], values: &[A]) { + self.append(&LabelWithCount(label, values.len() as u64)); + for value in values { + self.append(value); + } + } + /// Squeezes a challenge from the transcript. /// /// Each call produces a new challenge and advances the transcript state. From 1f5008b4864767c51c4c77876379e426edd2aa6c Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 27 May 2026 08:26:31 -0700 Subject: [PATCH 02/24] docs(spec): add committed bytecode program image spec (#1565) * chore: add committed bytecode stack plan Co-authored-by: Cursor * chore: add committed bytecode feature spec Co-authored-by: Cursor * chore: rename precommitted stack branch Co-authored-by: Cursor * chore(sumcheck): remove debug sanity checker Co-authored-by: Cursor * chore(sumcheck): match upstream main Co-authored-by: Cursor * docs(spec): align committed bytecode stack plan Update the stack spec to match the final branch slices, moved claim-reduction implementation scope, and expanded local CI coverage. Co-authored-by: Cursor * docs(spec): leave committed bytecode feature spec unchanged Restore the feature spec text to its original contents; only the stack plan should describe the updated PR split and local CI scope. Co-authored-by: Cursor * docs(spec): remove stack verification sections Keep the stack plan focused on PR scope and ownership; verification details are tracked outside this spec. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- .../1344-committed-bytecode-program-image.md | 414 ++++++++++++++++++ 1 file changed, 414 insertions(+) create mode 100644 specs/1344-committed-bytecode-program-image.md diff --git a/specs/1344-committed-bytecode-program-image.md b/specs/1344-committed-bytecode-program-image.md new file mode 100644 index 0000000000..b271b2263e --- /dev/null +++ b/specs/1344-committed-bytecode-program-image.md @@ -0,0 +1,414 @@ +# Spec: Committed Bytecode And Program Image Openings + +| Field | Value | +|-------------|------------------------------------------------------------------------| +| Author(s) | Amirhossein Khajehpour, Quang Dao | +| Created | 2026-05-11 | +| Status | draft | +| PR | [#1344](https://github.com/a16z/jolt/pull/1344) | + +## Summary + +This PR adds a committed program mode for bytecode and program-image openings. +In full mode, verifier preprocessing contains enough bytecode and RAM preprocessing to evaluate the bytecode and initial program image directly. +In committed mode, prover and verifier preprocessing instead agree on Dory commitments to bytecode chunks and to the initial program-image polynomial, then the prover proves all required bytecode/program-image openings through claim reductions that feed the existing Stage 8 batched Dory opening proof. + +The motivation is recursive and verifier-side efficiency. +Bytecode and program-image data are program constants, so a verifier should not have to repeatedly materialize and directly evaluate those tables when a preprocessing commitment and a succinct opening proof can bind the same data. + +## Intent + +### Goal + +Introduce a committed program mode that commits bytecode chunks and the initial program image, reduces all execution-derived claims about those committed polynomials into Stage 8, and preserves the same zkVM execution relation in both full and committed modes. + +The new proving-system surface is: + +- `ProgramMode::{Full, Committed}` in `jolt-core/src/zkvm/config.rs`. +- `ProgramPreprocessing::{Full, Committed}` in `jolt-core/src/zkvm/program.rs`. +- `CommittedPolynomial::BytecodeChunk(i)` and `CommittedPolynomial::ProgramImageInit` in `jolt-core/src/zkvm/witness.rs`. +- `VirtualPolynomial::BytecodeValStage(i)`, `VirtualPolynomial::BytecodeClaimReductionIntermediate`, and `VirtualPolynomial::ProgramImageInitContributionRw`. +- `SumcheckId::{BytecodeReadRafAddressPhase, BooleanityAddressPhase, BytecodeClaimReductionCyclePhase, BytecodeClaimReduction, ProgramImageClaimReductionCyclePhase, ProgramImageClaimReduction}`. +- Shared precommitted scheduling through `PrecommittedClaimReduction` in `jolt-core/src/zkvm/claim_reductions/precommitted.rs`. + +### Invariants + +- Full and committed program modes must prove the same guest execution relation. +- Committed mode must not let the verifier accept a proof for bytecode or program-image data that differs from the committed preprocessing. +- Prover and verifier must derive the same `ProgramMetadata`, bytecode chunk count, bytecode chunk geometry, program-image geometry, Dory layout, and precommitted scheduling reference. +- `bytecode_chunk_count` must be nonzero, at most `256`, a power of two, and must divide `bytecode_len`. +- The committed bytecode lane layout must encode the same values read by bytecode read-RAF: `rs1`, `rs2`, `rd`, unexpanded PC, immediate, circuit flags, instruction flags, lookup-table selector, and RAF flag. +- Every committed bytecode chunk polynomial must have length `committed_lanes() * (bytecode_len / bytecode_chunk_count)`. +- The program-image polynomial must be the RAM preprocessing bytecode-word slice padded to a power of two, with no semantic rewriting. +- Stage 4 program-image virtual claims must use the same remapped bytecode start address and RAM address challenge as the later program-image claim reduction. +- The precommitted opening-point permutation must be identical for prover, verifier, and Stage 8 RLC construction. +- A precommitted polynomial that does not participate in some cycle or address rounds must contribute exactly one factor of `1/2` per skipped round. +- The cycle-phase handoff scale and full final scale must not be conflated. +- In ZK mode, every `input_claim`, `output_claim_constraint`, and `*_constraint_challenge_values` formula for bytecode, program image, and advice reductions must match the non-ZK claim formula exactly. +- Stage 8 must use the same ordered opening IDs and the same `gamma_i * lagrange_i` coefficients in the prover's BlindFold opening-proof data and in the verifier's BlindFold constraints. + +Keep the implementation PR focused on direct prover/verifier tests and targeted unit tests. +New `jolt-eval` invariants for committed-program equivalence and Stage 8 opening-order consistency are useful follow-up work, but are not required to merge this PR. + +### Non-Goals + +- Do not redesign bytecode expansion or move program construction into `jolt-program`. + That work is covered by `specs/bytecode-expansion-crate.md` and PR [#1490](https://github.com/a16z/jolt/pull/1490). +- Do not change RISC-V instruction semantics, bytecode row semantics, RAM semantics, or advice semantics. +- Do not remove full program mode. +- Do not add separate Dory opening proofs for bytecode or program image. + Committed mode must batch these openings into the existing Stage 8 proof. +- Do not introduce compatibility shims for the old single-stage-6 proof serialization format. +- Do not make committed bytecode the default SDK path in this PR. +- Do not require external consumers to adopt a stable public committed-program API beyond the SDK helpers added for this branch. + +## Evaluation + +### Acceptance Criteria + +- [x] `ProgramPreprocessing::preprocess` still builds full bytecode and RAM preprocessing from decoded program data. +- [x] `ProgramPreprocessing::commit` converts full preprocessing into committed preprocessing by deriving bytecode chunk commitments, program-image commitments, and prover hints. +- [x] Serialized committed verifier preprocessing contains metadata and commitments, not prover-only full program data or opening hints. +- [x] `JoltSharedPreprocessing::new_committed` validates chunking, computes the committed maximum Dory variable count, derives a prover setup, commits program preprocessing, and updates `program_meta`. +- [x] Full mode continues to use the existing bytecode/RAM preprocessing semantics. +- [x] Committed mode appends `BytecodeChunk(i)` and `ProgramImageInit` to the expected proof commitment list. +- [x] Stage 4 caches `ProgramImageInitContributionRw` as a virtual opening when committed program mode is active. +- [x] Stage 6a verifies address-phase bytecode read-RAF and booleanity claims without needing full bytecode materialization on the verifier path. +- [x] Stage 6b includes bytecode and program-image claim reduction instances only in committed mode. +- [x] Stage 6b bytecode claim reduction converts staged `BytecodeValStage(i)` claims into committed bytecode chunk openings. +- [x] Stage 6b program-image claim reduction converts `ProgramImageInitContributionRw` into a committed `ProgramImageInit` opening. +- [x] Stage 7 address-phase reductions resume from the cycle-phase intermediate claims for all precommitted reductions that have address rounds. +- [x] Stage 8 includes bytecode chunks and program image in the random linear combination exactly when `ProgramMode::Committed` is used. +- [x] ZK mode BlindFold constraints include bytecode and program-image opening IDs with coefficients matching Stage 8 RLC coefficients. +- [x] Proof serialization includes `stage6a_sumcheck_proof`, `stage6b_sumcheck_proof`, and the new committed/virtual polynomial tags. +- [x] SDK macro output includes committed prover/shared preprocessing helpers that accept `bytecode_chunk_count`. +- [x] At least one end-to-end Dory test proves and verifies in committed program mode. + +### Testing Strategy + +Run standard and ZK e2e coverage: + +```bash +cargo nextest run -p jolt-core muldiv --cargo-quiet --features host +cargo nextest run -p jolt-core muldiv --cargo-quiet --features host,zk +cargo nextest run -p jolt-core muldiv_e2e_dory_committed_program_commitments --cargo-quiet --features host +cargo nextest run -p jolt-core muldiv_e2e_dory_committed_program_commitments --cargo-quiet --features host,zk +``` + +Run advice/precommitted regression coverage because advice shares the same precommitted scheduling path: + +```bash +cargo nextest run -p jolt-core advice_e2e_dory --cargo-quiet --features host +RUST_MIN_STACK=33554432 cargo nextest run -p jolt-core advice_e2e_dory --cargo-quiet --features host,zk +cargo nextest run -p jolt-core final_advice_output_scale --cargo-quiet --features host +``` + +Run strict linting: + +```bash +cargo clippy --all --features host -q --all-targets -- -D warnings +cargo clippy --all --features host,zk -q --all-targets -- -D warnings +cargo fmt -q +``` + +Targeted tests should cover: + +- invalid bytecode chunk counts, +- bytecode chunk coefficient layout for representative instructions, +- `ProgramPreprocessing::{Full, Committed}` serialization roundtrips, +- proof serialization roundtrips for `BytecodeChunk(i)` and `ProgramImageInit`, +- `PrecommittedClaimReduction` identity and non-identity permutations, +- skipped-round scaling in cycle-only and cycle-plus-address cases, +- Stage 8 opening ID ordering in full mode and committed mode. + +### Performance + +Committed mode should reduce verifier and recursive-verifier work by replacing direct bytecode/program-image evaluation with committed openings. +The prover may pay extra preprocessing and Stage 8 work, but those costs must be batched into the existing Dory opening proof rather than paid as separate PCS proofs. + +Performance-sensitive paths: + +- `build_committed_bytecode_chunk_coeffs` must remain linear in nonzero bytecode lane work and avoid dense per-lane overhead beyond the committed chunk vector itself. +- Program-image commitment derivation should be linear in the padded program-image length. +- Stage 8 streaming RLC construction must consume committed bytecode chunks and the program image through `PrecommittedPolynomial` without regenerating unrelated witness polynomials. +- Verifier Stage 8 must combine commitments homomorphically and must not require committed bytecode or program-image coefficients. + +Benchmarks should compare full mode and committed mode on at least `muldiv`, `sha2`, and one larger bytecode example. +The expected verifier-side direction is improvement for committed mode; prover preprocessing may increase. + +## Design + +### Architecture + +Committed mode extends the program preprocessing pipeline: + +```text +Decoded program data + -> FullProgramPreprocessing { + bytecode: BytecodePreprocessing, + ram: RAMPreprocessing, + } + -> ProgramPreprocessing::Committed { + meta: ProgramMetadata, + bytecode_commitments: TrustedBytecodeCommitments, + program_commitments: TrustedProgramCommitments, + prover_data: Option, + } +``` + +`ProgramMetadata` records the verifier-facing program shape: entry address, minimum bytecode address, program-image word length, and bytecode length. +Committed verifier preprocessing keeps only metadata and commitments. +Committed prover preprocessing may retain full preprocessing and opening hints so the prover can construct witnesses and Stage 8 opening hints. + +### Program Mode And Proof Surface + +`ProgramMode::Full` is the legacy behavior: verifier preprocessing has the full bytecode and program image available. +`ProgramMode::Committed` means bytecode chunks and program image are committed preprocessing objects, and all claims about them must be proven through the protocol. + +The proof and preprocessing surface changes in three places: + +- `JoltSharedPreprocessing` stores `ProgramPreprocessing` and `bytecode_chunk_count`. +- `JoltProof` stores separate `stage6a_sumcheck_proof` and `stage6b_sumcheck_proof`. +- `CommittedPolynomial` and `VirtualPolynomial` serialization add the tags needed for committed bytecode/program-image reductions. + +This is a wire-format change for proof serialization. +Old proofs with a single Stage 6 field are not expected to deserialize. + +### Committed Bytecode Polynomial + +Committed bytecode is represented as one or more chunk polynomials. +Each chunk has a fixed lane capacity: + +```text +committed_lanes() + = next_power_of_two( + 3 * REGISTER_COUNT + + 2 + + NUM_CIRCUIT_FLAGS + + NUM_INSTRUCTION_FLAGS + + number_of_lookup_tables + + 1 + ) +``` + +The lane layout is: + +- one-hot `rs1`, +- one-hot `rs2`, +- one-hot `rd`, +- scalar unexpanded PC, +- scalar immediate, +- circuit flags, +- instruction flags, +- lookup table selector, +- RAF flag. + +For a bytecode table of length `T_bc` split into `C` chunks, each chunk has cycle length `T_bc / C`. +The implementation caps `C` at `256`, matching the `u8` serialization used for `BytecodeChunk(i)`. +The chunk polynomial has dimensions `committed_lanes() * (T_bc / C)`. +The coefficient index is derived through the active Dory layout so that commitment-time layout and opening-time layout agree. + +### Committed Program Image Polynomial + +The program-image polynomial is the initial RAM bytecode-word region from `RAMPreprocessing`. +It is padded to a power of two and committed as `CommittedPolynomial::ProgramImageInit`. + +The verifier does not hold the full program-image word slice in committed mode. +Instead, Stage 4 caches the scalar inner product: + +```text +C_rw = sum_j ProgramWord[j] * eq(r_address_rw, start_index + j) +``` + +The prover computes this from RAM preprocessing and appends it as `VirtualPolynomial::ProgramImageInitContributionRw` under `SumcheckId::RamValCheck`. +The verifier appends the same virtual opening point without the value. +The later program-image claim reduction proves that this staged scalar is consistent with the committed `ProgramImageInit` polynomial. + +### Shared Precommitted Geometry + +Bytecode chunks, program image, trusted advice, and untrusted advice are precommitted polynomials. +They may have fewer or more variables than the main trace-domain polynomials. +`PrecommittedClaimReduction::scheduling_reference` computes a shared reference domain from: + +- main trace-domain total variables `log_T + log_K_chunk`, +- committed bytecode chunk variables, +- committed program-image variables, +- trusted advice variables, +- untrusted advice variables. + +The reference domain determines: + +- `reference_total_vars`, +- cycle alignment rounds, +- address rounds, +- joint column variables, +- each precommitted polynomial's projected Dory opening-round permutation, +- active cycle and address rounds for each precommitted polynomial. + +When a precommitted polynomial is smaller than the reference domain, inactive rounds are skipped by multiplying the running claim by `1/2`. +When a precommitted polynomial dominates the main geometry, Stage 8 is anchored by the dominant precommitted opening point. +If multiple dominant precommitted openings exist, prover and verifier require them to agree. + +### Proving-System Stage Changes + +#### Preprocessing + +Full preprocessing still produces bytecode preprocessing and RAM preprocessing. +Committed preprocessing starts from the same full preprocessing, then: + +1. derives Dory commitments for bytecode chunks, +2. derives a Dory commitment for the program image, +3. stores metadata and commitments for verifier preprocessing, +4. stores full preprocessing and Dory opening hints only for prover preprocessing. + +Bytecode chunk commitments are derived sequentially under one Dory context because Dory context selection is process-global. +The default chunk count is `1`, so this does not remove parallelism from the default committed path. + +The shared preprocessing digest binds the serialized committed preprocessing to the Fiat-Shamir transcript. + +#### Stage 4: RAM Val Check + +Committed mode adds a program-image virtual claim to the RAM val-check flow. +After `RamVal` is opened at the read-write point, prover and verifier split out the RAM address component. +The prover evaluates the initial program-image word slice against that address equality polynomial and appends the scalar as `ProgramImageInitContributionRw`. +The verifier appends the same opening point so later constraints can refer to it. + +This does not replace the RAM val check. +It stages a program-image scalar that will be bound to the committed program-image polynomial by a later claim reduction. + +#### Stage 6a: Address-Phase Bytecode RAF And Booleanity + +Stage 6 is split into `stage6a` and `stage6b`. +Stage 6a handles address-phase work for bytecode read-RAF and booleanity. + +In committed mode, bytecode read-RAF verifier construction does not require full bytecode preprocessing. +Instead, address-phase bytecode read-RAF stages the `BytecodeValStage(i)` virtual claims that summarize the bytecode value columns needed later. +These staged values become the input claims to `BytecodeClaimReduction`. + +#### Stage 6b: Cycle-Phase Work And Precommitted Claim Reductions + +Stage 6b batches the remaining cycle-oriented sumchecks and all precommitted claim reductions. +The base Stage 6b batch still includes: + +- bytecode read-RAF cycle phase, +- booleanity cycle phase, +- RAM hamming booleanity, +- RAM RA virtualization, +- instruction RA virtualization, +- increment claim reduction. + +When advice commitments are present, Stage 6b also includes trusted and/or untrusted `AdviceClaimReduction`. +When committed program mode is active, Stage 6b also includes: + +- `BytecodeClaimReduction`, +- `ProgramImageClaimReduction`. + +`BytecodeClaimReduction` input in cycle phase is: + +```text +sum_i eta^i * BytecodeValStage(i) +``` + +where `eta` is sampled from the transcript. +The output is either an intermediate virtual claim at `BytecodeClaimReductionCyclePhase` or final committed bytecode chunk openings if no address phase remains. + +`ProgramImageClaimReduction` input in cycle phase is `ProgramImageInitContributionRw`. +The output is either an intermediate committed claim under `ProgramImageClaimReductionCyclePhase` or the final committed `ProgramImageInit` opening if no address phase remains. + +#### Stage 7: Address-Phase Claim Reduction Completion + +Stage 7 completes address-phase rounds for reductions that still have address variables. +For bytecode, the address phase reduces the intermediate claim into openings of `CommittedPolynomial::BytecodeChunk(i)`. +For program image, the address phase reduces the intermediate claim into an opening of `CommittedPolynomial::ProgramImageInit`. +For advice, the address phase reduces the cycle handoff claim into the final advice opening. + +All three use the same precommitted scheduling abstraction, so their opening points are normalized consistently for Stage 8. + +#### Stage 8: Batched Dory Opening + +Stage 8 constructs one unified opening point. +It then gathers claims for: + +- `RamInc`, +- `RdInc`, +- instruction RA polynomials, +- bytecode RA polynomials, +- RAM RA polynomials, +- optional trusted advice, +- optional untrusted advice, +- committed bytecode chunks in committed mode, +- committed program image in committed mode. + +Each claim whose natural opening point is smaller than the unified Dory point is multiplied by `compute_lagrange_factor(unified_point, polynomial_point)`. +The transcript samples `gamma` powers after non-ZK claims are absorbed. +The prover constructs the streaming RLC polynomial with all main and precommitted polynomials and proves one Dory opening at the unified point. +The verifier computes the same joint commitment homomorphically from proof commitments, trusted advice commitments, bytecode chunk commitments, and program-image commitments. + +In ZK mode, the Stage 8 claim values remain hidden. +Instead, BlindFold receives: + +```text +OpeningProofData { + opening_ids, + constraint_coeffs = gamma_i * lagrange_i, + joint_claim, + y_blinding, +} +``` + +The verifier builds the same `opening_ids` list through `stage8_opening_ids`. +In committed mode, this list appends each `BytecodeChunk(i)` at `SumcheckId::BytecodeClaimReduction` and `ProgramImageInit` at `SumcheckId::ProgramImageClaimReduction`. + +### Alternatives Considered + +The verifier could keep direct access to full bytecode and program-image data. +That preserves the old implementation but does not reduce recursive verifier cost. + +The prover could produce separate Dory openings for bytecode and program image. +That is simpler locally but loses Stage 8 batching and adds verifier work. + +Bytecode, program image, and advice could each use bespoke scheduling. +The PR instead uses one precommitted scheduling abstraction so all non-main committed objects share the same Dory embedding logic. + +## Documentation + +The Jolt book should document: + +- the difference between full and committed program modes, +- why bytecode and program image are precommitted polynomials, +- how precommitted geometry changes the Stage 8 Dory opening point, +- how dominant precommitted polynomials anchor Stage 8, +- how committed bytecode chunk count affects preprocessing. + +This PR already expands `book/src/how/architecture/opening-proof.md` with a precommitted geometry section. +Follow-up documentation should add a user-facing example for `--committed-bytecode ` and guidance for choosing the chunk count. + +The module comments in `jolt-core/src/zkvm/transpilable_verifier.rs` should also be updated to describe Stage 6a and Stage 6b instead of the old monolithic Stage 6. + +## Execution + +Implementation is organized as: + +1. Add committed program metadata and preprocessing in `jolt-core/src/zkvm/program.rs`. +2. Add committed bytecode lane layout and chunk coefficient construction in `jolt-core/src/zkvm/bytecode/chunks.rs`. +3. Add `BytecodeChunk(i)` and `ProgramImageInit` committed polynomial variants. +4. Add bytecode and program-image virtual polynomial IDs for staged claims. +5. Add shared precommitted scheduling in `jolt-core/src/zkvm/claim_reductions/precommitted.rs`. +6. Add `BytecodeClaimReduction` over staged bytecode val claims and committed bytecode chunks. +7. Add `ProgramImageClaimReduction` over staged program-image RAM contribution and committed program-image opening. +8. Wire committed-mode reductions into Stage 6b and Stage 7. +9. Wire committed-mode openings into Stage 8 RLC and BlindFold opening-proof constraints. +10. Add SDK committed preprocessing helpers and canonical `fibonacci` / `muldiv` example CLI paths. +11. Add committed-mode e2e tests and precommitted scheduling regression tests. + +## References + +- PR [#1344](https://github.com/a16z/jolt/pull/1344) +- Related program preprocessing spec: `specs/bytecode-expansion-crate.md` +- Spec template: `specs/TEMPLATE.md` +- Opening proof documentation: `book/src/how/architecture/opening-proof.md` +- Core committed program code: `jolt-core/src/zkvm/program.rs` +- Committed bytecode code: `jolt-core/src/zkvm/bytecode/chunks.rs` +- Precommitted scheduling: `jolt-core/src/zkvm/claim_reductions/precommitted.rs` +- Bytecode claim reduction: `jolt-core/src/zkvm/claim_reductions/bytecode.rs` +- Program-image claim reduction: `jolt-core/src/zkvm/claim_reductions/program_image.rs` From 8e0ed1ed7017f6e4eb2b0cd20c60dcae442cc15a Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 27 May 2026 08:37:46 -0700 Subject: [PATCH 03/24] refactor: split stage 6 sumchecks (#1571) * chore: add committed bytecode stack plan Co-authored-by: Cursor * chore: add committed bytecode feature spec Co-authored-by: Cursor * chore: rename precommitted stack branch Co-authored-by: Cursor * chore(sumcheck): remove debug sanity checker Co-authored-by: Cursor * chore(sumcheck): match upstream main Co-authored-by: Cursor * docs(spec): align committed bytecode stack plan Update the stack spec to match the final branch slices, moved claim-reduction implementation scope, and expanded local CI coverage. Co-authored-by: Cursor * docs(spec): leave committed bytecode feature spec unchanged Restore the feature spec text to its original contents; only the stack plan should describe the updated PR split and local CI scope. Co-authored-by: Cursor * docs(spec): remove stack verification sections Keep the stack plan focused on PR scope and ownership; verification details are tracked outside this spec. Co-authored-by: Cursor * 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 (cherry picked from commit e9ec21c7b2b0e95f30ba3570ad802005449ceed7) Co-authored-by: Amirhossein Khajehpour Co-authored-by: Cursor * fix(zkvm): import verifier opening accumulator in stage6 split Co-authored-by: Cursor * fix(zkvm): make stage6 split verifier impls generic Co-authored-by: Cursor * fix(zkvm): update stage6 split serialization and transpiler Co-authored-by: Cursor * fix(zkvm): accept generic accumulators in stage6 verifier constructors Co-authored-by: Cursor * style: format stage6 split branch Co-authored-by: Cursor * fix(zkvm): update transpiler proof symbolization for stage6 split Co-authored-by: Cursor * refactor(zkvm): align stage6 sumcheck phases with final structure Co-authored-by: Cursor * fix(zkvm): keep booleanity exports module-local Co-authored-by: Cursor * fix(zkvm): pass stage6a bytecode read-raf state Co-authored-by: Cursor * fix(zkvm): align stage6 booleanity handoff Keep stage6a on the standard batched sumcheck path and derive cycle-phase booleanity params in stage6b so the split matches the reference branch. Made-with: Cursor Co-authored-by: Cursor * fix(zkvm): include stage6a in blindfold verification Treat stage6a as its own BlindFold stage after splitting stage6 so prover and verifier agree on the ZK stage layout. Made-with: Cursor Co-authored-by: Cursor * fix(advice): align round offsets with stage6 split Co-authored-by: Cursor * Avoid duplicate booleanity RA index computation Reuse the RA indices computed during the booleanity address phase by passing an opaque cycle input into the cycle phase, keeping prover orchestration unaware of booleanity internals. --------- Co-authored-by: Cursor Co-authored-by: Quang Dao --- jolt-core/src/poly/opening_proof.rs | 2 + jolt-core/src/subprotocols/booleanity.rs | 694 ++++-- jolt-core/src/subprotocols/mod.rs | 4 - .../src/zkvm/bytecode/read_raf_checking.rs | 2170 +++++++++-------- jolt-core/src/zkvm/claim_reductions/advice.rs | 25 +- jolt-core/src/zkvm/proof_serialization.rs | 14 +- jolt-core/src/zkvm/prover.rs | 125 +- jolt-core/src/zkvm/transpilable_verifier.rs | 65 +- jolt-core/src/zkvm/verifier.rs | 173 +- jolt-core/src/zkvm/witness.rs | 2 + transpiler/src/symbolic_proof.rs | 18 +- 11 files changed, 1951 insertions(+), 1341 deletions(-) diff --git a/jolt-core/src/poly/opening_proof.rs b/jolt-core/src/poly/opening_proof.rs index caaa32e72a..430de9b9a6 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 192fc39e4e..e6f99dd5f0 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 //! @@ -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, @@ -84,82 +83,6 @@ pub struct BooleanitySumcheckParams { pub one_hot_params: OneHotParams, } -impl SumcheckInstanceParams for BooleanitySumcheckParams { - fn degree(&self) -> usize { - DEGREE_BOUND - } - - fn num_rounds(&self) -> usize { - self.log_k_chunk + self.log_t - } - - fn input_claim(&self, _accumulator: &dyn OpeningAccumulator) -> F { - F::zero() - } - - fn normalize_opening_point( - &self, - sumcheck_challenges: &[F::Challenge], - ) -> OpeningPoint { - let mut opening_point = sumcheck_challenges.to_vec(); - opening_point[..self.log_k_chunk].reverse(); - opening_point[self.log_k_chunk..].reverse(); - opening_point.into() - } - - #[cfg(feature = "zk")] - fn input_claim_constraint(&self) -> InputClaimConstraint { - InputClaimConstraint::default() - } - - #[cfg(feature = "zk")] - fn input_constraint_challenge_values(&self, _: &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); - 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)], - )); - terms.push(ProductTerm::scaled( - ValueSource::Challenge(2 * i + 1), - 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 mut challenges = Vec::with_capacity(2 * self.polynomial_types.len()); - for gamma_2i in &self.gamma_powers_square { - let coeff = eq_eval * *gamma_2i; - challenges.push(coeff); - challenges.push(-coeff); - } - challenges - } -} - impl BooleanitySumcheckParams { /// Create booleanity params by taking r_cycle and r_address from Stage 5. /// @@ -191,19 +114,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,49 +174,68 @@ 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. #[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 BooleanityCycleInput { + params: BooleanitySumcheckParams, + ra_indices: Vec, +} + +/// Booleanity address-phase prover. +#[derive(Allocative)] +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: ExpandingTable, - /// eq(r_address, r_address) at end of phase 1 - eq_r_r: F, - /// RA indices (non-transposed, one per cycle) + /// RA indices computed alongside `G`, reused by the cycle phase. ra_indices: Vec, - pub params: BooleanitySumcheckParams, + /// F: Expanding table over address bits for phase 1. + F: ExpandingTable, + /// 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, + /// Address-only `SumcheckInstanceParams` wrapper. + 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::( trace, bytecode, @@ -311,55 +243,45 @@ impl BooleanitySumcheckProver { ¶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(), - params, + last_round_poly: None, + address_claim: None, + params: BooleanityAddressPhaseParams::new(params), } } - fn compute_phase1_message(&self, round: usize, previous_claim: F) -> UniPoly { - let m = round + 1; - let B = &self.B; - let N = self.params.polynomial_types.len(); + pub fn into_cycle_input(self) -> BooleanityCycleInput { + BooleanityCycleInput { + params: self.params.into_inner(), + ra_indices: self.ra_indices, + } + } +} + +impl SumcheckInstanceProver + for BooleanityAddressSumcheckProver +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.params + } - // Compute quadratic coefficients via generic split-eq fold - let quadratic_coeffs: [F; DEGREE_BOUND - 1] = B + fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { + let m = round + 1; + let n = self.params.common.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 +292,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 @@ -393,7 +314,7 @@ impl BooleanitySumcheckProver { |running, new| [running[0] + new[0], running[1] + new[1]], ); - let gamma_2i = self.params.gamma_powers_square[i]; + let gamma_2i = self.params.common.gamma_powers_square[i]; [ gamma_2i * F::reduce_mul_u64(inner_sum[0]), gamma_2i * F::reduce_mul_u64(inner_sum[1]), @@ -402,29 +323,131 @@ 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) + let poly = + self.B + .gruen_poly_deg_3(quadratic_coeffs[0], quadratic_coeffs[1], 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.params.common.log_k_chunk - 1 { + self.address_claim = Some(claim); + } + } + self.B.bind(r_j); + self.F.update(r_j); + } + + fn cache_openings( + &self, + 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( + 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); + } +} + +/// Booleanity cycle-phase prover. +#[derive(Allocative)] +pub struct BooleanityCycleSumcheckProver { + /// 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, + /// Cycle-only `SumcheckInstanceParams` wrapper. + params: BooleanityCyclePhaseParams, +} + +impl BooleanityCycleSumcheckProver { + /// Initialize cycle-phase state from the cached address-phase opening. + pub fn initialize( + input: BooleanityCycleInput, + opening_accumulator: &dyn OpeningAccumulator, + ) -> Self { + let params = BooleanityCyclePhaseParams::new(input.params, opening_accumulator); + let (eq_r_r, base_eq) = Self::compute_bound_address_eq_and_table(¶ms); + let num_polys = params.common.polynomial_types.len(); + let (gamma_powers, gamma_powers_inv) = compute_gamma_powers(params.common.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 { + D: GruenSplitEqPolynomial::new(¶ms.common.r_cycle, BindingOrder::LowToHigh), + H: SharedRaPolynomials::new( + tables, + input.ra_indices, + params.common.one_hot_params.clone(), + ), + eq_r_r, + gamma_powers, + gamma_powers_inv, + params, + } } - 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(); + fn compute_bound_address_eq_and_table(params: &BooleanityCyclePhaseParams) -> (F, Vec) { + let mut B = GruenSplitEqPolynomial::new(¶ms.common.r_address, BindingOrder::LowToHigh); + let k_chunk = 1 << params.common.log_k_chunk; + let mut F_table = ExpandingTable::new(k_chunk, BindingOrder::LowToHigh); + F_table.reset(F::one()); + for r_j in params.r_address_low_to_high.iter().copied() { + B.bind(r_j); + F_table.update(r_j); + } + (B.get_current_scalar(), F_table.clone_values()) + } +} - // Compute quadratic coefficients via generic split-eq fold (handles both E_in cases). - let quadratic_coeffs: [F; DEGREE_BOUND - 1] = D +impl SumcheckInstanceProver + for BooleanityCycleSumcheckProver +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.params + } + + fn compute_message(&mut self, _round: usize, 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 + // 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 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) @@ -438,76 +461,17 @@ impl BooleanitySumcheckProver { 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); - + self.D + .gruen_poly_deg_3(quadratic_coeffs[0], quadratic_coeffs[1], adjusted_claim); gruen_poly * self.eq_r_r } -} - -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 { - // 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); - } - } 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); - } - } + fn ingest_challenge(&mut self, r_j: F::Challenge, _round: usize) { + self.D.bind(r_j); + self.H.bind_in_place(r_j, BindingOrder::LowToHigh); } fn cache_openings( @@ -516,19 +480,15 @@ impl SumcheckInstanceProver for BooleanitySum 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]) + let claims: Vec = (0..self.H.num_polys()) + .map(|i| self.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(), + self.params.common.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(), + opening_point.r[..self.params.common.log_k_chunk].to_vec(), + opening_point.r[self.params.common.log_k_chunk..].to_vec(), claims, ); } @@ -539,27 +499,78 @@ impl SumcheckInstanceProver for BooleanitySum } } -/// Booleanity Sumcheck Verifier. -pub struct BooleanitySumcheckVerifier { - params: BooleanitySumcheckParams, +/// Booleanity address-phase verifier. +pub struct BooleanityAddressSumcheckVerifier { + params: BooleanityAddressPhaseParams, } -impl BooleanitySumcheckVerifier { +impl BooleanityAddressSumcheckVerifier { pub fn new(params: BooleanitySumcheckParams) -> Self { - Self { params } + Self { + params: BooleanityAddressPhaseParams::new(params), + } + } + + pub fn into_params(self) -> BooleanitySumcheckParams { + self.params.into_inner() } } impl> - SumcheckInstanceVerifier for BooleanitySumcheckVerifier + SumcheckInstanceVerifier for BooleanityAddressSumcheckVerifier +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.params + } + + fn expected_output_claim(&self, accumulator: &A, _sumcheck_challenges: &[F::Challenge]) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ) + .1 + } + + fn cache_openings(&self, accumulator: &mut A, 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), + ); + } +} + +/// Booleanity cycle-phase verifier. +pub struct BooleanityCycleSumcheckVerifier { + params: BooleanityCyclePhaseParams, +} + +impl BooleanityCycleSumcheckVerifier { + pub fn new( + params: BooleanitySumcheckParams, + opening_accumulator: &dyn OpeningAccumulator, + ) -> Self { + Self { + params: BooleanityCyclePhaseParams::new(params, opening_accumulator), + } + } +} + +impl> + SumcheckInstanceVerifier for BooleanityCycleSumcheckVerifier { fn get_params(&self) -> &dyn SumcheckInstanceParams { &self.params } fn expected_output_claim(&self, accumulator: &A, sumcheck_challenges: &[F::Challenge]) -> F { + let full_challenges = self.params.full_challenges(sumcheck_challenges); let ra_claims: Vec = self .params + .common .polynomial_types .iter() .map(|poly_type| { @@ -568,28 +579,183 @@ impl> .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::() + EqPolynomial::::mle( + &full_challenges, + &self.params.common.combined_r_big_endian(), + ) * zip(&self.params.common.gamma_powers_square, ra_claims) + .map(|(gamma_2i, ra)| (ra.square() - ra) * gamma_2i) + .sum::() } fn cache_openings(&self, accumulator: &mut A, sumcheck_challenges: &[F::Challenge]) { let opening_point = self.params.normalize_opening_point(sumcheck_challenges); accumulator.append_sparse( - self.params.polynomial_types.clone(), + self.params.common.polynomial_types.clone(), SumcheckId::Booleanity, opening_point.r, ); } } + +#[derive(Allocative, Clone)] +struct BooleanityAddressPhaseParams { + common: BooleanitySumcheckParams, +} + +impl BooleanityAddressPhaseParams { + fn new(common: BooleanitySumcheckParams) -> Self { + Self { common } + } + + fn into_inner(self) -> BooleanitySumcheckParams { + self.common + } +} + +impl SumcheckInstanceParams for BooleanityAddressPhaseParams { + fn degree(&self) -> usize { + DEGREE_BOUND + } + + fn num_rounds(&self) -> usize { + self.common.log_k_chunk + } + + fn input_claim(&self, _accumulator: &dyn OpeningAccumulator) -> F { + F::zero() + } + + 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 { + InputClaimConstraint::default() + } + + #[cfg(feature = "zk")] + fn input_constraint_challenge_values( + &self, + _accumulator: &dyn OpeningAccumulator, + ) -> Vec { + Vec::new() + } + + #[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)] +pub struct BooleanityCyclePhaseParams { + common: BooleanitySumcheckParams, + r_address_low_to_high: Vec, +} + +impl BooleanityCyclePhaseParams { + pub fn new( + common: BooleanitySumcheckParams, + opening_accumulator: &dyn OpeningAccumulator, + ) -> 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(); + + Self { + common, + 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 { + DEGREE_BOUND + } + + fn num_rounds(&self) -> usize { + self.common.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 mut opening_point = self.full_challenges(challenges); + opening_point[..self.common.log_k_chunk].reverse(); + opening_point[self.common.log_k_chunk..].reverse(); + opening_point.into() + } + + #[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 { + let mut terms = Vec::with_capacity(2 * self.common.polynomial_types.len()); + for (i, poly_type) in self.common.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)], + )); + terms.push(ProductTerm::scaled( + ValueSource::Challenge(2 * i + 1), + 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 full = self.full_challenges(sumcheck_challenges); + let eq_eval: F = EqPolynomial::::mle(&full, &self.common.combined_r_big_endian()); + let mut challenges = Vec::with_capacity(2 * self.common.polynomial_types.len()); + for gamma_2i in &self.common.gamma_powers_square { + let coeff = eq_eval * *gamma_2i; + challenges.push(coeff); + challenges.push(-coeff); + } + challenges + } +} 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/zkvm/bytecode/read_raf_checking.rs b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs index 4da681489c..358b30c6f7 100644 --- a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs +++ b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs @@ -1,4 +1,9 @@ -use std::{array, iter::once, sync::Arc}; +use std::{ + array, + iter::once, + ops::{Deref, DerefMut}, + sync::Arc, +}; use num_traits::Zero; @@ -117,46 +122,26 @@ const N_STAGES: usize = 5; /// in the Stage3 per-stage claim, then the stage itself is folded with an outer factor `γ^2`, /// yielding the advertised `γ^6` overall. #[derive(Allocative)] -pub struct BytecodeReadRafSumcheckProver { +pub struct BytecodeReadRafAddressSumcheckProver { /// Per-stage address MLEs F_i(k) built from eq(r_cycle_stage_i, (chunk_index, j)), /// bound low-to-high during the address-binding phase. F: [MultilinearPolynomial; N_STAGES], - /// Chunked RA polynomials over address variables (one per dimension `d`), used to form - /// the product ∏_i ra_i during the cycle-binding phase. - ra: Vec>, - /// Binding challenges for the first log_K variables of the sumcheck - r_address_prime: Vec, - /// Per-stage Gruen-split eq polynomials over cycle vars (low-to-high binding order). - gruen_eq_polys: [GruenSplitEqPolynomial; N_STAGES], /// Previous-round claims s_i(0)+s_i(1) per stage, needed for degree-(d+1) univariate recovery. prev_round_claims: [F; N_STAGES], /// Round polynomials per stage for advancing to the next claim at r_j. prev_round_polys: Option<[UniPoly; N_STAGES]>, - /// Final sumcheck claims of stage Val polynomials (with RAF Int folded where applicable). - bound_val_evals: Option<[F; N_STAGES]>, /// f_entry_trace[k] = Ra(k, 0): one-hot at PC of cycle 0 (from trace). f_entry_trace: MultilinearPolynomial, /// f_entry_expected[k] = C(k): one-hot at entry_bytecode_index (from preprocessing). /// Product f_entry_trace * f_entry_expected = Ra(k,0) * C(k); sums to 1 iff PC[0] = entry_bytecode_index. f_entry_expected: MultilinearPolynomial, - /// eq_zero(j) indicator (r_cycle = all zeros), used in the cycle phase. - gruen_eq_entry: GruenSplitEqPolynomial, - /// f_entry_expected bound at r_addr after the address phase (cached for output claim). - bound_f_entry: Option, /// Running entry claim over remaining free variables. prev_entry_claim: F, prev_entry_poly: Option>, - /// Trace for computing PCs on the fly in init_log_t_rounds. - #[allocative(skip)] - trace: Arc>, - /// Bytecode preprocessing for computing PCs. - #[allocative(skip)] - bytecode_preprocessing: Arc, - pub params: BytecodeReadRafSumcheckParams, + params: BytecodeReadRafAddressPhaseParams, } -impl BytecodeReadRafSumcheckProver { - #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckProver::initialize")] +impl BytecodeReadRafAddressSumcheckProver { pub fn initialize( params: BytecodeReadRafSumcheckParams, trace: Arc>, @@ -305,11 +290,6 @@ impl BytecodeReadRafSumcheckProver { let F = F.map(MultilinearPolynomial::from); - let gruen_eq_polys = params - .r_cycles - .each_ref() - .map(|r_cycle| GruenSplitEqPolynomial::new(r_cycle, BindingOrder::LowToHigh)); - let pc_0 = super::get_pc_for_cycle(&bytecode_preprocessing, &trace[0]); assert!( pc_0 < params.K, @@ -330,120 +310,54 @@ impl BytecodeReadRafSumcheckProver { f_entry_expected_vec[entry_bytecode_index] = F::one(); let f_entry_expected = MultilinearPolynomial::from(f_entry_expected_vec); - // eq_zero(j) = ∏(1 - j_i): indicator for cycle j = 0. r_cycle = all zeros. - let r_cycle_zero = vec![F::Challenge::default(); params.log_T]; - let gruen_eq_entry = GruenSplitEqPolynomial::new(&r_cycle_zero, BindingOrder::LowToHigh); - Self { F, f_entry_trace, f_entry_expected, - gruen_eq_entry, - bound_f_entry: None, // Initial entry claim = Σ_k f_entry_trace[k] * f_entry_expected[k] = 1 for honest prover // (both one-hots at same index when PC[0] = entry_bytecode_index). prev_entry_claim: F::one(), prev_entry_poly: None, - ra: Vec::with_capacity(params.d), - r_address_prime: Vec::with_capacity(params.log_K), - gruen_eq_polys, prev_round_claims: claim_per_stage, prev_round_polys: None, - bound_val_evals: None, - trace, - bytecode_preprocessing, - params, + params: BytecodeReadRafAddressPhaseParams::new(params), } } - fn init_log_t_rounds(&mut self) { - let int_poly = self.params.int_poly.final_sumcheck_claim(); - - // We have a separate Val polynomial for each stage - // Additionally, for stages 1 and 3 we have an Int polynomial for RAF - // So we would have: - // Stage 1: gamma^0 * (Val_1 + gamma^5 * Int) - // Stage 2: gamma^1 * (Val_2) - // Stage 3: gamma^2 * (Val_3 + gamma^4 * Int) - // Stage 4: gamma^3 * (Val_4) - // 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 bound_val_evals: [F; N_STAGES] = self - .params - .val_polys - .iter() - .zip([ - int_poly * self.params.gamma_powers[5], - F::zero(), - int_poly * self.params.gamma_powers[4], - F::zero(), - F::zero(), - ]) - .map(|(poly, int_poly)| poly.final_sumcheck_claim() + int_poly) - .collect::>() - .try_into() - .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); - self.params.bound_f_entry = Some(bound_f_entry); - - // Reverse r_address_prime to get the correct order (it was built low-to-high) - let mut r_address = std::mem::take(&mut self.r_address_prime); - r_address.reverse(); - - self.F = array::from_fn(|_| MultilinearPolynomial::default()); - self.f_entry_trace = MultilinearPolynomial::default(); - self.f_entry_expected = MultilinearPolynomial::default(); - self.params.val_polys = array::from_fn(|_| MultilinearPolynomial::default()); - self.params.int_poly = IdentityPolynomial::new(0); - - let r_address_chunks = self - .params - .one_hot_params - .compute_r_address_chunks::(&r_address); - - // Build RA polynomials by iterating over trace and computing PCs on the fly - self.ra = r_address_chunks - .iter() - .enumerate() - .map(|(i, r_address_chunk)| { - let ra_i: Vec> = self - .trace - .par_iter() - .map(|cycle| { - let pc = super::get_pc_for_cycle(&self.bytecode_preprocessing, cycle); - Some(self.params.one_hot_params.bytecode_pc_chunk(pc, i)) - }) - .collect(); - RaPolynomial::new(Arc::new(ra_i), EqPolynomial::evals(r_address_chunk)) - }) - .collect(); - - // Drop trace and preprocessing - no longer needed after this - self.trace = Arc::new(Vec::new()); + pub fn into_params(self) -> BytecodeReadRafSumcheckParams { + let mut params = self.params.into_inner(); + params.cycle_initial_round_claims = Some(self.prev_round_claims); + params.cycle_initial_entry_claim = Some(self.prev_entry_claim); + params } } impl SumcheckInstanceProver - for BytecodeReadRafSumcheckProver + for BytecodeReadRafAddressSumcheckProver { fn get_params(&self) -> &dyn SumcheckInstanceParams { &self.params } - #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckProver::compute_message")] + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.params.log_K + } + + fn input_claim(&self, accumulator: &ProverOpeningAccumulator) -> F { + self.params.input_claim(accumulator) + } + fn compute_message(&mut self, round: usize, _previous_claim: F) -> UniPoly { - if round < self.params.log_K { - const DEGREE: usize = 2; + debug_assert!(round < self.params.log_K); + const DEGREE: usize = 2; - // Evaluation at [0, 2] for each stage plus the entry term. - let (eval_per_stage, entry_evals): ([[F; DEGREE]; N_STAGES], [F; DEGREE]) = - (0..self.params.val_polys[0].len() / 2) + // Evaluation at [0, 2] for each stage plus the entry term. + let (eval_per_stage, entry_evals): ([[F; DEGREE]; N_STAGES], [F; DEGREE]) = + (0..self.params.val_polys[0].len() / 2) .into_par_iter() .map(|i| { let ra_evals = self.F.each_ref().map(|poly| { @@ -501,135 +415,334 @@ impl SumcheckInstanceProver ), ); - let mut round_polys: [_; N_STAGES] = array::from_fn(|_| UniPoly::zero()); - let mut agg_round_poly = UniPoly::zero(); + let mut round_polys: [_; N_STAGES] = array::from_fn(|_| UniPoly::zero()); + let mut agg_round_poly = UniPoly::zero(); - for (stage, evals) in eval_per_stage.into_iter().enumerate() { - let [eval_at_0, eval_at_2] = evals; - let eval_at_1 = self.prev_round_claims[stage] - eval_at_0; - let round_poly = UniPoly::from_evals(&[eval_at_0, eval_at_1, eval_at_2]); - agg_round_poly += &(&round_poly * self.params.gamma_powers[stage]); - round_polys[stage] = round_poly; - } + for (stage, evals) in eval_per_stage.into_iter().enumerate() { + let [eval_at_0, eval_at_2] = evals; + let eval_at_1 = self.prev_round_claims[stage] - eval_at_0; + let round_poly = UniPoly::from_evals(&[eval_at_0, eval_at_1, eval_at_2]); + agg_round_poly += &(&round_poly * self.params.gamma_powers[stage]); + round_polys[stage] = round_poly; + } - let [entry_at_0, entry_at_2] = entry_evals; - let entry_at_1 = self.prev_entry_claim - entry_at_0; - let entry_round_poly = UniPoly::from_evals(&[entry_at_0, entry_at_1, entry_at_2]); - agg_round_poly += &(&entry_round_poly * self.params.entry_gamma); - self.prev_entry_poly = Some(entry_round_poly); + let [entry_at_0, entry_at_2] = entry_evals; + let entry_at_1 = self.prev_entry_claim - entry_at_0; + let entry_round_poly = UniPoly::from_evals(&[entry_at_0, entry_at_1, entry_at_2]); + agg_round_poly += &(&entry_round_poly * self.params.entry_gamma); + self.prev_entry_poly = Some(entry_round_poly); - self.prev_round_polys = Some(round_polys); + self.prev_round_polys = Some(round_polys); - agg_round_poly - } else { - let degree = >::degree(self); + agg_round_poly + } - let out_len = self.gruen_eq_polys[0].E_out_current().len(); - let in_len = self.gruen_eq_polys[0].E_in_current().len(); - let in_n_vars = in_len.log_2(); + fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { + debug_assert!(round < self.params.log_K); + if let Some(prev_round_polys) = self.prev_round_polys.take() { + self.prev_round_claims = prev_round_polys.map(|poly| poly.evaluate(&r_j)); + } + if let Some(entry_poly) = self.prev_entry_poly.take() { + self.prev_entry_claim = entry_poly.evaluate(&r_j); + } - // Evaluations on [1, ..., degree - 2, inf] (for each stage + entry term). - let (mut evals_per_stage, mut entry_evals_raw): ([Vec; N_STAGES], Vec) = (0 - ..out_len) - .into_par_iter() - .map(|j_hi| { - let mut ra_eval_pairs = vec![(F::zero(), F::zero()); self.ra.len()]; - let mut ra_prod_evals = vec![F::zero(); degree - 1]; - let mut evals_per_stage: [_; N_STAGES] = - array::from_fn(|_| vec![F::UnreducedProductAccum::zero(); degree - 1]); - let mut entry_accum = vec![F::UnreducedProductAccum::zero(); degree - 1]; - - for j_lo in 0..in_len { - let j = j_lo + (j_hi << in_n_vars); - - for (i, ra_i) in self.ra.iter().enumerate() { - let ra_i_eval_at_j_0 = ra_i.get_bound_coeff(j * 2); - let ra_i_eval_at_j_1 = ra_i.get_bound_coeff(j * 2 + 1); - ra_eval_pairs[i] = (ra_i_eval_at_j_0, ra_i_eval_at_j_1); - } - eval_linear_prod_assign(&ra_eval_pairs, &mut ra_prod_evals); + self.params + .val_polys + .iter_mut() + .for_each(|poly| poly.bind_parallel(r_j, BindingOrder::LowToHigh)); + self.params + .int_poly + .bind_parallel(r_j, BindingOrder::LowToHigh); + self.F + .iter_mut() + .for_each(|poly| poly.bind_parallel(r_j, BindingOrder::LowToHigh)); + self.f_entry_trace + .bind_parallel(r_j, BindingOrder::LowToHigh); + self.f_entry_expected + .bind_parallel(r_j, BindingOrder::LowToHigh); + if round == self.params.log_K - 1 { + let int_poly = self.params.int_poly.final_sumcheck_claim(); + let bound_val_evals: [F; N_STAGES] = self + .params + .val_polys + .iter() + .zip([ + int_poly * self.params.gamma_powers[5], + F::zero(), + int_poly * self.params.gamma_powers[4], + F::zero(), + F::zero(), + ]) + .map(|(poly, int_poly)| poly.final_sumcheck_claim() + int_poly) + .collect::>() + .try_into() + .unwrap(); + self.params.bound_val_polys = Some(bound_val_evals); + self.params.bound_f_entry = Some(self.f_entry_expected.final_sumcheck_claim()); + } + } - for stage in 0..N_STAGES { - let eq_in_eval = self.gruen_eq_polys[stage].E_in_current()[j_lo]; - for i in 0..degree - 1 { - evals_per_stage[stage][i] += - eq_in_eval.mul_to_product_accum(ra_prod_evals[i]); - } - } + 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 = self + .prev_round_claims + .iter() + .zip(self.params.gamma_powers.iter()) + .take(N_STAGES) + .map(|(claim, gamma)| *claim * *gamma) + .sum::() + + self.params.entry_gamma * self.prev_entry_claim; + accumulator.append_virtual( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + opening_point.clone(), + address_claim, + ); + } + + #[cfg(feature = "allocative")] + fn update_flamegraph(&self, flamegraph: &mut FlameGraphBuilder) { + flamegraph.visit_root(self); + } +} + +#[derive(Allocative)] +pub struct BytecodeReadRafCycleSumcheckProver { + /// Chunked RA polynomials over address variables (one per dimension `d`), used to form + /// the product ∏_i ra_i during the cycle-binding phase. + ra: Vec>, + /// Per-stage Gruen-split eq polynomials over cycle vars (low-to-high binding order). + gruen_eq_polys: [GruenSplitEqPolynomial; N_STAGES], + /// Previous-round claims s_i(0)+s_i(1) per stage, needed for degree-(d+1) univariate recovery. + prev_round_claims: [F; N_STAGES], + /// Round polynomials per stage for advancing to the next claim at r_j. + prev_round_polys: Option<[UniPoly; N_STAGES]>, + /// Final sumcheck claims of stage Val polynomials (with RAF Int folded where applicable). + bound_val_evals: [F; N_STAGES], + /// eq_zero(j) indicator (r_cycle = all zeros), used in the cycle phase. + gruen_eq_entry: GruenSplitEqPolynomial, + /// f_entry_expected bound at r_addr after the address phase. + bound_f_entry: F, + /// Running entry claim over remaining free variables. + prev_entry_claim: F, + prev_entry_poly: Option>, + params: BytecodeReadRafCyclePhaseParams, +} + +impl BytecodeReadRafCycleSumcheckProver { + #[tracing::instrument(skip_all, name = "BytecodeReadRafCycleSumcheckProver::initialize")] + pub fn initialize( + mut params: BytecodeReadRafSumcheckParams, + trace: Arc>, + bytecode_preprocessing: Arc, + accumulator: &ProverOpeningAccumulator, + ) -> Self { + let (r_address_point, _) = accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ); + let r_address = r_address_point.r; + let mut r_address_low_to_high = r_address.clone(); + r_address_low_to_high.reverse(); + + let r_address_chunks = params + .one_hot_params + .compute_r_address_chunks::(&r_address); + let ra = r_address_chunks + .iter() + .enumerate() + .map(|(i, r_address_chunk)| { + let ra_i: Vec> = trace + .par_iter() + .map(|cycle| { + let pc = super::get_pc_for_cycle(&bytecode_preprocessing, cycle); + Some(params.one_hot_params.bytecode_pc_chunk(pc, i)) + }) + .collect(); + RaPolynomial::new(Arc::new(ra_i), EqPolynomial::evals(r_address_chunk)) + }) + .collect::>(); - let eq_in_entry = self.gruen_eq_entry.E_in_current()[j_lo]; + let gruen_eq_polys = params + .r_cycles + .each_ref() + .map(|r_cycle| GruenSplitEqPolynomial::new(r_cycle, BindingOrder::LowToHigh)); + + let r_cycle_zero = vec![F::Challenge::default(); params.log_T]; + let gruen_eq_entry = GruenSplitEqPolynomial::new(&r_cycle_zero, BindingOrder::LowToHigh); + + let bound_val_evals = params + .bound_val_polys + .take() + .expect("address phase must cache bound Val claims before cycle phase"); + let bound_f_entry = params + .bound_f_entry + .take() + .expect("address phase must cache bound entry claim before cycle phase"); + params.bound_val_polys = Some(bound_val_evals); + params.bound_f_entry = Some(bound_f_entry); + let prev_round_claims = params + .cycle_initial_round_claims + .take() + .expect("address phase must transfer cycle initial round claims before cycle phase"); + let prev_entry_claim = params + .cycle_initial_entry_claim + .take() + .expect("address phase must transfer cycle initial entry claim before cycle phase"); + + Self { + ra, + gruen_eq_polys, + prev_round_claims, + prev_round_polys: None, + bound_val_evals, + gruen_eq_entry, + bound_f_entry, + prev_entry_claim, + prev_entry_poly: None, + params: BytecodeReadRafCyclePhaseParams::new(params, r_address_low_to_high), + } + } +} + +impl SumcheckInstanceProver + for BytecodeReadRafCycleSumcheckProver +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.params + } + + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.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 degree = >::degree(self); + + let out_len = self.gruen_eq_polys[0].E_out_current().len(); + let in_len = self.gruen_eq_polys[0].E_in_current().len(); + let in_n_vars = in_len.log_2(); + + // Evaluations on [1, ..., degree - 2, inf] (for each stage + entry term). + let (mut evals_per_stage, mut entry_evals_raw): ([Vec; N_STAGES], Vec) = (0..out_len) + .into_par_iter() + .map(|j_hi| { + let mut ra_eval_pairs = vec![(F::zero(), F::zero()); self.ra.len()]; + let mut ra_prod_evals = vec![F::zero(); degree - 1]; + let mut evals_per_stage: [_; N_STAGES] = + array::from_fn(|_| vec![F::UnreducedProductAccum::zero(); degree - 1]); + let mut entry_accum = vec![F::UnreducedProductAccum::zero(); degree - 1]; + + for j_lo in 0..in_len { + let j = j_lo + (j_hi << in_n_vars); + + for (i, ra_i) in self.ra.iter().enumerate() { + let ra_i_eval_at_j_0 = ra_i.get_bound_coeff(j * 2); + let ra_i_eval_at_j_1 = ra_i.get_bound_coeff(j * 2 + 1); + ra_eval_pairs[i] = (ra_i_eval_at_j_0, ra_i_eval_at_j_1); + } + eval_linear_prod_assign(&ra_eval_pairs, &mut ra_prod_evals); + + for stage in 0..N_STAGES { + let eq_in_eval = self.gruen_eq_polys[stage].E_in_current()[j_lo]; for i in 0..degree - 1 { - entry_accum[i] += eq_in_entry.mul_to_product_accum(ra_prod_evals[i]); + evals_per_stage[stage][i] += + eq_in_eval.mul_to_product_accum(ra_prod_evals[i]); } } - let stage_evals = array::from_fn(|stage| { - let eq_out_eval = self.gruen_eq_polys[stage].E_out_current()[j_hi]; - evals_per_stage[stage] - .iter() - .map(|v| eq_out_eval * F::reduce_product_accum(*v)) - .collect() - }); - let eq_out_entry = self.gruen_eq_entry.E_out_current()[j_hi]; - let entry_evals: Vec = entry_accum + let eq_in_entry = self.gruen_eq_entry.E_in_current()[j_lo]; + for i in 0..degree - 1 { + entry_accum[i] += eq_in_entry.mul_to_product_accum(ra_prod_evals[i]); + } + } + + let stage_evals = array::from_fn(|stage| { + let eq_out_eval = self.gruen_eq_polys[stage].E_out_current()[j_hi]; + evals_per_stage[stage] .iter() - .map(|v| eq_out_entry * F::reduce_product_accum(*v)) - .collect(); + .map(|v| eq_out_eval * F::reduce_product_accum(*v)) + .collect() + }); + let eq_out_entry = self.gruen_eq_entry.E_out_current()[j_hi]; + let entry_evals: Vec = entry_accum + .iter() + .map(|v| eq_out_entry * F::reduce_product_accum(*v)) + .collect(); - (stage_evals, entry_evals) - }) - .reduce( - || { - ( - array::from_fn(|_| vec![F::zero(); degree - 1]), - vec![F::zero(); degree - 1], - ) - }, - |(a_stages, a_entry), (b_stages, b_entry)| { - let stages = array::from_fn(|i| { - zip_eq(&a_stages[i], &b_stages[i]) - .map(|(a, b)| *a + *b) - .collect() - }); - let entry: Vec = - zip_eq(&a_entry, &b_entry).map(|(a, b)| *a + *b).collect(); - (stages, entry) - }, - ); + (stage_evals, entry_evals) + }) + .reduce( + || { + ( + array::from_fn(|_| vec![F::zero(); degree - 1]), + vec![F::zero(); degree - 1], + ) + }, + |(a_stages, a_entry), (b_stages, b_entry)| { + let stages = array::from_fn(|i| { + zip_eq(&a_stages[i], &b_stages[i]) + .map(|(a, b)| *a + *b) + .collect() + }); + let entry: Vec = zip_eq(&a_entry, &b_entry).map(|(a, b)| *a + *b).collect(); + (stages, entry) + }, + ); - // Multiply by bound values. - let bound_val_evals = self.bound_val_evals.as_ref().unwrap(); - for (stage, evals) in evals_per_stage.iter_mut().enumerate() { - evals.iter_mut().for_each(|v| *v *= bound_val_evals[stage]); - } - let bound_f_entry = self.bound_f_entry.unwrap(); - entry_evals_raw.iter_mut().for_each(|v| *v *= bound_f_entry); - - let mut round_polys: [_; N_STAGES] = array::from_fn(|_| UniPoly::zero()); - let mut agg_round_poly = UniPoly::zero(); - - // Obtain round poly for each stage and perform RLC. - for (stage, evals) in evals_per_stage.iter().enumerate() { - let claim = self.prev_round_claims[stage]; - let round_poly = self.gruen_eq_polys[stage].gruen_poly_from_evals(evals, claim); - agg_round_poly += &(&round_poly * self.params.gamma_powers[stage]); - round_polys[stage] = round_poly; - } + // Multiply by bound values. + for (stage, evals) in evals_per_stage.iter_mut().enumerate() { + evals + .iter_mut() + .for_each(|v| *v *= self.bound_val_evals[stage]); + } + entry_evals_raw + .iter_mut() + .for_each(|v| *v *= self.bound_f_entry); + + let mut round_polys: [_; N_STAGES] = array::from_fn(|_| UniPoly::zero()); + let mut agg_round_poly = UniPoly::zero(); + + // Obtain round poly for each stage and perform RLC. + for (stage, evals) in evals_per_stage.iter().enumerate() { + let claim = self.prev_round_claims[stage]; + let round_poly = self.gruen_eq_polys[stage].gruen_poly_from_evals(evals, claim); + agg_round_poly += &(&round_poly * self.params.gamma_powers[stage]); + round_polys[stage] = round_poly; + } - let entry_round_poly = self - .gruen_eq_entry - .gruen_poly_from_evals(&entry_evals_raw, self.prev_entry_claim); - agg_round_poly += &(&entry_round_poly * self.params.entry_gamma); - self.prev_entry_poly = Some(entry_round_poly); + let entry_round_poly = self + .gruen_eq_entry + .gruen_poly_from_evals(&entry_evals_raw, self.prev_entry_claim); + agg_round_poly += &(&entry_round_poly * self.params.entry_gamma); + self.prev_entry_poly = Some(entry_round_poly); - self.prev_round_polys = Some(round_polys); + self.prev_round_polys = Some(round_polys); - agg_round_poly - } + agg_round_poly } - #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckProver::ingest_challenge")] fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { + debug_assert!(round < self.params.log_T); if let Some(prev_round_polys) = self.prev_round_polys.take() { self.prev_round_claims = prev_round_polys.map(|poly| poly.evaluate(&r_j)); } @@ -637,34 +750,13 @@ impl SumcheckInstanceProver self.prev_entry_claim = entry_poly.evaluate(&r_j); } - if round < self.params.log_K { - self.params - .val_polys - .iter_mut() - .for_each(|poly| poly.bind_parallel(r_j, BindingOrder::LowToHigh)); - self.params - .int_poly - .bind_parallel(r_j, BindingOrder::LowToHigh); - self.F - .iter_mut() - .for_each(|poly| poly.bind_parallel(r_j, BindingOrder::LowToHigh)); - self.f_entry_trace - .bind_parallel(r_j, BindingOrder::LowToHigh); - self.f_entry_expected - .bind_parallel(r_j, BindingOrder::LowToHigh); - self.r_address_prime.push(r_j); - if round == self.params.log_K - 1 { - self.init_log_t_rounds(); - } - } else { - self.ra - .iter_mut() - .for_each(|ra| ra.bind_parallel(r_j, BindingOrder::LowToHigh)); - self.gruen_eq_polys - .iter_mut() - .for_each(|poly| poly.bind(r_j)); - self.gruen_eq_entry.bind(r_j); - } + self.ra + .iter_mut() + .for_each(|ra| ra.bind_parallel(r_j, BindingOrder::LowToHigh)); + self.gruen_eq_polys + .iter_mut() + .for_each(|poly| poly.bind(r_j)); + self.gruen_eq_entry.bind(r_j); } fn cache_openings( @@ -698,41 +790,122 @@ impl SumcheckInstanceProver } } -pub struct BytecodeReadRafSumcheckVerifier { - params: BytecodeReadRafSumcheckParams, +pub struct BytecodeReadRafAddressSumcheckVerifier { + params: BytecodeReadRafAddressPhaseParams, } -impl BytecodeReadRafSumcheckVerifier { - pub fn gen>( +impl BytecodeReadRafAddressSumcheckVerifier { + pub fn new( bytecode_preprocessing: &BytecodePreprocessing, n_cycle_vars: usize, one_hot_params: &OneHotParams, - opening_accumulator: &A, + opening_accumulator: &dyn OpeningAccumulator, transcript: &mut impl Transcript, ) -> Self { + let params = BytecodeReadRafSumcheckParams::gen( + bytecode_preprocessing, + n_cycle_vars, + one_hot_params, + opening_accumulator, + transcript, + ); + Self { + params: BytecodeReadRafAddressPhaseParams::new(params), + } + } + + pub fn into_params(self) -> BytecodeReadRafSumcheckParams { + self.params.into_inner() + } +} + +impl> + SumcheckInstanceVerifier for BytecodeReadRafAddressSumcheckVerifier +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.params + } + + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.params.log_K + } + + fn input_claim(&self, accumulator: &A) -> F { + self.params.input_claim(accumulator) + } + + fn expected_output_claim(&self, accumulator: &A, _sumcheck_challenges: &[F::Challenge]) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ) + .1 + } + + fn cache_openings(&self, accumulator: &mut A, 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.clone()), + ); + } +} + +pub struct BytecodeReadRafCycleSumcheckVerifier { + params: BytecodeReadRafCyclePhaseParams, +} + +impl BytecodeReadRafCycleSumcheckVerifier { + pub fn new( + params: BytecodeReadRafSumcheckParams, + opening_accumulator: &dyn OpeningAccumulator, + ) -> 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(); Self { - params: BytecodeReadRafSumcheckParams::gen( - bytecode_preprocessing, - n_cycle_vars, - one_hot_params, - opening_accumulator, - transcript, - ), + params: BytecodeReadRafCyclePhaseParams::new(params, r_address_low_to_high), } } } impl> - SumcheckInstanceVerifier for BytecodeReadRafSumcheckVerifier + SumcheckInstanceVerifier for BytecodeReadRafCycleSumcheckVerifier { fn get_params(&self) -> &dyn SumcheckInstanceParams { &self.params } + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.params.log_T + } + + fn input_claim(&self, accumulator: &A) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ) + .1 + } + fn expected_output_claim(&self, accumulator: &A, sumcheck_challenges: &[F::Challenge]) -> F { let opening_point = self.params.normalize_opening_point(sumcheck_challenges); let (r_address_prime, r_cycle_prime) = opening_point.split_at(self.params.log_K); - // r_cycle is bound LowToHigh, so reverse let int_poly = self.params.int_poly.evaluate(&r_address_prime.r); @@ -745,64 +918,48 @@ impl> .1 }); - // We have a separate Val polynomial for each stage - // Additionally, for stages 1 and 3 we have an Int polynomial for RAF - // So we would have: - // Stage 1: gamma^0 * (Val_1 + gamma^5 * Int) - // Stage 2: gamma^1 * (Val_2) - // Stage 3: gamma^2 * (Val_3 + gamma^4 * Int) - // Stage 4: gamma^3 * (Val_4) - // 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| self.params.val_polys[stage].evaluate(&r_address_prime.r); + let int_poly_contrib_by_stage = [ + int_poly * self.params.gamma_powers[5], + F::zero(), + int_poly * self.params.gamma_powers[4], + F::zero(), + F::zero(), + ]; + 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; - let entry_bits: Vec = (0..log_k) - .map(|i| F::from_u64(((e >> (log_k - 1 - i)) & 1) as u64)) - .collect(); - EqPolynomial::::mle(&entry_bits, &r_address_prime.r) - }; - // eq_zero(r_cycle) = ∏_i (1 - r_cycle_prime.r[i]) - 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.params.entry_gamma * entry_f_at_r_addr * eq_zero_at_r_cycle; + let entry_bits: Vec = (0..self.params.log_K) + .map(|i| { + F::from_u64( + ((self.params.entry_bytecode_index >> (self.params.log_K - 1 - i)) & 1) as u64, + ) + }) + .collect(); + let entry_contrib = self.params.entry_gamma + * EqPolynomial::::mle(&entry_bits, &r_address_prime.r) + * EqPolynomial::::zero_selector(&r_cycle_prime.r); ra_claims.fold(val + entry_contrib, |running, ra_claim| running * ra_claim) } - fn cache_openings( - &self, - accumulator: &mut A, - sumcheck_challenges: &[::Challenge], - ) { + fn cache_openings(&self, accumulator: &mut A, sumcheck_challenges: &[F::Challenge]) { let opening_point = self.params.normalize_opening_point(sumcheck_challenges); let (r_address, r_cycle) = opening_point.split_at(self.params.log_K); - // Compute r_address_chunks with proper padding let r_address_chunks = self .params .one_hot_params @@ -820,579 +977,217 @@ impl> } #[derive(Allocative, Clone)] -pub struct BytecodeReadRafSumcheckParams { - /// Index `i` stores `gamma^i`. - pub gamma_powers: Vec, - /// Stage-specific gamma powers for input_claim_constraint - pub stage1_gammas: Vec, - pub stage2_gammas: Vec, - pub stage3_gammas: Vec, - pub stage4_gammas: Vec, - pub stage5_gammas: Vec, - /// RLC of stage rv_claims and RAF claims (per Stage1/Stage3) used as the sumcheck LHS. - pub input_claim: F, - /// RaParams - pub one_hot_params: OneHotParams, - /// Bytecode length. - pub K: usize, - /// log2(K) and log2(T) used to determine round counts. - pub log_K: usize, - pub log_T: usize, - /// Number of address chunks (and RA polynomials in the product). - pub d: usize, - /// Stage Val polynomials evaluated over address vars. - pub val_polys: [MultilinearPolynomial; N_STAGES], - /// Stage rv claims. - pub rv_claims: [F; N_STAGES], - pub raf_claim: F, - pub raf_shift_claim: F, - /// Identity polynomial over address vars used to inject RAF contributions. - 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, - /// Bytecode table index of the ELF entry point. - pub entry_bytecode_index: usize, - /// Prover-cached f_entry(r_addr) after address phase (None in verifier params). - pub bound_f_entry: Option, +struct BytecodeReadRafCyclePhaseParams { + inner: BytecodeReadRafSumcheckParams, + r_address_low_to_high: Vec, } -impl BytecodeReadRafSumcheckParams { - #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckParams::gen")] - pub fn gen( - bytecode_preprocessing: &BytecodePreprocessing, - n_cycle_vars: usize, - one_hot_params: &OneHotParams, - opening_accumulator: &dyn OpeningAccumulator, - transcript: &mut impl Transcript, +impl BytecodeReadRafCyclePhaseParams { + fn new( + inner: BytecodeReadRafSumcheckParams, + r_address_low_to_high: Vec, ) -> Self { - let gamma_powers = transcript.challenge_scalar_powers(8); + Self { + inner, + r_address_low_to_high, + } + } - let bytecode = &bytecode_preprocessing.bytecode; + 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 + } +} - // Generate all stage-specific gamma powers upfront (order must match verifier) - let stage1_gammas: Vec = transcript.challenge_scalar_powers(2 + NUM_CIRCUIT_FLAGS); - let stage2_gammas: Vec = transcript.challenge_scalar_powers(4); - let stage3_gammas: Vec = transcript.challenge_scalar_powers(9); - let stage4_gammas: Vec = transcript.challenge_scalar_powers(3); - let stage5_gammas: Vec = transcript.challenge_scalar_powers(2 + NUM_LOOKUP_TABLES); +impl Deref for BytecodeReadRafCyclePhaseParams { + type Target = BytecodeReadRafSumcheckParams; - // Compute rv_claims (these don't iterate bytecode, just query opening accumulator) - let rv_claim_1 = Self::compute_rv_claim_1(opening_accumulator, &stage1_gammas); - let rv_claim_2 = Self::compute_rv_claim_2(opening_accumulator, &stage2_gammas); - let rv_claim_3 = Self::compute_rv_claim_3(opening_accumulator, &stage3_gammas); - let rv_claim_4 = Self::compute_rv_claim_4(opening_accumulator, &stage4_gammas); - 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]; + fn deref(&self) -> &Self::Target { + &self.inner + } +} - // 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()]); +impl DerefMut for BytecodeReadRafCyclePhaseParams { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} - let r_register_5 = opening_accumulator +impl SumcheckInstanceParams for BytecodeReadRafCyclePhaseParams { + fn degree(&self) -> usize { + self.d + 1 + } + + fn num_rounds(&self) -> usize { + self.inner.log_T + } + + fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { + accumulator .get_virtual_polynomial_opening( - VirtualPolynomial::RdWa, - SumcheckId::RegistersValEvaluation, + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, ) - .0 - .r; - let eq_r_register_5 = - EqPolynomial::::evals(&r_register_5[..(REGISTER_COUNT as usize).log_2()]); + .1 + } - // 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, - ); + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + let mut r = self.full_challenges(challenges); + r[0..self.log_K].reverse(); + r[self.log_K..].reverse(); + OpeningPoint::new(r) + } - let int_poly = IdentityPolynomial::new(one_hot_params.bytecode_k.log_2()); + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + InputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + )) + } - let (_, raf_claim) = opening_accumulator - .get_virtual_polynomial_opening(VirtualPolynomial::PC, SumcheckId::SpartanOuter); - let (_, raf_shift_claim) = opening_accumulator - .get_virtual_polynomial_opening(VirtualPolynomial::PC, SumcheckId::SpartanShift); - let entry_gamma = gamma_powers[7]; - let entry_bytecode_index = super::entry_bytecode_index(bytecode_preprocessing); - // 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 = [ - rv_claim_1, - rv_claim_2, - rv_claim_3, - rv_claim_4, - rv_claim_5, - raf_claim, - raf_shift_claim, - ] - .iter() - .zip(&gamma_powers) - .map(|(claim, g)| *claim * g) - .sum::() - + entry_gamma; + #[cfg(feature = "zk")] + fn input_constraint_challenge_values( + &self, + _accumulator: &dyn OpeningAccumulator, + ) -> Vec { + Vec::new() + } - let (r_cycle_1, _) = opening_accumulator - .get_virtual_polynomial_opening(VirtualPolynomial::Imm, SumcheckId::SpartanOuter); - let (r_cycle_2, _) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::Jump), - SumcheckId::SpartanProductVirtualization, - ); - let (r_cycle_3, _) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::UnexpandedPC, - SumcheckId::SpartanShift, - ); - let (r, _) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::Rs1Ra, - SumcheckId::RegistersReadWriteChecking, - ); - let (_, r_cycle_4) = r.split_at((REGISTER_COUNT as usize).log_2()); - let (r, _) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::RdWa, - SumcheckId::RegistersValEvaluation, - ); - let (_, r_cycle_5) = r.split_at((REGISTER_COUNT as usize).log_2()); - let r_cycles = [ - r_cycle_1.r, - r_cycle_2.r, - r_cycle_3.r, - r_cycle_4.r, - r_cycle_5.r, - ]; + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + let ra_factors: Vec = (0..self.d) + .map(|i| { + ValueSource::Opening(OpeningId::committed( + CommittedPolynomial::BytecodeRa(i), + SumcheckId::BytecodeReadRaf, + )) + }) + .collect(); - Self { - gamma_powers, - entry_gamma, - entry_bytecode_index, - stage1_gammas, - stage2_gammas, - stage3_gammas, - stage4_gammas, - stage5_gammas, - input_claim, - one_hot_params: one_hot_params.clone(), - K: one_hot_params.bytecode_k, - log_K: one_hot_params.bytecode_k.log_2(), - d: one_hot_params.bytecode_d, - log_T: n_cycle_vars, - val_polys, - rv_claims, - raf_claim, - raf_shift_claim, - int_poly, - r_cycles, - bound_int_poly: None, - bound_val_polys: None, - bound_f_entry: None, - } + let terms = vec![ProductTerm::scaled(ValueSource::Challenge(0), ra_factors)]; + Some(OutputClaimConstraint::sum_of_products(terms)) } - /// Fused computation of all Val polynomials in a single parallel pass over bytecode. - /// - /// This computes all 5 stage-specific Val(k) polynomials simultaneously, avoiding - /// 5 separate passes through the bytecode. Each stage has its own gamma powers - /// and formula for Val(k). - #[allow(clippy::too_many_arguments)] - fn compute_val_polys( - bytecode: &[JoltInstructionRow], - eq_r_register_4: &[F], - eq_r_register_5: &[F], - stage1_gammas: &[F], - stage2_gammas: &[F], - stage3_gammas: &[F], - stage4_gammas: &[F], - stage5_gammas: &[F], - ) -> [MultilinearPolynomial; N_STAGES] { - let K = bytecode.len(); + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { + let opening_point = self.normalize_opening_point(sumcheck_challenges); + let (r_address_prime, r_cycle_prime) = opening_point.split_at(self.log_K); - // Pre-allocate output vectors for each stage - let mut vals: [Vec; N_STAGES] = array::from_fn(|_| unsafe_allocate_zero_vec(K)); - let [v0, v1, v2, v3, v4] = &mut vals; + // 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() + .zip(&self.r_cycles) + .zip(&self.gamma_powers) + .map(|((bound_val, r_cycle), gamma)| { + *bound_val * EqPolynomial::::mle(r_cycle, &r_cycle_prime.r) * gamma + }) + .sum() + } else { + let int_poly = self.int_poly.evaluate(&r_address_prime.r); + self.val_polys + .iter() + .zip(&self.r_cycles) + .zip(&self.gamma_powers) + .zip([ + int_poly * self.gamma_powers[5], + F::zero(), + int_poly * self.gamma_powers[4], + F::zero(), + F::zero(), + ]) + .map(|(((val, r_cycle), gamma), int_poly_contrib)| { + (val.evaluate(&r_address_prime.r) + int_poly_contrib) + * EqPolynomial::::mle(r_cycle, &r_cycle_prime.r) + * gamma + }) + .sum() + }; - // Fused parallel iteration: compute all 5 val entries for each instruction - bytecode - .par_iter() - .zip(v0.par_iter_mut()) - .zip(v1.par_iter_mut()) - .zip(v2.par_iter_mut()) - .zip(v3.par_iter_mut()) - .zip(v4.par_iter_mut()) - .for_each(|(((((instruction, o0), o1), o2), o3), o4)| { - let instr = *instruction; - let circuit_flags = instruction.circuit_flags(); - let instr_flags = instruction.instruction_flags(); + let f_entry_at_r_addr = if let Some(v) = self.bound_f_entry { + v + } else { + 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(); + EqPolynomial::::mle(&entry_bits, &r_address_prime.r) + }; + // eq_zero(r_cycle) = ∏_i (1 - r_cycle_prime.r[i]) + 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; - // Stage 1 (Spartan outer sumcheck) - // Val(k) = unexpanded_pc(k) + γ·imm(k) - // + γ²·circuit_flags[0](k) + γ³·circuit_flags[1](k) + ... - // This virtualizes claims output by Spartan's "outer" sumcheck. - { - let mut lc = F::from_u64(instr.address as u64); - lc += instr.operands.imm.field_mul(stage1_gammas[1]); - // sanity check - debug_assert!( - !circuit_flags[CircuitFlags::IsCompressed] - || !circuit_flags[CircuitFlags::DoNotUpdateUnexpandedPC] - ); - for (flag, gamma_power) in circuit_flags.iter().zip(stage1_gammas[2..].iter()) { - if *flag { - lc += *gamma_power; - } - } - *o0 = lc; - } + vec![val + entry_contrib] + } +} - // Stage 2 (product virtualization, de-duplicated factors) - // Val(k) = jump_flag(k) + γ·branch_flag(k) - // + γ²·write_lookup_output_to_rd_flag(k) + γ³·virtual_instruction(k) - // This Val matches the fused product sumcheck. - { - let mut lc = F::zero(); - if circuit_flags[CircuitFlags::Jump] { - lc += stage2_gammas[0]; - } - if instr_flags[InstructionFlags::Branch] { - lc += stage2_gammas[1]; - } - if circuit_flags[CircuitFlags::WriteLookupOutputToRD] { - lc += stage2_gammas[2]; - } - if circuit_flags[CircuitFlags::VirtualInstruction] { - lc += stage2_gammas[3]; - } - *o1 = lc; - } +#[derive(Allocative, Clone)] +struct BytecodeReadRafAddressPhaseParams { + inner: BytecodeReadRafSumcheckParams, +} - // Stage 3 (Shift sumcheck) - // Val(k) = imm(k) + γ·unexpanded_pc(k) - // + γ²·left_operand_is_rs1_value(k) + γ³·left_operand_is_pc(k) - // + γ⁴·right_operand_is_rs2_value(k) + γ⁵·right_operand_is_imm(k) - // + γ⁶·is_noop(k) + γ⁷·virtual_instruction(k) + γ⁸·is_first_in_sequence(k) - // This virtualizes claims output by the ShiftSumcheck. - { - let mut lc = F::from_i128(instr.operands.imm); - lc += stage3_gammas[1].mul_u64(instr.address as u64); - if instr_flags[InstructionFlags::LeftOperandIsRs1Value] { - lc += stage3_gammas[2]; - } - if instr_flags[InstructionFlags::LeftOperandIsPC] { - lc += stage3_gammas[3]; - } - if instr_flags[InstructionFlags::RightOperandIsRs2Value] { - lc += stage3_gammas[4]; - } - if instr_flags[InstructionFlags::RightOperandIsImm] { - lc += stage3_gammas[5]; - } - if instr_flags[InstructionFlags::IsNoop] { - lc += stage3_gammas[6]; - } - if circuit_flags[CircuitFlags::VirtualInstruction] { - lc += stage3_gammas[7]; - } - if circuit_flags[CircuitFlags::IsFirstInSequence] { - lc += stage3_gammas[8]; - } - *o2 = lc; - } +impl BytecodeReadRafAddressPhaseParams { + fn new(inner: BytecodeReadRafSumcheckParams) -> Self { + Self { inner } + } - // Stage 4 (registers read/write checking sumcheck) - // Val(k) = eq(rd(k), r_register) + γ·eq(rs1(k), r_register) + γ²·eq(rs2(k), r_register) - // where rd(k, r) = 1 if the k'th instruction in the bytecode has rd = r, - // and analogously for rs1(k, r) and rs2(k, r). - // This virtualizes claims output by the registers read/write checking sumcheck. - { - let rd_eq = instr - .operands - .rd - .map_or(F::zero(), |r| eq_r_register_4[r as usize]); - let rs1_eq = instr - .operands - .rs1 - .map_or(F::zero(), |r| eq_r_register_4[r as usize]); - let rs2_eq = instr - .operands - .rs2 - .map_or(F::zero(), |r| eq_r_register_4[r as usize]); - *o3 = rd_eq * stage4_gammas[0] - + rs1_eq * stage4_gammas[1] - + rs2_eq * stage4_gammas[2]; - } + fn into_inner(self) -> BytecodeReadRafSumcheckParams { + self.inner + } +} - // Stage 5 (registers val-evaluation + instruction lookups sumcheck) - // Val(k) = eq(rd(k), r_register) + γ·raf_flag(k) - // + γ²·lookup_table_flag[0](k) + γ³·lookup_table_flag[1](k) + ... - // where rd(k, r) = 1 if the k'th instruction in the bytecode has rd = r, - // and raf_flag(k) = 1 if instruction k is NOT interleaved operands. - // This virtualizes the claim output by the registers val-evaluation sumcheck - // and the instruction lookups sumcheck. - { - let mut lc = instr - .operands - .rd - .map_or(F::zero(), |r| eq_r_register_5[r as usize]); - if !circuit_flags.is_interleaved_operands() { - lc += stage5_gammas[1]; - } - if let Some(table) = InstructionLookup::::lookup_table(instruction) { - let table_index = LookupTables::::enum_index(&table); - lc += stage5_gammas[2 + table_index]; - } - *o4 = lc; - } - }); +impl Deref for BytecodeReadRafAddressPhaseParams { + type Target = BytecodeReadRafSumcheckParams; - vals.map(MultilinearPolynomial::from) + fn deref(&self) -> &Self::Target { + &self.inner } +} - fn compute_rv_claim_1( - opening_accumulator: &dyn OpeningAccumulator, - gamma_powers: &[F], - ) -> F { - let (_, unexpanded_pc_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::UnexpandedPC, - SumcheckId::SpartanOuter, - ); - let (_, imm_claim) = opening_accumulator - .get_virtual_polynomial_opening(VirtualPolynomial::Imm, SumcheckId::SpartanOuter); +impl DerefMut for BytecodeReadRafAddressPhaseParams { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} - let circuit_flag_claims: Vec = CircuitFlags::iter() - .map(|flag| { - opening_accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(flag), - SumcheckId::SpartanOuter, - ) - .1 - }) - .collect(); +impl SumcheckInstanceParams for BytecodeReadRafAddressPhaseParams { + fn degree(&self) -> usize { + self.d + 1 + } - std::iter::once(unexpanded_pc_claim) - .chain(std::iter::once(imm_claim)) - .chain(circuit_flag_claims) - .zip_eq(gamma_powers) - .map(|(claim, gamma)| claim * gamma) - .sum() + fn num_rounds(&self) -> usize { + self.inner.log_K } - fn compute_rv_claim_2( - opening_accumulator: &dyn OpeningAccumulator, - gamma_powers: &[F], - ) -> F { - let (_, jump_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::Jump), - SumcheckId::SpartanProductVirtualization, - ); - let (_, branch_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::Branch), - SumcheckId::SpartanProductVirtualization, - ); - let (_, write_lookup_output_to_rd_flag_claim) = opening_accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::WriteLookupOutputToRD), - SumcheckId::SpartanProductVirtualization, - ); - let (_, virtual_instruction_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::VirtualInstruction), - SumcheckId::SpartanProductVirtualization, - ); + fn input_claim(&self, _accumulator: &dyn OpeningAccumulator) -> F { + self.input_claim + } - [ - jump_claim, - branch_claim, - write_lookup_output_to_rd_flag_claim, - virtual_instruction_claim, - ] - .into_iter() - .zip_eq(gamma_powers) - .map(|(claim, gamma)| claim * gamma) - .sum() + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + let mut r = challenges.to_vec(); + r.reverse(); + OpeningPoint::new(r) } - fn compute_rv_claim_3( - opening_accumulator: &dyn OpeningAccumulator, - gamma_powers: &[F], - ) -> F { - let (_, imm_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::Imm, - SumcheckId::InstructionInputVirtualization, - ); - let (_, spartan_shift_unexpanded_pc_claim) = opening_accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::UnexpandedPC, - SumcheckId::SpartanShift, - ); - let (_, instruction_input_unexpanded_pc_claim) = opening_accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::UnexpandedPC, - SumcheckId::InstructionInputVirtualization, - ); + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + // input_claim = Σᵢ gamma_powers[i] * rv_claim_i + gamma_powers[5]*raf_claim + gamma_powers[6]*raf_shift_claim + // Each rv_claim_i = Σⱼ stage_i_gamma[j] * opening_ij + let mut terms = Vec::new(); + let mut challenge_idx = 0; - assert_eq!( - spartan_shift_unexpanded_pc_claim, - instruction_input_unexpanded_pc_claim - ); - - let unexpanded_pc_claim = spartan_shift_unexpanded_pc_claim; - let (_, left_is_rs1_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::LeftOperandIsRs1Value), - SumcheckId::InstructionInputVirtualization, - ); - let (_, left_is_pc_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::LeftOperandIsPC), - SumcheckId::InstructionInputVirtualization, - ); - let (_, right_is_rs2_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::RightOperandIsRs2Value), - SumcheckId::InstructionInputVirtualization, - ); - let (_, right_is_imm_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::RightOperandIsImm), - SumcheckId::InstructionInputVirtualization, - ); - let (_, is_noop_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::IsNoop), - SumcheckId::SpartanShift, - ); - let (_, is_virtual_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::VirtualInstruction), - SumcheckId::SpartanShift, - ); - let (_, is_first_in_sequence_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::IsFirstInSequence), - SumcheckId::SpartanShift, - ); - - [ - imm_claim, - unexpanded_pc_claim, - left_is_rs1_claim, - left_is_pc_claim, - right_is_rs2_claim, - right_is_imm_claim, - is_noop_claim, - is_virtual_claim, - is_first_in_sequence_claim, - ] - .into_iter() - .zip_eq(gamma_powers) - .map(|(claim, gamma)| claim * gamma) - .sum() - } - - fn compute_rv_claim_4( - opening_accumulator: &dyn OpeningAccumulator, - gamma_powers: &[F], - ) -> F { - std::iter::empty() - .chain(once(VirtualPolynomial::RdWa)) - .chain(once(VirtualPolynomial::Rs1Ra)) - .chain(once(VirtualPolynomial::Rs2Ra)) - .map(|vp| { - opening_accumulator - .get_virtual_polynomial_opening(vp, SumcheckId::RegistersReadWriteChecking) - .1 - }) - .zip(gamma_powers) - .map(|(claim, gamma)| claim * gamma) - .sum::() - } - - fn compute_rv_claim_5( - opening_accumulator: &dyn OpeningAccumulator, - gamma_powers: &[F], - ) -> F { - let (_, rd_wa_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::RdWa, - SumcheckId::RegistersValEvaluation, - ); - - let (_, raf_flag_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionRafFlag, - SumcheckId::InstructionReadRaf, - ); - - let mut sum = rd_wa_claim * gamma_powers[0]; - sum += raf_flag_claim * gamma_powers[1]; - - // Add lookup table flag claims from InstructionReadRaf - for i in 0..LookupTables::::COUNT { - let (_, claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::LookupTableFlag(i), - SumcheckId::InstructionReadRaf, - ); - sum += claim * gamma_powers[2 + i]; - } - - sum - } -} - -impl SumcheckInstanceParams for BytecodeReadRafSumcheckParams { - fn degree(&self) -> usize { - self.d + 1 - } - - fn num_rounds(&self) -> usize { - self.log_K + self.log_T - } - - fn input_claim(&self, _: &dyn OpeningAccumulator) -> F { - self.input_claim - } - - fn normalize_opening_point( - &self, - sumcheck_challenges: &[::Challenge], - ) -> OpeningPoint { - let mut r = sumcheck_challenges.to_vec(); - r[0..self.log_K].reverse(); - r[self.log_K..].reverse(); - OpeningPoint::new(r) - } - - #[cfg(feature = "zk")] - fn input_claim_constraint(&self) -> InputClaimConstraint { - // input_claim = Σᵢ gamma_powers[i] * rv_claim_i + gamma_powers[5]*raf_claim + gamma_powers[6]*raf_shift_claim - // Each rv_claim_i = Σⱼ stage_i_gamma[j] * opening_ij - // - // Challenge layout: - // - Stage 1 (SpartanOuter): 2 + NUM_CIRCUIT_FLAGS terms - // - Stage 2 (SpartanProductVirtualization): 5 terms - // - Stage 3 (InstructionInputVirtualization + SpartanShift): 10 terms (9 + 1 for split unexpanded_pc) - // - Stage 4 (RegistersReadWriteChecking): 3 terms - // - Stage 5 (RegistersValEvaluation + InstructionReadRaf): 2 + NUM_LOOKUP_TABLES terms - // - raf_claim (PC @ SpartanOuter): 1 term - // - raf_shift_claim (PC @ SpartanShift): 1 term - - let mut terms = Vec::new(); - let mut challenge_idx = 0; - - // Stage 1: SpartanOuter openings - // Order: UnexpandedPC, Imm, then CircuitFlags in order - terms.push(ProductTerm::scaled( - ValueSource::Challenge(challenge_idx), - vec![ValueSource::Opening(OpeningId::virt( - VirtualPolynomial::UnexpandedPC, - SumcheckId::SpartanOuter, - ))], - )); - challenge_idx += 1; + terms.push(ProductTerm::scaled( + ValueSource::Challenge(challenge_idx), + vec![ValueSource::Opening(OpeningId::virt( + VirtualPolynomial::UnexpandedPC, + SumcheckId::SpartanOuter, + ))], + )); + challenge_idx += 1; terms.push(ProductTerm::scaled( ValueSource::Challenge(challenge_idx), @@ -1414,8 +1209,6 @@ impl SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams, + ) -> Vec { + let mut challenges = Vec::new(); + + for g in &self.stage1_gammas { + challenges.push(self.gamma_powers[0] * *g); + } + + for g in &self.stage2_gammas { + challenges.push(self.gamma_powers[1] * *g); + } + + challenges.push(self.gamma_powers[2] * self.stage3_gammas[0]); + let half = F::from_u64(2).inverse().unwrap(); + challenges.push(self.gamma_powers[2] * self.stage3_gammas[1] * half); + for g in &self.stage3_gammas[2..] { + challenges.push(self.gamma_powers[2] * *g); + } + + for g in &self.stage4_gammas { + challenges.push(self.gamma_powers[3] * *g); + } + + for g in &self.stage5_gammas { + challenges.push(self.gamma_powers[4] * *g); + } + + challenges.push(self.gamma_powers[5]); + challenges.push(self.gamma_powers[6]); + challenges.push(self.entry_gamma); + + challenges + } + + #[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`. + pub gamma_powers: Vec, + /// Stage-specific gamma powers for input_claim_constraint + pub stage1_gammas: Vec, + pub stage2_gammas: Vec, + pub stage3_gammas: Vec, + pub stage4_gammas: Vec, + pub stage5_gammas: Vec, + /// RLC of stage rv_claims and RAF claims (per Stage1/Stage3) used as the sumcheck LHS. + pub input_claim: F, + /// RaParams + pub one_hot_params: OneHotParams, + /// Bytecode length. + pub K: usize, + /// log2(K) and log2(T) used to determine round counts. + pub log_K: usize, + pub log_T: usize, + /// Number of address chunks (and RA polynomials in the product). + pub d: usize, + /// Stage Val polynomials evaluated over address vars. + pub val_polys: [MultilinearPolynomial; N_STAGES], + /// Stage rv claims. + pub rv_claims: [F; N_STAGES], + pub raf_claim: F, + pub raf_shift_claim: F, + /// Identity polynomial over address vars used to inject RAF contributions. + 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_val_polys: 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. + pub entry_bytecode_index: usize, + /// Prover-cached f_entry(r_addr) after address phase (None in verifier params). + pub bound_f_entry: Option, + /// Prover-cached per-stage cycle claims after address binding. + pub cycle_initial_round_claims: Option<[F; N_STAGES]>, + /// Prover-cached entry cycle claim after address binding. + pub cycle_initial_entry_claim: Option, +} + +impl BytecodeReadRafSumcheckParams { + #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckParams::gen")] + pub fn gen( + bytecode_preprocessing: &BytecodePreprocessing, + n_cycle_vars: usize, + one_hot_params: &OneHotParams, + opening_accumulator: &dyn OpeningAccumulator, + transcript: &mut impl Transcript, + ) -> 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); + let stage2_gammas: Vec = transcript.challenge_scalar_powers(4); + let stage3_gammas: Vec = transcript.challenge_scalar_powers(9); + let stage4_gammas: Vec = transcript.challenge_scalar_powers(3); + let stage5_gammas: Vec = transcript.challenge_scalar_powers(2 + NUM_LOOKUP_TABLES); + + // Compute rv_claims (these don't iterate bytecode, just query opening accumulator) + let rv_claim_1 = Self::compute_rv_claim_1(opening_accumulator, &stage1_gammas); + let rv_claim_2 = Self::compute_rv_claim_2(opening_accumulator, &stage2_gammas); + let rv_claim_3 = Self::compute_rv_claim_3(opening_accumulator, &stage3_gammas); + let rv_claim_4 = Self::compute_rv_claim_4(opening_accumulator, &stage4_gammas); + 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]; + + 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()]); + + let val_polys = Self::compute_val_polys( + &bytecode_preprocessing.bytecode, + &eq_r_register_4, + &eq_r_register_5, + &stage1_gammas, + &stage2_gammas, + &stage3_gammas, + &stage4_gammas, + &stage5_gammas, + ); + + let int_poly = IdentityPolynomial::new(one_hot_params.bytecode_k.log_2()); + + let (_, raf_claim) = opening_accumulator + .get_virtual_polynomial_opening(VirtualPolynomial::PC, SumcheckId::SpartanOuter); + let (_, raf_shift_claim) = opening_accumulator + .get_virtual_polynomial_opening(VirtualPolynomial::PC, SumcheckId::SpartanShift); + let entry_gamma = gamma_powers[7]; + let entry_bytecode_index = super::entry_bytecode_index(bytecode_preprocessing); + // 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 mut input_claim: F = [ + rv_claim_1, + rv_claim_2, + rv_claim_3, + rv_claim_4, + rv_claim_5, + raf_claim, + raf_shift_claim, + ] + .iter() + .zip(&gamma_powers) + .map(|(claim, g)| *claim * g) + .sum::(); + input_claim += entry_gamma; + + let (r_cycle_1, _) = opening_accumulator + .get_virtual_polynomial_opening(VirtualPolynomial::Imm, SumcheckId::SpartanOuter); + let (r_cycle_2, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::Jump), + SumcheckId::SpartanProductVirtualization, + ); + let (r_cycle_3, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::UnexpandedPC, + SumcheckId::SpartanShift, + ); + let (r, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::Rs1Ra, + SumcheckId::RegistersReadWriteChecking, + ); + let (_, r_cycle_4) = r.split_at((REGISTER_COUNT as usize).log_2()); + let (r, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::RdWa, + SumcheckId::RegistersValEvaluation, + ); + let (_, r_cycle_5) = r.split_at((REGISTER_COUNT as usize).log_2()); + let r_cycles = [ + r_cycle_1.r, + r_cycle_2.r, + r_cycle_3.r, + r_cycle_4.r, + r_cycle_5.r, + ]; + + Self { + gamma_powers, + entry_gamma, + entry_bytecode_index, + stage1_gammas, + stage2_gammas, + stage3_gammas, + stage4_gammas, + stage5_gammas, + input_claim, + one_hot_params: one_hot_params.clone(), + K: one_hot_params.bytecode_k, + log_K: one_hot_params.bytecode_k.log_2(), + d: one_hot_params.bytecode_d, + log_T: n_cycle_vars, + val_polys, + rv_claims, + raf_claim, + raf_shift_claim, + int_poly, + r_cycles, + bound_val_polys: None, + bound_f_entry: None, + cycle_initial_round_claims: None, + cycle_initial_entry_claim: None, + } + } + + /// Fused computation of all Val polynomials in a single parallel pass over bytecode. + /// + /// This computes all 5 stage-specific Val(k) polynomials simultaneously, avoiding + /// 5 separate passes through the bytecode. Each stage has its own gamma powers + /// and formula for Val(k). + #[allow(clippy::too_many_arguments)] + fn compute_val_polys( + bytecode: &[JoltInstructionRow], + eq_r_register_4: &[F], + eq_r_register_5: &[F], + stage1_gammas: &[F], + stage2_gammas: &[F], + stage3_gammas: &[F], + stage4_gammas: &[F], + stage5_gammas: &[F], + ) -> [MultilinearPolynomial; N_STAGES] { + let K = bytecode.len(); + + // Pre-allocate output vectors for each stage + let mut vals: [Vec; N_STAGES] = array::from_fn(|_| unsafe_allocate_zero_vec(K)); + let [v0, v1, v2, v3, v4] = &mut vals; + + // Fused parallel iteration: compute all 5 val entries for each instruction + bytecode + .par_iter() + .zip(v0.par_iter_mut()) + .zip(v1.par_iter_mut()) + .zip(v2.par_iter_mut()) + .zip(v3.par_iter_mut()) + .zip(v4.par_iter_mut()) + .for_each(|(((((instruction, o0), o1), o2), o3), o4)| { + let instr = *instruction; + let circuit_flags = instruction.circuit_flags(); + let instr_flags = instruction.instruction_flags(); + + // Stage 1 (Spartan outer sumcheck) + // Val(k) = unexpanded_pc(k) + γ·imm(k) + // + γ²·circuit_flags[0](k) + γ³·circuit_flags[1](k) + ... + // This virtualizes claims output by Spartan's "outer" sumcheck. + { + let mut lc = F::from_u64(instr.address as u64); + lc += instr.operands.imm.field_mul(stage1_gammas[1]); + // sanity check + debug_assert!( + !circuit_flags[CircuitFlags::IsCompressed] + || !circuit_flags[CircuitFlags::DoNotUpdateUnexpandedPC] + ); + for (flag, gamma_power) in circuit_flags.iter().zip(stage1_gammas[2..].iter()) { + if *flag { + lc += *gamma_power; + } + } + *o0 = lc; + } + + // Stage 2 (product virtualization, de-duplicated factors) + // Val(k) = jump_flag(k) + γ·branch_flag(k) + // + γ²·write_lookup_output_to_rd_flag(k) + γ³·virtual_instruction(k) + // This Val matches the fused product sumcheck. + { + let mut lc = F::zero(); + if circuit_flags[CircuitFlags::Jump] { + lc += stage2_gammas[0]; + } + if instr_flags[InstructionFlags::Branch] { + lc += stage2_gammas[1]; + } + if circuit_flags[CircuitFlags::WriteLookupOutputToRD] { + lc += stage2_gammas[2]; + } + if circuit_flags[CircuitFlags::VirtualInstruction] { + lc += stage2_gammas[3]; + } + *o1 = lc; + } + + // Stage 3 (Shift sumcheck) + // Val(k) = imm(k) + γ·unexpanded_pc(k) + // + γ²·left_operand_is_rs1_value(k) + γ³·left_operand_is_pc(k) + // + γ⁴·right_operand_is_rs2_value(k) + γ⁵·right_operand_is_imm(k) + // + γ⁶·is_noop(k) + γ⁷·virtual_instruction(k) + γ⁸·is_first_in_sequence(k) + // This virtualizes claims output by the ShiftSumcheck. + { + let mut lc = F::from_i128(instr.operands.imm); + lc += stage3_gammas[1].mul_u64(instr.address as u64); + if instr_flags[InstructionFlags::LeftOperandIsRs1Value] { + lc += stage3_gammas[2]; + } + if instr_flags[InstructionFlags::LeftOperandIsPC] { + lc += stage3_gammas[3]; + } + if instr_flags[InstructionFlags::RightOperandIsRs2Value] { + lc += stage3_gammas[4]; + } + if instr_flags[InstructionFlags::RightOperandIsImm] { + lc += stage3_gammas[5]; + } + if instr_flags[InstructionFlags::IsNoop] { + lc += stage3_gammas[6]; + } + if circuit_flags[CircuitFlags::VirtualInstruction] { + lc += stage3_gammas[7]; + } + if circuit_flags[CircuitFlags::IsFirstInSequence] { + lc += stage3_gammas[8]; + } + *o2 = lc; + } + + // Stage 4 (registers read/write checking sumcheck) + // Val(k) = eq(rd(k), r_register) + γ·eq(rs1(k), r_register) + γ²·eq(rs2(k), r_register) + // where rd(k, r) = 1 if the k'th instruction in the bytecode has rd = r, + // and analogously for rs1(k, r) and rs2(k, r). + // This virtualizes claims output by the registers read/write checking sumcheck. + { + let rd_eq = instr + .operands + .rd + .map_or(F::zero(), |r| eq_r_register_4[r as usize]); + let rs1_eq = instr + .operands + .rs1 + .map_or(F::zero(), |r| eq_r_register_4[r as usize]); + let rs2_eq = instr + .operands + .rs2 + .map_or(F::zero(), |r| eq_r_register_4[r as usize]); + *o3 = rd_eq * stage4_gammas[0] + + rs1_eq * stage4_gammas[1] + + rs2_eq * stage4_gammas[2]; + } + + // Stage 5 (registers val-evaluation + instruction lookups sumcheck) + // Val(k) = eq(rd(k), r_register) + γ·raf_flag(k) + // + γ²·lookup_table_flag[0](k) + γ³·lookup_table_flag[1](k) + ... + // where rd(k, r) = 1 if the k'th instruction in the bytecode has rd = r, + // and raf_flag(k) = 1 if instruction k is NOT interleaved operands. + // This virtualizes the claim output by the registers val-evaluation sumcheck + // and the instruction lookups sumcheck. + { + let mut lc = instr + .operands + .rd + .map_or(F::zero(), |r| eq_r_register_5[r as usize]); + if !circuit_flags.is_interleaved_operands() { + lc += stage5_gammas[1]; + } + if let Some(table) = InstructionLookup::::lookup_table(instruction) { + let table_index = LookupTables::::enum_index(&table); + lc += stage5_gammas[2 + table_index]; + } + *o4 = lc; + } + }); + + vals.map(MultilinearPolynomial::from) } - #[cfg(feature = "zk")] - fn input_constraint_challenge_values(&self, _: &dyn OpeningAccumulator) -> Vec { - // Compute coefficients: gamma_powers[stage] * stage_gammas[idx] - // The order must match input_claim_constraint terms. - - let mut challenges = Vec::new(); - - // Stage 1: gamma_powers[0] * stage1_gammas[i] - for g in &self.stage1_gammas { - challenges.push(self.gamma_powers[0] * *g); - } + fn compute_rv_claim_1( + opening_accumulator: &dyn OpeningAccumulator, + gamma_powers: &[F], + ) -> F { + let (_, unexpanded_pc_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::UnexpandedPC, + SumcheckId::SpartanOuter, + ); + let (_, imm_claim) = opening_accumulator + .get_virtual_polynomial_opening(VirtualPolynomial::Imm, SumcheckId::SpartanOuter); - // Stage 2: gamma_powers[1] * stage2_gammas[i] - for g in &self.stage2_gammas { - challenges.push(self.gamma_powers[1] * *g); - } + let circuit_flag_claims: Vec = CircuitFlags::iter() + .map(|flag| { + opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(flag), + SumcheckId::SpartanOuter, + ) + .1 + }) + .collect(); - // Stage 3: gamma_powers[2] * stage3_gammas[i] - // Special handling for index 1 (unexpanded_pc) which is split between two openings - challenges.push(self.gamma_powers[2] * self.stage3_gammas[0]); // imm - // unexpanded_pc: split between SpartanShift and InstructionInputVirtualization - // Each gets half the coefficient - let half = F::from_u64(2).inverse().unwrap(); - challenges.push(self.gamma_powers[2] * self.stage3_gammas[1] * half); - // Continue with the rest of stage3 (indices 2..9) - for g in &self.stage3_gammas[2..] { - challenges.push(self.gamma_powers[2] * *g); - } + std::iter::once(unexpanded_pc_claim) + .chain(std::iter::once(imm_claim)) + .chain(circuit_flag_claims) + .zip_eq(gamma_powers) + .map(|(claim, gamma)| claim * gamma) + .sum() + } - // Stage 4: gamma_powers[3] * stage4_gammas[i] - for g in &self.stage4_gammas { - challenges.push(self.gamma_powers[3] * *g); - } + fn compute_rv_claim_2( + opening_accumulator: &dyn OpeningAccumulator, + gamma_powers: &[F], + ) -> F { + let (_, jump_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::Jump), + SumcheckId::SpartanProductVirtualization, + ); + let (_, branch_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::Branch), + SumcheckId::SpartanProductVirtualization, + ); + let (_, write_lookup_output_to_rd_flag_claim) = opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::WriteLookupOutputToRD), + SumcheckId::SpartanProductVirtualization, + ); + let (_, virtual_instruction_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::VirtualInstruction), + SumcheckId::SpartanProductVirtualization, + ); - // Stage 5: gamma_powers[4] * stage5_gammas[i] - for g in &self.stage5_gammas { - challenges.push(self.gamma_powers[4] * *g); - } + [ + jump_claim, + branch_claim, + write_lookup_output_to_rd_flag_claim, + virtual_instruction_claim, + ] + .into_iter() + .zip_eq(gamma_powers) + .map(|(claim, gamma)| claim * gamma) + .sum() + } - // raf_claim: gamma_powers[5] - challenges.push(self.gamma_powers[5]); + fn compute_rv_claim_3( + opening_accumulator: &dyn OpeningAccumulator, + gamma_powers: &[F], + ) -> F { + let (_, imm_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::Imm, + SumcheckId::InstructionInputVirtualization, + ); + let (_, spartan_shift_unexpanded_pc_claim) = opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::UnexpandedPC, + SumcheckId::SpartanShift, + ); + let (_, instruction_input_unexpanded_pc_claim) = opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::UnexpandedPC, + SumcheckId::InstructionInputVirtualization, + ); - // raf_shift_claim: gamma_powers[6] - challenges.push(self.gamma_powers[6]); + assert_eq!( + spartan_shift_unexpanded_pc_claim, + instruction_input_unexpanded_pc_claim + ); - // entry constraint - challenges.push(self.entry_gamma); + let unexpanded_pc_claim = spartan_shift_unexpanded_pc_claim; + let (_, left_is_rs1_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::LeftOperandIsRs1Value), + SumcheckId::InstructionInputVirtualization, + ); + let (_, left_is_pc_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::LeftOperandIsPC), + SumcheckId::InstructionInputVirtualization, + ); + let (_, right_is_rs2_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::RightOperandIsRs2Value), + SumcheckId::InstructionInputVirtualization, + ); + let (_, right_is_imm_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::RightOperandIsImm), + SumcheckId::InstructionInputVirtualization, + ); + let (_, is_noop_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::IsNoop), + SumcheckId::SpartanShift, + ); + let (_, is_virtual_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::VirtualInstruction), + SumcheckId::SpartanShift, + ); + let (_, is_first_in_sequence_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::IsFirstInSequence), + SumcheckId::SpartanShift, + ); - challenges + [ + imm_claim, + unexpanded_pc_claim, + left_is_rs1_claim, + left_is_pc_claim, + right_is_rs2_claim, + right_is_imm_claim, + is_noop_claim, + is_virtual_claim, + is_first_in_sequence_claim, + ] + .into_iter() + .zip_eq(gamma_powers) + .map(|(claim, gamma)| claim * gamma) + .sum() } - #[cfg(feature = "zk")] - fn output_claim_constraint(&self) -> Option { - let factors: Vec = (0..self.d) - .map(|i| { - let opening = OpeningId::committed( - CommittedPolynomial::BytecodeRa(i), - SumcheckId::BytecodeReadRaf, - ); - ValueSource::Opening(opening) + fn compute_rv_claim_4( + opening_accumulator: &dyn OpeningAccumulator, + gamma_powers: &[F], + ) -> F { + std::iter::empty() + .chain(once(VirtualPolynomial::RdWa)) + .chain(once(VirtualPolynomial::Rs1Ra)) + .chain(once(VirtualPolynomial::Rs2Ra)) + .map(|vp| { + opening_accumulator + .get_virtual_polynomial_opening(vp, SumcheckId::RegistersReadWriteChecking) + .1 }) - .collect(); - - let terms = vec![ProductTerm::scaled(ValueSource::Challenge(0), factors)]; - - Some(OutputClaimConstraint::sum_of_products(terms)) + .zip(gamma_powers) + .map(|(claim, gamma)| claim * gamma) + .sum::() } - #[cfg(feature = "zk")] - fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { - let opening_point = self.normalize_opening_point(sumcheck_challenges); - let (r_address_prime, r_cycle_prime) = opening_point.split_at(self.log_K); + fn compute_rv_claim_5( + opening_accumulator: &dyn OpeningAccumulator, + gamma_powers: &[F], + ) -> F { + let (_, rd_wa_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::RdWa, + SumcheckId::RegistersValEvaluation, + ); - // 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() - .zip(&self.r_cycles) - .zip(&self.gamma_powers) - .map(|((bound_val, r_cycle), gamma)| { - *bound_val * EqPolynomial::::mle(r_cycle, &r_cycle_prime.r) * gamma - }) - .sum() - } else { - let int_poly = self.int_poly.evaluate(&r_address_prime.r); - self.val_polys - .iter() - .zip(&self.r_cycles) - .zip(&self.gamma_powers) - .zip([ - int_poly * self.gamma_powers[5], - F::zero(), - int_poly * self.gamma_powers[4], - F::zero(), - F::zero(), - ]) - .map(|(((val, r_cycle), gamma), int_poly_contrib)| { - (val.evaluate(&r_address_prime.r) + int_poly_contrib) - * EqPolynomial::::mle(r_cycle, &r_cycle_prime.r) - * gamma - }) - .sum() - }; + let (_, raf_flag_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionRafFlag, + SumcheckId::InstructionReadRaf, + ); - // 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 f_entry_at_r_addr = if let Some(v) = self.bound_f_entry { - v - } else { - 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(); - EqPolynomial::::mle(&entry_bits, &r_address_prime.r) - }; - // eq_zero(r_cycle) = ∏_i (1 - r_cycle_prime.r[i]) - 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; + let mut sum = rd_wa_claim * gamma_powers[0]; + sum += raf_flag_claim * gamma_powers[1]; - vec![val + entry_contrib] + // Add lookup table flag claims from InstructionReadRaf + for i in 0..LookupTables::::COUNT { + let (_, claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::LookupTableFlag(i), + SumcheckId::InstructionReadRaf, + ); + sum += claim * gamma_powers[2 + i]; + } + + sum } } diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index 56b8668ab0..d6f9aea87e 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -569,17 +569,8 @@ 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, - } + fn round_offset(&self, _max_num_rounds: usize) -> usize { + 0 } #[cfg(feature = "allocative")] @@ -672,16 +663,8 @@ impl> } } - 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, - } + fn round_offset(&self, _max_num_rounds: usize) -> usize { + 0 } } diff --git a/jolt-core/src/zkvm/proof_serialization.rs b/jolt-core/src/zkvm/proof_serialization.rs index 2dea06bd78..71b17a1d69 100644 --- a/jolt-core/src/zkvm/proof_serialization.rs +++ b/jolt-core/src/zkvm/proof_serialization.rs @@ -48,7 +48,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, @@ -77,7 +78,8 @@ impl, PCS: CommitmentScheme, FS: Tr && self.stage3_sumcheck_proof.is_zk() == zk_mode && self.stage4_sumcheck_proof.is_zk() == zk_mode && self.stage5_sumcheck_proof.is_zk() == zk_mode - && self.stage6_sumcheck_proof.is_zk() == zk_mode + && self.stage6a_sumcheck_proof.is_zk() == zk_mode + && self.stage6b_sumcheck_proof.is_zk() == zk_mode && self.stage7_sumcheck_proof.is_zk() == zk_mode; if !consistent { @@ -403,6 +405,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), } } @@ -442,7 +446,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(_) @@ -520,6 +526,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 d2e7fa01ed..253bd2ddcd 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, BooleanityCycleInput, 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, @@ -532,7 +537,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_cycle_input) = + self.prove_stage6a(); + let (stage6b_sumcheck_proof, r_stage6) = + self.prove_stage6b(bytecode_read_raf_params, booleanity_cycle_input); let (stage7_sumcheck_proof, r_stage7) = self.prove_stage7(); let _sumcheck_challenges = [ @@ -579,7 +587,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, @@ -1218,14 +1227,15 @@ impl< } #[tracing::instrument(skip_all)] - fn prove_stage6( + fn prove_stage6a( &mut self, ) -> ( SumcheckInstanceProof, - Vec, + BytecodeReadRafSumcheckParams, + BooleanityCycleInput, ) { #[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, @@ -1235,9 +1245,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, @@ -1245,6 +1252,65 @@ 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"); + + 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"); + drop(instances); + + let booleanity_cycle_input = booleanity.into_cycle_input(); + + ( + sumcheck_proof, + bytecode_read_raf.into_params(), + booleanity_cycle_input, + ) + } + + #[tracing::instrument(skip_all)] + fn prove_stage6b( + &mut self, + bytecode_read_raf_params: BytecodeReadRafSumcheckParams, + booleanity_cycle_input: BooleanityCycleInput, + ) -> ( + 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, @@ -1261,7 +1327,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, @@ -1306,21 +1372,19 @@ 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 booleanity = BooleanityCycleSumcheckProver::initialize( + booleanity_cycle_input, + &self.opening_accumulator, ); let mut ram_hamming_booleanity = HammingBooleanitySumcheckProver::initialize(ram_hamming_booleanity_params, &self.trace); - let mut booleanity = BooleanitySumcheckProver::initialize( - booleanity_params, - &self.trace, - &self.preprocessing.shared.bytecode, - &self.program_io.memory_layout, - ); - let mut ram_ra_virtual = RamRaVirtualSumcheckProver::initialize( ram_ra_virtual_params, &self.trace, @@ -1334,8 +1398,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, @@ -1370,13 +1437,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); @@ -1387,7 +1454,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)] @@ -1412,8 +1479,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() ); @@ -3185,7 +3252,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/transpilable_verifier.rs b/jolt-core/src/zkvm/transpilable_verifier.rs index f376762f43..92c2925647 100644 --- a/jolt-core/src/zkvm/transpilable_verifier.rs +++ b/jolt-core/src/zkvm/transpilable_verifier.rs @@ -50,7 +50,10 @@ use crate::zkvm::claim_reductions::{ }; use crate::zkvm::config::OneHotParams; use crate::zkvm::{ - bytecode::read_raf_checking::BytecodeReadRafSumcheckVerifier, + bytecode::read_raf_checking::{ + BytecodeReadRafAddressSumcheckVerifier, BytecodeReadRafCycleSumcheckVerifier, + BytecodeReadRafSumcheckParams, + }, claim_reductions::{ IncClaimReductionSumcheckVerifier, InstructionLookupsClaimReductionSumcheckVerifier, RamRaClaimReductionSumcheckVerifier, @@ -87,7 +90,10 @@ use crate::{ poly::opening_proof::{AbstractVerifierOpeningAccumulator, VerifierOpeningAccumulator}, pprof_scope, subprotocols::{ - booleanity::{BooleanitySumcheckParams, BooleanitySumcheckVerifier}, + booleanity::{ + BooleanityAddressSumcheckVerifier, BooleanityCycleSumcheckVerifier, + BooleanitySumcheckParams, + }, sumcheck_verifier::SumcheckInstanceVerifier, }, transcripts::Transcript, @@ -554,25 +560,60 @@ impl< } 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 instances: Vec<&dyn SumcheckInstanceVerifier> = + vec![&bytecode_read_raf, &booleanity]; - let booleanity = BooleanitySumcheckVerifier::new(booleanity_params); + let _r_stage6a = BatchedSumcheck::verify_standard::( + extract_clear_proof(&self.proof.stage6a_sumcheck_proof), + instances, + &mut self.opening_accumulator, + &mut self.transcript, + )?; + + Ok((bytecode_read_raf.into_params(), booleanity.into_params())) + } + + 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, @@ -590,7 +631,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, @@ -623,8 +664,8 @@ impl< instances.push(advice); } - let _r_stage6 = BatchedSumcheck::verify_standard::( - extract_clear_proof(&self.proof.stage6_sumcheck_proof), + let _r_stage6b = BatchedSumcheck::verify_standard::( + extract_clear_proof(&self.proof.stage6b_sumcheck_proof), instances, &mut self.opening_accumulator, &mut self.transcript, diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index a12614aefa..6f3cbebaa2 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -42,7 +42,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, @@ -85,7 +88,10 @@ use crate::{ }, pprof_scope, subprotocols::{ - booleanity::{BooleanitySumcheckParams, BooleanitySumcheckVerifier}, + booleanity::{ + BooleanityAddressSumcheckVerifier, BooleanityCycleSumcheckVerifier, + BooleanitySumcheckParams, + }, sumcheck_verifier::SumcheckInstanceVerifier, }, transcripts::Transcript, @@ -459,7 +465,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 @@ -478,7 +484,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]; @@ -489,7 +496,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, ]; @@ -499,7 +507,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(), ]; @@ -513,17 +522,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(), ]; @@ -533,7 +544,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); let uniskip_output_constraints = [ @@ -1020,26 +1032,114 @@ 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 (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)) + } + + #[expect(clippy::type_complexity)] + fn verify_stage6a( + &mut self, + ) -> Result< + ( + BytecodeReadRafSumcheckParams, + BooleanitySumcheckParams, + StageVerifyResult, + ), + 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]; + let (batching_coefficients, r_stage6a) = BatchedSumcheck::verify( + &self.proof.stage6a_sumcheck_proof, + instances.clone(), + &mut self.opening_accumulator, + &mut self.transcript, + )?; + #[cfg(not(feature = "zk"))] + let _ = &batching_coefficients; + #[cfg(feature = "zk")] + { + 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, + )) + } + #[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))] + 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, @@ -1057,7 +1157,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, @@ -1092,8 +1192,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, @@ -1111,7 +1211,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() @@ -1124,7 +1224,7 @@ impl< ); } Ok(StageVerifyResult::new( - r_stage6, + r_stage6b, batched_output_constraint, output_constraint_challenge_values, batched_input_constraint, @@ -1134,7 +1234,7 @@ impl< } #[cfg(not(feature = "zk"))] Ok(StageVerifyResult { - challenges: r_stage6, + challenges: r_stage6b, }) } @@ -1142,12 +1242,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, @@ -1166,7 +1266,8 @@ impl< &self.proof.stage3_sumcheck_proof, &self.proof.stage4_sumcheck_proof, &self.proof.stage5_sumcheck_proof, - &self.proof.stage6_sumcheck_proof, + &self.proof.stage6a_sumcheck_proof, + &self.proof.stage6b_sumcheck_proof, &self.proof.stage7_sumcheck_proof, ]; @@ -1183,7 +1284,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() { @@ -1274,7 +1375,7 @@ impl< } } - // 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 @@ -1282,8 +1383,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]; @@ -1311,7 +1413,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], @@ -1321,6 +1423,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() { diff --git a/jolt-core/src/zkvm/witness.rs b/jolt-core/src/zkvm/witness.rs index 581ff454f1..bfd45f97b4 100644 --- a/jolt-core/src/zkvm/witness.rs +++ b/jolt-core/src/zkvm/witness.rs @@ -271,4 +271,6 @@ pub enum VirtualPolynomial { OpFlags(CircuitFlags), InstructionFlags(InstructionFlags), LookupTableFlag(usize), + BytecodeReadRafAddrClaim, + BooleanityAddrClaim, } diff --git a/transpiler/src/symbolic_proof.rs b/transpiler/src/symbolic_proof.rs index 75311d49a3..54798fd796 100644 --- a/transpiler/src/symbolic_proof.rs +++ b/transpiler/src/symbolic_proof.rs @@ -355,11 +355,18 @@ pub fn symbolize_proof( "stage5_sumcheck", ); - // === Symbolize stage 6 sumcheck proof === - let stage6_sumcheck = symbolize_sumcheck_variant::( - &real_proof.stage6_sumcheck_proof, + // === Symbolize stage 6a sumcheck proof === + let stage6a_sumcheck = symbolize_sumcheck_variant::( + &real_proof.stage6a_sumcheck_proof, &mut alloc, - "stage6_sumcheck", + "stage6a_sumcheck", + ); + + // === Symbolize stage 6b sumcheck proof === + let stage6b_sumcheck = symbolize_sumcheck_variant::( + &real_proof.stage6b_sumcheck_proof, + &mut alloc, + "stage6b_sumcheck", ); // === Symbolize stage 7 sumcheck proof === @@ -390,7 +397,8 @@ pub fn symbolize_proof( stage3_sumcheck_proof: stage3_sumcheck, stage4_sumcheck_proof: stage4_sumcheck, stage5_sumcheck_proof: stage5_sumcheck, - stage6_sumcheck_proof: stage6_sumcheck, + stage6a_sumcheck_proof: stage6a_sumcheck, + stage6b_sumcheck_proof: stage6b_sumcheck, stage7_sumcheck_proof: stage7_sumcheck, joint_opening_proof: AstProof::default(), untrusted_advice_commitment, From 8c87593a1f9fb25ef825b3ef63d26dabecf40b51 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Sat, 14 Mar 2026 15:24:47 -0700 Subject: [PATCH 04/24] 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 (cherry picked from commit d7b160bce6bf4910ab70470149defe3cee9dcb0b) Co-authored-by: Cursor --- .../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 | 654 +++++------------- 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 | 186 +++-- 10 files changed, 1392 insertions(+), 739 deletions(-) create mode 100644 jolt-core/src/zkvm/claim_reductions/precommitted.rs diff --git a/jolt-core/src/poly/commitment/dory/dory_globals.rs b/jolt-core/src/poly/commitment/dory/dory_globals.rs index 8772532d78..92ba091475 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, }; #[cfg(test)] @@ -143,6 +143,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); @@ -161,6 +162,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)] @@ -256,6 +259,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() @@ -288,11 +307,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!( @@ -305,18 +397,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) { @@ -365,6 +461,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 { @@ -430,6 +530,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 @@ -452,19 +566,30 @@ impl DoryGlobals { #[cfg(test)] Self::configure_test_cache_root(); - 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(()) } @@ -475,6 +600,7 @@ impl DoryGlobals { // 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; @@ -492,6 +618,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 430de9b9a6..6fddece585 100644 --- a/jolt-core/src/poly/opening_proof.rs +++ b/jolt-core/src/poly/opening_proof.rs @@ -855,37 +855,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 991134f986..f2885850cc 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 d6f9aea87e..16d5da4adb 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -1,47 +1,16 @@ -//! 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::{ - AbstractVerifierOpeningAccumulator, OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, - SumcheckId, BIG_ENDIAN, LITTLE_ENDIAN, + OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, SumcheckId, + VerifierOpeningAccumulator, BIG_ENDIAN, LITTLE_ENDIAN, }; use crate::poly::unipoly::UniPoly; #[cfg(feature = "zk")] @@ -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,236 +156,128 @@ impl SumcheckInstanceParams for AdviceClaimReductionParams { #[cfg(feature = "zk")] fn output_claim_constraint(&self) -> Option { match self.phase { - ReductionPhase::CycleVariables => { - if self.num_address_phase_rounds() > 0 { - let advice_opening = match self.kind { - AdviceKind::Trusted => { - OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) - } - AdviceKind::Untrusted => { - OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) - } - }; - return Some(OutputClaimConstraint::direct(advice_opening)); - } - self.final_advice_output_claim_constraint() + PrecommittedPhase::CycleVariables => { + let opening = match self.kind { + AdviceKind::Trusted => { + OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) + } + AdviceKind::Untrusted => { + OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) + } + }; + Some(OutputClaimConstraint::direct(opening)) + } + PrecommittedPhase::AddressVariables => { + let opening = match self.kind { + AdviceKind::Trusted => { + OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReduction) + } + AdviceKind::Untrusted => { + OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReduction) + } + }; + Some(OutputClaimConstraint::linear(vec![( + ValueSource::Challenge(0), + ValueSource::Opening(opening), + )])) } - ReductionPhase::AddressVariables => self.final_advice_output_claim_constraint(), } } #[cfg(feature = "zk")] fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { match self.phase { - ReductionPhase::CycleVariables if self.num_address_phase_rounds() > 0 => vec![], - ReductionPhase::CycleVariables | ReductionPhase::AddressVariables => { - vec![self.final_advice_output_scale(sumcheck_challenges)] + 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 scale: F = precommitted_skip_round_scale(&self.precommitted); + vec![eq_eval * scale] } } } } -impl AdviceClaimReductionParams { - #[cfg(feature = "zk")] - fn final_advice_output_claim_constraint(&self) -> Option { - let advice_opening = match self.kind { - AdviceKind::Trusted => OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReduction), - AdviceKind::Untrusted => 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), - )])) - } - - fn final_advice_output_scale(&self, sumcheck_challenges: &[F::Challenge]) -> F { - let opening_point = self.normalize_opening_point(sumcheck_challenges); - let eq_eval = EqPolynomial::mle(&opening_point.r, &self.r_val.r); - - let total_cycle_phase_rounds = 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() - }; - let active_cycle_phase_rounds = - self.cycle_phase_col_rounds.len() + self.cycle_phase_row_rounds.len(); - let dummy_rounds = total_cycle_phase_rounds.saturating_sub(active_cycle_phase_rounds); +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) + } - let two_inv = F::from_u64(2).inverse().unwrap(); - let scale = (0..dummy_rounds).fold(F::one(), |acc, _| acc * two_inv); + fn cycle_alignment_rounds(&self) -> usize { + self.precommitted.cycle_alignment_rounds() + } - eq_eval * scale + 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( @@ -518,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, @@ -569,10 +320,6 @@ impl SumcheckInstanceProver for AdviceClaimRe } } - fn round_offset(&self, _max_num_rounds: usize) -> usize { - 0 - } - #[cfg(feature = "allocative")] fn update_flamegraph(&self, flamegraph: &mut allocative::FlameGraphBuilder) { flamegraph.visit_root(self); @@ -586,11 +333,18 @@ pub struct AdviceClaimReductionVerifier { impl AdviceClaimReductionVerifier { pub fn new( kind: AdviceKind, - memory_layout: &MemoryLayout, + advice_size_bytes: usize, trace_len: usize, - accumulator: &dyn OpeningAccumulator, + 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), @@ -598,60 +352,65 @@ impl AdviceClaimReductionVerifier { } } -impl> - SumcheckInstanceVerifier for AdviceClaimReductionVerifier +impl SumcheckInstanceVerifier + for AdviceClaimReductionVerifier { fn get_params(&self) -> &dyn SumcheckInstanceParams { unsafe { &*self.params.as_ptr() } } - fn expected_output_claim(&self, accumulator: &A, sumcheck_challenges: &[F::Challenge]) -> F { + fn expected_output_claim( + &self, + accumulator: &VerifierOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) -> F { let params = self.params.borrow(); match params.phase { - ReductionPhase::CycleVariables if params.num_address_phase_rounds() > 0 => { + 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::CycleVariables | 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; - - // Account for Phase 1's internal dummy-gap traversal via constant scaling. - advice_claim * params.final_advice_output_scale(sumcheck_challenges) + let eq_eval = EqPolynomial::mle(&opening_point.r, ¶ms.r_val.r); + let scale: F = precommitted_skip_round_scale(¶ms.precommitted); + advice_claim * eq_eval * scale } } } - fn cache_openings(&self, accumulator: &mut A, sumcheck_challenges: &[F::Challenge]) { + fn cache_openings( + &self, + accumulator: &mut VerifierOpeningAccumulator, + 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 { @@ -663,45 +422,8 @@ impl> } } - fn round_offset(&self, _max_num_rounds: usize) -> usize { - 0 - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_bn254::Fr; - - type Challenge = ::Challenge; - - #[test] - fn final_advice_output_scale_counts_leading_dummy_rounds_when_col_range_is_empty() { - let challenges = [11, 12, 0] - .map(|value| Challenge::from(value as u128)) - .to_vec(); - let active_opening_point = - OpeningPoint::::new(challenges[2..3].to_vec()).match_endianness(); - let params = AdviceClaimReductionParams { - kind: AdviceKind::Trusted, - phase: ReductionPhase::CycleVariables, - log_k_chunk: 2, - log_t: 6, - advice_col_vars: 1, - advice_row_vars: 1, - main_col_vars: 4, - main_row_vars: 4, - cycle_phase_col_rounds: 0..0, - cycle_phase_row_rounds: 2..3, - r_val: active_opening_point, - cycle_var_challenges: vec![], - }; - - let two_inv = Fr::from_u64(2).inverse().unwrap(); - - assert_eq!( - params.final_advice_output_scale(&challenges), - two_inv * two_inv - ); + fn round_offset(&self, max_num_rounds: usize) -> usize { + let params = self.params.borrow(); + 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 3f91e7ca83..bbf39e34f7 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 253bd2ddcd..174152a447 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::{ @@ -282,81 +280,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( @@ -394,20 +392,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 @@ -682,30 +666,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() @@ -1327,12 +1309,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 @@ -1353,8 +1344,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 @@ -1960,12 +1952,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)); } } @@ -1973,12 +1965,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)); } } @@ -2005,70 +1997,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) @@ -2083,8 +2065,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, @@ -2100,8 +2081,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 6f3cbebaa2..0e9cc6defc 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; @@ -28,7 +28,6 @@ use crate::subprotocols::sumcheck_verifier::SumcheckInstanceParams; #[cfg(feature = "zk")] use crate::subprotocols::univariate_skip::UniSkipFirstRoundProofVariant; use crate::zkvm::bytecode::{BytecodePreprocessing, PreprocessingError}; -use crate::zkvm::claim_reductions::advice::ReductionPhase; use crate::zkvm::claim_reductions::RegistersClaimReductionSumcheckVerifier; use crate::zkvm::config::OneHotParams; #[cfg(feature = "prover")] @@ -49,7 +48,7 @@ use crate::zkvm::{ claim_reductions::{ AdviceClaimReductionVerifier, AdviceKind, HammingWeightClaimReductionVerifier, IncClaimReductionSumcheckVerifier, InstructionLookupsClaimReductionSumcheckVerifier, - RamRaClaimReductionSumcheckVerifier, + PrecommittedClaimReduction, RamRaClaimReductionSumcheckVerifier, }, fiat_shamir_preamble, instruction_lookups::{ @@ -79,12 +78,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::{ @@ -263,6 +259,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, @@ -1035,6 +1110,12 @@ impl< 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, stage6a_result) = self.verify_stage6a()?; let stage6b_result = self.verify_stage6b(bytecode_read_raf_params, booleanity_params)?; @@ -1157,20 +1238,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, )); } @@ -1540,7 +1631,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); } } @@ -1550,7 +1641,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); } } @@ -1603,61 +1694,60 @@ impl< /// Stage 8: Dory batch opening verification. fn verify_stage8(&mut self) -> Result, ProofVerifyError> { - // 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) @@ -1670,8 +1760,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, @@ -1684,8 +1773,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, From 6aa84a18de6afb6ccad33f3fe3171b519462c1a4 Mon Sep 17 00:00:00 2001 From: Omid Bodaghi <42227752+omibo@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:37:46 -0700 Subject: [PATCH 05/24] fix(claim-reduction): preserve advice handoff scale Keep the precommitted scale when rebuilding advice claim-reduction state from bound coefficients so GPU cycle-phase handoff remains consistent across dummy rounds. (cherry picked from commit e58c0df892593db257121830ef24897f5a3f9f40) --- jolt-core/src/zkvm/claim_reductions/advice.rs | 25 +++++++++++++++++++ .../src/zkvm/claim_reductions/precommitted.rs | 20 +++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index 16d5da4adb..ed1b0b30fe 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -261,6 +261,31 @@ impl AdviceClaimReductionProver { core: PrecomittedProver::new(params, advice_poly, eq_poly), } } + + pub fn from_bound_state( + params: AdviceClaimReductionParams, + advice_coeffs: Vec, + eq_coeffs: Vec, + ) -> Self { + Self::from_bound_state_with_scale(params, advice_coeffs, eq_coeffs, F::one()) + } + + pub fn from_bound_state_with_scale( + params: AdviceClaimReductionParams, + advice_coeffs: Vec, + eq_coeffs: Vec, + scale: F, + ) -> Self { + let mut prover = Self { + core: PrecomittedProver::new( + params, + MultilinearPolynomial::from(advice_coeffs), + MultilinearPolynomial::from(eq_coeffs), + ), + }; + prover.core.set_scale(scale); + prover + } } impl SumcheckInstanceProver for AdviceClaimReductionProver { diff --git a/jolt-core/src/zkvm/claim_reductions/precommitted.rs b/jolt-core/src/zkvm/claim_reductions/precommitted.rs index 3f201d482f..c6c6cd4d46 100644 --- a/jolt-core/src/zkvm/claim_reductions/precommitted.rs +++ b/jolt-core/src/zkvm/claim_reductions/precommitted.rs @@ -219,6 +219,18 @@ impl PrecommittedClaimReduction { .any(|&scheduled| scheduled == round) } + pub fn cycle_phase_rounds_debug(&self) -> &[usize] { + &self.cycle_phase_rounds + } + + pub fn address_phase_rounds_debug(&self) -> &[usize] { + &self.address_phase_rounds + } + + pub fn is_address_phase_active_round(&self, round: usize) -> bool { + self.address_phase_rounds.contains(&round) + } + #[inline] pub fn is_address_phase_round(&self, round: usize) -> bool { self.address_phase_rounds @@ -514,6 +526,14 @@ impl> PrecomittedProver { &mut self.params } + pub fn set_scale(&mut self, scale: F) { + self.scale = scale; + } + + pub fn scale(&self) -> F { + self.scale + } + fn compute_message_unscaled(&self, previous_claim_unscaled: F) -> UniPoly { let half = self.value_poly.len() / 2; let value_poly = &self.value_poly; From 4dc5cd9a5456863cbd7f28654c4ff245354dbc77 Mon Sep 17 00:00:00 2001 From: Quang Dao Date: Mon, 11 May 2026 18:18:42 -0600 Subject: [PATCH 06/24] fix(advice): align verifier scale with precommit order Compute the verifier advice equality evaluation in the same precommitted opening order used by the prover, while keeping the cycle-phase skip scale separate from the full address-phase scale. Avoid reading Dory main_t when the precommitted schedule does not need it, so unit tests that do not initialize Dory globals can still construct the schedule. (cherry picked from commit f19c179eed3fbf8bbb7424ebc2f6f7f7288eaa21) --- jolt-core/src/zkvm/claim_reductions/advice.rs | 136 ++++++++++++++++++ .../src/zkvm/claim_reductions/precommitted.rs | 7 +- 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index ed1b0b30fe..beeb6caa8b 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -198,6 +198,72 @@ impl SumcheckInstanceParams for AdviceClaimReductionParams { } } +impl AdviceClaimReductionParams { + #[cfg(feature = "zk")] + fn final_advice_output_claim_constraint(&self) -> Option { + let advice_opening = match self.kind { + AdviceKind::Trusted => OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReduction), + AdviceKind::Untrusted => OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReduction), + }; + Some(OutputClaimConstraint::linear(vec![( + ValueSource::Challenge(0), + ValueSource::Opening(advice_opening), + )])) + } + + fn final_advice_output_scale(&self, sumcheck_challenges: &[F::Challenge]) -> F { + let eq_eval = self.final_advice_eq_eval(sumcheck_challenges); + let scale = match self.phase { + PrecommittedPhase::CycleVariables => self.precommitted.cycle_phase_skip_scale(), + PrecommittedPhase::AddressVariables => { + precommitted_skip_round_scale(&self.precommitted) + } + }; + eq_eval * scale + } + + fn final_advice_eq_eval(&self, sumcheck_challenges: &[F::Challenge]) -> F { + let opening_point = OpeningPoint::::new( + self.precommitted + .poly_opening_round_permutation_be() + .iter() + .map(|&global_round| { + self.challenge_for_global_round(global_round, sumcheck_challenges) + }) + .collect(), + ); + EqPolynomial::mle(&opening_point.r, &self.r_val.r) + } + + fn challenge_for_global_round( + &self, + global_round: usize, + sumcheck_challenges: &[F::Challenge], + ) -> F::Challenge { + let cycle_rounds = self.precommitted.cycle_alignment_rounds(); + if global_round < cycle_rounds { + return match self.phase { + PrecommittedPhase::CycleVariables => sumcheck_challenges[global_round], + PrecommittedPhase::AddressVariables => { + let idx = self + .precommitted + .cycle_phase_rounds() + .binary_search(&global_round) + .expect("cycle round should be active for advice polynomial"); + self.precommitted.cycle_var_challenges[idx] + } + }; + } + + assert_eq!( + self.phase, + PrecommittedPhase::AddressVariables, + "cycle-phase final advice scale should not contain address rounds" + ); + sumcheck_challenges[global_round - cycle_rounds] + } +} + impl PrecomittedParams for AdviceClaimReductionParams { fn is_cycle_phase(&self) -> bool { self.phase == PrecommittedPhase::CycleVariables @@ -452,3 +518,73 @@ impl SumcheckInstanceVerifier params.round_offset(max_num_rounds) } } + +#[cfg(test)] +mod tests { + use super::*; + use ark_bn254::Fr; + + type Challenge = ::Challenge; + + #[test] + fn final_advice_output_scale_counts_leading_dummy_rounds_when_col_range_is_empty() { + let challenges = [0, 12, 13] + .map(|value| Challenge::from(value as u128)) + .to_vec(); + let active_opening_point = + OpeningPoint::::new(challenges[0..1].to_vec()).match_endianness(); + let scheduling_reference = PrecommittedSchedulingReference { + main_total_vars: 3, + reference_total_vars: 3, + cycle_alignment_rounds: 3, + address_rounds: 0, + joint_col_vars: 0, + }; + let params = AdviceClaimReductionParams { + kind: AdviceKind::Trusted, + phase: PrecommittedPhase::CycleVariables, + precommitted: PrecommittedClaimReduction::new(1, 0, scheduling_reference), + advice_col_vars: 0, + advice_row_vars: 1, + r_val: active_opening_point, + }; + + let two_inv = Fr::from_u64(2).inverse().unwrap(); + + assert_eq!( + params.final_advice_output_scale(&challenges), + two_inv * two_inv + ); + } + + #[test] + fn final_advice_output_scale_ignores_unrun_address_dummy_rounds() { + let challenges = [11, 12] + .map(|value| Challenge::from(value as u128)) + .to_vec(); + let r_val = OpeningPoint::::new(vec![Challenge::from(7)]); + let scheduling_reference = PrecommittedSchedulingReference { + main_total_vars: 4, + reference_total_vars: 4, + cycle_alignment_rounds: 2, + address_rounds: 2, + joint_col_vars: 0, + }; + let params = AdviceClaimReductionParams { + kind: AdviceKind::Trusted, + phase: PrecommittedPhase::CycleVariables, + precommitted: PrecommittedClaimReduction::new(1, 0, scheduling_reference), + advice_col_vars: 0, + advice_row_vars: 1, + r_val, + }; + + let two_inv = Fr::from_u64(2).inverse().unwrap(); + let expected_eq = EqPolynomial::mle(&[challenges[0]], ¶ms.r_val.r); + + assert_eq!( + params.final_advice_output_scale(&challenges), + expected_eq * two_inv + ); + } +} diff --git a/jolt-core/src/zkvm/claim_reductions/precommitted.rs b/jolt-core/src/zkvm/claim_reductions/precommitted.rs index c6c6cd4d46..866415d0fa 100644 --- a/jolt-core/src/zkvm/claim_reductions/precommitted.rs +++ b/jolt-core/src/zkvm/claim_reductions/precommitted.rs @@ -80,10 +80,15 @@ impl PrecommittedClaimReduction { 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 dense_cycle_prefix_rounds = if has_precommitted_dominance { + DoryGlobals::main_t().log_2() + } else { + 0 + }; let dory_opening_round_permutation_be = Self::reference_dory_opening_round_permutation_be( &scheduling_reference, has_precommitted_dominance, - DoryGlobals::main_t().log_2(), + dense_cycle_prefix_rounds, ); let poly_opening_round_permutation_be = Self::project_dory_round_permutation_for_poly( &dory_opening_round_permutation_be, From 7ab7f7fccadb9e45c3586bace3d4b452af503c17 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 11:02:47 -0700 Subject: [PATCH 07/24] fix(zkvm): align precommitted advice with stage6 split Co-authored-by: Cursor --- jolt-core/src/zkvm/claim_reductions/advice.rs | 14 +++++++----- jolt-core/src/zkvm/claim_reductions/mod.rs | 2 +- .../src/zkvm/claim_reductions/precommitted.rs | 22 +++++++++++++++++++ jolt-core/src/zkvm/verifier.rs | 2 +- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index beeb6caa8b..beb2190e04 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -9,8 +9,8 @@ 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, + AbstractVerifierOpeningAccumulator, OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, + SumcheckId, VerifierOpeningAccumulator, BIG_ENDIAN, LITTLE_ENDIAN, }; use crate::poly::unipoly::UniPoly; #[cfg(feature = "zk")] @@ -443,8 +443,12 @@ impl AdviceClaimReductionVerifier { } } -impl SumcheckInstanceVerifier +impl SumcheckInstanceVerifier for AdviceClaimReductionVerifier +where + F: JoltField, + T: Transcript, + A: AbstractVerifierOpeningAccumulator, { fn get_params(&self) -> &dyn SumcheckInstanceParams { unsafe { &*self.params.as_ptr() } @@ -452,7 +456,7 @@ impl SumcheckInstanceVerifier fn expected_output_claim( &self, - accumulator: &VerifierOpeningAccumulator, + accumulator: &A, sumcheck_challenges: &[F::Challenge], ) -> F { let params = self.params.borrow(); @@ -478,7 +482,7 @@ impl SumcheckInstanceVerifier fn cache_openings( &self, - accumulator: &mut VerifierOpeningAccumulator, + accumulator: &mut A, sumcheck_challenges: &[F::Challenge], ) { let mut params = self.params.borrow_mut(); diff --git a/jolt-core/src/zkvm/claim_reductions/mod.rs b/jolt-core/src/zkvm/claim_reductions/mod.rs index bbf39e34f7..b68d052551 100644 --- a/jolt-core/src/zkvm/claim_reductions/mod.rs +++ b/jolt-core/src/zkvm/claim_reductions/mod.rs @@ -8,7 +8,7 @@ pub mod registers; pub use advice::{ AdviceClaimReductionParams, AdviceClaimReductionProver, AdviceClaimReductionVerifier, - AdviceKind, ReductionPhase, + AdviceKind, }; pub use hamming_weight::{ HammingWeightClaimReductionParams, HammingWeightClaimReductionProver, diff --git a/jolt-core/src/zkvm/claim_reductions/precommitted.rs b/jolt-core/src/zkvm/claim_reductions/precommitted.rs index 866415d0fa..d87cceb440 100644 --- a/jolt-core/src/zkvm/claim_reductions/precommitted.rs +++ b/jolt-core/src/zkvm/claim_reductions/precommitted.rs @@ -228,10 +228,32 @@ impl PrecommittedClaimReduction { &self.cycle_phase_rounds } + pub fn cycle_phase_rounds(&self) -> &[usize] { + &self.cycle_phase_rounds + } + pub fn address_phase_rounds_debug(&self) -> &[usize] { &self.address_phase_rounds } + pub fn address_phase_rounds(&self) -> &[usize] { + &self.address_phase_rounds + } + + pub fn poly_opening_round_permutation_be(&self) -> &[usize] { + &self.poly_opening_round_permutation_be + } + + #[inline] + pub fn cycle_phase_skip_scale(&self) -> F { + let cycle_gap_len = self.cycle_phase_total_rounds - self.cycle_phase_rounds.len(); + if cycle_gap_len == 0 { + return F::one(); + } + let two_inv = F::from_u64(2).inverse().unwrap(); + (0..cycle_gap_len).fold(F::one(), |acc, _| acc * two_inv) + } + pub fn is_address_phase_active_round(&self, round: usize) -> bool { self.address_phase_rounds.contains(&round) } diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index 0e9cc6defc..e7e3444635 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, DoryGlobals}; +use crate::poly::commitment::dory::{bind_opening_inputs, DoryContext, DoryGlobals}; use crate::poly::commitment::pedersen::PedersenGenerators; #[cfg(feature = "zk")] use crate::poly::lagrange_poly::LagrangeHelper; From 711cdc209e4d3ca05c82772acb91e3354e7bf171 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 11:04:12 -0700 Subject: [PATCH 08/24] fix(zkvm): update transpilable verifier for precommitted advice Co-authored-by: Cursor --- jolt-core/src/zkvm/claim_reductions/advice.rs | 8 ++- jolt-core/src/zkvm/transpilable_verifier.rs | 55 +++++++++++++++++-- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index beb2190e04..f7fff54fc0 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -427,7 +427,7 @@ impl AdviceClaimReductionVerifier { advice_size_bytes: usize, trace_len: usize, scheduling_reference: PrecommittedSchedulingReference, - accumulator: &VerifierOpeningAccumulator, + accumulator: &dyn OpeningAccumulator, ) -> Self { let params = AdviceClaimReductionParams::new( kind, @@ -547,7 +547,8 @@ mod tests { let params = AdviceClaimReductionParams { kind: AdviceKind::Trusted, phase: PrecommittedPhase::CycleVariables, - precommitted: PrecommittedClaimReduction::new(1, 0, scheduling_reference), + precommitted: PrecommittedClaimReduction::new(1, 1, 0, scheduling_reference), + log_t: 0, advice_col_vars: 0, advice_row_vars: 1, r_val: active_opening_point, @@ -577,7 +578,8 @@ mod tests { let params = AdviceClaimReductionParams { kind: AdviceKind::Trusted, phase: PrecommittedPhase::CycleVariables, - precommitted: PrecommittedClaimReduction::new(1, 0, scheduling_reference), + precommitted: PrecommittedClaimReduction::new(1, 1, 0, scheduling_reference), + log_t: 0, advice_col_vars: 0, advice_row_vars: 1, r_val, diff --git a/jolt-core/src/zkvm/transpilable_verifier.rs b/jolt-core/src/zkvm/transpilable_verifier.rs index 92c2925647..3e784c53eb 100644 --- a/jolt-core/src/zkvm/transpilable_verifier.rs +++ b/jolt-core/src/zkvm/transpilable_verifier.rs @@ -41,12 +41,13 @@ use crate::curve::JoltCurve; use crate::poly::commitment::commitment_scheme::CommitmentScheme; +use crate::poly::commitment::dory::DoryGlobals; #[cfg(not(feature = "zk"))] use crate::poly::opening_proof::{OpeningPoint, BIG_ENDIAN}; use crate::subprotocols::sumcheck::{BatchedSumcheck, ClearSumcheckProof, SumcheckInstanceProof}; use crate::zkvm::claim_reductions::{ - AdviceClaimReductionVerifier, AdviceKind, HammingWeightClaimReductionVerifier, ReductionPhase, - RegistersClaimReductionSumcheckVerifier, + AdviceClaimReductionVerifier, AdviceKind, HammingWeightClaimReductionVerifier, + PrecommittedClaimReduction, RegistersClaimReductionSumcheckVerifier, }; use crate::zkvm::config::OneHotParams; use crate::zkvm::{ @@ -152,6 +153,36 @@ impl< A: AbstractVerifierOpeningAccumulator, > TranspilableVerifier<'a, F, C, PCS, ProofTranscript, A> { + #[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 + } + /// Create a TranspilableVerifier for real verification. /// /// This constructor creates a new `VerifierOpeningAccumulator` and populates @@ -560,6 +591,12 @@ impl< } 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) } @@ -631,20 +668,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, )); } From 938fe501b32fa66bd789d9197eb16f6572476ad3 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 11:04:52 -0700 Subject: [PATCH 09/24] fix(zkvm): transition precommitted advice in transpilable verifier Co-authored-by: Cursor --- jolt-core/src/zkvm/claim_reductions/advice.rs | 2 +- jolt-core/src/zkvm/transpilable_verifier.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index f7fff54fc0..2c824e346c 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -10,7 +10,7 @@ use crate::poly::multilinear_polynomial::MultilinearPolynomial; use crate::poly::opening_proof::OpeningId; use crate::poly::opening_proof::{ AbstractVerifierOpeningAccumulator, OpeningAccumulator, OpeningPoint, ProverOpeningAccumulator, - SumcheckId, VerifierOpeningAccumulator, BIG_ENDIAN, LITTLE_ENDIAN, + SumcheckId, BIG_ENDIAN, LITTLE_ENDIAN, }; use crate::poly::unipoly::UniPoly; #[cfg(feature = "zk")] diff --git a/jolt-core/src/zkvm/transpilable_verifier.rs b/jolt-core/src/zkvm/transpilable_verifier.rs index 3e784c53eb..a2147eb901 100644 --- a/jolt-core/src/zkvm/transpilable_verifier.rs +++ b/jolt-core/src/zkvm/transpilable_verifier.rs @@ -742,7 +742,7 @@ impl< { let mut params = advice_reduction_verifier_trusted.params.borrow_mut(); if params.num_address_phase_rounds() > 0 { - params.phase = ReductionPhase::AddressVariables; + params.transition_to_address_phase(); instances.push(advice_reduction_verifier_trusted); } } @@ -751,7 +751,7 @@ impl< { let mut params = advice_reduction_verifier_untrusted.params.borrow_mut(); if params.num_address_phase_rounds() > 0 { - params.phase = ReductionPhase::AddressVariables; + params.transition_to_address_phase(); instances.push(advice_reduction_verifier_untrusted); } } From 54490fca0ed7227e813aacba99bcae50e983e800 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 11:52:32 -0700 Subject: [PATCH 10/24] style: format precommitted advice branch Co-authored-by: Cursor --- jolt-core/src/zkvm/claim_reductions/advice.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index 2c824e346c..c5132256ed 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -443,8 +443,7 @@ impl AdviceClaimReductionVerifier { } } -impl SumcheckInstanceVerifier - for AdviceClaimReductionVerifier +impl SumcheckInstanceVerifier for AdviceClaimReductionVerifier where F: JoltField, T: Transcript, @@ -454,11 +453,7 @@ where unsafe { &*self.params.as_ptr() } } - fn expected_output_claim( - &self, - accumulator: &A, - sumcheck_challenges: &[F::Challenge], - ) -> F { + fn expected_output_claim(&self, accumulator: &A, sumcheck_challenges: &[F::Challenge]) -> F { let params = self.params.borrow(); match params.phase { PrecommittedPhase::CycleVariables => { @@ -480,11 +475,7 @@ where } } - fn cache_openings( - &self, - accumulator: &mut A, - sumcheck_challenges: &[F::Challenge], - ) { + fn cache_openings(&self, accumulator: &mut A, 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); From 57a76a9270dd2e168771f7ced1f8428097aedade Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 11:56:27 -0700 Subject: [PATCH 11/24] fix(zkvm): satisfy strict clippy for precommitted advice Co-authored-by: Cursor --- .../src/poly/commitment/dory/wrappers.rs | 138 +++++++++--------- jolt-core/src/zkvm/claim_reductions/advice.rs | 3 + .../src/zkvm/claim_reductions/precommitted.rs | 12 +- 3 files changed, 77 insertions(+), 76 deletions(-) diff --git a/jolt-core/src/poly/commitment/dory/wrappers.rs b/jolt-core/src/poly/commitment/dory/wrappers.rs index 32050be144..a850fe1372 100644 --- a/jolt-core/src/poly/commitment/dory/wrappers.rs +++ b/jolt-core/src/poly/commitment/dory/wrappers.rs @@ -28,6 +28,11 @@ pub use dory::backends::arkworks::{ }; pub type JoltFieldWrapper = ArkFr; +type DenseTier1Setup = ( + Vec, + usize, + Option>>, +); #[inline] pub fn jolt_to_ark(f: &Fr) -> ArkFr { @@ -215,76 +220,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): DenseTier1Setup = + 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/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index c5132256ed..709c147bbf 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -211,6 +211,7 @@ impl AdviceClaimReductionParams { )])) } + #[cfg(test)] fn final_advice_output_scale(&self, sumcheck_challenges: &[F::Challenge]) -> F { let eq_eval = self.final_advice_eq_eval(sumcheck_challenges); let scale = match self.phase { @@ -222,6 +223,7 @@ impl AdviceClaimReductionParams { eq_eval * scale } + #[cfg(test)] fn final_advice_eq_eval(&self, sumcheck_challenges: &[F::Challenge]) -> F { let opening_point = OpeningPoint::::new( self.precommitted @@ -235,6 +237,7 @@ impl AdviceClaimReductionParams { EqPolynomial::mle(&opening_point.r, &self.r_val.r) } + #[cfg(test)] fn challenge_for_global_round( &self, global_round: usize, diff --git a/jolt-core/src/zkvm/claim_reductions/precommitted.rs b/jolt-core/src/zkvm/claim_reductions/precommitted.rs index d87cceb440..f0c707d954 100644 --- a/jolt-core/src/zkvm/claim_reductions/precommitted.rs +++ b/jolt-core/src/zkvm/claim_reductions/precommitted.rs @@ -219,9 +219,7 @@ 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) } pub fn cycle_phase_rounds_debug(&self) -> &[usize] { @@ -260,9 +258,7 @@ impl PrecommittedClaimReduction { #[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] @@ -413,13 +409,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, From 23325c8117dfcb151ade12962d1b5a4dc2852d10 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 11:58:31 -0700 Subject: [PATCH 12/24] fix(zkvm): remove unused advice output constraint helper Co-authored-by: Cursor --- jolt-core/src/zkvm/claim_reductions/advice.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index 709c147bbf..a86b8650ce 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -199,18 +199,6 @@ impl SumcheckInstanceParams for AdviceClaimReductionParams { } impl AdviceClaimReductionParams { - #[cfg(feature = "zk")] - fn final_advice_output_claim_constraint(&self) -> Option { - let advice_opening = match self.kind { - AdviceKind::Trusted => OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReduction), - AdviceKind::Untrusted => OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReduction), - }; - Some(OutputClaimConstraint::linear(vec![( - ValueSource::Challenge(0), - ValueSource::Opening(advice_opening), - )])) - } - #[cfg(test)] fn final_advice_output_scale(&self, sumcheck_challenges: &[F::Challenge]) -> F { let eq_eval = self.final_advice_eq_eval(sumcheck_challenges); From b9bc3b5381b865de7e519ceced2c7fc053f86cd4 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 13:20:48 -0700 Subject: [PATCH 13/24] refactor(zkvm): align precommitted advice reduction shape Co-authored-by: Cursor --- jolt-core/src/zkvm/claim_reductions/advice.rs | 208 +++++-------- jolt-core/src/zkvm/claim_reductions/mod.rs | 4 +- .../src/zkvm/claim_reductions/precommitted.rs | 278 ++++++++++++------ jolt-core/src/zkvm/prover.rs | 4 +- jolt-core/src/zkvm/transpilable_verifier.rs | 10 +- jolt-core/src/zkvm/verifier.rs | 10 +- 6 files changed, 284 insertions(+), 230 deletions(-) diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index a86b8650ce..a4d3532291 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -18,14 +18,15 @@ 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, - PrecommittedSchedulingReference, TWO_PHASE_DEGREE_BOUND, + PrecommittedClaimReduction, PrecommittedPhase, PrecommittedSchedulingReference, + TWO_PHASE_DEGREE_BOUND, }; use allocative::Allocative; +use super::precommitted::{PrecommittedParams, PrecommittedProver}; + #[derive(Clone, Copy, Debug, PartialEq, Eq, Allocative)] pub enum AdviceKind { Trusted, @@ -35,9 +36,7 @@ pub enum AdviceKind { #[derive(Clone, Allocative)] 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 +46,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,44 +56,22 @@ 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, - phase: PrecommittedPhase::CycleVariables, precommitted, advice_col_vars, advice_row_vars, - log_t, r_val, } } - - pub fn num_address_phase_rounds(&self) -> usize { - 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 { + match self.precommitted.phase { PrecommittedPhase::CycleVariables => { accumulator .get_advice_opening(self.kind, SumcheckId::RamValCheck) @@ -117,21 +92,16 @@ impl SumcheckInstanceParams for AdviceClaimReductionParams { } fn num_rounds(&self) -> usize { - self.precommitted - .num_rounds_for_phase(self.phase == PrecommittedPhase::CycleVariables) + self.precommitted.num_rounds_for_current_phase() } 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(challenges) } #[cfg(feature = "zk")] fn input_claim_constraint(&self) -> InputClaimConstraint { - let opening = match self.phase { + let opening = match self.precommitted.phase { PrecommittedPhase::CycleVariables => match self.kind { AdviceKind::Trusted => OpeningId::TrustedAdvice(SumcheckId::RamValCheck), AdviceKind::Untrusted => OpeningId::UntrustedAdvice(SumcheckId::RamValCheck), @@ -155,54 +125,56 @@ impl SumcheckInstanceParams for AdviceClaimReductionParams { #[cfg(feature = "zk")] fn output_claim_constraint(&self) -> Option { - match self.phase { + match self.precommitted.phase { PrecommittedPhase::CycleVariables => { - let opening = match self.kind { - AdviceKind::Trusted => { - OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) - } - AdviceKind::Untrusted => { - OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) - } - }; - Some(OutputClaimConstraint::direct(opening)) - } - PrecommittedPhase::AddressVariables => { - let opening = match self.kind { - AdviceKind::Trusted => { - OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReduction) - } - AdviceKind::Untrusted => { - OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReduction) - } - }; - Some(OutputClaimConstraint::linear(vec![( - ValueSource::Challenge(0), - ValueSource::Opening(opening), - )])) + if self.precommitted.num_address_phase_rounds() > 0 { + let advice_opening = match self.kind { + AdviceKind::Trusted => { + OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) + } + AdviceKind::Untrusted => { + OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReductionCyclePhase) + } + }; + return Some(OutputClaimConstraint::direct(advice_opening)); + } + self.final_advice_output_claim_constraint() } + PrecommittedPhase::AddressVariables => self.final_advice_output_claim_constraint(), } } #[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_eval = EqPolynomial::mle(&opening_point.r, &self.r_val.r); - let scale: F = precommitted_skip_round_scale(&self.precommitted); - vec![eq_eval * scale] + match self.precommitted.phase { + PrecommittedPhase::CycleVariables + if self.precommitted.num_address_phase_rounds() > 0 => + { + vec![] + } + PrecommittedPhase::CycleVariables | PrecommittedPhase::AddressVariables => { + vec![self.final_advice_output_scale(sumcheck_challenges)] } } } } impl AdviceClaimReductionParams { - #[cfg(test)] + #[cfg(feature = "zk")] + fn final_advice_output_claim_constraint(&self) -> Option { + let advice_opening = match self.kind { + AdviceKind::Trusted => OpeningId::TrustedAdvice(SumcheckId::AdviceClaimReduction), + AdviceKind::Untrusted => OpeningId::UntrustedAdvice(SumcheckId::AdviceClaimReduction), + }; + Some(OutputClaimConstraint::linear(vec![( + ValueSource::Challenge(0), + ValueSource::Opening(advice_opening), + )])) + } + fn final_advice_output_scale(&self, sumcheck_challenges: &[F::Challenge]) -> F { let eq_eval = self.final_advice_eq_eval(sumcheck_challenges); - let scale = match self.phase { + let scale = match self.precommitted.phase { PrecommittedPhase::CycleVariables => self.precommitted.cycle_phase_skip_scale(), PrecommittedPhase::AddressVariables => { precommitted_skip_round_scale(&self.precommitted) @@ -211,7 +183,6 @@ impl AdviceClaimReductionParams { eq_eval * scale } - #[cfg(test)] fn final_advice_eq_eval(&self, sumcheck_challenges: &[F::Challenge]) -> F { let opening_point = OpeningPoint::::new( self.precommitted @@ -225,7 +196,6 @@ impl AdviceClaimReductionParams { EqPolynomial::mle(&opening_point.r, &self.r_val.r) } - #[cfg(test)] fn challenge_for_global_round( &self, global_round: usize, @@ -233,7 +203,7 @@ impl AdviceClaimReductionParams { ) -> F::Challenge { let cycle_rounds = self.precommitted.cycle_alignment_rounds(); if global_round < cycle_rounds { - return match self.phase { + return match self.precommitted.phase { PrecommittedPhase::CycleVariables => sumcheck_challenges[global_round], PrecommittedPhase::AddressVariables => { let idx = self @@ -247,7 +217,7 @@ impl AdviceClaimReductionParams { } assert_eq!( - self.phase, + self.precommitted.phase, PrecommittedPhase::AddressVariables, "cycle-phase final advice scale should not contain address rounds" ); @@ -255,35 +225,19 @@ impl AdviceClaimReductionParams { } } -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) +impl PrecommittedParams for AdviceClaimReductionParams { + fn precommitted(&self) -> &PrecommittedClaimReduction { + &self.precommitted } - 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); + fn precommitted_mut(&mut self) -> &mut PrecommittedClaimReduction { + &mut self.precommitted } } #[derive(Allocative)] pub struct AdviceClaimReductionProver { - core: PrecomittedProver>, + core: PrecommittedProver>, } impl AdviceClaimReductionProver { @@ -292,7 +246,7 @@ impl AdviceClaimReductionProver { } pub fn transition_to_address_phase(&mut self) { - self.core.params_mut().transition_to_address_phase(); + self.core.transition_to_address_phase(); } pub fn initialize( @@ -315,7 +269,7 @@ impl AdviceClaimReductionProver { }; Self { - core: PrecomittedProver::new(params, advice_poly, eq_poly), + core: PrecommittedProver::new(params, advice_poly, eq_poly, None), } } @@ -334,10 +288,11 @@ impl AdviceClaimReductionProver { scale: F, ) -> Self { let mut prover = Self { - core: PrecomittedProver::new( + core: PrecommittedProver::new( params, MultilinearPolynomial::from(advice_coeffs), MultilinearPolynomial::from(eq_coeffs), + None, ), }; prover.core.set_scale(scale); @@ -350,8 +305,8 @@ impl SumcheckInstanceProver for AdviceClaimRe self.core.params() } - fn round_offset(&self, max_num_rounds: usize) -> usize { - self.core.params().round_offset(max_num_rounds) + fn round_offset(&self, _max_num_rounds: usize) -> usize { + 0 } fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { @@ -369,7 +324,7 @@ impl SumcheckInstanceProver for AdviceClaimRe ) { let params = self.core.params(); let opening_point = params.normalize_opening_point(sumcheck_challenges); - if params.phase == PrecommittedPhase::CycleVariables { + if params.is_cycle_phase() { let c_mid = self.core.cycle_intermediate_claim(); match params.kind { @@ -416,14 +371,12 @@ impl AdviceClaimReductionVerifier { pub fn new( kind: AdviceKind, advice_size_bytes: usize, - trace_len: usize, scheduling_reference: PrecommittedSchedulingReference, accumulator: &dyn OpeningAccumulator, ) -> Self { let params = AdviceClaimReductionParams::new( kind, advice_size_bytes, - trace_len, scheduling_reference, accumulator, ); @@ -434,11 +387,8 @@ impl AdviceClaimReductionVerifier { } } -impl SumcheckInstanceVerifier for AdviceClaimReductionVerifier -where - F: JoltField, - T: Transcript, - A: AbstractVerifierOpeningAccumulator, +impl> + SumcheckInstanceVerifier for AdviceClaimReductionVerifier { fn get_params(&self) -> &dyn SumcheckInstanceParams { unsafe { &*self.params.as_ptr() } @@ -446,29 +396,30 @@ where fn expected_output_claim(&self, accumulator: &A, sumcheck_challenges: &[F::Challenge]) -> F { let params = self.params.borrow(); - match params.phase { - PrecommittedPhase::CycleVariables => { + match params.precommitted.phase { + PrecommittedPhase::CycleVariables + if params.precommitted.num_address_phase_rounds() > 0 => + { accumulator .get_advice_opening(params.kind, SumcheckId::AdviceClaimReductionCyclePhase) .unwrap_or_else(|| panic!("Cycle phase intermediate claim not found")) .1 } - PrecommittedPhase::AddressVariables => { - let opening_point = params.normalize_opening_point(sumcheck_challenges); + PrecommittedPhase::CycleVariables | PrecommittedPhase::AddressVariables => { 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 scale: F = precommitted_skip_round_scale(¶ms.precommitted); - advice_claim * eq_eval * scale + + // Account for Phase 1's internal dummy-gap traversal via constant scaling. + advice_claim * params.final_advice_output_scale(sumcheck_challenges) } } } fn cache_openings(&self, accumulator: &mut A, sumcheck_challenges: &[F::Challenge]) { let mut params = self.params.borrow_mut(); - if params.phase == PrecommittedPhase::CycleVariables { + if params.is_cycle_phase() { let opening_point = params.normalize_opening_point(sumcheck_challenges); match params.kind { AdviceKind::Trusted => accumulator.append_trusted_advice( @@ -486,9 +437,7 @@ where .set_cycle_var_challenges(opening_point_le.r); } - if params.num_address_phase_rounds() == 0 - || params.phase == PrecommittedPhase::AddressVariables - { + if params.precommitted.num_address_phase_rounds() == 0 || !params.is_cycle_phase() { let opening_point = params.normalize_opening_point(sumcheck_challenges); match params.kind { AdviceKind::Trusted => accumulator @@ -499,9 +448,8 @@ where } } - fn round_offset(&self, max_num_rounds: usize) -> usize { - let params = self.params.borrow(); - params.round_offset(max_num_rounds) + fn round_offset(&self, _max_num_rounds: usize) -> usize { + 0 } } @@ -528,9 +476,7 @@ mod tests { }; let params = AdviceClaimReductionParams { kind: AdviceKind::Trusted, - phase: PrecommittedPhase::CycleVariables, - precommitted: PrecommittedClaimReduction::new(1, 1, 0, scheduling_reference), - log_t: 0, + precommitted: PrecommittedClaimReduction::new(1, 0, scheduling_reference), advice_col_vars: 0, advice_row_vars: 1, r_val: active_opening_point, @@ -559,9 +505,7 @@ mod tests { }; let params = AdviceClaimReductionParams { kind: AdviceKind::Trusted, - phase: PrecommittedPhase::CycleVariables, - precommitted: PrecommittedClaimReduction::new(1, 1, 0, scheduling_reference), - log_t: 0, + precommitted: PrecommittedClaimReduction::new(1, 0, scheduling_reference), advice_col_vars: 0, advice_row_vars: 1, r_val, diff --git a/jolt-core/src/zkvm/claim_reductions/mod.rs b/jolt-core/src/zkvm/claim_reductions/mod.rs index b68d052551..81b1f73e0b 100644 --- a/jolt-core/src/zkvm/claim_reductions/mod.rs +++ b/jolt-core/src/zkvm/claim_reductions/mod.rs @@ -24,8 +24,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, + precommitted_sumcheck_inverse_index_permutation, PrecommittedClaimReduction, 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 index f0c707d954..88f5ef36ee 100644 --- a/jolt-core/src/zkvm/claim_reductions/precommitted.rs +++ b/jolt-core/src/zkvm/claim_reductions/precommitted.rs @@ -10,12 +10,6 @@ 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, @@ -34,9 +28,8 @@ 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, + pub phase: PrecommittedPhase, poly_opening_round_permutation_be: Vec, cycle_phase_rounds: Vec, cycle_phase_total_rounds: usize, @@ -72,14 +65,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 dense_cycle_prefix_rounds = if has_precommitted_dominance { DoryGlobals::main_t().log_2() } else { @@ -102,9 +93,8 @@ impl PrecommittedClaimReduction { ); Self { scheduling_reference, - embedding_mode, cycle_var_challenges: vec![], - dory_opening_round_permutation_be, + phase: PrecommittedPhase::CycleVariables, poly_opening_round_permutation_be, cycle_phase_rounds, cycle_phase_total_rounds: scheduling_reference.cycle_alignment_rounds, @@ -113,24 +103,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, @@ -212,6 +184,16 @@ impl PrecommittedClaimReduction { (cycle_phase_rounds, address_phase_rounds) } + #[inline] + pub fn is_cycle_phase(&self) -> bool { + self.phase == PrecommittedPhase::CycleVariables + } + + #[inline] + pub fn transition_to_address_phase(&mut self) { + self.phase = PrecommittedPhase::AddressVariables; + } + #[inline] pub fn num_address_phase_rounds(&self) -> usize { self.address_phase_rounds.len() @@ -222,26 +204,40 @@ impl PrecommittedClaimReduction { self.cycle_phase_rounds.contains(&round) } - pub fn cycle_phase_rounds_debug(&self) -> &[usize] { - &self.cycle_phase_rounds - } - + /// Indices of the cycle-phase rounds that this poly actively participates + /// in (i.e. rounds where the verifier evaluates the poly rather than + /// scaling by 1/2). The vector is sorted ascending and deduplicated. pub fn cycle_phase_rounds(&self) -> &[usize] { &self.cycle_phase_rounds } - pub fn address_phase_rounds_debug(&self) -> &[usize] { - &self.address_phase_rounds - } - + /// Indices of the address-phase rounds that this poly actively + /// participates in. Same conventions as [`Self::cycle_phase_rounds`]. pub fn address_phase_rounds(&self) -> &[usize] { &self.address_phase_rounds } + /// Big-endian round-permutation projected onto this poly's + /// `(poly_row_vars, poly_col_vars)` rectangle. + /// + /// The slice is `poly_row_vars + poly_col_vars` long: the first + /// `poly_row_vars` entries describe the row-side rounds, the rest the + /// column-side rounds. Pair this with + /// [`precommitted_sumcheck_inverse_index_permutation`] to permute a + /// length-`2^len` coefficient vector into opening order, instead of + /// re-deriving it from `scheduling_reference`. pub fn poly_opening_round_permutation_be(&self) -> &[usize] { &self.poly_opening_round_permutation_be } + /// The `(1/2)^cycle_gap` factor that "non-active" cycle-phase rounds + /// contribute to the running scale. Returns `F::one()` when there are + /// no inactive cycle-phase rounds. + /// + /// This is the cycle-only counterpart of + /// [`precommitted_skip_round_scale`], intended for callers that need + /// the scale strictly at the cycle-to-address handoff (e.g. when + /// constructing the address-phase prover). #[inline] pub fn cycle_phase_skip_scale(&self) -> F { let cycle_gap_len = self.cycle_phase_total_rounds - self.cycle_phase_rounds.len(); @@ -252,10 +248,6 @@ impl PrecommittedClaimReduction { (0..cycle_gap_len).fold(F::one(), |acc, _| acc * two_inv) } - pub fn is_address_phase_active_round(&self, round: usize) -> bool { - self.address_phase_rounds.contains(&round) - } - #[inline] pub fn is_address_phase_round(&self, round: usize) -> bool { self.address_phase_rounds.contains(&round) @@ -272,19 +264,14 @@ impl PrecommittedClaimReduction { } #[inline] - pub fn num_rounds_for_phase(&self, is_cycle_phase: bool) -> usize { - if is_cycle_phase { + pub fn num_rounds_for_current_phase(&self) -> usize { + if self.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 @@ -307,12 +294,9 @@ impl PrecommittedClaimReduction { 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 { + if self.is_cycle_phase() { let local_cycle_challenges: Vec = self .cycle_phase_rounds .iter() @@ -330,10 +314,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()); @@ -409,13 +389,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, @@ -473,7 +454,17 @@ fn precommitted_sumcheck_lsb_permutation( Some(old_lsb_to_new_lsb) } -fn precommitted_sumcheck_inverse_index_permutation( +/// Inverse index permutation for permuting a precommitted polynomial's +/// coefficient vector into the order implied by `poly_opening_round_permutation_be`. +/// +/// Returns `Some(perm)` such that `perm[new_idx] = old_idx`, suitable for +/// driving an out-of-place permute of a length-`coeffs_len` vector. Returns +/// `None` when the requested permutation is the identity, so callers can +/// short-circuit and skip the permute entirely. +/// +/// `coeffs_len` must equal `1 << poly_opening_round_permutation_be.len()`; +/// asserts otherwise. +pub fn precommitted_sumcheck_inverse_index_permutation( coeffs_len: usize, poly_opening_round_permutation_be: &[usize], ) -> Option> { @@ -510,33 +501,36 @@ fn precommitted_sumcheck_inverse_index_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); +pub trait PrecommittedParams: SumcheckInstanceParams { + fn precommitted(&self) -> &PrecommittedClaimReduction; + fn precommitted_mut(&mut self) -> &mut PrecommittedClaimReduction; + + fn is_cycle_phase(&self) -> bool { + self.precommitted().is_cycle_phase() + } } #[derive(Allocative)] -pub struct PrecomittedProver> { +pub struct PrecommittedProver> { params: P, value_poly: MultilinearPolynomial, eq_poly: MultilinearPolynomial, + aux_polys: Vec>, scale: F, } -impl> PrecomittedProver { +impl> PrecommittedProver { pub fn new( params: P, value_poly: MultilinearPolynomial, eq_poly: MultilinearPolynomial, + aux_polys: Option>>, ) -> Self { Self { params, value_poly, eq_poly, + aux_polys: aux_polys.unwrap_or_default(), scale: F::one(), } } @@ -545,16 +539,17 @@ impl> PrecomittedProver { &self.params } - pub fn params_mut(&mut self) -> &mut P { - &mut self.params + pub fn transition_to_address_phase(&mut self) { + self.params.precommitted_mut().transition_to_address_phase(); } pub fn set_scale(&mut self, scale: F) { self.scale = scale; } - pub fn scale(&self) -> F { - self.scale + #[expect(dead_code)] + pub fn aux_polys(&self) -> &[MultilinearPolynomial] { + &self.aux_polys } fn compute_message_unscaled(&self, previous_claim_unscaled: F) -> UniPoly { @@ -586,19 +581,20 @@ impl> PrecomittedProver { } pub fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { + let precommitted = self.params.precommitted(); let is_active_round = if self.params.is_cycle_phase() { - self.params.is_cycle_phase_round(round) + precommitted.is_cycle_phase_round(round) } else { - self.params.is_address_phase_round(round) + precommitted.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() + precommitted.cycle_alignment_rounds() } else { - self.params.address_alignment_rounds() + precommitted.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); @@ -609,19 +605,24 @@ impl> PrecomittedProver { 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) + let precommitted = self.params.precommitted(); + precommitted.is_cycle_phase_round(round) } else { - self.params.is_address_phase_round(round) + let precommitted = self.params.precommitted(); + precommitted.is_address_phase_round(round) }; if !is_active_round { self.scale *= F::from_u64(2).inverse().unwrap(); return; } + if self.params.is_cycle_phase() { + self.params.precommitted_mut().record_cycle_challenge(r_j); + } 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); + for aux_poly in self.aux_polys.iter_mut() { + aux_poly.bind_parallel(r_j, BindingOrder::LowToHigh); } } @@ -656,3 +657,116 @@ pub fn precommitted_skip_round_scale( let two_inv = F::from_u64(2).inverse().unwrap(); (0..gap_len).fold(F::one(), |acc, _| acc * two_inv) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::field::JoltField; + use ark_bn254::Fr; + use num_traits::One; + + fn make_reduction( + poly_opening_round_permutation_be: Vec, + cycle_phase_rounds: Vec, + cycle_phase_total_rounds: usize, + address_phase_rounds: Vec, + address_phase_total_rounds: usize, + ) -> PrecommittedClaimReduction { + let scheduling_reference = PrecommittedSchedulingReference { + main_total_vars: cycle_phase_total_rounds + address_phase_total_rounds, + reference_total_vars: cycle_phase_total_rounds + address_phase_total_rounds, + cycle_alignment_rounds: cycle_phase_total_rounds, + address_rounds: address_phase_total_rounds, + joint_col_vars: 0, + }; + PrecommittedClaimReduction { + scheduling_reference, + cycle_var_challenges: vec![], + phase: PrecommittedPhase::CycleVariables, + poly_opening_round_permutation_be, + cycle_phase_rounds, + cycle_phase_total_rounds, + address_phase_rounds, + address_phase_total_rounds, + } + } + + #[test] + fn poly_opening_round_permutation_be_returns_stored_field() { + let perm = vec![3usize, 0, 1, 2]; + let r = make_reduction(perm.clone(), vec![0, 1], 2, vec![0, 1], 2); + assert_eq!(r.poly_opening_round_permutation_be(), perm.as_slice()); + } + + #[test] + fn cycle_and_address_phase_rounds_accessors_match_internal_storage() { + let cycle = vec![0usize, 2, 3]; + let address = vec![1usize]; + let r = make_reduction(vec![0, 1, 2, 3], cycle.clone(), 4, address.clone(), 2); + assert_eq!(r.cycle_phase_rounds(), cycle.as_slice()); + assert_eq!(r.address_phase_rounds(), address.as_slice()); + } + + #[test] + fn cycle_phase_skip_scale_is_one_when_no_gap() { + let r = make_reduction(vec![0, 1], vec![0, 1], 2, vec![], 0); + assert_eq!(r.cycle_phase_skip_scale(), Fr::one()); + } + + #[test] + fn cycle_phase_skip_scale_is_two_inverse_per_inactive_round() { + let two_inv = Fr::from_u64(2).inverse().unwrap(); + // 1 inactive cycle round + let r1 = make_reduction(vec![0], vec![0], 2, vec![], 0); + assert_eq!(r1.cycle_phase_skip_scale(), two_inv); + // 3 inactive cycle rounds + let r3 = make_reduction(vec![0], vec![0], 4, vec![], 0); + assert_eq!(r3.cycle_phase_skip_scale(), two_inv * two_inv * two_inv); + // address-phase gap must NOT contribute (this is the cycle-only flavour) + let r_ignore = make_reduction(vec![0], vec![0], 1, vec![], 5); + assert_eq!(r_ignore.cycle_phase_skip_scale(), Fr::one()); + } + + #[test] + fn cycle_phase_skip_scale_agrees_with_full_skip_when_address_gap_is_zero() { + let r = make_reduction(vec![0, 1], vec![0], 3, vec![0, 1], 2); + // cycle_gap = 3 - 1 = 2, address_gap = 2 - 2 = 0 + // full = (1/2)^2; cycle_only = (1/2)^2; they should agree. + let full = precommitted_skip_round_scale(&r); + assert_eq!(r.cycle_phase_skip_scale(), full); + } + + #[test] + fn inverse_index_permutation_returns_none_for_identity() { + // BE descending = identity LSB permutation (no reordering). + let identity_be: Vec = (0..4).rev().collect(); + let perm = precommitted_sumcheck_inverse_index_permutation(1 << 4, &identity_be); + assert!( + perm.is_none(), + "identity permutation should be reported as None, got Some(len={})", + perm.map(|p| p.len()).unwrap_or(0), + ); + } + + #[test] + fn inverse_index_permutation_is_a_genuine_permutation_when_nontrivial() { + // Swap the two LSBs by reversing the BE round order partially. + let poly_perm_be: Vec = vec![0, 1, 3, 2]; + let coeffs_len = 1usize << poly_perm_be.len(); + let perm = precommitted_sumcheck_inverse_index_permutation(coeffs_len, &poly_perm_be) + .expect("non-identity permutation expected for this input"); + assert_eq!(perm.len(), coeffs_len); + let mut seen = vec![false; coeffs_len]; + for (new_idx, &old_idx) in perm.iter().enumerate() { + assert!( + old_idx < coeffs_len, + "perm[{new_idx}] = {old_idx} is out of bounds for coeffs_len={coeffs_len}", + ); + assert!( + !seen[old_idx], + "perm contains duplicate old_idx={old_idx} (at new_idx={new_idx})", + ); + seen[old_idx] = true; + } + } +} diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 174152a447..153100a3c4 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -1322,7 +1322,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, ); @@ -1345,7 +1344,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, ); @@ -1953,6 +1951,7 @@ impl< { if advice_reduction_prover_trusted .params() + .precommitted .num_address_phase_rounds() > 0 { @@ -1966,6 +1965,7 @@ impl< { if advice_reduction_prover_untrusted .params() + .precommitted .num_address_phase_rounds() > 0 { diff --git a/jolt-core/src/zkvm/transpilable_verifier.rs b/jolt-core/src/zkvm/transpilable_verifier.rs index a2147eb901..175cc24108 100644 --- a/jolt-core/src/zkvm/transpilable_verifier.rs +++ b/jolt-core/src/zkvm/transpilable_verifier.rs @@ -681,7 +681,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, )); @@ -690,7 +689,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, )); @@ -741,8 +739,8 @@ impl< self.advice_reduction_verifier_trusted.as_mut() { let mut params = advice_reduction_verifier_trusted.params.borrow_mut(); - if params.num_address_phase_rounds() > 0 { - params.transition_to_address_phase(); + if params.precommitted.num_address_phase_rounds() > 0 { + params.precommitted.transition_to_address_phase(); instances.push(advice_reduction_verifier_trusted); } } @@ -750,8 +748,8 @@ impl< self.advice_reduction_verifier_untrusted.as_mut() { let mut params = advice_reduction_verifier_untrusted.params.borrow_mut(); - if params.num_address_phase_rounds() > 0 { - params.transition_to_address_phase(); + if params.precommitted.num_address_phase_rounds() > 0 { + params.precommitted.transition_to_address_phase(); instances.push(advice_reduction_verifier_untrusted); } } diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index e7e3444635..b1dd181f6e 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -1251,7 +1251,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, )); @@ -1260,7 +1259,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, )); @@ -1629,9 +1627,9 @@ impl< self.advice_reduction_verifier_trusted.as_mut() { let mut params = advice_reduction_verifier_trusted.params.borrow_mut(); - if params.num_address_phase_rounds() > 0 { + if params.precommitted.num_address_phase_rounds() > 0 { // Transition phase - params.transition_to_address_phase(); + params.precommitted.transition_to_address_phase(); instances.push(advice_reduction_verifier_trusted); } } @@ -1639,9 +1637,9 @@ impl< self.advice_reduction_verifier_untrusted.as_mut() { let mut params = advice_reduction_verifier_untrusted.params.borrow_mut(); - if params.num_address_phase_rounds() > 0 { + if params.precommitted.num_address_phase_rounds() > 0 { // Transition phase - params.transition_to_address_phase(); + params.precommitted.transition_to_address_phase(); instances.push(advice_reduction_verifier_untrusted); } } From 275e05d8b85a0c0585fe57d63dcab342a1aa2c5a Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 13:38:50 -0700 Subject: [PATCH 14/24] refactor(dory): move address-major layout cleanup into precommitted stack Co-authored-by: Cursor --- .../src/poly/commitment/dory/dory_globals.rs | 21 ----------- jolt-core/src/poly/commitment/dory/tests.rs | 37 ++++++++++++++----- jolt-core/src/poly/one_hot_polynomial.rs | 10 ++--- 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/jolt-core/src/poly/commitment/dory/dory_globals.rs b/jolt-core/src/poly/commitment/dory/dory_globals.rs index 92ba091475..fdf430fc6b 100644 --- a/jolt-core/src/poly/commitment/dory/dory_globals.rs +++ b/jolt-core/src/poly/commitment/dory/dory_globals.rs @@ -395,26 +395,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 => { @@ -565,7 +545,6 @@ impl DoryGlobals { ) -> Option<()> { #[cfg(test)] Self::configure_test_cache_root(); - if context == DoryContext::Main { return Self::initialize_main_with_log_embedding(K, T, K.log_2() + T.log_2(), layout); } diff --git a/jolt-core/src/poly/commitment/dory/tests.rs b/jolt-core/src/poly/commitment/dory/tests.rs index ce4e73ea46..85f6a3ea57 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}; @@ -906,19 +907,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); @@ -1002,16 +1010,25 @@ 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 let Some(row) = i.checked_div(cycles_per_row) { + let col = (i % cycles_per_row) * K; + if row < num_rows && col < num_columns { + expected[col] += left_vec[row] * coeff; + } + } else { + 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; + } } } diff --git a/jolt-core/src/poly/one_hot_polynomial.rs b/jolt-core/src/poly/one_hot_polynomial.rs index 7134c04aac..f4d6b107ca 100644 --- a/jolt-core/src/poly/one_hot_polynomial.rs +++ b/jolt-core/src/poly/one_hot_polynomial.rs @@ -56,15 +56,11 @@ 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::AddressMajor => DoryGlobals::get_max_num_rows(), + DoryLayout::CycleMajor => { + (DoryGlobals::get_T() * self.K).div_ceil(DoryGlobals::get_num_columns()) } - DoryLayout::CycleMajor => (t * self.K).div_ceil(DoryGlobals::get_num_columns()), } } From 93634c088263059c0122cd2f69a4ae155d49e4aa Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 15:00:33 -0700 Subject: [PATCH 15/24] refactor(dory): align commitment scheme before committed programs Co-authored-by: Cursor --- .../poly/commitment/dory/commitment_scheme.rs | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/jolt-core/src/poly/commitment/dory/commitment_scheme.rs b/jolt-core/src/poly/commitment/dory/commitment_scheme.rs index fe08972379..5948cb2f95 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, @@ -44,6 +44,17 @@ impl DoryOpeningProofHint { } } + pub fn empty() -> Self { + Self { + row_commitments: Vec::new(), + commit_blind: ::zero(), + } + } + + pub fn rows(&self) -> &[ArkG1] { + &self.row_commitments + } + fn into_parts(self) -> (Vec, ArkFr) { (self.row_commitments, self.commit_blind) } @@ -63,6 +74,18 @@ fn maybe_blind_commitment(setup: &ArkworksProverSetup, commitment: ArkGT) -> (Ar } } +#[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], @@ -105,13 +128,13 @@ 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(test)] DoryGlobals::configure_test_cache_root(); - #[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 @@ -194,8 +217,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| { @@ -237,10 +259,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| { @@ -270,7 +290,7 @@ impl CommitmentScheme for DoryCommitmentScheme { setup.clone().into_inner(), &mut dory_transcript, ) - .map_err(|_| ProofVerifyError::InternalError)?; + .map_err(|err| ProofVerifyError::DoryError(format!("dory::verify failed: {err:?}")))?; Ok(()) } @@ -496,24 +516,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() - } -} From 49bfaeb967e67eb542620c5a48bf8d011328c7cf Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 15:08:04 -0700 Subject: [PATCH 16/24] chore(dory): remove unused opening hint accessors Co-authored-by: Cursor --- .../src/poly/commitment/dory/commitment_scheme.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/jolt-core/src/poly/commitment/dory/commitment_scheme.rs b/jolt-core/src/poly/commitment/dory/commitment_scheme.rs index 5948cb2f95..a52926f239 100644 --- a/jolt-core/src/poly/commitment/dory/commitment_scheme.rs +++ b/jolt-core/src/poly/commitment/dory/commitment_scheme.rs @@ -44,17 +44,6 @@ impl DoryOpeningProofHint { } } - pub fn empty() -> Self { - Self { - row_commitments: Vec::new(), - commit_blind: ::zero(), - } - } - - pub fn rows(&self) -> &[ArkG1] { - &self.row_commitments - } - fn into_parts(self) -> (Vec, ArkFr) { (self.row_commitments, self.commit_blind) } From 4d97029908ee996c25a559e221824bc6490fd80a Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 16:08:10 -0700 Subject: [PATCH 17/24] fix(dory): derive stage8 opening point by layout Use the cycle/address ordering expected by the active Dory layout when the main trace domain anchors Stage 8, so precommitted advice openings verify in AddressMajor mode. Co-authored-by: Cursor --- jolt-core/src/zkvm/prover.rs | 37 +++++++++++++++++++++++++--- jolt-core/src/zkvm/verifier.rs | 45 ++++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 153100a3c4..8129a40829 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -348,12 +348,43 @@ impl< } 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) + } + } } } diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index b1dd181f6e..d63d2de02c 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, DoryContext, DoryGlobals, DoryLayout}; use crate::poly::commitment::pedersen::PedersenGenerators; #[cfg(feature = "zk")] use crate::poly::lagrange_poly::LagrangeHelper; @@ -328,13 +328,48 @@ 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(format!( + "stage6 cycle challenges shorter than native cycle vars \ + (cycle_full_len={}, native_len={})", + r_cycle_stage6.len(), + native_cycle.len() + ))); + } + 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)) + } + } } } From cb11a7355153cf0339605399d90ab292165672af Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Wed, 20 May 2026 16:12:29 -0700 Subject: [PATCH 18/24] fix(dory): match final stage8 verifier error Keep the PR 02 Stage 8 verifier fix byte-for-byte aligned with the final merged branch while preserving the AddressMajor advice proof fix. Co-authored-by: Cursor --- jolt-core/src/zkvm/verifier.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index d63d2de02c..89e54d8d3a 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -349,12 +349,9 @@ impl< 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(format!( - "stage6 cycle challenges shorter than native cycle vars \ - (cycle_full_len={}, native_len={})", - 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!( From bb033e3e965928e4f1115a39caa3ca7ee838440c Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Fri, 22 May 2026 09:16:42 -0700 Subject: [PATCH 19/24] chore(dory): remove dense tier setup alias Use local type inference for the dense AddressMajor setup tuple to avoid carrying an intermediate-only alias. Made-with: Cursor Co-authored-by: Cursor --- jolt-core/src/poly/commitment/dory/wrappers.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/jolt-core/src/poly/commitment/dory/wrappers.rs b/jolt-core/src/poly/commitment/dory/wrappers.rs index a850fe1372..1ce7149ec3 100644 --- a/jolt-core/src/poly/commitment/dory/wrappers.rs +++ b/jolt-core/src/poly/commitment/dory/wrappers.rs @@ -28,11 +28,6 @@ pub use dory::backends::arkworks::{ }; pub type JoltFieldWrapper = ArkFr; -type DenseTier1Setup = ( - Vec, - usize, - Option>>, -); #[inline] pub fn jolt_to_ark(f: &Fr) -> ArkFr { @@ -220,7 +215,7 @@ where "Main+AddressMajor dense polynomial length exceeds trace T" ); - let (dense_affine_bases, dense_chunk_size, dense_sparse_row_terms): DenseTier1Setup = + 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; From 42eef5fd4abeae7125255f5bb50b2682bbf6ac25 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Fri, 22 May 2026 10:46:29 -0700 Subject: [PATCH 20/24] refactor(dory): share stage8 opening point selection Move the common Stage 8 Dory opening-point selection into the commitment scheme module so prover and verifier use the same layout logic. Made-with: Cursor Co-authored-by: Cursor --- jolt-core/src/zkvm/mod.rs | 88 +++++++++++++++++++++++++++++- jolt-core/src/zkvm/prover.rs | 83 +++-------------------------- jolt-core/src/zkvm/verifier.rs | 97 ++++------------------------------ 3 files changed, 105 insertions(+), 163 deletions(-) diff --git a/jolt-core/src/zkvm/mod.rs b/jolt-core/src/zkvm/mod.rs index 42564eb9c9..e5ee89f63e 100644 --- a/jolt-core/src/zkvm/mod.rs +++ b/jolt-core/src/zkvm/mod.rs @@ -7,9 +7,13 @@ use crate::{ field::JoltField, poly::commitment::commitment_scheme::CommitmentScheme, poly::commitment::dory::{DoryCommitmentScheme, DoryLayout}, - poly::opening_proof::ProverOpeningAccumulator, - poly::opening_proof::{OpeningId, SumcheckId}, + poly::opening_proof::{ + OpeningAccumulator, OpeningId, OpeningPoint, ProverOpeningAccumulator, SumcheckId, + BIG_ENDIAN, + }, transcripts::Transcript, + utils::errors::ProofVerifyError, + zkvm::claim_reductions::AdviceKind, }; // Compile-time error if multiple transcript features are enabled @@ -108,6 +112,86 @@ pub(crate) fn stage8_opening_ids( opening_ids } +pub(crate) fn compute_final_opening_point( + opening_accumulator: &impl OpeningAccumulator, + native_main_vars: usize, + log_k_chunk: usize, + layout: DoryLayout, +) -> Result, ProofVerifyError> { + let mut opening_candidates: Vec<(&str, OpeningPoint)> = Vec::new(); + if let Some((point, _)) = opening_accumulator + .get_advice_opening(AdviceKind::Trusted, SumcheckId::AdviceClaimReduction) + { + opening_candidates.push(("trusted_advice", point)); + } + if let Some((point, _)) = opening_accumulator + .get_advice_opening(AdviceKind::Untrusted, SumcheckId::AdviceClaimReduction) + { + opening_candidates.push(("untrusted_advice", point)); + } + + let (hamming_point, _) = opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::InstructionRa(0), + SumcheckId::HammingWeightClaimReduction, + ); + let (r_cycle_stage6, _) = opening_accumulator.get_committed_polynomial_opening( + CommittedPolynomial::RamInc, + SumcheckId::IncClaimReduction, + ); + + let max_len = opening_candidates + .iter() + .map(|(_, point)| point.r.len()) + .max() + .unwrap_or(0); + if max_len > native_main_vars { + let dominant = opening_candidates + .iter() + .find(|(_, point)| point.r.len() == max_len) + .expect("at least one dominant precommitted candidate expected"); + for (name, point) in opening_candidates + .iter() + .filter(|(_, point)| point.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 { + let r_address_stage7 = hamming_point.r[..log_k_chunk].to_vec(); + + match layout { + DoryLayout::AddressMajor => Ok(OpeningPoint::::new( + [r_cycle_stage6.r.as_slice(), r_address_stage7.as_slice()].concat(), + )), + DoryLayout::CycleMajor => { + let native_cycle = &hamming_point.r[log_k_chunk..]; + if r_cycle_stage6.r.len() < native_cycle.len() { + return Err(ProofVerifyError::DoryError( + "stage6 cycle challenges shorter than native cycle vars".to_string(), + )); + } + if r_cycle_stage6.r[..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.r.len(), + native_cycle.len() + ))); + } + let cycle_extra = &r_cycle_stage6.r[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)) + } + } + } +} + // Scoped CPU profiler for performance analysis. Feature-gated by "pprof". // Usage: let _guard = pprof_scope!("label"); // diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 8129a40829..9696e69d19 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -103,7 +103,7 @@ use crate::{ bytecode::read_raf_checking::{ BytecodeReadRafAddressSumcheckProver, BytecodeReadRafCycleSumcheckProver, }, - fiat_shamir_preamble, + compute_final_opening_point, fiat_shamir_preamble, instruction_lookups::{ ra_virtual::InstructionRaSumcheckProver as LookupsRaSumcheckProver, read_raf_checking::InstructionReadRafSumcheckProver, @@ -312,80 +312,13 @@ impl< 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 - ); - } - OpeningPoint::::new(dominant.1.r.clone()) - } else { - 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::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) - } - } - } + compute_final_opening_point( + &self.opening_accumulator, + native_main_vars, + self.one_hot_params.log_k_chunk, + DoryGlobals::get_layout(), + ) + .expect("invalid prover Stage-8 opening point") } pub fn gen_from_trace( diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index 89e54d8d3a..d09ccfc20b 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, DoryLayout}; +use crate::poly::commitment::dory::{bind_opening_inputs, DoryContext, DoryGlobals}; use crate::poly::commitment::pedersen::PedersenGenerators; #[cfg(feature = "zk")] use crate::poly::lagrange_poly::LagrangeHelper; @@ -50,7 +50,7 @@ use crate::zkvm::{ IncClaimReductionSumcheckVerifier, InstructionLookupsClaimReductionSumcheckVerifier, PrecommittedClaimReduction, RamRaClaimReductionSumcheckVerifier, }, - fiat_shamir_preamble, + compute_final_opening_point, fiat_shamir_preamble, instruction_lookups::{ ra_virtual::RaSumcheckVerifier as LookupsRaSumcheckVerifier, read_raf_checking::InstructionReadRafSumcheckVerifier, @@ -79,8 +79,8 @@ use crate::zkvm::{ use crate::{ field::JoltField, poly::opening_proof::{ - compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningId, OpeningPoint, - SumcheckId, VerifierOpeningAccumulator, BIG_ENDIAN, + compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningId, SumcheckId, + VerifierOpeningAccumulator, }, pprof_scope, subprotocols::{ @@ -289,87 +289,6 @@ impl< 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 { - 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::RamInc, - SumcheckId::IncClaimReduction, - ) - .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)) - } - } - } - } - pub fn new( preprocessing: &'a JoltVerifierPreprocessing, proof: JoltProof, @@ -1724,7 +1643,13 @@ impl< /// Stage 8: Dory batch opening verification. fn verify_stage8(&mut self) -> Result, ProofVerifyError> { - let opening_point = self.stage8_opening_point()?; + let native_main_vars = self.proof.trace_length.log_2() + self.one_hot_params.log_k_chunk; + let opening_point = compute_final_opening_point( + &self.opening_accumulator, + native_main_vars, + self.one_hot_params.log_k_chunk, + self.proof.dory_layout, + )?; // 1. Collect all (polynomial, claim) pairs let mut polynomial_claims = Vec::new(); From b12d91470ca51bf363042f0fbe734d995cff87c8 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Fri, 22 May 2026 23:35:41 -0700 Subject: [PATCH 21/24] fix(zkvm): preserve precommitted cycle openings Cache real cycle-phase opening points for precommitted advice reductions so verifier phase transitions reconstruct the address phase from accumulator state. Made-with: Cursor Co-authored-by: Cursor --- jolt-core/src/zkvm/claim_reductions/advice.rs | 43 +++--- jolt-core/src/zkvm/claim_reductions/mod.rs | 4 +- .../src/zkvm/claim_reductions/precommitted.rs | 37 +++-- jolt-core/src/zkvm/transpilable_verifier.rs | 34 +++-- jolt-core/src/zkvm/verifier.rs | 127 +++++++++++++++--- 5 files changed, 180 insertions(+), 65 deletions(-) diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index a4d3532291..b577c9d4ec 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -1,7 +1,5 @@ //! Two-phase advice claim reduction (Stage 6 cycle -> Stage 7 address). -use std::cell::RefCell; - use crate::field::JoltField; use crate::poly::commitment::dory::DoryGlobals; use crate::poly::eq_poly::EqPolynomial; @@ -233,6 +231,18 @@ impl PrecommittedParams for AdviceClaimReductionParams { fn precommitted_mut(&mut self) -> &mut PrecommittedClaimReduction { &mut self.precommitted } + + fn get_cycle_challenges>( + &self, + accumulator: &A, + ) -> Vec { + let (cycle_opening_point, _) = accumulator + .get_advice_opening(self.kind, SumcheckId::AdviceClaimReductionCyclePhase) + .expect("Cycle phase intermediate claim not found"); + let opening_point_le: OpeningPoint = + cycle_opening_point.match_endianness(); + opening_point_le.r + } } #[derive(Allocative)] @@ -324,18 +334,17 @@ impl SumcheckInstanceProver for AdviceClaimRe ) { let params = self.core.params(); let opening_point = params.normalize_opening_point(sumcheck_challenges); - if params.is_cycle_phase() { + if params.is_cycle_phase() && params.precommitted.num_address_phase_rounds() > 0 { let c_mid = self.core.cycle_intermediate_claim(); - match params.kind { AdviceKind::Trusted => accumulator.append_trusted_advice( SumcheckId::AdviceClaimReductionCyclePhase, - OpeningPoint::::new(vec![]), + opening_point.clone(), c_mid, ), AdviceKind::Untrusted => accumulator.append_untrusted_advice( SumcheckId::AdviceClaimReductionCyclePhase, - OpeningPoint::::new(vec![]), + opening_point.clone(), c_mid, ), } @@ -364,7 +373,7 @@ impl SumcheckInstanceProver for AdviceClaimRe } pub struct AdviceClaimReductionVerifier { - pub params: RefCell>, + pub params: AdviceClaimReductionParams, } impl AdviceClaimReductionVerifier { @@ -381,9 +390,7 @@ impl AdviceClaimReductionVerifier { accumulator, ); - Self { - params: RefCell::new(params), - } + Self { params } } } @@ -391,11 +398,11 @@ impl> SumcheckInstanceVerifier for AdviceClaimReductionVerifier { fn get_params(&self) -> &dyn SumcheckInstanceParams { - unsafe { &*self.params.as_ptr() } + &self.params } fn expected_output_claim(&self, accumulator: &A, sumcheck_challenges: &[F::Challenge]) -> F { - let params = self.params.borrow(); + let params = &self.params; match params.precommitted.phase { PrecommittedPhase::CycleVariables if params.precommitted.num_address_phase_rounds() > 0 => @@ -418,23 +425,19 @@ impl> } fn cache_openings(&self, accumulator: &mut A, sumcheck_challenges: &[F::Challenge]) { - let mut params = self.params.borrow_mut(); - if params.is_cycle_phase() { + let params = &self.params; + if params.is_cycle_phase() && params.precommitted.num_address_phase_rounds() > 0 { let opening_point = params.normalize_opening_point(sumcheck_challenges); match params.kind { AdviceKind::Trusted => accumulator.append_trusted_advice( SumcheckId::AdviceClaimReductionCyclePhase, - OpeningPoint::::new(vec![]), + opening_point.clone(), ), AdviceKind::Untrusted => accumulator.append_untrusted_advice( SumcheckId::AdviceClaimReductionCyclePhase, - OpeningPoint::::new(vec![]), + opening_point, ), } - let opening_point_le: OpeningPoint = opening_point.match_endianness(); - params - .precommitted - .set_cycle_var_challenges(opening_point_le.r); } if params.precommitted.num_address_phase_rounds() == 0 || !params.is_cycle_phase() { diff --git a/jolt-core/src/zkvm/claim_reductions/mod.rs b/jolt-core/src/zkvm/claim_reductions/mod.rs index 81b1f73e0b..f56ab2f77c 100644 --- a/jolt-core/src/zkvm/claim_reductions/mod.rs +++ b/jolt-core/src/zkvm/claim_reductions/mod.rs @@ -24,8 +24,8 @@ pub use instruction_lookups::{ }; pub use precommitted::{ permute_precommitted_polys, precommitted_eq_evals_with_scaling, precommitted_skip_round_scale, - precommitted_sumcheck_inverse_index_permutation, PrecommittedClaimReduction, PrecommittedPhase, - PrecommittedSchedulingReference, TWO_PHASE_DEGREE_BOUND, + precommitted_sumcheck_inverse_index_permutation, PrecommittedClaimReduction, + PrecommittedParams, 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 index 88f5ef36ee..240c042d8e 100644 --- a/jolt-core/src/zkvm/claim_reductions/precommitted.rs +++ b/jolt-core/src/zkvm/claim_reductions/precommitted.rs @@ -5,7 +5,9 @@ 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::opening_proof::{ + AbstractVerifierOpeningAccumulator, OpeningPoint, BIG_ENDIAN, LITTLE_ENDIAN, +}; use crate::poly::unipoly::UniPoly; use crate::subprotocols::sumcheck_verifier::SumcheckInstanceParams; use crate::utils::math::Math; @@ -189,11 +191,6 @@ impl PrecommittedClaimReduction { self.phase == PrecommittedPhase::CycleVariables } - #[inline] - pub fn transition_to_address_phase(&mut self) { - self.phase = PrecommittedPhase::AddressVariables; - } - #[inline] pub fn num_address_phase_rounds(&self) -> usize { self.address_phase_rounds.len() @@ -338,11 +335,6 @@ impl PrecommittedClaimReduction { 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( @@ -508,6 +500,27 @@ pub trait PrecommittedParams: SumcheckInstanceParams { fn is_cycle_phase(&self) -> bool { self.precommitted().is_cycle_phase() } + + fn get_cycle_challenges>( + &self, + accumulator: &A, + ) -> Vec; + + fn transition_to_address_phase>( + &mut self, + accumulator: &A, + ) { + let cycle_challenges = if self.precommitted().num_address_phase_rounds() > 0 { + Some(self.get_cycle_challenges(accumulator)) + } else { + None + }; + let precommitted = self.precommitted_mut(); + if let Some(cycle_challenges) = cycle_challenges { + precommitted.cycle_var_challenges = cycle_challenges; + } + precommitted.phase = PrecommittedPhase::AddressVariables; + } } #[derive(Allocative)] @@ -540,7 +553,7 @@ impl> PrecommittedProver { } pub fn transition_to_address_phase(&mut self) { - self.params.precommitted_mut().transition_to_address_phase(); + self.params.precommitted_mut().phase = PrecommittedPhase::AddressVariables; } pub fn set_scale(&mut self, scale: F) { diff --git a/jolt-core/src/zkvm/transpilable_verifier.rs b/jolt-core/src/zkvm/transpilable_verifier.rs index 175cc24108..fa3418d753 100644 --- a/jolt-core/src/zkvm/transpilable_verifier.rs +++ b/jolt-core/src/zkvm/transpilable_verifier.rs @@ -47,7 +47,7 @@ use crate::poly::opening_proof::{OpeningPoint, BIG_ENDIAN}; use crate::subprotocols::sumcheck::{BatchedSumcheck, ClearSumcheckProof, SumcheckInstanceProof}; use crate::zkvm::claim_reductions::{ AdviceClaimReductionVerifier, AdviceKind, HammingWeightClaimReductionVerifier, - PrecommittedClaimReduction, RegistersClaimReductionSumcheckVerifier, + PrecommittedClaimReduction, PrecommittedParams, RegistersClaimReductionSumcheckVerifier, }; use crate::zkvm::config::OneHotParams; use crate::zkvm::{ @@ -283,9 +283,9 @@ impl< .map_err(ProofVerifyError::InvalidReadWriteConfig)?; // Construct full params from the validated config - let bytecode_K = preprocessing.shared.bytecode.code_size; + let bytecode_len = preprocessing.shared.bytecode.code_size; let one_hot_params = - OneHotParams::from_config(&proof.one_hot_config, bytecode_K, proof.ram_K); + OneHotParams::from_config(&proof.one_hot_config, bytecode_len, proof.ram_K); Ok(TranspilableVerifier { trusted_advice_commitment, @@ -314,9 +314,9 @@ impl< opening_accumulator: A, ) -> Self { let spartan_key = UniformSpartanKey::new(proof.trace_length.next_power_of_two()); - let bytecode_K = preprocessing.shared.bytecode.code_size; + let bytecode_len = preprocessing.shared.bytecode.code_size; let one_hot_params = - OneHotParams::from_config(&proof.one_hot_config, bytecode_K, proof.ram_K); + OneHotParams::from_config(&proof.one_hot_config, bytecode_len, proof.ram_K); Self { trusted_advice_commitment, @@ -738,18 +738,30 @@ impl< if let Some(advice_reduction_verifier_trusted) = self.advice_reduction_verifier_trusted.as_mut() { - let mut params = advice_reduction_verifier_trusted.params.borrow_mut(); - if params.precommitted.num_address_phase_rounds() > 0 { - params.precommitted.transition_to_address_phase(); + if advice_reduction_verifier_trusted + .params + .precommitted + .num_address_phase_rounds() + > 0 + { + advice_reduction_verifier_trusted + .params + .transition_to_address_phase(&self.opening_accumulator); instances.push(advice_reduction_verifier_trusted); } } if let Some(advice_reduction_verifier_untrusted) = self.advice_reduction_verifier_untrusted.as_mut() { - let mut params = advice_reduction_verifier_untrusted.params.borrow_mut(); - if params.precommitted.num_address_phase_rounds() > 0 { - params.precommitted.transition_to_address_phase(); + if advice_reduction_verifier_untrusted + .params + .precommitted + .num_address_phase_rounds() + > 0 + { + advice_reduction_verifier_untrusted + .params + .transition_to_address_phase(&self.opening_accumulator); instances.push(advice_reduction_verifier_untrusted); } } diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index d09ccfc20b..524e37519b 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, DoryContext, DoryGlobals, DoryLayout}; use crate::poly::commitment::pedersen::PedersenGenerators; #[cfg(feature = "zk")] use crate::poly::lagrange_poly::LagrangeHelper; @@ -48,9 +48,9 @@ use crate::zkvm::{ claim_reductions::{ AdviceClaimReductionVerifier, AdviceKind, HammingWeightClaimReductionVerifier, IncClaimReductionSumcheckVerifier, InstructionLookupsClaimReductionSumcheckVerifier, - PrecommittedClaimReduction, RamRaClaimReductionSumcheckVerifier, + PrecommittedClaimReduction, PrecommittedParams, RamRaClaimReductionSumcheckVerifier, }, - compute_final_opening_point, fiat_shamir_preamble, + fiat_shamir_preamble, instruction_lookups::{ ra_virtual::RaSumcheckVerifier as LookupsRaSumcheckVerifier, read_raf_checking::InstructionReadRafSumcheckVerifier, @@ -79,8 +79,8 @@ use crate::zkvm::{ use crate::{ field::JoltField, poly::opening_proof::{ - compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningId, SumcheckId, - VerifierOpeningAccumulator, + compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningId, OpeningPoint, + SumcheckId, VerifierOpeningAccumulator, BIG_ENDIAN, }, pprof_scope, subprotocols::{ @@ -289,6 +289,87 @@ impl< 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 { + 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::RamInc, + SumcheckId::IncClaimReduction, + ) + .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)) + } + } + } + } + pub fn new( preprocessing: &'a JoltVerifierPreprocessing, proof: JoltProof, @@ -391,9 +472,9 @@ impl< .map_err(ProofVerifyError::InvalidReadWriteConfig)?; // Construct full params from the validated config. - let bytecode_K = preprocessing.shared.bytecode.code_size; + let bytecode_len = preprocessing.shared.bytecode.code_size; let one_hot_params = - OneHotParams::from_config(&proof.one_hot_config, bytecode_K, proof.ram_K); + OneHotParams::from_config(&proof.one_hot_config, bytecode_len, proof.ram_K); Ok(Self { trusted_advice_commitment, @@ -1577,20 +1658,32 @@ impl< if let Some(advice_reduction_verifier_trusted) = self.advice_reduction_verifier_trusted.as_mut() { - let mut params = advice_reduction_verifier_trusted.params.borrow_mut(); - if params.precommitted.num_address_phase_rounds() > 0 { + if advice_reduction_verifier_trusted + .params + .precommitted + .num_address_phase_rounds() + > 0 + { // Transition phase - params.precommitted.transition_to_address_phase(); + advice_reduction_verifier_trusted + .params + .transition_to_address_phase(&self.opening_accumulator); instances.push(advice_reduction_verifier_trusted); } } if let Some(advice_reduction_verifier_untrusted) = self.advice_reduction_verifier_untrusted.as_mut() { - let mut params = advice_reduction_verifier_untrusted.params.borrow_mut(); - if params.precommitted.num_address_phase_rounds() > 0 { + if advice_reduction_verifier_untrusted + .params + .precommitted + .num_address_phase_rounds() + > 0 + { // Transition phase - params.precommitted.transition_to_address_phase(); + advice_reduction_verifier_untrusted + .params + .transition_to_address_phase(&self.opening_accumulator); instances.push(advice_reduction_verifier_untrusted); } } @@ -1643,13 +1736,7 @@ impl< /// Stage 8: Dory batch opening verification. fn verify_stage8(&mut self) -> Result, ProofVerifyError> { - let native_main_vars = self.proof.trace_length.log_2() + self.one_hot_params.log_k_chunk; - let opening_point = compute_final_opening_point( - &self.opening_accumulator, - native_main_vars, - self.one_hot_params.log_k_chunk, - self.proof.dory_layout, - )?; + let opening_point = self.stage8_opening_point()?; // 1. Collect all (polynomial, claim) pairs let mut polynomial_claims = Vec::new(); From 672bf3a196a6db552d830b218c383902f7a31a91 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Mon, 25 May 2026 12:25:08 -0700 Subject: [PATCH 22/24] fix(zkvm): gate final opening helper behind prover Co-authored-by: Cursor --- jolt-core/src/zkvm/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/jolt-core/src/zkvm/mod.rs b/jolt-core/src/zkvm/mod.rs index e5ee89f63e..f8bc24ce4f 100644 --- a/jolt-core/src/zkvm/mod.rs +++ b/jolt-core/src/zkvm/mod.rs @@ -7,11 +7,12 @@ use crate::{ field::JoltField, poly::commitment::commitment_scheme::CommitmentScheme, poly::commitment::dory::{DoryCommitmentScheme, DoryLayout}, - poly::opening_proof::{ - OpeningAccumulator, OpeningId, OpeningPoint, ProverOpeningAccumulator, SumcheckId, - BIG_ENDIAN, - }, + poly::opening_proof::{OpeningId, ProverOpeningAccumulator, SumcheckId}, transcripts::Transcript, +}; +#[cfg(feature = "prover")] +use crate::{ + poly::opening_proof::{OpeningAccumulator, OpeningPoint, BIG_ENDIAN}, utils::errors::ProofVerifyError, zkvm::claim_reductions::AdviceKind, }; @@ -112,6 +113,7 @@ pub(crate) fn stage8_opening_ids( opening_ids } +#[cfg(feature = "prover")] pub(crate) fn compute_final_opening_point( opening_accumulator: &impl OpeningAccumulator, native_main_vars: usize, From 3f0c1f6b97124fae8987c713b9794d4efbf09506 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Mon, 25 May 2026 15:26:05 -0700 Subject: [PATCH 23/24] fix(zkvm): share final opening helper Co-authored-by: Cursor --- jolt-core/src/zkvm/mod.rs | 10 ++-- jolt-core/src/zkvm/prover.rs | 20 +++---- jolt-core/src/zkvm/verifier.rs | 97 ++++------------------------------ 3 files changed, 23 insertions(+), 104 deletions(-) diff --git a/jolt-core/src/zkvm/mod.rs b/jolt-core/src/zkvm/mod.rs index f8bc24ce4f..e5ee89f63e 100644 --- a/jolt-core/src/zkvm/mod.rs +++ b/jolt-core/src/zkvm/mod.rs @@ -7,12 +7,11 @@ use crate::{ field::JoltField, poly::commitment::commitment_scheme::CommitmentScheme, poly::commitment::dory::{DoryCommitmentScheme, DoryLayout}, - poly::opening_proof::{OpeningId, ProverOpeningAccumulator, SumcheckId}, + poly::opening_proof::{ + OpeningAccumulator, OpeningId, OpeningPoint, ProverOpeningAccumulator, SumcheckId, + BIG_ENDIAN, + }, transcripts::Transcript, -}; -#[cfg(feature = "prover")] -use crate::{ - poly::opening_proof::{OpeningAccumulator, OpeningPoint, BIG_ENDIAN}, utils::errors::ProofVerifyError, zkvm::claim_reductions::AdviceKind, }; @@ -113,7 +112,6 @@ pub(crate) fn stage8_opening_ids( opening_ids } -#[cfg(feature = "prover")] pub(crate) fn compute_final_opening_point( opening_accumulator: &impl OpeningAccumulator, native_main_vars: usize, diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 9696e69d19..0547fd2320 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -310,17 +310,6 @@ impl< candidates } - fn stage8_opening_point(&self) -> OpeningPoint { - let native_main_vars = self.trace.len().log_2() + self.one_hot_params.log_k_chunk; - compute_final_opening_point( - &self.opening_accumulator, - native_main_vars, - self.one_hot_params.log_k_chunk, - DoryGlobals::get_layout(), - ) - .expect("invalid prover Stage-8 opening point") - } - pub fn gen_from_trace( preprocessing: &'a JoltProverPreprocessing, lazy_trace: LazyTraceIterator, @@ -1961,7 +1950,14 @@ impl< ) -> PCS::Proof { tracing::info!("Stage 8 proving (Dory batch opening)"); - let opening_point = self.stage8_opening_point(); + let native_main_vars = self.trace.len().log_2() + self.one_hot_params.log_k_chunk; + let opening_point = compute_final_opening_point( + &self.opening_accumulator, + native_main_vars, + self.one_hot_params.log_k_chunk, + DoryGlobals::get_layout(), + ) + .expect("invalid prover Stage-8 opening point"); let mut polynomial_claims = Vec::new(); let mut scaling_factors = Vec::new(); diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index 524e37519b..d644e71d76 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, DoryLayout}; +use crate::poly::commitment::dory::{bind_opening_inputs, DoryContext, DoryGlobals}; use crate::poly::commitment::pedersen::PedersenGenerators; #[cfg(feature = "zk")] use crate::poly::lagrange_poly::LagrangeHelper; @@ -50,7 +50,7 @@ use crate::zkvm::{ IncClaimReductionSumcheckVerifier, InstructionLookupsClaimReductionSumcheckVerifier, PrecommittedClaimReduction, PrecommittedParams, RamRaClaimReductionSumcheckVerifier, }, - fiat_shamir_preamble, + compute_final_opening_point, fiat_shamir_preamble, instruction_lookups::{ ra_virtual::RaSumcheckVerifier as LookupsRaSumcheckVerifier, read_raf_checking::InstructionReadRafSumcheckVerifier, @@ -79,8 +79,8 @@ use crate::zkvm::{ use crate::{ field::JoltField, poly::opening_proof::{ - compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningId, OpeningPoint, - SumcheckId, VerifierOpeningAccumulator, BIG_ENDIAN, + compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningId, SumcheckId, + VerifierOpeningAccumulator, }, pprof_scope, subprotocols::{ @@ -289,87 +289,6 @@ impl< 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 { - 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::RamInc, - SumcheckId::IncClaimReduction, - ) - .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)) - } - } - } - } - pub fn new( preprocessing: &'a JoltVerifierPreprocessing, proof: JoltProof, @@ -1736,7 +1655,13 @@ impl< /// Stage 8: Dory batch opening verification. fn verify_stage8(&mut self) -> Result, ProofVerifyError> { - let opening_point = self.stage8_opening_point()?; + let native_main_vars = self.proof.trace_length.log_2() + self.one_hot_params.log_k_chunk; + let opening_point = compute_final_opening_point( + &self.opening_accumulator, + native_main_vars, + self.one_hot_params.log_k_chunk, + self.proof.dory_layout, + )?; // 1. Collect all (polynomial, claim) pairs let mut polynomial_claims = Vec::new(); From 90df745bdc84ba340d39db23a06b9b37c9139f32 Mon Sep 17 00:00:00 2001 From: Amirhossein Khajehpour Date: Mon, 25 May 2026 15:38:34 -0700 Subject: [PATCH 24/24] fix(zkvm): drop stale opening point imports Co-authored-by: Cursor --- jolt-core/src/zkvm/prover.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 0547fd2320..521b5036f1 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -38,8 +38,8 @@ use crate::{ }, multilinear_polynomial::MultilinearPolynomial, opening_proof::{ - compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, OpeningPoint, - ProverOpeningAccumulator, SumcheckId, BIG_ENDIAN, + compute_lagrange_factor, DoryOpeningState, OpeningAccumulator, + ProverOpeningAccumulator, SumcheckId, }, rlc_polynomial::{RLCStreamingData, TraceSource}, },