2026-10-14
This monthHalo2 query collision multiopen soundness bug
Produced by the ZK Security research pipeline. Published only after the automated quality gate. The gate is described in the editorial policy.
On 2025-07-09 zkSecurity published a write-up by Suneal Gong of a soundness bug in Halo2 that it named the "query collision bug" ([zkSecurity](https://blog.zksecurity.xyz/posts/halo2-query-collision/)…
On 2025-07-09 zkSecurity published a write-up by Suneal Gong of a soundness bug in Halo2 that it named the "query collision bug" (zkSecurity). The same day, Zcash merged the fix into zcash/halo2 (PR #846) and published halo2_proofs 0.3.1 to crates.io (crates.io).
The bug is not in Halo2's lookup or permutation arguments. It lives one layer lower, in the multipoint opening argument that batches every polynomial evaluation a circuit needs into a single opening proof. If two queries named the same commitment at the same evaluation point but carried different claimed values, one of them was never checked.
What happened
According to zkSecurity, the issue was "present in widely used versions," including the main Zcash implementation and the Privacy & Scaling Explorations (PSE) fork. The team says it first found the bug while auditing a downstream Halo2 fork. After confirming that upstream was affected, it reported the bug to Zcash, PSE, Axiom, ezkl and others. All of them have since patched it. zkSecurity reports that no production deployments were affected (zkSecurity).
Mechanism
A Halo2 circuit is a table of columns. Each column is committed as a polynomial, and a gate reads a column at a rotation (an offset relative to the current row). At proof level, reading column a at Rotation(k) means evaluating its polynomial at ω^k · x, where ω generates the size-2^k evaluation domain and x is the verifier's challenge. The verifier checks the gate identity at x. To get the values it needs, it uses polynomial-commitment openings, batched with the SHPLONK multipoint scheme (zkSecurity; Halo2 book).
zkSecurity describes the batching layer as, conceptually, a map from (Commitment, QueryPoint) to Value. A query collision happens when two independent queries produce the same key with different expected values. In that case "one evaluation silently overwrites the other in the map." The overwritten value still feeds the gate check, but the opening argument never ties it to the commitment (zkSecurity).
The Halo2 frontend already deduplicates identical queries. The post names two ways a collision can still happen:
- Two columns with equal commitments queried at the same point. In the Zcash and PSE code this was not exploitable, because commitments are compared by reference (
std::ptr::eq), not by value. The authors warn this "could become an issue if future implementations change this behavior." - One column queried at two rotations that are equal mod
2^k. Becauseω^(2^k) = 1,Rotation(0)andRotation(2^k)are the same point. The frontend treats them as different rotations and does not merge them. zkSecurity says this case "affects all widely used Halo2 versions" (zkSecurity).
The post's example circuit has 256 rows. An honest "eq gate" constrains advice1[cur] == advice2[cur]. A second "hack eq gate" constrains advice1[cur] == advice2[Rotation(256)]. Rotation 256 wraps around to the current row, so advice2 is queried twice at the same point. Any rotation that is 0 mod 2^k, such as 512 or 2560, would do the same.
Exploit
zkSecurity published a proof-of-concept against a PSE Halo2 checkout (PoC commit). The modified prover works in three steps:
- Compute the honest advice evaluations.
- Compute the quotient value
h(x)that the committed vanishing argument expects. - Adjust the second, colliding evaluation of
advice2by whatever difference makes the gate identity hold.
That second evaluation is dropped in the multipoint opening, so the verifier never notices the change. The result is an accepted proof for an invalid witness (zkSecurity).
Impact
The precondition is a circuit that queries the same column at two rotations that are congruent mod 2^k. zkSecurity calls this an "edge-case" and a "contrived" extra query. For a circuit that has one, soundness is lost completely: in the authors' words, "any Halo2 circuit with a duplicated wrapped query is not sound." Circuits without such a query were not exposed through case 2. Case 1 was blocked in the reviewed implementations by reference comparison (zkSecurity).
Fix
The Zcash fix is in PR #846, titled "Reject two queries with the same point and commitment, but different evaluations." It was opened by str4d on 2025-07-07 and merged on 2025-07-09. The PR changes halo2_proofs/src/poly/multiopen.rs along with its prover and verifier modules. The halo2_proofs changelog entry under [0.3.1] - 2025-07-07, in its Security section, says (CHANGELOG):
- the multiopen logic now detects this case;
- "both provers and verifiers now return an error instead";
- the multiopen logic now uses the
indexmapcrate; - the bug "was found by Suneal from zkSecurity."
crates.io records the 0.3.1 publish time as 2025-07-09T12:31Z (crates.io). zkSecurity says PSE and Axiom also patched their forks, but this article did not trace their fix commits.
What it means for teams shipping circuits
- Upgrade the proving system, not just the circuit. The fix is in the verifier's opening logic. Code that depends on
zcash/halo2needshalo2_proofs≥ 0.3.1. Forks need their own patch, and zkSecurity names PSE, Axiom and ezkl among the teams it notified. - Audit rotations mod domain size. A rotation whose absolute value is ≥
2^kis a signal that a query may alias. The post's general rule:Rotation(a)andRotation(b)refer to the same point whenevera ≡ b (mod 2^k). - Review batching layers in other systems. zkSecurity notes that multipoint opening is widely used, "so similar issues may exist in other frameworks." Any
(commitment, point) → valuemap that silently overwrites entries is a place to look (zkSecurity).
Timeline
| Date | Event |
|---|---|
| Undated | zkSecurity finds the bug while auditing a downstream Halo2 fork, then reports it to Zcash, PSE, Axiom, ezkl and others |
| 2025-07-07 | Zcash PR #846 opened. The changelog dates release 0.3.1 to this day |
| 2025-07-09 | PR #846 merged (12:32Z). halo2_proofs 0.3.1 published to crates.io (12:31Z). zkSecurity publishes the write-up |
Sources
- https://blog.zksecurity.xyz/posts/halo2-query-collision/
- https://github.com/zcash/halo2/pull/846
- https://github.com/zcash/halo2/blob/main/halo2_proofs/CHANGELOG.md
- https://crates.io/api/v1/crates/halo2_proofs/0.3.1
- https://zcash.github.io/halo2/design/proving-system/multipoint-opening.html
- https://github.com/randomsleep/pse-halo2/commit/03020e733eebe22ad92cbee2d45bcca7cc38c97d
Related guides: under-constrained circuits, auditing circom, choosing a proving system, trusted setup risk, the checklist.