Compare commits

...

53 Commits

Author SHA1 Message Date
1705dd1ce2 Set new software version for anti-contract withdrawal fix 2023-10-17 06:29:29 -07:00
1e99a136cc 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:49:56 -07:00
03c71f28bb Upgrade dependecies to self-hosted versions & bump node to 16 2023-09-18 22:50:24 -07:00
d04480a62f Update tornado-oracles to fetch gas price and limit only once 2023-09-07 10:48:28 -07:00
5191207cc2 Update tornado-oracles lib to fix gas price issues 2023-08-30 08:16:35 -07:00
bb76455d6a Calculate fees and gas via @tornado/tornado-oracles lib & bump relayer version to 5.2.0 2023-08-27 09:47:11 -07:00
c33a7f9ef2 Expose redis port for local deployment 2023-07-30 03:03:40 -07:00
0a7e97e62e Change fee parameter naming for more clearance 2023-07-30 02:15:55 -07:00
fb2fbb89f3 Add multiple chains deployment with docker & bump version to stable 5.1.0 and change README 2023-07-13 22:10:44 -07:00
bc6778bda2 Bump node in docker-container & add all .env files, except example, to gitignore 2023-07-03 12:49:35 -07:00
64ccb5d4d0 Correctly estimate withdrawal gas fee 2023-07-02 17:59:48 -07:00
b2ae3d3399 Change default oracle RPC from censored and change minimum required relayer balance for sidechains: 10 MATIC/AVAX or 0.1 ETH/BNB 2023-07-02 17:58:44 -07:00
9b14e3ed89 Bump gas-price-oracle to correct gas estimation & add 'concurrently' and change 'start' command, because if run server without concurrently, worker and healthWatcher wont be started 2023-07-02 17:52:39 -07:00
4c1500bd96 Set default fee for sidechains as 0.1% - calculation formula here: https://docs.tornado.ws/general/guides/relayer.html 2023-07-02 17:44:53 -07:00
Danil Kovtonyuk
a4916d1cb4 fix: calculate optimism l1 fee 2022-05-31 00:13:07 +10:00
Danil Kovtonyuk
7350591ebd bump tx-manager 2022-05-25 19:25:58 +10:00
smart_ex
a453444318 log healthWatcher errors 2022-05-25 19:25:58 +10:00
smart_ex
bca85764bc fix logging error messages 2022-05-25 19:25:58 +10:00
smart_ex
38fd1e6a4c fix queue collision 2022-05-25 19:25:58 +10:00
smart_ex
ecdcb545fc fix isKnownContract validation 2022-05-25 19:25:58 +10:00
smart_ex
ed9f003d50 block iframe 2022-05-25 19:25:58 +10:00
smart_ex
526f1889f9 clear errors on start 2022-05-25 19:25:58 +10:00
smart_ex
e5dd4b465f log errors to redis 2022-05-25 19:25:58 +10:00
smart_ex
92bc3bb0a8 log errors to redis 2022-05-25 19:25:58 +10:00
smart_ex
7d8a6dc30f log errors to redis 2022-05-25 19:25:58 +10:00
Danil Kovtonyuk
5b2815e6a0 bump gas price oracle 2022-04-13 23:57:02 +10:00
Danil Kovtonyuk
220a71fe09
fix: workflow 2022-04-13 21:52:53 +10:00
Danil Kovtonyuk
692ab64d49
Update README.md 2022-04-13 21:11:45 +10:00
_den
6eb3d702a4 added link to instructions for install rpc nodes 2022-03-10 21:11:13 +10:00
Alexey Pertsev
06848352f1
update docker tag 2022-03-01 11:38:21 +01:00
Alexey Pertsev
87d3528346
Update Readme 2022-03-01 11:36:40 +01:00
_den
1d6c2575f9
added discord notification (#87)
* added discord notification

* updated message

* updated discord notification

* updated discord notification message

Co-authored-by: _den <_den@outlook.com>
2022-02-16 10:37:44 +01:00
Danil Kovtonyuk
96b6e722d8 fix: add optimism gas 2022-01-28 11:01:25 +01:00
Danil Kovtonyuk
4a28d4646e feat: add optimism 2022-01-13 00:30:13 +10:00
Danil Kovtonyuk
3d952f1906 feat: add Arbitrum 2021-11-29 16:06:04 +03:00
Danil Kovtonyuk
07b091ff46
bump gas price oracle 2021-11-16 03:52:47 +10:00
Danil Kovtonyuk
10b2fed839
fix: update gas price oracle 2021-11-16 01:59:41 +10:00
Danil Kovtonyuk
c0320285b9
fix: update gas price oracle 2021-11-15 17:58:18 +10:00
Danil Kovtonyuk
7ea0b9abd6 feat: add Avalanche 2021-09-17 19:18:58 +10:00
Danil Kovtonyuk
aa7d389766 fix: update gas price oracle 2021-08-26 01:57:00 +10:00
Danil Kovtonyuk
e538236904 fix: update contracts 2021-08-26 01:57:00 +10:00
Danil Kovtonyuk
787c9663fe feat: add xDai 2021-08-26 01:57:00 +10:00
Danil Kovtonyuk
71b638e640 fix: variables 2021-06-29 00:50:44 +03:00
Danil Kovtonyuk
3d7a9b5f93 fix: get gas prices 2021-06-29 00:50:44 +03:00
Danil Kovtonyuk
96d937ee0d fix: update contracts 2021-06-29 00:50:44 +03:00
Danil Kovtonyuk
9b49cab775 fix: tx gas price 2021-06-29 00:50:44 +03:00
Danil Kovtonyuk
9747c1a397 feat: add polygon 2021-06-29 00:50:44 +03:00
Danil Kovtonyuk
3cf642fe68 fix: match tag 2021-06-11 12:10:43 +03:00
Danil Kovtonyuk
5cfe527be8 fix: bump gas price oracle 2021-06-10 23:41:43 +03:00
Danil Kovtonyuk
e24af4239e fix: match tag 2021-06-10 21:18:00 +03:00
Danil Kovtonyuk
a875bc77fa update build 2021-06-10 20:06:48 +03:00
Danil Kovtonyuk
7286aeaff0 fix: update constants 2021-06-10 13:02:53 +03:00
Danil Kovtonyuk
803a104a93 light relayer 2021-06-03 17:23:30 +03:00
38 changed files with 1557 additions and 652020 deletions

@ -1,8 +1,5 @@
NET_ID=1 NET_ID=56
HTTP_RPC_URL=https://mainnet.infura.io HTTP_RPC_URL=https://bsc-dataseed1.ninicoin.io/
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
REDIS_URL=redis://127.0.0.1:6379 REDIS_URL=redis://127.0.0.1:6379
# DNS settings # DNS settings
@ -12,12 +9,10 @@ APP_PORT=8000
# without 0x prefix # without 0x prefix
PRIVATE_KEY= PRIVATE_KEY=
# 0.05 means 0.05% # 0.1 means 0.1%
REGULAR_TORNADO_WITHDRAW_FEE=0.05 RELAYER_FEE=0.1
MINING_SERVICE_FEE=0.05
REWARD_ACCOUNT= REWARD_ACCOUNT=
CONFIRMATIONS=4 CONFIRMATIONS=4
# in GWEI # in GWEI
MAX_GAS_PRICE=1000 MAX_GAS_PRICE=1000
AGGREGATOR=0x8cb1436F64a3c33aD17bb42F94e255c4c0E871b2

@ -3,7 +3,7 @@ name: build
on: on:
push: push:
branches: ['*'] branches: ['*']
tags: ['v[0-9]+.[0-9]+.[0-9]+'] tags: ['v[0-9]+.[0-9]+.[0-9]+*']
pull_request: pull_request:
jobs: jobs:
@ -14,15 +14,15 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- run: yarn install - run: yarn install
- run: yarn test - run: yarn test
- run: yarn lint - run: yarn lint
- name: Telegram Failure Notification - name: Telegram Failure Notification
uses: appleboy/telegram-action@0.0.7 uses: appleboy/telegram-action@master
if: failure() if: failure()
with: with:
message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }} message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) for sidechains because of ${{ github.actor }}
format: markdown format: markdown
to: ${{ secrets.TELEGRAM_CHAT_ID }} to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }} token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
@ -43,7 +43,7 @@ jobs:
- name: Check package.json version vs tag - name: Check package.json version vs tag
run: | run: |
[ ${{ steps.vars.outputs.version }} = $(grep '"version":' package.json | grep -o "[0-9.]*") ] || (echo "Git tag doesn't match version in package.json" && false) [ ${{ 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 - name: Build and push Docker image
uses: docker/build-push-action@v1.1.0 uses: docker/build-push-action@v1.1.0
@ -51,36 +51,48 @@ jobs:
dockerfile: Dockerfile dockerfile: Dockerfile
repository: tornadocash/relayer repository: tornadocash/relayer
tag_with_ref: true tag_with_ref: true
tags: mining,candidate tags: light
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }} password: ${{ secrets.DOCKER_TOKEN }}
- name: Telegram Message Notify - name: Telegram Message Notify
uses: appleboy/telegram-action@0.0.7 uses: appleboy/telegram-action@master
with: with:
to: ${{ secrets.TELEGRAM_CHAT_ID }} to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }} token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: 🚀 Published a [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}) version ${{ steps.vars.outputs.version }} to docker hub 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 debug: true
format: markdown format: markdown
- name: Telegram Relayer Channel Notification - name: Telegram Relayer Channel Notification
uses: appleboy/telegram-action@0.0.7 uses: appleboy/telegram-action@master
with: with:
to: ${{ secrets.TELEGRAM_RELAYER_CHAT_ID }} to: ${{ secrets.TELEGRAM_RELAYER_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }} token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: | message: |
🚀 Published a new version of the relayer node service to docker hub: `tornadocash/relayer:v${{ steps.vars.outputs.version }}` and `tornadocash/relayer:mining`. 🚀 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 nodes ❗️ Please update your sidechains nodes ❗️
DO NOT TOUCH MAINNET AND NOVA RELAYERS.
debug: true debug: true
format: markdown 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 - name: Telegram Failure Notification
uses: appleboy/telegram-action@0.0.7 uses: appleboy/telegram-action@master
if: failure() if: failure()
with: with:
message: ❗ Failed to publish [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }} message: ❗ Failed to publish [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}/actions) for sidechains because of ${{ github.actor }}
format: markdown format: markdown
to: ${{ secrets.TELEGRAM_CHAT_ID }} to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }} token: ${{ secrets.TELEGRAM_BOT_TOKEN }}

5
.gitignore vendored

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

1
.npmrc Normal file

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

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

148
README.md

@ -1,62 +1,116 @@
# Relayer for Tornado Cash [![Build Status](https://github.com/tornadocash/relayer/workflows/build/badge.svg)](https://github.com/tornadocash/relayer/actions) [![Docker Image Version (latest semver)](https://img.shields.io/docker/v/tornadocash/relayer?logo=docker&logoColor=%23FFFFFF&sort=semver)](https://hub.docker.com/repository/docker/tornadocash/relayer) # Relayer for Tornado Cash [![Build Status](https://github.com/tornadocash/relayer/workflows/build/badge.svg)](https://github.com/tornadocash/relayer/actions) ![Static Badge](https://img.shields.io/badge/version-5.1.0-blue?logo=docker)
## Getting listed on app.tornado.cash ***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.**
If you would like to be listed in tornado.cash UI relayer's dropdown option, please do the following: ***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. Setup tornado.cash relayer node(see below for docker-compose.yml example)
2. Setup ENS subdomain(`goerli-v2.xxx.eth`, `mainnet-v2.xxx.eth`) with TEXT record and URL key that points to your DNS or IP address.
3. Test your relayer setup on Goerli testnet at https://app.tornado.cash by choosing custom relayer's option on withdraw tab. Enter your ens name and initiate a withdrawal.
4. Open new Github issue in https://github.com/tornadocash/tornado-relayer/issues and specify the following:
- your goerli ens url
- your mainnet ens url
- your telegram handle
- withdrawal tx on goerli
- withdrawal tx on mainnet
Please choose your testnet relayer's fee wisely.
Disclaimer: Please consult with legal and tax advisors regarding the compliance of running a relayer service in your jurisdiction. The authors of this project bear no responsibility.
USE AT YOUR OWN RISK.
## Deploy with docker-compose ## Deploy with docker-compose
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. __PREREQUISITES__
1. Download [docker-compose.yml](/docker-compose.yml) and [.env.example](/.env.example) 1. Update core dependencies
``` - `sudo apt-get update`
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
```
2. Setup environment variables 2. Install docker-compose
- set `NET_ID` (1 for mainnet, 5 for Goerli) - `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`
- 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
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. 3. Install Docker
3. Run `docker-compose up -d` - `curl -fsSL https://get.docker.com -o get-docker.sh && chmod +x get-docker.sh && ./get-docker.sh`
## Run locally 4. Install git
1. `npm i` - `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` 2. `cp .env.example .env`
3. Modify `.env` as needed 3. Modify `.env` as needed (described above)
4. `npm run start` 4. `yarn start`
5. Go to `http://127.0.0.1:8000` 5. Go to `http://127.0.0.1:8000`
6. In order to execute withdraw request, you can run following command 6. In order to execute withdraw request, you can run following command
@ -69,6 +123,10 @@ 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 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 tornado.cash UI from submitting your request over http connection
## Run own RPC nodes
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).
## Architecture ## 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 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

@ -1,319 +0,0 @@
[
{
"inputs": [
{
"internalType": "bytes32[]",
"name": "domains",
"type": "bytes32[]"
}
],
"name": "bulkResolve",
"outputs": [
{
"internalType": "address[]",
"name": "result",
"type": "address[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract Governance",
"name": "governance",
"type": "address"
}
],
"name": "getAllProposals",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "proposer",
"type": "address"
},
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "uint256",
"name": "startTime",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "endTime",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "forVotes",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "againstVotes",
"type": "uint256"
},
{
"internalType": "bool",
"name": "executed",
"type": "bool"
},
{
"internalType": "bool",
"name": "extended",
"type": "bool"
},
{
"internalType": "enum Governance.ProposalState",
"name": "state",
"type": "uint8"
}
],
"internalType": "struct GovernanceAggregator.Proposal[]",
"name": "proposals",
"type": "tuple[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract Governance",
"name": "governance",
"type": "address"
},
{
"internalType": "address[]",
"name": "accs",
"type": "address[]"
}
],
"name": "getGovernanceBalances",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "fromTokens",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "oneUnitAmounts",
"type": "uint256[]"
}
],
"name": "getPricesInETH",
"outputs": [
{
"internalType": "uint256[]",
"name": "prices",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract Governance",
"name": "governance",
"type": "address"
},
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "getUserData",
"outputs": [
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "latestProposalId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "latestProposalIdState",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "timelock",
"type": "uint256"
},
{
"internalType": "address",
"name": "delegatee",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract Miner",
"name": "miner",
"type": "address"
},
{
"internalType": "address[]",
"name": "instances",
"type": "address[]"
}
],
"name": "minerRates",
"outputs": [
{
"internalType": "uint256[]",
"name": "_rates",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "resolve",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract RewardSwap",
"name": "swap",
"type": "address"
}
],
"name": "swapState",
"outputs": [
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "poolWeight",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract Miner",
"name": "miner",
"type": "address"
},
{
"internalType": "address[]",
"name": "instances",
"type": "address[]"
},
{
"internalType": "contract RewardSwap",
"name": "swap",
"type": "address"
}
],
"name": "miningData",
"outputs": [
{
"internalType": "uint256[]",
"name": "_rates",
"type": "uint256[]"
},
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "poolWeight",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "fromTokens",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "oneUnitAmounts",
"type": "uint256[]"
},
{
"internalType": "contract RewardSwap",
"name": "swap",
"type": "address"
}
],
"name": "marketData",
"outputs": [
{
"internalType": "uint256[]",
"name": "prices",
"type": "uint256[]"
},
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]

@ -1,12 +0,0 @@
[
{
"inputs": [
{ "internalType": "contract IERC20", "name": "srcToken", "type": "address" },
{ "internalType": "contract IERC20", "name": "dstToken", "type": "address" }
],
"name": "getRate",
"outputs": [{ "internalType": "uint256", "name": "weightedRate", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
}
]

File diff suppressed because it is too large Load Diff

@ -0,0 +1,151 @@
[
{
"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"
}
]

105
abis/proxyLightABI.json Normal file

@ -0,0 +1,105 @@
[
{
"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,252 +0,0 @@
[
{
"inputs": [
{
"internalType": "bytes32",
"name": "_torn",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "_miner",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "_miningCap",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_initialLiquidity",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "newWeight",
"type": "uint256"
}
],
"name": "PoolWeightUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "pTORN",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "TORN",
"type": "uint256"
}
],
"name": "Swap",
"type": "event"
},
{
"inputs": [],
"name": "DURATION",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "initialLiquidity",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "liquidity",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "miner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolWeight",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "resolve",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "startTimestamp",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokensSold",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "torn",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "swap",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "getExpectedReturn",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tornVirtualBalance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newWeight",
"type": "uint256"
}
],
"name": "setPoolWeight",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]

@ -1,498 +0,0 @@
[
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "_newOperator",
"type": "address"
}
],
"name": "changeOperator",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "nullifierHashes",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"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": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "verifier",
"outputs": [
{
"internalType": "contract IVerifier",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "_left",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "_right",
"type": "bytes32"
}
],
"name": "hashLeftRight",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "FIELD_SIZE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "levels",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "operator",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "_root",
"type": "bytes32"
}
],
"name": "isKnownRoot",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "commitments",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "denomination",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "currentRootIndex",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "_newVerifier",
"type": "address"
}
],
"name": "updateVerifier",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "_commitment",
"type": "bytes32"
}
],
"name": "deposit",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getLastRoot",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "roots",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "ROOT_HISTORY_SIZE",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "_nullifierHash",
"type": "bytes32"
}
],
"name": "isSpent",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "zeros",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "ZERO_VALUE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "filledSubtrees",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "nextIndex",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IVerifier",
"name": "_verifier",
"type": "address"
},
{
"internalType": "uint256",
"name": "_denomination",
"type": "uint256"
},
{
"internalType": "uint32",
"name": "_merkleTreeHeight",
"type": "uint32"
},
{
"internalType": "address",
"name": "_operator",
"type": "address"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "commitment",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint32",
"name": "leafIndex",
"type": "uint32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "timestamp",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "nullifierHash",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "relayer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"name": "Withdrawal",
"type": "event"
}
]

@ -1,171 +0,0 @@
[
{
"inputs": [
{
"internalType": "bytes32",
"name": "_tornadoTrees",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "_governance",
"type": "bytes32"
},
{
"internalType": "contract ITornado[]",
"name": "_instances",
"type": "address[]"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "governance",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract ITornado",
"name": "",
"type": "address"
}
],
"name": "instances",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "resolve",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tornadoTrees",
"outputs": [
{
"internalType": "contract ITornadoTrees",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract ITornado",
"name": "tornado",
"type": "address"
},
{
"internalType": "bytes32",
"name": "commitment",
"type": "bytes32"
}
],
"name": "deposit",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract ITornado",
"name": "instance",
"type": "address"
},
{
"internalType": "bool",
"name": "update",
"type": "bool"
}
],
"name": "updateInstances",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract ITornado",
"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,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:

@ -1,115 +1,14 @@
version: '2' version: '2'
services: 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: 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
@ -146,6 +45,234 @@ services:
- nginx - nginx
- dockergen - dockergen
# ---------------------- BSC (Binance Smart Chain) ----------------------- #
bsc-server:
image: tornadocash/relayer:sidechain-v5
profiles: ['bsc']
restart: always
command: server
env_file: .env.bsc
environment:
NET_ID: 56
REDIS_URL: redis://redis/1
nginx_proxy_read_timeout: 600
depends_on: [redis]
bsc-healthWatcher:
image: tornadocash/relayer:sidechain-v5
profiles: ['bsc']
restart: always
command: healthWatcher
env_file: .env.bsc
environment:
NET_ID: 56
REDIS_URL: redis://redis/1
depends_on: [redis, bsc-server]
bsc-worker1:
image: tornadocash/relayer:sidechain-v5
profiles: ['bsc']
restart: always
command: worker
env_file: .env.bsc
environment:
NET_ID: 56
REDIS_URL: redis://redis/1
depends_on: [redis, bsc-server]
# -------------------------------------------------- #
# ---------------------- Polygon (MATIC) --------------------- #
polygon-server:
image: tornadocash/relayer:sidechain-v5
profiles: ['polygon']
restart: always
command: server
env_file: .env.polygon
environment:
NET_ID: 137
REDIS_URL: redis://redis/2
nginx_proxy_read_timeout: 600
depends_on: [redis]
polygon-healthWatcher:
image: tornadocash/relayer:sidechain-v5
profiles: ['polygon']
restart: always
command: healthWatcher
env_file: .env.polygon
environment:
NET_ID: 137
REDIS_URL: redis://redis/2
depends_on: [redis, polygon-server]
polygon-worker1:
image: tornadocash/relayer:sidechain-v5
profiles: ['polygon']
restart: always
command: worker
env_file: .env.polygon
environment:
NET_ID: 137
REDIS_URL: redis://redis/2
depends_on: [redis, polygon-server]
# -------------------------------------------------- #
# ---------------------- Gnosis (XDAI) ---------------------- #
gnosis-server:
image: tornadocash/relayer:sidechain-v5
profiles: ['gnosis']
restart: always
command: server
env_file: .env.gnosis
environment:
NET_ID: 100
REDIS_URL: redis://redis/3
nginx_proxy_read_timeout: 600
depends_on: [redis]
gnosis-healthWatcher:
image: tornadocash/relayer:sidechain-v5
profiles: ['gnosis']
restart: always
command: healthWatcher
env_file: .env.gnosis
environment:
NET_ID: 100
REDIS_URL: redis://redis/3
depends_on: [redis, gnosis-server]
gnosis-worker1:
image: tornadocash/relayer:sidechain-v5
profiles: ['gnosis']
restart: always
command: worker
env_file: .env.gnosis
environment:
NET_ID: 100
REDIS_URL: redis://redis/3
depends_on: [redis, gnosis-server]
# -------------------------------------------------- #
# ---------------------- AVAX ---------------------- #
avax-server:
image: tornadocash/relayer:sidechain-v5
profiles: ['avax']
restart: always
command: server
env_file: .env.avax
environment:
NET_ID: 43114
REDIS_URL: redis://redis/4
nginx_proxy_read_timeout: 600
depends_on: [redis]
avax-healthWatcher:
image: tornadocash/relayer:sidechain-v5
profiles: ['avax']
restart: always
command: healthWatcher
env_file: .env.avax
environment:
NET_ID: 43114
REDIS_URL: redis://redis/4
depends_on: [redis, avax-server]
avax-worker1:
image: tornadocash/relayer:sidechain-v5
profiles: ['avax']
restart: always
command: worker
env_file: .env.avax
environment:
NET_ID: 43114
REDIS_URL: redis://redis/4
depends_on: [redis, avax-server]
# -------------------------------------------------- #
# ---------------------- OP ------------------------ #
op-server:
image: tornadocash/relayer:sidechain-v5
profiles: ['op']
restart: always
command: server
env_file: .env.op
environment:
NET_ID: 10
REDIS_URL: redis://redis/5
nginx_proxy_read_timeout: 600
depends_on: [redis]
op-healthWatcher:
image: tornadocash/relayer:sidechain-v5
profiles: ['op']
restart: always
command: healthWatcher
env_file: .env.op
environment:
NET_ID: 10
REDIS_URL: redis://redis/5
depends_on: [redis, op-server]
op-worker1:
image: tornadocash/relayer:sidechain-v5
profiles: ['op']
restart: always
command: worker
env_file: .env.op
environment:
NET_ID: 10
REDIS_URL: redis://redis/5
depends_on: [redis, op-server]
# -------------------------------------------------- #
# ---------------------- Arbitrum ----------------------- #
arb-server:
image: tornadocash/relayer:sidechain-v5
profiles: ['arb']
restart: always
command: server
env_file: .env.arb
environment:
NET_ID: 42161
REDIS_URL: redis://redis/6
nginx_proxy_read_timeout: 600
depends_on: [redis]
arb-healthWatcher:
image: tornadocash/relayer:sidechain-v5
profiles: ['arb']
restart: always
command: healthWatcher
env_file: .env.arb
environment:
NET_ID: 42161
REDIS_URL: redis://redis/6
depends_on: [redis, arb-server]
arb-worker1:
image: tornadocash/relayer:sidechain-v5
profiles: ['arb']
restart: always
command: worker
env_file: .env.arb
environment:
NET_ID: 42161
REDIS_URL: redis://redis/6
depends_on: [redis, arb-server]
# -------------------------------------------------- #
volumes: volumes:
conf: conf:
vhost: vhost:

File diff suppressed because one or more lines are too long

Binary file not shown.

@ -1,40 +1,33 @@
{ {
"name": "relay", "name": "relay",
"version": "4.0.15", "version": "5.2.2",
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash", "description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
"scripts": { "scripts": {
"server": "node src/server.js", "server": "node src/server.js",
"worker": "node src/worker", "worker": "node src/worker",
"treeWatcher": "node src/treeWatcher",
"priceWatcher": "node src/priceWatcher",
"healthWatcher": "node src/healthWatcher", "healthWatcher": "node src/healthWatcher",
"eslint": "eslint --ext .js --ignore-path .gitignore .", "eslint": "eslint --ext .js --ignore-path .gitignore .",
"prettier:check": "npx prettier --check . --config .prettierrc", "prettier:check": "npx prettier --check . --config .prettierrc",
"prettier:fix": "npx prettier --write . --config .prettierrc", "prettier:fix": "npx prettier --write . --config .prettierrc",
"lint": "yarn eslint && yarn prettier:check", "lint": "yarn eslint && yarn prettier:check",
"test": "mocha", "test": "mocha",
"start": "yarn server & yarn priceWatcher & yarn treeWatcher & yarn worker & yarn healthWatcher" "build": "docker build -t tornadocash/relayer:sidechain-v5 .",
"start": "docker-compose up -d redis && concurrently \"yarn server\" \"yarn worker\" \"yarn healthWatcher\""
}, },
"author": "tornado.cash", "author": "tornado.cash",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tornado/tornado-config": "^1.0.8",
"@tornado/tornado-oracles": "^3.3.0",
"@tornado/tx-manager": "^0.4.9",
"ajv": "^6.12.5", "ajv": "^6.12.5",
"async-mutex": "^0.2.4",
"bull": "^3.12.1", "bull": "^3.12.1",
"circomlib": "git+https://github.com/tornadocash/circomlib.git#3b492f9801573eebcfe1b6c584afe8a3beecf2b4", "concurrently": "^8.2.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"eth-ens-namehash": "^2.0.8",
"express": "^4.17.1", "express": "^4.17.1",
"fixed-merkle-tree": "^0.4.0",
"gas-price-oracle": "^0.2.2",
"ioredis": "^4.14.1", "ioredis": "^4.14.1",
"node-fetch": "^2.6.0",
"torn-token": "1.0.4",
"tornado-anonymity-mining": "^2.1.2",
"tx-manager": "^0.2.9",
"uuid": "^8.3.0", "uuid": "^8.3.0",
"web3": "^1.3.0", "web3": "^1.3.0",
"web3-core-promievent": "^1.3.0",
"web3-utils": "^1.2.2" "web3-utils": "^1.2.2"
}, },
"devDependencies": { "devDependencies": {

@ -1,29 +1,27 @@
require('dotenv').config() require('dotenv').config()
const { jobType } = require('./constants') const tornConfig = require('@tornado/tornado-config')
const tornConfig = require('torn-token')
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 = { module.exports = {
netId: Number(process.env.NET_ID) || 1, netId,
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379', redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
httpRpcUrl: process.env.HTTP_RPC_URL, httpRpcUrl: process.env.HTTP_RPC_URL,
wsRpcUrl: process.env.WS_RPC_URL, oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://rpc.payload.de',
oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://mainnet.infura.io/',
offchainOracleAddress: '0x080AB73787A8B13EC7F40bd7d00d6CC07F9b24d0',
aggregatorAddress: process.env.AGGREGATOR,
minerMerkleTreeHeight: 20, minerMerkleTreeHeight: 20,
privateKey: process.env.PRIVATE_KEY, privateKey: process.env.PRIVATE_KEY,
instances: tornConfig.instances, instances,
torn: tornConfig,
port: process.env.APP_PORT || 8000, port: process.env.APP_PORT || 8000,
tornadoServiceFee: Number(process.env.REGULAR_TORNADO_WITHDRAW_FEE), tornadoServiceFee: Number(process.env.RELAYER_FEE),
miningServiceFee: Number(process.env.MINING_SERVICE_FEE),
rewardAccount: process.env.REWARD_ACCOUNT, rewardAccount: process.env.REWARD_ACCOUNT,
tornadoGoerliProxy: '0x454d870a72e29d5E5697f635128D18077BD04C60', gasPrices,
gasLimits: { proxyLight,
[jobType.TORNADO_WITHDRAW]: 390000, nativeCurrency,
WITHDRAW_WITH_EXTRA: 480000, minimumBalance: netId === 137 || netId === 43114 ? '10000000000000000000' : '100000000000000000', // 10 or 0.1
[jobType.MINING_REWARD]: 455000,
[jobType.MINING_WITHDRAW]: 400000,
},
minimumBalance: '1000000000000000000',
} }

@ -1,7 +1,7 @@
const jobType = Object.freeze({ const jobType = Object.freeze({
TORNADO_WITHDRAW: 'TORNADO_WITHDRAW', TORNADO_WITHDRAW: 'TORNADO_WITHDRAW',
MINING_REWARD: 'MINING_REWARD', OP_TORNADO_WITHDRAW: 'OP_TORNADO_WITHDRAW',
MINING_WITHDRAW: 'MINING_WITHDRAW', ARB_TORNADO_WITHDRAW: 'ARB_TORNADO_WITHDRAW',
}) })
const status = Object.freeze({ const status = Object.freeze({
@ -14,7 +14,65 @@ const status = Object.freeze({
FAILED: 'FAILED', 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 = { module.exports = {
jobType, jobType,
status, status,
networkConfig,
} }

@ -1,8 +1,4 @@
const { const { getTornadoWithdrawInputError } = require('./modules/validator')
getTornadoWithdrawInputError,
getMiningRewardInputError,
getMiningWithdrawInputError,
} = require('./validator')
const { postJob } = require('./queue') const { postJob } = require('./queue')
const { jobType } = require('./constants') const { jobType } = require('./constants')
@ -20,36 +16,6 @@ async function tornadoWithdraw(req, res) {
return res.json({ id }) return res.json({ id })
} }
async function miningReward(req, res) {
const inputError = getMiningRewardInputError(req.body)
if (inputError) {
console.log('Invalid input:', inputError)
return res.status(400).json({ error: inputError })
}
const id = await postJob({
type: jobType.MINING_REWARD,
request: req.body,
})
return res.json({ id })
}
async function miningWithdraw(req, res) {
const inputError = getMiningWithdrawInputError(req.body)
if (inputError) {
console.log('Invalid input:', inputError)
return res.status(400).json({ error: inputError })
}
const id = await postJob({
type: jobType.MINING_WITHDRAW,
request: req.body,
})
return res.json({ id })
}
module.exports = { module.exports = {
tornadoWithdraw, tornadoWithdraw,
miningReward,
miningWithdraw,
} }

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

9
src/modules/redis.js Normal file

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

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

@ -1,44 +0,0 @@
const Redis = require('ioredis')
const { redisUrl, offchainOracleAddress, oracleRpcUrl } = require('./config')
const { getArgsForOracle, setSafeInterval } = require('./utils')
const redis = new Redis(redisUrl)
const Web3 = require('web3')
const web3 = new Web3(
new Web3.providers.HttpProvider(oracleRpcUrl, {
timeout: 200000, // ms
}),
)
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')
async function main() {
try {
const ethPrices = {}
for (let i = 0; i < tokenAddresses.length; i++) {
try {
const price = await offchainOracle.methods
.getRate(tokenAddresses[i], '0x0000000000000000000000000000000000000000')
.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])
}
}
await redis.hmset('prices', ethPrices)
console.log('Wrote following prices to redis', ethPrices)
} catch (e) {
console.error('priceWatcher error', e)
}
}
setSafeInterval(main, 30 * 1000)

@ -1,11 +1,11 @@
const { v4: uuid } = require('uuid') const { v4: uuid } = require('uuid')
const Queue = require('bull') 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. lockDuration: 300000, // Key expiration time for job locks.
lockRenewTime: 30000, // Interval on which to acquire the job lock lockRenewTime: 30000, // Interval on which to acquire the job lock
stalledInterval: 30000, // How often check for stalled jobs (use 0 for never checking). stalledInterval: 30000, // How often check for stalled jobs (use 0 for never checking).

@ -1,30 +0,0 @@
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 ens = require('eth-ens-namehash')
class ENSResolver {
constructor() {
this.addresses = {}
}
async resolve(domains) {
if (!Array.isArray(domains)) {
domains = [domains]
}
const unresolved = domains.filter(d => !this.addresses[d])
if (unresolved.length) {
const resolved = await aggregator.methods.bulkResolve(unresolved.map(ens.hash)).call()
for (let i = 0; i < resolved.length; i++) {
this.addresses[domains[i]] = resolved[i]
}
}
const addresses = domains.map(domain => this.addresses[domain])
return addresses.length === 1 ? addresses[0] : addresses
}
}
module.exports = ENSResolver

29
src/router.js Normal file

@ -0,0 +1,29 @@
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,41 +1,16 @@
const express = require('express') const express = require('express')
const status = require('./status') const router = require('./router')
const controller = require('./controller')
const { port, rewardAccount } = require('./config') const { port, rewardAccount } = require('./config')
const { version } = require('../package.json') const { version } = require('../package.json')
const { isAddress } = require('web3-utils') 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)
if (!isAddress(rewardAccount)) { if (!isAddress(rewardAccount)) {
throw new Error('No REWARD_ACCOUNT specified') throw new Error('No REWARD_ACCOUNT specified')
} }
const app = express()
app.use(express.json())
app.use(router)
app.listen(port) app.listen(port)
console.log(`Relayer ${version} started on port ${port}`) console.log(`Relayer ${version} started on port ${port}`)

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

@ -1,125 +0,0 @@
const MerkleTree = require('fixed-merkle-tree')
const { redisUrl, wsRpcUrl, minerMerkleTreeHeight, torn } = 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 MinerABI = require('../abis/mining.abi.json')
let contract
// eslint-disable-next-line no-unused-vars
let tree, eventSubscription, blockSubscription
// todo handle the situation when we have two rewards in one block
async function fetchEvents(from = 0, to = 'latest') {
try {
const events = await contract.getPastEvents('NewAccount', {
fromBlock: from,
toBlock: to,
})
return events
.sort((a, b) => a.returnValues.index - b.returnValues.index)
.map(e => toBN(e.returnValues.commitment))
} catch (e) {
console.error('error fetching events', e)
}
}
async function processNewEvent(err, event) {
if (err) {
throw new Error(`Event handler error: ${err}`)
// console.error(err)
// return
}
console.log(
`New account event
Index: ${event.returnValues.index}
Commitment: ${event.returnValues.commitment}
Nullifier: ${event.returnValues.nullifier}
EncAcc: ${event.returnValues.encryptedAccount}`,
)
const { commitment, index } = event.returnValues
if (tree.elements().length === Number(index)) {
tree.insert(toBN(commitment))
await updateRedis()
} else if (tree.elements().length === Number(index) + 1) {
console.log('Replacing element', index)
tree.update(index, toBN(commitment))
await updateRedis()
} else {
console.log(`Invalid element index ${index}, rebuilding tree`)
rebuild()
}
}
async function processNewBlock(err) {
if (err) {
throw new Error(`Event handler error: ${err}`)
// console.error(err)
// return
}
// what if updateRedis takes more than 15 sec?
await updateRedis()
}
async function updateRedis() {
const rootOnContract = await contract.methods.getLastAccountRoot().call()
if (!tree.root().eq(toBN(rootOnContract))) {
console.log(`Invalid tree root: ${tree.root()} != ${toBN(rootOnContract)}, rebuilding tree`)
rebuild()
return
}
const rootInRedis = await redis.get('tree:root')
if (!rootInRedis || !tree.root().eq(toBN(rootInRedis))) {
const serializedTree = JSON.stringify(tree.serialize())
await redis.set('tree:elements', serializedTree)
await redis.set('tree:root', tree.root().toString())
await redis.publish('treeUpdate', tree.root().toString())
console.log('Updated tree in redis, new root:', tree.root().toString())
} else {
console.log('Tree in redis is up to date, skipping update')
}
}
function rebuild() {
process.exit(1)
// await eventSubscription.unsubscribe()
// await blockSubscription.unsubscribe()
// setTimeout(init, 3000)
}
async function init() {
try {
console.log('Initializing')
const miner = await resolver.resolve(torn.miningV2.address)
contract = new web3.eth.Contract(MinerABI, miner)
const block = await web3.eth.getBlockNumber()
const events = await fetchEvents(0, block)
tree = new MerkleTree(minerMerkleTreeHeight, events, { hashFunction: poseidonHash2 })
await updateRedis()
console.log(`Rebuilt tree with ${events.length} elements, root: ${tree.root()}`)
eventSubscription = contract.events.NewAccount({ fromBlock: block + 1 }, processNewEvent)
blockSubscription = web3.eth.subscribe('newBlockHeaders', processNewBlock)
} catch (e) {
console.error('error on init treeWatcher', e.message)
}
}
init()
process.on('unhandledRejection', error => {
console.error('Unhandled promise rejection', error)
process.exit(1)
})

@ -1,33 +1,28 @@
const { instances, netId } = require('./config') const { instances } = require('./config')
const { poseidon } = require('circomlib') const { toChecksumAddress, BN } = require('web3-utils')
const { toBN, toChecksumAddress, BN } = require('web3-utils')
const TOKENS = { const addressMap = new Map()
torn: { for (const [currency, { instanceAddress, symbol, decimals }] of Object.entries(instances)) {
tokenAddress: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C', Object.entries(instanceAddress).forEach(([amount, address]) =>
symbol: 'TORN', addressMap.set(address, {
decimals: 18, currency,
}, amount,
symbol,
decimals,
}),
)
} }
const sleep = ms => new Promise(res => setTimeout(res, ms))
function getInstance(address) { function getInstance(address) {
address = toChecksumAddress(address) address = toChecksumAddress(address)
const inst = instances[`netId${netId}`] const key = toChecksumAddress(address)
for (const currency of Object.keys(inst)) { if (addressMap.has(key)) {
for (const amount of Object.keys(inst[currency].instanceAddress)) { return addressMap.get(key)
if (inst[currency].instanceAddress[amount] === address) { } else {
return { currency, amount } throw new Error('Unknown contact address')
} }
}
}
return null
} }
const poseidonHash = items => toBN(poseidon(items).toString())
const poseidonHash2 = (a, b) => poseidonHash([a, b])
function setSafeInterval(func, interval) { function setSafeInterval(func, interval) {
func() func()
.catch(console.error) .catch(console.error)
@ -36,39 +31,6 @@ function setSafeInterval(func, interval) {
}) })
} }
/**
* A promise that resolves when the source emits specified event
*/
function when(source, event) {
return new Promise((resolve, reject) => {
source
.once(event, payload => {
resolve(payload)
})
.on('error', error => {
reject(error)
})
})
}
function getArgsForOracle() {
const tokens = {
...instances.netId1,
...TOKENS,
}
const tokenAddresses = []
const oneUintAmount = []
const currencyLookup = {}
Object.entries(tokens).map(([currency, data]) => {
if (currency !== 'eth') {
tokenAddresses.push(data.tokenAddress)
oneUintAmount.push(toBN('10').pow(toBN(data.decimals.toString())).toString())
currencyLookup[data.tokenAddress] = currency
}
})
return { tokenAddresses, oneUintAmount, currencyLookup }
}
function fromDecimals(value, decimals) { function fromDecimals(value, decimals) {
value = value.toString() value = value.toString()
let ether = value.toString() let ether = value.toString()
@ -118,12 +80,28 @@ function fromDecimals(value, decimals) {
return new BN(wei.toString(10), 10) 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 = { module.exports = {
getInstance, getInstance,
setSafeInterval, setSafeInterval,
poseidonHash2,
sleep,
when,
getArgsForOracle,
fromDecimals, fromDecimals,
logRelayerError,
readRelayerErrors,
clearRelayerErrors,
} }

@ -1,241 +1,98 @@
const fs = require('fs')
const Web3 = require('web3') const Web3 = require('web3')
const { toBN, toWei, fromWei, toChecksumAddress } = require('web3-utils') const { toBN, fromWei, isAddress, toChecksumAddress } = require('web3-utils')
const MerkleTree = require('fixed-merkle-tree') const { redis } = require('./modules/redis')
const Redis = require('ioredis') const proxyLightABI = require('../abis/proxyLightABI.json')
const { GasPriceOracle } = require('gas-price-oracle')
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 { queue } = require('./queue') const { queue } = require('./queue')
const { poseidonHash2, getInstance, fromDecimals, sleep } = require('./utils') const { getInstance, logRelayerError, clearRelayerErrors } = require('./utils')
const { jobType, status } = require('./constants') const { jobType, status } = require('./constants')
const { const { netId, gasPrices, privateKey, proxyLight, httpRpcUrl, tornadoServiceFee } = require('./config')
torn, const { TxManager } = require('@tornado/tx-manager')
netId, const { TornadoFeeOracleV5 } = require('@tornado/tornado-oracles')
redisUrl,
gasLimits,
instances,
privateKey,
httpRpcUrl,
oracleRpcUrl,
miningServiceFee,
tornadoServiceFee,
tornadoGoerliProxy,
} = require('./config')
const ENSResolver = require('./resolver')
const resolver = new ENSResolver()
const { TxManager } = require('tx-manager')
let web3 let web3
let currentTx let currentTx
let currentJob let currentJob
let tree
let txManager let txManager
let controller let tornadoProxyInstance
let swap const feeOracle = new TornadoFeeOracleV5(netId, httpRpcUrl, gasPrices)
let minerContract
const redis = new Redis(redisUrl)
const redisSubscribe = new Redis(redisUrl)
const gasPriceOracle = new GasPriceOracle({ defaultRpc: oracleRpcUrl })
async function fetchTree() { function start() {
const elements = await redis.get('tree:elements')
const convert = (_, val) => (typeof val === 'string' ? toBN(val) : val)
tree = MerkleTree.deserialize(JSON.parse(elements, convert), poseidonHash2)
if (currentTx && currentJob && ['MINING_REWARD', 'MINING_WITHDRAW'].includes(currentJob.data.type)) {
const { proof, args } = currentJob.data
if (toBN(args.account.inputRoot).eq(toBN(tree.root()))) {
console.log('Account root is up to date. Skipping Root Update operation...')
return
} else {
console.log('Account root is outdated. Starting Root Update operation...')
}
const update = await controller.treeUpdate(args.account.outputCommitment, tree)
const minerAddress = await resolver.resolve(torn.miningV2.address)
const instance = new web3.eth.Contract(miningABI, minerAddress)
const data =
currentJob.data.type === 'MINING_REWARD'
? instance.methods.reward(proof, args, update.proof, update.args).encodeABI()
: instance.methods.withdraw(proof, args, update.proof, update.args).encodeABI()
await currentTx.replace({
to: minerAddress,
data,
})
console.log('replaced pending tx')
}
}
async function start() {
try { try {
web3 = new Web3(httpRpcUrl) web3 = new Web3(httpRpcUrl)
tornadoProxyInstance = new web3.eth.Contract(proxyLightABI, proxyLight)
clearRelayerErrors(redis)
const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env 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({ txManager = new TxManager({
privateKey, privateKey,
rpcUrl: httpRpcUrl, rpcUrl: httpRpcUrl,
config: { CONFIRMATIONS, MAX_GAS_PRICE, THROW_ON_REVERT: false }, config: { CONFIRMATIONS, MAX_GAS_PRICE, THROW_ON_REVERT: false },
gasPriceOracleConfig,
}) })
swap = new web3.eth.Contract(swapABI, await resolver.resolve(torn.rewardSwap.address))
minerContract = new web3.eth.Contract(miningABI, await resolver.resolve(torn.miningV2.address))
redisSubscribe.subscribe('treeUpdate', fetchTree)
await fetchTree()
const provingKeys = {
treeUpdateCircuit: require('../keys/TreeUpdate.json'),
treeUpdateProvingKey: fs.readFileSync('./keys/TreeUpdate_proving_key.bin').buffer,
}
controller = new Controller({ provingKeys })
await controller.init()
queue.process(processJob) queue.process(processJob)
console.log('Worker started') console.log('Worker started')
} catch (e) { } catch (e) {
logRelayerError(redis, e)
console.error('error on start worker', e.message) console.error('error on start worker', e.message)
} }
} }
function checkFee({ data }) { async function checkTornadoFee({ data }, tx) {
if (data.type === jobType.TORNADO_WITHDRAW) { const userProvidedFee = toBN(data.args[4])
return checkTornadoFee(data) const { amount, decimals, currency } = getInstance(data.contract)
}
return checkMiningFee(data)
}
async function checkTornadoFee({ args, contract }) { const relayerDesiredFee = await feeOracle.calculateWithdrawalFeeViaRelayer({
const { currency, amount } = getInstance(contract) tx,
const { decimals } = instances[`netId${netId}`][currency] txType: 'relayer_withdrawal',
const [fee, refund] = [args[4], args[5]].map(toBN) relayerFeePercent: tornadoServiceFee,
const { fast } = await gasPriceOracle.gasPrices() currency,
amount,
decimals,
gasLimit: tx.gasLimit,
gasPrice: tx.gasPrice,
})
const ethPrice = await redis.hget('prices', currency)
const expense = toBN(toWei(fast.toString(), 'gwei')).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( console.log(
'sent fee, desired fee, feePercent', 'user-provided fee, desired fee',
fromWei(fee.toString()), fromWei(userProvidedFee.toString()),
fromWei(desiredFee.toString()), fromWei(toBN(relayerDesiredFee).toString()),
fromWei(feePercent.toString()),
) )
if (fee.lt(desiredFee)) { if (userProvidedFee.lt(toBN(relayerDesiredFee))) {
throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.') throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.')
} }
} }
async function checkMiningFee({ args }) {
const { fast } = await gasPriceOracle.gasPrices()
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 = toBN(toWei(fast.toString(), 'gwei')).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()
const poolWeight = await swap.methods.poolWeight().call()
const expenseInPoints = Utils.reverseTornadoFormula({ balance, tokens: expenseInTorn, poolWeight })
/* eslint-disable */
const serviceFeePercent = isMiningReward
? toBN(0)
: toBN(args.amount)
.sub(providedFee) // args.amount includes fee
.mul(toBN(parseInt(miningServiceFee * 1e10)))
.div(toBN(1e10 * 100))
/* eslint-enable */
const desiredFee = expenseInPoints.add(serviceFeePercent) // in points
console.log(
'user provided fee, desired fee, serviceFeePercent',
providedFee.toString(),
desiredFee.toString(),
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 getProxyContract() {
let proxyAddress
if (netId === 5) {
proxyAddress = tornadoGoerliProxy
} else {
proxyAddress = await resolver.resolve(torn.tornadoProxy.address)
}
const contract = new web3.eth.Contract(tornadoProxyABI, proxyAddress)
return {
contract,
isOldProxy: checkOldProxy(proxyAddress),
}
}
function checkOldProxy(address) {
const OLD_PROXY = '0x905b63Fff465B9fFBF41DeA908CEb12478ec7601'
return toChecksumAddress(address) === toChecksumAddress(OLD_PROXY)
}
async function getTxObject({ data }) { async function getTxObject({ data }) {
if (data.type === jobType.TORNADO_WITHDRAW) { const calldata = tornadoProxyInstance.methods.withdraw(data.contract, data.proof, ...data.args).encodeABI()
let { contract, isOldProxy } = await getProxyContract()
let calldata = contract.methods.withdraw(data.contract, data.proof, ...data.args).encodeABI() const incompleteTx = {
if (isOldProxy && getInstance(data.contract).currency !== 'eth') {
contract = new web3.eth.Contract(tornadoABI, data.contract)
calldata = contract.methods.withdraw(data.proof, ...data.args).encodeABI()
}
return {
value: data.args[5], value: data.args[5],
to: contract._address, to: tornadoProxyInstance._address,
data: calldata, data: calldata,
gasLimit: gasLimits['WITHDRAW_WITH_EXTRA'],
}
} else {
const method = data.type === jobType.MINING_REWARD ? 'reward' : 'withdraw'
const calldata = minerContract.methods[method](data.proof, data.args).encodeABI()
return {
to: minerContract._address,
data: calldata,
gasLimit: gasLimits[data.type],
}
} }
const { gasLimit, gasPrice } = await feeOracle.getGasParams({
tx: incompleteTx,
txType: 'relayer_withdrawal',
})
return { ...incompleteTx, gasLimit, gasPrice }
} }
async function isOutdatedTreeRevert(receipt, currentTx) { async function checkRecipient({ data }) {
try { const recipient = data.args[2]
await web3.eth.call(currentTx.tx, receipt.blockNumber) if (!isAddress(recipient)) throw new Error('Recipient address is invalid')
console.log('Simulated call successful')
return false const addressCode = await web3.eth.getCode(toChecksumAddress(recipient))
} catch (e) { if (addressCode !== '0x') throw new Error('Recipient cannot be a smart-contract, only EOA')
console.log('Decoded revert reason:', e.message)
return (
e.message.indexOf('Outdated account merkle root') !== -1 ||
e.message.indexOf('Outdated tree update merkle root') !== -1
)
}
} }
async function processJob(job) { async function processJob(job) {
@ -254,13 +111,11 @@ async function processJob(job) {
} }
} }
async function submitTx(job, retry = 0) { async function submitTx(job) {
await checkFee(job) await checkRecipient(job)
currentTx = await txManager.createTx(await getTxObject(job)) const tx = await getTxObject(job)
await checkTornadoFee(job, tx)
if (job.data.type !== jobType.TORNADO_WITHDRAW) { currentTx = await txManager.createTx(tx)
await fetchTree()
}
try { try {
const receipt = await currentTx const receipt = await currentTx
@ -277,37 +132,14 @@ async function submitTx(job, retry = 0) {
if (receipt.status === 1) { if (receipt.status === 1) {
await updateStatus(status.CONFIRMED) await updateStatus(status.CONFIRMED)
} else {
if (job.data.type !== jobType.TORNADO_WITHDRAW && (await isOutdatedTreeRevert(receipt, currentTx))) {
if (retry < 3) {
await updateStatus(status.RESUBMITTED)
await submitTx(job, retry + 1)
} else {
throw new Error('Tree update retry limit exceeded')
}
} else { } else {
throw new Error('Submitted transaction failed') throw new Error('Submitted transaction failed')
} }
}
} catch (e) { } catch (e) {
// todo this could result in duplicated error logs // todo this could result in duplicated error logs
// todo handle a case where account tree is still not up to date (wait and retry)? // todo handle a case where account tree is still not up to date (wait and retry)?
if (
job.data.type !== jobType.TORNADO_WITHDRAW &&
(e.message.indexOf('Outdated account merkle root') !== -1 ||
e.message.indexOf('Outdated tree update merkle root') !== -1)
) {
if (retry < 5) {
await sleep(3000)
console.log('Tree is still not up to date, resubmitting')
await submitTx(job, retry + 1)
} else {
throw new Error('Tree update retry limit exceeded')
}
} else {
throw new Error(`Revert by smart contract ${e.message}`) throw new Error(`Revert by smart contract ${e.message}`)
} }
}
} }
async function updateTxHash(txHash) { async function updateTxHash(txHash) {

@ -1,10 +1,6 @@
require('chai').should() require('chai').should()
const { const { getTornadoWithdrawInputError } = require('../src/modules/validator')
getTornadoWithdrawInputError,
getMiningRewardInputError,
getMiningWithdrawInputError,
} = require('../src/validator')
describe('Validator', () => { describe('Validator', () => {
describe('#getTornadoWithdrawInputError', () => { describe('#getTornadoWithdrawInputError', () => {
@ -19,7 +15,13 @@ describe('Validator', () => {
'.proof should match pattern "^0x[a-fA-F0-9]{512}$"', '.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', () => { it('should throw something is missing', () => {
const malformedData = { ...withdrawData } const malformedData = { ...withdrawData }
delete malformedData.proof delete malformedData.proof
@ -35,56 +37,6 @@ describe('Validator', () => {
malformedData.contract = withdrawData.contract malformedData.contract = withdrawData.contract
}) })
}) })
describe('#getMiningRewardInputError', () => {
it('should work', () => {
getMiningRewardInputError(rewardData)
})
it('should throw for incorrect proof', () => {
const malformedData = { ...rewardData }
malformedData.proof = '0xbeef'
getMiningRewardInputError(malformedData).should.be.equal(
'.proof should match pattern "^0x[a-fA-F0-9]{512}$"',
)
})
it('should throw something is missing', () => {
const malformedData = { ...rewardData }
delete malformedData.proof
getMiningRewardInputError(malformedData).should.be.equal(" should have required property 'proof'")
malformedData.proof = rewardData.proof
delete malformedData.args
getMiningRewardInputError(malformedData).should.be.equal(" should have required property 'args'")
malformedData.args = rewardData.args
})
})
describe('#getMiningWithdrawInputError', () => {
it('should work', () => {
getMiningWithdrawInputError(miningWithdrawData)
})
it('should throw for incorrect proof', () => {
const malformedData = { ...miningWithdrawData }
malformedData.proof = '0xbeef'
getMiningWithdrawInputError(malformedData).should.be.equal(
'.proof should match pattern "^0x[a-fA-F0-9]{512}$"',
)
})
it('should throw something is missing', () => {
const malformedData = { ...miningWithdrawData }
delete malformedData.proof
getMiningWithdrawInputError(malformedData).should.be.equal(" should have required property 'proof'")
malformedData.proof = miningWithdrawData.proof
delete malformedData.args
getMiningWithdrawInputError(malformedData).should.be.equal(" should have required property 'args'")
malformedData.args = miningWithdrawData.args
})
})
}) })
const withdrawData = { const withdrawData = {
@ -98,54 +50,5 @@ const withdrawData = {
'0x000000000000000000000000000000000000000000000000058d15e176280000', '0x000000000000000000000000000000000000000000000000058d15e176280000',
'0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000',
], ],
contract: '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936', contract: '0xd47438C816c9E7f2E2888E060936a499Af9582b3',
}
const rewardData = {
proof:
'0x2e0f4c76b35ce3275bf57492cbe12ddc76fae4eabdbeaacdcc7cd5255d0abb2325bd80b2a867f9c1bab854de5d7c443a18eb9ad796943dd53c30c04e8f0a37ae164916c932776b3c28dd49808a5d5e1648d8bc9006b2386096b88757644ce8f102f7e2f1505bb66385a1d53a101922a17d8ab653694dedd7d150ec71d543202e0f0a67e5d59904d75af1c52bef4dfac0a302c2beb2ca3bb29b6bbbe1038368702e5ba8d6d829d74968a94e321cc91cccbc0654f5df6460a0a6ad73b06c42b7d1289ff36655fc7106b5538bd2c6617dd0c313919331e63bcb4de9c9b45dc2207b098a5729efbecf79a4cab39ade3c99e5772bfbe5ae75d932facbf9e0910a34ae',
args: {
rate: '0x000000000000000000000000000000000000000000000000000000000000000a',
fee: '0x0000000000000000000000000000000000000000000000000000000000000000',
instance: '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
rewardNullifier: '0x08fdc416b85c76d246925994ae0c0df539789fd1669c45b57104907c7ef8b0b5',
extDataHash: '0x006c5f12c20933beab10cfffab31ea0c9d736cf9aa868ee29eed3047d4ea4c2e',
depositRoot: '0x0405962838a47fb25ffd75d80d53b268654a06bc1bdde7e5ad94c675c2f2f0ff',
withdrawalRoot: '0x1cd83f5df5dbc826fecbf6be87f05db9c9dc617a3f1b1f3a421b1335c1ff7dbf',
extData: {
relayer: '0x0000000000000000000000000000000000000000',
encryptedAccount:
'0x6a8494fca4c433ef323d03f0db3fede90c3d2c6f216d73345ffc77ceec79622f327a83c4254063a3027620c262835e335fa32c33600a70547a53b2aa311d3ff35cf943e8f9e8f321f60d4266f680e0606a5837d78deb4d74c8b4fa3e9b67414513c71b73e38995cd8d57fd08aa9e135b342cecaf4128d4cfbb26148022e7a87da8b2423440b62034be202a6a48b45baa9736def6455771b442baaf2358fc52aa6c1d14a9a452b064d280fafd69f2a3ba416c10c1d8276f1c3810c664b24e0f1eefc75d63',
},
account: {
inputRoot: '0x22e875e5e54d8569fb40d0c568984e87b4c97da6383d8d8a334a79e22b48fd54',
inputNullifierHash: '0x24be972a00e3938a58f44ea6f8ead271ecdd6ab2cab42d1910fb7190b5816188',
outputRoot: '0x04a3cd1e37487dcee5da51cbce4245742903262a5824aef77fb7aff84a3cb053',
outputPathIndices: '0x0000000000000000000000000000000000000000000000000000000000000000',
outputCommitment: '0x0ae58c1605312bd42fffdfc41d5e0f9a364ad458717c522bf9338068ab258601',
},
},
}
const miningWithdrawData = {
proof:
'0x087c02cdc5946b44f295e1adb8b65341708fe43854e44f05f205da6e46e2e4c4248b2dd5ee30236e7be2ea657265765b4e43dae263d67ff43190bb806faaafc10dd0a771f9d589b5061ddf0a713f27fc0b496d1b136dc4e98838b88f60efb072087c3018fa5c25b1f78b4bb968291b9afa3966d976e961d0a86719a8e07d771209dad29620f3bc2fc21c00510749a19e7ff369ade6b9fd1a7f05b74e70faee771fd839c710bd983927c9d3d5f39bb5e839a2ece19e899c4d50a91b29d5ac3f1a0e8faf7eeb2f6f672561bfba39bcb1d851f6c97d5c14b7fce6661cf315af3468119855a426fc4df511e848011bcdb704369deba20541a7651ab4d5813a60c056',
args: {
amount: '0x000000000000000000000000000000000000000000000000000000000000000f',
fee: '0x0000000000000000000000000000000000000000000000000000000000000000',
extDataHash: '0x00d95a201b89061613b5bc539bcf8fdee63a400ea80f1f5e813d6aacfee3ec67',
extData: {
recipient: '0xf17f52151ebef6c7334fad080c5704d77216b732',
relayer: '0x0000000000000000000000000000000000000000',
encryptedAccount:
'0x4bd7f84edab796b390181d8b1dd850c418c8b3fe41d63b9677b7b99a2fadc505dcc70df336a42847dc00fa39175d16ddfec0d80dc166282e024b5371f561467651ed94e71524fa2e365a8330b053d5cff7c3bcc3564b335fb9e74fb805a3a6e760b811db60e5d6b4e154376196c3cb61457bac6d5ea804f63208a389555cde72f40ab1b94705e728f692e699fc441504b9df34390b3992a1a1eac160dcf0df0b5c5a9ec9cd6c0c8f5f8aa11627fdf2b3bedece5836e9ca38b09d70ff7ba06702971d245d',
},
account: {
inputRoot: '0x1a756aeee7f7d05f276b20c8ca83150e110e1a436c2d959e501ab306420ab536',
inputNullifierHash: '0x0dc8ea0330171a1f868ef5f3f9f92e919d7be754846f6145c5e7819e87738e65',
outputRoot: '0x0d9d85371bd8c941400ae54815491799e98d1f335a9d263e41f0b81f22b55aa8',
outputPathIndices: '0x0000000000000000000000000000000000000000000000000000000000000001',
outputCommitment: '0x1ebd38a8bc53f47386687386397c8b5cefd33d55341b62a2a576b39d9bcec57c',
},
},
} }

15
tornado-stream.conf Normal 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

@ -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;
}
}

2177
yarn.lock

File diff suppressed because it is too large Load Diff