Compare commits

..

66 Commits

Author SHA1 Message Date
de05a8b64b Update repository link (name changed, because classic-relayer repo merged with nova-relayer) 2023-10-07 07:30:40 -07:00
91ccc7c2ad Add Nova relayer server to docker-compose bundle 2023-10-07 07:02:07 -07:00
c338f27819 Add Nova relayer installation to script and instructions about Nova to README 2023-10-07 05:59:54 -07:00
a14f535d34 Update version badges in REAMDE 2023-09-07 11:15:09 -07:00
21c822b9f8 Add instructions for relayer how to change parameters if relayer already deployed and ran 2023-09-04 03:21:04 -07:00
ef32badf36 Download updated main repo to use up-to-date docker-compose configuration 2023-08-31 10:14:29 -07:00
021ad0e83b Use v4 relayer for mainnet and v5 for sidechains for backwards compatibility 2023-08-28 01:52:33 -07:00
f3041b59b9 Use v5 docker images in deployment 2023-07-30 01:55:16 -07:00
2246d6acd5 Fix README: change script link 2023-07-23 07:30:38 -07:00
7eb5fb0a43 Bump version to 5.1.0 (add smart gas estimation) 2023-07-13 22:14:34 -07:00
d709b49204 Change docker images tags & build images in source folders 2023-07-13 11:34:54 -07:00
3b6289bf2c Prepare main repository for v4 multiple network relayer deployment 2023-07-12 10:08:11 -07:00
6adeb27b83 Change .env name for mainnet 2023-07-11 19:17:20 -07:00
58dae5b030 Edit docker-compose config: change docker images to prebuilt local, change .env files and profiles naming for sidechains 2023-06-10 19:16:11 +03:00
3b13d3f508 Create .env for ETH mainnet 2023-06-10 19:12:24 +03:00
6b1a41585b Update tornado cash site link on relayer site index page 2023-05-06 14:00:32 +03:00
e90d1c4086 Create script to autoinstall dependecies & configure nginx & build docker containers for debian-based distros 2023-05-06 13:57:38 +03:00
61f464441b Delete old docker-compose.yml 2023-05-06 13:55:47 +03: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
Danil Kovtonyuk
3c5eaa2c4b
Update README.md 2022-04-13 21:11:16 +10:00
_den
fd36dd5c5e added link to instructions for running rpc nodes 2022-03-10 21:11:06 +10:00
Alexey Pertsev
350d1f1d11
Update Readme 2022-03-01 11:32:30 +01:00
Danil Kovtonyuk
49a90872a2
fix: gas limit 2022-02-22 04:33:26 +10:00
Sergei SMART
2f79125dd1 init check, add Governance contract 2022-02-16 11:19:40 +01:00
_den
1f900843de
added discord notification (#86)
* added discord notification

* updated notification message

* updated discord notification

* updated discord notification message

Co-authored-by: _den <_den@outlook.com>
2022-02-16 10:37:24 +01:00
dependabot[bot]
cc5ec13d97 Bump simple-get from 2.8.1 to 2.8.2
Bumps [simple-get](https://github.com/feross/simple-get) from 2.8.1 to 2.8.2.
- [Release notes](https://github.com/feross/simple-get/releases)
- [Commits](https://github.com/feross/simple-get/compare/v2.8.1...v2.8.2)

---
updated-dependencies:
- dependency-name: simple-get
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-16 10:34:03 +01:00
dependabot[bot]
9e8a6c79ac Bump node-fetch from 2.6.1 to 2.6.7
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.1 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.1...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-16 10:33:45 +01:00
dependabot[bot]
38b1169eae Bump pathval from 1.1.0 to 1.1.1
Bumps [pathval](https://github.com/chaijs/pathval) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/chaijs/pathval/releases)
- [Changelog](https://github.com/chaijs/pathval/blob/master/CHANGELOG.md)
- [Commits](https://github.com/chaijs/pathval/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: pathval
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-16 10:32:56 +01:00
dependabot[bot]
d95d495853 Bump tar from 4.4.13 to 4.4.19
Bumps [tar](https://github.com/npm/node-tar) from 4.4.13 to 4.4.19.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v4.4.13...v4.4.19)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-16 10:32:45 +01:00
dependabot[bot]
54e1c03f9e Bump glob-parent from 5.1.1 to 5.1.2
Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/gulpjs/glob-parent/releases)
- [Changelog](https://github.com/gulpjs/glob-parent/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gulpjs/glob-parent/compare/v5.1.1...v5.1.2)

---
updated-dependencies:
- dependency-name: glob-parent
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-16 10:32:33 +01:00
dependabot[bot]
429723f370 Bump normalize-url from 4.5.0 to 4.5.1
Bumps [normalize-url](https://github.com/sindresorhus/normalize-url) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/sindresorhus/normalize-url/releases)
- [Commits](https://github.com/sindresorhus/normalize-url/commits)

---
updated-dependencies:
- dependency-name: normalize-url
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-16 10:32:18 +01:00
dependabot[bot]
b457820cf5 Bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-16 10:31:27 +01:00
Danil Kovtonyuk
31d697701e
fix: lint 2022-01-19 22:11:00 +10:00
_den
b103033103 added zabbix guide to README.md 2022-01-19 22:03:39 +10:00
_den
1bb2d5f044 updated installation instructions 2022-01-19 22:03:39 +10:00
_den
44f1e1ec7a added templates for monitoring with zabbix 2022-01-19 22:03:39 +10:00
Danil Kovtonyuk
043356f5fe fix: initializing mining tree 2021-10-09 03:15:56 +10:00
Danil Kovtonyuk
8f5b673f3b feat: add env baseFeeReserve variable 2021-09-08 23:15:25 +10:00
Danil Kovtonyuk
82e5f4ee70 feat: add EIP-1559 support 2021-09-08 23:15:25 +10:00
nikdementev
cc0a252040
fix: bump cdai gas limit 2021-07-30 20:39:03 +03:00
nikdementev
a46afed752
styles: prettier 2021-06-19 23:20:19 +03:00
nikdementev
0cd9e515ae
fix: price method 2021-06-19 23:10:31 +03:00
nikdementev
fa77997896 fix: prices oracle 2021-06-19 23:05:38 +03:00
31 changed files with 599 additions and 6408 deletions

@ -1,3 +0,0 @@
node_modules
.env
.git

@ -1,9 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

@ -1,18 +0,0 @@
NET_ID=56
HTTP_RPC_URL=https://bsc-dataseed1.ninicoin.io/
REDIS_URL=redis://127.0.0.1:6379
# DNS settings
VIRTUAL_HOST=example.duckdns.org
LETSENCRYPT_HOST=example.duckdns.org
APP_PORT=8000
# without 0x prefix
PRIVATE_KEY=
# 0.1 means 0.1%
RELAYER_FEE=0.1
REWARD_ACCOUNT=
CONFIRMATIONS=4
# in GWEI
MAX_GAS_PRICE=1000

@ -1,45 +0,0 @@
{
"env": {
"node": true,
"browser": true,
"es6": true,
"mocha": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": [
"error",
2,
{
"SwitchCase": 1
}
],
"linebreak-style": ["error", "unix"],
"quotes": [
"error",
"single",
{
"avoidEscape": true
}
],
"semi": ["error", "never"],
"object-curly-spacing": ["error", "always"],
"require-await": "error",
"comma-dangle": ["error", "only-multiline"],
"space-before-function-paren": [
"error",
{
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}
]
}
}

@ -1,98 +0,0 @@
name: build
on:
push:
branches: ['*']
tags: ['v[0-9]+.[0-9]+.[0-9]+*']
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
- run: yarn install
- run: yarn test
- run: yarn lint
- name: Telegram Failure Notification
uses: appleboy/telegram-action@master
if: failure()
with:
message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) for sidechains because of ${{ github.actor }}
format: markdown
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
publish:
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set vars
id: vars
run: |
echo "::set-output name=version::$(echo ${GITHUB_REF#refs/tags/v})"
echo "::set-output name=repo_name::$(echo ${GITHUB_REPOSITORY#*/})"
- name: Check package.json version vs tag
run: |
[ ${{ steps.vars.outputs.version }} = $(sed -nE 's/^\s*"version": "([0-9]+.[0-9]+.[0-9]+(-beta.[0-9]+)?)",$/\1/p' package.json) ] || (echo "Git tag doesn't match version in package.json" && false)
- name: Build and push Docker image
uses: docker/build-push-action@v1.1.0
with:
dockerfile: Dockerfile
repository: tornadocash/relayer
tag_with_ref: true
tags: light
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Telegram Message Notify
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: 🚀 Published a [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}) for sidechains version ${{ steps.vars.outputs.version }} to docker hub
debug: true
format: markdown
- name: Telegram Relayer Channel Notification
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_RELAYER_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: |
🚀 Published a new version of the sidechains relayer node service to docker hub: `tornadocash/relayer:v${{ steps.vars.outputs.version }}` and `tornadocash/relayer:light`.
Please update your sidechains nodes ❗️
DO NOT TOUCH MAINNET AND NOVA RELAYERS.
debug: true
format: markdown
- name: Discord Relayer Channel Notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELAYER_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: |
🚀 Published a new version of the sidechains relayer node service to docker hub: `tornadocash/relayer:v${{ steps.vars.outputs.version }}` and `tornadocash/relayer:light`.
Please update your sidechains nodes ❗️
DO NOT TOUCH MAINNET AND NOVA RELAYERS.
- name: Telegram Failure Notification
uses: appleboy/telegram-action@master
if: failure()
with:
message: ❗ Failed to publish [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}/actions) for sidechains because of ${{ github.actor }}
format: markdown
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}

11
.gitignore vendored

@ -1,9 +1,4 @@
.vscode node_modules
node_modules/
.env .env
.env.* .env*
!.env.*.example
!.env.example
kovan.*
dump.rdb
.idea

1
.npmrc

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

@ -1 +0,0 @@
keys/TreeUpdate.json

@ -1,7 +0,0 @@
{
"semi": false,
"arrowParens": "avoid",
"singleQuote": true,
"printWidth": 110,
"trailingComma": "all"
}

@ -1,9 +0,0 @@
FROM node:16
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn && yarn cache clean --force
COPY . .
EXPOSE 8000
ENTRYPOINT ["yarn"]

166
README.md

@ -1,142 +1,60 @@
# Relayer for Tornado Cash [![Build Status](https://github.com/tornadocash/relayer/workflows/build/badge.svg)](https://github.com/tornadocash/relayer/actions) ![Static Badge](https://img.shields.io/badge/version-5.1.0-blue?logo=docker) # Relayer for Tornado Cash [![Build Status](https://github.com/tornadocash/relayer/workflows/build/badge.svg)](https://github.com/tornadocash/relayer/actions)![Sidechains version](https://img.shields.io/badge/version-5.2.1-blue?logo=docker)![Mainnet version](https://img.shields.io/badge/version-4.1.5-blue?logo=docker)
***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.** **\*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 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).** **\*It is recommended to run your Relayer on a VPS instnace (Virtual Private Server). 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 ## Deploy with script and docker-compose
__PREREQUISITES__ _The following instructions are for Ubuntu 22.10, other operating systems may vary._
1. Update core dependencies #### Installation:
- `sudo apt-get update` Just run in terminal:
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`
__FIREWALL CONFIGURATION__
_* Warning: Failure to configure SSH as the first UFW rule, will lock you out of the instance_
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`
__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`
__DEPLOYMENT__
1. Clone the repository and enter the directory
- `git clone https://git.tornado.ws/tornadocash/classic-relayer -b sidechain-v5 && cd classic-relayer`
2. Check environment files:
By default each network is preconfigured the naming of `.env.<NETWORK>`
- `.env.bsc` for Binance Smart Chain
- `.env.arb` for Arbitrum
- `.env.op` for Optimism
- `.env.gnosis` for Gnosis (xdai)
- `.env.polygon` for Polygon (matic)
- `.env.avax` for Avalanche C-Chain
3. Configure (fill) environment files for those networks on which the relayer will be deployed:
- 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
- Add a A wildcard record DNS record with the value assigned to your instance IP address to configure subdomains
- Set `RELAYER_FEE` to what you would like to charge as your fee
- 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
4. Build docker image for sidechain with simple `npm run build` command
5. Uncomment the `env_file` lines (remove `# `) for the associated network services in `docker-compose.yml` for chosen chains (networks)
6. Run docker-compose for the configured networks specified via `--profile <NETWORK_SYMBOL>`, for example (if you run relayer only Binance Smart Chain and Arbitrum):
- `docker-compose --profile bsc --profile arb up -d`
7. Visit your domain addresses and check each `/status` endpoint to ensure there is no errors in the `status` fields
## Run locally for one chain
1. `yarn`
2. `cp .env.example .env`
3. Modify `.env` as needed (described above)
4. `yarn start`
5. Go to `http://127.0.0.1:8000`
6. In order to execute withdraw request, you can run following command
```bash ```bash
curl -X POST -H 'content-type:application/json' --data '<input data>' http://127.0.0.1:8000/relay curl -s https://git.tornado.ws/tornadocash/tornado-relayer/raw/branch/main/install.sh | bash
``` ```
Relayer should return a transaction hash #### Configuring environments:
In that case you will need to add https termination yourself because browsers with default settings will prevent https 1. Go to `tornado-relayer` folder on the server home directory
tornado.cash UI from submitting your request over http connection 2. Check environment files:
## Run own RPC nodes By default each network is preconfigured the naming of `.env.<NETWORK>`
It is strongly recommended that you use your own RPC nodes. Instruction on how to run full nodes can be found [here](https://github.com/feshchenkod/rpc-nodes). - `.env.eth` for Ethereum Mainnet
- `.env.bsc` for Binance Smart Chain
- `.env.arb` for Arbitrum
- `.env.op` for Optimism
- `.env.gnosis` for Gnosis (xdai)
- `.env.polygon` for Polygon (matic)
- `.env.avax` for Avalanche C-Chain
## Architecture 3. Configure (fill) environment files for those networks on which the relayer will be deployed:
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 - Set `PRIVATE_KEY` to your relayer address (remove the 0x from your private key) to each environment file
2. Server module is Express.js instance that accepts http requests - _It is recommended not to reuse the same private keys for each network as a security measure_
3. Controller contains handlers for the Server endpoints. It validates input data and adds a Job to Queue. - Set `VIRTUAL_HOST` and `LETSENCRYPT_HOST` a unique subndomain for every network to each environment file
4. Queue module is used by Controller to put and get Job from queue (bull wrapper) - eg: `mainnet.example.com` for Ethereum, `binance.example.com` for Binance etc
5. Status module contains handler to get a Job status. It's used by UI for pull updates - add a A wildcard record DNS record with the value assigned to your instance IP address to configure subdomains
6. Validate contains validation logic for all endpoints - Set `RELAYER_FEE` to what you would like to charge as your fee (remember 0.3% is deducted from your staked relayer balance)
7. Worker is the main module that gets a Job from queue and processes it - 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
Disclaimer: 4(Optional). If you want to run relayer for [Nova](https://nova.tornado.ws), fill `.env.nova` file by instructions in [Nova branch](https://git.tornado.ws/tornadocash/tornado-relayer/src/branch/nova), because config is very specific
#### Deployment:
1. Build and deploy the docker source for the configured networks specified via `--profile <NETWORK_SYMBOL>`, for example (if you run relayer only for Ethereum Mainnet, Binance Smart Chain and Arbitrum):
- `docker-compose --profile eth --profile bsc --profile arb up -d`
2. Visit your domain addresses and check each `/status` endpoint to ensure there is no errors in the `status` fields
2. Optional: if you want to run Nova relayer, just add `--profile nova` to docker-compose command
If you want to change some relayer parameters, for example, RPC url or fee percent, stop the relayer software with command `docker-compose down --remove-orphans`, change in corresponding `.env.{name}` file what you need and rerun relayer as described above.
#### Disclaimer:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -1,151 +0,0 @@
[
{
"inputs": [{ "internalType": "address", "name": "_owner", "type": "address" }],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "internalType": "uint256", "name": "", "type": "uint256" }],
"name": "DecimalsUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "internalType": "uint256", "name": "", "type": "uint256" }],
"name": "GasPriceUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "internalType": "uint256", "name": "", "type": "uint256" }],
"name": "L1BaseFeeUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "internalType": "uint256", "name": "", "type": "uint256" }],
"name": "OverheadUpdated",
"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"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "internalType": "uint256", "name": "", "type": "uint256" }],
"name": "ScalarUpdated",
"type": "event"
},
{
"inputs": [],
"name": "decimals",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "gasPrice",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "bytes", "name": "_data", "type": "bytes" }],
"name": "getL1Fee",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "bytes", "name": "_data", "type": "bytes" }],
"name": "getL1GasUsed",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "l1BaseFee",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "overhead",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "scalar",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "_decimals", "type": "uint256" }],
"name": "setDecimals",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "_gasPrice", "type": "uint256" }],
"name": "setGasPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "_baseFee", "type": "uint256" }],
"name": "setL1BaseFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "_overhead", "type": "uint256" }],
"name": "setOverhead",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "_scalar", "type": "uint256" }],
"name": "setScalar",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]

