Skip to content

fix: bind opening point in ZK evaluation proofs - #23

Merged
markosg04 merged 5 commits into
mainfrom
fix/zk-opening-point-binding
Jul 3, 2026
Merged

markosg04 merged 5 commits into
mainfrom
fix/zk-opening-point-binding

Conversation

@moodlezoup

@moodlezoup moodlezoup commented Jul 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes a critical soundness bug in ZK mode (zk feature): the evaluation opening point was not bound by the verifier. A ZK proof created for a point P verified at any point P', letting a malicious prover open a committed polynomial to a forged evaluation at an attacker-chosen point. Transparent mode was unaffected.

Special thanks to Veria Labs for finding and reporting this vulnerability!

Root cause

The evaluation point reaches the verifier only through the folded scalars s1_acc/s2_acc (accumulated in DoryVerifierState::process_round). The 0.3.0 ZK final check was a collapsed 1-pairing scalar-product argument over the pre-fold statement (C, D₁, D₂) and never read them — it dropped the point-binding terms (the transparent check's Pairs 2 & 3 and the s1_acc·s2_acc·HT term) with no zero-knowledge replacement.

Fix — restore the original Dory construction

Per the Dory paper (eprint 2020/1274, §4.1 Fold-Scalars → §3.1 Scalar-Product):

  • The prover applies the Fold-Scalars reduction to its witness (v₁ += γ·s₁·H₁, v₂ += γ⁻¹·s₂·H₂, r_C += γ·r_E2 + γ⁻¹·r_E1) via the new DoryProverState::apply_fold_scalars, before running the scalar-product Σ-argument. The argument now opens the point-dependent folded statement (C′, D₁′, D₂′).
  • The verifier reconstructs (C′, D₁′, D₂′) from its own point-derived s1_acc/s2_acc and the E-accumulators, batched into a 3-pairing check (mirrors Pairs 1–3 of the transparent check). This pins the point coefficient to the committed witness while v₁[0]/v₂[0] stay hidden.
  • The Σ-proof responses (E₁, E₂, r₁, r₂, r₃) are absorbed into the Fiat–Shamir transcript before the batching challenge d is drawn, and the unconstrained r_final1/r_final2 blinds are removed — closing the degree of freedom a point-agnostic fix would leave exploitable (advisory §5).

Breaking change

DoryProof::final_message is now Option<ScalarProductMessage> — Some in transparent mode (reveals the folded witness), None in ZK (the Σ-proof replaces it; revealing it would break hiding). ZK proofs generated by 0.3.0 no longer verify and must be regenerated. Documented in CHANGELOG.md under [Unreleased]; the version bump is left as a release decision.

Tests

  • examples/zk_wrong_point.rs (advisory PoC): reports wrong_point_accepted=false at every size after the fix (true before).
  • test_zk_wrong_point_rejected: an honest ZK proof is rejected at a shifted and at a random point.
  • test_zk_crafted_blind_shift_wrong_point_rejected: an adversarial wrong-point proof that shifts the Σ-blinds by the exact public point-deltas (§5's attack) is rejected.
  • test_soundness_missing_final_message / test_zk_soundness_unexpected_final_message: the clear final message must be present iff the proof is transparent.
  • Full suite green (97 with zk, 67 transparent), clippy/fmt clean across all feature combos, both proof formats round-trip, and zk_statistical (ZK uniformity + witness-independence, 1000 trials) still passes.

Downstream note

The Jolt-side items in the advisory (jolt-verifier stage 8, jolt-dory, and flipping zk_wrong_point_binding_poc to is_err()) live in a separate repo. When Jolt bumps this dependency, that PoC test should be flipped.

🤖 Generated with Claude Code

ZK mode did not bind the evaluation opening point: a proof created for a
point P verified at any point P'. The point reaches the verifier only
through the folded scalars s1_acc/s2_acc, and the 0.3.0 ZK final check
(a collapsed 1-pairing scalar-product argument over the pre-fold
statement) never read them.

Restore the original Dory construction (eprint 2020/1274): apply
Fold-Scalars (§4.1) to the witness before the Scalar-Product argument
(§3.1), so the argument opens the point-dependent folded statement
(C', D1', D2'). The verifier reconstructs (C', D1', D2') from its own
point-derived s1_acc/s2_acc and the E-accumulators, batched into a
3-pairing check. This pins the point coefficient to the committed
witness while the folded coordinates v1[0]/v2[0] stay hidden.

- Add DoryProverState::apply_fold_scalars (§4.1 witness update; used by
  both modes).
- Absorb the scalar-product responses (e1,e2,r1,r2,r3) into the
  transcript before the batching challenge d, and drop the unconstrained
  r_final blinds (the degree of freedom a point-agnostic fix would leave
  exploitable, advisory §5).
- DoryProof.final_message is now Option: Some in transparent mode
  (reveals the folded witness), None in ZK (the Sigma-proof replaces it).
  Breaking proof-format/API change; 0.3.0 ZK proofs must be regenerated.

Regression tests: honest ZK proof rejected at a wrong point; a crafted
wrong-point proof that shifts the Sigma-blinds by the public point-deltas
is rejected; final message must be present iff transparent. zk_statistical
(ZK/uniformity) still passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@moodlezoup
moodlezoup requested a review from markosg04 July 2, 2026 22:10
@markosg04
markosg04 merged commit 56a7243 into main Jul 3, 2026
9 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.

2 participants