We ran nine methods against Tornado Cash. The mixer held, and the money was traceable anyway
A methods paper from a live investigation. Nine techniques run against a real Tornado Cash cash-out, what each one produced, and why the one that broke the case open did not touch the mixer at all.

This is a companion to our forensic record of the Venus and Cronos attacks, which sets out what we found. This one sets out how, including the parts that did not work, because a methods paper that only lists its successes is not much use to anyone.
The headline finding is a negative one, and it should be stated first. We did not demix Tornado Cash. Nothing here breaks it. A withdrawal proves, in zero knowledge, that the withdrawer knows the secret behind one of the pool’s deposits. The nullifier hash it publishes derives from that same note material, so it is not literally unrelated to the commitment; the point is that it cannot be linked to a particular commitment without the note secret. No computation over the chain recovers which deposit a withdrawal spent: not from the Merkle root, not from the nullifier, not from gas, not from timing. That is the design, and in this case the design worked.
What follows is what you can do instead.
The subject
Every method below was run against one real cash-out: a wallet that took 83 withdrawals of 100 ETH from the Tornado 100 ETH pool between June 2025 and June 2026, and later put 2,575.5 ETH back in. The full record of what that wallet turned out to be doing is in the companion record.
0x7a79969a0B9D51D922C4810D2950560360F6f234
The eight that did not break it
1. Note counting, and why it is worthless without a control
Count deposits and withdrawals of a fixed denomination inside a window and look for a count that appears exactly once on each side. In April 2026 that worked: exactly one address deposited 23 notes and exactly one withdrew 23, in a window where the pool saw five depositors and eight withdrawers.
The control is the whole method. We ran the same test against the wallet’s other batches. Three matched on count alone, and all three failed the next test, which was following the funding backwards. A count match on its own is a coincidence generator.
Grade: counted, with a negative control. Strong in one window, and worth nothing without the control.
2. Measuring the anonymity set, which usually ends the conversation
Before spending effort, measure what you are up against. Divide the pool contract’s balance by the denomination.
| At | Unspent notes |
|---|---|
| First withdrawal, 6 June 2025 | 1,254 |
| Second batch, December 2025 | about 2,945 |
| Third batch, February 2026 | about 2,008 |
| Fourth batch, June 2026 | about 2,012 |
The 1,254 figure is confirmed against the contract’s own balance of 125,400 ETH at that block. With over a thousand unspent notes and 224 depositors in the prior month, no counting argument survives. This measurement is cheap and it is the first thing to run, because it tells you whether anything else is worth doing.
3. Merkle root staleness, which is the one genuinely new thing here
Every withdraw call carries the Merkle root the client had synced when it built
the proof. Decode that field across a run of withdrawals and compare.
Across the 83 withdrawals there are 24 distinct roots, and the root changes at 23 of the 82 transitions, advancing whenever deposits landed in between.
Be precise about what that bounds. Fifty-nine of the 82 consecutive pairs share a root with the one before, and the longest unbroken run is ten withdrawals. A shared root is exactly what a batch built in one sitting looks like.
So this establishes at least 24 separate occasions on which a client re-synced the tree, not 83 live moments, and it says nothing about whether a person or a script was driving. A proof can be built from cached data, or well before it is broadcast.
What it does rule out is one single batch: a single sitting would have left one root across all 83.
The way to sharpen this, which we have not done. Compare each withdrawal’s root against the block at which that root first existed on chain. A withdrawal that repeatedly spends a root only minutes old puts an upper bound on how stale the proof could be. That would be a real measurement of proof age. Absent it, the honest claim is two dozen syncs and 83 broadcasts.
Either way it gives an investigator something to ask for: any RPC or front-end provider used at those times may hold timestamped network telemetry. A local node, an indexer, rotating providers or a VPN would each blunt that.
Grade: decoded, and bounded. Stated at that size, still the most useful thing on this list for anyone holding a subpoena.
4. Reading the calldata rather than the rendering
The router call is
withdraw(address,bytes,bytes32,bytes32,address,address,uint256,uint256).
Decoding all 83 gives the relayer field as the zero address, the fee zero and the
refund zero, every time. That alone establishes only that no relayer was
named. Self-relaying needs one more check, and we ran it: on all 83, the
transaction’s own sender is the withdrawing wallet. Relayer field empty and
tx.from equal to the recipient is what makes it self-relayed, read from the
bytes rather than inferred from an absent fee.
It is also where the method’s limits show. Across the same period 2,951 of the pool’s withdrawals, 39 per cent, were self-relayed. Self-relaying is common. It is a filter, not a fingerprint.
5. Proof cadence
Time the gaps. Sixty-one of the gaps between consecutive withdrawals fall inside half an hour, with a median of 108 seconds and a floor of 60. That is proof generation plus a human confirming a dialogue, repeated. For contrast the same operator’s later deposits went in 12 to 24 seconds apart, because deposits need no proof: that is a script.
Grade this as inference, and weak inference. A 60 to 120 second interval is compatible with a person clicking, and equally compatible with a script that waits. Cadence narrows the range of explanations a little. It does not pick one, and it certainly does not separate one person from another.
6. Working hours, measured on days rather than events
The obvious mistake here is to count events. Eighty-three withdrawals across eighteen days is eighteen observations, not eighty-three, and the arithmetic changes completely depending on which you use.
On days, across the cluster: 23 of 26 are Monday to Friday, and none is a Sunday. Under a uniform assumption that is roughly one in 27.
Hour of day gave nothing. Activity spans 02:00 to 23:00 UTC with dead hours scattered through it and does not localise a time zone. We report that as a null result because a method that returns nothing is still information.
7. The gas-note graph
Withdrawal wallets need gas before they can self-relay, and it often comes from a smaller pool whose anonymity set is far thinner. Chaining “small-pool withdrawal, fresh address, self-relayed withdrawals” across eighteen months maps a whole operation rather than one wallet. It is how the second wallet in this cluster was found.
It also nearly produced a false positive, which is the part worth publishing. On 14 May 2025 one address deposited nine 10 ETH, four 1 ETH and seven 0.1 ETH notes, and another withdrew exactly those counts minutes later. Identical bundles minutes apart in thin pools is near-certain linkage, and it briefly looked like it belonged to a labelled exchange thief who had deposited nine 10 ETH notes ninety minutes earlier. The deposit timeline showed it was somebody else. The lesson is that the strongest-looking match in a thin pool is exactly where you should slow down.
8. Volume feasibility, which eliminates instead of suggesting
Most heuristics nominate suspects. This one removes them. A sole source must have deposited more notes than the target had withdrawn, at every date.
Because the cluster’s second wallet was already withdrawing on 25 January 2025, every theft that happened later in 2025 is ruled out as a sole source, however well the timing fits. That single constraint did more to narrow this case than any positive match, and it took an afternoon.
Two further checks that returned nothing
These sit outside the nine. They are recorded because a stated dead end saves the next person the afternoon.
Tornado’s optional note-account feature publishes encrypted notes as events from the depositor’s own address, which would link deposits to a key. The contract recorded no transactions at all across the whole period. Nobody used it.
Whether the withdrawals bypassed the public mempool would be a rare, strong fingerprint, and the relay operator would hold the addresses. Public explorers carry no marker for it, so it stays open.
The one that worked, and it never touched the mixer
After all of the above, the case broke on something much simpler.
Stop trying to see backwards through the mixer. Follow the money forwards.
From the first withdrawal onward, every step is a plain transfer, a labelled contract call, or a bridge. Bridges are the opposite of a mixer: Symbiosis, Relay, Stargate, Across and LI.FI all publish per-transaction status keyed by the source hash. You look it up.
Doing that resolved the whole trail: about $9m to BNB Smart Chain, where the same private keys had been buying a thin token in roughly 30,000 small purchases for nine months, which ended in an attack on a lending protocol, whose proceeds came back to the same wallet, and went back into the mixer. It is a loop, and the mixer is only one arc of it.
One private key produces the same address on every EVM chain. That is not a heuristic, it is the derivation. An address active on a second chain is the same key control, and that needs no argument. It is not the same as the same person: a key can be automated, shared, delegated, or held by a service on someone’s behalf.
What we would tell anyone doing this
Measure the anonymity set first. It is one call and it tells you whether the rest is worth attempting.
Never publish a count match without a control. Ours matched three times and was wrong three times.
Count observations, not events. Eighteen days is eighteen data points however many transactions sit inside them.
Publish the null results. The note-account check and the hour-of-day profile both returned nothing, and saying so is what lets a reader judge the rest.
A mixer is a wall across one step, not around the whole path. The most productive hours of this investigation were spent looking the other way.
The full case these methods were run against, with every address, hash and figure, is in the forensic record.
Sources
- Tornado Cash 100 ETH pool contract, Etherscanetherscan.io
- Tornado Cash 0.1 ETH pool contract, Etherscanetherscan.io
- The Tornado Cash router, Etherscanetherscan.io
- The subject wallet 0x7a79969a, Etherscanetherscan.io
- Venus Protocol, THE market incident post-mortemcommunity.venus.io
- BlockSec, Venus Thena (THE) incident analysisblocksec.com


