Merge pull request #2 from peppersec/erc20_support

Erc20 support
This commit is contained in:
Roman Storm 2019-12-04 11:20:03 -08:00 committed by GitHub
commit c760a3e056
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 5747 additions and 3977 deletions

View File

@ -1,4 +1,7 @@
NET_ID=42 NET_ID=42
RPC_URL=https://kovan.infura.io/v3/a3f4d001c1fc4a359ea70dd27fd9cb51 RPC_URL=https://kovan.infura.io/v3/a3f4d001c1fc4a359ea70dd27fd9cb51
PRIVATE_KEY= PRIVATE_KEY=
MIXER_ADDRESS=0xb2aD997a43768aB9279Cd9E72D5B75D789a09011 # 2.5 means 2.5%
RELAYER_FEE=2.5
APP_PORT=8000

View File

@ -16,7 +16,8 @@
"rules": { "rules": {
"indent": [ "indent": [
"error", "error",
2 2,
{"SwitchCase": 1}
], ],
"linebreak-style": [ "linebreak-style": [
"error", "error",

View File

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

View File

@ -1,40 +1,35 @@
# Relayer for Tornado mixer [![Build Status](https://travis-ci.org/peppersec/tornado-mixer-relayer.svg?branch=master)](https://travis-ci.org/peppersec/tornado-mixer-relayer) # Relayer for Tornado mixer [![Build Status](https://travis-ci.org/peppersec/tornado-mixer-relayer.svg?branch=master)](https://travis-ci.org/peppersec/tornado-mixer-relayer)
## Setup ## Setup
1. `npm i` 1. `npm i`
2. `cp .env.example .env` 2. `cp .env.example .env`
3. Modify `.env` as needed 3. Modify `.env` as needed
4. If you want to change contracts' addresses go to [config.js](./config.js) file.
## Deploy Kovan
1. `cp .env.example deploy/kovan/.env`
2. `cd deploy/kovan`
2. Modify `.env` as needed
3. `docker-compose -p kovan up -d`
## Run locally ## Run locally
1. `npm run start` 1. `npm run start`
2. `curl -X POST -H 'content-type:application/json' --data '<PROOF>' http://127.0.0.1:8000/relay` 2. `curl -X POST -H 'content-type:application/json' --data '<input data>' http://127.0.0.1:8000/relay`
Relayer should return a transaction hash. Relayer should return a transaction hash.
## Proof example
## Input data example
```json ```json
{ {
"pi_a":[ "proof": "0x0f8cb4c2ca9cbb23a5f21475773e19e39d3470436d7296f25c8730d19d88fcef2986ec694ad094f4c5fff79a4e5043bd553df20b23108bc023ec3670718143c20cc49c6d9798e1ae831fd32a878b96ff8897728f9b7963f0d5a4b5574426ac6203b2456d360b8e825d8f5731970bf1fc1b95b9713e3b24203667ecdd5939c2e40dec48f9e51d9cc8dc2f7f3916f0e9e31519c7df2bea8c51a195eb0f57beea4924cb846deaa78cdcbe361a6c310638af6f6157317bc27d74746bfaa2e1f8d2e9088fd10fa62100740874cdffdd6feb15c95c5a303f6bc226d5e51619c5b825471a17ddfeb05b250c0802261f7d05cf29a39a72c13e200e5bc721b0e4c50d55e6",
"0x0ed9b1afc791a551f5baa2f84786963b1463ca3f7c68eb0de3b267e6cb491f05", "args": [
"0x1335f2af3c71e442fd82f63f8f1c605ca2612b8d0fa22b4cbd1239cca839aa3d" "0x1579d41e5290ab5bcec9a7df16705e49b5c0b869095299196c19c5e14462c9e3",
], "0x0cf7f49c5b35c48b9e1d43713e0b46a75977e3d10521e9ac1e4c3cd5e3da1c5d",
"pi_b":[ "0x03ebd0748aa4d1457cf479cce56309641e0a98f5",
[ "0xbd4369dc854c5d5b79fe25492e3a3cfcb5d02da5",
"0x000189f7f1067a768d116cd86980eae6963dd9bc6c1f8204ceacf90a94f60d81", "0x000000000000000000000000000000000000000000000000058d15e176280000",
"0x1abb4b71da0efa67cbc76a97ac360826b17a88f07bd89151258bf076474a4804"
],
[
"0x0526b509ba2cda2b21b09401d70d23ea0225be4fdaa9097af842ff6783d1e0f4",
"0x15b11f9f5441adeea61534105902170a409b228e159fe7428abf6e863fc05273"
]
],
"pi_c":[
"0x2cd9a2305827f7da64aa1a3136c11ae1d3d7b3cb69832d8c04ab39d8b9393cda",
"0x2090cd3f9d09d66ca4e1e9bed2c72d5fa174b47599cb47e572324b1a98a3cb7a"
],
"publicSignals":[
"0x1e8a85160889dfb5c03a8e2a6cca18b4c476c0b486003e9ed666a33e04114658",
"0x00bfb0befe19eac571ecaf7858e50d70273fbe2952cc8431f59399bb28665796",
"0x00000000000000000000000003ebd0748aa4d1457cf479cce56309641e0a98f5",
"0x0000000000000000000000000000000000000000000000000000000000000000" "0x0000000000000000000000000000000000000000000000000000000000000000"
] ],
"contract": "0xA27E34Ad97F171846bAf21399c370c9CE6129e0D"
} }
``` ```

498
abis/mixerABI.json Normal file
View 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"
}
]

View File

@ -1,10 +1,56 @@
require('dotenv').config() require('dotenv').config()
module.exports = { module.exports = {
netId: process.env.NET_ID || 42, netId: Number(process.env.NET_ID) || 42,
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/v3/a3f4d001c1fc4a359ea70dd27fd9cb51', rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/v3/a3f4d001c1fc4a359ea70dd27fd9cb51',
privateKey: process.env.PRIVATE_KEY, privateKey: process.env.PRIVATE_KEY,
mixerAddress: process.env.MIXER_ADDRESS, nonce: 0,
mixers: {
netId1: {
dai: {
mixerAddress: {
'100': undefined,
'500': undefined,
'1000': undefined,
'5000': undefined
},
tokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f',
decimals: 18
},
eth: {
mixerAddress: {
'0.1': undefined,
'1': undefined,
'10': undefined,
'100': undefined
},
decimals: 18
}
},
netId42: {
dai: {
mixerAddress: {
'100': '0x5D4538D2b07cD8Eb7b93c33B327f3E01A42e68d8',
'500': undefined,
'1000': undefined,
'5000': undefined
},
tokenAddress: '0x8c158c7e57161dd4d3cb02bf1a3a97fcc78b75fd',
decimals: 18
},
eth: {
mixerAddress: {
'0.1': '0xB7F60Bf8b969CE4B95Bb50a671860D99478C81Ee',
'1': '0x27e94B8cfa33EA2b47E209Ba69804d44642B3545',
'10': undefined,
'100': undefined
},
decimals: 18
}
}
},
defaultGasPrice: 2, defaultGasPrice: 2,
gasOracleUrls: ['https://www.etherchain.org/api/gasPriceOracle', 'https://gasprice.poa.network/'] gasOracleUrls: ['https://www.etherchain.org/api/gasPriceOracle', 'https://gasprice.poa.network/'],
port: process.env.APP_PORT,
relayerServiceFee: Number(process.env.RELAYER_FEE)
} }

View File

@ -0,0 +1,18 @@
version: '2.2'
services:
relayer:
build: ../../
restart: always
environment:
NODE_ENV: production
VIRTUAL_HOST: kovan.tornado.cash
LETSENCRYPT_HOST: kovan.tornado.cash
env_file: ./.env
healthcheck:
test: curl -sS http://127.0.0.1:8000 || exit 1
networks:
default:
external:
name: frontend_default

View File

@ -0,0 +1,18 @@
version: '2.2'
services:
relayer:
build: ../../
restart: always
environment:
NODE_ENV: production
VIRTUAL_HOST: mainnet.tornado.cash
LETSENCRYPT_HOST: mainnet.tornado.cash
env_file: ./.env
healthcheck:
test: curl -sS http://127.0.0.1:8000 || exit 1
networks:
default:
external:
name: frontend_default

View File

@ -1,23 +0,0 @@
version: '2'
services:
relayer:
build: ./
restart: always
environment:
NODE_ENV: production
VIRTUAL_HOST: relayer.tornado.cash
LETSENCRYPT_HOST: relayer.tornado.cash
env_file: ./.env
monitor:
image: arefaslani/docker-telegram-notifier
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
env_file: ./.env
networks:
default:
external:
name: frontend_default

View File

@ -1,23 +0,0 @@
version: '2'
services:
nginx:
image: jwilder/nginx-proxy
restart: always
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- /etc/nginx/certs
- /etc/nginx/vhost.d
- /usr/share/nginx/html
letsencrypt:
image: jrcs/letsencrypt-nginx-proxy-companion
restart: always
volumes_from:
- nginx
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro

View File

@ -1,90 +0,0 @@
const { numberToHex, toWei, toHex, toBN } = require('web3-utils')
const Web3 = require('web3')
const express = require('express')
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()
})
const { netId, rpcUrl, privateKey, mixerAddress, defaultGasPrice } = require('./config')
const { fetchGasPrice, isValidProof } = require('./utils')
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
const mixerABI = require('./mixerABI.json')
const mixer = new web3.eth.Contract(mixerABI, mixerAddress)
const gasPrices = { fast: defaultGasPrice }
app.get('/', function (req, res) {
// just for testing purposes
res.send(`Tornado mixer relayer. Gas Price is ${JSON.stringify(gasPrices)}. Mixer address is ${mixerAddress}`)
})
app.post('/relay', async (req, resp) => {
const { valid , reason } = isValidProof(req.body)
if (!valid) {
console.log('Proof is invalid:', reason)
return resp.status(400).json({ error: 'Proof is invalid' })
}
let { pi_a, pi_b, pi_c, publicSignals } = req.body
const fee = toBN(publicSignals[3])
const desiredFee = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN('1000000'))
if (fee.lt(desiredFee)) {
console.log('Fee is too low')
return resp.status(400).json({ error: 'Fee is too low. Try to resend.' })
}
try {
const nullifier = publicSignals[1]
const isSpent = await mixer.methods.isSpent(nullifier).call()
if (isSpent) {
return resp.status(400).json({ error: 'The note has been spent.' })
}
const root = publicSignals[0]
const isKnownRoot = await mixer.methods.isKnownRoot(root).call()
if (!isKnownRoot) {
return resp.status(400).json({ error: 'The merkle root is too old or invalid.' })
}
const gas = await mixer.methods.withdraw(pi_a, pi_b, pi_c, publicSignals).estimateGas()
const result = mixer.methods.withdraw(pi_a, pi_b, pi_c, publicSignals).send({
gas: numberToHex(gas + 50000),
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
// TODO: nonce
})
result.once('transactionHash', function(hash){
resp.json({ txHash: hash })
}).on('error', function(e){
console.log(e)
return resp.status(400).json({ error: 'Proof is malformed.' })
})
} catch (e) {
console.log(e)
return resp.status(400).json({ error: 'Proof is malformed or spent.' })
}
})
app.listen(8000)
if (Number(netId) === 1) {
fetchGasPrice({ gasPrices })
console.log('Gas price oracle started.')
}

View File

@ -1,320 +0,0 @@
[
{
"constant": true,
"inputs": [],
"name": "filled_subtrees",
"outputs": [
{
"name": "",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "nullifierHashes",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "transferValue",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "roots",
"outputs": [
{
"name": "",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "commitments",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "zeros",
"outputs": [
{
"name": "",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "levels",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "left",
"type": "uint256"
},
{
"name": "right",
"type": "uint256"
}
],
"name": "hashLeftRight",
"outputs": [
{
"name": "mimc_hash",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "next_index",
"outputs": [
{
"name": "",
"type": "uint32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "current_root",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "root",
"type": "uint256"
}
],
"name": "isKnownRoot",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getLastRoot",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"name": "_verifier",
"type": "address"
},
{
"name": "_transferValue",
"type": "uint256"
},
{
"name": "_merkleTreeHeight",
"type": "uint8"
},
{
"name": "_emptyElement",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "commitment",
"type": "uint256"
},
{
"indexed": false,
"name": "leafIndex",
"type": "uint256"
},
{
"indexed": false,
"name": "timestamp",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "nullifierHash",
"type": "uint256"
},
{
"indexed": false,
"name": "fee",
"type": "uint256"
}
],
"name": "Withdraw",
"type": "event"
},
{
"constant": false,
"inputs": [
{
"name": "commitment",
"type": "uint256"
}
],
"name": "deposit",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "a",
"type": "uint256[2]"
},
{
"name": "b",
"type": "uint256[2][2]"
},
{
"name": "c",
"type": "uint256[2]"
},
{
"name": "input",
"type": "uint256[4]"
}
],
"name": "withdraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "nullifier",
"type": "uint256"
}
],
"name": "isSpent",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]

4900
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,22 +2,23 @@
"name": "relay", "name": "relay",
"version": "1.0.0", "version": "1.0.0",
"description": "Relayer for Tornado mixer. https://tornado.cash", "description": "Relayer for Tornado mixer. https://tornado.cash",
"main": "index.js", "main": "src/index.js",
"scripts": { "scripts": {
"start": "node index.js", "start": "node src/index.js",
"eslint": "npx eslint --ignore-path .gitignore .", "eslint": "npx eslint --ignore-path .gitignore .",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Alexey Pertsev <alexey@peppersec.com> (https://peppersec.com)", "author": "tornado.cash",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dotenv": "^8.0.0", "coingecko-api": "^1.0.6",
"dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"web3": "^1.0.0-beta.55", "web3": "^1.2.2",
"web3-utils": "^1.0.0" "web3-utils": "^1.2.2"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^6.0.1" "eslint": "^6.6.0"
} }
} }

77
src/Fetcher.js Normal file
View File

@ -0,0 +1,77 @@
const CoinGecko = require('coingecko-api')
const fetch = require('node-fetch')
const { toWei } = require('web3-utils')
const { gasOracleUrls, defaultGasPrice } = require('../config')
const { getMainnetTokens } = require('./utils')
const config = require ('../config')
class Fetcher {
constructor(web3) {
this.web3 = web3
this.ethPrices = {
dai: '6700000000000000' // 0.0067
}
this.gasPrices = {
fast: defaultGasPrice
}
}
async fetchPrices() {
const { tokenAddresses, currencyLookup } = getMainnetTokens()
try {
const CoinGeckoClient = new CoinGecko()
const price = await CoinGeckoClient.simple.fetchTokenPrice({
contract_addresses: tokenAddresses,
vs_currencies: 'eth',
assetPlatform: 'ethereum'
})
this.ethPrices = Object.entries(price.data).reduce((acc, token) => {
if (token[1].eth) {
acc[currencyLookup[token[0]]] = toWei(token[1].eth.toString())
}
return acc
}, {})
setTimeout(() => this.fetchPrices(), 1000 * 30)
} catch(e) {
setTimeout(() => this.fetchPrices(), 1000 * 30)
}
}
async fetchGasPrice({ oracleIndex = 0 } = {}) {
oracleIndex = (oracleIndex + 1) % gasOracleUrls.length
try {
const response = await fetch(gasOracleUrls[oracleIndex])
if (response.status === 200) {
const json = await response.json()
if (json.slow) {
this.gasPrices.low = Number(json.slow)
}
if (json.safeLow) {
this.gasPrices.low = Number(json.safeLow)
}
if (json.standard) {
this.gasPrices.standard = Number(json.standard)
}
if (json.fast) {
this.gasPrices.fast = Number(json.fast)
}
} else {
throw Error('Fetch gasPrice failed')
}
setTimeout(() => this.fetchGasPrice({ oracleIndex }), 15000)
} catch (e) {
setTimeout(() => this.fetchGasPrice({ oracleIndex }), 15000)
}
}
async fetchNonce() {
try {
config.nonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount)
console.log(`Current nonce: ${config.nonce}`)
} catch(e) {
console.error('fetchNonce failed', e.message)
setTimeout(this.fetchNonce, 3000)
}
}
}
module.exports = Fetcher

