From e0587a7ec5842098db2858a453f63eafaa2235f3 Mon Sep 17 00:00:00 2001 From: Yogesh Shahi Date: Thu, 20 Aug 2026 21:01:15 +0530 Subject: [PATCH 01/11] feat(common): add linear_form_refs covector helper Collects a slice of concrete covectors into the `Vec<&dyn LinearForm>` shape whir's opening APIs take. The prover and verifier both build this vector by hand at several call sites; a shared helper removes the repetition and keeps the trait-object cast in one place. --- provekit/common/src/prefix_covector.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/provekit/common/src/prefix_covector.rs b/provekit/common/src/prefix_covector.rs index e48717254..cfdb79cd3 100644 --- a/provekit/common/src/prefix_covector.rs +++ b/provekit/common/src/prefix_covector.rs @@ -186,6 +186,12 @@ pub fn make_public_weight(x: F, num_public_inputs: usize, m: usize) -> PrefixCovector::new(public_weights, domain_size) } +/// Collect `&dyn LinearForm` references to a slice of covectors. +#[must_use] +pub fn linear_form_refs>(weights: &[W]) -> Vec<&dyn LinearForm> { + weights.iter().map(|w| w as &dyn LinearForm).collect() +} + /// Build [`PrefixCovector`] weights from alpha vectors, consuming the alphas. /// /// Each alpha vector is padded to a power-of-two length (min 2) and wrapped From e7d6aaea1246a51fd435a519f11687511afba901 Mon Sep 17 00:00:00 2001 From: Yogesh Shahi Date: Thu, 20 Aug 2026 21:01:22 +0530 Subject: [PATCH 02/11] chore(deps): bump whir to segmented-polynomial + zook rev 8804e80 Moves the `whir` pin from 33fbecf to 8804e80 (worldfnd/whir, PR #270). That rev replaces the `Messages` + separate-mask Reed-Solomon interface with segmented `Polynomials`, and adds the `zook` protocol module the following commits switch the witness commitment over to. Also enables serde_json's `float_roundtrip` feature: zook's `KneeWeight` and `ListSize` wrap `OrderedFloat`, and the default fast float parser is off by one ULP on read-back, which breaks scheme JSON roundtrips. Cargo.lock additionally picks up a `zeroize` edge for whir and a batch of incidental `windows-sys` version moves from re-resolving the graph; none of those affect the build on our targets. --- Cargo.lock | 24 +++++++++++------------- Cargo.toml | 4 +++- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24bddaf31..34c0f6e5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,7 +169,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -180,7 +180,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1515,7 +1515,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1732,7 +1732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2606,7 +2606,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3726,7 +3726,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4529,7 +4529,6 @@ dependencies = [ "postcard", "provekit-backend-bn254", "provekit-common", - "provekit-gnark", "provekit-r1cs-compiler", "provekit-spark", "rayon", @@ -5265,7 +5264,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5934,7 +5933,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6156,7 +6155,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6186,7 +6185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6862,7 +6861,6 @@ dependencies = [ "chrono", "provekit-backend-bn254", "provekit-common", - "provekit-gnark", "reqwest", "serde", "serde_json", @@ -7099,7 +7097,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2818cfc73..4a45bb778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,7 +144,9 @@ reqwest = "0.12.23" ruint = { version = "1.12.3", features = ["num-traits", "rand"] } seq-macro = "0.3.6" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +# `float_roundtrip`: zook configs carry f64s; the default fast parser is off +# by one ULP on read-back, breaking scheme JSON roundtrips. +serde_json = { version = "1.0", features = ["float_roundtrip"] } # Pinned: blake3 1.8.4+ depends on digest 0.11, but the `whir` crate's # blake3 engine is built against digest 0.10. Bumping breaks compilation. blake3 = "=1.8.3" From de0aee8573962ba80f5745726a9cdbdf88e72ecf Mon Sep 17 00:00:00 2001 From: Yogesh Shahi Date: Thu, 20 Aug 2026 21:01:35 +0530 Subject: [PATCH 03/11] feat(common): commit the witness with zook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `WhirR1CSScheme::whir_witness` becomes a zook `ProtocolConfig` derived from an explicit `SecuritySpec`/`TuningSpec` pair rather than a plain `whir::Config`. The tuning knobs move to named constants: `Mode::Standard` for the commitment mode, and `RateSchedule::Adaptive` at the default 0.5 knee, which plans per-round inverse rates against the prover-time / proof-size pareto knee instead of stepping the rate unboundedly. `ProtocolConfig::derive` is fallible, so `new_for_r1cs`, `new_from_dimensions` and `new_witness_config_for_size` now return `anyhow::Result`, and the two dimension invariants become `ensure!` instead of `assert!` — library code should not panic on caller error. The blinding commitment stays a plain `whir::Config`: the Spartan mask `g` is extension-valued and does not go through the base-field zook path. --- provekit/common/src/whir_r1cs.rs | 94 ++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/provekit/common/src/whir_r1cs.rs b/provekit/common/src/whir_r1cs.rs index 8c8c36612..8cb43283e 100644 --- a/provekit/common/src/whir_r1cs.rs +++ b/provekit/common/src/whir_r1cs.rs @@ -18,11 +18,40 @@ use { }, serde::{Deserialize, Serialize}, whir::{ - algebra::embedding::Identity, engines::EngineId, parameters::ProtocolParameters, - protocols::whir::Config as GenericWhirConfig, transcript, + algebra::embedding::Identity, + engines::EngineId, + parameters::ProtocolParameters, + protocols::{ + params::{ + DecodingRegime, FoldingFactor, KneeWeight, Mode, PowBudget, RateSchedule, + SecuritySpec, TuningSpec, + }, + whir::Config as GenericWhirConfig, + zook::ProtocolConfig as ZookConfig, + }, + transcript, }, }; +/// Zook mode for the witness commitment; [`Mode::ZeroKnowledge`] makes it +/// hiding. +const ZOOK_MODE: Mode = Mode::Standard; + +/// Adaptive per-round rate schedule at the balanced (0.5) prover-time / +/// proof-size knee. +const ZOOK_RATE_SCHEDULE: RateSchedule = RateSchedule::Adaptive { + knee_weight: KneeWeight::DEFAULT, +}; + +/// Target security level for the witness and blinding commitments, in bits. +const WHIR_SECURITY_BITS: u32 = 128; + +/// Per-slot proof-of-work budget for both commitments, in bits. +const WHIR_POW_BITS: u32 = 10; + +/// Starting log inverse rate shared by both commitments. +const WHIR_STARTING_LOG_INV_RATE: u32 = 2; + /// WHIR witness-domain floor: prover work is flat at or below `2^13` variables, /// so smaller commitments are padded up to this many variables. pub const MIN_WHIR_NUM_VARIABLES: usize = 13; @@ -82,11 +111,13 @@ impl R1csHash { /// /// # Zero-knowledge /// -/// The ZK posture is fixed, not configurable: /// - Sumcheck ZK is always on: the Spartan sumcheck rounds are masked by a /// blinding polynomial `g`, committed separately in `whir_blinding`. -/// - Witness ZK is off: the witness is committed non-hiding in `whir_witness`, -/// whose WHIR openings leak witness values. It will be enabled by zkWHIR 3.0. +/// - Witness ZK follows the [`Mode`] recorded in `whir_witness` (new schemes +/// use `ZOOK_MODE`): [`Mode::Standard`] is non-hiding, +/// [`Mode::ZeroKnowledge`] hiding. A hiding commitment alone is not full +/// proof ZK — the alpha, public-input, and challenge evaluations are still +/// sent in the clear. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(bound = "")] pub struct WhirR1CSScheme { @@ -97,10 +128,8 @@ pub struct WhirR1CSScheme { pub num_challenges: usize, pub challenge_offsets: Vec, pub has_public_inputs: bool, - /// Base-field witness commitment. Non-hiding — WHIR openings leak witness - /// values, so this provides sumcheck ZK only, not witness ZK. - /// TODO: make the witness commitment hiding for full witness ZK. - pub whir_witness: GenericWhirConfig, + /// Base-field witness commitment, via zook. + pub whir_witness: ZookConfig, /// Separate ext-field commitment to the Spartan blinding polynomial `g`; /// masking the ext-valued sumcheck rounds needs ext randomness, so `g` /// cannot ride on the base witness commitment. @@ -152,7 +181,7 @@ impl WhirR1CSScheme

{ challenge_offsets: Vec, has_public_inputs: bool, hash_config: HashConfig, - ) -> Self { + ) -> anyhow::Result { let mut scheme = Self::new_from_dimensions( r1cs.num_witnesses(), r1cs.num_constraints(), @@ -162,9 +191,9 @@ impl WhirR1CSScheme

{ challenge_offsets, has_public_inputs, hash_config, - ); + )?; scheme.r1cs_hash = r1cs.hash(); - scheme + Ok(scheme) } /// Build a scheme from raw dimensions, leaving `r1cs_hash` unset (the @@ -179,14 +208,13 @@ impl WhirR1CSScheme

{ challenge_offsets: Vec, has_public_inputs: bool, hash_config: HashConfig, - ) -> Self { - assert_eq!( - num_challenges, - challenge_offsets.len(), + ) -> anyhow::Result { + anyhow::ensure!( + num_challenges == challenge_offsets.len(), "num_challenges ({num_challenges}) != challenge_offsets.len() ({})", challenge_offsets.len() ); - assert!(w1_size <= num_witnesses, "w1_size exceeds total witnesses"); + anyhow::ensure!(w1_size <= num_witnesses, "w1_size exceeds total witnesses"); let w2_size = num_witnesses - w1_size; let m1_raw = next_power_of_two(w1_size); @@ -196,11 +224,11 @@ impl WhirR1CSScheme

{ let m = m1_raw.max(m2_raw).max(MIN_WHIR_NUM_VARIABLES); let m_0 = m0_raw.max(MIN_SUMCHECK_NUM_VARIABLES); - Self { + Ok(Self { m, m_0, a_num_terms: next_power_of_two(a_num_entries), - whir_witness: Self::new_witness_config_for_size(m, hash_config.engine_id()), + whir_witness: Self::new_witness_config_for_size(m, hash_config.engine_id())?, whir_blinding: Self::new_blinding_config_for_size(m_0, hash_config.engine_id()), w1_size, num_challenges, @@ -208,18 +236,36 @@ impl WhirR1CSScheme

{ has_public_inputs, r1cs_hash: R1csHash::UNSET, hash_config, - } + }) } - /// Build the non-ZK witness WHIR config: commits in `P`'s base field, opens - /// at extension-field points. + /// Build the zook witness config: commits in `P`'s base field, opens at + /// extension-field points. pub fn new_witness_config_for_size( num_variables: usize, hash_id: EngineId, - ) -> GenericWhirConfig { + ) -> anyhow::Result> { P::register(); let nv = num_variables.max(MIN_WHIR_NUM_VARIABLES); - GenericWhirConfig::::new(1 << nv, &whir_protocol_params(hash_id, 1)) + let security = SecuritySpec { + mode: ZOOK_MODE, + decoding_regime: DecodingRegime::Johnson, + target_security_bits: WHIR_SECURITY_BITS, + pow_budget: PowBudget::per_slot(WHIR_POW_BITS), + hash_id, + }; + let tuning = TuningSpec { + vector_size: 1 << nv, + starting_log_inv_rate: WHIR_STARTING_LOG_INV_RATE, + folding_factor: FoldingFactor::ConstantFromSecondRound { + initial: WHIR_INITIAL_FOLDING_FACTOR, + rest: WHIR_FOLDING_FACTOR, + }, + rate_schedule: ZOOK_RATE_SCHEDULE, + }; + ZookConfig::::derive(security, tuning).map_err(|e| { + anyhow::anyhow!("zook witness config derivation failed at {nv} variables: {e}") + }) } /// Build the WHIR config for the blinding polynomial `g`: its `4 * m_0` From 27d457d514b6f28e601aac9aa970cc2afce0c248 Mon Sep 17 00:00:00 2001 From: Yogesh Shahi Date: Thu, 20 Aug 2026 21:02:11 +0530 Subject: [PATCH 04/11] feat(prover): open the witness commitment through zook `WhirR1CSCommitment::witness` holds a zook `CommittedWitness`, and the padded witness length now comes from the config's `tuning().vector_size` rather than `initial_num_variables()`. zook's `commit` consumes the buffer but the prove stage still needs the message for the covector evaluations, so the padded witness is cloned; an upstream `commit(&Buffer)` or a message accessor would remove that. Both the single- and dual-commitment opening paths hand zook borrowed linear forms via `linear_form_refs` instead of boxing each covector, and the SPARK covector snapshots go away with them: zook's `prove` returns `()`, so there is no final evaluation point to re-evaluate the alpha weights at. --- provekit/prover/src/whir_r1cs.rs | 207 +++++++------------------------ 1 file changed, 43 insertions(+), 164 deletions(-) diff --git a/provekit/prover/src/whir_r1cs.rs b/provekit/prover/src/whir_r1cs.rs index 24adf5308..9e3c2f60e 100644 --- a/provekit/prover/src/whir_r1cs.rs +++ b/provekit/prover/src/whir_r1cs.rs @@ -9,8 +9,8 @@ use { provekit_common::{ prefix_covector::{ build_prefix_covectors, compute_alpha_evals, compute_challenge_eval, - compute_public_eval, expand_powers, make_challenge_weight, make_public_weight, - OffsetCovector, + compute_public_eval, expand_powers, linear_form_refs, make_challenge_weight, + make_public_weight, OffsetCovector, }, utils::{ pad_to_power_of_two, @@ -31,7 +31,7 @@ use { mixed_dot, }, buffer::{Buffer, BufferOps}, - protocols::whir::Witness as WhirWitness, + protocols::{whir::Witness as WhirWitness, zook::CommittedWitness as ZookCommittedWitness}, transcript::{Codec, DuplexSpongeInterface, ProverState, VerifierMessage}, }, }; @@ -48,7 +48,7 @@ pub struct BlindingState { } pub struct WhirR1CSCommitment { - pub witness: WhirWitness, P::Embedding>, + pub witness: ZookCommittedWitness, pub polynomial: Buffer>, pub blinding: Option>, } @@ -135,18 +135,22 @@ where "R1CS constraints exceed scheme capacity" ); - let num_vars = self.whir_witness.initial_num_variables(); - let target_len = 1usize << num_vars; + let target_len = self.whir_witness.tuning().vector_size; let mut padded_witness = pad_to_power_of_two(witness); if padded_witness.len() < target_len { padded_witness.resize(target_len, >::zero()); } + ensure!( + padded_witness.len() == target_len, + "witness length exceeds the zook commitment size; scheme dimensions and witness \ + config are inconsistent" + ); - // Commit the base-field witness directly (non-hiding — openings leak - // witness values; see the `whir_witness` field docs). + // zook's `commit` consumes the buffer; the prove stage still needs + // the message for the covector evaluations, so pass a clone. let padded_witness = Buffer::from(padded_witness); - let witness_commitment = self.whir_witness.commit(merlin, &[&padded_witness]); + let witness_commitment = self.whir_witness.commit(merlin, padded_witness.clone()); // Commit the Spartan sumcheck blinding `g` separately, natively in the // extension field. Transcript order: this commitment is absorbed @@ -352,7 +356,7 @@ where // products through this embedding (a no-op under `Identity`). let embedding = ::default(); - let spark_queries: Option> = if is_single { + if is_single { // Single commitment path let commitment = commitments .into_iter() @@ -369,16 +373,6 @@ where merlin.prover_message(eval); } - // Snapshot the three alpha covectors (A, B, C) before the public - // weight is inserted; they are re-evaluated at the final WHIR point - // to form the SPARK query. - let spark_weights: Option>, usize)>> = spark_row.as_ref().map(|_| { - weights - .iter() - .map(|w| (w.vector().to_vec(), w.size())) - .collect() - }); - if public_inputs_len > 0 { let public_eval = compute_public_weight_evaluation( &embedding, @@ -391,32 +385,13 @@ where let evaluations = compute_evaluations(&embedding, &weights, &commitment.polynomial); - let boxed_weights: Vec>>> = weights - .into_iter() - .map(|w| Box::new(w) as Box>>) - .collect(); + let WhirR1CSCommitment { witness, .. } = commitment; - let final_claim = scheme.whir_witness.prove( - &mut merlin, - &[&commitment.polynomial], - vec![&commitment.witness], - boxed_weights, - Buffer::from(evaluations), - ); + let form_refs = linear_form_refs(&weights); - spark_row.zip(spark_weights).map(|(row, spark_weights)| { - let [claimed_a, claimed_b, claimed_c] = - evaluate_spark_weights(spark_weights, &final_claim.evaluation_point); - SparkQueryData { - row, - queries: vec![SparkColQueryData { - col: final_claim.evaluation_point, - claimed_a, - claimed_b, - claimed_c, - }], - } - }) + scheme + .whir_witness + .prove(&mut merlin, witness, &form_refs, &evaluations); } else { // Dual commitment path let mut commitments = commitments.into_iter(); @@ -470,23 +445,11 @@ where None }; - let WhirR1CSCommitment { - witness: w1, - polynomial: p1, - .. - } = c1; - let (final_claim_1, claimed_1) = { - let mut weights = build_prefix_covectors(scheme.m, alphas_1); + let WhirR1CSCommitment { witness: w1, .. } = c1; + let WhirR1CSCommitment { witness: w2, .. } = c2; - // Snapshot the three alpha covectors (A, B, C) before the public - // weight is inserted; they are re-evaluated at the final WHIR - // point to form the SPARK query. - let spark_weights: Option>, usize)>> = spark_row.as_ref().map(|_| { - weights - .iter() - .map(|w| (w.vector().to_vec(), w.size())) - .collect() - }); + { + let mut weights = build_prefix_covectors(scheme.m, alphas_1); let mut evaluations: Vec> = Vec::new(); if let Some(pe) = public_1 { @@ -495,98 +458,33 @@ where } evaluations.extend_from_slice(&evals_1); - let boxed_weights: Vec>>> = weights - .into_iter() - .map(|w| Box::new(w) as Box>>) - .collect(); - - let final_claim = scheme.whir_witness.prove( - &mut merlin, - &[&p1], - vec![&w1], - boxed_weights, - Buffer::from(evaluations), - ); + let form_refs = linear_form_refs(&weights); - let claimed = - spark_weights.map(|sw| evaluate_spark_weights(sw, &final_claim.evaluation_point)); - (final_claim, claimed) - }; - drop(p1); - drop(w1); - - let WhirR1CSCommitment { - witness: w2, - polynomial: p2, - .. - } = c2; - let (final_claim_2, claimed_2) = { - let weights = build_prefix_covectors(scheme.m, alphas_2); + scheme + .whir_witness + .prove(&mut merlin, w1, &form_refs, &evaluations); + } - // The first three weights are the alpha covectors (A, B, C); the - // challenge weight, if any, is appended after them. - let spark_weights: Option>, usize)>> = spark_row.as_ref().map(|_| { - weights - .iter() - .map(|w| (w.vector().to_vec(), w.size())) - .collect() - }); + { + let weights = build_prefix_covectors(scheme.m, alphas_2); let mut evaluations: Vec> = evals_2; - let mut boxed_weights: Vec>>> = weights - .into_iter() - .map(|w| Box::new(w) as Box>>) - .collect(); - - if let Some(ce) = challenge_eval { - let cw = make_challenge_weight(x, &scheme.challenge_offsets, scheme.m); + let challenge_covector = challenge_eval.map(|ce| { evaluations.push(ce); - boxed_weights.push(Box::new(cw)); - } - - let final_claim = scheme.whir_witness.prove( - &mut merlin, - &[&p2], - vec![&w2], - boxed_weights, - Buffer::from(evaluations), - ); + make_challenge_weight(x, &scheme.challenge_offsets, scheme.m) + }); - let claimed = - spark_weights.map(|sw| evaluate_spark_weights(sw, &final_claim.evaluation_point)); - (final_claim, claimed) - }; + let mut form_refs = linear_form_refs(&weights); + if let Some(ref cw) = challenge_covector { + form_refs.push(cw as &dyn LinearForm>); + } - // The SPARK column axis spans w1 | w2: prefix the per-commitment WHIR - // points with a selector coordinate (0 selects w1, 1 selects w2). - spark_row - .zip(claimed_1.zip(claimed_2)) - .map(|(row, ([a1, b1, c1], [a2, b2, c2]))| { - let mut col_1 = final_claim_1.evaluation_point; - col_1.insert(0, >::zero()); - let query_1 = SparkColQueryData { - col: col_1, - claimed_a: a1, - claimed_b: b1, - claimed_c: c1, - }; - - let mut col_2 = final_claim_2.evaluation_point; - col_2.insert(0, >::one()); - let query_2 = SparkColQueryData { - col: col_2, - claimed_a: a2, - claimed_b: b2, - claimed_c: c2, - }; - - SparkQueryData { - row, - queries: vec![query_1, query_2], - } - }) - }; + scheme + .whir_witness + .prove(&mut merlin, w2, &form_refs, &evaluations); + } + } // Open the ext blinding commitment: prove `blinding_eval` is the evaluation // of the committed blinding vector (g flattened at offset 0) against the @@ -612,29 +510,10 @@ where #[cfg(debug_assertions)] pattern: proof.pattern, }, - spark_queries, + None, )) } -/// Re-evaluate snapshotted alpha covectors (as `(prefix, domain_size)` pairs) -/// at the final WHIR evaluation point, yielding the claimed A, B, C matrix -/// evaluations for a SPARK query. -fn evaluate_spark_weights( - spark_weights: Vec<(Vec, usize)>, - evaluation_point: &[F], -) -> [F; 3] { - let evals: Vec = spark_weights - .into_iter() - .take(3) - .map(|(vector, domain_size)| { - PrefixCovector::new(vector, domain_size).mle_evaluate(evaluation_point) - }) - .collect(); - evals - .try_into() - .unwrap_or_else(|_| panic!("exactly 3 alpha-weight evaluations")) -} - pub fn compute_blinding_coefficients_for_round( g_univariates: &[[F; 4]], compute_for: usize, From 9ea3a8dd492ad387b31554007a0dea08d83fd055 Mon Sep 17 00:00:00 2001 From: Yogesh Shahi Date: Thu, 20 Aug 2026 21:02:15 +0530 Subject: [PATCH 05/11] feat(verifier): verify the witness commitment through zook zook's `verify` takes the linear forms and their claimed evaluations up front and returns a `FinalClaim` whose `verify` now takes a slice rather than an iterator. Its sumcheck also draws extension-field randomness, so the impl picks up a `Standard: Distribution>` bound. All three weight-reference sites switch to `linear_form_refs`. --- provekit/verifier/src/whir_r1cs.rs | 41 ++++++++++++++---------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/provekit/verifier/src/whir_r1cs.rs b/provekit/verifier/src/whir_r1cs.rs index c4ea7ddac..0ff7b8e6a 100644 --- a/provekit/verifier/src/whir_r1cs.rs +++ b/provekit/verifier/src/whir_r1cs.rs @@ -1,11 +1,14 @@ use { anyhow::{ensure, Context, Result}, ark_ff::Field, - ark_std::One, + ark_std::{ + rand::distributions::{Distribution, Standard}, + One, + }, provekit_common::{ prefix_covector::{ - build_prefix_covectors, expand_powers, make_challenge_weight, make_public_weight, - OffsetCovector, + build_prefix_covectors, expand_powers, linear_form_refs, make_challenge_weight, + make_public_weight, OffsetCovector, }, utils::sumcheck::{ calculate_eq, eval_cubic_poly, multiply_transposed_by_eq_alpha, transpose_r1cs_matrices, @@ -36,7 +39,10 @@ pub trait WhirR1CSVerifier { ) -> Result<()>; } -impl WhirR1CSVerifier

for WhirR1CSScheme

{ +impl WhirR1CSVerifier

for WhirR1CSScheme

+where + Standard: Distribution>, +{ #[instrument(skip_all)] fn verify( &self, @@ -184,30 +190,24 @@ impl WhirR1CSVerifier

for WhirR1CSScheme

{ None }; - let weight_refs_1: Vec<&dyn LinearForm>> = weights_1 - .iter() - .map(|w| w as &dyn LinearForm>) - .collect(); + let weight_refs_1 = linear_form_refs(&weights_1); let fc_1 = self .whir_witness - .verify(&mut arthur, &[&commitment_1], &evaluations_1) + .verify(&mut arthur, commitment_1, &weight_refs_1, &evaluations_1) .map_err(|_| anyhow::anyhow!("WHIR verification failed for c1"))?; - fc_1.verify(weight_refs_1.iter().copied()) + fc_1.verify(&weight_refs_1) .map_err(|_| anyhow::anyhow!("WHIR final-claim check failed for c1"))?; - let mut weight_refs_2: Vec<&dyn LinearForm>> = weights_2 - .iter() - .map(|w| w as &dyn LinearForm>) - .collect(); + let mut weight_refs_2 = linear_form_refs(&weights_2); if let Some(ref cw) = challenge_covector { weight_refs_2.push(cw as &dyn LinearForm>); } let fc_2 = self .whir_witness - .verify(&mut arthur, &[&commitment_2], &evaluations_2) + .verify(&mut arthur, commitment_2, &weight_refs_2, &evaluations_2) .map_err(|_| anyhow::anyhow!("WHIR verification failed for c2"))?; - fc_2.verify(weight_refs_2.iter().copied()) + fc_2.verify(&weight_refs_2) .map_err(|_| anyhow::anyhow!("WHIR final-claim check failed for c2"))?; ( @@ -241,16 +241,13 @@ impl WhirR1CSVerifier

for WhirR1CSScheme

{ evals.to_vec() }; - let weight_refs: Vec<&dyn LinearForm>> = weights - .iter() - .map(|w| w as &dyn LinearForm>) - .collect(); + let weight_refs = linear_form_refs(&weights); let fc = self .whir_witness - .verify(&mut arthur, &[&commitment_1], &evaluations) + .verify(&mut arthur, commitment_1, &weight_refs, &evaluations) .map_err(|_| anyhow::anyhow!("WHIR verification failed"))?; - fc.verify(weight_refs.iter().copied()) + fc.verify(&weight_refs) .map_err(|_| anyhow::anyhow!("WHIR final-claim check failed"))?; (evals[0], evals[1], evals[2]) From 35de76f7625d23caa5d4a431d1693dbfb134ec9c Mon Sep 17 00:00:00 2001 From: Yogesh Shahi Date: Thu, 20 Aug 2026 21:02:21 +0530 Subject: [PATCH 06/11] refactor: reject SPARK query production under zook A SPARK query needs the final WHIR evaluation point to re-evaluate the A, B, C covectors at. zook's `prove` returns `()` and keeps that point internal, so the query cannot be built. Rather than silently emitting nothing, both the generic and the Mavros prove paths now fail fast with an explanatory error, and `ProveFromAlphasCtx::spark_row` is documented as reserved. Restoring this needs a whir-side change that exposes the prover's final claim; the call sites are marked with a TODO pointing at it. --- provekit/backend/bn254/src/mavros_prove.rs | 4 ++++ provekit/prover/src/whir_r1cs.rs | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/provekit/backend/bn254/src/mavros_prove.rs b/provekit/backend/bn254/src/mavros_prove.rs index 1f3acdcc7..a678d24a4 100644 --- a/provekit/backend/bn254/src/mavros_prove.rs +++ b/provekit/backend/bn254/src/mavros_prove.rs @@ -120,6 +120,10 @@ fn prove_mavros_inner( produce_spark_query: bool, ) -> Result<(WhirR1CSProof, Option>)> { ensure!(!commitments.is_empty(), "Need at least one commitment"); + ensure!( + !produce_spark_query, + "SPARK queries are not supported with the zook witness commitment" + ); let blinding = commitments[0] .blinding diff --git a/provekit/prover/src/whir_r1cs.rs b/provekit/prover/src/whir_r1cs.rs index 9e3c2f60e..5511413b1 100644 --- a/provekit/prover/src/whir_r1cs.rs +++ b/provekit/prover/src/whir_r1cs.rs @@ -237,6 +237,10 @@ where Standard: Distribution> + Distribution>, { ensure!(!commitments.is_empty(), "Need at least one commitment"); + ensure!( + !produce_spark_query, + "SPARK queries are not supported with the zook witness commitment" + ); let (a, b, c) = calculate_witness_bounds(&r1cs, &full_witness); drop(full_witness); @@ -286,8 +290,8 @@ pub struct ProveFromAlphasCtx { pub blinding_eval: Ext

, pub blinding_weights: Vec>, pub commitments: Vec>, - /// When set, SPARK query data is produced with this Spartan sumcheck - /// point as the shared row axis. + /// Reserved SPARK row axis; must be `None` — SPARK is unsupported with + /// the zook witness commitment. pub spark_row: Option>>, } @@ -337,6 +341,13 @@ where spark_row, } = ctx; + // TODO: restore SPARK once zook's `prove` exposes its final evaluation + // point. + ensure!( + spark_row.is_none(), + "SPARK queries are not supported with the zook witness commitment" + ); + let public_inputs_hash = P::hash_public_inputs(scheme.hash_config, &public_inputs.0); let public_inputs_len = public_inputs.len(); From 1b978188c90877fca665c255687e88148bbe652d Mon Sep 17 00:00:00 2001 From: Yogesh Shahi Date: Thu, 20 Aug 2026 21:02:27 +0530 Subject: [PATCH 07/11] refactor(r1cs-compiler): propagate fallible scheme construction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scheme construction can now fail while deriving the zook config, so the Noir and Mavros compilers propagate the error and `MavrosSchemeBuilder` returns `anyhow::Result`. `assert_configs_secure` no longer reads a security level off the witness config — zook owns that calculation. It runs zook's own `validate` and asserts the target stays at 128 bits. The blinding half still checks `security_level` directly, since it remains a plain whir config. --- .../r1cs-compiler/src/noir_proof_scheme.rs | 4 +- provekit/r1cs-compiler/src/whir_r1cs.rs | 38 +++++++++++++------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/provekit/r1cs-compiler/src/noir_proof_scheme.rs b/provekit/r1cs-compiler/src/noir_proof_scheme.rs index 6c802dedb..02f767fee 100644 --- a/provekit/r1cs-compiler/src/noir_proof_scheme.rs +++ b/provekit/r1cs-compiler/src/noir_proof_scheme.rs @@ -165,7 +165,7 @@ impl NoirCompiler { challenge_offsets, has_public_inputs, hash_config, - ); + )?; Ok(NoirProofScheme::Noir(NoirSchemeData { program: program.bytecode, @@ -237,7 +237,7 @@ impl MavrosCompiler { challenge_offsets, num_public_inputs > 0, hash_config, - ); + )?; whir_for_witness.r1cs_hash = r1cs.hash(); Ok(NoirProofScheme::Mavros(MavrosSchemeData { diff --git a/provekit/r1cs-compiler/src/whir_r1cs.rs b/provekit/r1cs-compiler/src/whir_r1cs.rs index e49b833f3..d8d78600d 100644 --- a/provekit/r1cs-compiler/src/whir_r1cs.rs +++ b/provekit/r1cs-compiler/src/whir_r1cs.rs @@ -22,7 +22,9 @@ pub trait MavrosSchemeBuilder { challenge_offsets: Vec, has_public_inputs: bool, hash_config: HashConfig, - ) -> Self; + ) -> anyhow::Result + where + Self: Sized; } impl MavrosSchemeBuilder for WhirR1CSScheme { @@ -33,7 +35,9 @@ impl MavrosSchemeBuilder for WhirR1CSScheme { challenge_offsets: Vec, has_public_inputs: bool, hash_config: HashConfig, - ) -> Self { + ) -> anyhow::Result { + provekit_backend_bn254::register(); + let num_witnesses = r1cs.witness_layout.size(); let num_constraints = r1cs.constraints.len(); let a_num_entries: usize = r1cs.constraints.iter().map(|c| c.a.len()).sum(); @@ -82,7 +86,8 @@ mod tests { vec![], false, HashConfig::Sha256, - ); + ) + .expect("scheme from dimensions"); assert_eq!(from_dimensions.m, expected_m); assert_eq!(from_dimensions.m_0, expected_m_0); assert_eq!(from_dimensions.w1_size, w1_size); @@ -95,7 +100,8 @@ mod tests { vec![], false, HashConfig::Sha256, - ); + ) + .expect("scheme from r1cs"); assert_eq!(from_r1cs.m, expected_m); assert_eq!(from_r1cs.m_0, expected_m_0); assert_eq!(from_r1cs.w1_size, w1_size); @@ -105,17 +111,24 @@ mod tests { } /// Assert both WHIR commitments reach 128-bit security for field `P`. - fn assert_configs_secure(size: usize) { let field = std::any::type_name::

(); - let witness = WhirR1CSScheme::

::new_witness_config_for_size(size, whir::hash::SHA2); + let witness = WhirR1CSScheme::

::new_witness_config_for_size(size, whir::hash::SHA2) + .expect("witness config derivation"); let blinding = WhirR1CSScheme::

::new_blinding_config_for_size(size, whir::hash::SHA2); - let sec_witness = witness.security_level(witness.initial_committer.num_vectors(), 1); - let sec_blinding = blinding.security_level(blinding.initial_committer.num_vectors(), 1); - assert!( - sec_witness >= 128.0, - "Witness commitment security {sec_witness:.2} < 128 bits at size {size} for {field}" + + // zook's `validate` checks the plan against its own `SecuritySpec`. + witness.validate().unwrap_or_else(|e| { + panic!("Witness commitment fails zook validation at size {size} for {field}: {e:?}") + }); + assert_eq!( + witness.security().target_security_bits, + 128, + "Witness commitment target must stay at 128 bits at size {size} for {field}" ); + + // The blinding commitment is still a plain WHIR config. + let sec_blinding = blinding.security_level(blinding.initial_committer.num_vectors(), 1); assert!( sec_blinding >= 128.0, "Blinding commitment security {sec_blinding:.2} < 128 bits at size {size} for {field}" @@ -145,7 +158,8 @@ mod tests { vec![0, 1], false, HashConfig::Sha256, - ); + ) + .expect("mavros-sized scheme"); assert_eq!(scheme.m, 19); } From 603ae9f587f8ba3098432092a5adb76f09bd8666 Mon Sep 17 00:00:00 2001 From: Yogesh Shahi Date: Thu, 20 Aug 2026 21:02:28 +0530 Subject: [PATCH 08/11] refactor(fixtures): propagate fallible scheme construction `prove_setup` returns `Result>` and the harness builders propagate the scheme-construction error. --- tooling/provekit-fixtures/src/harness.rs | 14 +++++++------- tooling/provekit-fixtures/tests/profile.rs | 5 +++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tooling/provekit-fixtures/src/harness.rs b/tooling/provekit-fixtures/src/harness.rs index 8aec25db9..e909424cd 100644 --- a/tooling/provekit-fixtures/src/harness.rs +++ b/tooling/provekit-fixtures/src/harness.rs @@ -48,7 +48,7 @@ where Vec::new(), !public_inputs.is_empty(), HASH, - ); + )?; let instance = public_inputs.hash_bytes::

(HASH); let ds = scheme.create_domain_separator().instance(&instance); @@ -182,7 +182,7 @@ where challenge_offsets.clone(), !public.is_empty(), hash, - ); + )?; let num_witnesses = r1cs.num_witnesses(); let num_constraints = r1cs.num_constraints(); @@ -265,7 +265,7 @@ where challenge_offsets, false, HASH, - ); + )?; let num_witnesses = r1cs.num_witnesses(); let num_constraints = r1cs.num_constraints(); @@ -302,18 +302,18 @@ pub struct ProveInputs { /// Build the scheme and owned witness copies for a single-commit prove. Kept /// separate from [`time_prove_core`] so a profiler can install its subscriber /// between setup and the timed region (only the latter is captured). -pub fn prove_setup

(r1cs: &R1CS>, witness: &[Base

]) -> ProveInputs

+pub fn prove_setup

(r1cs: &R1CS>, witness: &[Base

]) -> Result> where P: FieldHash, Standard: Distribution> + Distribution>, { - let scheme = WhirR1CSScheme::

::new_for_r1cs(r1cs, witness.len(), 0, Vec::new(), true, HASH); - ProveInputs { + let scheme = WhirR1CSScheme::

::new_for_r1cs(r1cs, witness.len(), 0, Vec::new(), true, HASH)?; + Ok(ProveInputs { scheme, r1cs: r1cs.clone(), witness_commit: witness.to_vec(), witness_prove: witness.to_vec(), - } + }) } /// Time the single-commit proving core — `commit` + `prove_noir` — from diff --git a/tooling/provekit-fixtures/tests/profile.rs b/tooling/provekit-fixtures/tests/profile.rs index 70f333452..1c7d61c2b 100644 --- a/tooling/provekit-fixtures/tests/profile.rs +++ b/tooling/provekit-fixtures/tests/profile.rs @@ -40,7 +40,7 @@ where let public_inputs = PublicInputs::from_vec(vec![w[1]]); // Only commit + prove_noir are timed (setup excluded above). - let inp = prove_setup::

(&r1cs, &w); + let inp = prove_setup::

(&r1cs, &w).expect("prove setup"); let (prove_t, proof, scheme) = time_prove_core::

(inp, &public_inputs).expect("prove"); let t = Instant::now(); @@ -141,7 +141,8 @@ fn flamegraph_2pow20_bf() { let depth = (1usize << log_size) - 2; let (r1cs, w) = squaring_chain::>(2, depth); let public_inputs = PublicInputs::from_vec(vec![w[1]]); - let inp = prove_setup::(&r1cs, &w); + let inp = prove_setup::(&r1cs, &w) + .expect("prove setup"); // Global subscriber required: prove() uses rayon, and only the global // default captures spans created on rayon worker threads. From d8ddb6b8e8151bae4b681dd7b365a1bef3a769e1 Mon Sep 17 00:00:00 2001 From: Yogesh Shahi Date: Thu, 20 Aug 2026 21:02:34 +0530 Subject: [PATCH 09/11] chore(tooling): disable gnark parameter export under zook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gnark exporter serialises a plain whir `Config`; it has no way to describe zook's `ProtocolConfig`, and the Go recursive verifier would have to be updated in lockstep to consume one. Until that paired change lands, both entry points — the `generate-gnark-inputs` CLI command and the verifier server's parameter preparation — fail with an error that says so, and `provekit-gnark` drops out of the two crates' dependencies so `cargo machete` stays quiet. Also drops two `return` statements from the verifier server's cancellation branches; they are the last statement in their `select!` arm and clippy flags them. --- tooling/cli/Cargo.toml | 1 - tooling/cli/src/cmd/generate_gnark_inputs.rs | 54 +++---------------- tooling/verifier-server/Cargo.toml | 1 - .../src/services/verification.rs | 51 +++++------------- 4 files changed, 21 insertions(+), 86 deletions(-) diff --git a/tooling/cli/Cargo.toml b/tooling/cli/Cargo.toml index aa95e19d5..f5a75094b 100644 --- a/tooling/cli/Cargo.toml +++ b/tooling/cli/Cargo.toml @@ -13,7 +13,6 @@ repository.workspace = true mavros-artifacts.workspace = true provekit-common.workspace = true provekit-backend-bn254 = { workspace = true, features = ["witness-generation", "parallel"] } -provekit-gnark.workspace = true provekit-r1cs-compiler.workspace = true provekit-spark.workspace = true diff --git a/tooling/cli/src/cmd/generate_gnark_inputs.rs b/tooling/cli/src/cmd/generate_gnark_inputs.rs index fd04de451..31bcd4041 100644 --- a/tooling/cli/src/cmd/generate_gnark_inputs.rs +++ b/tooling/cli/src/cmd/generate_gnark_inputs.rs @@ -1,13 +1,4 @@ -use { - crate::Command, - anyhow::{Context, Result}, - argh::FromArgs, - provekit_backend_bn254::{Bn254Field, ProvekitProof, Verifier}, - provekit_common::file::read, - provekit_gnark::write_gnark_parameters_to_file, - std::{fs::File, io::Write, path::PathBuf}, - tracing::{info, instrument}, -}; +use {crate::Command, anyhow::Result, argh::FromArgs, std::path::PathBuf, tracing::instrument}; /// Generate input compatible with gnark. #[derive(FromArgs, PartialEq, Eq, Debug)] @@ -37,43 +28,12 @@ pub struct Args { impl Command for Args { #[instrument(skip_all)] fn run(&self) -> Result<()> { - let verifier: Verifier = - read(&self.verifier_path).context("while reading Verifier data")?; - let (constraints, witnesses) = ( - verifier.r1cs.num_constraints(), - verifier.r1cs.num_witnesses(), + // The gnark exporter cannot describe zook's `ProtocolConfig`; the Go + // recursive verifier needs a paired update first. + anyhow::bail!( + "cannot write gnark parameters to {}: gnark parameter export is not supported with \ + the zook witness commitment; the Go recursive verifier needs a paired update", + self.params_for_recursive_verifier ); - info!(constraints, witnesses, "Read verifier data"); - - // Read the proof - let proof: ProvekitProof = - read(&self.proof_path).context("while reading proof")?; - - let wfw = verifier - .whir_for_witness - .as_ref() - .context("verifier is missing whir_for_witness config")?; - - write_gnark_parameters_to_file( - &verifier.whir_for_witness.clone().unwrap(), - &wfw.whir_witness, - &wfw.whir_blinding, - &proof.whir_r1cs_proof, - wfw.m_0, - wfw.m, - wfw.a_num_terms, - wfw.num_challenges, - wfw.w1_size, - &proof.public_inputs, - &self.params_for_recursive_verifier, - ); - - let json = - serde_json::to_string(&verifier.r1cs).context("while serializing R1CS to JSON")?; - let mut file = File::create(&self.r1cs_path).context("while creating R1CS file")?; - file.write_all(json.as_bytes()) - .context("while writing R1CS file")?; - - Ok(()) } } diff --git a/tooling/verifier-server/Cargo.toml b/tooling/verifier-server/Cargo.toml index 66435cb27..6fc5cc3dd 100644 --- a/tooling/verifier-server/Cargo.toml +++ b/tooling/verifier-server/Cargo.toml @@ -12,7 +12,6 @@ repository.workspace = true # Workspace crates provekit-common.workspace = true provekit-backend-bn254.workspace = true -provekit-gnark.workspace = true # 3rd party anyhow.workspace = true diff --git a/tooling/verifier-server/src/services/verification.rs b/tooling/verifier-server/src/services/verification.rs index f1b7a6736..f4768261f 100644 --- a/tooling/verifier-server/src/services/verification.rs +++ b/tooling/verifier-server/src/services/verification.rs @@ -10,7 +10,6 @@ use { services::artifact::ArtifactPaths, }, provekit_backend_bn254::{Bn254Field, ProvekitProof, Verifier}, - provekit_gnark::write_gnark_parameters_to_file, std::time::Instant, tokio_util::sync::CancellationToken, tracing::{info, warn}, @@ -35,6 +34,9 @@ impl VerificationService { } /// Perform complete proof verification + /// + /// Currently always fails: gnark parameters cannot be exported with the + /// zook witness commitment (see `Self::prepare_gnark_parameters`). pub async fn verify_proof( &self, request: &VerifyRequest, @@ -66,43 +68,20 @@ impl VerificationService { } /// Prepare gnark parameters file for verification + /// + /// The gnark exporter cannot describe zook's `ProtocolConfig`; the Go + /// recursive verifier needs a paired update before this can be restored. fn prepare_gnark_parameters( &self, - proof: &ProvekitProof, - verifier: &Verifier, - paths: &ArtifactPaths, + _proof: &ProvekitProof, + _verifier: &Verifier, + _paths: &ArtifactPaths, ) -> AppResult<()> { - info!( - gnark_params_file = %paths.gnark_params_file.display(), - "Preparing gnark parameters" - ); - - let gnark_params_path = paths - .gnark_params_file - .to_str() - .ok_or_else(|| AppError::Internal("Invalid gnark params path".to_string()))?; - - let whir_scheme = verifier - .whir_for_witness - .as_ref() - .ok_or_else(|| AppError::Internal("WHIR scheme not found in verifier".to_string()))?; - - write_gnark_parameters_to_file( - whir_scheme, - &whir_scheme.whir_witness, - &whir_scheme.whir_blinding, - &proof.whir_r1cs_proof, - whir_scheme.m_0, - whir_scheme.m, - whir_scheme.a_num_terms, - whir_scheme.num_challenges, - whir_scheme.w1_size, - &proof.public_inputs, - gnark_params_path, - ); - - info!("Gnark parameters prepared successfully"); - Ok(()) + Err(AppError::Internal( + "gnark parameter export is not supported with the zook witness commitment; the Go \ + recursive verifier needs a paired update" + .to_string(), + )) } /// Execute the external verifier binary @@ -181,7 +160,6 @@ impl VerificationService { tokio::select! { _ = token.cancelled() => { info!("Stdout logging task cancelled"); - return; } _ = async { while let Ok(Some(line)) = lines.next_line().await { @@ -206,7 +184,6 @@ impl VerificationService { tokio::select! { _ = token.cancelled() => { warn!("Stderr logging task cancelled"); - return; } _ = async { while let Ok(Some(line)) = lines.next_line().await { From 8a5f844370f3843c627f69b94fa577dba5ddb53b Mon Sep 17 00:00:00 2001 From: Yogesh Shahi Date: Thu, 27 Aug 2026 21:04:54 +0530 Subject: [PATCH 10/11] chore(deps): bump whir to zook prover-claim rev 78e9460 zook's `prove` now returns a `ProverClaim` carrying the final evaluation point and RLC coefficients (whir #274), which is what SPARK query production needs. --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34c0f6e5a..08375811c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7047,7 +7047,7 @@ dependencies = [ [[package]] name = "whir" version = "0.1.0" -source = "git+https://github.com/worldfnd/whir.git?rev=8804e80e8e890d01bb585f2bd5e5b564ac0fd80d#8804e80e8e890d01bb585f2bd5e5b564ac0fd80d" +source = "git+https://github.com/worldfnd/whir.git?rev=78e9460420da0954768feb1e52f581a0529c2124#78e9460420da0954768feb1e52f581a0529c2124" dependencies = [ "ark-ff", "ark-serialize", diff --git a/Cargo.toml b/Cargo.toml index 4a45bb778..44073997b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -214,4 +214,4 @@ spongefish = { git = "https://github.com/arkworks-rs/spongefish", features = [ "sha2", ], rev = "fcc277f8a857fdeeadd7cca92ab08de63b1ff1a1" } spongefish-pow = { git = "https://github.com/arkworks-rs/spongefish", rev = "fcc277f8a857fdeeadd7cca92ab08de63b1ff1a1" } -whir = { git = "https://github.com/worldfnd/whir.git", rev = "8804e80e8e890d01bb585f2bd5e5b564ac0fd80d", features = ["tracing", "rs_in_order"] } +whir = { git = "https://github.com/worldfnd/whir.git", rev = "78e9460420da0954768feb1e52f581a0529c2124", features = ["tracing", "rs_in_order"] } From 4b58922bb571cac534cabfc1217c8252a7cf0714 Mon Sep 17 00:00:00 2001 From: Yogesh Shahi Date: Thu, 27 Aug 2026 21:07:34 +0530 Subject: [PATCH 11/11] feat(prover): restore SPARK query production under zook whir #274 makes zook's `prove` return a `ProverClaim` with the final evaluation point, so the A, B, C covectors can again be re-evaluated at the final WHIR point to form the SPARK queries. Restores the same query assembly as the pre-zook prover (selector-prefixed columns in the dual- commitment path) and drops the fail-fast guards from the generic and Mavros prove paths. --- provekit/backend/bn254/src/mavros_prove.rs | 4 - provekit/prover/src/whir_r1cs.rs | 134 +++++++++++++++++---- 2 files changed, 111 insertions(+), 27 deletions(-) diff --git a/provekit/backend/bn254/src/mavros_prove.rs b/provekit/backend/bn254/src/mavros_prove.rs index a678d24a4..1f3acdcc7 100644 --- a/provekit/backend/bn254/src/mavros_prove.rs +++ b/provekit/backend/bn254/src/mavros_prove.rs @@ -120,10 +120,6 @@ fn prove_mavros_inner( produce_spark_query: bool, ) -> Result<(WhirR1CSProof, Option>)> { ensure!(!commitments.is_empty(), "Need at least one commitment"); - ensure!( - !produce_spark_query, - "SPARK queries are not supported with the zook witness commitment" - ); let blinding = commitments[0] .blinding diff --git a/provekit/prover/src/whir_r1cs.rs b/provekit/prover/src/whir_r1cs.rs index 5511413b1..bab35462f 100644 --- a/provekit/prover/src/whir_r1cs.rs +++ b/provekit/prover/src/whir_r1cs.rs @@ -237,10 +237,6 @@ where Standard: Distribution> + Distribution>, { ensure!(!commitments.is_empty(), "Need at least one commitment"); - ensure!( - !produce_spark_query, - "SPARK queries are not supported with the zook witness commitment" - ); let (a, b, c) = calculate_witness_bounds(&r1cs, &full_witness); drop(full_witness); @@ -290,8 +286,8 @@ pub struct ProveFromAlphasCtx { pub blinding_eval: Ext

, pub blinding_weights: Vec>, pub commitments: Vec>, - /// Reserved SPARK row axis; must be `None` — SPARK is unsupported with - /// the zook witness commitment. + /// When set, SPARK query data is produced with this Spartan sumcheck + /// point as the shared row axis. pub spark_row: Option>>, } @@ -341,13 +337,6 @@ where spark_row, } = ctx; - // TODO: restore SPARK once zook's `prove` exposes its final evaluation - // point. - ensure!( - spark_row.is_none(), - "SPARK queries are not supported with the zook witness commitment" - ); - let public_inputs_hash = P::hash_public_inputs(scheme.hash_config, &public_inputs.0); let public_inputs_len = public_inputs.len(); @@ -367,7 +356,7 @@ where // products through this embedding (a no-op under `Identity`). let embedding = ::default(); - if is_single { + let spark_queries: Option> = if is_single { // Single commitment path let commitment = commitments .into_iter() @@ -384,6 +373,16 @@ where merlin.prover_message(eval); } + // Snapshot the three alpha covectors (A, B, C) before the public + // weight is inserted; they are re-evaluated at the final WHIR point + // to form the SPARK query. + let spark_weights: Option>, usize)>> = spark_row.as_ref().map(|_| { + weights + .iter() + .map(|w| (w.vector().to_vec(), w.size())) + .collect() + }); + if public_inputs_len > 0 { let public_eval = compute_public_weight_evaluation( &embedding, @@ -400,9 +399,23 @@ where let form_refs = linear_form_refs(&weights); - scheme + let final_claim = scheme .whir_witness .prove(&mut merlin, witness, &form_refs, &evaluations); + + spark_row.zip(spark_weights).map(|(row, spark_weights)| { + let [claimed_a, claimed_b, claimed_c] = + evaluate_spark_weights(spark_weights, &final_claim.evaluation_point); + SparkQueryData { + row, + queries: vec![SparkColQueryData { + col: final_claim.evaluation_point, + claimed_a, + claimed_b, + claimed_c, + }], + } + }) } else { // Dual commitment path let mut commitments = commitments.into_iter(); @@ -459,9 +472,19 @@ where let WhirR1CSCommitment { witness: w1, .. } = c1; let WhirR1CSCommitment { witness: w2, .. } = c2; - { + let (final_claim_1, claimed_1) = { let mut weights = build_prefix_covectors(scheme.m, alphas_1); + // Snapshot the three alpha covectors (A, B, C) before the public + // weight is inserted; they are re-evaluated at the final WHIR + // point to form the SPARK query. + let spark_weights: Option>, usize)>> = spark_row.as_ref().map(|_| { + weights + .iter() + .map(|w| (w.vector().to_vec(), w.size())) + .collect() + }); + let mut evaluations: Vec> = Vec::new(); if let Some(pe) = public_1 { weights.insert(0, make_public_weight(x, public_inputs_len, scheme.m)); @@ -471,14 +494,27 @@ where let form_refs = linear_form_refs(&weights); - scheme + let final_claim = scheme .whir_witness .prove(&mut merlin, w1, &form_refs, &evaluations); - } - { + let claimed = + spark_weights.map(|sw| evaluate_spark_weights(sw, &final_claim.evaluation_point)); + (final_claim, claimed) + }; + + let (final_claim_2, claimed_2) = { let weights = build_prefix_covectors(scheme.m, alphas_2); + // The first three weights are the alpha covectors (A, B, C); the + // challenge weight, if any, is appended after them. + let spark_weights: Option>, usize)>> = spark_row.as_ref().map(|_| { + weights + .iter() + .map(|w| (w.vector().to_vec(), w.size())) + .collect() + }); + let mut evaluations: Vec> = evals_2; let challenge_covector = challenge_eval.map(|ce| { @@ -491,11 +527,44 @@ where form_refs.push(cw as &dyn LinearForm>); } - scheme + let final_claim = scheme .whir_witness .prove(&mut merlin, w2, &form_refs, &evaluations); - } - } + + let claimed = + spark_weights.map(|sw| evaluate_spark_weights(sw, &final_claim.evaluation_point)); + (final_claim, claimed) + }; + + // The SPARK column axis spans w1 | w2: prefix the per-commitment WHIR + // points with a selector coordinate (0 selects w1, 1 selects w2). + spark_row + .zip(claimed_1.zip(claimed_2)) + .map(|(row, ([a1, b1, c1], [a2, b2, c2]))| { + let mut col_1 = final_claim_1.evaluation_point; + col_1.insert(0, >::zero()); + let query_1 = SparkColQueryData { + col: col_1, + claimed_a: a1, + claimed_b: b1, + claimed_c: c1, + }; + + let mut col_2 = final_claim_2.evaluation_point; + col_2.insert(0, >::one()); + let query_2 = SparkColQueryData { + col: col_2, + claimed_a: a2, + claimed_b: b2, + claimed_c: c2, + }; + + SparkQueryData { + row, + queries: vec![query_1, query_2], + } + }) + }; // Open the ext blinding commitment: prove `blinding_eval` is the evaluation // of the committed blinding vector (g flattened at offset 0) against the @@ -521,10 +590,29 @@ where #[cfg(debug_assertions)] pattern: proof.pattern, }, - None, + spark_queries, )) } +/// Re-evaluate snapshotted alpha covectors (as `(prefix, domain_size)` pairs) +/// at the final WHIR evaluation point, yielding the claimed A, B, C matrix +/// evaluations for a SPARK query. +fn evaluate_spark_weights( + spark_weights: Vec<(Vec, usize)>, + evaluation_point: &[F], +) -> [F; 3] { + let evals: Vec = spark_weights + .into_iter() + .take(3) + .map(|(vector, domain_size)| { + PrefixCovector::new(vector, domain_size).mle_evaluate(evaluation_point) + }) + .collect(); + evals + .try_into() + .unwrap_or_else(|_| panic!("exactly 3 alpha-weight evaluations")) +} + pub fn compute_blinding_coefficients_for_round( g_univariates: &[[F; 4]], compute_for: usize,