Witness / Public-Input Binding Failure / Split-Statement Soundness Bug

  • Name: witness / public-input binding failure / split-statement soundness bug
  • Category: exploit pattern / zk-proof circuit soundness failure / public-input-witness decoupling / fake-state-membership proof
  • Summary: A zk system can verify a proof and still prove the wrong thing if the circuit lets one security-critical fact live twice, once as a public input trusted by the contract and again as a witness-side value trusted by ownership or state-transition logic, without forcing the two to match. The verifier then authenticates two independent statements that operators mentally treat as one. Sometimes that looks like Aztec 2.0: the attacker proves note ownership against an invented witness-side ledger while separately supplying the genuine onchain root the contract checks. Sometimes it looks like MACI: the public batch semantics say process each message against its indexed voter state, but the witness still gets to choose which state leaf each message is actually tested against before invalidity branching. In both shapes, proof verified has drifted away from the proof established this exact public-state transition against this exact live object, and ordinary withdrawal, tally, or release code turns that weaker statement into real security authority.
  • What it is:
    • A proof circuit consumes some security-critical value such as a Merkle root, accumulator root, epoch root, or state commitment in more than one place.
    • One use sits on the public-input side and is later compared against live onchain state or another trusted anchor.
    • Another use governs witness-side membership checks, note ownership, nullifier validity, or some other private correctness claim.
    • The protocol implicitly assumes those two uses refer to the same underlying value or the same indexed object.
    • In the vulnerable case, the circuit or verification key never enforces that equality or pairing.
    • The attacker supplies a real public input the contract accepts and a different witness-side root, state leaf, or per-item object under which fabricated balances, notes, entitlements, or invalidity outcomes become provable.
  • Typical preconditions:
    • The protocol relies on a zk proof as direct authority for withdrawal, mint, exit, or state-release actions.
    • One semantic value is duplicated across public inputs and witness-side circuit logic.
    • The circuit does not constrain those duplicated uses to match, or the deployed verification key still corresponds to an older circuit that failed to do so.
    • Settlement code separately validates the public-input version against live state and assumes that also authenticates the witness-side state.
    • Successful proof verification immediately reaches money-moving logic without a second independent provenance check.
  • Common vulnerable surfaces:
    • escape-hatch or emergency-withdrawal circuits that prove note ownership against a ledger root while also exposing a root as a public input
    • zk bridges or private ledgers where membership roots, exit trees, or accumulator commitments appear in both private and public roles
    • long-lived deployed verifier keys whose source circuit was later patched or removed, but the onchain key still accepts proofs for the old flawed statement
    • proof systems that separate state this proof references from state the contract checks publicly and only assume those are the same
    • decommissioned or legacy proof systems where few people still inspect the exact relation encoded by old verification keys
  • Exploit sequence:
    1. The attacker finds a circuit where one security-critical state commitment is used both publicly and privately without a binding equality constraint.
    2. They construct a witness-side state they control, such as a fake Merkle tree containing fabricated notes or balances.
    3. They generate a proof against that fake witness-side state.
    4. They separately provide the genuine live root or commitment as the public input the contract expects.
    5. The verifier accepts because it was never required to prove that the witness-side root and public-input root were the same object.
    6. The contract checks the public input against live state, concludes the proof references the canonical ledger, and releases real assets.
  • Variants:
    • Dual-root decoupling — one root feeds witness-side membership while another equivalent-looking root is surfaced publicly, as in Aztec 2.0’s old_data_root issue where the accepted proof could describe a fake-note withdrawal while the contract still checked the genuine live root.
    • Indexed-object witness substitution — the circuit says each public command or message should be applied to one indexed state object, but the witness still gets to supply a different per-item object before invalidity branching or state updates. MACI 1.0’s ProcessMessages bug is the crisp censorship exemplar.
    • Epoch / accumulator split — the proof establishes private membership against one accumulator while the contract only checks a different epoch commitment publicly.
    • Patched source, stale verifier key — the repository removes the flawed relation later, but already-deployed verification keys still accept proofs for the old statement.
    • Emergency-exit circuit rot — the flawed statement survives in rarely used escape or recovery paths that still custody real value.