@ -1,105 +0,0 @@
[
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes",
"name": "encryptedNote",
"type": "bytes"
}
],
"name": "EncryptedNote",
"type": "event"
},
{
"inputs": [
{
"internalType": "bytes[]",
"name": "_encryptedNotes",
"type": "bytes[]"
}
],
"name": "backupNotes",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract ITornadoInstance",
"name": "_tornado",
"type": "address"
},
{
"internalType": "bytes32",
"name": "_commitment",
"type": "bytes32"
},
{
"internalType": "bytes",
"name": "_encryptedNote",
"type": "bytes"
}
],
"name": "deposit",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract ITornadoInstance",
"name": "_tornado",
"type": "address"
},
{
"internalType": "bytes",
"name": "_proof",
"type": "bytes"
},
{
"internalType": "bytes32",
"name": "_root",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "_nullifierHash",
"type": "bytes32"
},
{
"internalType": "address payable",
"name": "_recipient",
"type": "address"
},
{
"internalType": "address payable",
"name": "_relayer",
"type": "address"
},
{
"internalType": "uint256",
"name": "_fee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_refund",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}
]

1
app.js

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

@ -1,14 +1,12 @@
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
@ -45,11 +43,131 @@ services:
- nginx - nginx
- dockergen - dockergen
# ---------------------- ETH Mainnet ----------------------- #
eth-server:
build: .
image: tornadocash/relayer:mainnet-v4
profiles: ["eth"]
restart: always
command: server
env_file: .env.eth
environment:
NET_ID: 1
REDIS_URL: redis://redis/0
nginx_proxy_read_timeout: 600
depends_on: [redis]
eth-treeWatcher:
image: tornadocash/relayer:mainnet-v4
profiles: ["eth"]
restart: always
command: treeWatcher
env_file: .env.eth
environment:
NET_ID: 1
REDIS_URL: redis://redis/0
depends_on: [redis, eth-server]
eth-priceWatcher:
image: tornadocash/relayer:mainnet-v4
profiles: ["eth"]
restart: always
command: priceWatcher
env_file: .env.eth
environment:
NET_ID: 1
REDIS_URL: redis://redis/0
depends_on: [redis, eth-server]
eth-healthWatcher:
image: tornadocash/relayer:mainnet-v4
profiles: ["eth"]
restart: always
command: healthWatcher
env_file: .env.eth
environment:
NET_ID: 1
REDIS_URL: redis://redis/0
depends_on: [redis, eth-server]
eth-worker1:
image: tornadocash/relayer:mainnet-v4
profiles: ["eth"]
restart: always
command: worker
env_file: .env.eth
environment:
NET_ID: 1
REDIS_URL: redis://redis/0
depends_on: [redis, eth-server]
# # This is additional worker for ethereum mainnet
# # So you can process transactions from multiple addresses, but before it you need to set up those addresses as workers
# eth-worker2:
# image: tornadocash/relayer:mainnet-v4
# profiles: [ 'eth' ]
# restart: always
# command: worker
# env_file: .env2.eth
# environment:
# 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
# -------------------------------------------------- #
# ---------------------- BSC (Binance Smart Chain) ----------------------- # # ---------------------- BSC (Binance Smart Chain) ----------------------- #
bsc-server: bsc-server:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['bsc'] profiles: ["bsc"]
restart: always restart: always
command: server command: server
env_file: .env.bsc env_file: .env.bsc
@ -61,7 +179,7 @@ services:
bsc-healthWatcher: bsc-healthWatcher:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['bsc'] profiles: ["bsc"]
restart: always restart: always
command: healthWatcher command: healthWatcher
env_file: .env.bsc env_file: .env.bsc
@ -72,7 +190,7 @@ services:
bsc-worker1: bsc-worker1:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['bsc'] profiles: ["bsc"]
restart: always restart: always
command: worker command: worker
env_file: .env.bsc env_file: .env.bsc
@ -87,7 +205,7 @@ services:
polygon-server: polygon-server:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['polygon'] profiles: ["polygon"]
restart: always restart: always
command: server command: server
env_file: .env.polygon env_file: .env.polygon
@ -99,7 +217,7 @@ services:
polygon-healthWatcher: polygon-healthWatcher:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['polygon'] profiles: ["polygon"]
restart: always restart: always
command: healthWatcher command: healthWatcher
env_file: .env.polygon env_file: .env.polygon
@ -110,7 +228,7 @@ services:
polygon-worker1: polygon-worker1:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['polygon'] profiles: ["polygon"]
restart: always restart: always
command: worker command: worker
env_file: .env.polygon env_file: .env.polygon
@ -125,7 +243,7 @@ services:
gnosis-server: gnosis-server:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['gnosis'] profiles: ["gnosis"]
restart: always restart: always
command: server command: server
env_file: .env.gnosis env_file: .env.gnosis
@ -137,7 +255,7 @@ services:
gnosis-healthWatcher: gnosis-healthWatcher:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['gnosis'] profiles: ["gnosis"]
restart: always restart: always
command: healthWatcher command: healthWatcher
env_file: .env.gnosis env_file: .env.gnosis
@ -148,7 +266,7 @@ services:
gnosis-worker1: gnosis-worker1:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['gnosis'] profiles: ["gnosis"]
restart: always restart: always
command: worker command: worker
env_file: .env.gnosis env_file: .env.gnosis
@ -163,7 +281,7 @@ services:
avax-server: avax-server:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['avax'] profiles: ["avax"]
restart: always restart: always
command: server command: server
env_file: .env.avax env_file: .env.avax
@ -175,7 +293,7 @@ services:
avax-healthWatcher: avax-healthWatcher:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['avax'] profiles: ["avax"]
restart: always restart: always
command: healthWatcher command: healthWatcher
env_file: .env.avax env_file: .env.avax
@ -186,7 +304,7 @@ services:
avax-worker1: avax-worker1:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['avax'] profiles: ["avax"]
restart: always restart: always
command: worker command: worker
env_file: .env.avax env_file: .env.avax
@ -201,7 +319,7 @@ services:
op-server: op-server:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['op'] profiles: ["op"]
restart: always restart: always
command: server command: server
env_file: .env.op env_file: .env.op
@ -213,7 +331,7 @@ services:
op-healthWatcher: op-healthWatcher:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['op'] profiles: ["op"]
restart: always restart: always
command: healthWatcher command: healthWatcher
env_file: .env.op env_file: .env.op
@ -224,7 +342,7 @@ services:
op-worker1: op-worker1:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['op'] profiles: ["op"]
restart: always restart: always
command: worker command: worker
env_file: .env.op env_file: .env.op
@ -239,7 +357,7 @@ services:
arb-server: arb-server:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['arb'] profiles: ["arb"]
restart: always restart: always
command: server command: server
env_file: .env.arb env_file: .env.arb
@ -251,7 +369,7 @@ services:
arb-healthWatcher: arb-healthWatcher:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['arb'] profiles: ["arb"]
restart: always restart: always
command: healthWatcher command: healthWatcher
env_file: .env.arb env_file: .env.arb
@ -262,7 +380,7 @@ services:
arb-worker1: arb-worker1:
image: tornadocash/relayer:sidechain-v5 image: tornadocash/relayer:sidechain-v5
profiles: ['arb'] profiles: ["arb"]
restart: always restart: always
command: worker command: worker
env_file: .env.arb env_file: .env.arb
@ -273,6 +391,78 @@ services:
# -------------------------------------------------- # # -------------------------------------------------- #
# ---------------------- Goerli (Ethereum Testnet) ---------------------- #
goerli-server:
image: tornadocash/relayer:mainnet-v4
profiles: ["geth"]
restart: always
command: server
env_file: .env.goerli
environment:
NET_ID: 5
REDIS_URL: redis://redis/7
nginx_proxy_read_timeout: 600
depends_on: [redis]
goerli-treeWatcher:
image: tornadocash/relayer:mainnet-v4
profiles: ["goerli"]
restart: always
command: treeWatcher
env_file: .env.goerli
environment:
NET_ID: 5
REDIS_URL: redis://redis/7
depends_on: [redis, goerli-server]
goerli-priceWatcher:
image: tornadocash/relayer:mainnet-v4
profiles: ["goerli"]
restart: always
command: priceWatcher
env_file: .env.goerli
environment:
NET_ID: 5
REDIS_URL: redis://redis/7
depends_on: [redis, goerli-server]
goerli-healthWatcher:
image: tornadocash/relayer:mainnet-v4
profiles: ["goerli"]
restart: always
command: healthWatcher
env_file: .env.goerli
environment:
NET_ID: 5
REDIS_URL: redis://redis/7
depends_on: [redis, goerli-server]
goerli-worker1:
image: tornadocash/relayer:mainnet-v4
profiles: ["goerli"]
restart: always
command: worker
env_file: .env.goerli
environment:
NET_ID: 5
REDIS_URL: redis://redis/7
depends_on: [redis, goerli-server]
# -------------------------------------------------- #
# ---------------------- Tornado Nova (Gnosis Chain) ----------------------- #
server:
image: tornadocash/relayer:nova
profiles: ["nova"]
restart: always
command: start:prod
env_file: .env.nova
environment:
REDIS_URL: redis://redis/8
nginx_proxy_read_timeout: 600
depends_on: [redis]
volumes: volumes:
conf: conf:
vhost: vhost:

120
install.sh Normal file

@ -0,0 +1,120 @@
#!/bin/bash
# Script must be running from root
if [ "$EUID" -ne 0 ];
then echo "Please run as root";
exit 1;
fi;
relayer_soft_git_repo="https://git.tornado.ws/tornadocash/tornado-relayer";
user_home_dir=$(eval echo ~$USER);
relayer_folder="$user_home_dir/tornado-relayer";
relayer_mainnet_soft_source_folder="$relayer_folder/mainnet-soft-source";
relayer_sidechains_soft_source_folder="$relayer_folder/sidechains-soft-source";
nova_relayer_soft_source_folder="$relayer_folder/nova-soft-source";
script_log_file="/tmp/tornado-relayer-installation.log"
if [ -f $script_log_file ]; then rm $script_log_file; fi;
function echo_log_err(){
echo $1 1>&2;
echo -e "$1\n" &>> $script_log_file;
}
function echo_log_err_and_exit(){
echo_log_err "$1";
exit 1;
}
function is_package_installed(){
if [ $(dpkg-query -W -f='${Status}' $1 2>/dev/null | grep -c "ok installed") -eq 0 ]; then return 1; else return 0; fi;
}
function install_requred_packages(){
apt update &>> $script_log_file;
requred_packages=("curl" "git-all" "ufw" "nginx");
local package;
for package in ${requred_packages[@]}; do
if ! is_package_installed $package; then
# Kill apache process, because Debian configuring nginx package right during installation
if [ $package = "nginx" ]; then systemctl stop apache2; fi;
apt install --yes --force-yes -o DPkg::Options::="--force-confold" $package &>> $script_log_file;
if ! is_package_installed $package; then
echo_log_err_and_exit "Error: cannot install \"$package\" package";
fi;
fi;
done;
echo -e "\nAll required packages installed successfully";
}
function install_node(){
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash;
. ~/.nvm/nvm.sh;
. ~/.profile;
. ~/.bashrc;
nvm install 14.21.3;
}
function install_repositories(){
git clone $relayer_soft_git_repo -b main $relayer_folder
git clone $relayer_soft_git_repo -b mainnet-v4 $relayer_mainnet_soft_source_folder;
git clone $relayer_soft_git_repo -b sidechain-v5 $relayer_sidechains_soft_source_folder;
git clone $relayer_soft_git_repo -b nova $nova_relayer_soft_source_folder;
}
function install_docker_utilities(){
local kernel_name=$(uname -s);
local processor_type=$(uname -m);
curl -SL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-$kernel_name-$processor_type -o /usr/local/bin/docker-compose;
chmod +x /usr/local/bin/docker-compose;
curl -s https://get.docker.com | bash;
}
function configure_firewall(){
ufw allow https/tcp;
ufw allow http/tcp;
ufw insert 1 allow OpenSSH;
echo "y" | ufw enable;
}
function configure_nginx_reverse_proxy(){
systemctl stop apache2;
cp $relayer_folder/tornado.conf /etc/nginx/sites-available/default;
echo "stream { map_hash_bucket_size 128; map_hash_max_size 128; include /etc/nginx/conf.d/streams/*.conf; }" >> /etc/nginx/nginx.conf;
mkdir /etc/nginx/conf.d/streams;
cp $relayer_folder/tornado-stream.conf /etc/nginx/conf.d/streams/tornado-stream.conf;
systemctl restart nginx;
systemctl stop nginx;
}
function build_relayer_docker_containers(){
cd $relayer_mainnet_soft_source_folder && npm run build;
cd $relayer_sidechains_soft_source_folder && npm run build;
cd $nova_relayer_soft_source_folder && npm run build:docker;
}
function prepare_environments(){
cp $relayer_mainnet_soft_source_folder/.env.example $relayer_folder/.env.eth;
cp $nova_relayer_soft_source_folder/.env.example $relayer_folder/.env.nova;
tee $relayer_folder/.env.bsc $relayer_folder/.env.arb $relayer_folder/.env.goerli $relayer_folder/.env.polygon $relayer_folder/.env.op \
$relayer_folder/.env.avax $relayer_folder/.env.gnosis < $relayer_sidechains_soft_source_folder/.env.example > /dev/null;
}
function main(){
install_requred_packages;
install_node;
install_repositories;
configure_firewall;
configure_nginx_reverse_proxy;
install_docker_utilities;
build_relayer_docker_containers;
prepare_environments;
cd $relayer_folder;
}
main;

@ -1,41 +0,0 @@
{
"name": "relay",
"version": "5.2.2",
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
"scripts": {
"server": "node src/server.js",
"worker": "node src/worker",
"healthWatcher": "node src/healthWatcher",
"eslint": "eslint --ext .js --ignore-path .gitignore .",
"prettier:check": "npx prettier --check . --config .prettierrc",
"prettier:fix": "npx prettier --write . --config .prettierrc",
"lint": "yarn eslint && yarn prettier:check",
"test": "mocha",
"build": "docker build -t tornadocash/relayer:sidechain-v5 .",
"start": "docker-compose up -d redis && concurrently \"yarn server\" \"yarn worker\" \"yarn healthWatcher\""
},
"author": "tornado.cash",
"license": "MIT",
"dependencies": {
"@tornado/tornado-config": "^1.0.8",
"@tornado/tornado-oracles": "^3.3.0",
"@tornado/tx-manager": "^0.4.9",
"ajv": "^6.12.5",
"bull": "^3.12.1",
"concurrently": "^8.2.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"ioredis": "^4.14.1",
"uuid": "^8.3.0",
"web3": "^1.3.0",
"web3-utils": "^1.2.2"
},
"devDependencies": {
"chai": "^4.2.0",
"eslint": "^6.6.0",
"eslint-config-prettier": "^6.12.0",
"eslint-plugin-prettier": "^3.1.4",
"mocha": "^8.1.3",
"prettier": "^2.1.2"
}
}

@ -1,27 +0,0 @@
require('dotenv').config()
const tornConfig = require('@tornado/tornado-config')
const { networkConfig } = require('./constants')
const netId = Number(process.env.NET_ID) || 56
const instances = tornConfig.instances[netId]
const proxyLight = tornConfig.tornadoProxyLight.address
const { gasPrices, nativeCurrency } = networkConfig[`netId${netId}`]
module.exports = {
netId,
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
httpRpcUrl: process.env.HTTP_RPC_URL,
oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://rpc.payload.de',
minerMerkleTreeHeight: 20,
privateKey: process.env.PRIVATE_KEY,
instances,
port: process.env.APP_PORT || 8000,
tornadoServiceFee: Number(process.env.RELAYER_FEE),
rewardAccount: process.env.REWARD_ACCOUNT,
gasPrices,
proxyLight,
nativeCurrency,
minimumBalance: netId === 137 || netId === 43114 ? '10000000000000000000' : '100000000000000000', // 10 or 0.1
}

@ -1,78 +0,0 @@
const jobType = Object.freeze({
TORNADO_WITHDRAW: 'TORNADO_WITHDRAW',
OP_TORNADO_WITHDRAW: 'OP_TORNADO_WITHDRAW',
ARB_TORNADO_WITHDRAW: 'ARB_TORNADO_WITHDRAW',
})
const status = Object.freeze({
QUEUED: 'QUEUED',
ACCEPTED: 'ACCEPTED',
SENT: 'SENT',
MINED: 'MINED',
RESUBMITTED: 'RESUBMITTED',
CONFIRMED: 'CONFIRMED',
FAILED: 'FAILED',
})
const networkConfig = {
netId56: {
gasPrices: {
instant: 5,
fast: 5,
standard: 5,
low: 5,
},
nativeCurrency: 'bnb',
},
netId10: {
gasPrices: {
instant: 0.001,
fast: 0.001,
standard: 0.001,
low: 0.001,
},
nativeCurrency: 'eth',
},
netId100: {
gasPrices: {
instant: 6,
fast: 5,
standard: 4,
low: 1,
},
nativeCurrency: 'xdai',
},
netId137: {
gasPrices: {
instant: 100,
fast: 75,
standard: 50,
low: 30,
},
nativeCurrency: 'matic',
},
netId42161: {
gasPrices: {
instant: 4,
fast: 3,
standard: 2.52,
low: 2.29,
},
nativeCurrency: 'eth',
},
netId43114: {
gasPrices: {
instant: 225,
fast: 35,
standard: 25,
low: 25,
},
nativeCurrency: 'avax',
},
}
module.exports = {
jobType,
status,
networkConfig,
}

