Compare commits

...

12 Commits

16 changed files with 2563 additions and 3202 deletions

@ -12,8 +12,8 @@ APP_PORT=8000
# without 0x prefix # without 0x prefix
PRIVATE_KEY= PRIVATE_KEY=
# 0.05 means 0.05% # 0.4 means 0.4%
REGULAR_TORNADO_WITHDRAW_FEE=0.05 REGULAR_TORNADO_WITHDRAW_FEE=0.4
MINING_SERVICE_FEE=0.05 MINING_SERVICE_FEE=0.05
REWARD_ACCOUNT= REWARD_ACCOUNT=
CONFIRMATIONS=4 CONFIRMATIONS=4

1
.gitignore vendored

@ -6,3 +6,4 @@ node_modules/
kovan.* kovan.*
dump.rdb dump.rdb
.idea .idea
yarn-error.log

1
.npmrc Normal file

@ -0,0 +1 @@
@tornado:registry=https://git.tornado.ws/api/packages/tornado-packages/npm/

@ -1,4 +1,4 @@
FROM node:12 FROM node:16
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./

@ -1,8 +1,8 @@
# Relayer for Tornado Cash [![Build Status](https://github.com/tornadocash/relayer/workflows/build/badge.svg)](https://github.com/tornadocash/relayer/actions) [![Docker Image Version (latest semver)](https://img.shields.io/docker/v/tornadocash/relayer?logo=docker&logoColor=%23FFFFFF&sort=semver)](https://hub.docker.com/repository/docker/tornadocash/relayer) # Relayer for Tornado Cash [![Build Status](https://github.com/tornadocash/relayer/workflows/build/badge.svg)](https://github.com/tornadocash/relayer/actions) [![Docker Image Version (latest semver)](https://img.shields.io/docker/v/tornadocash/relayer?logo=docker&logoColor=%23FFFFFF&sort=semver)](https://hub.docker.com/repository/docker/tornadocash/relayer)
__*Tornado Cash was sanctioned by the US Treasury on 08/08/2022, this makes it illegal for US citizens to interact with Tornado Cash and all of it's associated deloyed smart contracts. Please understand the laws where you live and take all necessary steps to protect and anonomize yourself.__ __*Tornado Cash was sanctioned by the US Treasury on 08/08/2022, this makes it illegal for US citizens to interact with Tornado Cash and all of it's associated deployed smart contracts. Please understand the laws where you live and take all necessary steps to protect and anonymize yourself.__
__*It is recommended to run your Relayer on a VPS instnace ([Virtual Private Server](https://njal.la/)). Ensure SSH configuration is enabled for security, you can find information about SSH keygen and management [here](https://www.ssh.com/academy/ssh/keygen).__ __*It is recommended to run your Relayer on a VPS instance ([Virtual Private Server](https://njal.la/)). Ensure SSH configuration is enabled for security, you can find information about SSH keygen and management [here](https://www.ssh.com/academy/ssh/keygen).__
## Deploy with docker-compose (recommended) ## Deploy with docker-compose (recommended)
@ -29,65 +29,38 @@ _* Warning: Failure to configure SSH as the first UFW rule, will lock you out of
1. Make sure UFW is installed by running `apt update` and `apt install ufw` 1. Make sure UFW is installed by running `apt update` and `apt install ufw`
2. Allow SSH in the first position in UFW by running `ufw insert 1 allow ssh`* 2. Allow SSH in the first position in UFW by running `ufw insert 1 allow ssh`*
3. Allow HTTP, and HTTPS by running `ufw allow https/tcp/http` 3. Allow HTTP, and HTTPS by running `ufw allow https/tcp/http`
4. Finalise changes and enable firewall `ufw enable` 4. Finalize changes and enable firewall `ufw enable`
__NETWORK DEPLOYMENT OPTIONS__ __DEPLOYMENT__
_Ethereum (eth), Binance (bnb), Gnosis (xdai), Polygon (matic), Optimisim (op), Arbitrum (arb) and Goerli (geth)_
__SINGLE NETWORK DEPLOYMENT__
1. Clone the repository and enter the directory 1. Clone the repository and enter the directory
- `git clone https://development.tornadocash.community/tornadocash/classic-relayer && cd classic-relayer` - `git clone https://git.tornado.ws/tornadocash/classic-relayer -b mainnet-v4 && cd classic-relayer`
2. Clone the example enviroment file `.env.example` to configure for the perferred network 2. Clone the example environment file `.env.example` to configure for the preferred network - `cp .env.example .env` , then fill `.env` file.
- By default each network is preconfigured the naming of `.env.<NETWORK SYMBOL>`
- `cp .env.example .env.eth`
- Set `PRIVATE_KEY` for your relayer address (remove the 0x from your private key) - Set `PRIVATE_KEY` for your relayer address (remove the 0x from your private key)
- Set `VIRTUAL_HOST` and `LETSENCRYPT_HOST` to your domain address - Set `VIRTUAL_HOST` and `LETSENCRYPT_HOST` to your domain address
- add a A record DNS record with the value assigned to your instance IP address to configure the domain - add a A record DNS record with the value assigned to your instance IP address to configure the domain
- Set `RELAYER_FEE` to what you would like to charge as your fee (remember 0.3% is deducted from your staked relayer balance) - Set `RELAYER_FEE` to what you would like to charge as your fee (remember 0.3% is deducted from your staked relayer balance)
- Set `RPC_URL` to a non-censoring RPC endpoint (You can [run your own](https://github.com/feshchenkod/rpc-nodes), or use a [free option](https://chainnodes.org/)) - Set `RPC_URL` to a non-censoring RPC endpoint (You can [run your own](https://github.com/feshchenkod/rpc-nodes), or use a [free option](https://chainnodes.org/))
- Set `ORACLE_RPC_URL` to an Ethereum native RPC endpoint - Set `ORACLE_RPC_URL` to an Ethereum native RPC endpoint
4. Uncomment the `env_file` lines (remove `# `) for the associated network services in `docker-compose.yml` 4. Uncomment the `env_file` lines (remove `# `) for the associated network services in `docker-compose.yml`
5. Build and deploy the docker source by specifying the network through `--profile <NETWORK_SYMBOL>` 5. Build and deploy the docker source by specifying the network through:
- `docker-compose --profile eth up -d`
- `npm run build`
- `docker-compose up -d`
5. Visit your domain address and check the `/status` endpoint and ensure there is no errors in the `status` field 5. Visit your domain address and check the `/status` endpoint and ensure there is no errors in the `status` field
__NGINX REVERSE PROXY__ __NGINX REVERSE PROXY__
1. Copy the pre-modified nginx policy as your default policy 1. Copy the pre-modified nginx policy as your default policy
- `cp tornado.conf /etc/nginx/sites-available/default` - `cp tornado.conf /etc/nginx/sites-available/default`
2. Append the default nginx configuraiton to include streams 2. Append the default nginx configuration to include streams
- `echo "stream { map_hash_bucket_size 128; map_hash_max_size 128; include /etc/nginx/conf.d/streams/*.conf; }" >> /etc/nginx/nginx.conf` - `echo "stream { map_hash_bucket_size 128; map_hash_max_size 128; include /etc/nginx/conf.d/streams/*.conf; }" >> /etc/nginx/nginx.conf`
3. Create the stream configruation 3. Create the stream configuration
- `mkdir /etc/nginx/conf.d/streams && cp tornado-stream.conf /etc/nginx/conf.d/streams/tornado-stream.conf` - `mkdir /etc/nginx/conf.d/streams && cp tornado-stream.conf /etc/nginx/conf.d/streams/tornado-stream.conf`
4. Start nginx to make sure the configuration is correct 4. Start nginx to make sure the configuration is correct
- `sudo systemctl restart nginx` - `sudo systemctl restart nginx`
5. Stop nginx 5. Stop nginx
- `sudo systemctl stop nginx` - `sudo systemctl stop nginx`
__MULTIPLE NETWORK DEPLOYMENT__
1. Setup the instructions stated to setup an nginx reverse proxy
2. Clone the example enviroment file `.env.example` for the networks of choice and configure
- By default each network is preconfigured the naming of `.env.<NETWORK SYMBOL>`
- `cp .env.example .env.eth`
- `cp .env.example .env.bnb`
- `cp .env.example .env.arb`
- `cp .env.example .env.op`
- `cp .env.example .env.xdai`
- Set `PRIVATE_KEY` to your relayer address (remove the 0x from your private key) to each environment file
- *It is recommended not to reuse the same private keys for each network as a security measure*
- Set `VIRTUAL_HOST` and `LETSENCRYPT_HOST` a unique subndomain for every network to each environment file
- eg: `mainnet.example.com` for Ethereum, `binance.example.com` for Binance etc
- ensure that the parent domain domain is specified first with the subdomain after sperated by a comma for SAN certificates
- eg: `VIRTUAL_HOST=example.com,eth.example.com` and `LETSENCRYPT_HOST=example.com,eth.example.com`
- add a A wildcard record DNS record with the value assigned to your instance IP address to configure submdomains
- Set `RELAYER_FEE` to what you would like to charge as your fee (remember 0.3% is deducted from your staked relayer balance)
- Set `RPC_URL` to a non-censoring RPC (You can [run your own](https://github.com/feshchenkod/rpc-nodes), or use a [free option](https://chainnodes.org/))
- Set `ORACLE_RPC_URL` to an Ethereum native RPC endpoint
3. Uncomment the `env_file` lines (remove `# `) for the associated network services in `docker-compose.yml`
4. Build and deploy the docker source for the configured neworks specified via `--profile <NETWORK_SYMBOL>`
- `docker-compose --profile eth --profile bnb --profile arb --profile --profile op --profile xdai up -d`
5. Visit your domain addresses and check each `/status` endpoint to ensure there is no errors in the `status` fields
## Run locally ## Run locally
1. `npm i` 1. `npm i`

@ -1,181 +0,0 @@
[
{
"inputs": [
{ "internalType": "contract MultiWrapper", "name": "_multiWrapper", "type": "address" },
{ "internalType": "contract IOracle[]", "name": "existingOracles", "type": "address[]" },
{ "internalType": "enum OffchainOracle.OracleType[]", "name": "oracleTypes", "type": "uint8[]" },
{ "internalType": "contract IERC20[]", "name": "existingConnectors", "type": "address[]" },
{ "internalType": "contract IERC20", "name": "wBase", "type": "address" }
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "internalType": "contract IERC20", "name": "connector", "type": "address" }
],
"name": "ConnectorAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "internalType": "contract IERC20", "name": "connector", "type": "address" }
],
"name": "ConnectorRemoved",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "internalType": "contract MultiWrapper", "name": "multiWrapper", "type": "address" }
],
"name": "MultiWrapperUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "internalType": "contract IOracle", "name": "oracle", "type": "address" },
{
"indexed": false,
"internalType": "enum OffchainOracle.OracleType",
"name": "oracleType",
"type": "uint8"
}
],
"name": "OracleAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "internalType": "contract IOracle", "name": "oracle", "type": "address" },
{
"indexed": false,
"internalType": "enum OffchainOracle.OracleType",
"name": "oracleType",
"type": "uint8"
}
],
"name": "OracleRemoved",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" },
{ "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" }
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"inputs": [{ "internalType": "contract IERC20", "name": "connector", "type": "address" }],
"name": "addConnector",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "contract IOracle", "name": "oracle", "type": "address" },
{ "internalType": "enum OffchainOracle.OracleType", "name": "oracleKind", "type": "uint8" }
],
"name": "addOracle",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "connectors",
"outputs": [{ "internalType": "contract IERC20[]", "name": "allConnectors", "type": "address[]" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "contract IERC20", "name": "srcToken", "type": "address" },
{ "internalType": "contract IERC20", "name": "dstToken", "type": "address" },
{ "internalType": "bool", "name": "useWrappers", "type": "bool" }
],
"name": "getRate",
"outputs": [{ "internalType": "uint256", "name": "weightedRate", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "contract IERC20", "name": "srcToken", "type": "address" },
{ "internalType": "bool", "name": "useSrcWrappers", "type": "bool" }
],
"name": "getRateToEth",
"outputs": [{ "internalType": "uint256", "name": "weightedRate", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "multiWrapper",
"outputs": [{ "internalType": "contract MultiWrapper", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "oracles",
"outputs": [
{ "internalType": "contract IOracle[]", "name": "allOracles", "type": "address[]" },
{ "internalType": "enum OffchainOracle.OracleType[]", "name": "oracleTypes", "type": "uint8[]" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "contract IERC20", "name": "connector", "type": "address" }],
"name": "removeConnector",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "contract IOracle", "name": "oracle", "type": "address" },
{ "internalType": "enum OffchainOracle.OracleType", "name": "oracleKind", "type": "uint8" }
],
"name": "removeOracle",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "contract MultiWrapper", "name": "_multiWrapper", "type": "address" }],
"name": "setMultiWrapper",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]

