initialise

This commit is contained in:
gozzy 2022-08-21 13:00:01 +00:00
commit 1e43aa4373
17 changed files with 5629 additions and 0 deletions

10
Dockerfile Normal file

@ -0,0 +1,10 @@
FROM node:12
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install && npm cache clean --force
COPY . .
EXPOSE 8000
HEALTHCHECK CMD curl -f http://localhost:8000/status
CMD ["npm", "run", "start"]

61
README.md Normal file

@ -0,0 +1,61 @@
# 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)
## Run locally
1. `npm i`
2. `cp .env.example .env`
3. Modify `.env` as needed
4. `npm run start`
5. Go to `http://127.0.0.1:8000`
6. In order to execute withdraw request, you can run following command
```bash
curl -X POST -H 'content-type:application/json' --data '<input data>' http://127.0.0.1:8000/relay
```
Relayer should return a transaction hash.
_Note._ If you want to change contracts' addresses go to [config.js](./config.js) file.
## 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.
1. Download docker-compose.yml
2. Change environment variables for `kovan` containers as appropriate
- add `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
- customize `RELAYER_FEE`
- update `RPC_URL` if needed
- update `REDIS_URL` if needed
3. Run `docker-compose up -d`
## Run as a Docker container
1. `cp .env.example .env`
2. Modify `.env` as needed
3. `docker run -d --env-file .env -p 80:8000 tornadocash/relayer`
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
## Input data example
```json
{
"proof": "0x0f8cb4c2ca9cbb23a5f21475773e19e39d3470436d7296f25c8730d19d88fcef2986ec694ad094f4c5fff79a4e5043bd553df20b23108bc023ec3670718143c20cc49c6d9798e1ae831fd32a878b96ff8897728f9b7963f0d5a4b5574426ac6203b2456d360b8e825d8f5731970bf1fc1b95b9713e3b24203667ecdd5939c2e40dec48f9e51d9cc8dc2f7f3916f0e9e31519c7df2bea8c51a195eb0f57beea4924cb846deaa78cdcbe361a6c310638af6f6157317bc27d74746bfaa2e1f8d2e9088fd10fa62100740874cdffdd6feb15c95c5a303f6bc226d5e51619c5b825471a17ddfeb05b250c0802261f7d05cf29a39a72c13e200e5bc721b0e4c50d55e6",
"args": [
"0x1579d41e5290ab5bcec9a7df16705e49b5c0b869095299196c19c5e14462c9e3",
"0x0cf7f49c5b35c48b9e1d43713e0b46a75977e3d10521e9ac1e4c3cd5e3da1c5d",
"0x03ebd0748aa4d1457cf479cce56309641e0a98f5",
"0xbd4369dc854c5d5b79fe25492e3a3cfcb5d02da5",
"0x000000000000000000000000000000000000000000000000058d15e176280000",
"0x0000000000000000000000000000000000000000000000000000000000000000"
],
"contract": "0xA27E34Ad97F171846bAf21399c370c9CE6129e0D"
}
```
Disclaimer:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

28
abis/PriceOracle.abi.json Normal file

@ -0,0 +1,28 @@
[
{
"constant": true,
"inputs": [
{
"internalType": "contract IERC20[]",
"name": "fromTokens",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "oneUnitAmounts",
"type": "uint256[]"
}
],
"name": "getPricesInETH",
"outputs": [
{
"internalType": "uint256[]",
"name": "prices",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]

498
abis/mixerABI.json Normal file

@ -0,0 +1,498 @@
[
{
"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
app.js Normal file

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

153
config.js Normal file

@ -0,0 +1,153 @@
require('dotenv').config()
module.exports = {
netId: Number(process.env.NET_ID) || 42,
redisUrl: process.env.REDIS_URL,
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/',
oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://mainnet.infura.io/',
oracleAddress: '0xA2b8E7ee7c8a18ea561A5CF7C9C365592026E374',
privateKey: process.env.PRIVATE_KEY,
mixers: {
netId1: {
eth: {
mixerAddress: {
'0.1': '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
'1': '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
'10': '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
'100': '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291'
},
symbol: 'ETH',
decimals: 18
},
dai: {
mixerAddress: {
'100': '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
'1000': '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
'10000': '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A',
'100000': undefined
},
tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
symbol: 'DAI',
decimals: 18
},
cdai: {
mixerAddress: {
'5000': '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
'50000': '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659',
'500000': '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00',
'5000000': undefined
},
tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
symbol: 'cDAI',
decimals: 8
},
usdc: {
mixerAddress: {
'100': '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
'1000': '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
'10000': '0xD691F27f38B395864Ea86CfC7253969B409c362d',
'100000': undefined
},
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
symbol: 'USDC',
decimals: 6
},
cusdc: {
mixerAddress: {
'5000': '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6',
'50000': '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a',
'500000': '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D',
'5000000': undefined
},
tokenAddress: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
symbol: 'cUSDC',
decimals: 8
},
usdt: {
mixerAddress: {
'100': '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
'1000': '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
'10000': '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a',
'100000': '0x9AD122c22B14202B4490eDAf288FDb3C7cb3ff5E'
},
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
symbol: 'USDT',
decimals: 6
}
},
netId42: {
eth: {
mixerAddress: {
'0.1': '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
'1': '0xD6a6AC46d02253c938B96D12BE439F570227aE8E',
'10': '0xe1BE96331391E519471100c3c1528B66B8F4e5a7',
'100': '0xd037E0Ac98Dab2fCb7E296c69C6e52767Ae5414D'
},
symbol: 'ETH',
decimals: 18
},
dai: {
mixerAddress: {
'100': '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D',
'1000': '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD',
'10000': '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3',
'100000': undefined
},
tokenAddress: '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa',
symbol: 'DAI',
decimals: 18
},
cdai: {
mixerAddress: {
'5000': '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8',
'50000': '0x7182EA067e0f050997444FCb065985Fd677C16b6',
'500000': '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae',
'5000000': undefined
},
tokenAddress: '0xe7bc397DBd069fC7d0109C0636d06888bb50668c',
symbol: 'cDAI',
decimals: 8
},
usdc: {
mixerAddress: {
'100': '0x137E2B6d185018e7f09f6cf175a970e7fC73826C',
'1000': '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af',
'10000': '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF',
'100000': undefined
},
tokenAddress: '0x75B0622Cec14130172EaE9Cf166B92E5C112FaFF',
symbol: 'USDC',
decimals: 6
},
cusdc: {
mixerAddress: {
'5000': '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0',
'50000': '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D',
'500000': '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1',
'5000000': undefined
},
tokenAddress: '0xcfC9bB230F00bFFDB560fCe2428b4E05F3442E35',
symbol: 'cUSDC',
decimals: 8
},
usdt: {
mixerAddress: {
'100': '0x327853Da7916a6A0935563FB1919A48843036b42',
'1000': '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0',
'10000': '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD',
'100000': '0x14aEd24B67EaF3FF28503eB92aeb217C47514364'
},
tokenAddress: '0x03c5F29e9296006876d8DF210BCFfD7EA5Db1Cf1',
symbol: 'USDT',
decimals: 6
}
}
},
defaultGasPrice: 20,
port: process.env.APP_PORT,
relayerServiceFee: Number(process.env.RELAYER_FEE),
maxGasPrice: process.env.MAX_GAS_PRICE || 200,
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000,
pendingTxTimeout: Number(process.env.ALLOWABLE_PENDING_TX_TIMEOUT || 180) * 1000,
gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20
}

89
docker-compose.yml Normal file

@ -0,0 +1,89 @@
version: '2'
services:
kovan:
image: tornadocash/relayer
restart: always
environment:
VIRTUAL_HOST: example.duckdns.org
LETSENCRYPT_HOST: example.duckdns.org
NET_ID: 42
RPC_URL: https://kovan.infura.io
# ORACLE_RPC_URL should always point to the mainnet
ORACLE_RPC_URL: https://mainnet.infura.io
# without 0x prefix
PRIVATE_KEY:
# 2.5 means 2.5%
RELAYER_FEE: 2.5
REDIS_URL: redis://redis/0
nginx_proxy_read_timeout: 600
depends_on:
- redis
mainnet:
image: tornadocash/relayer
restart: always
environment:
VIRTUAL_HOST: example2.duckdns.org
LETSENCRYPT_HOST: example2.duckdns.org
NET_ID: 1
RPC_URL: https://mainnet.infura.io
# ORACLE_RPC_URL should always point to the mainnet
ORACLE_RPC_URL: https://mainnet.infura.io
# without 0x prefix
PRIVATE_KEY:
# 2.5 means 2.5%
RELAYER_FEE: 2.5
REDIS_URL: redis://redis/1
nginx_proxy_read_timeout: 600
depends_on:
- redis
redis:
image: redis
restart: always
command: [redis-server, --appendonly, 'yes']
volumes:
- redis:/data
nginx:
image: nginx:alpine
container_name: nginx
restart: always
ports:
- 80:80
- 443:443
volumes:
- conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs
logging:
driver: none
dockergen:
image: poma/docker-gen
container_name: dockergen
restart: always
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
volumes_from:
- nginx
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
letsencrypt:
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: letsencrypt
restart: always
environment:
NGINX_DOCKER_GEN_CONTAINER: dockergen
volumes_from:
- nginx
- dockergen
volumes:
conf:
vhost:
html:
certs:
redis:

4138
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file

@ -0,0 +1,26 @@
{
"name": "relay",
"version": "3.0.3",
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
"main": "app.js",
"scripts": {
"start": "node app.js",
"eslint": "npx eslint --ignore-path .gitignore .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "tornado.cash",
"license": "MIT",
"dependencies": {
"bull": "^3.12.1",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"gas-price-oracle": "^0.2.2",
"ioredis": "^4.14.1",
"node-fetch": "^2.6.0",
"web3": "^1.2.2",
"web3-utils": "^1.2.2"
},
"devDependencies": {
"eslint": "^6.6.0"
}
}

56
src/Fetcher.js Normal file

@ -0,0 +1,56 @@
const Web3 = require('web3')
const { defaultGasPrice, oracleRpcUrl, oracleAddress } = require('../config')
const { getArgsForOracle } = require('./utils')
const { redisClient } = require('./redis')
const priceOracleABI = require('../abis/PriceOracle.abi.json')
class Fetcher {
constructor(web3) {
this.web3 = web3
this.oracleWeb3 = new Web3(oracleRpcUrl)
this.oracle = new this.oracleWeb3.eth.Contract(priceOracleABI, oracleAddress)
this.ethPrices = {
dai: '6700000000000000', // 0.0067
cdai: '157380000000000',
cusdc: '164630000000000',
usdc: '7878580000000000',
usdt: '7864940000000000'
}
this.tokenAddresses
this.oneUintAmount
this.currencyLookup
this.gasPrices = {
fast: defaultGasPrice
}
const { tokenAddresses, oneUintAmount, currencyLookup } = getArgsForOracle()
this.tokenAddresses = tokenAddresses
this.oneUintAmount = oneUintAmount
this.currencyLookup = currencyLookup
}
async fetchPrices() {
try {
let prices = await this.oracle.methods.getPricesInETH(this.tokenAddresses, this.oneUintAmount).call()
this.ethPrices = prices.reduce((acc, price, i) => {
acc[this.currencyLookup[this.tokenAddresses[i]]] = price
return acc
}, {})
setTimeout(() => this.fetchPrices(), 1000 * 30)
} catch (e) {
console.error('fetchPrices', e.message)
setTimeout(() => this.fetchPrices(), 1000 * 30)
}
}
async fetchNonce() {
try {
const nonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount)
await redisClient.set('nonce', nonce)
console.log(`Current nonce: ${nonce}`)
} catch (e) {
console.error('fetchNonce failed', e.message)
setTimeout(this.fetchNonce, 3000)
}
}
}
module.exports = Fetcher

98
src/index.js Normal file

@ -0,0 +1,98 @@
const express = require('express')
const {
netId,
port,
relayerServiceFee,
gasBumpPercentage,
pendingTxTimeout,
watherInterval,
maxGasPrice
} = require('../config')
const relayController = require('./relayController')
const { fetcher, web3, gasPriceOracle } = require('./instances')
const { getMixers } = require('./utils')
const mixers = getMixers()
const { redisClient } = require('./redis')
const { version } = require('../package.json')
const app = express()
app.use(express.json())
app.use((err, req, res, next) => {
if (err) {
console.log('Invalid Request data')
res.send('Invalid Request data')
} else {
next()
}
})
app.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
next()
})
app.get('/', function (req, res) {
// just for testing purposes
res.send(
'This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/status>/status</a> for settings'
)
})
app.get('/status', async function (req, res) {
let nonce = await redisClient.get('nonce')
let latestBlock = null
try {
latestBlock = await web3.eth.getBlockNumber()
} catch (e) {
console.error('Problem with RPC', e)
}
const { ethPrices } = fetcher
res.json({
relayerAddress: web3.eth.defaultAccount,
mixers,
gasPrices: await gasPriceOracle.gasPrices(),
netId,
ethPrices,
relayerServiceFee,
nonce,
version,
latestBlock
})
})
app.post('/relay', relayController)
console.log('Version:', version)
let server = app.listen(port || 8000)
server.setTimeout(600000)
console.log('Gas price oracle started.')
fetcher.fetchPrices()
fetcher.fetchNonce()
console.log('Relayer started on port', port || 8000)
console.log(`relayerAddress: ${web3.eth.defaultAccount}`)
console.log(`mixers: ${JSON.stringify(mixers)}`)
console.log(`netId: ${netId}`)
console.log(`ethPrices: ${JSON.stringify(fetcher.ethPrices)}`)
const {
GAS_PRICE_BUMP_PERCENTAGE,
ALLOWABLE_PENDING_TX_TIMEOUT,
NONCE_WATCHER_INTERVAL,
MAX_GAS_PRICE
} = process.env
if (!NONCE_WATCHER_INTERVAL) {
console.log(`NONCE_WATCHER_INTERVAL is not set. Using default value ${watherInterval / 1000} sec`)
}
if (!GAS_PRICE_BUMP_PERCENTAGE) {
console.log(`GAS_PRICE_BUMP_PERCENTAGE is not set. Using default value ${gasBumpPercentage}%`)
}
if (!ALLOWABLE_PENDING_TX_TIMEOUT) {
console.log(`ALLOWABLE_PENDING_TX_TIMEOUT is not set. Using default value ${pendingTxTimeout / 1000} sec`)
}
if (!MAX_GAS_PRICE) {
console.log(`ALLOWABLE_PENDING_TX_TIMEOUT is not set. Using default value ${maxGasPrice} Gwei`)
}

15
src/instances.js Normal file

@ -0,0 +1,15 @@
const { rpcUrl } = require('../config')
const Fetcher = require('./Fetcher')
const Sender = require('./sender')
const { GasPriceOracle } = require('gas-price-oracle')
const web3 = require('./setupWeb3')
const fetcher = new Fetcher(web3)
const sender = new Sender(web3)
const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
module.exports = {
fetcher,
web3,
sender,
gasPriceOracle
}

19
src/redis.js Normal file

@ -0,0 +1,19 @@
const { redisUrl } = require('../config')
const Redis = require('ioredis')
const redisClient = new Redis(redisUrl)
const subscriber = new Redis(redisUrl)
const redisOpts = {
createClient: function (type) {
switch (type) {
case 'client':
return redisClient
case 'subscriber':
return subscriber
default:
return new Redis(redisUrl)
}
}
}
module.exports = { redisOpts, redisClient }

162
src/relayController.js Normal file

@ -0,0 +1,162 @@
const Queue = require('bull')
const { numberToHex, toWei, toHex, toBN, toChecksumAddress } = require('web3-utils')
const mixerABI = require('../abis/mixerABI.json')
const { isValidProof, isValidArgs, isKnownContract, isEnoughFee } = require('./utils')
const config = require('../config')
const { redisClient, redisOpts } = require('./redis')
const { web3, fetcher, sender, gasPriceOracle } = require('./instances')
const withdrawQueue = new Queue('withdraw', redisOpts)
const reponseCbs = {}
let respLambda = (job, { msg, status }) => {
const resp = reponseCbs[job.id]
resp.status(status).json(msg)
delete reponseCbs[job.id]
}
withdrawQueue.on('completed', respLambda)
async function relayController(req, resp) {
let requestJob
const { proof, args, contract } = req.body
let { valid, reason } = isValidProof(proof)
if (!valid) {
console.log('Proof is invalid:', reason)
return resp.status(400).json({ error: 'Proof format is invalid' })
}
// eslint-disable-next-line no-extra-semi
;({ valid, reason } = isValidArgs(args))
if (!valid) {
console.log('Args are invalid:', reason)
return resp.status(400).json({ error: 'Withdraw arguments are invalid' })
}
let currency, amount
;({ valid, currency, amount } = isKnownContract(contract))
if (!valid) {
console.log('Contract does not exist:', contract)
return resp.status(400).json({ error: 'This relayer does not support the token' })
}
const [root, nullifierHash, recipient, relayer, fee, refund] = [
args[0],
args[1],
toChecksumAddress(args[2]),
toChecksumAddress(args[3]),
toBN(args[4]),
toBN(args[5])
]
console.log('fee, refund', fee.toString(), refund.toString(), recipient)
if (currency === 'eth' && !refund.isZero()) {
return resp.status(400).json({ error: 'Cannot send refund for eth currency.' })
}
if (relayer !== web3.eth.defaultAccount) {
console.log('This proof is for different relayer:', relayer)
return resp.status(400).json({ error: 'Relayer address is invalid' })
}
requestJob = await withdrawQueue.add(
{
contract,
nullifierHash,
root,
proof,
args,
currency,
amount,
fee: fee.toString(),
refund: refund.toString()
},
{ removeOnComplete: true }
)
reponseCbs[requestJob.id] = resp
}
withdrawQueue.process(async function (job, done) {
console.log(Date.now(), ' withdraw started', job.id)
const gasPrices = await gasPriceOracle.gasPrices()
const { contract, nullifierHash, root, proof, args, refund, currency, amount, fee } = job.data
console.log(JSON.stringify(job.data))
// job.data contains the custom data passed when the job was created
// job.id contains id of this job.
try {
const mixer = new web3.eth.Contract(mixerABI, contract)
const isSpent = await mixer.methods.isSpent(nullifierHash).call()
if (isSpent) {
done(null, {
status: 400,
msg: {
error: 'The note has been spent.'
}
})
return
}
const isKnownRoot = await mixer.methods.isKnownRoot(root).call()
if (!isKnownRoot) {
done(null, {
status: 400,
msg: {
error: 'The merkle root is too old or invalid.'
}
})
return
}
let gas = await mixer.methods.withdraw(proof, ...args).estimateGas({
from: web3.eth.defaultAccount,
value: refund
})
gas += 50000
const ethPrices = fetcher.ethPrices
const { isEnough, reason } = isEnoughFee({
gas,
gasPrices,
currency,
amount,
refund: toBN(refund),
ethPrices,
fee: toBN(fee)
})
if (!isEnough) {
console.log(`Wrong fee: ${reason}`)
done(null, {
status: 400,
msg: { error: reason }
})
return
}
const data = mixer.methods.withdraw(proof, ...args).encodeABI()
let nonce = Number(await redisClient.get('nonce'))
console.log('nonce', nonce)
const tx = {
from: web3.eth.defaultAccount,
value: numberToHex(refund),
gas: numberToHex(gas),
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
// you can use this gasPrice to test watcher
// gasPrice: numberToHex(100000000),
to: mixer._address,
netId: config.netId,
data,
nonce
}
tx.date = Date.now()
await redisClient.set('tx:' + nonce, JSON.stringify(tx))
nonce += 1
await redisClient.set('nonce', nonce)
sender.sendTx(tx, done)
} catch (e) {
console.error(e, 'estimate gas failed')
done(null, {
status: 400,
msg: { error: 'Internal Relayer Error. Please use a different relayer service' }
})
}
})
module.exports = relayController

81
src/sender.js Normal file

@ -0,0 +1,81 @@
const { redisClient } = require('./redis')
const config = require('../config')
const { toBN, toHex, toWei, BN, fromWei } = require('web3-utils')
class Sender {
constructor(web3) {
this.web3 = web3
this.watherInterval = config.watherInterval
this.pendingTxTimeout = config.pendingTxTimeout
this.gasBumpPercentage = 100 + Number(config.gasBumpPercentage)
this.watcher()
}
async watcher() {
try {
const networkNonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount)
let tx = await redisClient.get('tx:' + networkNonce)
if (tx) {
tx = JSON.parse(tx)
if (Date.now() - tx.date > this.pendingTxTimeout) {
const newGasPrice = toBN(tx.gasPrice).mul(toBN(this.gasBumpPercentage)).div(toBN(100))
const maxGasPrice = toBN(toWei(config.maxGasPrice.toString(), 'Gwei'))
tx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
tx.date = Date.now()
await redisClient.set('tx:' + tx.nonce, JSON.stringify(tx))
console.log('resubmitting with gas price', fromWei(tx.gasPrice.toString(), 'gwei'), ' gwei')
this.sendTx(tx, null, 9999)
}
}
} catch (e) {
console.error('watcher error:', e)
} finally {
setTimeout(() => this.watcher(), this.watherInterval)
}
}
async sendTx(tx, done, retryAttempt = 1) {
let signedTx = await this.web3.eth.accounts.signTransaction(tx, config.privateKey)
let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction)
result
.once('transactionHash', (txHash) => {
console.log(`A new successfully sent tx ${txHash}`)
if (done) {
done(null, {
status: 200,
msg: { txHash }
})
}
})
.on('error', async (e) => {
console.log(`Error for tx with nonce ${tx.nonce}\n${e.message}`)
if (
e.message ===
'Returned error: Transaction gas price supplied is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce.' ||
e.message === 'Returned error: Transaction nonce is too low. Try incrementing the nonce.' ||
e.message === 'Returned error: nonce too low' ||
e.message === 'Returned error: replacement transaction underpriced'
) {
console.log('nonce too low, retrying')
if (retryAttempt <= 10) {
retryAttempt++
const newNonce = tx.nonce + 1
tx.nonce = newNonce
await redisClient.set('nonce', newNonce)
await redisClient.set('tx:' + newNonce, JSON.stringify(tx))
this.sendTx(tx, done, retryAttempt)
return
}
}
if (done) {
done(null, {
status: 400,
msg: { error: 'Internal Relayer Error. Please use a different relayer service' }
})
}
})
}
}
module.exports = Sender

16
src/setupWeb3.js Normal file

@ -0,0 +1,16 @@
const Web3 = require('web3')
const { rpcUrl, privateKey } = require('../config')
function setup() {
try {
const web3 = new Web3(rpcUrl, null, { transactionConfirmationBlocks: 1 })
const account = web3.eth.accounts.privateKeyToAccount('0x' + privateKey)
web3.eth.accounts.wallet.add('0x' + privateKey)
web3.eth.defaultAccount = account.address
return web3
} catch (e) {
console.error('web3 failed')
}
}
const web3 = setup()
module.exports = web3

178
src/utils.js Normal file

@ -0,0 +1,178 @@
const { isHexStrict, toBN, toWei, BN } = require('web3-utils')
const { netId, mixers, relayerServiceFee } = require('../config')
function isValidProof(proof) {
// validator expects `websnarkUtils.toSolidityInput(proof)` output
if (!proof) {
return { valid: false, reason: 'The proof is empty.' }
}
if (!isHexStrict(proof) || proof.length !== 2 + 2 * 8 * 32) {
return { valid: false, reason: 'Corrupted proof' }
}
return { valid: true }
}
function isValidArgs(args) {
if (!args) {
return { valid: false, reason: 'Args are empty' }
}
if (args.length !== 6) {
return { valid: false, reason: 'Length of args is lower than 6' }
}
for (let signal of args) {
if (!isHexStrict(signal)) {
return { valid: false, reason: `Corrupted signal ${signal}` }
}
}
if (
args[0].length !== 66 ||
args[1].length !== 66 ||
args[2].length !== 42 ||
args[3].length !== 42 ||
args[4].length !== 66 ||
args[5].length !== 66
) {
return { valid: false, reason: 'The length one of the signals is incorrect' }
}
return { valid: true }
}
function isKnownContract(contract) {
const mixers = getMixers()
for (let currency of Object.keys(mixers)) {
for (let amount of Object.keys(mixers[currency].mixerAddress)) {
if (mixers[currency].mixerAddress[amount] === contract) {
return { valid: true, currency, amount }
}
}
}
return { valid: false }
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function fromDecimals(value, decimals) {
value = value.toString()
let ether = value.toString()
const base = new BN('10').pow(new BN(decimals))
const baseLength = base.toString(10).length - 1 || 1
const negative = ether.substring(0, 1) === '-'
if (negative) {
ether = ether.substring(1)
}
if (ether === '.') {
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, invalid value')
}
// Split it into a whole and fractional part
const comps = ether.split('.')
if (comps.length > 2) {
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal points')
}
let whole = comps[0]
let fraction = comps[1]
if (!whole) {
whole = '0'
}
if (!fraction) {
fraction = '0'
}
if (fraction.length > baseLength) {
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal places')
}
while (fraction.length < baseLength) {
fraction += '0'
}
whole = new BN(whole)
fraction = new BN(fraction)
let wei = whole.mul(base).add(fraction)
if (negative) {
wei = wei.mul(negative)
}
return new BN(wei.toString(10), 10)
}
function isEnoughFee({ gas, gasPrices, currency, amount, refund, ethPrices, fee }) {
const { decimals } = mixers[`netId${netId}`][currency]
const decimalsPoint =
Math.floor(relayerServiceFee) === relayerServiceFee
? 0
: relayerServiceFee.toString().split('.')[1].length
const roundDecimal = 10 ** decimalsPoint
const feePercent = toBN(fromDecimals(amount, decimals))
.mul(toBN(relayerServiceFee * roundDecimal))
.div(toBN(roundDecimal * 100))
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(gas))
let desiredFee
switch (currency) {
case 'eth': {
desiredFee = expense.add(feePercent)
break
}
default: {
desiredFee = expense
.add(refund)
.mul(toBN(10 ** decimals))
.div(toBN(ethPrices[currency]))
desiredFee = desiredFee.add(feePercent)
break
}
}
console.log(
'sent fee, desired fee, feePercent',
fee.toString(),
desiredFee.toString(),
feePercent.toString()
)
if (fee.lt(desiredFee)) {
return { isEnough: false, reason: 'Not enough fee' }
}
return { isEnough: true }
}
function getArgsForOracle() {
const tokens = mixers['netId1']
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 getMixers() {
return mixers[`netId${netId}`]
}
module.exports = {
isValidProof,
isValidArgs,
sleep,
isKnownContract,
isEnoughFee,
getMixers,
getArgsForOracle
}