@ -1,21 +0,0 @@
const { getTornadoWithdrawInputError } = require('./modules/validator')
const { postJob } = require('./queue')
const { jobType } = require('./constants')
async function tornadoWithdraw(req, res) {
const inputError = getTornadoWithdrawInputError(req.body)
if (inputError) {
console.log('Invalid input:', inputError)
return res.status(400).json({ error: inputError })
}
const id = await postJob({
type: jobType.TORNADO_WITHDRAW,
request: req.body,
})
return res.json({ id })
}
module.exports = {
tornadoWithdraw,
}

@ -1,27 +0,0 @@
const Web3 = require('web3')
const { toBN, fromWei } = require('web3-utils')
const { setSafeInterval, logRelayerError } = require('./utils')
const { httpRpcUrl, privateKey, minimumBalance, nativeCurrency, instances } = require('./config')
const web3 = new Web3(httpRpcUrl)
const { redis } = require('./modules/redis')
async function main() {
try {
const { address } = web3.eth.accounts.privateKeyToAccount(privateKey)
const balance = await web3.eth.getBalance(address)
if (toBN(balance).lt(toBN(minimumBalance))) {
const currency = instances[nativeCurrency].symbol
throw new Error(`Not enough balance, less than ${fromWei(minimumBalance)} ${currency}`)
}
await redis.hset('health', { status: true, error: '' })
} catch (e) {
console.error('healthWatcher', e.message)
await logRelayerError(redis, e)
await redis.hset('health', { status: false, error: e.message })
}
}
setSafeInterval(main, 30 * 1000)

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

@ -1,200 +0,0 @@
const { isAddress, toChecksumAddress } = require('web3-utils')
const { getInstance } = require('../utils')
const { rewardAccount } = require('../config')
const Ajv = require('ajv')
const ajv = new Ajv({ format: 'fast' })
ajv.addKeyword('isAddress', {
validate: (schema, data) => {
try {
return isAddress(data)
} catch (e) {
return false
}
},
errors: true,
})
ajv.addKeyword('isKnownContract', {
validate: (schema, data) => {
try {
return !!getInstance(data)
} catch (e) {
return false
}
},
errors: true,
})
ajv.addKeyword('isFeeRecipient', {
validate: (schema, data) => {
try {
return toChecksumAddress(rewardAccount) === toChecksumAddress(data)
} catch (e) {
return false
}
},
errors: true,
})
const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$', isAddress: true }
const proofType = { type: 'string', pattern: '^0x[a-fA-F0-9]{512}$' }
const encryptedAccountType = { type: 'string', pattern: '^0x[a-fA-F0-9]{392}$' }
const bytes32Type = { type: 'string', pattern: '^0x[a-fA-F0-9]{64}$' }
const instanceType = { ...addressType, isKnownContract: true }
const relayerType = { ...addressType, isFeeRecipient: true }
const tornadoWithdrawSchema = {
type: 'object',
properties: {
proof: proofType,
contract: instanceType,
args: {
type: 'array',
maxItems: 6,
minItems: 6,
items: [bytes32Type, bytes32Type, addressType, relayerType, bytes32Type, bytes32Type],
},
},
additionalProperties: false,
required: ['proof', 'contract', 'args'],
}
const miningRewardSchema = {
type: 'object',
properties: {
proof: proofType,
args: {
type: 'object',
properties: {
rate: bytes32Type,
fee: bytes32Type,
instance: instanceType,
rewardNullifier: bytes32Type,
extDataHash: bytes32Type,
depositRoot: bytes32Type,
withdrawalRoot: bytes32Type,
extData: {
type: 'object',
properties: {
relayer: relayerType,
encryptedAccount: encryptedAccountType,
},
additionalProperties: false,
required: ['relayer', 'encryptedAccount'],
},
account: {
type: 'object',
properties: {
inputRoot: bytes32Type,
inputNullifierHash: bytes32Type,
outputRoot: bytes32Type,
outputPathIndices: bytes32Type,
outputCommitment: bytes32Type,
},
additionalProperties: false,
required: [
'inputRoot',
'inputNullifierHash',
'outputRoot',
'outputPathIndices',
'outputCommitment',
],
},
},
additionalProperties: false,
required: [
'rate',
'fee',
'instance',
'rewardNullifier',
'extDataHash',
'depositRoot',
'withdrawalRoot',
'extData',
'account',
],
},
},
additionalProperties: false,
required: ['proof', 'args'],
}
const miningWithdrawSchema = {
type: 'object',
properties: {
proof: proofType,
args: {
type: 'object',
properties: {
amount: bytes32Type,
extDataHash: bytes32Type,
extData: {
type: 'object',
properties: {
fee: bytes32Type,
recipient: addressType,
relayer: relayerType,
encryptedAccount: encryptedAccountType,
},
additionalProperties: false,
required: ['fee', 'relayer', 'encryptedAccount', 'recipient'],
},
account: {
type: 'object',
properties: {
inputRoot: bytes32Type,
inputNullifierHash: bytes32Type,
outputRoot: bytes32Type,
outputPathIndices: bytes32Type,
outputCommitment: bytes32Type,
},
additionalProperties: false,
required: [
'inputRoot',
'inputNullifierHash',
'outputRoot',
'outputPathIndices',
'outputCommitment',
],
},
},
additionalProperties: false,
required: ['amount', 'extDataHash', 'extData', 'account'],
},
},
additionalProperties: false,
required: ['proof', 'args'],
}
const validateTornadoWithdraw = ajv.compile(tornadoWithdrawSchema)
const validateMiningReward = ajv.compile(miningRewardSchema)
const validateMiningWithdraw = ajv.compile(miningWithdrawSchema)
function getInputError(validator, data) {
validator(data)
if (validator.errors) {
const error = validator.errors[0]
return `${error.dataPath} ${error.message}`
}
return null
}
function getTornadoWithdrawInputError(data) {
return getInputError(validateTornadoWithdraw, data)
}
function getMiningRewardInputError(data) {
return getInputError(validateMiningReward, data)
}
function getMiningWithdrawInputError(data) {
return getInputError(validateMiningWithdraw, data)
}
module.exports = {
getTornadoWithdrawInputError,
getMiningRewardInputError,
getMiningWithdrawInputError,
}

