feat: commit the R1CS witness over the base field (proof format 2.0)#470
Conversation
FIELD_ID tags each field's on-disk proof so one field's proof is rejected by another field's verifier; bn254 reserves 0 for legacy byte-identity. The 2.0 major bump makes an incompatible WhirR1CSScheme/WHIR payload fail fast on read instead of mis-deserializing.
…per field Commit the witness over the base field (whir_witness: GenericWhirConfig <P::Embedding>) and move the Spartan blinding polynomial g to a separate extension-field commitment (whir_blinding). g masks ext-valued sumcheck rounds, so it needs ext randomness and cannot be embedded in a base witness vector; sizing it to 4*m_0 coefficients drops the old m-padding. Make the covector eval helpers embedding-aware (base×ext mixed arithmetic) since the polynomial is now base while eval points are ext. Fold ProofField::FIELD_ID into the .np magic (bn254 id 0 unchanged) so a proof from one field fails another field's format check.
Mirror the scheme split: commit the base-field witness with the non-ZK whir_witness config (no more base->ext lift) and commit the Spartan blinding g with the separate ext whir_blinding config. BlindingState carries g's flattened vector plus its own WHIR witness instead of an offset into the witness padding. The sumcheck sources blinding_eval from the standalone g vector (offset 0), and g is opened by a dedicated whir_blinding.prove after all witness opens, replacing the OffsetCovector-into-the-witness-open. Eval helpers take the embedding for base x ext mixed products.
Receive the ext blinding commitment right after the witness commitment (before challenge sampling), matching the prover's transcript order. Verify the witness with the two-step whir::Config API (verify -> final-claim check) and drop the blinding covector that was mixed into the witness open. Verify g by a dedicated whir_blinding open at the end, against the power covector at offset 0.
GoldilocksField switches its embedding from Identity<Field64_3> to Basefield<Field64_3> (FIELD_ID 1): the witness commits in Field64, with Field64_3 only for challenges and sumcheck — the base-commit path. Add GoldilocksEfField (Identity<Field64_3>, FIELD_ID 2) as a stand-in for challenge-bearing fixtures whose witness holds ext values a base witness can't. Its distinct FIELD_ID reflects the different base byte layout, and its TODO records the concrete removal path (base transcript codec + k-fold repetition for 128-bit soundness). Factor the shared FieldHash glue into a macro over both fields.
Drop the zkWHIR config (WhirZkConfig) — bn254 now uses the same non-ZK base-commit path as every field, with Identity<Fr> so base == ext (no actual base savings, no witness hiding). FIELD_ID 0 keeps the .np format magic byte-identical. mavros_prove sources blinding_eval from the separate g vector, matching the prover.
cli and verifier-server pass whir_witness + whir_blinding to the gnark parameter writer instead of the removed zkWHIR blinded/blinding commitments. The r1cs-compiler security tests check the two new configs (new_witness_config_for_size + new_blinding_config_for_size), and the dimension test drops to m=13 (the blinding-room padding bump is gone).
WhirR1CSProver now requires Standard: Distribution<Base<P>>; add it to the harness runners that prove.
Split the roundtrip/soundness macros into base-compatible and challenge-bearing suites. GoldilocksField runs the base suites (real base-leaf commitment); GoldilocksEfField runs the challenge suites (ext witness holds challenge values). bn254 runs both on itself. Thread the base-sampling bound through the shared helpers.
Complete the base-sampling bound on `tampered_public_input_is_rejected` with `Distribution<Base<P>>` — the merge left it with only the ext bound, but the generic test body commits in the base field via `prove`. Rewrap the `np_format` doc comment to satisfy rustfmt. Reuse the `next_power_of_two` helper for `nv_blind` instead of a re-implemented ceil-log2, and describe `GoldilocksEfField` by its purpose (identity-embedding Goldilocks for challenge-bearing circuits).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
CSP benchmarks
Prover time, peak RSS, peak heap, and verifier time are arithmetic means across the iterations. Peak heap comes from the largest Each metric cell shows the current value followed by the percentage delta against the latest successful Results
|
zkfriendly
left a comment
There was a problem hiding this comment.
PR looks great!
One thing to bring up is the fact that bn254 path is no longer zk (I understand this is because zook not landed yet). But I was wondering, even with zook, it's worth considering making zk optional (there are use cases that don't need it, and this can make the prover faster) and being very explicit regarding whether zk is enabled or not. Currently, it is a bit odd that proof version 2 is NOT zk but 1* is.
- test(r1cs-compiler): assert 128-bit WHIR security for Goldilocks too, not just bn254. Genericize assert_configs_secure over the field and pull in provekit-backend-goldilocks as a dev-dependency (no dependency cycle). - feat(gnark): warn when writing gnark parameters in the 2.0 split witness/blinding layout, which the Go recursive-verifier does not yet parse (it deserializes without error but fails verification silently). - docs(common): spell out the WhirR1CSScheme ZK posture (sumcheck-ZK on, witness-ZK off) and drop the stale zkWHIR naming in the gnark config.
|
minor: Field-tag / version check is binary-only The PR description says proofs are "tagged per field so one field's proof is rejected by another field's verifier," and that the 2.0 bump "fails fast." That's true on the binary path: A cross-field or downgraded |
zkfriendly
left a comment
There was a problem hiding this comment.
Follow-ups would be to add optional zk. WHIR is already making this optional with an upcoming PR.
Confirmed, and it applies to all four Two things I checked. A 1.x JSON proof deserializes cleanly into 2.0, because Both still fail in transcript replay, so nothing unsound gets through. It just fails after the full verification run, with an error that points somewhere else. Added a |
The suite split put `tampered_public_input_is_rejected` in `challenge_soundness_suite!`, so it only ran on `GoldilocksEfField` and the base-commit witness path lost its dedicated public-input tamper test. It uses `squaring_chain` and `two_public_inputs` and carries no `Embedding<Target = Base<P>>` bound, unlike the two tests it was grouped with, so it belongs in `soundness_suite!`. Moving it back gives `GoldilocksField` both halves of the public-input pair: the hash-guard rejection and the binding-covector rejection.
`np_format` derives the `.np` magic by offsetting the final byte with `FIELD_ID`, which reserves all 256 values of that byte for `.np`. Nothing recorded or enforced that reservation. Assert it at compile time against the other three format constants, so a field whose magic would collide is a build error rather than a file one reader silently accepts as another. Offsetting one byte is a bijection on `u8`, so distinct ids always give distinct magics and no field can collide with another. What `np_format` cannot see, working one field at a time, is two backends declaring the same `FIELD_ID`. `field_tags.rs` is the only place all three fields are in scope together, so it asserts the ids and magics are pairwise unique. Reading `FORMAT` there also const-evaluates the new assert for the non-bn254 fields, which nothing else in the workspace does.
The magic, format, major/minor, and per-field checks live in the binary header, so `read_json` accepts a downgraded or cross-field artifact and it fails much later in Fiat-Shamir transcript replay, with an error that points somewhere else. Warn on the JSON read path so nothing trusts an untagged artifact silently. Tagging JSON properly is a breaking schema change to the verifier-server `/verify` endpoint and the wasm package; tracked in #472.
f74f0db to
04e93d6
Compare
Summary
Commits the R1CS witness over the base field and moves the Spartan blinding polynomial
gto a separate extension-field commitment, instead of committing the whole witness (padded with the blinding coefficients) in the extension field. The proof format is bumped to 2.0 and tagged per field so one field's proof is rejected by another field's verifier. bn254 and Goldilocks both ride the new uniform non-ZK base-commit path.Builds on the field-generic spine + two-backend work (#458/#459/#460). Independent of the CI guard PR (#469).
Why
Goldilocks is a 64-bit prime: its whole value is that committed data (witness, matrix coefficients) stays in the base field
Field64, with only challenges/sumcheck in the degree-3 extensionField64_3. The old scheme committed the witness in the extension and paddedw1with room for the blinding polynomial — that forces base data through extension-width commitments and couples witness sizing to blinding room. Splitting the two commitments lets the witness commit natively in the base field and sizes the blinding commitment to just its4 * m_0cubic coefficients.What changed
provekit-common—WhirR1CSSchemegainswhir_witness: GenericWhirConfig<P::Embedding>(base-field, non-ZK) and a separatewhir_blinding: GenericWhirConfig<Identity<Ext<P>>>; newnew_witness_config_for_size/new_blinding_config_for_size/whir_protocol_params; the old4 * m_0w1-padding is gone..npformat magic is derived fromProofField::FIELD_ID(np_format); all format versions bump to2.0(major must match on read). Covector eval helpers are embedding-aware (base × ext).provekit-prover— commits the base-field witness withwhir_witness(no base→ext lift) and commitsgwith the separatewhir_blinding;BlindingStatecarriesg's own vector + WHIR witness;gis opened by a dedicatedwhir_blinding.proveafter the witness opens.provekit-verifier— receives the ext blinding commitment right after the witness commitment (matching prover transcript order), verifies the witness with the two-stepwhir::ConfigAPI, and verifiesgby a dedicated open at the end.Identity<Fr>,FIELD_ID 0,.npmagic byte-identical); Goldilocks switches toBasefield<Field64_3>(FIELD_ID 1). AddsGoldilocksEfField(Identity<Field64_3>,FIELD_ID 2) as a stand-in for challenge-bearing fixtures.whir_witness+whir_blindingto the gnark writer; r1cs-compiler security tests check the two new configs (dimension test drops tom = 13); fixtures route base vs challenge suites to the right field.Caveats
GoldilocksEfFieldis a stand-in. A singleField64challenge is only ~64-bit sound, so challenge-bearing fixtures run on the ext-leaf field until a base transcript codec + k-fold repetition land (see theTODOingoldilocks/src/field.rs).whir_witnessTODO).Out of scope (tracked separately)
This PR changes only the Rust WHIR proving/verifying path. It intentionally does not touch:
recursive-verifier/(which still parses the old fused zkWHIR commitment layout), orprovekit-gnarkbridge / Groth16 prover path.Consequence: the WHIR →
recursive-verifier(Go) → Groth16 on-chain route will not verify 2.0 proofs until a paired Go update lands, tracked as follow-up. The direct Groth16+BSB22 backend (#427 / #447) is a separate crate and is unaffected by this change (the two only share a few tooling files). This keeps the PR focused on the Rust proof-system change; the recursive-verifier port is deliberately left for a coordinated follow-up.Verification
cargo fmt --all --check,cargo clippy --all-targets --all-features(no new errors),cargo check --all-targets --all-featuresgreen.provekit-r1cs-compiler41 tests pass (security + dimension, split config).Commits
9 authored commits (base-field split across common / prover / verifier / backends / tooling / fixtures) plus one commit reconciling the base-sampling bound and rustfmt onto current
main.