Skip to content

feat: BlindFold zero-knowledge protocol#1205

Merged
0xAndoroid merged 100 commits intomainfrom
feat/zk
Mar 3, 2026
Merged

feat: BlindFold zero-knowledge protocol#1205
0xAndoroid merged 100 commits intomainfrom
feat/zk

Conversation

@0xAndoroid
Copy link
Copy Markdown
Collaborator

@0xAndoroid 0xAndoroid commented Jan 15, 2026

Summary

Adds native zero-knowledge to Jolt via the BlindFold protocol — a folding-based scheme that makes all sumcheck proofs ZK without SNARK composition (no Groth16/Plonk wrapper needed).

Approach

Instead of revealing sumcheck round polynomial coefficients in the clear, the prover sends Pedersen commitments to them. The sumcheck verifier's algebraic checks (round consistency, degree bounds, final claim binding) are encoded into a small verifier R1CS circuit. A single Nova fold + Spartan proof over this R1CS proves all sumcheck rounds were executed correctly without revealing the witness.

Protocol overview

  1. ZK sumcheck rounds: Each sumcheck round commits polynomial coefficients via Pedersen (C = Σ cᵢGᵢ + rH) instead of sending them in the clear. Fiat-Shamir challenges are derived from commitments.

  2. Verifier R1CS construction: Sumcheck verification logic — round consistency (g(0)+g(1)=claim), evaluation chaining (g(rⱼ)=next_claim), uni-skip power-sum constraints, final output bindings, and input claim derivations — is encoded as sparse R1CS constraints. Public inputs (challenges, initial claims) are baked into matrix coefficients so they don't appear in the witness.

  3. Nova folding: The real R1CS instance (from actual sumcheck data) is folded with a randomly-sampled satisfying instance. The cross-term T = (AZ₁)∘(BZ₂) + (AZ₂)∘(BZ₁) − u₁(CZ₂) − u₂(CZ₁) is committed row-wise. Folding produces a relaxed R1CS instance (A·Z)∘(B·Z) = u·(C·Z) + E that hides the real witness.

  4. Spartan over folded R1CS: An outer sumcheck proves Σ_x eq(τ,x)·[Az(x)·Bz(x) − u·Cz(x) − E(x)] = 0, followed by an inner sumcheck reducing the linear claim to a single witness evaluation.

  5. Hyrax-style openings: The witness W and error E are arranged as R'×C grids. Pedersen row commitments enable evaluation proofs at arbitrary points without revealing the full vectors. Combined rows (Σᵢ eq(r,i)·row_i) are opened to verify W(ry) and E(rx).

  6. Dory ZK evaluation commitments: The Dory PCS opening proof produces an evaluation commitment y_com (Pedersen commitment to the claimed evaluation) instead of a cleartext value. BlindFold's extra constraints verify this binding.

Key design decisions

  • No composition overhead: Unlike Groth16/Plonk wrapping, BlindFold operates at the same algebraic level as Jolt's existing sumcheck machinery.
  • Baked public inputs: Fiat-Shamir-derived values are embedded in R1CS matrix coefficients rather than witness variables, keeping the witness vector minimal.
  • Commitment reuse: Sumcheck round commitments double as W row commitments for the Hyrax grid, avoiding redundant group operations.
  • Relaxed R1CS: Nova-style relaxation (u·Cz + E) enables folding without cross-term explosion.

New modules

  • subprotocols/blindfold/ — R1CS builder, Nova folding, Spartan prover/verifier, Hyrax openings, witness assignment, output/input constraint gadgets
  • poly/commitment/pedersen.rs — Pedersen commitment scheme for small vectors
  • curve.rsJoltCurve/JoltGroupElement traits abstracting elliptic curve operations