@ -1,57 +0,0 @@
const { v4: uuid } = require('uuid')
const Queue = require('bull')
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).
maxStalledCount: 3, // Max amount of times a stalled job will be re-processed.
guardInterval: 5000, // Poll interval for delayed jobs and added jobs.
retryProcessDelay: 5000, // delay before processing next job in case of internal error.
drainDelay: 5, // A timeout for when the queue is in drained state (empty waiting for jobs).
})
async function postJob({ type, request }) {
const id = uuid()
const job = await queue.add(
{
id,
type,
status: status.QUEUED,
...request, // proof, args, ?contract
},
{
//removeOnComplete: true
},
)
await redis.set(`job:${id}`, job.id)
return id
}
async function getJob(uuid) {
const id = await redis.get(`job:${uuid}`)
return queue.getJobFromId(id)
}
async function getJobStatus(uuid) {
const job = await getJob(uuid)
if (!job) {
return null
}
return {
...job.data,
failedReason: job.failedReason,
}
}
module.exports = {
postJob,
getJob,
getJobStatus,
queue,
}

@ -1,29 +0,0 @@
const controller = require('./controller')
const status = require('./status')
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)
module.exports = router

@ -1,16 +0,0 @@
const express = require('express')
const router = require('./router')
const { port, rewardAccount } = require('./config')
const { version } = require('../package.json')
const { isAddress } = require('web3-utils')
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}`)

@ -1,38 +0,0 @@
const queue = require('./queue')
const { netId, tornadoServiceFee, instances, rewardAccount } = require('./config')
const { version } = require('../package.json')
const { readRelayerErrors } = require('./utils')
const { redis } = require('./modules/redis')
async function status(req, res) {
const health = await redis.hgetall('health')
health.errorsLog = await readRelayerErrors(redis)
const { waiting: currentQueue } = await queue.queue.getJobCounts()
res.json({
rewardAccount,
instances,
netId,
tornadoServiceFee,
version,
health,
currentQueue,
})
}
function index(req, res) {
res.send(
'This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/v1/status>/status</a> for settings',
)
}
async function getJob(req, res) {
const status = await queue.getJobStatus(req.params.id)
return status ? res.json(status) : res.status(400).json({ error: "The job doesn't exist" })
}
module.exports = {
status,
index,
getJob,
}

@ -1,107 +0,0 @@
const { instances } = require('./config')
const { toChecksumAddress, BN } = require('web3-utils')
const addressMap = new Map()
for (const [currency, { instanceAddress, symbol, decimals }] of Object.entries(instances)) {
Object.entries(instanceAddress).forEach(([amount, address]) =>
addressMap.set(address, {
currency,
amount,
symbol,
decimals,
}),
)
}
function getInstance(address) {
address = toChecksumAddress(address)
const key = toChecksumAddress(address)
if (addressMap.has(key)) {
return addressMap.get(key)
} else {
throw new Error('Unknown contact address')
}
}
function setSafeInterval(func, interval) {
func()
.catch(console.error)
.finally(() => {
setTimeout(() => setSafeInterval(func, interval), interval)
})
}
function fromDecimals(value, decimals) {
value = value.toString()
let ether = value.toString()
const base = new BN('10').pow(new BN(decimals))
const baseLength = base.toString(10).length - 1 || 1
const negative = ether.substring(0, 1) === '-'
if (negative) {
ether = ether.substring(1)
}
if (ether === '.') {
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, invalid value')
}
// Split it into a whole and fractional part
const comps = ether.split('.')
if (comps.length > 2) {
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal points')
}
let whole = comps[0]
let fraction = comps[1]
if (!whole) {
whole = '0'
}
if (!fraction) {
fraction = '0'
}
if (fraction.length > baseLength) {
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal places')
}
while (fraction.length < baseLength) {
fraction += '0'
}
whole = new BN(whole)
fraction = new BN(fraction)
let wei = whole.mul(base).add(fraction)
if (negative) {
wei = wei.mul(negative)
}
return new BN(wei.toString(10), 10)
}
const logRelayerError = async (redis, e) => {
await redis.zadd('errors', 'INCR', 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
}
const clearRelayerErrors = redis => {
redis.del('errors')
}
module.exports = {
getInstance,
setSafeInterval,
fromDecimals,
logRelayerError,
readRelayerErrors,
clearRelayerErrors,
}

@ -1,163 +0,0 @@
const Web3 = require('web3')
const { toBN, fromWei, isAddress, toChecksumAddress } = require('web3-utils')
const { redis } = require('./modules/redis')
const proxyLightABI = require('../abis/proxyLightABI.json')
const { queue } = require('./queue')
const { getInstance, logRelayerError, clearRelayerErrors } = require('./utils')
const { jobType, status } = require('./constants')
const { netId, gasPrices, privateKey, proxyLight, httpRpcUrl, tornadoServiceFee } = require('./config')
const { TxManager } = require('@tornado/tx-manager')
const { TornadoFeeOracleV5 } = require('@tornado/tornado-oracles')
let web3
let currentTx
let currentJob
let txManager
let tornadoProxyInstance
const feeOracle = new TornadoFeeOracleV5(netId, httpRpcUrl, gasPrices)
function start() {
try {
web3 = new Web3(httpRpcUrl)
tornadoProxyInstance = new web3.eth.Contract(proxyLightABI, proxyLight)
clearRelayerErrors(redis)
const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env
const gasPriceOracleConfig = {
chainId: netId,
defaultRpc: httpRpcUrl,
defaultFallbackGasPrices: gasPrices,
minPriority: 0.05,
percentile: 5,
blocksCount: 20,
}
txManager = new TxManager({
privateKey,
rpcUrl: httpRpcUrl,
config: { CONFIRMATIONS, MAX_GAS_PRICE, THROW_ON_REVERT: false },
gasPriceOracleConfig,
})
queue.process(processJob)
console.log('Worker started')
} catch (e) {
logRelayerError(redis, e)
console.error('error on start worker', e.message)
}
}
async function checkTornadoFee({ data }, tx) {
const userProvidedFee = toBN(data.args[4])
const { amount, decimals, currency } = getInstance(data.contract)
const relayerDesiredFee = await feeOracle.calculateWithdrawalFeeViaRelayer({
tx,
txType: 'relayer_withdrawal',
relayerFeePercent: tornadoServiceFee,
currency,
amount,
decimals,
gasLimit: tx.gasLimit,
gasPrice: tx.gasPrice,
})
console.log(
'user-provided fee, desired fee',
fromWei(userProvidedFee.toString()),
fromWei(toBN(relayerDesiredFee).toString()),
)
if (userProvidedFee.lt(toBN(relayerDesiredFee))) {
throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.')
}
}
async function getTxObject({ data }) {
const calldata = tornadoProxyInstance.methods.withdraw(data.contract, data.proof, ...data.args).encodeABI()
const incompleteTx = {
value: data.args[5],
to: tornadoProxyInstance._address,
data: calldata,
}
const { gasLimit, gasPrice } = await feeOracle.getGasParams({
tx: incompleteTx,
txType: 'relayer_withdrawal',
})
return { ...incompleteTx, gasLimit, gasPrice }
}
async function checkRecipient({ data }) {
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 processJob(job) {
try {
if (!jobType[job.data.type]) {
throw new Error(`Unknown job type: ${job.data.type}`)
}
currentJob = job
await updateStatus(status.ACCEPTED)
console.log(`Start processing a new ${job.data.type} job #${job.id}`)
await submitTx(job)
} catch (e) {
console.error('processJob', e.message)
await updateStatus(status.FAILED)
throw e
}
}
async function submitTx(job) {
await checkRecipient(job)
const tx = await getTxObject(job)
await checkTornadoFee(job, tx)
currentTx = await txManager.createTx(tx)
try {
const receipt = await currentTx
.send()
.on('transactionHash', txHash => {
updateTxHash(txHash)
updateStatus(status.SENT)
})
.on('mined', receipt => {
console.log('Mined in block', receipt.blockNumber)
updateStatus(status.MINED)
})
.on('confirmations', updateConfirmations)
if (receipt.status === 1) {
await updateStatus(status.CONFIRMED)
} else {
throw new Error('Submitted transaction failed')
}
} catch (e) {
// todo this could result in duplicated error logs
// todo handle a case where account tree is still not up to date (wait and retry)?
throw new Error(`Revert by smart contract ${e.message}`)
}
}
async function updateTxHash(txHash) {
console.log(`A new successfully sent tx ${txHash}`)
currentJob.data.txHash = txHash
await currentJob.update(currentJob.data)
}
async function updateConfirmations(confirmations) {
console.log(`Confirmations count ${confirmations}`)
currentJob.data.confirmations = confirmations
await currentJob.update(currentJob.data)
}
async function updateStatus(status) {
console.log(`Job status updated ${status}`)
currentJob.data.status = status
await currentJob.update(currentJob.data)
}
start()