Three proof-state promotions reviewers should not collapse

  1. Some proof-shaped object or state commitment exists

    • A public root, witness-side root, indexed leaf list, intermediate root, or verifier key is present and the verifier can be made to accept something about it.
    • That is only the weakest fact: some proof-adjacent state story exists.
  2. That object deserves to count as canonical proof truth for this exact live state object

    • The harder question is whether the public-input root, witness-side root, indexed object identity, and intermediate-root transition path still describe one non-fungible security object rather than two separately satisfiable stories.
    • Most incidents in this family break here: the protocol promoted root-like object exists into this exact live ledger, note set, or indexed voter state was proven even though witness truth and public truth had already drifted apart.
  3. That accepted proof truth deserves withdrawal, release, or tally authority

    • Even a proof-valid object should not automatically mean release this asset, honor this escape, or publish this tally result unless downstream code is still honoring the same canonical state object the proof actually bound.
    • This is the final cash-out question: why does this accepted proof-state object still get to matter this much here?

Healthy review gets materially better once teams stop flattening those three promotions into one vague proof verified against the root moment.

  • Detection heuristics:
    • Search circuits for one semantic value appearing twice under different variable names or plumbing paths, especially once in public inputs and once in witness-side membership logic.
    • For batch-processing circuits, ask the per-item version of the same question: what exact constraint forces command[i] or message[i] to operate on the indexed leaf / account / note the public semantics claim, rather than on a coordinator-supplied sibling object?
    • Ask explicitly: what exact equality constraints force the contract-checked public root and the witness-used membership root to be identical?
    • Audit deployed verification keys, not just current repository circuits; the onchain key defines the statement users can still prove.
    • Build negative tests that keep the public input canonical while mutating the witness-side root, or keep the indexed message set canonical while mutating the witness-side leaf pairing, and require proof generation or verification to fail.
    • Treat old escape-hatch, migration, tally, and decommissioning circuits as first-class solvency or governance surfaces even if they are rarely invoked.
    • When postmortems say the verifier worked as intended, ask whether the intended statement itself was wrong or incomplete.
  • Defenses:
    • Model each security-critical state commitment as one statement object with one meaning across public and private proof components.
    • Add explicit equality constraints whenever the same semantic root, epoch, or accumulator appears in multiple circuit roles.
    • Version and inventory deployed verification keys so operators know which exact statement each onchain verifier still accepts.
    • Revoke, quarantine, or economically drain old proof paths before deprecating them operationally.
    • Add withdrawal-side invariants that make proof verified insufficient unless the proof also establishes provenance against the exact live state the contract references.
    • Treat verifier keys as executable policy: if the key still accepts the old statement, the old bug is still live no matter what the repository says now.

What sound design looks like

  • Proof authority stays staged rather than collapsing into one verifier event

    • The design should keep proof-shaped object exists, object counts as canonical live state truth, and that accepted truth deserves downstream withdrawal / release / tally authority as separate promotions with explicit constraints and provenance at each step.
    • The fastest way to hide a split-statement bug is to narrate all three promotions as one proof verified against the root fact.
  • One semantic state commitment should appear once as a security object

    • If a note-membership root, exit tree root, or accumulator root matters both to private ownership logic and to public contract checks, the safest design is to make those uses literally the same constrained variable instead of two parallel variables that operators merely intend to be equal.
    • The withdrawal or tally contract should be able to answer a crisp provenance question: which exact live commitment did this proof establish ownership or state-transition authority against?
  • Batch circuits should make per-item state binding non-fungible across intermediate roots

    • If a message, command, or note update is supposed to act on indexed object i, the proof should first show that object i is a member of the current or prior intermediate root, then apply the update to that exact object before any invalidity branching, skipping, or next-step root computation can weaken the relation.
    • Proof-state object unification and indexed-binding discipline is the direct positive-design companion for this one canonical root, one exact indexed object, one non-fungible transition path target.
  • Deployed verification keys should be treated as live policy artifacts, not passive history

    • Healthy systems inventory which statement each key proves, which balances remain reachable through it, and whether legacy escape or migration paths have been economically drained enough that an old flawed statement can no longer matter.
    • Verification-key statement inventory and asset-reachability discipline is the lifecycle companion for keeping that live-authority map explicit.
  • Residual proof paths should degrade toward bounded exit service, not broad ambient authority

    • If a deprecated escape hatch must survive, it should keep the narrowest possible asset surface, the smallest practical balance sheet, and the clearest possible separation between legacy withdrawal compatibility and normal production settlement.
    • Even a real proof-valid legacy statement should not automatically inherit the same payout or outcome authority it once had at peak usage.
  • The useful reviewer question is stronger than does the proof verify?

    • It is did the proof verify one unified statement about this exact live state commitment, or did we accidentally let public-state truth and witness-side truth drift into two separately satisfiable claims before downstream code cashed that out?
  • High-signal incidents:

    • [[../../../incidents/proofs-verifiers/aztec-2-escapehatch-old-data-root-decoupling-unsound-proof-withdrawal-2026-06-17|Aztec 2.0 escapeHatch() old_data_root decoupling / unsound proof withdrawal (2026-06-17)]] — the clean current root-decoupling exemplar: a deprecated escape-hatch verifier key accepted a proof whose witness-side ownership checks could be satisfied against fabricated state while the public input still matched the real onchain root; Aztec Labs says the accepted proof even encoded deposit = 0 and withdraw = 1,158 ETH, releasing about $2.2M.
    • [[../../../incidents/proofs-verifiers/maci-1-0-processmessages-under-constrained-message-state-binding-coordinator-censorship-near-miss-2021-12-01|MACI 1.0 ProcessMessages under-constrained message/state binding / coordinator censorship near miss (2021-12-01)]] — the batch-processing sibling where the proof system did not force each decrypted message onto the Merkle-proven voter state leaf identified by its stateIndex before invalidity branching, letting the coordinator prove a censored tally transition rather than the anti-censorship relation MACI claimed.

