add sepolia and etc support, add docker to verify #4

Merged
Theo merged 1 commits from :tornadosto into master 2025-06-19 14:45:44 +03:00
5 changed files with 205 additions and 34 deletions

24
Dockerfile Normal file
View File

@ -0,0 +1,24 @@
# FROM node 14.21.3-bullseye-slim
FROM node@sha256:0f5b374fae506741ff14db84daff2937ae788e88fb48a6c66d15de5ee808ccd3
RUN apt update && apt install --yes --no-install-recommends wget git apt-transport-https ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /home/root/tornado-cli
ENV GIT_REPOSITORY=https://git.tornado.ws/tornadosto/tornado-cli.git
ENV GIT_COMMIT_HASH=1ae2aec71d3cfb28911ce4c60bdd35650e93e5e4
RUN git init && \
git remote add origin $GIT_REPOSITORY && \
git fetch --depth 1 origin $GIT_COMMIT_HASH && \
git checkout $GIT_COMMIT_HASH
RUN npm ci
RUN npm install -g pkg@5.8.1
RUN node scripts/createDeterministicExecutable.js
RUN printf '#!/bin/sh\ncp /home/root/tornado-cli/tornado-cli.exe /output/' > /copy_out.sh && chmod +x /copy_out.sh
CMD ["/bin/bash"]

View File

