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.linkThis 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).
- Code repository: https://codeberg.org/torndao/classic-ui/commits/branch/development
- UI code commit version: https://codeberg.org/torndao/classic-ui/commit/d52c01688f5697e60a1d3c598a4c30989403fb0f
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):
- https://codeberg.org/torndao/classic-ui/commit/6ed828199d8a9eb4fedec2764712af81bd13a7e5 (commit message: "Update the @tornado library file URL."),
- https://codeberg.org/torndao/classic-ui/commit/25c5a6fc9801706d3fb9f009f3e470b24b5bb2b9 (commit message: "update event files"),
- https://codeberg.org/torndao/classic-ui/commit/d52c01688f5697e60a1d3c598a4c30989403fb0f (commit message: "Update Ethereum RPCs").
- Commit
6ed828199d8a9eb4fedec2764712af81bd13a7e5(commit message: "Update the @tornado library file URL.") is the most dangerous one since it changes thenpmregistry link to@tornado:registry=https://codeberg.org/api/packages/torndao/npm/(see https://codeberg.org/torndao/-/packages) and also updates theyarn.lockfile.
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
doneLet'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-newdiff -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-newdiff -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-newdiff -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 validJSONand no suspicious patterns were found in any file. I used the following Bash command to check for any suspicious patterns in theeventsdirectory:
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!
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.txtwhich 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.orgwhich 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: bafybeie5hxovqc4ifcnrnhvmjbefxgeix6oqvzaspyytdxiyscji22v5puSo we're good here.
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.lockfile and 1) anyyarn installwill throw withIntegrity check failed for ...if a modified package is published under the same tag (i.e. there is a hash mismatch) and 2) if theyarn.lockfile is updated to incorporate the new (malicious) dependencies, the IPFS hash would change again and requires a new proposal to pass.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.