50
src/index.js Normal file
View File

@ -0,0 +1,50 @@
const express = require('express')
const { netId, port, relayerServiceFee } = require('../config')
const relayController = require('./relayController')
const { fetcher, web3 } = require('./instances')
const { getMixers } = require('./utils')
const mixers = getMixers()
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 /status for settings')
})
app.get('/status', function (req, res) {
const { ethPrices, gasPrices } = fetcher
res.json({ relayerAddress: web3.eth.defaultAccount, mixers, gasPrices, netId, ethPrices, relayerServiceFee })
})
app.post('/relay', relayController)
app.listen(port || 8000)
console.log('Gas price oracle started.')
fetcher.fetchGasPrice()
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(`gasPrices: ${JSON.stringify(fetcher.gasPrices)}`)
console.log(`netId: ${netId}`)
console.log(`ethPrices: ${JSON.stringify(fetcher.ethPrices)}`)
console.log(`Service fee: ${relayerServiceFee}%`)

8
src/instances.js Normal file
View File

@ -0,0 +1,8 @@
const Fetcher = require('./Fetcher')
const web3 = require('./setupWeb3')
const fetcher = new Fetcher(web3)
module.exports = {
fetcher,
web3
}

103
src/relayController.js Normal file
View File

@ -0,0 +1,103 @@
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 { web3, fetcher } = require('./instances')
async function relay (req, resp) {
const { proof, args, contract } = req.body
const gasPrices = fetcher.gasPrices
let { valid , reason } = isValidProof(proof)
if (!valid) {
console.log('Proof is invalid:', reason)
return resp.status(400).json({ error: 'Proof format is invalid' })
}
({ 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())
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' })
}
try {
const mixer = new web3.eth.Contract(mixerABI, req.body.contract)
const isSpent = await mixer.methods.isSpent(nullifierHash).call()
if (isSpent) {
return resp.status(400).json({ error: 'The note has been spent.' })
}
const isKnownRoot = await mixer.methods.isKnownRoot(root).call()
if (!isKnownRoot) {
return resp.status(400).json({ error: 'The merkle root is too old or invalid.' })
}
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, ethPrices, fee })
if (!isEnough) {
console.log(`Wrong fee: ${reason}`)
return resp.status(400).json({ error: reason })
}
const data = mixer.methods.withdraw(proof, ...args).encodeABI()
const tx = {
from: web3.eth.defaultAccount,
value: numberToHex(refund),
gas: numberToHex(gas),
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
to: mixer._address,
netId: config.netId,
data,
nonce: config.nonce
}
config.nonce++
let signedTx = await web3.eth.accounts.signTransaction(tx, config.privateKey)
let result = web3.eth.sendSignedTransaction(signedTx.rawTransaction)
result.once('transactionHash', function(txHash){
resp.json({ txHash })
console.log(`A new successfully sent tx ${txHash} for the ${recipient}`)
}).on('error', function(e){
config.nonce--
console.error('on transactionHash error', e.message)
return resp.status(400).json({ error: 'Proof is malformed.' })
})
} catch (e) {
console.error(e, 'estimate gas failed')
return resp.status(400).json({ error: 'Proof is malformed or spent.' })
}
}
module.exports = relay

