Skip to content

Instantly share code, notes, and snippets.

@pcaversaccio
Last active February 19, 2026 19:11
Show Gist options
  • Select an option

  • Save pcaversaccio/ea7f62fd21b6e22f301980007f7c767e to your computer and use it in GitHub Desktop.

Select an option

Save pcaversaccio/ea7f62fd21b6e22f301980007f7c767e to your computer and use it in GitHub Desktop.
Review Tornado Cash Proposal 65

Review Tornado Cash Proposal 65

Proposal #65: Update ETH RPC List and ENS

Reason: The current ETH RPC List on the UI no longer support historical queries, necessitating an update to the default ETH RPC list.

UI Update Details:

ETH RPCs:  blockscoutRPC,blastRPC,xrpc, gasHawkRPC, lavaRPC,torndaoRPC,sentioRPC,tornadoRPC

Target: Update the IPFS hash for the Tornado Cash classic UI associated with the ENS domain tornadocash.eth.
        
UI Code Repository: https://codeberg.org/torndao/classic-ui/commits/branch/development
        
UI Code commit: https://codeberg.org/torndao/classic-ui/commit/d52c01688f5697e60a1d3c598a4c30989403fb0f
        
IPFS Hash: bafybeie5hxovqc4ifcnrnhvmjbefxgeix6oqvzaspyytdxiyscji22v5pu (accessible via https://ipfs.io/ipfs/bafybeie5hxovqc4ifcnrnhvmjbefxgeix6oqvzaspyytdxiyscji22v5pu)
        
IPFS Hash Verification Tool: https://codeberg.org/torndao/tornado-ipfs-ui
        
Content Hash: e301017012209d3ddd580b88289b169eac48485b9888bf9d0ae4127e3131dd1890928d6abd7d (calculated using the tool at https://codeberg.org/torndao/tornado-ipfs-ui/src/branch/main/ContentHash.html)
       
Affected DomainsThe updated IPFS hash will affect the following domains:
tornadocash.eth.limo
tornadocash.eth.link
ipfs.io/ipns/tornadocash.eth
tornadocash-eth.ipns.dweb.link

This proposal passed with 330.32k TORN (72%) voting For and 125.59k TORN (28%) voting Against. The required quorum of 100k TORN was met, and the proposal was successfully executed here (Feb-14-2026 12:20:23 PM UTC).

Analysis

Relevant Links

There are three new commits compared to the last version (https://tornadocash.eth.limo/governance/63; https://codeberg.org/torndao/classic-ui/commit/30c4b44a5cd96811e17ef46d921ea8e1841a05d0; https://ipfs.io/ipfs/bafybeiadcezfjx4ewel2xbdic66a4oj5ei6wtrvcbnirao5543cqtckukm):

Commit Analysis

  • Commit 6ed828199d8a9eb4fedec2764712af81bd13a7e5 (commit message: "Update the @tornado library file URL.") is the most dangerous one since it changes the npm registry link to @tornado:registry=https://codeberg.org/api/packages/torndao/npm/ (see https://codeberg.org/torndao/-/packages) and also updates the yarn.lock file.

The SHA-1 and SHA-512 hashes have been asserted via this Bash script:

#!/usr/bin/env bash

set -Eeuo pipefail

# See https://codeberg.org/torndao/classic-ui/commit/6ed828199d8a9eb4fedec2764712af81bd13a7e5.
declare -A sha1_expected=(
	["fixed-merkle-tree"]="dfcb5e870513e3d0b9300a5c3cb471b7dbddba96"
	["gas-price-oracle"]="2bfecf70437d22e33e8912a04d9e0149bf7b67de"
	["snarkjs"]="715aaf30248fffb9b7a1ee558e9d1b9a765a0e99"
	["tornado-config"]="0ab73fae5d6b95712396479911c6da9a6b407c89"
	["tornado-oracles"]="6a1766e0561a322b0be224f5e15fb085b30d5a7c"
	["websnark"]="c15196db16e8c5965bd7c9938692b4289fd4e284"
)

# See https://codeberg.org/torndao/classic-ui/commit/6ed828199d8a9eb4fedec2764712af81bd13a7e5.
declare -A sha512_expected=(
	["fixed-merkle-tree"]="IZ+NG1yIV9TPApuaqSzylcZn2ZYqBoG67hSVTif4dagsF4pvCF9NaM3YUrZPy1UdpJp4xps08DXlUuF/xlLIeg=="
	["gas-price-oracle"]="fHGLKxSMEWY0LtMj8ErRX4NcqTueryYPEmmNzJx+vNydAwx5ecjBQSV4zQPRNPrJdokn5WhLb/1SFGOBqHaSuA=="
	["snarkjs"]="Df0QvExjephUuWYWyXeIEZI91KZv+bBtIk/1aCWNXtglYl7ZnoeU3/BPvfR2JKRLR/WdxcFTXZSdKaIR10JlQg=="
	["tornado-config"]="HRcZtzKhVOhsotiuguGe0UMb/30nqFweRw+3PmnULLxKrXKV6zH9V1jj2R+6TBAeXFJwr8Yhe16BdHvG2/7SRg=="
	["tornado-oracles"]="m2m7+I+cpxCeGx2drq/IHuuzC6TsZKmagkaX2t4MDQaGr7xu367FDkpMhex1wvqPxrJSSqsp6gEVOQBvRxwR6Q=="
	["websnark"]="TZ2xanChs6CWCze2VfwlPbTbExdEblHZN8qKAXKAF1F5wkKfh4AZAUN+BnkSIWhc1eXJV/YPU74tQfgmFt5v7g=="
)

declare -A versions=(
	["fixed-merkle-tree"]="0.7.3"
	["gas-price-oracle"]="0.5.3"
	["snarkjs"]="0.1.20"
	["tornado-config"]="2.0.0"
	["tornado-oracles"]="2.1.0"
	["websnark"]="0.0.4"
)

for pkg in "${!versions[@]}"; do
	ver=${versions[$pkg]}
	url="https://codeberg.org/api/packages/torndao/npm/@tornado%2F$pkg/-/$pkg-$ver.tgz"

	echo "Checking $pkg@$ver..."

	# Download in memory and compute hashes.
	sha1_actual=$(curl -sL "$url" | sha1sum | awk '{print $1}')
	sha512_actual=$(curl -sL "$url" | openssl dgst -sha512 -binary | openssl base64 -A)

	# Assert the SHA-1 hashes.
	if [ "$sha1_actual" == "${sha1_expected[$pkg]}" ]; then
		echo "  SHA-1 checks out"
	else
		echo "  SHA-1 expected ${sha1_expected[$pkg]}, got $sha1_actual"
	fi

	# Assert the SHA-512 hashes.
	if [ "$sha512_actual" == "${sha512_expected[$pkg]}" ]; then
		echo "  SHA-512 checks out"
	else
		echo "  SHA-512 expected ${sha512_expected[$pkg]}, got $sha512_actual"
	fi

	echo
done

Let's look at the diffs of the different packages.

package-fixed-merkle-tree-old:

npm pack @tornado/fixed-merkle-tree@0.7.3 --registry=https://git.tornado.ws/api/packages/tornado-packages/npm/

package-fixed-merkle-tree-new:

npm pack @tornado/fixed-merkle-tree@0.7.3 --registry=https://codeberg.org/api/packages/torndao/npm/
diff -r -w -B package-fixed-merkle-tree-old package-fixed-merkle-tree-new --exclude="*.d.ts"
diff -r -w -B package-fixed-merkle-tree-old/package.json package-fixed-merkle-tree-new/package.json
5c5
<   "repository": "https://git.tornado.ws/tornado-packages/fixed-merkle-tree.git",
---
>   "repository": "https://codeberg.org/torndao/fixed-merkle-tree.git",

=> The diff looks safe. I excluded the .d.ts files from the comparison because they are compile-time type artifacts and cannot introduce runtime behaviour or supply-chain payloads.

package-gas-price-oracle-old:

npm pack @tornado/gas-price-oracle@0.5.3 --registry=https://git.tornado.ws/api/packages/tornado-packages/npm/

package-gas-price-oracle-new:

npm pack @tornado/gas-price-oracle@0.5.3 --registry=https://codeberg.org/api/packages/torndao/npm/
diff -r -w -B package-tornado-gas-price-oracle-old package-tornado-gas-price-oracle-new --exclude="*.d.ts"
Only in package-tornado-gas-price-oracle-new/lib: esm
diff -r -w -B '--exclude=*.d.ts' package-tornado-gas-price-oracle-old/lib/services/cacher/cacheNode.js package-tornado-gas-price-oracle-new/lib/services/cacher/cacheNode.js
17c17
<         while (g && (g = 0, op[0] && (_ = 0)), _) try {
---
>         while (_) try {
diff -r -w -B '--exclude=*.d.ts' package-tornado-gas-price-oracle-old/lib/services/gas-estimation/eip1559.js package-tornado-gas-price-oracle-new/lib/services/gas-estimation/eip1559.js
28c28
<         while (g && (g = 0, op[0] && (_ = 0)), _) try {
---
>         while (_) try {
diff -r -w -B '--exclude=*.d.ts' package-tornado-gas-price-oracle-old/lib/services/gas-price-oracle/gas-price-oracle.js package-tornado-gas-price-oracle-new/lib/services/gas-price-oracle/gas-price-oracle.js
28c28
<         while (g && (g = 0, op[0] && (_ = 0)), _) try {
---
>         while (_) try {
diff -r -w -B '--exclude=*.d.ts' package-tornado-gas-price-oracle-old/lib/services/legacy-gas-price/legacy.js package-tornado-gas-price-oracle-new/lib/services/legacy-gas-price/legacy.js
28c28
<         while (g && (g = 0, op[0] && (_ = 0)), _) try {
---
>         while (_) try {
diff -r -w -B '--exclude=*.d.ts' package-tornado-gas-price-oracle-old/lib/services/rpcFetcher/fetcher.js package-tornado-gas-price-oracle-new/lib/services/rpcFetcher/fetcher.js
17c17
<         while (g && (g = 0, op[0] && (_ = 0)), _) try {
---
>         while (_) try {
diff -r -w -B '--exclude=*.d.ts' package-tornado-gas-price-oracle-old/package.json package-tornado-gas-price-oracle-new/package.json
5c5
<   "homepage": "https://git.tornado.ws/tornado-packages/gas-price-oracle",
---
>   "homepage": "https://codeberg.org/torndao/gas-price-oracle",
11c11
<     "url": "https://git.tornado.ws/tornado-packages/gas-price-oracle"
---
>     "url": "https://codeberg.org/torndao/gas-price-oracle"
21,22c21
<     "prepare": "yarn build && yarn build:esm",
<     "prepublishOnly": "yarn test && yarn lint"
---
>     "prepare": "yarn build && yarn build:esm"

=> The diff looks safe because the changes are purely compiler-output and metadata updates, with no runtime or behavioural modifications. I excluded the .d.ts files from the comparison because they are compile-time type artifacts and cannot introduce runtime behaviour or supply-chain payloads.

package-snarkjs-old:

npm pack @tornado/snarkjs@0.1.20 --registry=https://git.tornado.ws/api/packages/tornado-packages/npm/

package-snarkjs-new:

npm pack @tornado/snarkjs@0.1.20 --registry=https://codeberg.org/api/packages/torndao/npm/
diff -r -w -B package-tornado-snarkjs-old package-tornado-snarkjs-new
diff -r -w -B package-tornado-snarkjs-old/package.json package-tornado-snarkjs-new/package.json
28c28
<         "url": "https://git.tornado.ws/tornado-packages/snarkjs"
---
>         "url": "https://codeberg.org/torndao/snarkjs"

=> The diff looks safe.

package-tornado-config-old:

npm pack @tornado/tornado-config@2.0.0 --registry=https://git.tornado.ws/api/packages/tornado-packages/npm/

package-tornado-config-new:

npm pack @tornado/tornado-config@2.0.0 --registry=https://codeberg.org/api/packages/torndao/npm/
diff -r -w -B package-tornado-config-old package-tornado-config-new --exclude="*.map"
diff -r -w -B package-tornado-config-old package-tornado-config-new --exclude="*.map"
diff -r -w -B '--exclude=*.map' package-tornado-config-old/lib/config.js package-tornado-config-new/lib/config.js
8c8
<     pausePeriod: 45 * 24 * 3600,
---
>     pausePeriod: 45 * 24 * 3600, // 45 days
diff -r -w -B '--exclude=*.map' package-tornado-config-old/package.json package-tornado-config-new/package.json
11c11
<     "url": "https://git.tornado.ws/tornado-packages/tornado-config.git"
---
>         "url": "https://codeberg.org/torndao/tornado-config.git"

=> The diff looks safe. I excluded the .map file from the comparison because sourcemaps are non-executable debugging artifacts and do not affect runtime behaviour.

package-tornado-oracles-old:

npm pack @tornado/tornado-oracles@2.1.0 --registry=https://git.tornado.ws/api/packages/tornado-packages/npm/

package-tornado-oracles-new:

npm pack @tornado/tornado-config@2.0.0 --registry=https://codeberg.org/api/packages/torndao/npm/
diff -r -w -B package-tornado-oracles-old package-tornado-oracles-new
diff -r -w -B package-tornado-oracles-old/package.json package-tornado-oracles-new/package.json
18c18
<     "url": "https://git.tornado.ws/tornado-packages/tornado-oracles.git"
---
>     "url": "https://codeberg.org/torndao/tornado-oracles.git"
47,50c47
<   ],
<   "publishConfig": {
<     "registry": "https://git.tornado.ws/api/packages/tornado-packages/npm"
<   }
---

=> The diff looks safe.

package-tornado-websnark-old:

npm pack @tornado/websnark@0.0.4 --registry=https://git.tornado.ws/api/packages/tornado-packages/npm/

package-tornado-websnark-new:

npm pack @tornado/websnark@0.0.4 --registry=https://codeberg.org/api/packages/torndao/npm/
diff -r -w -B package-tornado-websnark-old package-tornado-websnark-new
diff -r -w -B package-tornado-websnark-old/package.json package-tornado-websnark-new/package.json
25c25
<         "url": "https://git.tornado.ws/tornado-packages/websnark"
---
>         "url": "https://codeberg.org/torndao/websnark"

=> The diff looks safe.

  • Commit 25c5a6fc9801706d3fb9f009f3e470b24b5bb2b9 (commit message: "update event files") looks fine; all files are valid JSON and no suspicious patterns were found in any file. I used the following Bash command to check for any suspicious patterns in the events directory:
find . \( -name "*.json.gz" -o -name "*.json" \) -exec python3 -c "
import sys,json,zlib,re
f=sys.argv[1]
print('---',f,'---')
try:
    data=open(f,'rb').read()
    try:
        text=zlib.decompress(data).decode('utf-8')
    except:
        text=data.decode('utf-8')
    obj=json.loads(text)
    print('[OK] Valid JSON')
    allowed={'blockNumber','transactionHash','commitment','leafIndex','timestamp','nullifierHash','to','fee','logIndex','from','txHash','encryptedNote'}
    bad_fields=[k for o in (obj if isinstance(obj,list) else [obj]) for k in (o.keys() if isinstance(o,dict) else []) if k not in allowed]
    bad_vals=[v for o in (obj if isinstance(obj,list) else [obj]) for v in (o.values() if isinstance(o,dict) else []) if isinstance(v,str) and not (re.match(r'^(0x)?[0-9a-fA-F]+$',v) or re.match(r'^[0-9]+$',v))]
    print('[ALERT] BAD FIELDS:',list(set(bad_fields))[:5]) if bad_fields else print('[OK] Fields OK')
    print('[ALERT] BAD VALUES:',bad_vals[:3]) if bad_vals else print('[OK] Values OK')
except Exception as e:
    print('[FAIL]',str(e))
print()
" {} \;
  • Commit d52c01688f5697e60a1d3c598a4c30989403fb0f (commit message: "Update Ethereum RPCs") looks fine, but as always use your own node!

IPFS Hash Verification

First, we verify that the claimed commit hash bafybeie5hxovqc4ifcnrnhvmjbefxgeix6oqvzaspyytdxiyscji22v5pu is correctly calculated. I use https://codeberg.org/torndao/tornado-ipfs-ui - the original version seemed fine to me until now & the latest commit is just the commit hash update: https://codeberg.org/torndao/tornado-ipfs-ui/commit/982f9f4efbe65e756dd5e3a47069613457558bc2.

~$ git clone https://codeberg.org/torndao/tornado-ipfs-ui.git
~$ docker build -t tornado-classic-ui .
~$ docker container run --rm -it --entrypoint cat tornado-classic-ui /app/ipfs_hash.txt

which returns bafybeie5hxovqc4ifcnrnhvmjbefxgeix6oqvzaspyytdxiyscji22v5pu. So the claimed IPFS hash is correctly derived.

Let's use on-chain data to verify this as well. Let's get the ENS public resolver (0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e is the ENS registry):

cast call 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e "resolver(bytes32)(address)" $(cast namehash tornadocash.eth) -r https://eth.drpc.org

which returns 0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41.

Now we can get the IPFS hash (sorry some custom Bash magic lol):

cast call 0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41 "contenthash(bytes32)(bytes)" $(cast namehash tornadocash.eth) -r https://eth.drpc.org | sed 's/^0xe301//' | xxd -r -p | base32 | tr -d '=' | tr '[:upper:]' '[:lower:]' | sed 's/^a/ipfs:\/\/ba/'

which returns ipfs://bafybeie5hxovqc4ifcnrnhvmjbefxgeix6oqvzaspyytdxiyscji22v5pu. Looks good!

Finally, let's cross-check with with the eth.limo headers (since we don't trust anyone):

curl -sI https://tornadocash.eth.limo/ | grep -i "x-ipfs"

which returns

access-control-expose-headers: Content-Length,Content-Range,X-Chunked-Output,X-Ipfs-Path,X-Ipfs-Roots,X-Stream-Output
x-ipfs-path: /ipfs/bafybeie5hxovqc4ifcnrnhvmjbefxgeix6oqvzaspyytdxiyscji22v5pu/
x-ipfs-roots: bafybeie5hxovqc4ifcnrnhvmjbefxgeix6oqvzaspyytdxiyscji22v5pu

So we're good here.

@illlefr4u
Copy link

Great analysis of the existing codebase, thank you for the thorough review.

On Tornado Cash Proposal #65

The proposal has already been executed, and the current code has been reviewed and appears clean. That's not the concern.

The concern is the control architecture going forward. The proposal migrates npm packages to a Codeberg registry (codeberg.org/api/packages/torndao/npm/) controlled by a single anonymous account. This account can publish modified packages at any time, and doing so does not require a new governance proposal. The UI would rebuild with new dependencies on the next deploy, and no one would notice without a full re-audit.

Additionally, the default RPC list now includes a privately controlled endpoint tornadocash-rpc.com, registered through a Hong Kong registrar. The operator of this RPC can log all user transactions, a critical attack vector in a privacy protocol.

A separate note on voting: the largest FOR vote (119K TORN, ~35% of all votes in favor) belongs to a wallet directly funded by the proposal author through a shared master wallet. Voting for your own proposal is normal practice, and I wouldn't see an issue with it if the community retained control over what can be changed post-approval. But given that the author now has unilateral ability to modify the registry and RPC infrastructure without further governance oversight, the concentration of both voting power and infrastructure control in one anonymous entity is worth flagging.

@pcaversaccio
Copy link
Author

The concern is the control architecture going forward. The proposal migrates npm packages to a Codeberg registry (codeberg.org/api/packages/torndao/npm/) controlled by a single anonymous account. This account can publish modified packages at any time, and doing so does not require a new governance proposal. The UI would rebuild with new dependencies on the next deploy, and no one would notice without a full re-audit.

So this can be indeed part of a long-term attack agreed, i.e. in an upcoming new proposal some dependencies become malicious (and thus needs careful review always!). While the account can publish modified packages, yes, it's not automatically deployed to tornadocash.eth.limo etc. - this will require a new governance proposal. I have verified the diff for the SHA-1 and SHA-512 hashes of the yarn.lock file and 1) any yarn install will throw with Integrity check failed for ... if a modified package is published under the same tag (i.e. there is a hash mismatch) and 2) if the yarn.lock file is updated to incorporate the new (malicious) dependencies, the IPFS hash would change again and requires a new proposal to pass.

Additionally, the default RPC list now includes a privately controlled endpoint tornadocash-rpc.com, registered through a Hong Kong registrar. The operator of this RPC can log all user transactions, a critical attack vector in a privacy protocol.

Well, if you really care about privacy you never trust any RPC and run your own node (I even write this in my gist above). Any external RPCs cannot be trusted by default and this new proposal doesn't change this.

@illlefr4u
Copy link

Thanks for the detailed verification on the yarn.lock integrity checks. Fair point on the RPC too, agree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment