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.
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.