16
src/setupWeb3.js Normal file
View 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

105
src/utils.js Normal file
View File

@ -0,0 +1,105 @@
const { isHexStrict, toBN, toWei } = 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 isEnoughFee({ gas, gasPrices, currency, amount, refund, ethPrices, fee }) {
// TODO tokens can have less then 18 decimals
const feePercent = toBN(toWei(amount)).mul(toBN(relayerServiceFee * 10)).div(toBN('1000'))
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(gas))
let desiredFee
switch (currency) {
case 'eth': {
desiredFee = expense.add(feePercent)
break
}
case 'dai': {
desiredFee =
expense.add(refund)
.mul(toBN(10 ** 18))
.div(toBN(ethPrices.dai))
desiredFee = desiredFee.add(feePercent)
break
}
}
console.log('desired fee, feePercent', desiredFee.toString(), feePercent.toString())
if (fee.lt(desiredFee)) {
return { isEnough: false, reason: 'Not enough fee' }
}
return { isEnough: true }
}
function getMainnetTokens() {
const tokens = mixers['netId1']
const tokenAddresses = []
const currencyLookup = {}
Object.entries(tokens).map(([currency, data]) => {
if (currency !== 'eth') {
tokenAddresses.push(data.tokenAddress)
currencyLookup[data.tokenAddress] = currency
}
})
return { tokenAddresses, currencyLookup }
}
function getMixers() {
return mixers[`netId${netId}`]
}
module.exports = { isValidProof, isValidArgs, sleep, isKnownContract, isEnoughFee, getMixers, getMainnetTokens }

