Aztec Plonk C++ Verifier W_z / W_zw Zero / Point-at-Infinity Proof Forgery Near Miss (2021-12-14)

  • Name: Aztec Plonk C++ verifier W_z / W_zw zero / point-at-infinity proof forgery near miss
  • Date: 2021-12-14
  • Protocol: Aztec Plonk C++ verifier
  • Chain: Ethereum ecosystem
  • Category: proof verification failure / malformed curve-point admission / point-at-infinity misclassification / forged-proof near miss
  • Loss: no public onchain theft was documented in the disclosure materials reviewed here, but the reported verifier behavior would let an attacker forge a proof if a deployed application trusted that verifier outcome directly
  • Summary: A public 2021 report on Aztec’s C++ Plonk verifier showed that two prover-controlled opening proof elements, [W_z]_1 and [W_zw]_1, could be set to 0 and still pass verification. The core failure was not a broken Plonk statement; it was an implementation-boundary collapse around malformed curve points, point-at-infinity handling, and later pairing normalization. The verifier initially recognized the zero encoding as not lying on the elliptic curve, but it did not fail closed there. Later verifier logic effectively let the same 0 inputs behave like points at infinity, and batch normalization then collapsed the pairing inputs so both pairing terms evaluated as identity. This is a clean malformed-proof-element incident: the proof system only broke because the verifier let one impossible curve-point encoding survive long enough to acquire a second, more permissive meaning inside later arithmetic.
  • Trigger / entry point:
    • The issue was reported against Aztec’s C++ Plonk verifier implementation rather than a higher-level application wrapper.
    • The report focused on the proof elements [W_z]_1 and [W_zw]_1, which are curve points used inside the verifier’s pairing equation.
    • By manually setting those elements to 0, the reporter could make the verifier accept a forged proof regardless of the other proof contents.
    • The public disclosure artifact was Nguyen Thoi Minh Quan’s December 2021 repository and accompanying PDF writeup, later summarized by 0xPARC’s zk bug tracker.
  • Root cause:
    • The verifier did not maintain one consistent admission rule for malformed curve-point encodings.
    • A value of 0 was recognized as not lying on the elliptic curve, but that failed check did not terminate verification immediately.
    • Later code paths effectively interpreted the same 0 value as the point at infinity, which is a neutral element in elliptic-curve arithmetic.
    • Public secondary analysis also highlights two additional implementation hazards that made the bug cash out inside verifier math rather than dying as an input-validation error: an infinity check that misclassified the manipulated point and a zero-inversion / batch-normalization path that let one bad projective point corrupt another pairing input.
    • Once that malformed value crossed from invalid point into acceptable infinity-like point, the verifier’s pairing logic no longer enforced the statement it was supposed to enforce.
    • The implementation therefore violated the basic invariant that proof-supplied curve points must have one unambiguous encoding and must be rejected before any pairing arithmetic or normalization if they are malformed.
  • Failure path:
    1. The attacker prepares a Plonk proof for the vulnerable C++ verifier.
    2. They replace the opening-proof elements [W_z]_1 and [W_zw]_1 with the zero encoding.
    3. The verifier’s curve-membership logic recognizes that 0 is not a normal on-curve point, but verification does not fail closed at that boundary.
    4. The malformed elements continue into later verifier logic that builds the pairing inputs usually summarized as P[1] = [W_z]_1 + u·[W_zw]_1 and P[0] = -(z·[W_z]_1 + uzω·[W_zw]_1 + [F]_1 - [E]_1).
    5. Because both prover-controlled opening terms are 0, the first pairing input collapses to zero immediately, and later infinity-handling / normalization code does not reject it.
    6. Public secondary analysis says the batch-normalization path then propagates the bad zero-state into the other pairing input as well, turning P[0] into an all-zero / infinity-like object instead of preserving the nonzero [F]_1 - [E]_1 term that should have saved soundness.
    7. The final pairing check therefore evaluates identity against identity and returns success for a forged proof.
    8. Any application treating verifier success as authority to release value or accept state would then be relying on a false proof result.
  • What the attacker needed to be true:
    • The target verifier had to deserialize or otherwise accept the attacker-controlled proof elements [W_z]_1 and [W_zw]_1 without a fail-closed malformed-point boundary.
    • The implementation had to give the zero encoding inconsistent meanings across different stages of verification.
    • Later pairing or point-arithmetic logic had to remain willing to process those zero-valued proof elements as infinity-like inputs.
    • The surrounding application, if any, had to trust verifier success as a sufficient authorization signal.
  • Technical anatomy worth remembering:
    • This is the pure-verifier cousin to AZTEC v1’s later demo exploit. AZTEC v1 combined malformed-proof admission with a second zero-address ownership bug, while this Plonk case already shows that the verifier alone can become unsound if malformed proof elements survive admission.
    • The bug is about inconsistent semantics, not just a missing isOnCurve helper. A value that is not a valid curve point in one stage of execution cannot safely become the point at infinity in a later stage without destroying the verifier’s meaning.
    • The affected proof objects were not obscure side data; [W_z]_1 and [W_zw]_1 are part of the core Plonk opening checks, so once their admissibility drifted, the pairing equation itself answered the wrong question.
    • The high-value implementation lesson is that parser checks alone were not enough: the same malformed object also touched infinity testing, inversion-by-zero behavior, and batch normalization of projective points. This is exactly the kind of one invalid object crosses multiple helper boundaries and picks up a safer meaning in each one chain that good verifier review should assume exists.
    • Public writeups also note that any one of several missing fail-closed checks would have killed the exploit. That matters because it shows the real problem was not one unlucky branch but a verifier pipeline that never committed to one canonical malformed-point death point.
    • The report is also a reminder that verifier implementations need negative tests for encodings that protocol designers may think are impossible. Attackers do not share the protocol author’s assumptions about what inputs are supposed to exist.
  • Representative artifacts:
    • Public disclosure repository: cryptosubtlety/00
    • Public writeups in that repository: 00.pdf and Using Zero to Attack Zero-Knowledge Proof (ZKP) PLONK C++.pdf
    • Reported malformed proof elements: [W_z]_1 and [W_zw]_1
    • Pairing inputs highlighted in secondary analysis: P[1] and P[0]
    • Reported dangerous encoding: 0
    • Public secondary summary: 0xPARC zk-bug-tracker entry Aztec Plonk Verifier: 0 Bug
  • Response / mitigation:
    • The immediate mitigation for this bug class is to reject malformed proof elements before any later verifier arithmetic can reinterpret them.
    • More specifically, point-at-infinity handling, curve-membership checks, and proof-element decoding have to share one consistent representation boundary. If 0 is not a valid encoding for an admissible point, verification must abort at that first determination.
    • The broader engineering lesson is to test verifier implementations with malformed and theoretically impossible proof objects, not only with well-formed proofs and expected failure cases.