Failure-prevention checklist

  • Treat proof-shaped object exists, object counts as canonical live state truth, and that accepted truth deserves withdrawal / release / tally authority as separate promotions in circuit design, contract design, and monitoring. Do not let bare verifier success silently stand in for all three.
  • Represent each security-critical commitment once in the proof statement. If a root, epoch, accumulator, or note set matters to both private membership logic and public settlement checks, make it one constrained variable rather than two semantically equivalent plumbing paths.
  • Add explicit equality constraints wherever the same semantic value must appear in multiple circuit roles. We pass the same root in practice is not a control; the circuit has to make divergence unsatisfiable.
  • For batch circuits, bind each public command to one exact witness object before any invalidity branching or state update. If a message carries stateIndex, the circuit should Merkle-prove and operate on that indexed leaf against the current or prior intermediate root rather than on coordinator-chosen side inputs that only should correspond to it.
  • Inventory deployed verification keys and treat them as live authorization policy. Source-code patches do not matter if an old onchain key still proves the vulnerable statement against funded contracts.
  • Gate withdrawal, mint, exit, and tally paths on proof provenance against the exact live state commitment or indexed object the contract references, not just on bare verifier success.
  • Version public-input schemas and witness schemas together. Reviews should diff what the contract checks publicly and what the circuit uses privately as one change set.
  • Build negative tests that keep the public root canonical while mutating the witness-side root, note tree, accumulator, or per-item witness pairing. The required result is proof generation or verification fails, not merely the withdrawal or tally later looks suspicious.
  • Quarantine or economically drain legacy escape hatches, migration verifiers, and deprecated proof routes before renouncing admin control. Rarely used proof paths should have the smallest possible balance sheet.
  • Require per-key ownership documentation: what statement does this verification key prove, what contracts still trust it, and what assets remain reachable through it?
  • Add runtime monitoring that flags impossible proof claims even when verification passes, such as deposit = 0 paired with a large withdrawal, or tally batches whose invalidation pattern cannot be explained by the indexed voter state.
  • Treat emergency, governance, and decommissioned proof code as treasury-critical or outcome-critical code. The least-used circuit is often the least-reviewed live authority surface.

