works for regular tornado. dirty and WIP though

This commit is contained in:
Alexey 2020-09-30 18:35:48 +03:00
parent d888fdbd44
commit c16164876e
7 changed files with 114 additions and 144 deletions

@ -1,6 +1,11 @@
require('dotenv').config() require('dotenv').config()
module.exports = { function updateConfig(options) {
config = Object.assign(config, options)
}
let config = {
updateConfig,
netId: Number(process.env.NET_ID) || 42, netId: Number(process.env.NET_ID) || 42,
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379', redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/', rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/',
@ -154,3 +159,5 @@ module.exports = {
gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20, gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20,
rewardAccount: '0x0000000000000000000000000000000000000000', rewardAccount: '0x0000000000000000000000000000000000000000',
} }
module.exports = config

@ -10,7 +10,7 @@ async function tornadoWithdraw(req, res) {
const { proof, args, contract } = req.body const { proof, args, contract } = req.body
const id = await postJob({ const id = await postJob({
type: 'withdraw', type: 'tornadoWithdraw',
data: { proof, args, contract }, data: { proof, args, contract },
}) })
return res.json({ id }) return res.json({ id })

@ -6,11 +6,10 @@ const redis = new Redis(redisUrl)
const queue = new Queue('proofs', redisUrl) const queue = new Queue('proofs', redisUrl)
async function postJob(type, data) { async function postJob({ type, data }) {
const id = uuid() const id = uuid()
const job = await queue.add( const job = await queue.add(
'proofs',
{ {
id, id,
type, type,

@ -1,8 +1,9 @@
const express = require('express') const express = require('express')
const status = require('status') const status = require('./status')
const controller = require('controller') const controller = require('./controller')
const { port } = require('../config') const { port } = require('../config')
const { version } = require('../package.json') const { version } = require('../package.json')
const worker = require('./worker')
const app = express() const app = express()
app.use(express.json()) app.use(express.json())
@ -27,8 +28,11 @@ app.get('/', status.index)
app.get('/v1/status', status.status) app.get('/v1/status', status.status)
app.post('/v1/jobs/:id', status.getJob) app.post('/v1/jobs/:id', status.getJob)
app.post('/v1/tornadoWithdraw', controller.tornadoWithdraw) app.post('/v1/tornadoWithdraw', controller.tornadoWithdraw)
app.post('/v1/miningReward', controller.miningReward) app.get('/status', status.status)
app.post('/v1/miningWithdraw', controller.miningWithdraw) app.post('/relay', controller.tornadoWithdraw)
// app.post('/v1/miningReward', controller.miningReward)
// app.post('/v1/miningWithdraw', controller.miningWithdraw)
console.log('Version:', version) worker.start()
app.listen(port) app.listen(port || 8000)
console.log(`Relayer ${version} started on port ${port || 8000}`)

@ -1,29 +1,34 @@
const queue = require('queue') const queue = require('./queue')
const { GasPriceOracle } = require('gas-price-oracle')
const gasPriceOracle = new GasPriceOracle()
const { netId, relayerServiceFee, instances } = require('../config')
const { version } = require('../package.json')
async function status(req, res) { async function status(req, res) {
let nonce = await redisClient.get('nonce') const ethPrices = {
let latestBlock = null dai: '6700000000000000', // 0.0067
try { cdai: '157380000000000',
latestBlock = await web3.eth.getBlockNumber() cusdc: '164630000000000',
} catch (e) { usdc: '7878580000000000',
console.error('Problem with RPC', e) usdt: '7864940000000000',
} }
const { ethPrices } = fetcher
res.json({ res.json({
relayerAddress: web3.eth.defaultAccount, relayerAddress: require('../config').rewardAccount,
mixers, instances: instances.netId42,
gasPrices: await gasPriceOracle.gasPrices(), gasPrices: await gasPriceOracle.gasPrices(),
netId, netId,
ethPrices, ethPrices,
relayerServiceFee, relayerServiceFee,
nonce, nonce: 123,
version, version,
latestBlock latestBlock: 12312312,
}) })
} }
function index(req, res) { function index(req, res) {
res.send('This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/v1/status>/status</a> for settings') res.send(
'This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/v1/status>/status</a> for settings',
)
} }
async function getJob(req, res) { async function getJob(req, res) {

@ -1,6 +1,5 @@
const { isAddress } = require('web3-utils') const { isAddress, toChecksumAddress } = require('web3-utils')
const { getInstance } = require('./utils') const { getInstance } = require('./utils')
const { rewardAccount } = require('../config')
const Ajv = require('ajv') const Ajv = require('ajv')
const ajv = new Ajv({ format: 'fast' }) const ajv = new Ajv({ format: 'fast' })
@ -13,7 +12,7 @@ ajv.addKeyword('isAddress', {
return false return false
} }
}, },
errors: true errors: true,
}) })
ajv.addKeyword('isKnownContract', { ajv.addKeyword('isKnownContract', {
@ -24,18 +23,18 @@ ajv.addKeyword('isKnownContract', {
return false return false
} }
}, },
errors: true errors: true,
}) })
ajv.addKeyword('isFeeRecipient', { ajv.addKeyword('isFeeRecipient', {
validate: (schema, data) => { validate: (schema, data) => {
try { try {
return rewardAccount === data return require('../config').rewardAccount === toChecksumAddress(data)
} catch (e) { } catch (e) {
return false return false
} }
}, },
errors: true errors: true,
}) })
const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$', isAddress: true } const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$', isAddress: true }
@ -54,11 +53,11 @@ const tornadoWithdrawSchema = {
type: 'array', type: 'array',
maxItems: 6, maxItems: 6,
minItems: 6, minItems: 6,
items: [bytes32Type, bytes32Type, addressType, relayerType, bytes32Type, bytes32Type] items: [bytes32Type, bytes32Type, addressType, relayerType, bytes32Type, bytes32Type],
} },
}, },
additionalProperties: false, additionalProperties: false,
required: ['proof', 'contract', 'args'] required: ['proof', 'contract', 'args'],
} }
const miningRewardSchema = { const miningRewardSchema = {
@ -79,10 +78,10 @@ const miningRewardSchema = {
type: 'object', type: 'object',
properties: { properties: {
relayer: relayerType, relayer: relayerType,
encryptedAccount: encryptedAccountType encryptedAccount: encryptedAccountType,
}, },
additionalProperties: false, additionalProperties: false,
required: ['relayer', 'encryptedAccount'] required: ['relayer', 'encryptedAccount'],
}, },
account: { account: {
type: 'object', type: 'object',
@ -91,11 +90,17 @@ const miningRewardSchema = {
inputNullifierHash: bytes32Type, inputNullifierHash: bytes32Type,
outputRoot: bytes32Type, outputRoot: bytes32Type,
outputPathIndices: bytes32Type, outputPathIndices: bytes32Type,
outputCommitment: bytes32Type outputCommitment: bytes32Type,
}, },
additionalProperties: false, additionalProperties: false,
required: ['inputRoot', 'inputNullifierHash', 'outputRoot', 'outputPathIndices', 'outputCommitment'] required: [
} 'inputRoot',
'inputNullifierHash',
'outputRoot',
'outputPathIndices',
'outputCommitment',
],
},
}, },
additionalProperties: false, additionalProperties: false,
required: [ required: [
@ -107,12 +112,12 @@ const miningRewardSchema = {
'depositRoot', 'depositRoot',
'withdrawalRoot', 'withdrawalRoot',
'extData', 'extData',
'account' 'account',
] ],
} },
}, },
additionalProperties: false, additionalProperties: false,
required: ['proof', 'args'] required: ['proof', 'args'],
} }
const miningWithdrawSchema = { const miningWithdrawSchema = {
@ -130,10 +135,10 @@ const miningWithdrawSchema = {
properties: { properties: {
recipient: addressType, recipient: addressType,
relayer: relayerType, relayer: relayerType,
encryptedAccount: encryptedAccountType encryptedAccount: encryptedAccountType,
}, },
additionalProperties: false, additionalProperties: false,
required: ['relayer', 'encryptedAccount', 'recipient'] required: ['relayer', 'encryptedAccount', 'recipient'],
}, },
account: { account: {
type: 'object', type: 'object',
@ -142,18 +147,24 @@ const miningWithdrawSchema = {
inputNullifierHash: bytes32Type, inputNullifierHash: bytes32Type,
outputRoot: bytes32Type, outputRoot: bytes32Type,
outputPathIndices: bytes32Type, outputPathIndices: bytes32Type,
outputCommitment: bytes32Type outputCommitment: bytes32Type,
}, },
additionalProperties: false, additionalProperties: false,
required: ['inputRoot', 'inputNullifierHash', 'outputRoot', 'outputPathIndices', 'outputCommitment'] required: [
} 'inputRoot',
'inputNullifierHash',
'outputRoot',
'outputPathIndices',
'outputCommitment',
],
},
}, },
additionalProperties: false, additionalProperties: false,
required: ['amount', 'fee', 'extDataHash', 'extData', 'account'] required: ['amount', 'fee', 'extDataHash', 'extData', 'account'],
} },
}, },
additionalProperties: false, additionalProperties: false,
required: ['proof', 'args'] required: ['proof', 'args'],
} }
const validateTornadoWithdraw = ajv.compile(tornadoWithdrawSchema) const validateTornadoWithdraw = ajv.compile(tornadoWithdrawSchema)
@ -184,5 +195,5 @@ function getMiningWithdrawInputError(data) {
module.exports = { module.exports = {
getTornadoWithdrawInputError, getTornadoWithdrawInputError,
getMiningRewardInputError, getMiningRewardInputError,
getMiningWithdrawInputError getMiningWithdrawInputError,
} }

@ -1,27 +1,27 @@
const { queue } = require('./queue')
const Web3 = require('web3') const Web3 = require('web3')
const { rpcUrl, redisUrl, privateKey, netId, gasBumpInterval, gasBumpPercentage, maxGasPrice } = require('../config') const { numberToHex, toBN } = require('web3-utils')
const { numberToHex, toWei, toHex, toBN, fromWei, toChecksumAddress, BN } = require('web3-utils')
const tornadoABI = require('../abis/tornadoABI.json')
const MerkleTree = require('fixed-merkle-tree') const MerkleTree = require('fixed-merkle-tree')
const { setSafeInterval, poseidonHash2 } = require('./utils')
const Redis = require('ioredis') const Redis = require('ioredis')
const redis = new Redis(redisUrl)
const redisSubscribe = new Redis(redisUrl)
const { GasPriceOracle } = require('gas-price-oracle') const { GasPriceOracle } = require('gas-price-oracle')
const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
queue.process(process) const tornadoABI = require('../abis/tornadoABI.json')
redisSubscribe.subscribe('treeUpdate', fetchTree) const { queue } = require('./queue')
const { poseidonHash2 } = require('./utils')
const { rpcUrl, redisUrl, privateKey, updateConfig, rewardAccount } = require('../config')
const TxManager = require('./TxManager')
let web3 let web3
let nonce
let currentTx let currentTx
let currentJob let currentJob
let tree let tree
let txManager
const redis = new Redis(redisUrl)
const redisSubscribe = new Redis(redisUrl)
const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
async function fetchTree() { async function fetchTree() {
const elements = await redis.get('tree:elements') const elements = await redis.get('tree:elements')
const convert = (_, val) => typeof(val) === 'string' ? toBN(val) : val const convert = (_, val) => (typeof val === 'string' ? toBN(val) : val)
tree = MerkleTree.deserialize(JSON.parse(elements, convert), poseidonHash2) tree = MerkleTree.deserialize(JSON.parse(elements, convert), poseidonHash2)
if (currentTx) { if (currentTx) {
@ -29,75 +29,55 @@ async function fetchTree() {
} }
} }
async function watcher() { async function start() {
if (currentTx && Date.now() - currentTx.date > gasBumpInterval) {
bumpGasPrice()
}
}
async function bumpGasPrice() {
const newGasPrice = toBN(currentTx.gasPrice).mul(toBN(gasBumpPercentage)).div(toBN(100))
const maxGasPrice = toBN(toWei(maxGasPrice.toString(), 'Gwei'))
currentTx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
currentTx.date = Date.now()
console.log(`Resubmitting with gas price ${fromWei(currentTx.gasPrice.toString(), 'gwei')} gwei`)
await sendTx(currentTx, updateTxHash)
}
async function init() {
web3 = new Web3(rpcUrl, null, { transactionConfirmationBlocks: 1 }) web3 = new Web3(rpcUrl, null, { transactionConfirmationBlocks: 1 })
const account = web3.eth.accounts.privateKeyToAccount('0x' + privateKey) const account = web3.eth.accounts.privateKeyToAccount('0x' + privateKey)
web3.eth.accounts.wallet.add('0x' + privateKey) web3.eth.accounts.wallet.add('0x' + privateKey)
web3.eth.defaultAccount = account.address web3.eth.defaultAccount = account.address
nonce = await web3.eth.getTransactionCount(account.address, 'latest') updateConfig({ rewardAccount: account.address })
txManager = new TxManager({ privateKey, rpcUrl })
queue.process(process)
redisSubscribe.subscribe('treeUpdate', fetchTree)
await fetchTree() await fetchTree()
setSafeInterval(watcher, 1000) console.log('Worker started')
} }
async function checkTornadoFee(contract, fee, refund) { async function checkTornadoFee(/* contract, fee, refund*/) {
const { fast } = await gasPriceOracle.gasPrices()
console.log('fast', fast)
} }
async function process(job) { async function process(job) {
if (job.type !== 'tornadoWithdraw') { if (job.data.type !== 'tornadoWithdraw') {
throw new Error('not implemented') throw new Error('not implemented')
} }
currentJob = job currentJob = job
console.log(Date.now(), ' withdraw started', job.id) console.log(Date.now(), ' withdraw started', job.id)
const { proof, args, contract } = job.data const { proof, args, contract } = job.data.data
const fee = toBN(args[4]) const fee = toBN(args[4])
const refund = toBN(args[5]) const refund = toBN(args[5])
await checkTornadoFee(contract, fee, refund) await checkTornadoFee(contract, fee, refund)
const instance = new web3.eth.Contract(tornadoABI, contract) const instance = new web3.eth.Contract(tornadoABI, contract)
const data = instance.methods.withdraw(proof, ...args).encodeABI() const data = instance.methods.withdraw(proof, ...args).encodeABI()
const gasPrices = await gasPriceOracle.gasPrices() currentTx = await txManager.createTx({
currentTx = {
from: web3.eth.defaultAccount,
value: numberToHex(refund), value: numberToHex(refund),
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
to: contract, to: contract,
netId,
data, data,
nonce, })
}
try { try {
// eslint-disable-next-line require-atomic-updates await currentTx
currentTx.gas = await web3.eth.estimateGas(currentTx) .send()
} .on('transactionHash', updateTxHash)
catch (e) { .on('mined', (receipt) => {
console.log('Mined in block', receipt.blockNumber)
})
.on('confirmations', updateConfirmations)
} catch (e) {
console.error('Revert', e) console.error('Revert', e)
throw new Error(`Revert by smart contract ${e.message}`) throw new Error(`Revert by smart contract ${e.message}`)
} }
nonce++
await sendTx(currentTx, updateTxHash)
}
async function waitForTx(hash) {
} }
async function updateTxHash(txHash) { async function updateTxHash(txHash) {
@ -106,46 +86,10 @@ async function updateTxHash(txHash) {
await currentJob.update(currentJob.data) await currentJob.update(currentJob.data)
} }
async function sendTx(tx, onTxHash, retryAttempt) { async function updateConfirmations(confirmations) {
let signedTx = await this.web3.eth.accounts.signTransaction(tx, privateKey) console.log(`Confirmations count ${confirmations}`)
let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction) currentJob.data.confirmations = confirmations
await currentJob.update(currentJob.data)
if (onTxHash) {
result.once('transactionHash', onTxHash)
}
try { // await returns once tx is mined
await result
} catch (e) {
console.log(`Error for tx with nonce ${tx.nonce}\n${e.message}`)
if (nonceErrors.includes(e.message)) {
console.log('nonce too low, retrying')
if (retryAttempt <= 10) {
tx.nonce++
return sendTx(tx, onTxHash, retryAttempt + 1)
}
}
if (gasPriceErrors.includes(e.message)) {
return bumpGasPrice()
}
throw new Error(e)
}
} }
const nonceErrors = [ module.exports = { start, process }
'Returned error: Transaction nonce is too low. Try incrementing the nonce.',
'Returned error: nonce too low',
]
const gasPriceErrors = [
'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.',
'Returned error: replacement transaction underpriced',
]
async function main() {
await init()
}
// main()
fetchTree()