feat: BlindFold zero-knowledge protocol#1205
Conversation
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
…oilerplate" This reverts commit dad0a47.
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>
moodlezoup
left a comment
There was a problem hiding this comment.
Some initial comments/questions, still reading 🥵
| // 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()); |
There was a problem hiding this comment.
this feels a little sketchy to me; we should make sure that this is secure
…::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.
markosg04
left a comment
There was a problem hiding this comment.
Initial peek. I am gonna look again later, gonna take a look at dory first then revisit this one.
| /// 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; |
There was a problem hiding this comment.
flagging that we probably now have some redundancy / inconsistency with msm abstractions (SmallScalar, msm module, arkworks itself)
| #[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>; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
I'm not sure what you mean. I feature gated everything ZK related.
…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.
…a reconstruction" This reverts commit edf4141.
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
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
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.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.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) + Ethat hides the real witness.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.Hyrax-style openings: The witness
Wand errorEare arranged asR'×Cgrids. Pedersen row commitments enable evaluation proofs at arbitrary points without revealing the full vectors. Combined rows (Σᵢ eq(r,i)·row_i) are opened to verifyW(ry)andE(rx).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
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 gadgetspoly/commitment/pedersen.rs— Pedersen commitment scheme for small vectorscurve.rs—JoltCurve/JoltGroupElementtraits abstracting elliptic curve operationsChanges across existing modules
sumcheck.rs/univariate_skip.rs—prove_zk/verify_zkvariants that commit coefficients and collect BlindFold stage datazkvm/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 propagationpoly/opening_proof.rs—ProverOpeningAccumulatorextended with ZK stage data collectionzkvm/submodules —SumcheckInstanceParamstrait implementations for BlindFold constraint generationTest 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