diff --git a/provekit/backend/bn254/src/field.rs b/provekit/backend/bn254/src/field.rs index 2cdf7e41a..076b690b4 100644 --- a/provekit/backend/bn254/src/field.rs +++ b/provekit/backend/bn254/src/field.rs @@ -5,16 +5,11 @@ use { crate::{bytes::field_to_bytes_le, FieldElement, TranscriptSponge}, provekit_common::{Base, Ext, FieldHash, HashConfig, ProofField}, - whir::{ - algebra::embedding::Identity, - protocols::{whir::Config as GenericWhirConfig, whir_zk::Config as GenericWhirZkConfig}, - }, + whir::{algebra::embedding::Identity, protocols::whir::Config as GenericWhirConfig}, }; /// The WHIR config over the bn254 embedding. pub type WhirConfig = GenericWhirConfig>; -/// The zero-knowledge WHIR config over the bn254 extension field. -pub type WhirZkConfig = GenericWhirZkConfig; /// bn254 proof field: the `Identity` embedding (base == ext). #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -22,6 +17,9 @@ pub struct Bn254Field; impl ProofField for Bn254Field { type Embedding = Identity; + + // Field tag written into the `.np` proof format header; 0 identifies bn254. + const FIELD_ID: u8 = 0; } impl FieldHash for Bn254Field { @@ -52,6 +50,6 @@ mod tests { assert_eq!(bytes_to_field(&bytes), x); } - // Byte-exact public-input hashing is pinned by the KATs in - // `crate::field_hash::tests` (the bn254 bit-identical regression gate). + // Byte-exact public-input hashing is pinned by the known-answer tests in + // `crate::field_hash::tests`. } diff --git a/provekit/backend/bn254/src/lib.rs b/provekit/backend/bn254/src/lib.rs index 4ad54c98c..1031a88f1 100644 --- a/provekit/backend/bn254/src/lib.rs +++ b/provekit/backend/bn254/src/lib.rs @@ -24,7 +24,7 @@ pub use { acir::FieldElement as NoirElement, ark_bn254::Fr as FieldElement, compress::CompressedLayers, - field::{Bn254Field, WhirConfig, WhirZkConfig}, + field::{Bn254Field, WhirConfig}, frontend::{noir_to_native, NoirWitnessGenerator, PrintAbi}, ntt::RSFr, prove::Prove, diff --git a/provekit/backend/bn254/src/mavros_prove.rs b/provekit/backend/bn254/src/mavros_prove.rs index 1bb19755e..79de3ea2e 100644 --- a/provekit/backend/bn254/src/mavros_prove.rs +++ b/provekit/backend/bn254/src/mavros_prove.rs @@ -1,8 +1,10 @@ -//! Mavros WHIR proving, welded to bn254 (the Mavros VM's automatic- -//! differentiation pass returns `ark_bn254::Fr`). It composes the generic -//! proving primitives from `provekit-prover` (`run_zk_sumcheck_prover`, -//! `prove_from_alphas`) rather than living in the field-generic spine, and -//! folds back into `WhirR1CSProver

` once the Mavros VM is field-agnostic. +//! Mavros WHIR proving for bn254. The Mavros VM's automatic-differentiation +//! pass returns `ark_bn254::Fr`, so this proving path is bn254-specific; it +//! composes the generic proving primitives from `provekit-prover` +//! (`run_zk_sumcheck_prover`, `prove_from_alphas`). +//! +//! TODO: make field-generic and fold into `WhirR1CSProver