Pattern tags

Why the exploit worked

  • Aztec’s verifier did not keep one fail-closed interpretation of the proof objects [W_z]_1 and [W_zw]_1: the zero encoding was first recognized as invalid, then still allowed to flow into later logic that treated it like an infinity-shaped point.
  • The forged proof therefore did not need to beat the Plonk statement on its own terms; it only needed to keep one impossible curve-point encoding alive long enough for pairing construction and batch normalization to reinterpret it.
  • Once both prover-controlled opening terms were set to 0, P[1] collapsed immediately and public secondary analysis says the same bad zero-state then poisoned P[0] during later normalization as well, so the final pairing check compared identity against identity.
  • Any application layer trusting this verifier would have inherited the same false authority, which is why malformed-proof admission belongs in the custody boundary even before any downstream spend logic exists.

Detection and audit pivots

  • Trace one proof-supplied curve point through the whole verifier pipeline — decoding, curve-membership checks, infinity checks, inversion, normalization, and pairing assembly — and prove that a malformed encoding cannot survive long enough to change meaning between stages.
  • Fuzz 0, point-at-infinity encodings, off-curve coordinates, low-order points, and other should be impossible proof objects directly against the verifier, not only against upstream proof generators.
  • Ask the sharp boundary question explicitly: where is the one canonical death point for a malformed proof element, and can any later helper still reinterpret that same bytestring as a neutral or admissible object?
  • Negative-test the exact pairing inputs (P[0], P[1], or their verifier-specific equivalents) under malformed opening elements so reviewers can see whether one bad projective point corrupts later normalization or zero-inversion paths.