@ -1,64 +0,0 @@
version: '2'
# ssh-agent && ssh-add -K ~/.ssh/id_rsa
# DOCKER_BUILDKIT=1 docker build --ssh default -t tornadocash/relayer .
services:
server:
image: tornadocash/relayer
restart: always
command: server
env_file: .env
ports:
- 8000:8000
environment:
REDIS_URL: redis://redis/0
nginx_proxy_read_timeout: 600
depends_on: [redis]
treeWatcher:
image: tornadocash/relayer
restart: always
command: treeWatcher
env_file: .env
environment:
REDIS_URL: redis://redis/0
depends_on: [redis]
priceWatcher:
image: tornadocash/relayer
restart: always
command: priceWatcher
env_file: .env
environment:
REDIS_URL: redis://redis/0
depends_on: [redis]
worker1:
image: tornadocash/relayer
restart: always
command: worker
env_file: .env
environment:
REDIS_URL: redis://redis/0
depends_on: [redis]
# worker2:
# image: tornadocash/relayer
# restart: always
# command: worker
# env_file: .env
# environment:
# PRIVATE_KEY: qwe
# REDIS_URL: redis://redis/0
redis:
image: redis
restart: always
ports:
- '6379:6379'
command: [redis-server, --appendonly, 'yes']
volumes:
- redis:/data
volumes:
redis:

@ -1,13 +1,14 @@
version: '2' version: '2'
services: services:
redis: redis:
image: redis image: redis
restart: always restart: always
command: [redis-server, --appendonly, 'yes'] command: [redis-server, --appendonly, 'yes']
volumes: volumes:
- redis:/data - redis:/data
ports:
- '127.0.0.1:6379:6379'
nginx: nginx:
image: nginx:alpine image: nginx:alpine
@ -48,63 +49,58 @@ services:
eth-server: eth-server:
build: . build: .
image: local/tornadocash/relayer/4.14 image: tornadocash/relayer:mainnet-v4
profiles: [ 'eth' ]
restart: always restart: always
command: server command: server
# env_file: .env.eth env_file: .env
environment: environment:
NET_ID: 1 NET_ID: 1
REDIS_URL: redis://redis/0 REDIS_URL: redis://redis/0
nginx_proxy_read_timeout: 600 nginx_proxy_read_timeout: 600
depends_on: [redis] depends_on: [redis]
# eth-treeWatcher: eth-treeWatcher:
# image: local/tornadocash/relayer/4.14 image: tornadocash/relayer:mainnet-v4
# profiles: [ 'eth' ] restart: always
# restart: always command: treeWatcher
# command: treeWatcher env_file: .env
# env_file: .env.eth environment:
# environment: NET_ID: 1
# NET_ID: 1 REDIS_URL: redis://redis/0
# REDIS_URL: redis://redis/0 depends_on: [redis, eth-server]
# depends_on: [ redis, eth-server ]
eth-priceWatcher: eth-priceWatcher:
image: local/tornadocash/relayer/4.14 image: tornadocash/relayer:mainnet-v4
profiles: [ 'eth' ]
restart: always restart: always
command: priceWatcher command: priceWatcher
# env_file: .env.eth env_file: .env
environment: environment:
NET_ID: 1 NET_ID: 1
REDIS_URL: redis://redis/0 REDIS_URL: redis://redis/0
depends_on: [redis, eth-server] depends_on: [redis, eth-server]
eth-healthWatcher: eth-healthWatcher:
image: local/tornadocash/relayer/4.14 image: tornadocash/relayer:mainnet-v4
profiles: [ 'eth' ]
restart: always restart: always
command: healthWatcher command: healthWatcher
# env_file: .env.eth env_file: .env
environment: environment:
NET_ID: 1 NET_ID: 1
REDIS_URL: redis://redis/0 REDIS_URL: redis://redis/0
depends_on: [redis, eth-server] depends_on: [redis, eth-server]
eth-worker1: eth-worker1:
image: local/tornadocash/relayer/4.14 image: tornadocash/relayer:mainnet-v4
profiles: [ 'eth' ]
restart: always restart: always
command: worker command: worker
# env_file: .env.eth env_file: .env
environment: environment:
NET_ID: 1 NET_ID: 1
REDIS_URL: redis://redis/0 REDIS_URL: redis://redis/0
depends_on: [redis, eth-server] depends_on: [redis, eth-server]
# worker2: # worker2:
# image: tornadocash/relayer:mining # image: tornadocash/relayer:mainnet-v4
# restart: always # restart: always
# command: worker # command: worker
# env_file: .env # env_file: .env
@ -128,393 +124,6 @@ services:
# ... # ...
# -----END RSA PRIVATE KEY----- # -----END RSA PRIVATE KEY-----
# # auto update docker containers when new image is pushed to docker hub (be careful with that)
# watchtower:
# image: v2tec/watchtower
# restart: always
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock
# # this container will send Telegram notifications when other containers are stopped/restarted
# # it's best to run this container on some other instance, otherwise it can't notify if the whole instance goes down
# notifier:
# image: poma/docker-telegram-notifier
# restart: always
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock:ro
# environment:
# # How to create bot: https://core.telegram.org/bots#3-how-do-i-create-a-bot
# # How to get chat id: https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id/32572159#32572159
# TELEGRAM_NOTIFIER_BOT_TOKEN: ...
# TELEGRAM_NOTIFIER_CHAT_ID: ...
# # this container will send Telegram notifications if specified address doesn't have enough funds
# monitor_mainnet:
# image: peppersec/monitor_eth
# restart: always
# environment:
# TELEGRAM_NOTIFIER_BOT_TOKEN: ...
# TELEGRAM_NOTIFIER_CHAT_ID: ...
# ADDRESS: '0x0000000000000000000000000000000000000000'
# THRESHOLD: 0.5 # ETH
# RPC_URL: https://mainnet.infura.io
# BLOCK_EXPLORER: etherscan.io
# -------------------------------------------------- #
# ---------------------- BNB ----------------------- #
bnb-server:
build: .
image: local/tornadocash/relayer/4.14
profiles: [ 'bnb' ]
restart: always
command: server
# env_file: .env.bnb
environment:
NET_ID: 56
REDIS_URL: redis://redis/1
nginx_proxy_read_timeout: 600
depends_on: [ redis ]
bnb-priceWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'bnb' ]
restart: always
command: priceWatcher
# env_file: .env.bnb
environment:
NET_ID: 56
REDIS_URL: redis://redis/1
depends_on: [ redis, bnb-server ]
bnb-healthWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'bnb' ]
restart: always
command: healthWatcher
# env_file: .env.bnb
environment:
NET_ID: 56
REDIS_URL: redis://redis/1
depends_on: [ redis, bnb-server ]
bnb-worker1:
image: local/tornadocash/relayer/4.14
profiles: [ 'bnb' ]
restart: always
command: worker
# env_file: .env.bnb
environment:
NET_ID: 56
REDIS_URL: redis://redis/1
depends_on: [ redis, bnb-server ]
# -------------------------------------------------- #
# ---------------------- MATIC --------------------- #
matic-server:
build: .
image: local/tornadocash/relayer/4.14
profiles: [ 'matic' ]
restart: always
command: server
# env_file: .env.matic
environment:
NET_ID: 137
REDIS_URL: redis://redis/2
nginx_proxy_read_timeout: 600
depends_on: [ redis ]
matic-priceWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'matic' ]
restart: always
command: priceWatcher
# env_file: .env.matic
environment:
NET_ID: 137
REDIS_URL: redis://redis/2
depends_on: [ redis, matic-server ]
matic-healthWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'matic' ]
restart: always
command: healthWatcher
# env_file: .env.matic
environment:
NET_ID: 137
REDIS_URL: redis://redis/2
depends_on: [ redis, matic-server ]
matic-worker1:
image: local/tornadocash/relayer/4.14
profiles: [ 'matic' ]
restart: always
command: worker
# env_file: .env.matic
environment:
NET_ID: 137
REDIS_URL: redis://redis/2
depends_on: [ redis, matic-server ]
# -------------------------------------------------- #
# ---------------------- XDAI ---------------------- #
xdai-server:
build: .
image: local/tornadocash/relayer/4.14
profiles: [ 'xdai' ]
restart: always
command: server
# env_file: .env.xdai
environment:
NET_ID: 100
REDIS_URL: redis://redis/3
nginx_proxy_read_timeout: 600
depends_on: [ redis ]
xdai-priceWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'xdai' ]
restart: always
command: priceWatcher
# env_file: .env.xdai
environment:
NET_ID: 100
REDIS_URL: redis://redis/3
depends_on: [ redis, xdai-server ]
xdai-healthWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'xdai' ]
restart: always
command: healthWatcher
# env_file: .env.xdai
environment:
NET_ID: 100
REDIS_URL: redis://redis/3
depends_on: [ redis, xdai-server ]
xdai-worker1:
image: local/tornadocash/relayer/4.14
profiles: [ 'xdai' ]
restart: always
command: worker
# env_file: .env.xdai
environment:
NET_ID: 100
REDIS_URL: redis://redis/3
depends_on: [ redis, xdai-server ]
# -------------------------------------------------- #
# ---------------------- AVAX ---------------------- #
avax-server:
build: .
image: local/tornadocash/relayer/4.14
profiles: [ 'avax' ]
restart: always
command: server
# env_file: .env.avax
environment:
NET_ID: 43114
REDIS_URL: redis://redis/4
nginx_proxy_read_timeout: 600
depends_on: [ redis ]
avax-priceWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'avax' ]
restart: always
command: priceWatcher
# env_file: .env.avax
environment:
NET_ID: 43114
REDIS_URL: redis://redis/4
depends_on: [ redis, avax-server ]
avax-healthWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'avax' ]
restart: always
command: healthWatcher
# env_file: .env.avax
environment:
NET_ID: 43114
REDIS_URL: redis://redis/4
depends_on: [ redis, avax-server ]
avax-worker1:
image: local/tornadocash/relayer/4.14
profiles: [ 'avax' ]
restart: always
command: worker
# env_file: .env.avax
environment:
NET_ID: 43114
REDIS_URL: redis://redis/4
depends_on: [ redis, avax-server ]
# -------------------------------------------------- #
# ---------------------- OP ------------------------ #
op-server:
build: .
image: local/tornadocash/relayer/4.14
profiles: [ 'op' ]
restart: always
command: server
# env_file: .env.op
environment:
NET_ID: 10
REDIS_URL: redis://redis/5
nginx_proxy_read_timeout: 600
depends_on: [ redis ]
op-priceWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'op' ]
image: tornadocash/relayer:mining
restart: always
command: priceWatcher
# env_file: .env.op
environment:
NET_ID: 10
REDIS_URL: redis://redis/5
depends_on: [ redis, op-server ]
op-healthWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'op' ]
image: tornadocash/relayer:mining
restart: always
command: healthWatcher
# env_file: .env.op
environment:
NET_ID: 10
REDIS_URL: redis://redis/5
depends_on: [ redis, op-server ]
op-worker1:
image: local/tornadocash/relayer/4.14
profiles: [ 'op' ]
image: tornadocash/relayer:mining
restart: always
command: worker
# env_file: .env.op
environment:
NET_ID: 10
REDIS_URL: redis://redis/5
depends_on: [ redis, op-server ]
# -------------------------------------------------- #
# ---------------------- ARB ----------------------- #
arb-server:
build: .
image: local/tornadocash/relayer/4.14
profiles: [ 'arb' ]
restart: always
command: server
# env_file: .env.arb
environment:
NET_ID: 42161
REDIS_URL: redis://redis/6
nginx_proxy_read_timeout: 600
depends_on: [ redis ]
arb-priceWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'arb' ]
restart: always
command: priceWatcher
# env_file: .env.arb
environment:
NET_ID: 42161
REDIS_URL: redis://redis/6
depends_on: [ redis, arb-server ]
arb-healthWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'arb' ]
restart: always
command: healthWatcher
# env_file: .env.arb
environment:
NET_ID: 42161
REDIS_URL: redis://redis/6
depends_on: [ redis, arb-server ]
arb-worker1:
image: local/tornadocash/relayer/4.14
profiles: [ 'arb' ]
restart: always
command: worker
# env_file: .env.arb
environment:
NET_ID: 42161
REDIS_URL: redis://redis/6
depends_on: [ redis, arb-server ]
# -------------------------------------------------- #
# ---------------------- GETH ---------------------- #
geth-server:
build: .
image: local/tornadocash/relayer/4.14
profiles: [ 'geth' ]
restart: always
command: server
# env_file: .env.geth
environment:
NET_ID: 5
REDIS_URL: redis://redis/7
nginx_proxy_read_timeout: 600
depends_on: [ redis ]
geth-priceWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'geth' ]
restart: always
command: priceWatcher
# env_file: .env.geth
environment:
NET_ID: 5
REDIS_URL: redis://redis/7
depends_on: [ redis, geth-server ]
geth-healthWatcher:
image: local/tornadocash/relayer/4.14
profiles: [ 'geth' ]
restart: always
command: healthWatcher
# env_file: .env.geth
environment:
NET_ID: 5
REDIS_URL: redis://redis/7
depends_on: [ redis, geth-server ]
geth-worker1:
image: local/tornadocash/relayer/4.14
profiles: [ 'geth' ]
restart: always
command: worker
# env_file: .env.geth
environment:
NET_ID: 5
REDIS_URL: redis://redis/7
depends_on: [ redis, geth-server ]
# -------------------------------------------------- #
volumes: volumes:
conf: conf:
vhost: vhost:

@ -1,6 +1,6 @@
{ {
"name": "relay", "name": "relay",
"version": "4.1.4", "version": "4.1.6",
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash", "description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
"scripts": { "scripts": {
"server": "node src/server.js", "server": "node src/server.js",
@ -13,25 +13,27 @@
"prettier:fix": "npx prettier --write . --config .prettierrc", "prettier:fix": "npx prettier --write . --config .prettierrc",
"lint": "yarn eslint && yarn prettier:check", "lint": "yarn eslint && yarn prettier:check",
"test": "mocha", "test": "mocha",
"start": "yarn server & yarn priceWatcher & yarn treeWatcher & yarn worker & yarn healthWatcher" "build": "docker build -t tornadocash/relayer:mainnet-v4 .",
"start": "docker-compose up -d redis && concurrently \"yarn server\" \"yarn priceWatcher\" \"yarn treeWatcher\" \"yarn worker\" \"yarn healthWatcher\""
}, },
"author": "tornado.cash", "author": "tornado.cash",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tornado/anonymity-mining": "^2.1.5",
"@tornado/circomlib": "^0.0.21",
"@tornado/fixed-merkle-tree": "^0.4",
"@tornado/tornado-config": "^1",
"@tornado/tornado-oracles": "1.2.2",
"@tornado/tx-manager": "^0.4.9",
"ajv": "^6.12.5", "ajv": "^6.12.5",
"async-mutex": "^0.2.4", "async-mutex": "^0.2.4",
"bull": "^3.12.1", "bull": "^3.12.1",
"circomlib": "git+https://github.com/tornadocash/circomlib.git#3b492f9801573eebcfe1b6c584afe8a3beecf2b4", "concurrently": "^8.2.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"eth-ens-namehash": "^2.0.8", "eth-ens-namehash": "^2.0.8",
"express": "^4.17.1", "express": "^4.17.1",
"fixed-merkle-tree": "^0.4.0",
"gas-price-oracle": "^0.4.7",
"ioredis": "^4.14.1", "ioredis": "^4.14.1",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"torn-token": "1.0.6",
"tornado-anonymity-mining": "^2.1.2",
"tx-manager": "^0.4.8",
"uuid": "^8.3.0", "uuid": "^8.3.0",
"web3": "^1.3.0", "web3": "^1.3.0",
"web3-core-promievent": "^1.3.0", "web3-core-promievent": "^1.3.0",

@ -1,14 +1,13 @@
require('dotenv').config() require('dotenv').config()
const { jobType } = require('./constants') const { jobType } = require('./constants')
const tornConfig = require('torn-token') const tornConfig = require('@tornado/tornado-config')
module.exports = { module.exports = {
netId: Number(process.env.NET_ID) || 1, netId: Number(process.env.NET_ID) || 1,
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379', redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
httpRpcUrl: process.env.HTTP_RPC_URL, httpRpcUrl: process.env.HTTP_RPC_URL,
wsRpcUrl: process.env.WS_RPC_URL, wsRpcUrl: process.env.WS_RPC_URL,
oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://mainnet.infura.io/', oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://api.securerpc.com/v1',
offchainOracleAddress: '0x07D91f5fb9Bf7798734C3f606dB065549F6893bb',
aggregatorAddress: process.env.AGGREGATOR, aggregatorAddress: process.env.AGGREGATOR,
minerMerkleTreeHeight: 20, minerMerkleTreeHeight: 20,
privateKey: process.env.PRIVATE_KEY, privateKey: process.env.PRIVATE_KEY,
@ -21,8 +20,6 @@ module.exports = {
governanceAddress: '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce', governanceAddress: '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce',
tornadoGoerliProxy: '0x454d870a72e29d5E5697f635128D18077BD04C60', tornadoGoerliProxy: '0x454d870a72e29d5E5697f635128D18077BD04C60',
gasLimits: { gasLimits: {
[jobType.TORNADO_WITHDRAW]: 390000,
WITHDRAW_WITH_EXTRA: 700000,
[jobType.MINING_REWARD]: 455000, [jobType.MINING_REWARD]: 455000,
[jobType.MINING_WITHDRAW]: 400000, [jobType.MINING_WITHDRAW]: 400000,
}, },

@ -12,7 +12,7 @@ async function status(req, res) {
res.json({ res.json({
rewardAccount, rewardAccount,
instances: instances[`netId${netId}`], instances: instances[netId],
netId, netId,
ethPrices, ethPrices,
tornadoServiceFee, tornadoServiceFee,

@ -1,41 +1,13 @@
const { offchainOracleAddress } = require('./config') const { setSafeInterval, RelayerError, logRelayerError } = require('./utils')
const {
getArgsForOracle,
setSafeInterval,
toChecksumAddress,
toBN,
RelayerError,
logRelayerError,
} = require('./utils')
const { redis } = require('./modules/redis') const { redis } = require('./modules/redis')
const web3 = require('./modules/web3')('oracle') const { TokenPriceOracle } = require('@tornado/tornado-oracles')
const { oracleRpcUrl } = require('./config')
const offchainOracleABI = require('../abis/OffchainOracle.abi.json') const priceOracle = new TokenPriceOracle(oracleRpcUrl)
const offchainOracle = new web3.eth.Contract(offchainOracleABI, offchainOracleAddress)
const { tokenAddresses, oneUintAmount, currencyLookup } = getArgsForOracle()
async function main() { async function main() {
try { try {
const ethPrices = {} const ethPrices = await priceOracle.fetchPrices()
for (let i = 0; i < tokenAddresses.length; i++) {
try {
const isWrap =
toChecksumAddress(tokenAddresses[i]) ===
toChecksumAddress('0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643')
const price = await offchainOracle.methods.getRateToEth(tokenAddresses[i], isWrap).call()
const numerator = toBN(oneUintAmount[i])
const denominator = toBN(10).pow(toBN(18)) // eth decimals
const priceFormatted = toBN(price).mul(numerator).div(denominator)
ethPrices[currencyLookup[tokenAddresses[i]]] = priceFormatted.toString()
} catch (e) {
console.error('cant get price of ', tokenAddresses[i])
}
}
if (!Object.values(ethPrices).length) {
throw new RelayerError('Can`t update prices', 1)
}
await redis.hmset('prices', ethPrices) await redis.hmset('prices', ethPrices)
console.log('Wrote following prices to redis', ethPrices) console.log('Wrote following prices to redis', ethPrices)
} catch (e) { } catch (e) {

@ -1,4 +1,4 @@
const MerkleTree = require('fixed-merkle-tree') const MerkleTree = require('@tornado/fixed-merkle-tree')
const { minerMerkleTreeHeight, torn, netId } = require('./config') const { minerMerkleTreeHeight, torn, netId } = require('./config')
const { poseidonHash2, toBN, logRelayerError } = require('./utils') const { poseidonHash2, toBN, logRelayerError } = require('./utils')
const resolver = require('./modules/resolver') const resolver = require('./modules/resolver')

@ -1,17 +1,9 @@
const { instances, netId } = require('./config') const { instances, netId } = require('./config')
const { poseidon } = require('circomlib') const { poseidon } = require('@tornado/circomlib')
const { toBN, toChecksumAddress, BN, fromWei, isAddress, toWei } = require('web3-utils') const { toBN, toChecksumAddress, BN, fromWei, isAddress, toWei } = require('web3-utils')
const TOKENS = {
torn: {
tokenAddress: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
symbol: 'TORN',
decimals: 18,
},
}
const addressMap = new Map() const addressMap = new Map()
const instance = instances[`netId${netId}`] const instance = instances[netId]
for (const [currency, { instanceAddress, symbol, decimals }] of Object.entries(instance)) { for (const [currency, { instanceAddress, symbol, decimals }] of Object.entries(instance)) {
Object.entries(instanceAddress).forEach(([amount, address]) => Object.entries(instanceAddress).forEach(([amount, address]) =>
@ -61,24 +53,6 @@ function when(source, event) {
}) })
} }
function getArgsForOracle() {
const tokens = {
...instances.netId1,
...TOKENS,
}
const tokenAddresses = []
const oneUintAmount = []
const currencyLookup = {}
Object.entries(tokens).map(([currency, data]) => {
if (currency !== 'eth') {
tokenAddresses.push(data.tokenAddress)
oneUintAmount.push(toBN('10').pow(toBN(data.decimals.toString())).toString())
currencyLookup[data.tokenAddress] = currency
}
})
return { tokenAddresses, oneUintAmount, currencyLookup }
}
function fromDecimals(value, decimals) { function fromDecimals(value, decimals) {
value = value.toString() value = value.toString()
let ether = value.toString() let ether = value.toString()
@ -155,7 +129,6 @@ module.exports = {
poseidonHash2, poseidonHash2,
sleep, sleep,
when, when,
getArgsForOracle,
fromDecimals, fromDecimals,
toBN, toBN,
toChecksumAddress, toChecksumAddress,

@ -1,7 +1,7 @@
const fs = require('fs') const fs = require('fs')
const MerkleTree = require('fixed-merkle-tree') const MerkleTree = require('@tornado/fixed-merkle-tree')
const { GasPriceOracle } = require('gas-price-oracle') const { TornadoFeeOracleV4, bump } = require('@tornado/tornado-oracles')
const { Utils, Controller } = require('tornado-anonymity-mining') const { Utils, Controller } = require('@tornado/anonymity-mining')
const swapABI = require('../abis/swap.abi.json') const swapABI = require('../abis/swap.abi.json')
const miningABI = require('../abis/mining.abi.json') const miningABI = require('../abis/mining.abi.json')
@ -11,11 +11,9 @@ const { queue } = require('./queue')
const { const {
poseidonHash2, poseidonHash2,
getInstance, getInstance,
fromDecimals, isAddress,
sleep, sleep,
toBN, toBN,
toWei,
fromWei,
toChecksumAddress, toChecksumAddress,
RelayerError, RelayerError,
logRelayerError, logRelayerError,
@ -32,9 +30,10 @@ const {
miningServiceFee, miningServiceFee,
tornadoServiceFee, tornadoServiceFee,
tornadoGoerliProxy, tornadoGoerliProxy,
rewardAccount,
} = require('./config') } = require('./config')
const resolver = require('./modules/resolver') const resolver = require('./modules/resolver')
const { TxManager } = require('tx-manager') const { TxManager } = require('@tornado/tx-manager')
const { redis, redisSubscribe } = require('./modules/redis') const { redis, redisSubscribe } = require('./modules/redis')
const getWeb3 = require('./modules/web3') const getWeb3 = require('./modules/web3')
@ -46,7 +45,7 @@ let txManager
let controller let controller
let swap let swap
let minerContract let minerContract
const gasPriceOracle = new GasPriceOracle({ defaultRpc: oracleRpcUrl }) const feeOracle = new TornadoFeeOracleV4(netId, oracleRpcUrl)
async function fetchTree() { async function fetchTree() {
const elements = await redis.get('tree:elements') const elements = await redis.get('tree:elements')
@ -118,51 +117,22 @@ function checkFee({ data }) {
return checkMiningFee(data) return checkMiningFee(data)
} }
async function getGasPrice() {
const block = await web3.eth.getBlock('latest')
if (block && block.baseFeePerGas) {
return toBN(block.baseFeePerGas)
}
const { fast } = await gasPriceOracle.gasPrices()
return toBN(toWei(fast.toString(), 'gwei'))
}
async function checkTornadoFee({ args, contract }) { async function checkTornadoFee({ args, contract }) {
const { currency, amount, decimals } = getInstance(contract) const { currency, amount, decimals } = getInstance(contract)
const [fee, refund] = [args[4], args[5]].map(toBN) const [userProvidedFee, refund] = [args[4], args[5]]
const gasPrice = await getGasPrice()
const ethPrice = await redis.hget('prices', currency) const ethPrice = await redis.hget('prices', currency)
const expense = gasPrice.mul(toBN(gasLimits[jobType.TORNADO_WITHDRAW])) const relayerEstimatedFee = await feeOracle.calculateWithdrawalFeeViaRelayer(
'relayer_withdrawal_check_v4',
const feePercent = toBN(fromDecimals(amount, decimals)) {},
.mul(toBN(parseInt(tornadoServiceFee * 1e10))) tornadoServiceFee,
.div(toBN(1e10 * 100)) currency,
amount,
let desiredFee decimals,
switch (currency) { refund,
case 'eth': { ethPrice,
desiredFee = expense.add(feePercent)
break
}
default: {
desiredFee = expense
.add(refund)
.mul(toBN(10 ** decimals))
.div(toBN(ethPrice))
desiredFee = desiredFee.add(feePercent)
break
}
}
console.log(
'sent fee, desired fee, feePercent',
fromWei(fee.toString()),
fromWei(desiredFee.toString()),
fromWei(feePercent.toString()),
) )
if (fee.lt(desiredFee)) { if (toBN(relayerEstimatedFee).gt(toBN(userProvidedFee))) {
throw new RelayerError( throw new RelayerError(
'Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.', 'Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.',
0, 0,
@ -171,12 +141,12 @@ async function checkTornadoFee({ args, contract }) {
} }
async function checkMiningFee({ args }) { async function checkMiningFee({ args }) {
const gasPrice = await getGasPrice() const gasPrice = await feeOracle.getGasPriceInHex()
const ethPrice = await redis.hget('prices', 'torn') const ethPrice = await redis.hget('prices', 'torn')
const isMiningReward = currentJob.data.type === jobType.MINING_REWARD const isMiningReward = currentJob.data.type === jobType.MINING_REWARD
const providedFee = isMiningReward ? toBN(args.fee) : toBN(args.extData.fee) const providedFee = isMiningReward ? toBN(args.fee) : toBN(args.extData.fee)
const expense = gasPrice.mul(toBN(gasLimits[currentJob.data.type])) const expense = toBN(gasPrice).mul(toBN(gasLimits[currentJob.data.type]))
const expenseInTorn = expense.mul(toBN(1e18)).div(toBN(ethPrice)) const expenseInTorn = expense.mul(toBN(1e18)).div(toBN(ethPrice))
// todo make aggregator for ethPrices and rewardSwap data // todo make aggregator for ethPrices and rewardSwap data
const balance = await swap.methods.tornVirtualBalance().call() const balance = await swap.methods.tornVirtualBalance().call()
@ -222,6 +192,17 @@ function checkOldProxy(address) {
return toChecksumAddress(address) === toChecksumAddress(OLD_PROXY) return toChecksumAddress(address) === toChecksumAddress(OLD_PROXY)
} }
async function checkRecipient({ data }) {
// Checks only for default withdrawals
if (data.type !== jobType.TORNADO_WITHDRAW) return
const recipient = data.args[2]
if (!isAddress(recipient)) throw new Error('Recipient address is invalid')
const addressCode = await web3.eth.getCode(toChecksumAddress(recipient))
if (addressCode !== '0x') throw new Error('Recipient cannot be a smart-contract, only EOA')
}
async function getTxObject({ data }) { async function getTxObject({ data }) {
if (data.type === jobType.TORNADO_WITHDRAW) { if (data.type === jobType.TORNADO_WITHDRAW) {
let { contract, isOldProxy } = await getProxyContract() let { contract, isOldProxy } = await getProxyContract()
@ -233,12 +214,17 @@ async function getTxObject({ data }) {
calldata = contract.methods.withdraw(data.proof, ...data.args).encodeABI() calldata = contract.methods.withdraw(data.proof, ...data.args).encodeABI()
} }
return { const incompleteTx = {
value: data.args[5], value: data.args[5],
to: contract._address, to: contract._address,
data: calldata, data: calldata,
gasLimit: gasLimits['WITHDRAW_WITH_EXTRA'],
} }
const [gasPrice, gasLimit] = await Promise.all([
feeOracle.getGasPrice('relayer_withdrawal'),
feeOracle.getGasLimit(incompleteTx, 'relayer_withdrawal'),
])
return Object.assign({ gasLimit, gasPrice }, incompleteTx)
} else { } else {
const method = data.type === jobType.MINING_REWARD ? 'reward' : 'withdraw' const method = data.type === jobType.MINING_REWARD ? 'reward' : 'withdraw'
const calldata = minerContract.methods[method](data.proof, data.args).encodeABI() const calldata = minerContract.methods[method](data.proof, data.args).encodeABI()
@ -280,9 +266,20 @@ async function processJob(job) {
} }
} }
async function checkRevert(tx) {
try {
await web3.eth.estimateGas(Object.assign({ from: rewardAccount }, tx))
} catch (e) {
throw new Error('Estimation error: transaction will possibly be reverted')
}
}
async function submitTx(job, retry = 0) { async function submitTx(job, retry = 0) {
await checkRecipient(job)
await checkFee(job) await checkFee(job)
currentTx = await txManager.createTx(await getTxObject(job)) const tx = await getTxObject(job)
await checkRevert(tx)
currentTx = await txManager.createTx(tx)
if (job.data.type !== jobType.TORNADO_WITHDRAW) { if (job.data.type !== jobType.TORNADO_WITHDRAW) {
await fetchTree() await fetchTree()

4819
yarn.lock

File diff suppressed because it is too large Load Diff