2026-10-14

This month

Halo2 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:

  1. 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."
  2. One column queried at two rotations that are equal mod 2^k. Because ω^(2^k) = 1, Rotation(0) and Rotation(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:

  1. Compute the honest advice evaluations.
  2. Compute the quotient value h(x) that the committed vanishing argument expects.
  3. Adjust the second, colliding evaluation of advice2 by 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 indexmap crate;
  • 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/halo2 needs halo2_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^k is a signal that a query may alias. The post's general rule: Rotation(a) and Rotation(b) refer to the same point whenever a ≡ 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) → value map 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

Related guides: under-constrained circuits, auditing circom, choosing a proving system, trusted setup risk, the checklist.