View File

@ -1,85 +0,0 @@
const fetch = require('node-fetch')
const { isHexStrict } = require('web3-utils')
const { gasOracleUrls } = require('./config')
async function fetchGasPrice({ gasPrices, oracleIndex = 0 }) {
oracleIndex = (oracleIndex + 1) % gasOracleUrls.length
try {
const response = await fetch(gasOracleUrls[oracleIndex])
if (response.status === 200) {
const json = await response.json()
if (json.slow) {
gasPrices.low = Number(json.slow)
}
if (json.safeLow) {
gasPrices.low = Number(json.safeLow)
}
if (json.standard) {
gasPrices.standard = Number(json.standard)
}
if (json.fast) {
gasPrices.fast = Number(json.fast)
}
} else {
throw Error('Fetch gasPrice failed')
}
setTimeout(() => fetchGasPrice({ gasPrices, oracleIndex }), 15000)
} catch (e) {
setTimeout(() => fetchGasPrice({ gasPrices, oracleIndex }), 15000)
}
}
function isValidProof(proof) {
// validator expects `websnarkUtils.toSolidityInput(proof)` output
if (!(proof.pi_a && proof.pi_b && proof.pi_c && proof.publicSignals)) {
return { valid: false, reason: 'One of inputs is empty. There must be pi_a, pi_b, pi_c and publicSignals' }
}
Object.keys(proof).forEach(key => {
if (!Array.isArray(proof[key])) {
return { valid: false, reason: `Corrupted ${key}` }
}
if (key === 'pi_b') {
if (!Array.isArray(proof[key][0]) || !Array.isArray(proof[key][1])) {
return { valid: false, reason: `Corrupted ${key}` }
}
}
})
if (proof.pi_a.length !== 2) {
return { valid: false, reason: 'Corrupted pi_a' }
}
if (proof.pi_b.length !== 2 || proof.pi_b[0].length !== 2 || proof.pi_b[1].length !== 2) {
return { valid: false, reason: 'Corrupted pi_b' }
}
if (proof.pi_c.length !== 2) {
return { valid: false, reason: 'Corrupted pi_c' }
}
if (proof.publicSignals.length !== 4) {
return { valid: false, reason: 'Corrupted publicSignals' }
}
for (let [key, input] of Object.entries(proof)) {
if (key === 'pi_b') {
input = input[0].concat(input[1])
}
for (let i = 0; i < input.length; i++ ) {
if (!isHexStrict(input[i]) || input[i].length !== 66) {
return { valid: false, reason: `Corrupted ${key}` }
}
}
}
return { valid: true }
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
module.exports = { fetchGasPrice, isValidProof, sleep }

3233
yarn.lock Normal file

File diff suppressed because it is too large Load Diff