tornado-demix 2.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. tornado_demix-2.13.0/LICENSE +21 -0
  2. tornado_demix-2.13.0/PKG-INFO +460 -0
  3. tornado_demix-2.13.0/README.md +427 -0
  4. tornado_demix-2.13.0/pyproject.toml +67 -0
  5. tornado_demix-2.13.0/setup.cfg +4 -0
  6. tornado_demix-2.13.0/tests/test_attribution.py +203 -0
  7. tornado_demix-2.13.0/tests/test_broadcaster.py +87 -0
  8. tornado_demix-2.13.0/tests/test_characterize.py +185 -0
  9. tornado_demix-2.13.0/tests/test_cli.py +488 -0
  10. tornado_demix-2.13.0/tests/test_cluster.py +361 -0
  11. tornado_demix-2.13.0/tests/test_cluster_gate.py +199 -0
  12. tornado_demix-2.13.0/tests/test_cluster_tokens.py +109 -0
  13. tornado_demix-2.13.0/tests/test_config.py +200 -0
  14. tornado_demix-2.13.0/tests/test_constants.py +25 -0
  15. tornado_demix-2.13.0/tests/test_core_edges.py +278 -0
  16. tornado_demix-2.13.0/tests/test_demix.py +853 -0
  17. tornado_demix-2.13.0/tests/test_edge_cases.py +389 -0
  18. tornado_demix-2.13.0/tests/test_encoding.py +139 -0
  19. tornado_demix-2.13.0/tests/test_ens_labels.py +110 -0
  20. tornado_demix-2.13.0/tests/test_errors.py +227 -0
  21. tornado_demix-2.13.0/tests/test_etherscan.py +536 -0
  22. tornado_demix-2.13.0/tests/test_events.py +114 -0
  23. tornado_demix-2.13.0/tests/test_graph.py +209 -0
  24. tornado_demix-2.13.0/tests/test_groups.py +82 -0
  25. tornado_demix-2.13.0/tests/test_multi.py +369 -0
  26. tornado_demix-2.13.0/tests/test_networks.py +180 -0
  27. tornado_demix-2.13.0/tests/test_packaging.py +75 -0
  28. tornado_demix-2.13.0/tests/test_placebo.py +254 -0
  29. tornado_demix-2.13.0/tests/test_pools.py +161 -0
  30. tornado_demix-2.13.0/tests/test_properties.py +367 -0
  31. tornado_demix-2.13.0/tests/test_registry_validation.py +106 -0
  32. tornado_demix-2.13.0/tests/test_report.py +950 -0
  33. tornado_demix-2.13.0/tests/test_rpc.py +257 -0
  34. tornado_demix-2.13.0/tests/test_scoring.py +394 -0
  35. tornado_demix-2.13.0/tests/test_scoring_independence.py +222 -0
  36. tornado_demix-2.13.0/tests/test_sensitivity.py +70 -0
  37. tornado_demix-2.13.0/tests/test_shipped_registry.py +198 -0
  38. tornado_demix-2.13.0/tests/test_simulate.py +54 -0
  39. tornado_demix-2.13.0/tests/test_thresholds.py +118 -0
  40. tornado_demix-2.13.0/tests/test_trace.py +123 -0
  41. tornado_demix-2.13.0/tests/test_verify_pools.py +272 -0
  42. tornado_demix-2.13.0/tests/test_webui.py +484 -0
  43. tornado_demix-2.13.0/tests/test_windows.py +202 -0
  44. tornado_demix-2.13.0/tornado_demix/__init__.py +25 -0
  45. tornado_demix-2.13.0/tornado_demix/__main__.py +6 -0
  46. tornado_demix-2.13.0/tornado_demix/attribution.py +113 -0
  47. tornado_demix-2.13.0/tornado_demix/characterize.py +165 -0
  48. tornado_demix-2.13.0/tornado_demix/cli.py +698 -0
  49. tornado_demix-2.13.0/tornado_demix/cluster.py +243 -0
  50. tornado_demix-2.13.0/tornado_demix/config.py +165 -0
  51. tornado_demix-2.13.0/tornado_demix/constants.py +37 -0
  52. tornado_demix-2.13.0/tornado_demix/data/networks.csv +56 -0
  53. tornado_demix-2.13.0/tornado_demix/demix.py +770 -0
  54. tornado_demix-2.13.0/tornado_demix/errors.py +39 -0
  55. tornado_demix-2.13.0/tornado_demix/etherscan.py +525 -0
  56. tornado_demix-2.13.0/tornado_demix/events.py +101 -0
  57. tornado_demix-2.13.0/tornado_demix/graph.py +121 -0
  58. tornado_demix-2.13.0/tornado_demix/groups.py +98 -0
  59. tornado_demix-2.13.0/tornado_demix/heuristics.py +603 -0
  60. tornado_demix-2.13.0/tornado_demix/multi.py +356 -0
  61. tornado_demix-2.13.0/tornado_demix/networks.py +343 -0
  62. tornado_demix-2.13.0/tornado_demix/placebo.py +163 -0
  63. tornado_demix-2.13.0/tornado_demix/pools.py +94 -0
  64. tornado_demix-2.13.0/tornado_demix/relayer.py +150 -0
  65. tornado_demix-2.13.0/tornado_demix/report.py +38 -0
  66. tornado_demix-2.13.0/tornado_demix/report_csv.py +488 -0
  67. tornado_demix-2.13.0/tornado_demix/report_html.py +1447 -0
  68. tornado_demix-2.13.0/tornado_demix/report_json.py +107 -0
  69. tornado_demix-2.13.0/tornado_demix/rpc.py +182 -0
  70. tornado_demix-2.13.0/tornado_demix/trace.py +191 -0
  71. tornado_demix-2.13.0/tornado_demix.egg-info/PKG-INFO +460 -0
  72. tornado_demix-2.13.0/tornado_demix.egg-info/SOURCES.txt +74 -0
  73. tornado_demix-2.13.0/tornado_demix.egg-info/dependency_links.txt +1 -0
  74. tornado_demix-2.13.0/tornado_demix.egg-info/entry_points.txt +2 -0
  75. tornado_demix-2.13.0/tornado_demix.egg-info/requires.txt +14 -0
  76. tornado_demix-2.13.0/tornado_demix.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tornado-demix contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,460 @@
1
+ Metadata-Version: 2.4
2
+ Name: tornado-demix
3
+ Version: 2.13.0
4
+ Summary: Tornado Cash forensics: evidence-banded exit leads, exit groups, operator clusters and FIFO tracing on 8 EVM chains, with a placebo check on real data.
5
+ Author: tornado-demix contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://prettydeath.github.io/TornadoCash-demixer/
8
+ Project-URL: Repository, https://github.com/prettydeath/TornadoCash-demixer
9
+ Project-URL: Documentation, https://prettydeath.github.io/TornadoCash-demixer/
10
+ Project-URL: Issues, https://github.com/prettydeath/TornadoCash-demixer/issues
11
+ Keywords: ethereum,tornado-cash,blockchain,forensics,demixing,etherscan,osint,aml,deanonymization,evm
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Intended Audience :: Information Technology
16
+ Classifier: Topic :: Security :: Cryptography
17
+ Classifier: Topic :: Security
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: requests<3,>=2.32.4
22
+ Provides-Extra: web
23
+ Requires-Dist: flask<4,>=3.0; extra == "web"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest<10,>=8.0; extra == "dev"
26
+ Requires-Dist: ruff<0.17,>=0.6; extra == "dev"
27
+ Requires-Dist: hypothesis<7,>=6.100; extra == "dev"
28
+ Requires-Dist: pycryptodome<4,>=3.20; extra == "dev"
29
+ Provides-Extra: labels
30
+ Requires-Dist: pycryptodome<4,>=3.20; extra == "labels"
31
+ Requires-Dist: eth-abi<6,>=5; extra == "labels"
32
+ Dynamic: license-file
33
+
34
+ # TornadoCash Demixer
35
+
36
+ Probabilistic demixing of Tornado Cash deposits using only public on-chain data.
37
+ The toolkit links a depositor wallet to likely withdrawal addresses and reports
38
+ each candidate with an evidence band and the evidence behind it. A band needs a
39
+ linked address (a direct counterparty, or a withdrawal sent by the depositor's
40
+ side): on real depositors amount+timing and gas-price matches turn up as often by
41
+ chance as for real exits, so they only corroborate. It also groups exits paid out
42
+ together, links wallets of one operator and follows withdrawn funds forward.
43
+
44
+ Output is a set of leads for further investigation, not proof. Tornado Cash breaks
45
+ the deposit-withdrawal link cryptographically; the tool looks for behavioural
46
+ traces users leave around it.
47
+
48
+ ## What it does
49
+
50
+ - Finds a wallet's deposits into the 55 registered pools on 8 EVM networks
51
+ (native transfers, internal transfers from contract wallets, ERC-20 transfers,
52
+ direct or through a router).
53
+ - Groups deposits into vouchers (one pool, one session, `--gap-hours`, default 24)
54
+ and gives each voucher its own withdrawal search window.
55
+ - Reads the pools' `Withdrawal` events in those windows and counts how many
56
+ withdrawals each recipient received. A recipient that received exactly *N*
57
+ withdrawals matches a voucher of *N* notes.
58
+ - Scores every recipient: a count match weighted by how much of the recipient
59
+ field it eliminates (`disc`; counted only at `disc >= 0.5` in a field of at
60
+ least 5 recipients), self-relayed withdrawals, reuse of a deposit gas price
61
+ (only in blocks without an EIP-1559 base fee), and direct transactions with the
62
+ depositor (not counted when the counterparty is a contract).
63
+ - Reports a band per candidate: `strong` (a linked address plus another evidence
64
+ family), `moderate` (a linked address alone), `weak` (amount+timing and/or gas
65
+ price without a linked address — chance-level on real depositors, see the
66
+ placebo test in [EVALUATION.md](https://github.com/prettydeath/TornadoCash-demixer/blob/main/docs/EVALUATION.md)), with the evidence behind it: every family, whether it holds,
67
+ and the numbers (share of the recipient field with the same count, `disc`,
68
+ gas price). Candidates are ordered by band, then by an uncalibrated noisy-OR
69
+ score taken across evidence families (within a family only the strongest
70
+ signal counts). Reports state the analysis parameters and the block ranges read.
71
+ - Marks the top candidates that had at most a day of history before their first
72
+ withdrawal (fresh, disposable exits; shown as context, never scored).
73
+ - Groups exits withdrawn together in repeated bursts, the payout rhythm of an
74
+ operator that pooled several deposits; a group is listed when a corroborated
75
+ candidate or an exit you already know (`--known-exit`) anchors it.
76
+ - Marks a linked exit that withdrew within 72 hours of the deposit (early exit;
77
+ context, never scored).
78
+ - Optionally runs a placebo check (`--placebo`, or the web UI option): the same
79
+ analysis on a decoy window that ends before the wallet's first deposit, so the
80
+ report shows how many leads chance alone produces for this wallet.
81
+ - Opens the HTML report with a case overview and a flow diagram: depositor, pools,
82
+ candidate exits coloured by band.
83
+ - For several wallets: shared candidates, denomination-profile matches,
84
+ cross-wallet consolidators graded against the window-overlap artefact, deposit
85
+ synchronicity, shared private funders, and operator clusters (union-find on
86
+ discriminating edges only).
87
+ - Traces split exits one hop forward to a common collection address (`cluster`).
88
+ - Follows withdrawn funds forward over several hops with FIFO attribution and
89
+ DEX-swap resolution, up to a labelled service (`trace`).
90
+ - Characterises an exit candidate: activity, pool inflows, next hops, a disposable
91
+ flag, optional address labels (`characterize`).
92
+
93
+ The method, thresholds and the reasoning behind them are in
94
+ [docs/METHODOLOGY.md](https://github.com/prettydeath/TornadoCash-demixer/blob/main/docs/METHODOLOGY.md).
95
+
96
+ ## Components
97
+
98
+ | Module | Role |
99
+ |---|---|
100
+ | `tornado_demix/demix.py` | deposit detection, vouchers, search windows, withdrawal counting |
101
+ | `tornado_demix/heuristics.py` | signals, `count_discrimination`, noisy-OR score, bands |
102
+ | `tornado_demix/multi.py`, `graph.py` | multi-wallet correlation, consolidator grading, clustering |
103
+ | `tornado_demix/cluster.py` | one-hop tracing of split exits, on-disk cache |
104
+ | `tornado_demix/groups.py` | exit groups from joint withdrawal bursts |
105
+ | `tornado_demix/trace.py` | multi-hop FIFO tracing of withdrawn funds with swap resolution |
106
+ | `tornado_demix/characterize.py`, `attribution.py` | recipient-side analysis, address labels |
107
+ | `tornado_demix/etherscan.py`, `rpc.py`, `events.py` | explorer client, JSON-RPC probes, event decoding |
108
+ | `tornado_demix/networks.py`, `pools.py`, `data/networks.csv` | verified pool registry |
109
+ | `tornado_demix/report.py` (`report_csv`, `report_html`, `report_json`) | CSV, self-contained HTML and JSON reports |
110
+ | `tornado_demix/cli.py`, `webui/app.py` | command line and local Flask UI over the same functions |
111
+ | `tools/verify_pools.py` | on-chain verification of registry entries |
112
+ | `tools/calibrate.py` | precision/recall against confirmed cases (needs private data) |
113
+ | `tools/sensitivity.py` | ranking stability of saved results under perturbed weights |
114
+ | `tools/simulate.py` | synthetic benchmark: ablation, negative controls, counter-measures ([results](https://github.com/prettydeath/TornadoCash-demixer/blob/main/docs/EVALUATION.md)) |
115
+ | `tools/real_cases.py` | the method on two public laundering cases (KuCoin, Harmony) |
116
+ | `tools/ens_labels.py`, `tools/evaluate_labels.py` | an ENS-labelled set of depositor/exit pairs and the evaluation on it ([results](https://github.com/prettydeath/TornadoCash-demixer/blob/main/docs/EVALUATION.md#a-labelled-set-from-ens)) |
117
+ | `tools/wang_baseline.py` | the ENS set scored under the Wang et al. (2023) protocol, with their H2/H3/H5 re-implemented |
118
+ | `tools/placebo_eval.py`, `tools/placebo_windows.py` | placebo (target-decoy) test on random real depositors, per evidence family and exit window ([results](https://github.com/prettydeath/TornadoCash-demixer/blob/main/docs/EVALUATION.md#placebo-test-on-real-depositors)) |
119
+ | `tools/review_sample.py` | a blinded manual-review sheet of leads and hidden controls, and its scoring |
120
+
121
+ ## Requirements
122
+
123
+ - Python 3.9 or newer. Runtime dependency: `requests`. The web UI adds `flask`.
124
+ - An Etherscan V2 API key. One key covers Ethereum, BNB Smart Chain, Polygon,
125
+ Arbitrum, Base and Gnosis. Avalanche (Routescan) and Optimism (Blockscout) need
126
+ no key. On the free Etherscan plan the `getLogs` endpoint was not
127
+ available for BNB Smart Chain, Gnosis and Base at the time of writing.
128
+
129
+ ## Installation
130
+
131
+ From a checkout of the repository:
132
+
133
+ ```bash
134
+ python -m pip install . # core: the tornado-demix command and the pool registry
135
+ python -m pip install ".[web]" # plus Flask; run the UI from a checkout
136
+ python -m pip install -e ".[dev]" # for development: tests and ruff
137
+ ```
138
+
139
+ Dependencies and their version bounds are declared in `pyproject.toml`.
140
+
141
+ ## Configuration
142
+
143
+ Copy `config/api.csv.example` to `config/api.csv` and put your key in it, or set
144
+ `ETHERSCAN_API_KEY`:
145
+
146
+ ```csv
147
+ service,api_key
148
+ etherscan,YOUR_KEY
149
+ ```
150
+
151
+ Config files are looked up in `$TORNADO_DEMIX_CONFIG` (only there, when set),
152
+ then `./config/`, then the repository's `config/`. One directory per case keeps
153
+ case inputs apart. `config/wallets.csv` (column `address`) can replace addresses
154
+ on the command line. `config/networks.csv`, if present, overrides the bundled
155
+ registry.
156
+
157
+ ### Address labels (optional)
158
+
159
+ Labels are not bundled. The tool reads one `<network>.csv` per chain
160
+ (`ethereum.csv`, `bsc.csv`, ...) with an `address` column and optionally `entity`,
161
+ `label`, `category`, `source`, `confidence`, from the directory given by
162
+ `--attribution-dir` or `TORNADO_DEMIX_ATTRIBUTION`.
163
+
164
+ A ready dataset in this format is
165
+ [prettydeath/wallet-attribution](https://github.com/prettydeath/wallet-attribution):
166
+ about 115,000 addresses (exchanges, OFAC-sanctioned entities, scams, mixers,
167
+ bridges, DeFi) on 47 networks, covering all eight networks of this tool.
168
+
169
+ ```bash
170
+ git clone https://github.com/prettydeath/wallet-attribution
171
+ python -m tornado_demix characterize <address> --attribution-dir wallet-attribution/data
172
+ ```
173
+
174
+ It is aggregated from public sources: the OFAC SDN address list (US government
175
+ public record, via 0xB10C/ofac-sanctioned-digital-currency-addresses),
176
+ dawsbot/eth-labels, tradezon/cex-list, MyEtherWallet/ethereum-lists and the
177
+ proof-of-reserves wallets in DefiLlama-Adapters. The dataset's code is MIT; each
178
+ label keeps the license of its upstream source, so check those before
179
+ redistributing labels or using them commercially.
180
+
181
+ Labels are only displayed. They never change a score or a band, and a label is
182
+ a claim by its source, not a finding of this tool.
183
+
184
+ ## Command line
185
+
186
+ ```bash
187
+ # one wallet: CSV files, an HTML report and the full result as JSON
188
+ python -m tornado_demix demix 0x019b5bb2051797e33f726d0e7a8cb9b9c2003ac2 \
189
+ --network ethereum --out-dir out/demix --report out/demix.html --json out/demix.json
190
+
191
+ # narrow the search to 6 hours after each voucher's last deposit
192
+ python -m tornado_demix demix <wallet> --exit-window 6
193
+
194
+ # list the exit group of an exit already known from the investigation
195
+ python -m tornado_demix demix <wallet> --known-exit <exit address>
196
+
197
+ # also run the same analysis on a decoy window before the first deposit
198
+ python -m tornado_demix demix <wallet> --placebo
199
+
200
+ # a 7-day exit window, as seen in public laundering cases
201
+ python -m tornado_demix demix <wallet> --rapid
202
+
203
+ # several wallets
204
+ python -m tornado_demix multi <wallet1> <wallet2> --report out/multi.html
205
+ python -m tornado_demix multi --wallets-csv config/wallets.csv
206
+
207
+ # split exits, one hop forward (resumable cache in --cache-dir)
208
+ python -m tornado_demix cluster <wallet> --cache-dir .cache/case-42
209
+
210
+ # describe an exit candidate
211
+ python -m tornado_demix characterize <address> --attribution-dir path/to/labels
212
+
213
+ # follow 10 ETH withdrawn to <address> in block 12000000 over up to 4 hops
214
+ python -m tornado_demix trace <address> --amount 10 --start-block 12000000 --report out/trace.html
215
+ ```
216
+
217
+ `python -m tornado_demix <command> --help` lists every option. A configuration or
218
+ provider error exits with a non-zero status and a one-line message. Progress goes
219
+ to stderr; the summary to stdout.
220
+
221
+ ## Web UI
222
+
223
+ ```bash
224
+ python webui/app.py # http://127.0.0.1:5000
225
+ ```
226
+
227
+ The form runs the same five analyses. For a demix run it shows the strongest
228
+ band, the ranked candidates with their band and score, and for each candidate an
229
+ **Evidence** panel listing every evidence family that was checked, whether it
230
+ holds, and the numbers behind it. **Analysis parameters** lists the assumptions
231
+ (voucher gap, window, thresholds) and the block ranges read. The CSV table, the
232
+ HTML report and the full JSON result can be downloaded.
233
+
234
+ For `trace`, enter one exit address, the amount to follow and optionally the
235
+ block the funds arrived in, an ERC-20 token and the number of hops (up to 8). The
236
+ page lists where the traced funds stop and why (labelled address, hop limit, a
237
+ contract that paid nothing back, not moved on) and every edge with its attributed
238
+ amount, swaps included; the HTML report, CSV and JSON can be downloaded.
239
+
240
+ ![demix in the web UI with the evidence panel open](https://raw.githubusercontent.com/prettydeath/TornadoCash-demixer/main/docs/img/demix-evidence.png)
241
+ *A demix run on Avalanche: two vouchers, a `strong` candidate (count match plus a direct transaction with the depositor) and the evidence panel listing every family that was checked.*
242
+
243
+ The UI binds to localhost and has no authentication (forms carry a CSRF token); see
244
+ [SECURITY.md](https://github.com/prettydeath/TornadoCash-demixer/blob/main/SECURITY.md).
245
+
246
+ ## Documented cases
247
+
248
+ Five public cases with an external source of truth. Each can be re-run from the
249
+ addresses shown (an Etherscan API key is enough; the Ronin labels need an
250
+ attribution set). The screenshots are from the web UI; the CLI prints the same
251
+ results. A new run can show slightly different counts as the chain grows and open
252
+ search windows are clamped to the current block.
253
+
254
+ | # | Case | Analysis | What it shows | Source of truth |
255
+ |---|---|---|---|---|
256
+ | 1 | Ronin Bridge 2022 | `characterize` | OFAC-labelled exploiter, bridge and USDC calls, 12,595 ETH to a second sanctioned address | [OFAC, 14 Apr 2022](https://ofac.treasury.gov/recent-actions/20220414) |
257
+ | 2 | Wintermute 2022 | `characterize` | one 9.9435 ETH inflow from the 10 ETH pool; a disposable address | [Merkle Science](https://www.merklescience.com/blog/hack-track-analysis-of-wintermute-attack) |
258
+ | 3 | Beanstalk 2022 | `demix --exit-window 24` | 271 deposits in three hours; no candidate above `weak` | [Merkle Science](https://www.merklescience.com/blog/hack-track-analysis-of-beanstalk-flash-loan-attack) |
259
+ | 4 | KuCoin 2020 | `demix` | 6 of 7 exits found through the linked withdrawal sender | [tayvano/lazarus-bluenoroff-research](https://github.com/tayvano/lazarus-bluenoroff-research) |
260
+ | 5 | Harmony 2022 | `demix --known-exit`, `multi`, `trace` | an exit group from one known exit; 14 depositors in 5 funder clusters; a three-hop trace | [tayvano/lazarus-bluenoroff-research](https://github.com/tayvano/lazarus-bluenoroff-research) |
261
+
262
+ Attributing the Ronin, KuCoin and Harmony thefts to Lazarus is the conclusion of
263
+ government and industry investigators, not of on-chain analysis; the identity of
264
+ the Wintermute and Beanstalk attackers is not established.
265
+
266
+ ### 1. Ronin Bridge exploiter
267
+
268
+ ```bash
269
+ python -m tornado_demix characterize 0x098B716B8Aaf21512996dC57EB0615e2383E2f96
270
+ ```
271
+
272
+ The exploiter received nothing from a Tornado pool; `characterize` shows where
273
+ its funds went next. With the attribution set loaded, the address itself and the
274
+ 12,595.3 ETH destination are labelled OFAC-sanctioned, and the contract calls go
275
+ to Circle: USDC and the Ronin Bridge. Without labels the same hops are shown
276
+ unlabelled.
277
+
278
+ ![Ronin exploiter in characterize](https://raw.githubusercontent.com/prettydeath/TornadoCash-demixer/main/docs/img/case-ronin.png)
279
+ *`characterize` on the Ronin exploiter: attribution labels, no pool inflows, and the dominant next hops with the 12,595.3 ETH transfer to a second sanctioned address.*
280
+
281
+ ### 2. Wintermute attacker
282
+
283
+ ```bash
284
+ python -m tornado_demix characterize 0xe74b28c2eAe8679e3cCc3a94d5d0dE83CCB84705
285
+ ```
286
+
287
+ One inflow of 9.9435 ETH from the 10 ETH pool on the day of the attack
288
+ (2022-09-20), the first activity of the address: it is flagged as disposable, the
289
+ pattern of 98.6 % of laundering exits in the MixLaunder cases.
290
+
291
+ ![Wintermute attacker in characterize](https://raw.githubusercontent.com/prettydeath/TornadoCash-demixer/main/docs/img/case-wintermute.png)
292
+ *`characterize` on the Wintermute attacker: one pool inflow, classified as a possible personal exit and flagged disposable.*
293
+
294
+ ### 3. Beanstalk attacker
295
+
296
+ ```bash
297
+ python -m tornado_demix demix 0x1c5dCdd006EA78a7E4783f9e6021C32935a10fb4 --exit-window 24
298
+ ```
299
+
300
+ 271 deposits (247 x 100, 14 x 10, 9 x 1 and 1 x 0.1 ETH) within about three
301
+ hours. With a 24-hour exit window the count match leaves one `weak` candidate and
302
+ nothing corroborated: a careful operator leaves no lead, and the tool says so
303
+ rather than naming an unrelated address.
304
+
305
+ ![Beanstalk attacker in demix](https://raw.githubusercontent.com/prettydeath/TornadoCash-demixer/main/docs/img/case-beanstalk.png)
306
+ *`demix` on the Beanstalk attacker: four vouchers, 271 notes, and a single weak count-match candidate.*
307
+
308
+ ### 4. KuCoin hack (2020)
309
+
310
+ ```bash
311
+ python -m tornado_demix demix 0x820a7a97dd146fd97f79881afdf4767624973368
312
+ ```
313
+
314
+ The attacker called `withdraw()` itself from an address investigators attribute
315
+ to it: 128 withdrawals to 7 exits. No exit received a voucher-sized count (each
316
+ got 11-29 notes against vouchers of 24 and 30 notes), so the count match finds nothing. The
317
+ depositor had transacted with that caller, so every withdrawal it sent marks its
318
+ recipient (`linked_sender`): 6 candidates, all 6 true exits; the seventh
319
+ received its withdrawals months later, outside the window. The report opens with
320
+ a flow diagram from the depositor through the pool to the candidates.
321
+
322
+ ![KuCoin depositor in demix](https://raw.githubusercontent.com/prettydeath/TornadoCash-demixer/main/docs/img/case-kucoin.png)
323
+ *`demix` on a KuCoin depositor: six moderate candidates, each an exit that received withdrawals sent by the depositor's counterparty; the evidence panel shows which families hold.*
324
+
325
+ ![KuCoin case overview in the HTML report](https://raw.githubusercontent.com/prettydeath/TornadoCash-demixer/main/docs/img/case-kucoin-flow.png)
326
+ *The case overview of the HTML report: depositor, pool and the six candidates, coloured by band.*
327
+
328
+ With `--placebo` the same analysis also runs on a decoy window that ends a day
329
+ before the first deposit, where no withdrawal can spend this depositor's notes:
330
+
331
+ ![Placebo check on the KuCoin depositor](https://raw.githubusercontent.com/prettydeath/TornadoCash-demixer/main/docs/img/case-kucoin-placebo.png)
332
+ *The placebo check: six `moderate` leads in the real window, none in the decoy window; the one decoy candidate is `weak`.*
333
+
334
+ ### 5. Harmony Bridge hack (2022)
335
+
336
+ Investigators listed 14 depositors and 55 withdrawal addresses. The attacker
337
+ pooled 857 notes of the 100 ETH pool and paid them out mostly six at a time, so
338
+ the count match finds no exit and no false one. Three other analyses do.
339
+
340
+ **Exit groups.** Starting from one exit known from the investigation, `demix`
341
+ lists the recipients paid out in the same bursts:
342
+
343
+ ```bash
344
+ python -m tornado_demix demix 0xe71d5fa89d1086d5c3b0ab03eeee2483d2d5ca97 \
345
+ --known-exit 0x0562ddf7ea5ab56728852eea2eacab61c4b78a1a
346
+ ```
347
+
348
+ ![Harmony exit group from one known exit](https://raw.githubusercontent.com/prettydeath/TornadoCash-demixer/main/docs/img/case-harmony-groups.png)
349
+ *An exit group anchored by one known exit: 22 addresses paid out in joint bursts, 20 of them on the investigators' list. Across all depositors, one known exit gives 23 addresses on average, 73 % of them on the list.*
350
+
351
+ **Shared funders.** Correlating the 14 depositors links all of them through five
352
+ immediate funders, each of which was paid directly by the bridge exploiter
353
+ `0x0d04...ded00`; a sixth, busy funder is left out:
354
+
355
+ ```bash
356
+ python -m tornado_demix multi --wallets-csv docs/cases/harmony_depositors.csv
357
+ ```
358
+
359
+ ![Harmony depositors linked by shared funders](https://raw.githubusercontent.com/prettydeath/TornadoCash-demixer/main/docs/img/case-harmony-funders.png)
360
+ *`multi` on the 14 Harmony depositors: operator clusters linked by a shared funder, and the funders themselves.*
361
+
362
+ **Trace.** From an exit, `trace` follows the 100 ETH it received forward:
363
+
364
+ ```bash
365
+ python -m tornado_demix trace 0x04bca8fa79f36749fa605597e9c9f6788c126944 --amount 100 --max-hops 3
366
+ ```
367
+
368
+ ![Three-hop trace from a Harmony exit](https://raw.githubusercontent.com/prettydeath/TornadoCash-demixer/main/docs/img/case-harmony-trace.png)
369
+ *`trace` from a Harmony exit: the 100 ETH moves through two intermediate addresses; the trace stops at the hop limit.*
370
+
371
+ The method on both public cases, with the numbers behind each claim, is in
372
+ [docs/EVALUATION.md](https://github.com/prettydeath/TornadoCash-demixer/blob/main/docs/EVALUATION.md); `python tools/real_cases.py` repeats it.
373
+
374
+ ## Limitations
375
+
376
+ - Leads, not proof. The weights and `MIN_COUNT_DISCRIMINATION = 0.5` are expert
377
+ judgements; the score has not been calibrated on cases with known outcomes.
378
+ The band does not depend on the weights at all; they only order candidates
379
+ within a band (check a saved result with `tools/sensitivity.py`). How each
380
+ component behaves on generated data with a known answer is in
381
+ [docs/EVALUATION.md](https://github.com/prettydeath/TornadoCash-demixer/blob/main/docs/EVALUATION.md).
382
+ - On real depositors the count match, self-relay and gas-price reuse do not beat
383
+ chance: a placebo test on 152 random Ethereum depositors found as many such
384
+ leads in decoy windows before the first deposit as in the real windows, at every
385
+ window from 6 hours to 30 days. Only a linked address (a direct counterparty, or
386
+ a withdrawal sent by the depositor's side) stood above chance (30 against 7 over
387
+ 30 days, 19 against 1 within 72 hours), so only it makes a `moderate` or `strong`
388
+ band ([details](https://github.com/prettydeath/TornadoCash-demixer/blob/main/docs/EVALUATION.md#placebo-test-on-real-depositors)).
389
+ - On 31 depositor/exit pairs labelled through ENS (2019-2026), demix found 16 of
390
+ the 21 pairs inside its window, all through a direct transaction between the
391
+ two addresses, which the label sees as well; without that signal it found one.
392
+ Most labelled depositors made single-note deposits, which the count match
393
+ cannot narrow ([details](https://github.com/prettydeath/TornadoCash-demixer/blob/main/docs/EVALUATION.md#a-labelled-set-from-ens)).
394
+ - Single-note vouchers cannot be narrowed by count: every recipient in the
395
+ window has count 1.
396
+ - Busy pools and long windows produce many equal counts. `--exit-window` trades
397
+ recall for discrimination.
398
+ - A careful user defeats the method: relayers, long delays, split withdrawals
399
+ to fresh addresses. The result is then "no lead", or weak leads on unrelated
400
+ addresses that happen to share the voucher count, never a `moderate` or `strong` one.
401
+ On the public KuCoin and Harmony laundering cases the count match found no exit
402
+ (every exit collected notes of several deposits); in KuCoin the linked withdrawal
403
+ sender found 6 of 7 exits for one depositor ([docs/EVALUATION.md](https://github.com/prettydeath/TornadoCash-demixer/blob/main/docs/EVALUATION.md)).
404
+ - Only Tornado Cash pools in the registry are covered. Bridges, other mixers and
405
+ cross-chain hops are not followed; `cluster` follows one hop only, and `trace`
406
+ follows funds by FIFO attribution, a convention that does not hold when an
407
+ address mixes the traced funds with others.
408
+ - Native chains other than Ethereum, Polygon and Avalanche have no verified
409
+ router in the registry, so routed deposits there are not detected.
410
+ - `characterize` counts inflows from native pools only.
411
+ - Results depend on the explorer API. Provider failures raise errors instead of
412
+ returning empty results, and pools whose search window cannot be resolved are
413
+ reported as not searched.
414
+
415
+ ## Development
416
+
417
+ ```bash
418
+ python -m pytest -q # network access is blocked in tests
419
+ python -m pytest -m live # on-chain registry check, needs network
420
+ python -m ruff check . && python -m ruff format --check .
421
+ ```
422
+
423
+ Weight sensitivity of saved results (no API calls):
424
+
425
+ ```bash
426
+ python tools/sensitivity.py out/demix.json:<expected exit> --samples 1000 --spread 0.5
427
+ ```
428
+
429
+ Synthetic benchmark (no API calls, about half a minute):
430
+
431
+ ```bash
432
+ python tools/simulate.py --experiment all --trials 200 --seed 1
433
+ ```
434
+
435
+ Public laundering cases (KuCoin 2020, Harmony 2022; needs an API key):
436
+
437
+ ```bash
438
+ python tools/real_cases.py
439
+ ```
440
+
441
+ `requirements-lock.txt` pins every dependency with hashes (generated by
442
+ `uv pip compile pyproject.toml --extra web --extra dev --python-version 3.9
443
+ --generate-hashes --universal`); install it with
444
+ `pip install --require-hashes -r requirements-lock.txt` and then
445
+ `pip install --no-deps -e .` to reproduce a result with the same libraries.
446
+
447
+ CI runs the tests on Linux and Windows with Python 3.9 and 3.13 against the lock file, a test under a
448
+ legacy Windows codepage, the linter, and a wheel install check.
449
+
450
+ ## Legal and ethical use
451
+
452
+ For research, compliance and authorised investigations. Every address processed
453
+ is public on-chain data, but a label or a band attached to an address is an
454
+ allegation about a person. Corroborate any lead independently before it appears
455
+ in a report, a referral or a freeze request, and follow the terms of service of
456
+ the data providers you use.
457
+
458
+ ## License
459
+
460
+ MIT, see [LICENSE](https://github.com/prettydeath/TornadoCash-demixer/blob/main/LICENSE).