Changes across existing modules

  • sumcheck.rs / univariate_skip.rsprove_zk / verify_zk variants that commit coefficients and collect BlindFold stage data
  • zkvm/prover.rs / verifier.rs — BlindFold integration at Stage 8 (after all sumcheck stages complete)
  • dory/commitment_scheme.rs — ZK evaluation commitments (y_com) and blinding propagation
  • poly/opening_proof.rsProverOpeningAccumulator extended with ZK stage data collection
  • Various zkvm/ submodules — SumcheckInstanceParams trait implementations for BlindFold constraint generation

Test plan

  • cargo nextest run -p jolt-core muldiv --cargo-quiet (e2e correctness)
  • cargo nextest run -p jolt-core blindfold --cargo-quiet (protocol unit tests)
  • cargo clippy --all --features allocative,host -- -D warnings

Signed-off-by: Andrew Tretyakov <42178850+0xAndoroid@users.noreply.github.com>
Signed-off-by: Andrew Tretyakov <42178850+0xAndoroid@users.noreply.github.com>
Signed-off-by: Andrew Tretyakov <42178850+0xAndoroid@users.noreply.github.com>
Signed-off-by: Andrew Tretyakov <42178850+0xAndoroid@users.noreply.github.com>
Signed-off-by: Andrew Tretyakov <42178850+0xAndoroid@users.noreply.github.com>
- Add BlindFoldProof to JoltProof with initial_claims for all 6 stages
- Implement prove_blindfold() in prover after stage 6 sumcheck
- Implement verify_blindfold() with full verification:
  - Initial claims match between JoltProof and BlindFoldProof
  - Commitment openings (W and E)
  - R1CS satisfaction of folded witness
- Support multiple independent chains in R1CS (one per Jolt stage)
  via StageConfig.starts_new_chain flag
- Add deterministic Pedersen generator creation for consistent
  generators between prover and verifier
- Update BatchedSumcheck::prove/prove_zk to return initial claims
- Add hash_to_g1 to JoltCurve trait with try-and-increment implementation
  for BN254, ensuring blinding generator has unknown discrete log
- Bind real_instance to Fiat-Shamir transcript before deriving challenge
  to prevent adaptive attacks
- Convert debug_assert to assert for commitment verification in prover
- Replace duplicate mock_generators with PedersenGenerators::deterministic()
  across 4 test modules (folding, protocol, relaxed_r1cs, prover)
- Remove unused verify_cross_term, alloc_var, alloc_vars functions
- Inline verify_folding_preserves_satisfaction into its test
- Move verify_commitment_opening inside test module

Net reduction of ~110 lines.
SECURITY: Previously, BlindFold replayed challenges from a fresh transcript
using only polynomial coefficients, which produced challenges unrelated to
those derived by the verifier from the main transcript. This meant BlindFold
was proving an R1CS instance with arbitrary challenges, not the actual
sumcheck challenges.

Fix:
- Prover: Pass actual sumcheck challenges from prove_stageN to prove_blindfold
- prove_blindfold: Use passed challenges instead of replaying from fresh transcript
- Verifier: Collect challenges from verify_stageN and compare against BlindFold
  proof's public inputs to ensure they match

Also removes unused to_z_vector function with incorrect Z vector layout.

Remaining issues deferred to separate PRs:
- ZK sumcheck commitments are not opened/verified
- Uni-skip first rounds are not included in BlindFold
Document two remaining security issues for future work:

1. TODO(#ZK-SUMCHECK): Pedersen commitments in ZK sumcheck are used for
   Fiat-Shamir but never opened/verified, allowing challenge manipulation.

2. TODO(#UNI-SKIP): Stages 1-2 uni-skip rounds reveal full polynomials
   without commitments and are not included in BlindFold R1CS.
ZK sumcheck proofs now contain only Pedersen commitments and polynomial
degrees - coefficients are never exposed to the verifier. This ensures
the zero-knowledge property where the verifier learns nothing beyond
validity.

Changes:
- Split SumcheckInstanceProof into Standard (coefficients visible) and
  Zk (only commitments) variants with proper enum dispatch
- Store coefficients and blindings in ProverOpeningAccumulator's new
  ZkStageData for BlindFold to retrieve during prove_blindfold
- Add poly_degrees field to ZkSumcheckProof so verifier can construct
  correct R1CS structure without seeing actual coefficients
- Update verifier to use poly_degrees instead of hardcoded default
- Update tests to handle both proof variants

Security: Coefficients are verified indirectly through BlindFold's R1CS
constraints which prove committed values satisfy sumcheck relations.
Stages 1-2 previously revealed full polynomial coefficients in the
transcript via prove_uniskip_round(). This broke the ZK property.

Changes:
- Add ZkUniSkipFirstRoundProof with Pedersen commitment to coefficients
- Add UniSkipFirstRoundProofVariant enum (Standard/Zk) for proof types
- Extend BlindFold R1CS to verify uni-skip sum constraints via power sums
- Store uni-skip data in accumulator for BlindFold verification
- Update prover to use prove_uniskip_round_zk() in ZK mode
- Update verifier to defer uni-skip verification to BlindFold
- Add unit tests for uni-skip R1CS constraint satisfaction
prove_zk was missing the finalize() loop that exists in standard prove,
which could cause incorrect state for sumcheck instances with deferred
operations (e.g., flushing delayed bindings).
Placeholder function that was never called. Actual stage configs are
built dynamically in verify_blindfold.
Append input claims to transcript BEFORE deriving batching coefficients,
matching the ordering in BatchedSumcheck::prove. The previous inverted
ordering would cause transcript desync between prover and verifier.
Fixes soundness vulnerability where prover could commit to compressed
coefficients [c0, c2, c3] but use a different c1 in BlindFold witness.

Changes:
- Commit to full coefficients [c0, c1, c2, c3] instead of compressed
- Separate uni-skip and regular rounds into independent chains (9 total)
  since batching makes their claims incompatible for direct chaining
- Add round commitment verification in BlindFold verifier
- Cross-check round commitments match between sumcheck proofs and BlindFold

The original code reconstructed c1 from claimed_sum, which masked the
mismatch between uni-skip output and batched ZK sumcheck initial claim.
With full coefficients, c1 is fixed, exposing the chain structure bug.
Add verification that coefficients in witness W match round_coefficients
stored separately for commitment opening. This binds the R1CS constraints
to the committed values.

Changes:
- Add verify_round_coefficients_consistency to RelaxedR1CSWitness
- Call consistency check in BlindFold verifier after commitment openings
- Fix sample_random_satisfying_pair to generate W with proper structure
  (coefficients, intermediates, next_claim per round) so that
  round_coefficients extracted from W match the committed values

The previous implementation generated round_coefficients independently
of W in the random satisfying pair, breaking consistency after folding.
Signed-off-by: Andrew Tretyakov <42178850+0xAndoroid@users.noreply.github.com>
The BlindFold R1CS witness assignment was leaving auxiliary variables
as zeros for general sum-of-products constraints. R1CS only verifies
values, it doesn't compute them - so aux vars must be computed during
witness assignment.

Changes:
- Add compute_aux_vars() to witness.rs that calculates intermediate
  product values matching R1CS allocation order
- Add output_claim_constraint() to verifier sumcheck instances
  (ValEvaluationSumcheckVerifier, ValFinalSumcheckVerifier,
  RegistersReadWriteCheckingVerifier)
- Update verifier to collect and batch constraints from instances
- Re-enable prover final_output constraint code
- Clean up debug prints and unused imports
Move constraint challenge values (batching coefficients, gammas, eq_evals)
from witness to public inputs in BlindFold R1CS. Add explicit verification
in verify_blindfold that compares values in the proof against transcript-
derived expected values.

Changes:
- r1cs.rs: Allocate constraint_challenge_vars as public inputs
- witness.rs: Assign challenge values to public input section
- folding.rs: Exclude challenge vars from witness allocation
- protocol.rs: Update FinalOutputInfo to exclude challenge vars
- verifier.rs: Compute batching coefficients and verify all constraint
  challenge values match between proof and verifier computation
…stages

Add output_claim_constraint() and output_constraint_challenge_values()
to all sumcheck prover instances to match their verifier counterparts.
This enables BlindFold R1CS verification by ensuring prover and verifier
produce identical constraint structures and challenge values.

Provers updated:
- BooleanitySumcheckProver
- RamRaVirtualSumcheckProver
- InstructionRaSumcheckProver
- BytecodeReadRafSumcheckProver
- HammingBooleanitySumcheckProver
- And various claim reduction/evaluation provers

Also removes debug eprintln! statements and fixes clippy warnings.
Add input constraint support to BlindFold R1CS, mirroring output constraints.
This enables verification that input claims are correctly derived from
polynomial openings and challenges.

- Add InputClaimConstraint type alias and trait methods
- Extend ZkStageData with input constraint fields
- Add initial_input to StageConfig with builder method
- Implement input constraints for Registers and InstructionLookups sumchecks
- Integrate input constraint collection in prover
Introduce jolt-core-macros crate with #[sumcheck_claims] proc macro that
generates input_claim, input_claim_constraint, input_constraint_challenge_values,
expected_output_claim, output_claim_constraint, and output_constraint_challenge_values
methods from a declarative DSL.

Applied to registers.rs and instruction_lookups.rs, eliminating ~220 lines of
boilerplate while maintaining the same semantics. Complex sumchecks (increments,
ram_ra, hamming_weight, advice) retain manual implementations due to their
specialized patterns.
…dd product_of

- Remove ClaimSpec::None and ClaimSpec::Manual variants (all sumchecks
  now require full specification)
- Add product_of field to ClaimSpecContent for sum-of-products pattern
  (e.g., Σ_i coeff_i * ∏_j opening_{f(i,j)})
- Move output methods (expected_output_claim, output_claim_constraint,
  output_constraint_challenge_values) from SumcheckClaims trait into
  SumcheckInstanceParams trait with default implementations
- Remove SumcheckClaims trait entirely
- Update macro codegen to generate single SumcheckInstanceParams impl
- Remove SharedStreamingSumcheckState trait, use SumcheckInstanceParams directly
- Make get_params() required on SumcheckInstanceProver/Verifier traits
- Delegate constraint methods to params instead of providing defaults
- Remove OuterStreamingProverParams, reuse OuterRemainingSumcheckParams
- Make StreamingSumcheck Allocative impl feature-gated
Signed-off-by: Andrew Tretyakov <42178850+0xAndoroid@users.noreply.github.com>
Move output_claim_constraint, output_constraint_challenge_values,
input_claim_constraint, and input_constraint_challenge_values from
SumcheckInstanceProver/Verifier traits to SumcheckInstanceParams.

Call sites updated to use get_params().method_name(). BytecodeReadRaf
stores bound values before clearing polys for prover compatibility.
Signed-off-by: Andrew Tretyakov <42178850+0xAndoroid@users.noreply.github.com>
@0xAndoroid 0xAndoroid requested a review from markosg04 February 16, 2026 01:19
Copy link
Copy Markdown
Collaborator

@moodlezoup moodlezoup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments/questions, still reading 🥵

Comment thread book/src/how/blindfold.md Outdated
Comment thread book/src/how/blindfold.md
Comment thread book/src/roadmap/zk.md Outdated
Comment thread jolt-core/src/poly/commitment/dory/commitment_scheme.rs Outdated
Comment thread jolt-core/src/poly/opening_proof.rs
Comment thread jolt-core/src/subprotocols/blindfold/protocol.rs Outdated
Comment thread jolt-core/src/subprotocols/blindfold/protocol.rs Outdated
Comment thread jolt-core/src/subprotocols/blindfold/spartan.rs Outdated
Comment thread jolt-core/src/subprotocols/blindfold/protocol.rs
Comment thread jolt-core/src/subprotocols/blindfold/output_constraint.rs Outdated
Comment thread jolt-core/src/subprotocols/blindfold/r1cs.rs Outdated
Comment on lines +222 to +224
// In ZK mode, don't absorb cleartext claims — polynomial commitments provide binding.
// Batching coefficients are still unpredictable (from transcript state after commitments).
let batching_coeffs: Vec<F> = transcript.challenge_vector(sumcheck_instances.len());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels a little sketchy to me; we should make sure that this is secure

Comment thread jolt-core/src/curve.rs Outdated
Comment thread jolt-core/src/poly/opening_proof.rs Outdated
Comment thread jolt-core/src/zkvm/verifier.rs Outdated
Comment thread jolt-core/src/zkvm/verifier.rs Outdated
…::verify

Eliminates 7 transcript clone-and-peek blocks in the verifier by having
BatchedSumcheck::verify return the batching coefficients it already computes.
Replace with check_satisfaction().unwrap() — same logic, but reports
the failing constraint index on panic.
…ccumulator

Move ZK-specific data (ZkStageData, UniSkipStageData, Stage8ZkData) out of
ProverOpeningAccumulator into a dedicated BlindFoldAccumulator<F, C>. Stores
Pedersen commitments as native C::G1 curve points instead of serialized bytes,
eliminating pointless serialize/deserialize round-trips in prove_blindfold.
…ormRand

Seed ChaCha20Rng from SHA3-256(domain) and delegate to G1Projective::rand,
which uses the same try-random-x approach internally. Removes ~40 lines of
manual BN254 curve arithmetic.
Copy link
Copy Markdown
Collaborator

@markosg04 markosg04 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial peek. I am gonna look again later, gonna take a look at dory first then revisit this one.

Comment thread book/src/how/blindfold.md Outdated
Comment thread book/src/how/blindfold.md Outdated
Comment thread jolt-core/src/curve.rs
Comment on lines +81 to +85
/// Multi-scalar multiplication in G1: Σᵢ scalars[i] * bases[i]
fn g1_msm<F: JoltField>(bases: &[Self::G1], scalars: &[F]) -> Self::G1;

/// Multi-scalar multiplication in G2: Σᵢ scalars[i] * bases[i]
fn g2_msm<F: JoltField>(bases: &[Self::G2], scalars: &[F]) -> Self::G2;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flagging that we probably now have some redundancy / inconsistency with msm abstractions (SmallScalar, msm module, arkworks itself)

Comment thread jolt-core/src/curve.rs Outdated
Comment thread jolt-core/src/zkvm/prover.rs Outdated
Comment thread jolt-core/src/subprotocols/sumcheck.rs Outdated
Comment on lines +58 to +67
#[cfg(feature = "zk")]
fn input_claim_constraint(&self) -> InputClaimConstraint;

#[cfg(feature = "zk")]
fn input_constraint_challenge_values(&self, accumulator: &dyn OpeningAccumulator<F>) -> Vec<F>;

#[cfg(feature = "zk")]
fn output_claim_constraint(&self) -> Option<OutputClaimConstraint>;
#[cfg(feature = "zk")]
fn output_constraint_challenge_values(&self, _sumcheck_challenges: &[F::Challenge]) -> Vec<F>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems that sometimes we have some things feature gated and others don't. Is this because of some weird clippy complaints about dead code sometimes when things are not re-exported?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean. I feature gated everything ZK related.

Comment thread jolt-core/src/subprotocols/sumcheck.rs Outdated
Comment thread jolt-core/src/subprotocols/blindfold/output_constraint.rs
Comment thread jolt-core/src/subprotocols/blindfold/mod.rs
…eningProof

BlindFoldProof: replace 5 random_* fields with `random_instance: RelaxedR1CSInstance`,
replace w/e combined_row + combined_blinding pairs with `w_opening`/`e_opening: HyraxOpeningProof`.

Move Hyrax free functions (combined_row, evaluate, combined_blinding) to
poly/commitment/hyrax.rs and remove the now-dead HyraxParams wrapper methods.
…tion

Simplify expected_claim() to take full (az_r, bz_r, cz_r) evaluations
instead of witness-only portions, and call it from the verifier instead
of inlining the formula.
Resolve conflicts in 7 files, updating BlindFold ZK constraints
for main's API changes:

- RamValCheck: merged ValEvaluation + ValFinal into single
  gamma-batched sumcheck; updated advice constraints accordingly
- InstructionClaimReduction: added γ³·LeftInstructionInput +
  γ⁴·RightInstructionInput terms to input/output constraints
  (matching main's PR #1264 instruction input folding)
- InstructionInput: simplified from 4-claim to 2-claim constraints
  (stage-1 claims moved to InstructionClaimReduction)
- Removed stale transcript params from append_virtual calls
  (feat/zk removed transcript from opening accumulator API)
`ArkworksProverSetup::new_from_urs` is gated behind
`not(target_arch = "wasm32")` in dory-pcs. Fall back to
`ArkworksProverSetup::new` (no disk cache) on wasm targets.
- Remove unused `from_dory_generators` from pedersen.rs
- Delete ~25 AI-meta-comments across sumcheck.rs, prover.rs, verifier.rs
- Fix blindfold.md: clarify ZK advantage over sigma protocols, clarify "cache" terminology
…o-curve

Pedersen generators for ZK sumcheck commitments now come from the Dory
URS (g1_vec for message generators, h1 for blinding) rather than an
independent hash-to-curve derivation. This ensures all curve points in
the proof system share the same trusted setup.

- Add pedersen_generators/pedersen_generators_verifier to ZkEvalCommitment trait
- Prover slices generators from ProverSetup.g1_vec
- Verifier reconstructs generators from deterministic Dory URS seed
- Remove hash_to_g1 from JoltCurve trait (now test-only in pedersen.rs)
- Feature-gate pedersen_generators field behind cfg(feature = "zk")
…truction

DoryVerifierSetup wraps ArkworksVerifierSetup and caches g1_vec under
the zk feature. setup_verifier copies g1_vec from ProverSetup directly.
pedersen_generators_verifier now slices from the cached g1_vec instead
of re-deriving generators from the URS seed on every call.
Strip Dory URS to 128 ZK Pedersen generators and store on
JoltSharedPreprocessing. Both prover and verifier now read
generators from the same source — eliminates ChaCha20Rng
reconstruction on verifier side.

- Add `zk_generators_raw()` to CommitmentScheme trait
- Populate generators in JoltProverPreprocessing::new()
- Remove unused `pedersen_generators_verifier` from ZkEvalCommitment
OUTER_FIRST_ROUND_POLY_NUM_COEFFS = 28 is the actual max,
next_power_of_two gives 32. Saves 9 KB on shared preprocessing.
Prover now reads Pedersen generators from shared preprocessing
(populated from Dory URS at JoltProverPreprocessing::new time).

Remove max_pedersen_generators and pedersen_generators from
ZkEvalCommitment trait — both prover call sites now use
shared.pedersen_generators::<C>(count) instead.

Revert generator count back to 128: the bottleneck is output_claims
(stage 6 has ~83 claims with log_k_chunk=4), not round poly coeffs.
Adapt to API changes: Polynomial::commit now takes Mode type param
and returns commit_blind; prove() dropped RNG arg, takes commit_blind;
setup constructors take only max_log_n; commit_zk merged into commit.
Dense polynomials (RdInc, RamInc) were replicated K times in the Dory
matrix to match one-hot polynomial dimensions. This is unnecessary —
zero-padding is correct and simpler.

- Remove DoryHintPad enum, simplify DoryOpeningProofHint to newtype
- combine_hints: always zero-pad instead of replicate/zero-pad
- aggregate_chunks: remove dense replication
- Streaming VMP: use row_weight instead of row_factor for dense polys
- Always apply lagrange_factor via EqPolynomial::zero_selector for
  dense claims in prover/verifier
@0xAndoroid 0xAndoroid merged commit 62e9f94 into main Mar 3, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants