36 Commits

Author SHA1 Message Date
89367a8b43 Estimate gas to check possibly revert of invalid transaction that relayer don't spend gas accidentally 2023-10-27 23:04:10 -07:00
8ebcfe6b55 Change software version after fixing withdrawal bug that leads to revert if recipient is specific contract 2023-10-17 07:07:20 -07:00
8a7bb849cf Add protection from abusing relayers (force spending fee for reverted tx): fail if address is invalid or smart-contract (not EOA) 2023-10-16 16:37:49 -07:00
a2f7adce52 Update dependencies & bump required node version to 16 2023-09-18 23:17:43 -07:00
776d1a832d Use lib @tornado/tornado-oracles to fetch token prices and gas & bump relayer version to 4.1.5 2023-08-25 08:22:04 -07:00
4a996ddb91 Add volumes and redis ports to run local relayer correctly 2023-08-21 10:17:00 -07:00
01897a60e3 Add build command to create docker image for mainnet relayer locally from source 2023-07-12 12:56:11 -07:00
d1582f1d35 Change instructions to deployment on Ethereum Mainnet & fix lexical errors 2023-07-12 12:19:45 -07:00
49b4751c20 Change docker-compose.yml to single-network deployment 2023-07-12 12:18:52 -07:00
cc56bd77b5 Bump node to 14 2023-07-12 10:36:31 -07:00
1435457e67 Add concurrently to allow run local relayer on Windows & start redis in docker before running relayer software 2023-07-12 10:36:16 -07:00
e60b4a12e5 Change default withdraw fee to 0.4% on mainnet 2023-07-12 10:14:05 -07:00
gozzy
ed5d99cf44 nginx template ddos mitigation 2023-03-26 21:50:17 +00:00
gozzy
7d10fe2ab9 decrease minimumBalance & disable treeWatcher by default 2023-03-26 11:21:24 +00:00
gozzy
7d3cb5be49 update default rpc 2023-03-20 01:09:46 +00:00
gozzy
ff05e30bd2 update reverse-proxy app port 2023-03-19 21:18:15 +00:00
gozzy
1e8ddffdf1 update README.md 2023-03-19 21:13:12 +00:00
gozzy
3dc9314e29 local image compilation 2023-03-19 19:43:40 +00:00
gozzy
5f3da2578a yaml multi-network config 2023-03-18 23:01:06 +00:00
gozzy
cd6bd25d2c reinstate #1 2023-03-14 16:33:51 +00:00
smart_ex
2247730603 bump tx-manager 2022-05-25 19:21:03 +10:00
smart_ex
c915c97b85 priceWatcher oracle fix 2022-05-25 19:21:03 +10:00
smart_ex
582af773e6 tornado proxy address 2022-05-25 19:21:03 +10:00
smart_ex
9488090892 fix logging error messages 2022-05-25 19:21:03 +10:00
smart_ex
632dce129d fix queue collision 2022-05-25 19:21:03 +10:00
smart_ex
76cda01ee1 validate only selected network contract addresses 2022-05-25 19:21:03 +10:00
smart_ex
7f657c1d7d add test for isKnownContract validation 2022-05-25 19:21:03 +10:00
smart_ex
e386a1d23c fix hash map key 2022-05-25 19:21:03 +10:00
smart_ex
16d8e0fc28 fix contract address validation 2022-05-25 19:21:03 +10:00
smart_ex
a7fc9c4b24 block iframe 2022-05-25 19:21:03 +10:00
smart_ex
ee9e27ecad move errorsLog to health 2022-05-25 19:21:03 +10:00
smart_ex
95c6dc23c6 increment errors score 2022-05-25 19:21:03 +10:00
smart_ex
cfcf1c8677 show errors on status page 2022-05-25 19:21:03 +10:00
smart_ex
8868040882 write errors with positive score 2022-05-25 19:21:03 +10:00
smart_ex
50054e0516 bit of refactor, add RelayerError class 2022-05-25 19:21:03 +10:00
smart_ex
76209e11c0 remove proposal 10 check 2022-05-25 19:21:03 +10:00
30 changed files with 2995 additions and 3046 deletions

View File

@@ -1,8 +1,8 @@
NET_ID=1
HTTP_RPC_URL=https://mainnet.infura.io
WS_RPC_URL=wss://mainnet.infura.io/ws/v3/
HTTP_RPC_URL=https://api.securerpc.com/v1
# WS_RPC_URL=wss://mainnet.infura.io/ws/v3/
# ORACLE_RPC_URL should always point to the mainnet
ORACLE_RPC_URL=https://mainnet.infura.io
ORACLE_RPC_URL=https://api.securerpc.com/v1
REDIS_URL=redis://127.0.0.1:6379
# DNS settings
@@ -12,8 +12,8 @@ APP_PORT=8000
# without 0x prefix
PRIVATE_KEY=
# 0.05 means 0.05%
REGULAR_TORNADO_WITHDRAW_FEE=0.05
# 0.4 means 0.4%
REGULAR_TORNADO_WITHDRAW_FEE=0.4
MINING_SERVICE_FEE=0.05
REWARD_ACCOUNT=
CONFIRMATIONS=4

1
.gitignore vendored
View File

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

1
.npmrc Normal file
View File

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

View File

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

116
README.md
View File

@@ -1,42 +1,72 @@
# 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)
## Deploy with docker-compose
__*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.__
docker-compose.yml contains a stack that will automatically provision SSL certificates for your domain name and will add a https redirect to port 80.
__*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).__
1. Download [docker-compose.yml](/docker-compose.yml) and [.env.example](/.env.example)
## Deploy with docker-compose (recommended)
```
wget https://raw.githubusercontent.com/tornadocash/tornado-relayer/master/docker-compose.yml
wget https://raw.githubusercontent.com/tornadocash/tornado-relayer/master/.env.example -O .env
```
*The following instructions are for Ubuntu 22.10, other operating systems may vary. These instructions include automated SSL configuration with LetsEncrypt.*
2. Setup environment variables
__PREREQUISITES__
1. Update core dependencies
- `sudo apt-get update`
2. Install docker-compose
- `curl -SL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose && sudo chmod +x /usr/local/bin/docker-compose`
3. Install Docker
- `curl -fsSL https://get.docker.com -o get-docker.sh && chmod +x get-docker.sh && ./get-docker.sh`
4. Install git
- `sudo apt-get install git-all`
5. Install nginx
- `sudo apt install nginx`
6. Stop apache2 instance (enabled by default)
- `sudo systemctl stop apache2`
- set `NET_ID` (1 for mainnet, 5 for Goerli)
- set `HTTP_RPC_URL` rpc url for your ethereum node
- set `WS_RPC_URL` websocket url
- set `ORACLE_RPC_URL` - rpc url for mainnet node for fetching prices(always have to be on mainnet)
- set `PRIVATE_KEY` for your relayer address (without 0x prefix)
- set `VIRTUAL_HOST` and `LETSENCRYPT_HOST` to your domain and add DNS record pointing to your relayer ip address
- set `REGULAR_TORNADO_WITHDRAW_FEE` - fee in % that is used for tornado pool withdrawals
- set `MINING_SERVICE_FEE` - fee in % that is used for mining AP withdrawals
- set `REWARD_ACCOUNT` - eth address that is used to collect fees
- update `AGGREGATOR` if needed - Contract address of aggregator instance.
- update `CONFIRMATIONS` if needed - how many block confirmations to wait before processing an event. Not recommended to set less than 3
- update `MAX_GAS_PRICE` if needed - maximum value of gwei value for relayer's transaction
- update `BASE_FEE_RESERVE_PERCENTAGE` if needed - how much in % will the network baseFee increase
__FIREWALL CONFIGURATION__
If you want to use more than 1 eth address for relaying transactions, please add as many `workers` as you want. For example, you can comment out `worker2` in docker-compose.yml file, but please use a different `PRIVATE_KEY` for each worker.
_* Warning: Failure to configure SSH as the first UFW rule, will lock you out of the instance_
3. Run `docker-compose up -d`
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`*
3. Allow HTTP, and HTTPS by running `ufw allow https/tcp/http`
4. Finalize changes and enable firewall `ufw enable`
__DEPLOYMENT__
1. Clone the repository and enter the directory
- `git clone https://git.tornado.ws/tornadocash/classic-relayer -b mainnet-v4 && cd classic-relayer`
2. Clone the example environment file `.env.example` to configure for the preferred network - `cp .env.example .env` , then fill `.env` file.
- Set `PRIVATE_KEY` for your relayer address (remove the 0x from your private key)
- 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
- 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 `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`
5. Build and deploy the docker source by specifying the network through:
- `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
__NGINX REVERSE PROXY__
1. Copy the pre-modified nginx policy as your default policy
- `cp tornado.conf /etc/nginx/sites-available/default`
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`
3. Create the stream configuration
- `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
- `sudo systemctl restart nginx`
5. Stop nginx
- `sudo systemctl stop nginx`
## Run locally
1. `yarn`
1. `npm i`
2. `cp .env.example .env`
3. Modify `.env` as needed
4. `yarn start`
4. `npm run start`
5. Go to `http://127.0.0.1:8000`
6. In order to execute withdraw request, you can run following command
@@ -44,28 +74,26 @@ wget https://raw.githubusercontent.com/tornadocash/tornado-relayer/master/.env.e
curl -X POST -H 'content-type:application/json' --data '<input data>' http://127.0.0.1:8000/relay
```
Relayer should return a transaction hash
Relayer should return a transaction hash.
In that case you will need to add https termination yourself because browsers with default settings will prevent https
tornado.cash UI from submitting your request over http connection
_Note._ If you want to change contracts' addresses go to [config.js](./config.js) file.
## Run geth node
## Input data example
It is strongly recommended that you use your own RPC node. Instruction on how to run full node with `geth` can be found [here](https://github.com/feshchenkod/rpc-nodes).
## Monitoring
You can find the guide on how to install the Zabbix server in the [/monitoring/README.md](/monitoring/README.md).
## Architecture
1. TreeWatcher module keeps track of Account Tree changes and automatically caches the actual state in Redis and emits `treeUpdate` event to redis pub/sub channel
2. Server module is Express.js instance that accepts http requests
3. Controller contains handlers for the Server endpoints. It validates input data and adds a Job to Queue.
4. Queue module is used by Controller to put and get Job from queue (bull wrapper)
5. Status module contains handler to get a Job status. It's used by UI for pull updates
6. Validate contains validation logic for all endpoints
7. Worker is the main module that gets a Job from queue and processes it
```json
{
"proof": "0x0f8cb4c2ca9cbb23a5f21475773e19e39d3470436d7296f25c8730d19d88fcef2986ec694ad094f4c5fff79a4e5043bd553df20b23108bc023ec3670718143c20cc49c6d9798e1ae831fd32a878b96ff8897728f9b7963f0d5a4b5574426ac6203b2456d360b8e825d8f5731970bf1fc1b95b9713e3b24203667ecdd5939c2e40dec48f9e51d9cc8dc2f7f3916f0e9e31519c7df2bea8c51a195eb0f57beea4924cb846deaa78cdcbe361a6c310638af6f6157317bc27d74746bfaa2e1f8d2e9088fd10fa62100740874cdffdd6feb15c95c5a303f6bc226d5e51619c5b825471a17ddfeb05b250c0802261f7d05cf29a39a72c13e200e5bc721b0e4c50d55e6",
"args": [
"0x1579d41e5290ab5bcec9a7df16705e49b5c0b869095299196c19c5e14462c9e3",
"0x0cf7f49c5b35c48b9e1d43713e0b46a75977e3d10521e9ac1e4c3cd5e3da1c5d",
"0x03ebd0748aa4d1457cf479cce56309641e0a98f5",
"0xbd4369dc854c5d5b79fe25492e3a3cfcb5d02da5",
"0x000000000000000000000000000000000000000000000000058d15e176280000",
"0x0000000000000000000000000000000000000000000000000000000000000000"
],
"contract": "0xA27E34Ad97F171846bAf21399c370c9CE6129e0D"
}
```
Disclaimer:

View File

@@ -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"
}
]

2
app.js
View File

@@ -1 +1 @@
module.exports = require('./src/index')
module.exports = require('./src/server')

View File

@@ -1,62 +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
command: [redis-server, --appendonly, 'yes']
volumes:
- redis:/data
volumes:
redis:

View File

@@ -1,115 +1,14 @@
version: '2'
services:
server:
image: tornadocash/relayer:mining
restart: always
command: server
env_file: .env
environment:
REDIS_URL: redis://redis/0
nginx_proxy_read_timeout: 600
depends_on: [redis]
treeWatcher:
image: tornadocash/relayer:mining
restart: always
command: treeWatcher
env_file: .env
environment:
REDIS_URL: redis://redis/0
depends_on: [redis]
priceWatcher:
image: tornadocash/relayer:mining
restart: always
command: priceWatcher
env_file: .env
environment:
REDIS_URL: redis://redis/0
depends_on: [redis]
healthWatcher:
image: tornadocash/relayer:mining
restart: always
command: healthWatcher
env_file: .env
environment:
REDIS_URL: redis://redis/0
depends_on: [redis]
worker1:
image: tornadocash/relayer:mining
restart: always
command: worker
env_file: .env
environment:
REDIS_URL: redis://redis/0
depends_on: [redis]
# worker2:
# image: tornadocash/relayer:mining
# restart: always
# command: worker
# env_file: .env
# environment:
# PRIVATE_KEY: qwe
# REDIS_URL: redis://redis/0
# # this container will proxy *.onion domain to the server container
# # if you want to run *only* as .onion service, you don't need `nginx`, `letsencrypt`, `dockergen` containers
# tor:
# image: strm/tor
# restart: always
# depends_on: [server]
# environment:
# LISTEN_PORT: 80
# REDIRECT: server:8000
# # Generate a new key with
# # docker run --rm --entrypoint shallot strm/tor-hiddenservice-nginx ^foo
# PRIVATE_KEY: |
# -----BEGIN 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
redis:
image: redis
restart: always
command: [redis-server, --appendonly, 'yes']
volumes:
- redis:/data
ports:
- '127.0.0.1:6379:6379'
nginx:
image: nginx:alpine
@@ -146,6 +45,85 @@ services:
- nginx
- dockergen
# ---------------------- ETH ----------------------- #
eth-server:
build: .
image: tornadocash/relayer:mainnet-v4
restart: always
command: server
env_file: .env
environment:
NET_ID: 1
REDIS_URL: redis://redis/0
nginx_proxy_read_timeout: 600
depends_on: [redis]
eth-treeWatcher:
image: tornadocash/relayer:mainnet-v4
restart: always
command: treeWatcher
env_file: .env
environment:
NET_ID: 1
REDIS_URL: redis://redis/0
depends_on: [redis, eth-server]
eth-priceWatcher:
image: tornadocash/relayer:mainnet-v4
restart: always
command: priceWatcher
env_file: .env
environment:
NET_ID: 1
REDIS_URL: redis://redis/0
depends_on: [redis, eth-server]
eth-healthWatcher:
image: tornadocash/relayer:mainnet-v4
restart: always
command: healthWatcher
env_file: .env
environment:
NET_ID: 1
REDIS_URL: redis://redis/0
depends_on: [redis, eth-server]
eth-worker1:
image: tornadocash/relayer:mainnet-v4
restart: always
command: worker
env_file: .env
environment:
NET_ID: 1
REDIS_URL: redis://redis/0
depends_on: [redis, eth-server]
# worker2:
# image: tornadocash/relayer:mainnet-v4
# restart: always
# command: worker
# env_file: .env
# environment:
# PRIVATE_KEY: qwe
# REDIS_URL: redis://redis/0
# # this container will proxy *.onion domain to the server container
# # if you want to run *only* as .onion service, you don't need `nginx`, `letsencrypt`, `dockergen` containers
# tor:
# image: strm/tor
# restart: always
# depends_on: [server]
# environment:
# LISTEN_PORT: 80
# REDIRECT: server:8000
# # Generate a new key with
# # docker run --rm --entrypoint shallot strm/tor-hiddenservice-nginx ^foo
# PRIVATE_KEY: |
# -----BEGIN RSA PRIVATE KEY-----
# ...
# -----END RSA PRIVATE KEY-----
volumes:
conf:
vhost:

View File

@@ -1,6 +1,6 @@
{
"name": "relay",
"version": "4.1.3",
"version": "4.1.6",
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
"scripts": {
"server": "node src/server.js",
@@ -13,25 +13,27 @@
"prettier:fix": "npx prettier --write . --config .prettierrc",
"lint": "yarn eslint && yarn prettier:check",
"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",
"license": "MIT",
"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",
"async-mutex": "^0.2.4",
"bull": "^3.12.1",
"circomlib": "git+https://github.com/tornadocash/circomlib.git#3b492f9801573eebcfe1b6c584afe8a3beecf2b4",
"concurrently": "^8.2.0",
"dotenv": "^8.2.0",
"eth-ens-namehash": "^2.0.8",
"express": "^4.17.1",
"fixed-merkle-tree": "^0.4.0",
"gas-price-oracle": "^0.3.5",
"ioredis": "^4.14.1",
"node-fetch": "^2.6.7",
"torn-token": "1.0.6",
"tornado-anonymity-mining": "^2.1.2",
"tx-manager": "^0.4.1",
"uuid": "^8.3.0",
"web3": "^1.3.0",
"web3-core-promievent": "^1.3.0",

View File

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

View File

@@ -2,9 +2,9 @@ const {
getTornadoWithdrawInputError,
getMiningRewardInputError,
getMiningWithdrawInputError,
} = require('./validator')
const { postJob } = require('./queue')
const { jobType } = require('./constants')
} = require('../modules/validator')
const { postJob } = require('../queue')
const { jobType } = require('../constants')
async function tornadoWithdraw(req, res) {
const inputError = getTornadoWithdrawInputError(req.body)

4
src/contollers/index.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
controller: require('./controller'),
status: require('./status'),
}

View File

@@ -1,18 +1,18 @@
const queue = require('./queue')
const { netId, tornadoServiceFee, miningServiceFee, instances, redisUrl, rewardAccount } = require('./config')
const { version } = require('../package.json')
const Redis = require('ioredis')
const redis = new Redis(redisUrl)
const queue = require('../queue')
const { netId, tornadoServiceFee, miningServiceFee, instances, rewardAccount } = require('../config')
const { version } = require('../../package.json')
const { redis } = require('../modules/redis')
const { readRelayerErrors } = require('../utils')
async function status(req, res) {
const ethPrices = await redis.hgetall('prices')
const health = await redis.hgetall('health')
health.errorsLog = await readRelayerErrors(redis)
const { waiting: currentQueue } = await queue.queue.getJobCounts()
res.json({
rewardAccount,
instances: instances[`netId${netId}`],
instances: instances[netId],
netId,
ethPrices,
tornadoServiceFee,

View File

@@ -1,20 +1,14 @@
const Web3 = require('web3')
const Redis = require('ioredis')
const { toBN, fromWei } = require('web3-utils')
const { setSafeInterval } = require('./utils')
const { redisUrl, httpRpcUrl, privateKey, minimumBalance } = require('./config')
const web3 = new Web3(httpRpcUrl)
const redis = new Redis(redisUrl)
const { setSafeInterval, toBN, fromWei, RelayerError } = require('./utils')
const { privateKey, minimumBalance } = require('./config')
const { redis } = require('./modules/redis')
const web3 = require('./modules/web3')()
async function main() {
try {
const { address } = web3.eth.accounts.privateKeyToAccount(privateKey)
const balance = await web3.eth.getBalance(address)
if (toBN(balance).lt(toBN(minimumBalance))) {
throw new Error(`Not enough balance, less than ${fromWei(minimumBalance)} ETH`)
throw new RelayerError(`Not enough balance, less than ${fromWei(minimumBalance)} ETH`, 1)
}
await redis.hset('health', { status: true, error: '' })

11
src/modules/redis.js Normal file
View File

@@ -0,0 +1,11 @@
const { createClient } = require('ioredis')
const { redisUrl } = require('../config')
const redis = createClient(redisUrl)
const redisSubscribe = createClient(redisUrl)
module.exports = {
redis,
redisSubscribe,
redisUrl,
}

View File

@@ -1,7 +1,7 @@
const { httpRpcUrl, aggregatorAddress } = require('./config')
const Web3 = require('web3')
const web3 = new Web3(httpRpcUrl)
const aggregator = new web3.eth.Contract(require('../abis/Aggregator.abi.json'), aggregatorAddress)
const { aggregatorAddress } = require('../config')
const web3 = require('./web3')()
const aggregator = new web3.eth.Contract(require('../../abis/Aggregator.abi.json'), aggregatorAddress)
const ens = require('eth-ens-namehash')
class ENSResolver {
@@ -26,5 +26,4 @@ class ENSResolver {
return addresses.length === 1 ? addresses[0] : addresses
}
}
module.exports = ENSResolver
module.exports = new ENSResolver()

View File

@@ -1,6 +1,6 @@
const { isAddress, toChecksumAddress } = require('web3-utils')
const { getInstance } = require('./utils')
const { rewardAccount } = require('./config')
const { getInstance } = require('../utils')
const { rewardAccount } = require('../config')
const Ajv = require('ajv')
const ajv = new Ajv({ format: 'fast' })
@@ -19,7 +19,7 @@ ajv.addKeyword('isAddress', {
ajv.addKeyword('isKnownContract', {
validate: (schema, data) => {
try {
return getInstance(data) !== null
return !!getInstance(data)
} catch (e) {
return false
}

30
src/modules/web3.js Normal file
View File

@@ -0,0 +1,30 @@
const Web3 = require('web3')
const { oracleRpcUrl, httpRpcUrl, wsRpcUrl } = require('../config')
const getWeb3 = (type = 'http') => {
let url
switch (type) {
case 'oracle':
url = oracleRpcUrl
break
case 'ws':
url = wsRpcUrl
return new Web3(
new Web3.providers.WebsocketProvider(wsRpcUrl, {
clientConfig: {
maxReceivedFrameSize: 100000000,
maxReceivedMessageSize: 100000000,
},
}),
)
case 'http':
default:
url = httpRpcUrl
break
}
return new Web3(
new Web3.providers.HttpProvider(url, {
timeout: 200000, // ms
}),
)
}
module.exports = getWeb3

View File

@@ -1,45 +1,17 @@
const Redis = require('ioredis')
const { redisUrl, offchainOracleAddress, oracleRpcUrl } = require('./config')
const { getArgsForOracle, setSafeInterval } = require('./utils')
const { toChecksumAddress } = require('web3-utils')
const redis = new Redis(redisUrl)
const Web3 = require('web3')
const web3 = new Web3(
new Web3.providers.HttpProvider(oracleRpcUrl, {
timeout: 200000, // ms
}),
)
const { setSafeInterval, RelayerError, logRelayerError } = require('./utils')
const { redis } = require('./modules/redis')
const { TokenPriceOracle } = require('@tornado/tornado-oracles')
const { oracleRpcUrl } = require('./config')
const offchainOracleABI = require('../abis/OffchainOracle.abi.json')
const offchainOracle = new web3.eth.Contract(offchainOracleABI, offchainOracleAddress)
const { tokenAddresses, oneUintAmount, currencyLookup } = getArgsForOracle()
const { toBN } = require('web3-utils')
const priceOracle = new TokenPriceOracle(oracleRpcUrl)
async function main() {
try {
const ethPrices = {}
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])
}
}
const ethPrices = await priceOracle.fetchPrices()
await redis.hmset('prices', ethPrices)
console.log('Wrote following prices to redis', ethPrices)
} catch (e) {
await logRelayerError(redis, e)
console.error('priceWatcher error', e)
}
}

View File

@@ -1,11 +1,11 @@
const { v4: uuid } = require('uuid')
const Queue = require('bull')
const Redis = require('ioredis')
const { redisUrl } = require('./config')
const { status } = require('./constants')
const redis = new Redis(redisUrl)
const queue = new Queue('proofs', redisUrl, {
const { netId } = require('./config')
const { status } = require('./constants')
const { redis, redisUrl } = require('./modules/redis')
const queue = new Queue(`proofs_${netId}`, redisUrl, {
lockDuration: 300000, // Key expiration time for job locks.
lockRenewTime: 30000, // Interval on which to acquire the job lock
stalledInterval: 30000, // How often check for stalled jobs (use 0 for never checking).

30
src/router.js Normal file
View File

@@ -0,0 +1,30 @@
const { controller, status } = require('./contollers')
const router = require('express').Router()
// Add CORS headers
router.use((req, res, next) => {
res.header('X-Frame-Options', 'DENY')
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
next()
})
// Log error to console but don't send it to the client to avoid leaking data
router.use((err, req, res, next) => {
if (err) {
console.error(err)
return res.sendStatus(500)
}
next()
})
router.get('/', status.index)
router.get('/v1/status', status.status)
router.get('/v1/jobs/:id', status.getJob)
router.post('/v1/tornadoWithdraw', controller.tornadoWithdraw)
router.get('/status', status.status)
router.post('/relay', controller.tornadoWithdraw)
router.post('/v1/miningReward', controller.miningReward)
router.post('/v1/miningWithdraw', controller.miningWithdraw)
module.exports = router

View File

@@ -1,41 +1,14 @@
const express = require('express')
const status = require('./status')
const controller = require('./controller')
const { port, rewardAccount } = require('./config')
const { version } = require('../package.json')
const { isAddress } = require('web3-utils')
const app = express()
app.use(express.json())
// Add CORS headers
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
next()
})
// Log error to console but don't send it to the client to avoid leaking data
app.use((err, req, res, next) => {
if (err) {
console.error(err)
return res.sendStatus(500)
}
next()
})
app.get('/', status.index)
app.get('/v1/status', status.status)
app.get('/v1/jobs/:id', status.getJob)
app.post('/v1/tornadoWithdraw', controller.tornadoWithdraw)
app.get('/status', status.status)
app.post('/relay', controller.tornadoWithdraw)
app.post('/v1/miningReward', controller.miningReward)
app.post('/v1/miningWithdraw', controller.miningWithdraw)
const { isAddress } = require('./utils')
const router = require('./router')
if (!isAddress(rewardAccount)) {
throw new Error('No REWARD_ACCOUNT specified')
}
const app = express()
app.use(express.json())
app.use(router)
app.listen(port)
console.log(`Relayer ${version} started on port ${port}`)

View File

@@ -1,21 +1,10 @@
const MerkleTree = require('fixed-merkle-tree')
const { redisUrl, wsRpcUrl, minerMerkleTreeHeight, torn, netId } = require('./config')
const { poseidonHash2 } = require('./utils')
const { toBN } = require('web3-utils')
const Redis = require('ioredis')
const redis = new Redis(redisUrl)
const ENSResolver = require('./resolver')
const resolver = new ENSResolver()
const Web3 = require('web3')
const web3 = new Web3(
new Web3.providers.WebsocketProvider(wsRpcUrl, {
clientConfig: {
maxReceivedFrameSize: 100000000,
maxReceivedMessageSize: 100000000,
},
}),
)
const MerkleTree = require('@tornado/fixed-merkle-tree')
const { minerMerkleTreeHeight, torn, netId } = require('./config')
const { poseidonHash2, toBN, logRelayerError } = require('./utils')
const resolver = require('./modules/resolver')
const web3 = require('./modules/web3')('ws')
const MinerABI = require('../abis/mining.abi.json')
const { redis } = require('./modules/redis')
let contract
// eslint-disable-next-line no-unused-vars
@@ -123,7 +112,7 @@ async function init() {
const newCommitments = newEvents
.sort((a, b) => a.returnValues.index - b.returnValues.index)
.map(e => toBN(e.returnValues.commitment))
.filter((item, index, arr) => !index || item != arr[index - 1])
.filter((item, index, arr) => !index || item !== arr[index - 1])
const commitments = cachedCommitments.concat(newCommitments)
@@ -134,6 +123,7 @@ async function init() {
eventSubscription = contract.events.NewAccount({ fromBlock: toBlock + 1 }, processNewEvent)
blockSubscription = web3.eth.subscribe('newBlockHeaders', processNewBlock)
} catch (e) {
await logRelayerError(redis, e)
console.error('error on init treeWatcher', e.message)
}
}

View File

@@ -1,28 +1,30 @@
const { instances, netId } = require('./config')
const { poseidon } = require('circomlib')
const { toBN, toChecksumAddress, BN } = require('web3-utils')
const { poseidon } = require('@tornado/circomlib')
const { toBN, toChecksumAddress, BN, fromWei, isAddress, toWei } = require('web3-utils')
const TOKENS = {
torn: {
tokenAddress: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
symbol: 'TORN',
decimals: 18,
},
const addressMap = new Map()
const instance = instances[netId]
for (const [currency, { instanceAddress, symbol, decimals }] of Object.entries(instance)) {
Object.entries(instanceAddress).forEach(([amount, address]) =>
addressMap.set(`${netId}_${address}`, {
currency,
amount,
symbol,
decimals,
}),
)
}
const sleep = ms => new Promise(res => setTimeout(res, ms))
function getInstance(address) {
address = toChecksumAddress(address)
const inst = instances[`netId${netId}`]
for (const currency of Object.keys(inst)) {
for (const amount of Object.keys(inst[currency].instanceAddress)) {
if (inst[currency].instanceAddress[amount] === address) {
return { currency, amount }
}
}
const key = `${netId}_${toChecksumAddress(address)}`
if (addressMap.has(key)) {
return addressMap.get(key)
} else {
throw new Error('Unknown contact address')
}
return null
}
const poseidonHash = items => toBN(poseidon(items).toString())
@@ -51,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) {
value = value.toString()
let ether = value.toString()
@@ -118,12 +102,41 @@ function fromDecimals(value, decimals) {
return new BN(wei.toString(10), 10)
}
class RelayerError extends Error {
constructor(message, score = 0) {
super(message)
this.score = score
}
}
const logRelayerError = async (redis, e) => {
await redis.zadd('errors', 'INCR', e.score || 1, e.message)
}
const readRelayerErrors = async redis => {
const set = await redis.zrevrange('errors', 0, -1, 'WITHSCORES')
const errors = []
while (set.length) {
const [message, score] = set.splice(0, 2)
errors.push({ message, score })
}
return errors
}
module.exports = {
getInstance,
setSafeInterval,
poseidonHash2,
sleep,
when,
getArgsForOracle,
fromDecimals,
toBN,
toChecksumAddress,
fromWei,
toWei,
BN,
isAddress,
RelayerError,
logRelayerError,
readRelayerErrors,
}

View File

@@ -1,25 +1,28 @@
const fs = require('fs')
const Web3 = require('web3')
const { toBN, toWei, fromWei, toChecksumAddress } = require('web3-utils')
const MerkleTree = require('fixed-merkle-tree')
const Redis = require('ioredis')
const { GasPriceOracle } = require('gas-price-oracle')
const { Utils, Controller } = require('tornado-anonymity-mining')
const MerkleTree = require('@tornado/fixed-merkle-tree')
const { TornadoFeeOracleV4, bump } = require('@tornado/tornado-oracles')
const { Utils, Controller } = require('@tornado/anonymity-mining')
const swapABI = require('../abis/swap.abi.json')
const miningABI = require('../abis/mining.abi.json')
const tornadoABI = require('../abis/tornadoABI.json')
const tornadoProxyABI = require('../abis/tornadoProxyABI.json')
const aggregatorAbi = require('../abis/Aggregator.abi.json')
const { queue } = require('./queue')
const { poseidonHash2, getInstance, fromDecimals, sleep } = require('./utils')
const {
poseidonHash2,
getInstance,
isAddress,
sleep,
toBN,
toChecksumAddress,
RelayerError,
logRelayerError,
} = require('./utils')
const { jobType, status } = require('./constants')
const {
torn,
netId,
redisUrl,
gasLimits,
instances,
privateKey,
httpRpcUrl,
oracleRpcUrl,
@@ -27,12 +30,12 @@ const {
miningServiceFee,
tornadoServiceFee,
tornadoGoerliProxy,
governanceAddress,
aggregatorAddress,
rewardAccount,
} = require('./config')
const ENSResolver = require('./resolver')
const resolver = new ENSResolver()
const { TxManager } = require('tx-manager')
const resolver = require('./modules/resolver')
const { TxManager } = require('@tornado/tx-manager')
const { redis, redisSubscribe } = require('./modules/redis')
const getWeb3 = require('./modules/web3')
let web3
let currentTx
@@ -42,9 +45,7 @@ let txManager
let controller
let swap
let minerContract
const redis = new Redis(redisUrl)
const redisSubscribe = new Redis(redisUrl)
const gasPriceOracle = new GasPriceOracle({ defaultRpc: oracleRpcUrl })
const feeOracle = new TornadoFeeOracleV4(netId, oracleRpcUrl)
async function fetchTree() {
const elements = await redis.get('tree:elements')
@@ -78,7 +79,8 @@ async function fetchTree() {
async function start() {
try {
web3 = new Web3(httpRpcUrl)
await clearErrors()
web3 = getWeb3()
const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env
txManager = new TxManager({
privateKey,
@@ -103,6 +105,7 @@ async function start() {
queue.process(processJob)
console.log('Worker started')
} catch (e) {
await logRelayerError(redis, e)
console.error('error on start worker', e.message)
}
}
@@ -114,65 +117,36 @@ function checkFee({ data }) {
return checkMiningFee(data)
}
async function getGasPrice() {
const block = await web3.eth.getBlock('latest')
if (block && block.baseFeePerGas) {
const baseFeePerGas = toBN(block.baseFeePerGas)
return baseFeePerGas
}
const { fast } = await gasPriceOracle.gasPrices()
const gasPrice = toBN(toWei(fast.toString(), 'gwei'))
return gasPrice
}
async function checkTornadoFee({ args, contract }) {
const { currency, amount } = getInstance(contract)
const { decimals } = instances[`netId${netId}`][currency]
const [fee, refund] = [args[4], args[5]].map(toBN)
const gasPrice = await getGasPrice()
const { currency, amount, decimals } = getInstance(contract)
const [userProvidedFee, refund] = [args[4], args[5]]
const ethPrice = await redis.hget('prices', currency)
const expense = gasPrice.mul(toBN(gasLimits[jobType.TORNADO_WITHDRAW]))
const feePercent = toBN(fromDecimals(amount, decimals))
.mul(toBN(parseInt(tornadoServiceFee * 1e10)))
.div(toBN(1e10 * 100))
let desiredFee
switch (currency) {
case 'eth': {
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()),
const relayerEstimatedFee = await feeOracle.calculateWithdrawalFeeViaRelayer(
'relayer_withdrawal_check_v4',
{},
tornadoServiceFee,
currency,
amount,
decimals,
refund,
ethPrice,
)
if (fee.lt(desiredFee)) {
throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.')
if (toBN(relayerEstimatedFee).gt(toBN(userProvidedFee))) {
throw new RelayerError(
'Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.',
0,
)
}
}
async function checkMiningFee({ args }) {
const gasPrice = await getGasPrice()
const gasPrice = await feeOracle.getGasPriceInHex()
const ethPrice = await redis.hget('prices', 'torn')
const isMiningReward = currentJob.data.type === jobType.MINING_REWARD
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))
// todo make aggregator for ethPrices and rewardSwap data
const balance = await swap.methods.tornVirtualBalance().call()
@@ -194,21 +168,7 @@ async function checkMiningFee({ args }) {
serviceFeePercent.toString(),
)
if (toBN(providedFee).lt(desiredFee)) {
throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.')
}
}
async function isLatestProposalExecuted() {
const PROPOSAL_EXECUTED_STATUS = 5
const expectedProposalId = 10
try {
const aggregator = new web3.eth.Contract(aggregatorAbi, aggregatorAddress)
const proposals = await aggregator.methods.getAllProposals(governanceAddress).call()
const expectedProposal = proposals[expectedProposalId - 1]
return expectedProposal && Number(expectedProposal['state']) === PROPOSAL_EXECUTED_STATUS
} catch (e) {
console.error(e.message)
return false
throw new RelayerError('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.')
}
}
@@ -217,10 +177,7 @@ async function getProxyContract() {
if (netId === 5) {
proxyAddress = tornadoGoerliProxy
} else {
const latestProposalExecuted = await isLatestProposalExecuted()
proxyAddress = latestProposalExecuted
? await resolver.resolve(torn.tornadoRouter.address)
: await resolver.resolve(torn.tornadoProxy.address)
proxyAddress = await resolver.resolve(torn.tornadoRouter.address)
}
const contract = new web3.eth.Contract(tornadoProxyABI, proxyAddress)
@@ -235,6 +192,17 @@ function checkOldProxy(address) {
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 }) {
if (data.type === jobType.TORNADO_WITHDRAW) {
let { contract, isOldProxy } = await getProxyContract()
@@ -246,12 +214,17 @@ async function getTxObject({ data }) {
calldata = contract.methods.withdraw(data.proof, ...data.args).encodeABI()
}
return {
const incompleteTx = {
value: data.args[5],
to: contract._address,
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 {
const method = data.type === jobType.MINING_REWARD ? 'reward' : 'withdraw'
const calldata = minerContract.methods[method](data.proof, data.args).encodeABI()
@@ -280,7 +253,7 @@ async function isOutdatedTreeRevert(receipt, currentTx) {
async function processJob(job) {
try {
if (!jobType[job.data.type]) {
throw new Error(`Unknown job type: ${job.data.type}`)
throw new RelayerError(`Unknown job type: ${job.data.type}`)
}
currentJob = job
await updateStatus(status.ACCEPTED)
@@ -289,13 +262,24 @@ async function processJob(job) {
} catch (e) {
console.error('processJob', e.message)
await updateStatus(status.FAILED)
throw e
throw new RelayerError(e.message)
}
}
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) {
await checkRecipient(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) {
await fetchTree()
@@ -322,10 +306,10 @@ async function submitTx(job, retry = 0) {
await updateStatus(status.RESUBMITTED)
await submitTx(job, retry + 1)
} else {
throw new Error('Tree update retry limit exceeded')
throw new RelayerError('Tree update retry limit exceeded')
}
} else {
throw new Error('Submitted transaction failed')
throw new RelayerError('Submitted transaction failed')
}
}
} catch (e) {
@@ -341,10 +325,10 @@ async function submitTx(job, retry = 0) {
console.log('Tree is still not up to date, resubmitting')
await submitTx(job, retry + 1)
} else {
throw new Error('Tree update retry limit exceeded')
throw new RelayerError('Tree update retry limit exceeded')
}
} else {
throw new Error(`Revert by smart contract ${e.message}`)
throw new RelayerError(`Revert by smart contract ${e.message}`)
}
}
}
@@ -367,4 +351,9 @@ async function updateStatus(status) {
await currentJob.update(currentJob.data)
}
async function clearErrors() {
console.log('Errors list cleared')
await redis.del('errors')
}
start()

View File

@@ -4,7 +4,7 @@ const {
getTornadoWithdrawInputError,
getMiningRewardInputError,
getMiningWithdrawInputError,
} = require('../src/validator')
} = require('../src/modules/validator')
describe('Validator', () => {
describe('#getTornadoWithdrawInputError', () => {
@@ -19,7 +19,13 @@ describe('Validator', () => {
'.proof should match pattern "^0x[a-fA-F0-9]{512}$"',
)
})
it('should throw if unknown contract', () => {
const malformedData = { ...withdrawData }
malformedData.contract = '0xf17f52151ebef6c7334fad080c5704d77216b732'
getTornadoWithdrawInputError(malformedData).should.be.equal(
'.contract should pass "isKnownContract" keyword validation',
)
})
it('should throw something is missing', () => {
const malformedData = { ...withdrawData }
delete malformedData.proof

15
tornado-stream.conf Normal file
View File

@@ -0,0 +1,15 @@
map $ssl_preread_server_name $name {
yourdomain.com tornado_mainnet;
default tornado_mainnet;
}
upstream tornado_mainnet {
server 127.0.0.1:4380;
}
server {
listen 0.0.0.0:443;
proxy_pass $name;
ssl_preread on;
}

87
tornado.conf Normal file
View File

@@ -0,0 +1,87 @@
# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the
# scheme used to connect to this server
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
# If we receive X-Forwarded-Port, pass it through; otherwise, pass along the
# server port the client connected to
map $http_x_forwarded_port $proxy_x_forwarded_port {
default $http_x_forwarded_port;
'' $server_port;
}
# If we receive Upgrade, set Connection to "upgrade"; otherwise, delete any
# Connection header that may have been passed to this server
map $http_upgrade $proxy_connection {
default upgrade;
'' close;
}
# Apply fix for very long server names
server_names_hash_bucket_size 128;
# Default dhparam
# Set appropriate X-Forwarded-Ssl header based on $proxy_x_forwarded_proto
map $proxy_x_forwarded_proto $proxy_x_forwarded_ssl {
default off;
https on;
}
gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
log_format vhost '$host $remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$upstream_addr"';
# HTTP 1.1 support
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Original-URI $request_uri;
# Mitigate httpoxy attack (see README for details)
proxy_set_header Proxy "";
# Request rate limiting per second, 2Mb zone @ 5 requests per second
limit_req_zone $binary_remote_addr zone=one:2m rate=5r/s;
# Connections per IP limited to 2
limit_conn_zone $binary_remote_addr zone=two:2m;
server {
server_name _; # This is just an invalid value which will never trigger on a real hostname.
server_tokens off;
listen 80;
access_log /var/log/nginx/access.log vhost;
return 503;
}
server {
server_name yourdomain.com;
# Connection timeouts
client_body_timeout 10s;
client_header_timeout 10s;
listen 80;
access_log /var/log/nginx/access.log vhost;
# Do not HTTPS redirect LetsEncrypt ACME challenge
location ^~ /.well-known/acme-challenge/ {
limit_req zone=one;
limit_conn two 1;
proxy_pass http://127.0.0.1:8000;
break;
}
location / {
limit_req zone=one;
limit_conn two 1;
return 301 https://$host$request_uri;
}
}

4838
yarn.lock

File diff suppressed because it is too large Load Diff