From c16164876e772850b8e5f185f0d0ef4b9ece260e Mon Sep 17 00:00:00 2001 From: Alexey Date: Wed, 30 Sep 2020 18:35:48 +0300 Subject: [PATCH] works for regular tornado. dirty and WIP though --- config.js | 9 +++- src/controller.js | 2 +- src/queue.js | 3 +- src/server.js | 16 +++--- src/status.js | 31 ++++++----- src/validator.js | 65 +++++++++++++---------- src/worker.js | 132 +++++++++++++--------------------------------- 7 files changed, 114 insertions(+), 144 deletions(-) diff --git a/config.js b/config.js index 7543f0b..3b32a15 100644 --- a/config.js +++ b/config.js @@ -1,6 +1,11 @@ require('dotenv').config() -module.exports = { +function updateConfig(options) { + config = Object.assign(config, options) +} + +let config = { + updateConfig, netId: Number(process.env.NET_ID) || 42, redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379', rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/', @@ -154,3 +159,5 @@ module.exports = { gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20, rewardAccount: '0x0000000000000000000000000000000000000000', } + +module.exports = config diff --git a/src/controller.js b/src/controller.js index 025b9aa..3f34463 100644 --- a/src/controller.js +++ b/src/controller.js @@ -10,7 +10,7 @@ async function tornadoWithdraw(req, res) { const { proof, args, contract } = req.body const id = await postJob({ - type: 'withdraw', + type: 'tornadoWithdraw', data: { proof, args, contract }, }) return res.json({ id }) diff --git a/src/queue.js b/src/queue.js index bf82abb..cc44ecc 100644 --- a/src/queue.js +++ b/src/queue.js @@ -6,11 +6,10 @@ const redis = new Redis(redisUrl) const queue = new Queue('proofs', redisUrl) -async function postJob(type, data) { +async function postJob({ type, data }) { const id = uuid() const job = await queue.add( - 'proofs', { id, type, diff --git a/src/server.js b/src/server.js index c610169..4b3ef25 100644 --- a/src/server.js +++ b/src/server.js @@ -1,8 +1,9 @@ const express = require('express') -const status = require('status') -const controller = require('controller') +const status = require('./status') +const controller = require('./controller') const { port } = require('../config') const { version } = require('../package.json') +const worker = require('./worker') const app = express() app.use(express.json()) @@ -27,8 +28,11 @@ app.get('/', status.index) app.get('/v1/status', status.status) app.post('/v1/jobs/:id', status.getJob) app.post('/v1/tornadoWithdraw', controller.tornadoWithdraw) -app.post('/v1/miningReward', controller.miningReward) -app.post('/v1/miningWithdraw', controller.miningWithdraw) +app.get('/status', status.status) +app.post('/relay', controller.tornadoWithdraw) +// app.post('/v1/miningReward', controller.miningReward) +// app.post('/v1/miningWithdraw', controller.miningWithdraw) -console.log('Version:', version) -app.listen(port) +worker.start() +app.listen(port || 8000) +console.log(`Relayer ${version} started on port ${port || 8000}`) diff --git a/src/status.js b/src/status.js index 47115db..73bbf08 100644 --- a/src/status.js +++ b/src/status.js @@ -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) { - 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 = { + dai: '6700000000000000', // 0.0067 + cdai: '157380000000000', + cusdc: '164630000000000', + usdc: '7878580000000000', + usdt: '7864940000000000', } - const { ethPrices } = fetcher res.json({ - relayerAddress: web3.eth.defaultAccount, - mixers, + relayerAddress: require('../config').rewardAccount, + instances: instances.netId42, gasPrices: await gasPriceOracle.gasPrices(), netId, ethPrices, relayerServiceFee, - nonce, + nonce: 123, version, - latestBlock + latestBlock: 12312312, }) } function index(req, res) { - res.send('This is tornado.cash Relayer service. Check the /status for settings') + res.send( + 'This is tornado.cash Relayer service. Check the /status for settings', + ) } async function getJob(req, res) { diff --git a/src/validator.js b/src/validator.js index 949ad7c..7d5353d 100644 --- a/src/validator.js +++ b/src/validator.js @@ -1,6 +1,5 @@ -const { isAddress } = require('web3-utils') +const { isAddress, toChecksumAddress } = require('web3-utils') const { getInstance } = require('./utils') -const { rewardAccount } = require('../config') const Ajv = require('ajv') const ajv = new Ajv({ format: 'fast' }) @@ -13,7 +12,7 @@ ajv.addKeyword('isAddress', { return false } }, - errors: true + errors: true, }) ajv.addKeyword('isKnownContract', { @@ -24,18 +23,18 @@ ajv.addKeyword('isKnownContract', { return false } }, - errors: true + errors: true, }) ajv.addKeyword('isFeeRecipient', { validate: (schema, data) => { try { - return rewardAccount === data + return require('../config').rewardAccount === toChecksumAddress(data) } catch (e) { return false } }, - errors: true + errors: true, }) const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$', isAddress: true } @@ -54,11 +53,11 @@ const tornadoWithdrawSchema = { type: 'array', maxItems: 6, minItems: 6, - items: [bytes32Type, bytes32Type, addressType, relayerType, bytes32Type, bytes32Type] - } + items: [bytes32Type, bytes32Type, addressType, relayerType, bytes32Type, bytes32Type], + }, }, additionalProperties: false, - required: ['proof', 'contract', 'args'] + required: ['proof', 'contract', 'args'], } const miningRewardSchema = { @@ -79,10 +78,10 @@ const miningRewardSchema = { type: 'object', properties: { relayer: relayerType, - encryptedAccount: encryptedAccountType + encryptedAccount: encryptedAccountType, }, additionalProperties: false, - required: ['relayer', 'encryptedAccount'] + required: ['relayer', 'encryptedAccount'], }, account: { type: 'object', @@ -91,11 +90,17 @@ const miningRewardSchema = { inputNullifierHash: bytes32Type, outputRoot: bytes32Type, outputPathIndices: bytes32Type, - outputCommitment: bytes32Type + outputCommitment: bytes32Type, }, additionalProperties: false, - required: ['inputRoot', 'inputNullifierHash', 'outputRoot', 'outputPathIndices', 'outputCommitment'] - } + required: [ + 'inputRoot', + 'inputNullifierHash', + 'outputRoot', + 'outputPathIndices', + 'outputCommitment', + ], + }, }, additionalProperties: false, required: [ @@ -107,12 +112,12 @@ const miningRewardSchema = { 'depositRoot', 'withdrawalRoot', 'extData', - 'account' - ] - } + 'account', + ], + }, }, additionalProperties: false, - required: ['proof', 'args'] + required: ['proof', 'args'], } const miningWithdrawSchema = { @@ -130,10 +135,10 @@ const miningWithdrawSchema = { properties: { recipient: addressType, relayer: relayerType, - encryptedAccount: encryptedAccountType + encryptedAccount: encryptedAccountType, }, additionalProperties: false, - required: ['relayer', 'encryptedAccount', 'recipient'] + required: ['relayer', 'encryptedAccount', 'recipient'], }, account: { type: 'object', @@ -142,18 +147,24 @@ const miningWithdrawSchema = { inputNullifierHash: bytes32Type, outputRoot: bytes32Type, outputPathIndices: bytes32Type, - outputCommitment: bytes32Type + outputCommitment: bytes32Type, }, additionalProperties: false, - required: ['inputRoot', 'inputNullifierHash', 'outputRoot', 'outputPathIndices', 'outputCommitment'] - } + required: [ + 'inputRoot', + 'inputNullifierHash', + 'outputRoot', + 'outputPathIndices', + 'outputCommitment', + ], + }, }, additionalProperties: false, - required: ['amount', 'fee', 'extDataHash', 'extData', 'account'] - } + required: ['amount', 'fee', 'extDataHash', 'extData', 'account'], + }, }, additionalProperties: false, - required: ['proof', 'args'] + required: ['proof', 'args'], } const validateTornadoWithdraw = ajv.compile(tornadoWithdrawSchema) @@ -184,5 +195,5 @@ function getMiningWithdrawInputError(data) { module.exports = { getTornadoWithdrawInputError, getMiningRewardInputError, - getMiningWithdrawInputError + getMiningWithdrawInputError, } diff --git a/src/worker.js b/src/worker.js index 6914ee9..861b96e 100644 --- a/src/worker.js +++ b/src/worker.js @@ -1,27 +1,27 @@ -const { queue } = require('./queue') const Web3 = require('web3') -const { rpcUrl, redisUrl, privateKey, netId, gasBumpInterval, gasBumpPercentage, maxGasPrice } = require('../config') -const { numberToHex, toWei, toHex, toBN, fromWei, toChecksumAddress, BN } = require('web3-utils') -const tornadoABI = require('../abis/tornadoABI.json') +const { numberToHex, toBN } = require('web3-utils') const MerkleTree = require('fixed-merkle-tree') -const { setSafeInterval, poseidonHash2 } = require('./utils') const Redis = require('ioredis') -const redis = new Redis(redisUrl) -const redisSubscribe = new Redis(redisUrl) const { GasPriceOracle } = require('gas-price-oracle') -const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl }) -queue.process(process) -redisSubscribe.subscribe('treeUpdate', fetchTree) + +const tornadoABI = require('../abis/tornadoABI.json') +const { queue } = require('./queue') +const { poseidonHash2 } = require('./utils') +const { rpcUrl, redisUrl, privateKey, updateConfig, rewardAccount } = require('../config') +const TxManager = require('./TxManager') let web3 -let nonce let currentTx let currentJob let tree +let txManager +const redis = new Redis(redisUrl) +const redisSubscribe = new Redis(redisUrl) +const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl }) async function fetchTree() { 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) if (currentTx) { @@ -29,75 +29,55 @@ async function fetchTree() { } } -async function watcher() { - 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() { +async function start() { 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 - 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() - 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) { - if (job.type !== 'tornadoWithdraw') { + if (job.data.type !== 'tornadoWithdraw') { throw new Error('not implemented') } currentJob = job 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 refund = toBN(args[5]) await checkTornadoFee(contract, fee, refund) - const instance = new web3.eth.Contract(tornadoABI, contract) const data = instance.methods.withdraw(proof, ...args).encodeABI() - const gasPrices = await gasPriceOracle.gasPrices() - currentTx = { - from: web3.eth.defaultAccount, + currentTx = await txManager.createTx({ value: numberToHex(refund), - gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')), to: contract, - netId, data, - nonce, - } + }) try { - // eslint-disable-next-line require-atomic-updates - currentTx.gas = await web3.eth.estimateGas(currentTx) - } - catch (e) { + await currentTx + .send() + .on('transactionHash', updateTxHash) + .on('mined', (receipt) => { + console.log('Mined in block', receipt.blockNumber) + }) + .on('confirmations', updateConfirmations) + } catch (e) { console.error('Revert', e) throw new Error(`Revert by smart contract ${e.message}`) } - - nonce++ - await sendTx(currentTx, updateTxHash) -} - -async function waitForTx(hash) { - } async function updateTxHash(txHash) { @@ -106,46 +86,10 @@ async function updateTxHash(txHash) { await currentJob.update(currentJob.data) } -async function sendTx(tx, onTxHash, retryAttempt) { - let signedTx = await this.web3.eth.accounts.signTransaction(tx, privateKey) - let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction) - - 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) - } +async function updateConfirmations(confirmations) { + console.log(`Confirmations count ${confirmations}`) + currentJob.data.confirmations = confirmations + await currentJob.update(currentJob.data) } -const nonceErrors = [ - '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() +module.exports = { start, process }