` when the Mavros +//! VM is field-agnostic. use { crate::{Bn254Field, FieldElement, TranscriptSponge}, @@ -49,9 +51,11 @@ impl MavrosR1CSProver for WhirR1CSScheme { let blinding = commitments[0] .blinding .as_ref() - .expect("c1 must carry blinding state"); + .ok_or_else(|| anyhow::anyhow!("c1 must carry blinding state"))?; let [a, b, c] = [witgen.out_a, witgen.out_b, witgen.out_c]; + // `g` is committed separately in the extension field; open `blinding_eval` + // against the blinding vector (g flattened at offset 0). let (alpha, blinding_eval) = run_zk_sumcheck_prover( a, b, @@ -59,8 +63,7 @@ impl MavrosR1CSProver for WhirR1CSScheme { &mut merlin, self.m_0, &blinding.polynomial, - &commitments[0].polynomial, - blinding.offset, + &blinding.vector, ); let eq_alpha = @@ -73,7 +76,6 @@ impl MavrosR1CSProver for WhirR1CSScheme { )?; let alphas = [ad_a, ad_b, ad_c]; - let blinding_offset = blinding.offset; let blinding_weights = expand_powers::<4, _>(&alpha); prove_from_alphas( @@ -81,7 +83,6 @@ impl MavrosR1CSProver for WhirR1CSScheme { merlin, alphas, blinding_eval, - blinding_offset, blinding_weights, commitments, public_inputs, diff --git a/provekit/backend/goldilocks/src/field.rs b/provekit/backend/goldilocks/src/field.rs index 050180e9a..77db1bdca 100644 --- a/provekit/backend/goldilocks/src/field.rs +++ b/provekit/backend/goldilocks/src/field.rs @@ -1,35 +1,76 @@ -//! The Goldilocks degree-3 extension proof field and its `FieldHash` glue. +//! The Goldilocks proof fields (base-leaf and ext-leaf) and their shared +//! `FieldHash` glue. use { crate::{bytes::field_to_bytes_le, field_hash::hash_field_elements, TranscriptSponge}, provekit_common::{Base, Ext, FieldHash, HashConfig, ProofField}, - whir::algebra::{embedding::Identity, fields::Field64_3}, + whir::algebra::{ + embedding::{Basefield, Identity}, + fields::Field64_3, + }, }; -/// Goldilocks degree-3 extension proof field. +/// The Goldilocks proof field: `Basefield`. +/// +/// Commits the witness in the base field `Field64` and uses the degree-3 +/// extension `Field64_3` only for challenges and sumcheck. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct GoldilocksField; impl ProofField for GoldilocksField { + type Embedding = Basefield; + + const FIELD_ID: u8 = 1; +} + +/// Goldilocks proof field with the identity embedding (`Identity`, +/// base == ext): challenges are both drawn from and committed in the full +/// `Field64_3` extension. +/// +/// Serves challenge-bearing circuits, where an extension challenge cannot be +/// stored in a base-field (`Field64`) witness slot; committing in the extension +/// keeps 128-bit soundness. +// TODO: `GoldilocksField` (base-committed) can subsume this once it binds +// challenges directly — needs (1) a base transcript codec so base challenges can +// be drawn from Fiat-Shamir (the `FieldHash` `Source` byte bridge), and (2) +// k-fold repetition, since a single `Field64` challenge is only ~64-bit sound. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct GoldilocksEfField; + +impl ProofField for GoldilocksEfField { type Embedding = Identity; + + // Distinct from `GoldilocksField`: the ext-leaf base layout (`Field64_3`) + // differs from its base-commit layout (`Field64`), so their serialized bytes + // differ. + const FIELD_ID: u8 = 2; } -impl FieldHash for GoldilocksField { - fn hash_public_inputs(config: HashConfig, inputs: &[Base]) -> Ext { - hash_field_elements(config, inputs) - } +/// Both Goldilocks fields share the same hash/byte glue: the challenge field is +/// `Field64_3` either way, and the public-input hash is base-generic. +macro_rules! impl_goldilocks_field_hash { + ($field:ty) => { + impl FieldHash for $field { + fn hash_public_inputs(config: HashConfig, inputs: &[Base]) -> Ext { + hash_field_elements(config, inputs) + } - fn ext_to_bytes_le(x: &Ext) -> Vec { - field_to_bytes_le(*x).to_vec() - } + fn ext_to_bytes_le(x: &Ext) -> Vec { + field_to_bytes_le(*x).to_vec() + } - type Sponge = TranscriptSponge; + type Sponge = TranscriptSponge; - fn transcript_sponge(config: HashConfig) -> Self::Sponge { - TranscriptSponge::from_config(config) - } + fn transcript_sponge(config: HashConfig) -> Self::Sponge { + TranscriptSponge::from_config(config) + } + } + }; } +impl_goldilocks_field_hash!(GoldilocksField); +impl_goldilocks_field_hash!(GoldilocksEfField); + #[cfg(test)] mod tests { use {super::*, crate::bytes::bytes_to_field}; diff --git a/provekit/backend/goldilocks/src/lib.rs b/provekit/backend/goldilocks/src/lib.rs index 77ef21cf0..c1698ef5c 100644 --- a/provekit/backend/goldilocks/src/lib.rs +++ b/provekit/backend/goldilocks/src/lib.rs @@ -6,7 +6,10 @@ mod field; mod field_hash; mod transcript_sponge; -pub use {field::GoldilocksField, transcript_sponge::TranscriptSponge}; +pub use { + field::{GoldilocksEfField, GoldilocksField}, + transcript_sponge::TranscriptSponge, +}; /// Register the Goldilocks engines in whir's global registries. /// diff --git a/provekit/common/src/field.rs b/provekit/common/src/field.rs index 88a3075e1..631fe93b6 100644 --- a/provekit/common/src/field.rs +++ b/provekit/common/src/field.rs @@ -9,14 +9,20 @@ use { /// Algebra carrier for a proof field: `Embedding::Source` is the base /// (committed) field, `Embedding::Target` the extension (challenge) field. /// -/// bn254 uses `Identity` (base == ext); goldilocks uses -/// `Identity` pre-v3, `Basefield` once zkWHIR v3 lands. +/// bn254 uses `Identity` (base == ext); a field with a genuine +/// extension uses an embedding whose source and target differ (e.g. +/// `Basefield`). pub trait ProofField: Copy + Debug + Eq + Send + Sync { /// `Default` lets the spine construct the embedding instance for mixed /// base×ext products (`Identity`/`Basefield` both derive it). The extension /// (`Target`) must be FFT-friendly and transcript-codec-able — WHIR's /// requirement for any challenge/commitment field. type Embedding: Embedding + Default; + + /// Per-field tag folded into the `.np` proof magic so one field's proof is + /// rejected by another field's verifier. Must be unique per field; bn254 + /// reserves `0` to keep its legacy magic byte-identical. + const FIELD_ID: u8; } /// Base (committed) field of a [`ProofField`]. @@ -44,6 +50,5 @@ pub trait FieldHash: ProofField { /// Construct the transcript sponge matching `config`. fn transcript_sponge(config: crate::HashConfig) -> Self::Sponge; - // TODO(base-bridge): add a base (`Source`) bridge when base commitment lands - // (V-stage). + // TODO: add a base-field (`Source`) byte bridge when one is needed. } diff --git a/provekit/common/src/file/binary_format.rs b/provekit/common/src/file/binary_format.rs index c4a25f335..dacac3160 100644 --- a/provekit/common/src/file/binary_format.rs +++ b/provekit/common/src/file/binary_format.rs @@ -12,16 +12,19 @@ pub const XZ_MAGIC: [u8; 6] = [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00]; // --------------------------------------------------------------------------- // Per-format identifiers and versions +// +// Major must match exactly on read (an incompatible layout is rejected); a +// reader accepts any file whose minor is >= its own. // --------------------------------------------------------------------------- pub const PROVER_FORMAT: [u8; 8] = *b"PrvKitPr"; -pub const PROVER_VERSION: (u16, u16) = (1, 3); +pub const PROVER_VERSION: (u16, u16) = (2, 0); pub const VERIFIER_FORMAT: [u8; 8] = *b"PrvKitVr"; -pub const VERIFIER_VERSION: (u16, u16) = (1, 3); +pub const VERIFIER_VERSION: (u16, u16) = (2, 0); pub const NOIR_PROOF_SCHEME_FORMAT: [u8; 8] = *b"NrProScm"; -pub const NOIR_PROOF_SCHEME_VERSION: (u16, u16) = (1, 3); +pub const NOIR_PROOF_SCHEME_VERSION: (u16, u16) = (2, 0); pub const NOIR_PROOF_FORMAT: [u8; 8] = *b"NPSProof"; -pub const NOIR_PROOF_VERSION: (u16, u16) = (1, 1); +pub const NOIR_PROOF_VERSION: (u16, u16) = (2, 0); diff --git a/provekit/common/src/prefix_covector.rs b/provekit/common/src/prefix_covector.rs index ef61bd61f..e45fb099c 100644 --- a/provekit/common/src/prefix_covector.rs +++ b/provekit/common/src/prefix_covector.rs @@ -1,6 +1,6 @@ use { - ark_ff::Field, - whir::algebra::{dot, linear_form::LinearForm, multilinear_extend}, + ark_ff::{Field, One, Zero}, + whir::algebra::{embedding::Embedding, linear_form::LinearForm, mixed_dot, multilinear_extend}, }; /// A covector that stores only a power-of-two prefix, with the rest @@ -207,13 +207,14 @@ pub fn build_prefix_covectors( /// allocating [`PrefixCovector`] weights. Used to write transcript hints /// before deferring weight construction (saves memory in dual-commit). #[must_use] -pub fn compute_alpha_evals( - polynomial: &[F], - alphas: &[Vec; N], -) -> Vec { +pub fn compute_alpha_evals( + embedding: &M, + polynomial: &[M::Source], + alphas: &[Vec; N], +) -> Vec { alphas .iter() - .map(|w| dot(w, &polynomial[..w.len()])) + .map(|w| mixed_dot(embedding, w, &polynomial[..w.len()])) .collect() } @@ -221,12 +222,17 @@ pub fn compute_alpha_evals( /// without allocating a [`PrefixCovector`]. Covers the R1CS constant at /// position 0 and `num_public_inputs` public input positions. #[must_use] -pub fn compute_public_eval(x: F, num_public_inputs: usize, polynomial: &[F]) -> F { +pub fn compute_public_eval( + embedding: &M, + x: M::Target, + num_public_inputs: usize, + polynomial: &[M::Source], +) -> M::Target { let n = num_public_inputs + 1; - let mut eval = F::zero(); - let mut x_pow = F::one(); + let mut eval = M::Target::zero(); + let mut x_pow = M::Target::one(); for &p in polynomial.iter().take(n) { - eval += x_pow * p; + eval += embedding.mixed_mul(x_pow, p); x_pow *= x; } eval @@ -321,11 +327,16 @@ pub fn make_challenge_weight( /// `⟨[1, x, x², …], poly[offsets[0]], poly[offsets[1]], …⟩` without /// allocating a [`SparseCovector`]. #[must_use] -pub fn compute_challenge_eval(x: F, challenge_offsets: &[usize], polynomial: &[F]) -> F { - let mut eval = F::zero(); - let mut x_pow = F::one(); +pub fn compute_challenge_eval( + embedding: &M, + x: M::Target, + challenge_offsets: &[usize], + polynomial: &[M::Source], +) -> M::Target { + let mut eval = M::Target::zero(); + let mut x_pow = M::Target::one(); for &offset in challenge_offsets { - eval += x_pow * polynomial[offset]; + eval += embedding.mixed_mul(x_pow, polynomial[offset]); x_pow *= x; } eval @@ -605,7 +616,8 @@ mod tests { poly[5] = fe(99); poly[11] = fe(17); - let eval = compute_challenge_eval(x, &offsets, &poly); + let embedding = whir::algebra::embedding::Identity::::new(); + let eval = compute_challenge_eval(&embedding, x, &offsets, &poly); let expected = fe(42) + fe(7) * fe(99) + fe(49) * fe(17); assert_eq!(eval, expected); } diff --git a/provekit/common/src/whir_r1cs.rs b/provekit/common/src/whir_r1cs.rs index b8aeb2a0c..df60b2c52 100644 --- a/provekit/common/src/whir_r1cs.rs +++ b/provekit/common/src/whir_r1cs.rs @@ -18,8 +18,8 @@ use { }, serde::{Deserialize, Serialize}, whir::{ - engines::EngineId, parameters::ProtocolParameters, - protocols::whir_zk::Config as GenericWhirZkConfig, transcript, + algebra::embedding::Identity, engines::EngineId, parameters::ProtocolParameters, + protocols::whir::Config as GenericWhirConfig, transcript, }, }; @@ -27,6 +27,14 @@ use { /// so smaller commitments are padded up to this many variables. pub const MIN_WHIR_NUM_VARIABLES: usize = 13; +/// WHIR folding factors, shared by the witness and blinding commitments. +const WHIR_INITIAL_FOLDING_FACTOR: usize = 3; +const WHIR_FOLDING_FACTOR: usize = 3; + +/// Domain floor for the ext blinding commitment: smallest WHIR domain valid for +/// the folding factors (not the witness floor — `g` is only `4 * m_0` coeffs). +const MIN_BLINDING_NUM_VARIABLES: usize = WHIR_INITIAL_FOLDING_FACTOR + WHIR_FOLDING_FACTOR; + /// Minimum sumcheck rounds, keeping the constraint-domain polynomial /// non-trivial. const MIN_SUMCHECK_NUM_VARIABLES: usize = 1; @@ -62,7 +70,14 @@ pub struct WhirR1CSScheme { pub num_challenges: usize, pub challenge_offsets: Vec, pub has_public_inputs: bool, - pub whir_witness: GenericWhirZkConfig>, + /// 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, + /// 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. + pub whir_blinding: GenericWhirConfig>>, pub r1cs_hash: R1csHash, /// Hash configuration for Merkle commitments, Fiat-Shamir sponge, and /// public-input instance binding. Source of truth; the WHIR engine ID @@ -76,6 +91,11 @@ impl WhirR1CSScheme

{ 1usize << self.m } + /// Return the Spartan-blinding commitment domain size. + pub fn blinding_domain_size(&self) -> usize { + 1usize << self.whir_blinding.initial_num_variables() + } + /// Create a domain separator for the provekit outer protocol. /// /// The domain separator serializes the entire scheme (including @@ -96,8 +116,8 @@ impl WhirR1CSScheme

{ /// transcript can later be bound to this instance (in /// [`Self::create_domain_separator`]). /// - /// Witness commitment domain size, sumcheck rounds, blinding room, and the - /// zkWHIR configuration are derived purely from R1CS dimensions. + /// The witness commitment domain size, sumcheck rounds, and blinding + /// commitment size are derived purely from R1CS dimensions. pub fn new_for_r1cs( r1cs: &R1CS>, w1_size: usize, @@ -146,19 +166,15 @@ impl WhirR1CSScheme

{ let m2_raw = next_power_of_two(w2_size); let m0_raw = next_power_of_two(num_constraints); - let mut m = m1_raw.max(m2_raw).max(MIN_WHIR_NUM_VARIABLES); + let m = m1_raw.max(m2_raw).max(MIN_WHIR_NUM_VARIABLES); let m_0 = m0_raw.max(MIN_SUMCHECK_NUM_VARIABLES); - // Ensure w1's zero-padding has room for the blinding polynomial coefficients. - if (1usize << m) - w1_size < 4 * m_0 { - m += 1; - } - Self { m, m_0, a_num_terms: next_power_of_two(a_num_entries), - whir_witness: Self::new_whir_zk_config_for_size(m, 1, 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, challenge_offsets, @@ -168,31 +184,43 @@ impl WhirR1CSScheme

{ } } - /// Build the zkWHIR configuration for a polynomial of `num_variables` over - /// the extension (challenge) field of `P`. - pub fn new_whir_zk_config_for_size( - num_variables: usize, - num_polynomials: usize, - hash_id: EngineId, - ) -> GenericWhirZkConfig> { - let nv = num_variables.max(MIN_WHIR_NUM_VARIABLES); - - // Parameters tuned for 128-bit security under the Johnson bound (the old - // ConjectureList soundness was disproven). Rate=2 balances query count vs - // codeword size; ff=3 keeps blinding polynomials small; pow_bits=10 shifts - // security budget toward algebraic hardness (118 bits) with light PoW per - // round, which is faster than the default ~18-bit grinding. - let whir_params = ProtocolParameters { + /// Shared WHIR parameters for the witness and blinding commitments: 128-bit + /// security under the Johnson bound, rate 2, folding factor 3, pow_bits 10 + /// (≈118-bit algebraic hardness plus light per-round PoW). + fn whir_protocol_params(hash_id: EngineId) -> ProtocolParameters { + ProtocolParameters { unique_decoding: false, security_level: 128, pow_bits: 10, - initial_folding_factor: 3, - folding_factor: 3, + initial_folding_factor: WHIR_INITIAL_FOLDING_FACTOR, + folding_factor: WHIR_FOLDING_FACTOR, starting_log_inv_rate: 2, batch_size: 1, hash_id, - }; - GenericWhirZkConfig::>::new(1 << nv, &whir_params, num_polynomials) + } + } + + /// Build the non-ZK witness WHIR 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 { + let nv = num_variables.max(MIN_WHIR_NUM_VARIABLES); + GenericWhirConfig::::new(1 << nv, &Self::whir_protocol_params(hash_id)) + } + + /// Build the WHIR config for the blinding polynomial `g`: its `4 * m_0` + /// cubic coefficients committed in the ext field via `Identity`. + pub fn new_blinding_config_for_size( + m_0: usize, + hash_id: EngineId, + ) -> GenericWhirConfig>> { + let nv_blind = next_power_of_two(4 * m_0).max(MIN_BLINDING_NUM_VARIABLES); + GenericWhirConfig::>>::new( + 1 << nv_blind, + &Self::whir_protocol_params(hash_id), + ) } } @@ -221,9 +249,19 @@ pub struct ProvekitProof { pub whir_r1cs_proof: WhirR1CSProof, } +/// Derive the `.np` format magic from [`ProofField::FIELD_ID`] by offsetting +/// the final magic byte: bn254 (id 0) keeps the historical magic, other fields +/// a distinct one. +#[cfg(not(target_arch = "wasm32"))] +const fn np_format(field_id: u8) -> [u8; 8] { + let mut f = binary_format::NOIR_PROOF_FORMAT; + f[7] = f[7].wrapping_add(field_id); + f +} + #[cfg(not(target_arch = "wasm32"))] impl FileFormat for ProvekitProof

{ - const FORMAT: [u8; 8] = binary_format::NOIR_PROOF_FORMAT; + const FORMAT: [u8; 8] = np_format(

::FIELD_ID); const EXTENSION: &'static str = "np"; const VERSION: (u16, u16) = binary_format::NOIR_PROOF_VERSION; const COMPRESSION: Compression = Compression::Zstd; @@ -235,3 +273,16 @@ impl MaybeHashAware for ProvekitProof

{ None } } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::{binary_format, np_format}; + + #[test] + fn np_format_preserves_bn254_and_distinguishes_fields() { + assert_eq!(np_format(0), binary_format::NOIR_PROOF_FORMAT); + assert_ne!(np_format(1), np_format(0)); + assert_ne!(np_format(2), np_format(0)); + assert_ne!(np_format(2), np_format(1)); + } +} diff --git a/provekit/prover/src/whir_r1cs.rs b/provekit/prover/src/whir_r1cs.rs index 0fcce1558..8456b813e 100644 --- a/provekit/prover/src/whir_r1cs.rs +++ b/provekit/prover/src/whir_r1cs.rs @@ -25,20 +25,31 @@ use { }, std::borrow::Cow, whir::{ - algebra::{dot, embedding::Embedding, linear_form::LinearForm}, - protocols::whir_zk::Witness as WhirZkWitness, + algebra::{ + dot, + embedding::{Embedding, Identity}, + linear_form::LinearForm, + mixed_dot, + }, + protocols::whir::Witness as WhirWitness, transcript::{Codec, DuplexSpongeInterface, ProverState, VerifierMessage}, }, }; +/// Spartan sumcheck blinding `g`, committed separately in the extension field. pub struct BlindingState { + /// The `m_0` cubic blinding univariates (4 ext coefficients each). pub polynomial: Vec<[Ext

; 4]>, - pub offset: usize, + /// `polynomial` flattened (length `4 * m_0`) and zero-padded to the + /// blinding commitment domain — the vector actually committed. + pub vector: Vec>, + /// WHIR witness for the ext blinding commitment. + pub witness: WhirWitness, Identity>>, } pub struct WhirR1CSCommitment { - pub witness: WhirZkWitness>, - pub polynomial: Vec>, + pub witness: WhirWitness, P::Embedding>, + pub polynomial: Vec>, pub blinding: Option>, } @@ -64,7 +75,7 @@ pub trait WhirR1CSProver { impl WhirR1CSProver

for WhirR1CSScheme

where - Standard: Distribution>, + Standard: Distribution> + Distribution>, { #[instrument(skip_all)] fn commit( @@ -94,7 +105,7 @@ where "R1CS constraints exceed scheme capacity" ); - let num_vars = self.whir_witness.num_witness_variables(); + let num_vars = self.whir_witness.initial_num_variables(); let target_len = 1usize << num_vars; let mut padded_witness = pad_to_power_of_two(witness); @@ -102,35 +113,30 @@ where padded_witness.resize(target_len, >::zero()); } - // Pre-v3 WHIR ZK commits over a single field (`whir_zk::Config` - // hardcodes `Identity`), so lift the base witness to the - // extension. The lift is a no-op whenever base == ext (the `Identity` - // embedding), as today for both bn254 and goldilocks. - // TODO(base-commit): drop this lift once zkWHIR v3 (`Basefield`) lets - // the ZK path commit base-field elements directly (V-stage). - let embedding = ::default(); - let mut padded_witness: Vec> = embedding.map_vec(padded_witness); + // Commit the base-field witness directly (non-hiding — openings leak + // witness values; see the `whir_witness` field docs). + let witness_commitment = self.whir_witness.commit(merlin, &[&padded_witness]); + // Commit the Spartan sumcheck blinding `g` separately, natively in the + // extension field. Transcript order: this commitment is absorbed + // immediately after the witness commitment (mirrored in the verifier). let blinding = if is_w1 { let g = generate_blinding_univariates::>(self.m_0); - let offset = witness_size; - for (i, coeffs) in g.iter().enumerate() { - for (j, &c) in coeffs.iter().enumerate() { - padded_witness[offset + i * 4 + j] = c; - } - } + let blind_len = self.blinding_domain_size(); + let mut g_vector: Vec> = g.iter().flatten().copied().collect(); + g_vector.resize(blind_len, >::zero()); + let blinding_witness = self.whir_blinding.commit(merlin, &[&g_vector]); Some(BlindingState { polynomial: g, - offset, + vector: g_vector, + witness: blinding_witness, }) } else { None }; - let zk_witness = self.whir_witness.commit(merlin, &[&padded_witness]); - Ok(WhirR1CSCommitment { - witness: zk_witness, + witness: witness_commitment, polynomial: padded_witness, blinding, }) @@ -163,8 +169,11 @@ where let blinding = commitments[0] .blinding .as_ref() - .expect("c1 must carry blinding state"); + .ok_or_else(|| anyhow::anyhow!("c1 must carry blinding state"))?; + // The Spartan sumcheck runs entirely in the extension field; `g` is + // native ext, committed separately. `blinding_eval` opens the ext + // blinding vector (which holds `g` flattened at offset 0). let (alpha, blinding_eval) = run_zk_sumcheck_prover( a, b, @@ -172,21 +181,18 @@ where &mut merlin, self.m_0, &blinding.polynomial, - &commitments[0].polynomial, - blinding.offset, + &blinding.vector, ); let (at, bt, ct) = transpose_r1cs_matrices(&r1cs); let alphas = multiply_transposed_by_eq_alpha(&embedding, &at, &bt, &ct, &alpha, &r1cs); - let blinding_offset = blinding.offset; let blinding_weights = expand_powers::<4, _>(&alpha); prove_from_alphas( self, merlin, alphas, blinding_eval, - blinding_offset, blinding_weights, commitments, public_inputs, @@ -200,22 +206,31 @@ pub fn prove_from_alphas( mut merlin: ProverState, alphas: [Vec>; 3], blinding_eval: Ext

, - blinding_offset: usize, blinding_weights: Vec>, - commitments: Vec>, + mut commitments: Vec>, public_inputs: &PublicInputs>, ) -> Result where - Standard: Distribution>, + Standard: Distribution> + Distribution>, { let public_inputs_hash = P::hash_public_inputs(scheme.hash_config, &public_inputs.0); let public_inputs_len = public_inputs.len(); + // The ext blinding commitment lives on c0; pull it out before the base + // commitments are consumed below. It is opened at the very end, after all + // base-witness opens (mirrored in the verifier). + let blinding_state = commitments[0] + .blinding + .take() + .ok_or_else(|| anyhow::anyhow!("c0 must carry blinding state"))?; + let is_single = commitments.len() == 1; let (x, public_weight) = get_public_weights(public_inputs_hash, public_inputs_len, &mut merlin, scheme.m); - let domain_size = 1usize << scheme.m; + // Witness is base, covectors/evaluations are ext: evaluations use mixed + // products through this embedding (a no-op under `Identity`). + let embedding = ::default(); if is_single { // Single commitment path @@ -223,8 +238,12 @@ where .into_iter() .next() .expect("single-commitment path requires at least one commitment"); - let (mut weights, evals) = - create_weights_and_evaluations::<3, _>(scheme.m, &commitment.polynomial, alphas); + let (mut weights, evals) = create_weights_and_evaluations::<3, _>( + &embedding, + scheme.m, + &commitment.polynomial, + alphas, + ); for eval in &evals { merlin.prover_message(eval); @@ -232,6 +251,7 @@ where if public_inputs_len > 0 { let public_eval = compute_public_weight_evaluation( + &embedding, &mut weights, &commitment.polynomial, public_weight, @@ -239,21 +259,17 @@ where merlin.prover_message(&public_eval); } - let mut evaluations = compute_evaluations(&weights, &commitment.polynomial); - evaluations.push(blinding_eval); + let evaluations = compute_evaluations(&embedding, &weights, &commitment.polynomial); - let blinding_covector = OffsetCovector::new(blinding_weights, blinding_offset, domain_size); - - let mut boxed_weights: Vec>>> = weights + let boxed_weights: Vec>>> = weights .into_iter() .map(|w| Box::new(w) as Box>>) .collect(); - boxed_weights.push(Box::new(blinding_covector)); let _ = scheme.whir_witness.prove( &mut merlin, vec![Cow::Borrowed(commitment.polynomial.as_slice())], - commitment.witness, + vec![Cow::Owned(commitment.witness)], boxed_weights, Cow::Borrowed(&evaluations), ); @@ -282,8 +298,8 @@ where .try_into() .expect("alphas_2 must have exactly 3 elements"); - let evals_1 = compute_alpha_evals(&c1.polynomial, &alphas_1); - let evals_2 = compute_alpha_evals(&c2.polynomial, &alphas_2); + let evals_1 = compute_alpha_evals(&embedding, &c1.polynomial, &alphas_1); + let evals_2 = compute_alpha_evals(&embedding, &c2.polynomial, &alphas_2); for eval in &evals_1 { merlin.prover_message(eval); } @@ -292,7 +308,7 @@ where } let public_1 = if public_inputs_len > 0 { - let p1 = compute_public_eval(x, public_inputs_len, &c1.polynomial); + let p1 = compute_public_eval(&embedding, x, public_inputs_len, &c1.polynomial); merlin.prover_message(&p1); Some(p1) } else { @@ -302,7 +318,8 @@ where // Challenge binding: prove that w2 contains the correct Fiat-Shamir // challenge values at the expected positions. let challenge_eval = if !scheme.challenge_offsets.is_empty() { - let ce = compute_challenge_eval(x, &scheme.challenge_offsets, &c2.polynomial); + let ce = + compute_challenge_eval(&embedding, x, &scheme.challenge_offsets, &c2.polynomial); merlin.prover_message(&ce); Some(ce) } else { @@ -322,21 +339,16 @@ where evaluations.push(pe); } evaluations.extend_from_slice(&evals_1); - evaluations.push(blinding_eval); - - let blinding_covector = - OffsetCovector::new(blinding_weights, blinding_offset, domain_size); - let mut boxed_weights: Vec>>> = weights + let boxed_weights: Vec>>> = weights .into_iter() .map(|w| Box::new(w) as Box>>) .collect(); - boxed_weights.push(Box::new(blinding_covector)); let _ = scheme.whir_witness.prove( &mut merlin, vec![Cow::Borrowed(p1.as_slice())], - w1, + vec![Cow::Owned(w1)], boxed_weights, Cow::Borrowed(&evaluations), ); @@ -366,13 +378,29 @@ where let _ = scheme.whir_witness.prove( &mut merlin, vec![Cow::Borrowed(p2.as_slice())], - w2, + vec![Cow::Owned(w2)], boxed_weights, Cow::Borrowed(&evaluations), ); } } + // Open the ext blinding commitment: prove `blinding_eval` is the evaluation + // of the committed blinding vector (g flattened at offset 0) against the + // sumcheck power covector. Sent after all base-witness opens (mirrored in + // the verifier). + { + let blind_domain = scheme.blinding_domain_size(); + let blinding_covector = OffsetCovector::new(blinding_weights, 0, blind_domain); + let _ = scheme.whir_blinding.prove( + &mut merlin, + vec![Cow::Borrowed(blinding_state.vector.as_slice())], + vec![Cow::Owned(blinding_state.witness)], + vec![Box::new(blinding_covector) as Box>>], + Cow::Borrowed(&[blinding_eval]), + ); + } + let proof = merlin.proof(); Ok(WhirR1CSProof { narg_string: proof.narg_string, @@ -475,8 +503,7 @@ pub fn run_zk_sumcheck_prover merlin: &mut ProverState, m_0: usize, blinding_polynomial: &[[F; 4]], - w1_polynomial: &[F], - blinding_offset: usize, + blinding_vector: &[F], ) -> (Vec, F) { let r: Vec = merlin.verifier_message_vec(m_0); let mut eq = calculate_evaluations_over_boolean_hypercube_for_eq(&r, 1 << r.len()); @@ -568,20 +595,18 @@ pub fn run_zk_sumcheck_prover drop((a, b, c, eq)); let weight_vec = expand_powers::<4, _>(alpha.as_slice()); - let blinding_eval = dot( - &weight_vec, - &w1_polynomial[blinding_offset..blinding_offset + weight_vec.len()], - ); + let blinding_eval = dot(&weight_vec, &blinding_vector[..weight_vec.len()]); merlin.prover_message(&blinding_eval); (alpha, blinding_eval) } -fn create_weights_and_evaluations( +fn create_weights_and_evaluations( + embedding: &M, m: usize, - polynomial: &[F], - alphas: [Vec; N], -) -> (Vec>, Vec) { + polynomial: &[M::Source], + alphas: [Vec; N], +) -> (Vec>, Vec) { let domain_size = 1usize << m; let mut weights = Vec::with_capacity(N); @@ -589,29 +614,34 @@ fn create_weights_and_evaluations( for mut w in alphas { let base_len = w.len().next_power_of_two().max(2); - w.resize(base_len, F::zero()); + w.resize(base_len, M::Target::zero()); - evals.push(dot(&w, &polynomial[..base_len])); + evals.push(mixed_dot(embedding, &w, &polynomial[..base_len])); weights.push(PrefixCovector::new(w, domain_size)); } (weights, evals) } -fn compute_evaluations(weights: &[PrefixCovector], polynomial: &[F]) -> Vec { +fn compute_evaluations( + embedding: &M, + weights: &[PrefixCovector], + polynomial: &[M::Source], +) -> Vec { weights .iter() - .map(|w| dot(w.vector(), &polynomial[..w.vector().len()])) + .map(|w| mixed_dot(embedding, w.vector(), &polynomial[..w.vector().len()])) .collect() } -fn compute_public_weight_evaluation( - weights: &mut Vec>, - polynomial: &[F], - public_weights: PrefixCovector, -) -> F { +fn compute_public_weight_evaluation( + embedding: &M, + weights: &mut Vec>, + polynomial: &[M::Source], + public_weights: PrefixCovector, +) -> M::Target { let n = public_weights.vector().len(); - let eval = dot(public_weights.vector(), &polynomial[..n]); + let eval = mixed_dot(embedding, public_weights.vector(), &polynomial[..n]); weights.insert(0, public_weights); eval } diff --git a/provekit/r1cs-compiler/src/whir_r1cs.rs b/provekit/r1cs-compiler/src/whir_r1cs.rs index 3946cdebd..a9dd2fece 100644 --- a/provekit/r1cs-compiler/src/whir_r1cs.rs +++ b/provekit/r1cs-compiler/src/whir_r1cs.rs @@ -103,49 +103,31 @@ mod tests { assert_eq!(from_r1cs.num_challenges, from_dimensions.num_challenges); } - #[test] - fn verify_security_level() { - let config = - WhirR1CSScheme::::new_whir_zk_config_for_size(20, 1, whir::hash::SHA2); - let sec_blinded = config - .blinded_commitment - .security_level(config.blinded_commitment.initial_committer.num_vectors, 1); - let sec_blinding = config - .blinding_commitment - .security_level(config.blinding_commitment.initial_committer.num_vectors, 1); + fn assert_configs_secure(size: usize) { + let witness = + WhirR1CSScheme::::new_witness_config_for_size(size, whir::hash::SHA2); + 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_blinded >= 128.0, - "Blinded commitment security {sec_blinded:.2} < 128 bits" + sec_witness >= 128.0, + "Witness commitment security {sec_witness:.2} < 128 bits at size {size}" ); assert!( sec_blinding >= 128.0, - "Blinding commitment security {sec_blinding:.2} < 128 bits" + "Blinding commitment security {sec_blinding:.2} < 128 bits at size {size}" ); } + #[test] + fn verify_security_level() { + assert_configs_secure(20); + } + #[test] fn verify_security_level_min_variables() { - let config = WhirR1CSScheme::::new_whir_zk_config_for_size( - MIN_WHIR_NUM_VARIABLES, - 1, - whir::hash::SHA2, - ); - let sec_blinded = config - .blinded_commitment - .security_level(config.blinded_commitment.initial_committer.num_vectors, 1); - let sec_blinding = config - .blinding_commitment - .security_level(config.blinding_commitment.initial_committer.num_vectors, 1); - assert!( - sec_blinded >= 128.0, - "Blinded commitment security {sec_blinded:.2} < 128 bits at nv={}", - MIN_WHIR_NUM_VARIABLES - ); - assert!( - sec_blinding >= 128.0, - "Blinding commitment security {sec_blinding:.2} < 128 bits at nv={}", - MIN_WHIR_NUM_VARIABLES - ); + assert_configs_secure(MIN_WHIR_NUM_VARIABLES); } #[test] @@ -175,7 +157,7 @@ mod tests { } #[test] - fn dimension_builders_bump_exact_power_of_two_w1_for_blinding_room() { - assert_dimension_builders(12_288, 2_048, 8_192, 14, 11); + fn dimension_builders_exact_power_of_two_w1() { + assert_dimension_builders(12_288, 2_048, 8_192, 13, 11); } } diff --git a/provekit/verifier/src/whir_r1cs.rs b/provekit/verifier/src/whir_r1cs.rs index 4402da9db..c4ea7ddac 100644 --- a/provekit/verifier/src/whir_r1cs.rs +++ b/provekit/verifier/src/whir_r1cs.rs @@ -66,14 +66,21 @@ impl WhirR1CSVerifier

for WhirR1CSScheme

{ let commitment_1 = self .whir_witness - .receive_commitments(&mut arthur, 1) + .receive_commitment(&mut arthur) .map_err(|_| anyhow::anyhow!("Failed to parse commitment 1"))?; + // Mirror the prover: the ext blinding commitment is absorbed immediately + // after the (w1) witness commitment, before any challenge sampling. + let blinding_commitment = self + .whir_blinding + .receive_commitment(&mut arthur) + .map_err(|_| anyhow::anyhow!("Failed to parse blinding commitment"))?; + let (commitment_2, logup_challenges) = if self.num_challenges > 0 { let challenges: Vec> = arthur.verifier_message_vec(self.num_challenges); let commitment = self .whir_witness - .receive_commitments(&mut arthur, 1) + .receive_commitment(&mut arthur) .map_err(|_| anyhow::anyhow!("Failed to parse commitment 2"))?; (Some(commitment), Some(challenges)) } else { @@ -109,8 +116,6 @@ impl WhirR1CSVerifier

for WhirR1CSScheme

{ let blinding_eval = data_from_sumcheck_verifier.blinding_eval; let blinding_weights = expand_powers::<4, _>(&data_from_sumcheck_verifier.alpha); - let domain_size = self.domain_size(); - let blinding_covector = OffsetCovector::new(blinding_weights, self.w1_size, domain_size); let (az_at_alpha, bz_at_alpha, cz_at_alpha) = if let Some(commitment_2) = commitment_2 { let (alphas_1, alphas_2): (Vec<_>, Vec<_>) = alphas @@ -153,7 +158,7 @@ impl WhirR1CSVerifier

for WhirR1CSScheme

{ let mut weights_1 = build_prefix_covectors(self.m, alphas_1); let weights_2 = build_prefix_covectors(self.m, alphas_2); - let mut evaluations_1 = if !public_inputs.is_empty() { + let evaluations_1 = if !public_inputs.is_empty() { let public_1: Ext

= arthur .prover_message() .map_err(|_| anyhow::anyhow!("Failed to read public_1"))?; @@ -163,7 +168,6 @@ impl WhirR1CSVerifier

for WhirR1CSScheme

{ } else { evals_1.to_vec() }; - evaluations_1.push(blinding_eval); let mut evaluations_2 = evals_2.to_vec(); // Challenge binding: verify that w2 contains the correct @@ -180,15 +184,17 @@ impl WhirR1CSVerifier

for WhirR1CSScheme

{ None }; - let mut weight_refs_1: Vec<&dyn LinearForm>> = weights_1 + let weight_refs_1: Vec<&dyn LinearForm>> = weights_1 .iter() .map(|w| w as &dyn LinearForm>) .collect(); - weight_refs_1.push(&blinding_covector as &dyn LinearForm>); - self.whir_witness - .verify(&mut arthur, &weight_refs_1, &evaluations_1, &commitment_1) + let fc_1 = self + .whir_witness + .verify(&mut arthur, &[&commitment_1], &evaluations_1) .map_err(|_| anyhow::anyhow!("WHIR verification failed for c1"))?; + fc_1.verify(weight_refs_1.iter().copied()) + .map_err(|_| anyhow::anyhow!("WHIR final-claim check failed for c1"))?; let mut weight_refs_2: Vec<&dyn LinearForm>> = weights_2 .iter() @@ -197,9 +203,12 @@ impl WhirR1CSVerifier

for WhirR1CSScheme

{ if let Some(ref cw) = challenge_covector { weight_refs_2.push(cw as &dyn LinearForm>); } - self.whir_witness - .verify(&mut arthur, &weight_refs_2, &evaluations_2, &commitment_2) + let fc_2 = self + .whir_witness + .verify(&mut arthur, &[&commitment_2], &evaluations_2) .map_err(|_| anyhow::anyhow!("WHIR verification failed for c2"))?; + fc_2.verify(weight_refs_2.iter().copied()) + .map_err(|_| anyhow::anyhow!("WHIR final-claim check failed for c2"))?; ( evals_1[0] + evals_2[0], @@ -221,7 +230,7 @@ impl WhirR1CSVerifier

for WhirR1CSScheme

{ let mut weights = build_prefix_covectors(self.m, alphas); - let mut evaluations = if !public_inputs.is_empty() { + let evaluations = if !public_inputs.is_empty() { let public_eval: Ext

= arthur .prover_message() .map_err(|_| anyhow::anyhow!("Failed to read public eval"))?; @@ -231,21 +240,39 @@ impl WhirR1CSVerifier

for WhirR1CSScheme

{ } else { evals.to_vec() }; - evaluations.push(blinding_eval); - let mut weight_refs: Vec<&dyn LinearForm>> = weights + let weight_refs: Vec<&dyn LinearForm>> = weights .iter() .map(|w| w as &dyn LinearForm>) .collect(); - weight_refs.push(&blinding_covector as &dyn LinearForm>); - self.whir_witness - .verify(&mut arthur, &weight_refs, &evaluations, &commitment_1) + let fc = self + .whir_witness + .verify(&mut arthur, &[&commitment_1], &evaluations) .map_err(|_| anyhow::anyhow!("WHIR verification failed"))?; + fc.verify(weight_refs.iter().copied()) + .map_err(|_| anyhow::anyhow!("WHIR final-claim check failed"))?; (evals[0], evals[1], evals[2]) }; + // Open the ext blinding commitment: verify `blinding_eval` against the + // sumcheck power covector over the committed blinding vector (g at + // offset 0). Mirrors the prover's single blinding open after all base + // opens. + { + let blind_domain = self.blinding_domain_size(); + let blinding_covector = OffsetCovector::new(blinding_weights, 0, blind_domain); + let fc_b = self + .whir_blinding + .verify(&mut arthur, &[&blinding_commitment], &[blinding_eval]) + .map_err(|_| anyhow::anyhow!("WHIR verification failed for blinding"))?; + fc_b.verify(std::iter::once( + &blinding_covector as &dyn LinearForm>, + )) + .map_err(|_| anyhow::anyhow!("WHIR final-claim check failed for blinding"))?; + } + ensure!( data_from_sumcheck_verifier.f_at_alpha == (az_at_alpha * bz_at_alpha - cz_at_alpha) diff --git a/tooling/cli/src/cmd/generate_gnark_inputs.rs b/tooling/cli/src/cmd/generate_gnark_inputs.rs index 1c0c315c6..fd04de451 100644 --- a/tooling/cli/src/cmd/generate_gnark_inputs.rs +++ b/tooling/cli/src/cmd/generate_gnark_inputs.rs @@ -56,8 +56,8 @@ impl Command for Args { write_gnark_parameters_to_file( &verifier.whir_for_witness.clone().unwrap(), - &wfw.whir_witness.blinded_commitment, - &wfw.whir_witness.blinding_commitment, + &wfw.whir_witness, + &wfw.whir_blinding, &proof.whir_r1cs_proof, wfw.m_0, wfw.m, diff --git a/tooling/provekit-fixtures/src/harness.rs b/tooling/provekit-fixtures/src/harness.rs index b5079c46e..879efa613 100644 --- a/tooling/provekit-fixtures/src/harness.rs +++ b/tooling/provekit-fixtures/src/harness.rs @@ -39,7 +39,7 @@ pub fn prove

( ) -> Result<(WhirR1CSScheme

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

::new_for_r1cs( r1cs, @@ -80,7 +80,7 @@ pub fn prove_and_verify

( ) -> Result<()> where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, { let (scheme, proof) = prove::

(r1cs, witness, public_inputs)?; scheme.verify(&proof, public_inputs, r1cs) @@ -245,7 +245,7 @@ pub fn time_dual_commit_prove

( ) -> Result where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, { ensure!( w1_size <= r1cs.num_witnesses() && r1cs.num_witnesses() <= full_witness.len(), diff --git a/tooling/provekit-fixtures/tests/bn254_roundtrip.rs b/tooling/provekit-fixtures/tests/bn254_roundtrip.rs index 4b9574170..357bb92ba 100644 --- a/tooling/provekit-fixtures/tests/bn254_roundtrip.rs +++ b/tooling/provekit-fixtures/tests/bn254_roundtrip.rs @@ -5,3 +5,4 @@ mod shared; use provekit_backend_bn254::{register, Bn254Field}; roundtrip_suite!(Bn254Field, register); +challenge_roundtrip_suite!(Bn254Field, register); diff --git a/tooling/provekit-fixtures/tests/bn254_soundness.rs b/tooling/provekit-fixtures/tests/bn254_soundness.rs index 9cd4f50a0..1343238cf 100644 --- a/tooling/provekit-fixtures/tests/bn254_soundness.rs +++ b/tooling/provekit-fixtures/tests/bn254_soundness.rs @@ -6,3 +6,4 @@ mod shared; use provekit_backend_bn254::{register, Bn254Field}; soundness_suite!(Bn254Field, register); +challenge_soundness_suite!(Bn254Field, register); diff --git a/tooling/provekit-fixtures/tests/goldilocks_roundtrip.rs b/tooling/provekit-fixtures/tests/goldilocks_roundtrip.rs index 9cfa36af4..a1639810e 100644 --- a/tooling/provekit-fixtures/tests/goldilocks_roundtrip.rs +++ b/tooling/provekit-fixtures/tests/goldilocks_roundtrip.rs @@ -1,7 +1,16 @@ //! Goldilocks prove→verify roundtrips over synthetic R1CS fixtures. +//! +//! Base-compatible fixtures run on the canonical base-leaf `GoldilocksField`. +//! The challenge-bearing fixtures (LogUp, multi-challenge) place extension +//! challenge values in the witness, so they run on `GoldilocksEfField`, whose +//! base and extension fields coincide. +//! +//! TODO: run the challenge-bearing fixtures on the base-leaf field once a +//! base-field LogUp construction is available. mod shared; -use provekit_backend_goldilocks::{register, GoldilocksField}; +use provekit_backend_goldilocks::{register, GoldilocksEfField, GoldilocksField}; roundtrip_suite!(GoldilocksField, register); +challenge_roundtrip_suite!(GoldilocksEfField, register); diff --git a/tooling/provekit-fixtures/tests/goldilocks_soundness.rs b/tooling/provekit-fixtures/tests/goldilocks_soundness.rs index df5d30fa6..94d8f1a51 100644 --- a/tooling/provekit-fixtures/tests/goldilocks_soundness.rs +++ b/tooling/provekit-fixtures/tests/goldilocks_soundness.rs @@ -1,8 +1,11 @@ //! Goldilocks soundness checks: malformed witnesses and public inputs must be //! rejected at verification. +//! +//! See `goldilocks_roundtrip.rs` for the base-leaf vs ext-leaf field split. mod shared; -use provekit_backend_goldilocks::{register, GoldilocksField}; +use provekit_backend_goldilocks::{register, GoldilocksEfField, GoldilocksField}; soundness_suite!(GoldilocksField, register); +challenge_soundness_suite!(GoldilocksEfField, register); diff --git a/tooling/provekit-fixtures/tests/shared/mod.rs b/tooling/provekit-fixtures/tests/shared/mod.rs index d4b38b176..62d547682 100644 --- a/tooling/provekit-fixtures/tests/shared/mod.rs +++ b/tooling/provekit-fixtures/tests/shared/mod.rs @@ -48,7 +48,7 @@ pub fn oracle_accepts_satisfying_and_rejects_broken() { pub fn two_public_inputs_roundtrip

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, { let (r1cs, w) = two_public_inputs::>(6, 7); let public_inputs = PublicInputs::from_vec(vec![w[1], w[2]]); @@ -62,7 +62,7 @@ where pub fn squaring_chain_size_sweep_roundtrip

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, { const WITNESS_FLOOR: usize = 8192; // 2^13 @@ -90,7 +90,7 @@ where pub fn random_satisfiable_proves_and_perturbation_rejects

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, { for seed in 0..8u64 { let (r1cs, w) = random_satisfiable::>(seed, 4, 8); @@ -118,7 +118,7 @@ fn logup_roundtrip

( ) -> anyhow::Result<()> where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, P::Embedding: Embedding>, { let LogUpInstance { @@ -138,7 +138,7 @@ where pub fn logup_lookup_size_sweep_roundtrip

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, P::Embedding: Embedding>, { for seed in 0..6u64 { @@ -154,7 +154,7 @@ where pub fn multi_challenge_binding_roundtrip

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, P::Embedding: Embedding>, { let (r1cs, w1, offsets) = multi_challenge_inverses::>(4); @@ -174,7 +174,7 @@ where pub fn dual_commit_with_public_roundtrip

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, P::Embedding: Embedding>, { let (r1cs, w1, offsets) = challenge_with_public_input::>(7); @@ -200,7 +200,7 @@ where pub fn corrupted_witness_is_rejected

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, { let (r1cs, mut w) = squaring_chain::>(3, 8); let last = w.len() - 1; @@ -218,7 +218,7 @@ where pub fn wrong_r1cs_is_rejected

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, { let (r1cs_a, w) = squaring_chain::>(3, 8); let public_inputs = PublicInputs::from_vec(vec![w[1]]); @@ -240,7 +240,7 @@ where pub fn public_input_binding_mismatch_is_rejected

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, { // N = 1: prove and verify with the same wrong public input. let (r1cs, w) = squaring_chain::>(3, 8); @@ -274,7 +274,7 @@ where pub fn tampered_public_input_is_rejected

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, { // N = 1. let (r1cs, w) = squaring_chain::>(3, 8); @@ -304,7 +304,7 @@ where pub fn tampered_challenge_is_rejected

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, P::Embedding: Embedding>, { let LogUpInstance { @@ -334,7 +334,7 @@ fn logup_corrupted_verify

( ) -> anyhow::Result<()> where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, P::Embedding: Embedding>, { let LogUpInstance { @@ -359,7 +359,7 @@ where pub fn logup_corruption_is_rejected

() where P: FieldHash, - Standard: Distribution>, + Standard: Distribution> + Distribution>, P::Embedding: Embedding>, { // First lookup sits at w1[1 + table_len]; set it outside `0..table_len`. @@ -404,6 +404,17 @@ macro_rules! roundtrip_suite { $register(); $crate::shared::random_satisfiable_proves_and_perturbation_rejects::<$field>(); } + }; +} + +/// Challenge-bearing roundtrip suite (LogUp + multi-challenge). The witness +/// holds challenge-derived (ext) values, so this requires an `Identity` field +/// where the base and extension fields coincide. +// TODO: support fields whose base and extension differ by drawing the LogUp +// challenges in the base field. +#[macro_export] +macro_rules! challenge_roundtrip_suite { + ($field:ty, $register:path) => { #[test] fn logup_lookup_size_sweep_roundtrip() { $register(); @@ -441,6 +452,14 @@ macro_rules! soundness_suite { $register(); $crate::shared::public_input_binding_mismatch_is_rejected::<$field>(); } + }; +} + +/// Challenge-bearing soundness suite (see [`challenge_roundtrip_suite!`]): +/// requires an `Identity` field where the base and extension fields coincide. +#[macro_export] +macro_rules! challenge_soundness_suite { + ($field:ty, $register:path) => { #[test] fn tampered_public_input_is_rejected() { $register(); diff --git a/tooling/verifier-server/src/services/verification.rs b/tooling/verifier-server/src/services/verification.rs index 7b956b74a..f1b7a6736 100644 --- a/tooling/verifier-server/src/services/verification.rs +++ b/tooling/verifier-server/src/services/verification.rs @@ -89,8 +89,8 @@ impl VerificationService { write_gnark_parameters_to_file( whir_scheme, - &whir_scheme.whir_witness.blinded_commitment, - &whir_scheme.whir_witness.blinding_commitment, + &whir_scheme.whir_witness, + &whir_scheme.whir_blinding, &proof.whir_r1cs_proof, whir_scheme.m_0, whir_scheme.m,