Module 8 · Ethereum, the EVM & on-chain zero-knowledge
Case study: coercion-resistant financial privacy on the EVM
A privacy pool hides which deposit funded a withdrawal, but it does nothing for a holder who is physically compelled to withdraw — the "$5 wrench attack". Earlier anti-coercion ideas fail for a sharp reason: their decision logic sits in the contract, where an attacker can read it and demand every secret. This case study follows a paper (attributed to Raven) that moves the entire decision inside a Groth16 witness. A branchless arithmetic selector picks between a real and a decoy nullifier so the R1CS constraint structure is identical on both paths, making the on-chain transcript indistinguishable regardless of which path ran. It then shows the production side: an EIP-2535 Diamond deployed to beat the 24 KB bytecode limit, an atomic in-pool transfer that removes the temporal-correlation leak of withdraw-then-redeposit, and on-chain Groth16 verification. We separate what is PROVEN under assumptions from what is only TESTED in a circuit or MEASURED in simulation — and we are blunt that an unobservable decision is not a safe user.
Learning objective
Explain the "$5 wrench" threat model, why any defense whose logic is visible on-chain fails, how moving the entire withdrawal decision inside the zero-knowledge witness — a branchless dual-nullifier selector with identical R1CS structure — makes the real and decoy paths computationally indistinguishable, and how the production system lives inside EVM constraints (an EIP-2535 Diamond against the 24 KB limit, Stealth Shielded Transfers against temporal correlation, on-chain Groth16 verification), with crisp PROVEN-vs-TESTED-vs-MEASURED labelling.
- Track
- Research
- Module
- 8 · Ethereum, the EVM & on-chain zero-knowledge
- Prerequisites
- On-chain cryptography and zero-knowledge verification · delegatecall, proxies and the Diamond (EIP-2535) pattern
Conceptual introduction: the wrench attack
A privacy pool breaks the on-chain link between a deposit and a later withdrawal: you deposit a fixed denomination, the contract stores a commitment to your note in a Merkle tree, and later you withdraw by proving in zero knowledge that your note is one of the committed leaves — without revealing which one. A published nullifier (derived from the note secret) stops you spending the same note twice. This defeats an adversary who reads the chain. It does nothing against an adversary who reads you.
The colloquial name is the "$5 wrench attack": rather than break the cryptography, the attacker stands over the holder and compels a withdrawal, or demands the secret that controls the funds. The paper this case study follows (public work attributed to Raven) takes this adversary seriously and asks an engineering question: can a withdrawal under duress be made to look, on-chain and to the user, exactly like a normal withdrawal — so that there is nothing for the coercer to detect and demand?
Framing up front: the goal is PLAUSIBLE deniability of a decision, not physical safety. Making the real-vs-decoy choice unobservable denies the attacker a target to extract. It does not stop a determined adversary with unlimited reach, and it is not a substitute for operational security. This boundary is stated once and not re-litigated per claim.
Threat model
The paper orders adversaries by capability and targets the strongest. The point of interest is the physical adversary, who is assumed to hold every advantage short of breaking the math.
- Passive observer
- Reads all public on-chain data and the mempool. Defeated by ordinary pool unlinkability.
- Professional analyst
- Adds chain-analysis tooling, exchange KYC data and law-enforcement reach — correlation across sources.
- Active attacker
- Can dust, phish and attempt contract exploitation.
- Physical adversary
- Has the victim in person, in real time: can compel disclosure of passwords and keys, watch the victim operate the wallet, read all contract source and documentation, and demand proof that no hidden funds exist. This is the adversary the construction is built against.
The key requirement the paper sets for the physical adversary is undetectability: even with complete knowledge of the protocol — that a decoy mechanism exists, how the circuit is wired, all contract code — the attacker must gain no way to tell whether a coercion defense was activated. That is a much stronger bar than "the secret is hidden"; it demands that the observable state carry no signal at all.
The visible-logic failure mode
Why do earlier anti-coercion ideas fail? Because their decision lives in the contract. A naive "duress password" scheme branches on which secret was supplied — and that branch is public bytecode on a block explorer. The attacker reads the contract, sees that a decoy path exists, and simply demands both secrets. The defense advertises its own existence.
synthetic / illustrative — a detectable anti-coercion withdraw (the failure mode)// WARNING: synthetic example of what NOT to do. // Both branches are visible on any block explorer. function withdraw(bytes32 password) external { if (keccak256(abi.encode(password)) == duressHash) { _transferDecoy(msg.sender); // attacker can SEE this branch exists } else if (keccak256(abi.encode(password)) == realHash) { _transferReal(msg.sender); // ...so they demand BOTH passwords } // The control flow itself is the leak: the decoy path is // discoverable from the contract source, so it protects no one. }
The lesson the paper draws is categorical: the "$5 wrench attack" cannot be closed by adding cleverer logic to observable state, because the observable state is exactly what the attacker reads. It can only be closed by ensuring there is no observable state that distinguishes the two outcomes. On the EVM, "observable" includes the contract's branches, the emitted events, the storage writes and the gas profile — all of it must be identical.
A second, quieter leak lives in the standard withdraw-then-redeposit pattern used to move funds between users: funds leave the pool at t1 and re-enter at t2, and an observer correlates the two by amount and timing. The paper treats this temporal-correlation signal as a separate failure mode, addressed later by an atomic in-pool transfer.
The dual-nullifier arithmetic selector
The construction moves the entire decision inside the zero-knowledge witness, where it is invisible by definition. On deposit, a single commitment binds two nullifiers derived from the same secret s: a real one and a decoy one. On withdrawal, the circuit emits exactly one output nullifier, chosen by an arithmetic selector that never branches.
- Dual nullifier
- From one secret s and a ZK-friendly hash H, the circuit derives N_real = H(s, 0) and N_decoy = H(s, 1). The paper argues these are computationally independent: given one, deriving the other requires s (reduction to the pseudorandomness of the hash under a Sponge-PRF assumption). Both are bound into a single on-chain commitment.
- Selector bit a
- A private witness value a in {0,1} computed as a product of in-circuit conditions (for example, a correctness check on the supplied passphrase). a = 1 selects the real path; a = 0 selects the decoy. It is never revealed.
- Branchless selection
- The output is N_out = a * H(N_real) + (1 - a) * H(N_decoy). No "if": one multiplication and one addition pick the path arithmetically.
The crux is why this is indistinguishable. In a Rank-1 Constraint System, the constraint structure depends only on the circuit topology, not on the witness values. The selector expression out = a*v0 + (1-a)*v1 rewrites as v1 + a*(v0 - v1): one multiplication constraint (t = a*(v0 - v1)) plus one addition (out = v1 + t, free in R1CS). That shape is fixed for both a = 0 and a = 1. The paper reports the selector adds 3 R1CS constraints — under 8% overhead — on a base withdrawal circuit it reports at roughly 12,800 constraints, and that the Groth16 proof stays a constant size (the paper reports ~128 bytes on BN254).
synthetic / illustrative — the branchless selector (contrast with the failure mode above)// Synthetic sketch of the in-witness selector (arithmetic, no branch). // a, v0, v1 are circuit signals; all of this is PRIVATE witness. // a = passphrase_correct * inference_bit // a in {0,1}, product of checks // out = a * v0 + (1 - a) * v1 // pick real (v0) or decoy (v1) // // R1CS form — identical constraints for a = 0 and a = 1: // t = a * (v0 - v1) // 1 multiplication constraint // out = v1 + t // 1 addition (free in R1CS) // // The ONLY public outputs are: root, out (the nullifier), // recipient, relayer, fee, and the Groth16 proof. // None of them is a function of 'a' that an observer can invert.
Security is argued PROVEN under stated assumptions via a four-game sequence-of-games reduction. Sketch: Game 0 is the real execution; Game 1 replaces the hash outputs with uniform random values (justified by the Sponge-PRF assumption on the hash); Game 2 replaces the real proof with a simulated one (justified by Groth16 zero-knowledge); in the final game the adversary's view carries no function of a, so its guessing advantage collapses to 1/2 plus a negligible term. The soundness of the proof system and the hardness of forging a proof rest on a q-SDH assumption. The paper calls the resulting property "known-mechanism security": complete protocol knowledge gives the attacker no advantage, because the deciding value lives in the witness and never touches observable state.
Cost of the guarantee (a real trade-off, not a free lunch): the real and decoy nullifiers share one commitment, so consuming the decoy marks that commitment spent and permanently burns the real nullifier for that deposit. Coercion resistance for a given note is a one-time sacrifice of that note's decoy balance; the user's other deposits are unaffected. This is a design choice, not a flaw — but it is a cost.
Worked example (synthetic)
A synthetic walk-through of one coerced withdrawal. Numbers and names are illustrative; the point is the sequence, not any specific value.
- 1Setup: a user deposits 1 unit. The circuit derives N_real = H(s, 0) and N_decoy = H(s, 1) and inserts a single commitment C = H(s, N_real, N_decoy) as a Merkle leaf. The user memorizes a normal passphrase and a natural-sounding "shadow" passphrase.
- 2Coercion: an attacker forces a withdrawal and demands "the real passphrase". The user supplies the shadow passphrase instead.
- 3In-circuit: the passphrase check yields 0, so the selector bit a = 0. The branchless selector computes N_out = H(N_decoy). No branch is taken; the R1CS structure is the same one it would have taken for a = 1.
- 4On-chain: the contract verifies the Groth16 proof, records N_out as spent, and transfers the pool denomination to the recipient. The emitted events and storage writes are the same as any other withdrawal.
- 5Attacker's view: a valid transaction moving valid funds. There is no cryptographic test to decide whether real or decoy funds were withdrawn, whether the defense fired, or whether other deposits exist. The attacker can demand "the real one" again, but cannot verify any answer.
- 6Aftermath: the decoy path for that commitment is now spent and the real nullifier for it is permanently unusable. To restore coercion resistance the user must create a fresh deposit with a new secret.
Notice what the example does and does not establish. It shows the on-chain transcript is uninformative about a — the PROVEN property. It does not show the user is safe: the attacker still physically controls the victim, and may escalate. The construction removes a target for extraction; it does not remove the attacker.
Living inside the EVM
A formal circuit is not a deployment. The paper's second half is an engineering story about fitting a full-featured privacy system into EVM constraints, and it is worth studying as EVM engineering regardless of the privacy angle.
The hard wall is EIP-170 (the Spurious Dragon 24 KB bytecode limit): a single contract carrying multi-token support, tiered compliance and upgrade governance does not fit. The paper reports adopting an EIP-2535 Diamond — one proxy that routes calls to independent facets via delegatecall over shared storage — reporting 10 independently upgradeable facets exposing 104 functions, with upgrades gated by a timelock. This is the familiar delegatecall-proxy pattern pushed to a multi-facet architecture: each facet is under 24 KB on its own, while the Diamond presents one logical contract. The paper reports deploying identical bytecode across two public testnets and an enterprise-style network.
- EIP-170 / 24 KB limit
- The per-contract bytecode ceiling introduced by the Spurious Dragon hardfork. It is a hard protocol limit, not a tuning knob — the reason a monolithic design cannot scale in features.
- EIP-2535 Diamond
- A proxy that delegatecalls into multiple facet contracts sharing one storage layout, so total code can exceed 24 KB while behaving as a single upgradeable contract. The paper reports this as the first production privacy-protocol deployment of the pattern.
- On-chain Groth16 verification
- A verifier facet checks the ~128-byte proof against public inputs (root, output nullifier, recipient, relayer, fee) in constant time using the EVM's BN254 precompiles. Constant-time, succinct verification is why Groth16 is chosen over larger-proof or logarithmic-verification systems for a many-pool deployment.
- Stealth Shielded Transfer (SST)
- An atomic intra-pool ownership transfer: in one transaction it marks the sender's nullifier spent and inserts the recipient's new commitment, without funds leaving the pool. It replaces the two-transaction withdraw-then-redeposit pattern and removes its temporal-correlation signal.
On gas, the paper reports that the atomic transfer costs about 60% less than the two-transaction alternative (it reports ~112K gas for the atomic transfer against ~430K for a full withdrawal plus redeposit), while a full withdrawal with a compliance proof is reported around 280K gas. The paper also reports a multi-tier compliance oracle with 4 KYC levels queried per deposit and withdrawal, and an overall footprint of 92 privacy pools across 12 tokens on two testnets. Treat these as MEASURED on testnets under stated hardware, not as mainnet or audited figures.
The paper is explicit that its atomic transfer is NOT claimed to be indistinguishable from an ordinary deposit — a deposit consumes no nullifier, and the transfer does. The claim is narrower: it removes the TEMPORAL link between a withdrawal and a re-entry. Keep the claim exactly as scoped; do not round it up to "fully hidden".
Measured results & what they do / do not prove
Separate the three kinds of evidence, because they support very different conclusions.
- ▸PROVEN (under stated assumptions): real/decoy indistinguishability via the four-game reduction; withdrawal soundness (no withdrawal without a valid committed deposit); double-spend prevention (either nullifier marks the shared commitment spent); front-running / relayer-binding resistance (relayer and fee are public inputs bound into the proof); known-mechanism security. These hold relative to the Sponge-PRF and q-SDH assumptions and Groth16 zero-knowledge — not unconditionally.
- ▸TESTED (circuit / verifier results the paper reports): the selector adds 3 R1CS constraints (<8% overhead) on a ~12,800-constraint base circuit; proofs are a constant ~128 bytes; the paper reports a 24/24 test-suite pass covering deposits, withdrawals, double-spend, front-running, the dual-logic truth table, compliance and Diamond facet upgrades.
- ▸MEASURED (simulation / testnet numbers the paper reports): a Random Forest classifier trained on 10,000 simulated transactions (balanced real/decoy) reaches 50.3% accuracy, AUC-ROC 0.501, p = 0.87 — statistically indistinguishable from random guessing. Gas figures (~112K atomic transfer, ~280K full withdrawal, ~60% saving) are measured on testnets on specified hardware.
- ▸OPEN: the "peace of mind" principle (that coerced and normal operation are behaviorally identical) is a design principle the paper itself declines to call a provable cryptographic parameter, pending a user study; real-deployment side channels (gas metering, network timing, multi-pool interaction) are unmodeled; the automatic-detection signal is an unvalidated extension point; mainnet readiness awaits an independent professional audit.
How to read the classifier result: 50.3% on a balanced two-class problem is not a weak model — it is the expected outcome when the target variable is independent of every observable feature. The paper's own interpretation is that this is a consequence of the identical R1CS structure: if the on-chain data carries no information about a, no classifier can do better than chance. That is corroboration of the PROVEN property at the system level, across the tokens and networks tested; it is not itself a proof, and the paper flags that simulated transactions may miss side channels a real deployment would introduce.
The central honesty point: an indistinguishable proof protects the DECISION from observation — it proves an observer cannot tell real from decoy. It does not prove the user is safe. The holder is still physically present with the attacker; operational security, endpoint and wallet compromise, metadata correlation and funding-source control are all out of scope of the cryptographic guarantee. A valid, unobservable proof proves a property, not that the outcome was correct or that the person is unharmed.
Risks, limitations & open problems
- ▸Plausible, not absolute: the guarantee is plausible deniability bounded by anonymity-set size. It degrades if the adversary can compel all secrets, inspect full device state, control the funding source, or correlate deposits via operational metadata — none of which the on-chain protocol can enforce; they are requirements on the wallet and the user.
- ▸One-time sacrifice: triggering the decoy burns the real nullifier for that commitment. Coercion resistance per note is spent once and must be re-established with a fresh deposit.
- ▸Testnet, not mainnet: every production number is reported on testnets and validated by an internally developed tool. The paper states that mainnet deployment requires an independent professional audit; absent that, the engineering figures are indicative, not assured.
- ▸Assumption-relative proofs: the indistinguishability and soundness results are only as strong as the Sponge-PRF and q-SDH assumptions and the correctness of the trusted setup. A broken assumption or a buggy setup voids the guarantee silently.
- ▸Implementation is the usual failure surface: a non-constant-time verifier path, a leaky event, an uneven gas profile across the two paths, or a weak Fiat-Shamir transcript could reintroduce the very observable signal the design removes. The math being clean does not make the deployment clean.
- ▸Side channels and metadata (OPEN): gas metering, network-level timing, relayer behavior and multi-pool scanning are not modeled in the simulation; a real adversary may find signal the classifier experiment could not.
- ▸Out of scope entirely: physical safety of the coerced user. The construction denies the attacker a distinguishing test; it does not deny the attacker the victim.
Exercise
An engineer proposes "fixing" the detectable withdraw contract from the visible-logic section by moving the if/else that chooses real-vs-decoy into an internal (non-public) Solidity function, reasoning that a private function hides the branch. Explain why this still fails against the physical adversary, and state precisely what has to be true of the two execution paths — and where the decision must live — for indistinguishability to hold.
Expected direction: Solidity "internal" only restricts call visibility, not observability: the bytecode, the control-flow branches, the emitted events, the storage writes and the gas profile are all still on-chain and readable, so an attacker who reads the contract sees the decoy path exists and demands both secrets — the decision is still in observable state. For indistinguishability the two paths must produce an identical observable transcript (same public outputs, same events, same storage effects, same constant-size proof, and no path-dependent gas signal), which requires that the deciding value never appears in observable state at all. The construction achieves this by computing the choice as a branchless arithmetic selector inside the ZK witness, so the R1CS constraint structure is identical for a = 0 and a = 1 and the only public outputs (root, output nullifier, recipient, relayer, fee, proof) carry no invertible function of the selector. The decision must live in the private witness, not in the contract.
Check your understanding
- Why does any anti-coercion scheme whose logic is a visible contract branch fail against a physical adversary?
- Because the branch is public bytecode: the attacker reads the contract, sees that a decoy path exists, and simply demands every secret. The defense advertises its own presence, so there is a target to extract. The only fix is to leave no observable state that distinguishes the outcomes.
- What makes the dual-nullifier selector indistinguishable, and what is it PROVEN relative to?
- The path is chosen by a branchless arithmetic expression (out = a*v0 + (1-a)*v1) inside the witness, so the R1CS constraint structure is identical for a = 0 and a = 1 and the public outputs carry no function of a. Indistinguishability is PROVEN via a four-game reduction, but only relative to the Sponge-PRF assumption on the hash, the q-SDH assumption, and Groth16 zero-knowledge — not unconditionally.
- Why does the paper deploy an EIP-2535 Diamond, and what does it buy on the EVM?
- A monolithic contract with multi-token support, tiered compliance and upgrade governance cannot fit under the EIP-170 24 KB bytecode limit. A Diamond routes calls via delegatecall to multiple independently upgradeable facets sharing one storage layout, so total code exceeds 24 KB while behaving as a single upgradeable contract. The paper reports 10 facets and 104 functions.
- The Random Forest reaches 50.3% accuracy. Does that prove the system is secure, and what does it actually show?
- No. It is MEASURED corroboration on simulated transactions, not a proof. 50.3% on a balanced two-class problem is the expected result when the label is independent of every observable feature, which is consistent with the identical R1CS structure. It supports the PROVEN indistinguishability claim at the system level but may miss side channels a real deployment introduces, and it says nothing about the physical safety of a coerced user.
References
- [1]J. Groth — On the Size of Pairing-Based Non-Interactive Arguments of Knowledge (Groth16), EUROCRYPT 2016
- [2]N. Mudge — EIP-2535: Diamonds, Multi-Facet Proxy
- [3]V. Buterin — EIP-170: Contract Code Size Limit (Spurious Dragon)
- [4]Goldwasser, Micali & Rackoff — The Knowledge Complexity of Interactive Proof Systems, 1985
- [5]Buterin, Illum, Nadler, Schär, Soleimani — Privacy Pools: Making Blockchain Privacy Compliant
References point to public, well-known technical sources for further study.
Related articles