@ -1,54 +0,0 @@
require('chai').should()
const { getTornadoWithdrawInputError } = require('../src/modules/validator')
describe('Validator', () => {
describe('#getTornadoWithdrawInputError', () => {
it('should work', () => {
getTornadoWithdrawInputError(withdrawData)
})
it('should throw for incorrect proof', () => {
const malformedData = { ...withdrawData }
malformedData.proof = '0xbeef'
getTornadoWithdrawInputError(malformedData).should.be.equal(
'.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
getTornadoWithdrawInputError(malformedData).should.be.equal(" should have required property 'proof'")
malformedData.proof = withdrawData.proof
delete malformedData.args
getTornadoWithdrawInputError(malformedData).should.be.equal(" should have required property 'args'")
malformedData.args = withdrawData.args
delete malformedData.contract
getTornadoWithdrawInputError(malformedData).should.be.equal(" should have required property 'contract'")
malformedData.contract = withdrawData.contract
})
})
})
const withdrawData = {
proof:
'0x0f8cb4c2ca9cbb23a5f21475773e19e39d3470436d7296f25c8730d19d88fcef2986ec694ad094f4c5fff79a4e5043bd553df20b23108bc023ec3670718143c20cc49c6d9798e1ae831fd32a878b96ff8897728f9b7963f0d5a4b5574426ac6203b2456d360b8e825d8f5731970bf1fc1b95b9713e3b24203667ecdd5939c2e40dec48f9e51d9cc8dc2f7f3916f0e9e31519c7df2bea8c51a195eb0f57beea4924cb846deaa78cdcbe361a6c310638af6f6157317bc27d74746bfaa2e1f8d2e9088fd10fa62100740874cdffdd6feb15c95c5a303f6bc226d5e51619c5b825471a17ddfeb05b250c0802261f7d05cf29a39a72c13e200e5bc721b0e4c50d55e6',
args: [
'0x1579d41e5290ab5bcec9a7df16705e49b5c0b869095299196c19c5e14462c9e3',
'0x0cf7f49c5b35c48b9e1d43713e0b46a75977e3d10521e9ac1e4c3cd5e3da1c5d',
'0xbd4369dc854c5d5b79fe25492e3a3cfcb5d02da5',
'0x0000000000000000000000000000000000000000',
'0x000000000000000000000000000000000000000000000000058d15e176280000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
],
contract: '0xd47438C816c9E7f2E2888E060936a499Af9582b3',
}

4717
yarn.lock

File diff suppressed because it is too large Load Diff