Why this incident matters

  • It is a compact, sourceable exemplar for the proofs/verifiers corpus because it isolates the malformed-proof-element problem without needing a second application-layer bug to explain the verifier failure.
  • It strengthens the curve-point validation failure family by preserving the pure zero value acquired two incompatible meanings inside one verifier story.
  • It also complements the existing AZTEC v1 incident nicely: together they show both ends of the same family, from a pure verifier-unsoundness bug to a real cash-out path where malformed-proof admission composed with downstream default-value authority.
  • Because this was disclosed as a verifier-implementation bug rather than as one app-specific exploit, it is also a good reminder that patched source is not yet the whole response object. Once a public verifier family is shown unsound, teams still need to inventory which deployed applications, generated verifiers, or copied bindings continue to trust that family in production.

Failure-prevention checklist

  • Give malformed proof elements one canonical death point before any later arithmetic, infinity handling, or normalization can reinterpret them. not on curve, bad encoding, and point at infinity cannot be loosely recoverable states.
  • Keep parser, curve-membership, infinity, inversion, and normalization code on one shared representation contract for proof-supplied points. If a byte pattern is inadmissible in one stage, no later helper should be able to resurrect it as a neutral element.
  • Add negative tests for zero encodings, point-at-infinity encodings, off-curve points, low-order points, and other should be impossible proof objects at the exact verifier boundary, not only in prover-side libraries.
  • Fail closed before batch operations. Batch normalization, batch inversion, and aggregate pairing setup should only run after every proof-supplied point in the batch is individually proven admissible.
  • Treat zero-inversion and infinity-edge behavior as first-class verifier security cases, not merely math-library oddities. A helper that returns 0 for 0^-1 can become a proof-forgery primitive if later code treats the result as meaningful.
  • Review verifier families as deployment families. Once one public verifier implementation is shown to admit malformed points, inventory copied bindings, generated verifiers, and downstream applications that still trust the same point-handling stack.
  • Keep invariants on the final pairing inputs themselves. Tests should prove that malformed opening elements cannot collapse P[0], P[1], or equivalent verifier-specific pairings into identity objects that trivially satisfy the check.
  • Prefer explicit reject infinity here or reject invalid point here assertions over implicit assumptions that later code will naturally choke on malformed objects.
  • Re-run malformed-input regression suites after performance refactors to normalization, coordinate conversion, batching, or point utility code; those are exactly the places where one invalid object can silently acquire a new meaning.

Audit prompts

  • Where is the one canonical point in the verifier where an invalid or zero-encoded proof element must die, and can any later helper still reinterpret that same bytestring as infinity or another admissible object?
  • Do curve-membership checks, infinity checks, and deserialization all agree on what (0, 0) or other special encodings mean?
  • Can any batch-normalization, batch-inversion, or coordinate-conversion path let one malformed projective point poison neighboring pairing inputs?
  • If a proof element is rejected as not on curve, what exact control flow prevents later arithmetic from still consuming it?
  • Are there any math-library edge cases where 0, NaN-like sentinels, or special coordinates return a value that downstream verifier code silently treats as legitimate?
  • Do end-to-end negative tests hit the actual forged shape here — zeroed [W_z]_1 and [W_zw]_1 or equivalent opening elements — and assert verifier failure before pairing evaluation succeeds?
  • If this verifier bug were disclosed today, which deployed verifiers, copied libraries, or generated bindings would still need a statement inventory and emergency review?
  • Are reviewers checking only that isOnCurve exists, or also that every later helper preserves the same invalid-point semantics?
  • Can the final pairing equation ever reduce to identity-by-identity because malformed proof elements zeroed out intermediate points rather than proving the intended relation?

Lessons that should already be standard

  • A malformed proof element cannot be allowed to pick up a safer meaning later in the pipeline. invalid now, infinity later is already verifier unsoundness.
  • Verifier review has to follow objects across helper boundaries, not just inspect one parser function in isolation.
  • Batch utilities are part of the proof statement boundary. If normalization or inversion can rewrite one bad point into harmless-looking zeros, the bug is no longer just math plumbing.
  • Impossible inputs are exactly the inputs auditors should try first. Attackers do not care which encodings protocol authors hoped no prover would ever submit.
  • Public disclosure of a verifier-family bug should trigger a live deployment sweep, not only a local source patch.
  • Pairing checks that end at 1 == 1 are not reassuring when malformed inputs were allowed to collapse both sides into identity objects.
  • In proof systems, consistent object meaning matters as much as explicit validation. One byte pattern must not mean reject, then neutral element, then safe to pair depending on which helper touched it.

Internal linkages

Control surface