2026-10-14

This month

Zcash Orchard soundness bug: a missing copy constraint in halo2_gadgets variable-base scalar multipl…

Produced by the ZK Security research pipeline. Published only after the automated quality gate. The gate is described in the editorial policy.

On May 29, 2026, Taylor Hornby, an independent researcher auditing the Orchard protocol on behalf of Shielded Labs, found a soundness flaw in the Orchard Action circuit and disclosed it to ZODL engine…

Zcash Orchard soundness bug: a missing copy constraint in halo2_gadgets variable-base scalar multiplication

What happened

On May 29, 2026, Taylor Hornby, an independent researcher auditing the Orchard protocol on behalf of Shielded Labs, found a soundness flaw in the Orchard Action circuit and disclosed it to ZODL engineers that evening (ZF blog). The bug sat in the variable-base scalar-multiplication gadget of the halo2_gadgets crate and had existed since Orchard launched with NU5 on May 31, 2022 (GHSA-ww9q-8r59-xv46). The advisory is rated critical, CVSS 9.3, and assigned CVE-2026-54496 (GHSA-ww9q-8r59-xv46).

The network response came in two steps. First, a soft fork (Zebra 4.5.3, zcashd 6.12.5) rejected all Orchard transactions from mainnet height 3,363,426. Then the NU6.2 hard fork at height 3,364,600 re-enabled Orchard with a corrected circuit (ZF blog; zcashd 6.12.5). The Zebra soft-fork advisory, GHSA-jfw5-j458-pfv6, is referenced in the Zebra 4.5.3 and 5.0.0 release notes (v4.5.3; v5.0.0). On 2026-10-14 its advisory page returned HTTP 404, so this article relies on GHSA-ww9q-8r59-xv46 for technical detail.

The mechanism

Orchard's variable-base scalar multiplication is a double-and-add. Its intermediate stage uses incomplete point-addition formulas for performance, and complete addition is used at the start and end (GHSA-ww9q). In halo2_gadgets/src/ecc/chip/mul/incomplete.rs, the base point's coordinates (x_p, y_p) were placed into the incomplete-addition rows with assign_advice(). That call writes a witness value but adds no constraint linking it to the actual base (GHSA-ww9q).

The q_mul_2 gate did force (x_p, y_p) to stay constant across the loop rows, but nothing tied that constant to the real base (GHSA-ww9q). The fix commit adds that the constancy chain "reached neither the doubling-row nor the complete-addition base anchors". So a prover could run the loop on a free point B′ ≠ base, and the gadget would then output [a] base + [b] B' rather than [scalar] base (halo2 commit d8e48ef).

In Orchard, this gadget enforces diversified-address integrity, pk_d = [ivk] g_d (GHSA-ww9q). Given target pk_d, g_d and ivk, a prover can solve for the base value that makes the equation hold, including for inputs an honest prover could never satisfy (GHSA-ww9q). That check is what binds the Action to the correct ivk. Through ivk = Commit^ivk_rivk(Extract_P(ak^P), nk), it also binds the Action to the correct nullifier key nk and spend validating key ak (GHSA-ww9q).

The advisory describes two exploits:

  • Double-spend. The attacker picks a wrong nk for a note and derives the matching wrong ivk honestly. The free base then satisfies the integrity check anyway. Each distinct nk gives a distinct valid nullifier, so the same note can be spent repeatedly (GHSA-ww9q).
  • Theft. The attacker picks an ak whose private key ask they know, then signs a spend of the victim's note. This requires knowing the note plaintext, for example through the incoming viewing key. The advisory notes this could bypass hardware-wallet protection of spending keys if the linked software wallet were compromised (GHSA-ww9q).

Impact