@ -163,3 +163,21 @@ View transaction on block explorer https://goerli.etherscan.io/tx/0x6ded443caed8
Tornado contract balance is xxx.x ETH
Sender account balance is x.xxxxxxx ETH
```
#### To verify:
```bash
$ docker build -t tornado-cli:latest .
```
wait for docker to build
```bash
$ docker run --rm -v %cd%/output:/output tornado-cli:latest /copy_out.sh
```
copy exe to current folder in windows
```bash
CertUtil -hashfile output/tornado-cli.exe SHA256
CertUtil -hashfile tornado-cli.exe SHA256
```
compare with the exe in git

102
cli.js
View File

@ -74,7 +74,8 @@ const relayerSubdomains = Object.values(config.deployments).map(({ ensSubdomainK
*/
/** @type {ProgramGlobals} */
const globals = {
const globals =
{
privateKey: undefined,
web3Instance: undefined,
relayerWeb3Instance: undefined,
@ -288,7 +289,8 @@ async function generateTransaction(to, encodedData, value = 0, txType = 'other')
/**
* Create deposit object from secret and nullifier
*/
function createDeposit({ nullifier, secret }) {
function createDeposit({ nullifier, secret })
{
let deposit = { nullifier, secret };
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)]);
deposit.commitment = pedersenHash(deposit.preimage);
@ -328,7 +330,8 @@ async function backupInvoice({ currency, amount, netId, commitmentNote, invoiceS
* @param currency Сurrency
* @param amount Deposit amount
*/
async function createInvoice({ currency, amount, chainId }) {
async function createInvoice({ currency, amount, chainId })
{
const deposit = createDeposit({
nullifier: rbigint(31),
secret: rbigint(31)
@ -422,7 +425,8 @@ async function deposit({ currency, amount, commitmentNote }) {
* @param {number} amount Tornado instance amount, like 0.1 (ETH or BNB) or 10
* @return {Promise<MerkleProof>} Calculated valid merkle tree (proof)
*/
async function generateMerkleProof(deposit, currency, amount) {
async function generateMerkleProof(deposit, currency, amount)
{
const { web3Instance, multiCallAddress, tornadoInstanceContract } = globals;
// Get all deposit events from smart contract and assemble merkle tree from them
@ -469,13 +473,16 @@ async function generateMerkleProof(deposit, currency, amount) {
* @param {MerkleProof} [args.merkleProof] Valid merkle tree proof
* @returns {Promise<ProofData>} Proof data
*/
async function generateProof({ deposit, currency, amount, recipient, relayerAddress = 0, fee = 0, refund = 0, merkleProof }) {
async function generateProof({ deposit, currency, amount, recipient, relayerAddress = 0, fee = 0, refund = 0, merkleProof })
{
// Compute merkle proof of our commitment
if (merkleProof === undefined) merkleProof = await generateMerkleProof(deposit, currency, amount);
if (merkleProof === undefined)
merkleProof = await generateMerkleProof(deposit, currency, amount);
const { root, pathElements, pathIndices } = merkleProof;
// Prepare circuit input
const input = {
const input =
{
// Public snark inputs
root: root,
nullifierHash: deposit.nullifierHash,
@ -518,65 +525,86 @@ async function generateProof({ deposit, currency, amount, recipient, relayerAddr
* @param noteString Note to withdraw
* @param recipient Recipient address
*/
async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund, privateKey }) {
async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund, privateKey })
{
const { web3Instance, signerAddress, tornadoProxyAddress, requestOptions, feeOracle, tornadoInstanceAddress, tornadoProxyContract, netSymbol, netId, shouldPromptConfirmation } = globals;
if (currency === netSymbol.toLowerCase() && refund && refund !== '0') {
if (currency === netSymbol.toLowerCase() && refund && refund !== '0')
{
throw new Error('The ETH purchase is supposed to be 0 for ETH withdrawals');
}
if (!isNaN(Number(refund))) refund = toWei(refund, 'ether');
else refund = toBN(await feeOracle.fetchRefundInETH(currency.toLowerCase()));
if (!isNaN(Number(refund)))
refund = toWei(refund, 'ether');
else
refund = toBN(await feeOracle.fetchRefundInETH(currency.toLowerCase()));
if (!web3Utils.isAddress(recipient)) {
if (!web3Utils.isAddress(recipient))
{
throw new Error('Recipient address is not valid');
}
const depositInfo = await loadDepositData({ amount, currency, deposit });
const allDeposits = loadCachedEvents({ type: "deposit", currency, amount });
if ((depositInfo.leafIndex > allDeposits[allDeposits.length - 1].leafIndex - 10) && allDeposits.length > 10){
if ((depositInfo.leafIndex > allDeposits[allDeposits.length - 1].leafIndex - 10)
&& allDeposits.length > 10)
{
console.log("\nWARNING: you're trying to withdraw your deposit too early, there are not enough subsequent deposits to ensure good anonymity level. Read: https://docs.tornado.ws/general/guides/opsec.html");
if (shouldPromptConfirmation) await promptConfirmation("Continue withdrawal with risks to anonymity? [Y/n]: ")
if (shouldPromptConfirmation)
await promptConfirmation("Continue withdrawal with risks to anonymity? [Y/n]: ")
}
const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit });
if(withdrawInfo) {
if(withdrawInfo)
{
console.error("\nError: note has already been withdrawn. Use `compliance` command to check deposit and withdrawal info.\n");
process.exit(1);
}
if (privateKey || globals.privateKey) {
if (privateKey || globals.privateKey)
{
// using private key
// check if the address of recepient matches with the account of provided private key from environment to prevent accidental use of deposit address for withdrawal transaction.
assert(
assert
(
recipient.toLowerCase() == signerAddress.toLowerCase(),
'Withdrawal recepient mismatches with the account of provided private key from environment file'
);
const checkBalance = await web3Instance.getBalance(signerAddress);
assert(checkBalance !== 0, 'You have 0 balance, make sure to fund account by withdrawing from tornado using relayer first');
assert
(
checkBalance !== 0,
'You have 0 balance, make sure to fund account by withdrawing from tornado using relayer first'
);
const { proof, args } = await generateProof({ deposit, currency, amount, recipient, refund });
console.log('Submitting withdraw transaction');
await generateTransaction(
await generateTransaction
(
tornadoProxyAddress,
tornadoProxyContract.methods.withdraw(tornadoInstanceAddress, proof, ...args).encodeABI(),
toBN(args[5]),
'user_withdrawal'
);
}
else {
else
{
let relayerInfo;
if (relayerURL) {
try {
if (relayerURL)
{
try
{
relayerURL = new URL(relayerURL).origin;
res = await axios.get(relayerURL + '/status', requestOptions);
relayerInfo = res.data;
} catch (err) {
} catch (err)
{
console.error(err);
throw new Error('Cannot get relayer status');
}
}
else {
else
{
const availableRelayers = await getRelayers(netId);
if(availableRelayers.length === 0) throw new Error("Cannot automatically pick a relayer to withdraw your note. Provide relayer manually with `--relayer` cmd option or use private key withdrawal")
relayerInfo = pickWeightedRandomRelayer(availableRelayers);
@ -667,9 +695,12 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
}
}
if (currency === netSymbol.toLowerCase()) {
if (currency === netSymbol.toLowerCase())
{
await printETHBalance({ address: recipient, name: 'Recipient' });
} else {
}
else
{
await printERC20Balance({ address: recipient, name: 'Recipient' });
}
console.log('Done withdrawal from Tornado Cash');
@ -870,6 +901,10 @@ function toDecimals(value, decimals, fixed) {
// List fetched from https://github.com/ethereum-lists/chains/blob/master/_data/chains
function getExplorerLink() {
switch (globals.netId) {
case 61:
return 'etc.blockscout.com';
case 11155111:
return 'sepolia.etherscan.io';
case 56:
return 'bscscan.com';
case 100:
@ -894,6 +929,10 @@ function getExplorerLink() {
// List fetched from https://github.com/trustwallet/assets/tree/master/blockchains
function getCurrentNetworkName() {
switch (globals.netId) {
case 61:
return 'EthereumClassic';
case 11155111:
return 'Sepolia';
case 56:
return 'BinanceSmartChain';
case 100:
@ -920,6 +959,8 @@ function getCurrentNetworkName() {
*/
function getCurrentNetworkSymbol(chainId) {
switch (Number(chainId)) {
case 61:
return 'ETC';
case 56:
return 'BNB';
case 100:
@ -1399,10 +1440,12 @@ async function fetchEvents({ type, currency, amount }) {
* Parses Tornado Cash note
* @param {string} noteString the note
*/
function parseNote(noteString) {
function parseNote(noteString)
{
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g;
const match = noteRegex.exec(noteString);
if (!match) {
if (!match)
{
throw new Error('The note has invalid format');
}
@ -1563,7 +1606,7 @@ async function initNetwork({rpc, chainId, privateKey, torPort, onlyRpc, eventTyp
}
globals.web3Instance = await createWeb3Instance(rpc)
globals.netId = await globals.web3Instance.net.getId();
globals.netId = await globals.web3Instance.getChainId()
globals.netName = getCurrentNetworkName();
globals.netSymbol = getCurrentNetworkSymbol(globals.netId);
@ -1635,6 +1678,7 @@ async function init({ rpc, chainId, currency = 'dai', amount = '100', privateKey
await initNetwork({rpc, chainId, privateKey, torPort, onlyRpc, eventType, relayer});
const { netId, web3Instance } = globals;
// console.log(netId, chainId);
if (chainId && Number(chainId) !== netId) {
throw new Error('This note is for a different network. Specify the --rpc option explicitly');
}

View File

@ -114,9 +114,15 @@ module.exports = {
relayerAggregator: '0xE8F47A78A6D52D317D0D2FFFac56739fE14D1b49',
proxy: '0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b',
multicall: '0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441',
subgraphs: ['https://gateway.thegraph.com/api/6a217817dd87d33db10beed79b044a91/subgraphs/id/Ec6fVMDVqXTDQZ3c4jxcyV3zBXqkdgMWfhdtCgtqn7Sh', 'https://gateway.thegraph.com/api/8b164501e1862078eff5fb9dda136c6c/subgraphs/id/Ec6fVMDVqXTDQZ3c4jxcyV3zBXqkdgMWfhdtCgtqn7Sh', 'https://tornadocash-rpc.com/subgraphs/name/tornadocash/mainnet-tornado-subgraph'],
subgraphs: ['https://gateway.thegraph.com/api/6a217817dd87d33db10beed79b044a91/subgraphs/id/Ec6fVMDVqXTDQZ3c4jxcyV3zBXqkdgMWfhdtCgtqn7Sh',
'https://gateway.thegraph.com/api/8b164501e1862078eff5fb9dda136c6c/subgraphs/id/Ec6fVMDVqXTDQZ3c4jxcyV3zBXqkdgMWfhdtCgtqn7Sh',
'https://tornadocash-rpc.com/subgraphs/name/tornadocash/mainnet-tornado-subgraph'],
relayerSubgraphs: ['https://gateway.thegraph.com/api/6a217817dd87d33db10beed79b044a91/subgraphs/id/DgKwfAbLfynpiq7fDJy59LDnVnia4Y5nYeRDBYi9qezc', 'https://gateway.thegraph.com/api/8b164501e1862078eff5fb9dda136c6c/subgraphs/id/DgKwfAbLfynpiq7fDJy59LDnVnia4Y5nYeRDBYi9qezc', 'https://tornadocash-rpc.com/subgraphs/name/tornadocash/tornado-relayer-registry'],
defaultRpcs: ['https://ethereum.blockpi.network/v1/rpc/public', 'https://eth.drpc.org', 'https://ethereum-rpc.publicnode.com', 'https://mainnet.chainnodes.org/3ae3d849-a613-4917-a56e-080f181aa4da', 'https://tornadocash-rpc.com']
defaultRpcs: ['https://ethereum.blockpi.network/v1/rpc/public',
'https://eth.drpc.org',
'https://ethereum-rpc.publicnode.com',
'https://mainnet.chainnodes.org/3ae3d849-a613-4917-a56e-080f181aa4da',
'https://tornadocash-rpc.com']
},
netId56: {
tokens: {
@ -143,7 +149,12 @@ module.exports = {
proxy: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
multicall: '0x41263cBA59EB80dC200F3E2544eda4ed6A90E76C',
subgraphs: ['https://gateway.thegraph.com/api/6a217817dd87d33db10beed79b044a91/subgraphs/id/CiwGzefDBZCavXRPnwarnnF8xDDoLw4boBuySomJWYnV', 'https://tornadocash-rpc.com/subgraphs/name/tornadocash/bsc-tornado-subgraph', 'https://gateway.thegraph.com/api/8b164501e1862078eff5fb9dda136c6c/subgraphs/id/CiwGzefDBZCavXRPnwarnnF8xDDoLw4boBuySomJWYnV'],
defaultRpcs: ['https://bsc-rpc.publicnode.com', 'https://endpoints.omniatech.io/v1/bsc/mainnet/public', 'https://bsc-mainnet.chainnodes.org/3ae3d849-a613-4917-a56e-080f181aa4da', 'https://bsc-dataseed1.ninicoin.io', 'https://bsc.drpc.org', 'https://bsc-mainnet.public.blastapi.io']
defaultRpcs: ['https://bsc-rpc.publicnode.com',
'https://endpoints.omniatech.io/v1/bsc/mainnet/public',
'https://bsc-mainnet.chainnodes.org/3ae3d849-a613-4917-a56e-080f181aa4da',
'https://bsc-dataseed1.ninicoin.io',
'https://bsc.drpc.org',
'https://bsc-mainnet.public.blastapi.io']
},
netId100: {
tokens: {
@ -277,6 +288,80 @@ module.exports = {
multicall: '0x142E2FEaC30d7fc3b61f9EE85FCCad8e560154cc',
subgraphs: ['https://gateway.thegraph.com/api/6a217817dd87d33db10beed79b044a91/subgraphs/id/GvkbnEVhLD6KArXpEzLFtSKRmspBW29ApKFqR5FjuP2P', 'https://gateway.thegraph.com/api/8b164501e1862078eff5fb9dda136c6c/subgraphs/id/GvkbnEVhLD6KArXpEzLFtSKRmspBW29ApKFqR5FjuP2P', 'https://tornadocash-rpc.com/subgraphs/name/tornadocash/optimism-tornado-subgraph'],
defaultRpcs: ['https://optimism.blockpi.network/v1/rpc/public', 'https://optimism-mainnet.chainnodes.org/3ae3d849-a613-4917-a56e-080f181aa4da', 'https://endpoints.omniatech.io/v1/op/mainnet/public', 'https://optimism-mainnet.public.blastapi.io', 'https://optimism.drpc.org']
},
netId11155111: {
tokens:
{
eth:
{
instanceAddress:
{
0.1: '0x8C4A04d872a6C1BE37964A21ba3a138525dFF50b',
1: '0x8cc930096B4Df705A007c4A039BDFA1320Ed2508',
10: '0x8D10d506D29Fc62ABb8A290B99F66dB27Fc43585',
},
deployedBlockNumber:
{
0.1: 5594400,
1: 5594401,
10: 5594402,
},
miningEnabled: false,
symbol: 'ETH',
decimals: 18
}
},
ensSubdomainKey: 'sepolia-tornado',
firstDeploymentTransaction: '0x7c7260a119bd0682b785da8860def277877ffaa50c2068ee78d6cb51f50bdc1f',
proxy: '0x1572AFE6949fdF51Cb3E0856216670ae9Ee160Ee',
multicall: '0x53c43764255c17bd724f74c4ef150724ac50a3ed',
subgraphs: ['https://gateway.thegraph.com/api/6a217817dd87d33db10beed79b044a91/subgraphs/id/8kJGz92AYUm72wfyUoze1as3E11ynDSTZM8emiRWrRPy',
'https://gateway.thegraph.com/api/8b164501e1862078eff5fb9dda136c6c/subgraphs/id/8kJGz92AYUm72wfyUoze1as3E11ynDSTZM8emiRWrRPy',
'https://tornadocash-rpc.com/subgraphs/name/tornadocash/sepolia-tornado-subgraph'
],
defaultRpcs: ['https://ethereum-sepolia-rpc.publicnode.com',
'https://sepolia.chainnodes.org/61b7de01-6cc4-40dc-a6c2-b6e4a61bb042',
'https://sepolia.drpc.org',
'https://eth-sepolia.g.alchemy.com/v2/demo',
'https://eth-sepolia.public.blastapi.io',
'https://eth-sepolia.api.onfinality.io/public']
},
netId61:
{
tokens:
{
etc:
{
instanceAddress:
{
1: '0x2f56d5aFC058B8734350B162EFEe75ee48f034e0',
10: '0x59fCB629A23e8eD0a60A0188771E221042260118',
100: '0x784B3a7a7981B959bd8d9D9e73c2013BE819Fbf2',
},
deployedBlockNumber:
{
1: 22385618,
10: 22385618,
100: 22385618,
},
miningEnabled: false,
symbol: 'ETC',
decimals: 18
}
},
ensSubdomainKey: 'etc-tornado',
firstDeploymentTransaction: '0x4d9232046d3503138525bb3b921e131153d02b436f9f81426f52929e50ab359e',
proxy: '0xac97AB4fBd872ea762974CbBB0Ee72351afe16F3',
multicall: '0xA52EE88C0F24EF8b96C3989cAb42cfC6008041A8',
subgraphs: ['https://graph.torndao.com/subgraphs/name/tornadocash/etc-tornado-subgraph'],
defaultRpcs: ['https://etc.etcdesktop.com',
'https://etc.rivet.link',
'https://etc.mytokenpocket.vip',
'https://0xrpc.io/etc',
'https://geth-at.etc-network.info',
'https://besu-at.etc-network.info'],
}
}
};

Binary file not shown.