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 verifiedhas drifted away fromthe 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 referencesfromstate the contract checks publiclyand 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:
- The attacker finds a circuit where one security-critical state commitment is used both publicly and privately without a binding equality constraint.
- They construct a witness-side state they control, such as a fake Merkle tree containing fabricated notes or balances.
- They generate a proof against that fake witness-side state.
- They separately provide the genuine live root or commitment as the public input the contract expects.
- The verifier accepts because it was never required to prove that the witness-side root and public-input root were the same object.
- 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_rootissue 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
ProcessMessagesbug 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.
- Dual-root decoupling — one root feeds witness-side membership while another equivalent-looking root is surfaced publicly, as in Aztec 2.0’s
Three proof-state promotions reviewers should not collapse
-
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.
-
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 existsintothis exact live ledger, note set, or indexed voter state was proveneven though witness truth and public truth had already drifted apart.
-
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, orpublish this tally resultunless 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?
- Even a proof-valid object should not automatically mean
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 verifiedinsufficient 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, andthat accepted truth deserves downstream withdrawal / release / tally authorityas 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 rootfact.
- The design should keep
-
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 objectiis 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 pathtarget.
- If a message, command, or note update is supposed to act on indexed object
-
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 compatibilityand 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.
- 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
-
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?
- It is
-
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_rootdecoupling / 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 encodeddeposit = 0andwithdraw = 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
ProcessMessagesunder-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 itsstateIndexbefore invalidity branching, letting the coordinator prove a censored tally transition rather than the anti-censorship relation MACI claimed.
- [[../../../incidents/proofs-verifiers/aztec-2-escapehatch-old-data-root-decoupling-unsound-proof-withdrawal-2026-06-17|Aztec 2.0
Failure-prevention checklist
- Treat
proof-shaped object exists,object counts as canonical live state truth, andthat accepted truth deserves withdrawal / release / tally authorityas 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 practiceis 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 publiclyandwhat the circuit uses privatelyas 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 merelythe 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 = 0paired 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 verifiedas 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 verifiedis not the same asthe 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, andthat accepted truth may now move assets or decide outcomesare 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 promiseintuition 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
- Best current incident exemplars: aztec-2-escapehatch-old-data-root-decoupling-unsound-proof-withdrawal-2026-06-17, maci-1-0-processmessages-under-constrained-message-state-binding-coordinator-censorship-near-miss-2021-12-01.
- Positive-design companion: proof-state-object-unification-and-indexed-binding-discipline — use it when the sharper question is how one canonical root, one indexed object identity, and one intermediate-root transition path should have been made non-fungible inside the proof statement itself.
- Live-key lifecycle companion: verification-key-statement-inventory-and-asset-reachability-discipline — use it when the sharper follow-up is how an older proof statement stayed live, what contracts still trusted it, and what residual balances or exits it could still reach.
- Adjacent bridge-proof family: proof-to-message-binding-failure — both notes punish
trusted root but wrong thing proved, but that note is leaf / message membership while this note is split public-input versus witness-side state. - Adjacent proof-backed outcome family: proof-backed-outcome-integrity-failure — useful when the underbound proof statement’s practical consequence is that a tally, command-result set, or other proof-backed canonical outcome can drift from the protocol’s public promise even without immediate reserve movement.
- Adjacent proof-consumer family: proof-to-effects-binding-failure — useful when the proof statement itself was fine, but the economic consumer later honored a different effect set.
- Lifecycle cousin: deprecation-without-deauthorization — the operational reason this kind of bug often survives long enough to matter.
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, andthat accepted truth still deserves withdrawal / release / tally authorityare not one fact unless the circuit and downstream consumer make them one fact. -
Public state matchedandprivate ownership provedare 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