Integrity impact is high. Confidentiality is not affected: the zero-knowledge property held, which is also why a double-spend leaves no on-chain signature. Double-spend nullifiers cannot be distinguished from honest ones (GHSA-ww9q). Zcash's turnstile, which tracks ZEC across value pools, limited any inflation to value inside the Orchard pool and protected the total supply (GHSA-ww9q). According to the Zcash Foundation, the turnstile "confirmed that the total supply remained intact throughout". It also reports no evidence of unauthorized value creation. User privacy was not affected, and Sapling and transparent transactions kept running (ZF blog). The advisory states there is no evidence the bug was exploited before remediation (GHSA-ww9q).

Earlier farm drafts claimed a "$100M liquidation". None of the primaries listed here mentions any liquidation or dollar loss, so this article does not repeat that claim.

Affected packages per the advisory text: halo2_gadgets < 0.5.0, orchard < 0.14.0, zcash_primitives < 0.28.0, zcashd < 6.20.0, zebrad < 5.0.0 (GHSA-ww9q). The sources disagree on node-version ranges. The advisory's machine-readable metadata lists zebrad <4.5.1, and the ZF post lists zebrad "below v4.5.1" and zcashd "v5.0.0–v6.12.3" (ZF blog).

The fix

The circuit fix makes the first incomplete-addition row use copy_advice() for the base coordinates instead of assign_advice(). With the existing q_mul_2 equality, that constrains every loop row's base to the real base (GHSA-ww9q). The halo2 commit (authored 2026-05-31) adds a CircuitVersion enum with AnchoredBase and InsecureUnanchoredBase variants. The insecure variant exists only to rebuild the historical verifying key so pre-fix proofs can still be verified during sync (halo2 commit d8e48ef). The commit also adds a regression test asserting that the fixed circuit's equality constraints are a superset of the old ones, differing only by the two base anchors (halo2 commit d8e48ef).

Changing the permutation argument changes the pinned verifying key, so a hard fork was required (ZF blog). NU6.2 uses consensus branch ID 0x5437f330 and routes Orchard proofs to per-circuit verifying keys InsecurePreNu6_2 / FixedPostNu6_2 (Zebra v5.0.0). The ZF post explains why the network disabled Orchard first instead of shipping the fix directly: a direct patch "would have revealed too much about the nature of the flaw" (ZF blog).

NU6.2 also added a separate rule rejecting Orchard proofs of non-canonical size. zcashd's notes say that before this, Orchard proofs were not length-checked and could carry arbitrary trailing data that ZIP 317 fees did not account for (zcashd 6.20.0; Zebra v5.0.0).

What it means for circuit teams

The advisory's own guidance for downstream projects: upgrade the affected crates, and audit other assign_advice() uses where a witnessed value is meant to equal a known value. Such sites should use copy_advice() or an explicit constraint (GHSA-ww9q). The bug class is a value that is equal across rows but never anchored to its source. A gate enforcing internal consistency does not by itself enforce correctness.

Timeline

Date (as reported) Event Source
2022-05-31 11:50 MDT NU5 activates; bug introduced GHSA-ww9q
2026-05-29 23:53 MDT Disclosed to ZODL engineers GHSA-ww9q
2026-05-30 06:30 MDT Disclosure acknowledged GHSA-ww9q
2026-05-31 halo2 fix commit authored; private coordination with miners and exchanges begins that evening halo2 commit; ZF blog
2026-06-02 06:25 UTC Zebra 4.5.3 released on GitHub Zebra v4.5.3
2026-06-02 ~02:00 UTC (ZF) / 2026-06-01 ~21:30 MDT (advisory) Soft fork activates at height 3,363,426 after a first attempt "encountered coordination challenges" ZF blog; GHSA-ww9q
2026-06-03 00:05 EDT NU6.2 activates at height 3,364,600; Orchard re-enabled ZF blog; GHSA-ww9q
2026-06-03 21:24 UTC zcashd 6.20.0 released on GitHub zcashd v6.20.0
2026-06-15 GHSA-ww9q-8r59-xv46 published GHSA-ww9q

The two soft-fork activation times differ by about 90 minutes between sources. Zebra 4.5.3's GitHub publish time comes after the ZF-reported activation. Release binaries may have been distributed privately first, but no primary states this.

Sources

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