Audit prompts

  • Which proof-state promotion is happening at each step here: proof-shaped object existence, accepted canonical live-state truth, or downstream authority to withdraw / release / finalize against that accepted truth?
  • Where exactly is the equality constraint that forces the witness-side membership root to equal the public-input root the contract checks onchain?
  • Does the deployed verification key correspond to the audited circuit, or could an older key still accept a broader or stale statement?
  • If I hold the public input fixed to the live root, can I alter the witness-side tree, accumulator, or note set and still generate a proof?
  • Which proof-facing values appear twice under different names or serialization paths, and are reviewers relying on convention rather than constraints to keep them equal?
  • Does any withdrawal, escape hatch, bridge exit, tally processor, or migration function treat proof verified as sufficient even though the proof may not bind ownership to the same live state commitment or indexed object the contract references?
  • What balances, notes, or assets are still reachable through legacy verifiers or deprecated proof routes, and who would even notice if those routes proved the wrong statement?
  • If the repository removed a flawed circuit later, what exact onchain contracts and verification keys still preserve that old statement as live policy?
  • Can monitoring explain why each accepted proof’s claimed deposits, spends, nullifiers, and withdrawals are mutually coherent, or is the system blind once the verifier returns true?
  • During decommissioning, who confirmed that residual operator roles, escape-hatch permissions, and legacy keys were not still enough to turn a stale proof statement into a payout right?
  • If an attacker presented one accepted withdrawal proof today, what exact artifact would prove that the ownership claim was established against the same canonical root the contract compared publicly?
  • For batch-processing systems, can an operator swap the per-item witness object (currentStateLeaves[], note list, accumulator witness, etc.) while still producing plausible intermediate roots, or does each step prove membership of the exact indexed object before the next root is committed?

Lessons that should already be standard

  • Proof verified is not the same as the right thing was proven. Soundness failures often live in the statement boundary, not in broken cryptography.
  • A proof-shaped object exists, that object counts as canonical live-state truth, and that accepted truth may now move assets or decide outcomes are three different claims.
  • Public-input truth and witness-side truth are one security object whenever the contract assumes they describe the same state. If the circuit does not force that identity, the protocol is already running on folklore.
  • The same is true one level down for indexed objects inside batch proofs: if the application promise says message k was processed against voter leaf k's declared stateIndex, that pairing has to be circuit truth, not coordinator etiquette.
  • Verification keys are production policy artifacts. Leaving an old key deployed against funded contracts is operationally equivalent to leaving old authorization logic live.
  • Escape hatches, migration circuits, tally circuits, and decommissioned proof paths deserve more suspicion, not less. They are where stale or underbound statements survive longest while everyone assumes the product is already gone.
  • Repository history is not a mitigation. If the onchain key still encodes the old relation, the old bug still exists regardless of later refactors.
  • Proof-driven payout and governance systems need impossible-state monitoring in addition to verifier correctness. Accepted claims that violate basic economic intuition or tally semantics are often the fastest signal that the verifier is proving the wrong world.
  • The safest design is to make one live commitment do one job everywhere: the root the contract trusts publicly should be the same constrained root the circuit uses privately, and each indexed command should be forced onto the exact witness object the public semantics name before any intermediate-root transition can make the mismatch disappear.

Where this pattern stops

  • Use proof-to-message-binding-failure when the decisive failure is that a verifier checked a trusted root or header without proving the specific message or leaf being executed actually belonged under it. That family is about root-to-message membership in bridge-style verifiers. This family is about zk circuits that split one supposedly shared state commitment into unconstrained public and private halves.
  • Use proof-backed-outcome-integrity-failure when the same proof-backed result drifted from the public promise intuition is present, but the sharper preserved lesson is that the protocol never committed one exact canonical command-result object that uniquely determined the published tally or final result.
  • Use proof-to-effects-binding-failure when the proof statement may be sound, but a downstream settlement consumer still executes or charges for a different economically relevant effect set than the one the proof authenticated.
  • Use deprecation-without-deauthorization when the main lesson is lifecycle control and residual authority rather than the specific proof statement bug. Aztec 2.0 is both; this note preserves the proof-mechanics half.
  • In short: this pattern begins when a proof system verifies an underbound statement because public-input truth and witness-side truth were never fused into one relation, and it stops when the sharper story is instead forged message membership, downstream effect-set mismatch, or pure lifecycle negligence.

Internal linkages

Control surface

  • The leverage sits in circuit constraints, deployed verification keys, duplicated root semantics, and any settlement path that treats verifier success as complete provenance proof.

  • The recurring mistake is subtle but decisive: some proof-shaped state object exists, that object still means this exact live state truth, and that accepted truth still deserves withdrawal / release / tally authority are not one fact unless the circuit and downstream consumer make them one fact.

  • Public state matched and private ownership proved are not one fact unless the circuit makes them one fact.

  • A verifier can be cryptographically correct about the wrong statement. Once withdrawals trust that statement, the distinction becomes treasury-critical.

  • Sources:

  • Last reviewed: 2026-07-29 UTC