bit of refactor, add RelayerError class

This commit is contained in:
smart_ex 2022-03-24 01:35:41 +10:00 committed by Danil Kovtonyuk
parent 76209e11c0
commit 50054e0516
16 changed files with 164 additions and 114 deletions

View File

@ -2,9 +2,9 @@ const {
getTornadoWithdrawInputError, getTornadoWithdrawInputError,
getMiningRewardInputError, getMiningRewardInputError,
getMiningWithdrawInputError, getMiningWithdrawInputError,
} = require('./validator') } = require('../modules/validator')
const { postJob } = require('./queue') const { postJob } = require('../queue')
const { jobType } = require('./constants') const { jobType } = require('../constants')
async function tornadoWithdraw(req, res) { async function tornadoWithdraw(req, res) {
const inputError = getTornadoWithdrawInputError(req.body) const inputError = getTornadoWithdrawInputError(req.body)

4
src/contollers/index.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
controller: require('./controller'),
status: require('./status'),
}

View File

@ -1,8 +1,7 @@
const queue = require('./queue') const queue = require('../queue')
const { netId, tornadoServiceFee, miningServiceFee, instances, redisUrl, rewardAccount } = require('./config') const { netId, tornadoServiceFee, miningServiceFee, instances, rewardAccount } = require('../config')
const { version } = require('../package.json') const { version } = require('../../package.json')
const Redis = require('ioredis') const { redis } = require('../modules/redis')
const redis = new Redis(redisUrl)
async function status(req, res) { async function status(req, res) {
const ethPrices = await redis.hgetall('prices') const ethPrices = await redis.hgetall('prices')

View File

@ -1,18 +1,18 @@
const Web3 = require('web3') const { setSafeInterval, toBN, fromWei } = require('./utils')
const Redis = require('ioredis') const { privateKey, minimumBalance } = require('./config')
const { toBN, fromWei } = require('web3-utils') const { redis } = require('./modules/redis')
const web3 = require('./modules/web3')()
const { setSafeInterval } = require('./utils')
const { redisUrl, httpRpcUrl, privateKey, minimumBalance } = require('./config')
const web3 = new Web3(httpRpcUrl)
const redis = new Redis(redisUrl)
async function main() { async function main() {
try { try {
const { address } = web3.eth.accounts.privateKeyToAccount(privateKey) const { address } = web3.eth.accounts.privateKeyToAccount(privateKey)
const balance = await web3.eth.getBalance(address) const balance = await web3.eth.getBalance(address)
const errors = await redis.zrevrange('errors', 0, -1)
if (errors.length > 3) {
console.log({ errors })
throw new Error('Too many errors on relayer')
}
if (toBN(balance).lt(toBN(minimumBalance))) { if (toBN(balance).lt(toBN(minimumBalance))) {
throw new Error(`Not enough balance, less than ${fromWei(minimumBalance)} ETH`) throw new Error(`Not enough balance, less than ${fromWei(minimumBalance)} ETH`)
} }

11
src/modules/redis.js Normal file
View File

@ -0,0 +1,11 @@
const Redis = require('ioredis')
const { redisUrl } = require('../config')
const redis = new Redis(redisUrl)
const redisSubscribe = new Redis(redisUrl)
module.exports = {
redis,
redisSubscribe,
redisUrl,
}

View File

@ -1,7 +1,7 @@
const { httpRpcUrl, aggregatorAddress } = require('./config') const { aggregatorAddress } = require('../config')
const Web3 = require('web3') const web3 = require('./web3')()
const web3 = new Web3(httpRpcUrl)
const aggregator = new web3.eth.Contract(require('../abis/Aggregator.abi.json'), aggregatorAddress) const aggregator = new web3.eth.Contract(require('../../abis/Aggregator.abi.json'), aggregatorAddress)
const ens = require('eth-ens-namehash') const ens = require('eth-ens-namehash')
class ENSResolver { class ENSResolver {
@ -26,5 +26,4 @@ class ENSResolver {
return addresses.length === 1 ? addresses[0] : addresses return addresses.length === 1 ? addresses[0] : addresses
} }
} }
module.exports = new ENSResolver()
module.exports = ENSResolver

View File

@ -1,6 +1,6 @@
const { isAddress, toChecksumAddress } = require('web3-utils') const { isAddress, toChecksumAddress } = require('web3-utils')
const { getInstance } = require('./utils') const { getInstance } = require('../utils')
const { rewardAccount } = require('./config') const { rewardAccount } = require('../config')
const Ajv = require('ajv') const Ajv = require('ajv')
const ajv = new Ajv({ format: 'fast' }) const ajv = new Ajv({ format: 'fast' })

30
src/modules/web3.js Normal file
View File

@ -0,0 +1,30 @@
const Web3 = require('web3')
const { oracleRpcUrl, httpRpcUrl, wsRpcUrl } = require('../config')
const getWeb3 = (type = 'http') => {
let url
switch (type) {
case 'oracle':
url = oracleRpcUrl
break
case 'ws':
url = wsRpcUrl
return new Web3(
new Web3.providers.WebsocketProvider(wsRpcUrl, {
clientConfig: {
maxReceivedFrameSize: 100000000,
maxReceivedMessageSize: 100000000,
},
}),
)
case 'http':
default:
url = httpRpcUrl
break
}
return new Web3(
new Web3.providers.HttpProvider(url, {
timeout: 200000, // ms
}),
)
}
module.exports = getWeb3

View File

@ -1,22 +1,13 @@
const Redis = require('ioredis') const { offchainOracleAddress } = require('./config')
const { redisUrl, offchainOracleAddress, oracleRpcUrl } = require('./config') const { getArgsForOracle, setSafeInterval, toChecksumAddress, toBN } = require('./utils')
const { getArgsForOracle, setSafeInterval } = require('./utils') const { redis } = require('./modules/redis')
const { toChecksumAddress } = require('web3-utils') const web3 = require('./modules/web3')()
const redis = new Redis(redisUrl)
const Web3 = require('web3')
const web3 = new Web3(
new Web3.providers.HttpProvider(oracleRpcUrl, {
timeout: 200000, // ms
}),
)
const offchainOracleABI = require('../abis/OffchainOracle.abi.json') const offchainOracleABI = require('../abis/OffchainOracle.abi.json')
const offchainOracle = new web3.eth.Contract(offchainOracleABI, offchainOracleAddress) const offchainOracle = new web3.eth.Contract(offchainOracleABI, offchainOracleAddress)
const { tokenAddresses, oneUintAmount, currencyLookup } = getArgsForOracle() const { tokenAddresses, oneUintAmount, currencyLookup } = getArgsForOracle()
const { toBN } = require('web3-utils')
async function main() { async function main() {
try { try {
const ethPrices = {} const ethPrices = {}
@ -40,6 +31,7 @@ async function main() {
await redis.hmset('prices', ethPrices) await redis.hmset('prices', ethPrices)
console.log('Wrote following prices to redis', ethPrices) console.log('Wrote following prices to redis', ethPrices)
} catch (e) { } catch (e) {
redis.zadd('errors', new Date().getTime(), e.message)
console.error('priceWatcher error', e) console.error('priceWatcher error', e)
} }
} }

View File

@ -1,9 +1,7 @@
const { v4: uuid } = require('uuid') const { v4: uuid } = require('uuid')
const Queue = require('bull') const Queue = require('bull')
const Redis = require('ioredis')
const { redisUrl } = require('./config')
const { status } = require('./constants') const { status } = require('./constants')
const redis = new Redis(redisUrl) const { redis, redisUrl } = require('./modules/redis')
const queue = new Queue('proofs', redisUrl, { const queue = new Queue('proofs', redisUrl, {
lockDuration: 300000, // Key expiration time for job locks. lockDuration: 300000, // Key expiration time for job locks.

29
src/router.js Normal file
View File

@ -0,0 +1,29 @@
const { controller, status } = require('./contollers')
const router = require('express').Router()
// Add CORS headers
router.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
next()
})
// Log error to console but don't send it to the client to avoid leaking data
router.use((err, req, res, next) => {
if (err) {
console.error(err)
return res.sendStatus(500)
}
next()
})
router.get('/', status.index)
router.get('/v1/status', status.status)
router.get('/v1/jobs/:id', status.getJob)
router.post('/v1/tornadoWithdraw', controller.tornadoWithdraw)
router.get('/status', status.status)
router.post('/relay', controller.tornadoWithdraw)
router.post('/v1/miningReward', controller.miningReward)
router.post('/v1/miningWithdraw', controller.miningWithdraw)
module.exports = router

View File

@ -1,41 +1,14 @@
const express = require('express') const express = require('express')
const status = require('./status')
const controller = require('./controller')
const { port, rewardAccount } = require('./config') const { port, rewardAccount } = require('./config')
const { version } = require('../package.json') const { version } = require('../package.json')
const { isAddress } = require('web3-utils') const { isAddress } = require('./utils')
const router = require('./router')
const app = express()
app.use(express.json())
// Add CORS headers
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
next()
})
// Log error to console but don't send it to the client to avoid leaking data
app.use((err, req, res, next) => {
if (err) {
console.error(err)
return res.sendStatus(500)
}
next()
})
app.get('/', status.index)
app.get('/v1/status', status.status)
app.get('/v1/jobs/:id', status.getJob)
app.post('/v1/tornadoWithdraw', controller.tornadoWithdraw)
app.get('/status', status.status)
app.post('/relay', controller.tornadoWithdraw)
app.post('/v1/miningReward', controller.miningReward)
app.post('/v1/miningWithdraw', controller.miningWithdraw)
if (!isAddress(rewardAccount)) { if (!isAddress(rewardAccount)) {
throw new Error('No REWARD_ACCOUNT specified') throw new Error('No REWARD_ACCOUNT specified')
} }
const app = express()
app.use(express.json())
app.use(router)
app.listen(port) app.listen(port)
console.log(`Relayer ${version} started on port ${port}`) console.log(`Relayer ${version} started on port ${port}`)

View File

@ -1,21 +1,10 @@
const MerkleTree = require('fixed-merkle-tree') const MerkleTree = require('fixed-merkle-tree')
const { redisUrl, wsRpcUrl, minerMerkleTreeHeight, torn, netId } = require('./config') const { minerMerkleTreeHeight, torn, netId } = require('./config')
const { poseidonHash2 } = require('./utils') const { poseidonHash2, toBN } = require('./utils')
const { toBN } = require('web3-utils') const resolver = require('./modules/resolver')
const Redis = require('ioredis') const web3 = require('./modules/web3')('ws')
const redis = new Redis(redisUrl)
const ENSResolver = require('./resolver')
const resolver = new ENSResolver()
const Web3 = require('web3')
const web3 = new Web3(
new Web3.providers.WebsocketProvider(wsRpcUrl, {
clientConfig: {
maxReceivedFrameSize: 100000000,
maxReceivedMessageSize: 100000000,
},
}),
)
const MinerABI = require('../abis/mining.abi.json') const MinerABI = require('../abis/mining.abi.json')
const { redis } = require('./modules/redis')
let contract let contract
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
@ -123,7 +112,7 @@ async function init() {
const newCommitments = newEvents const newCommitments = newEvents
.sort((a, b) => a.returnValues.index - b.returnValues.index) .sort((a, b) => a.returnValues.index - b.returnValues.index)
.map(e => toBN(e.returnValues.commitment)) .map(e => toBN(e.returnValues.commitment))
.filter((item, index, arr) => !index || item != arr[index - 1]) .filter((item, index, arr) => !index || item !== arr[index - 1])
const commitments = cachedCommitments.concat(newCommitments) const commitments = cachedCommitments.concat(newCommitments)
@ -134,6 +123,7 @@ async function init() {
eventSubscription = contract.events.NewAccount({ fromBlock: toBlock + 1 }, processNewEvent) eventSubscription = contract.events.NewAccount({ fromBlock: toBlock + 1 }, processNewEvent)
blockSubscription = web3.eth.subscribe('newBlockHeaders', processNewBlock) blockSubscription = web3.eth.subscribe('newBlockHeaders', processNewBlock)
} catch (e) { } catch (e) {
redis.zadd('errors', new Date().getTime(), e.message)
console.error('error on init treeWatcher', e.message) console.error('error on init treeWatcher', e.message)
} }
} }

View File

@ -1,6 +1,6 @@
const { instances, netId } = require('./config') const { instances, netId } = require('./config')
const { poseidon } = require('circomlib') const { poseidon } = require('circomlib')
const { toBN, toChecksumAddress, BN } = require('web3-utils') const { toBN, toChecksumAddress, BN, fromWei, isAddress, toWei } = require('web3-utils')
const TOKENS = { const TOKENS = {
torn: { torn: {
@ -118,6 +118,13 @@ function fromDecimals(value, decimals) {
return new BN(wei.toString(10), 10) return new BN(wei.toString(10), 10)
} }
class RelayerError extends Error {
constructor(message, score = 0) {
super(message)
this.score = score
}
}
module.exports = { module.exports = {
getInstance, getInstance,
setSafeInterval, setSafeInterval,
@ -126,4 +133,11 @@ module.exports = {
when, when,
getArgsForOracle, getArgsForOracle,
fromDecimals, fromDecimals,
toBN,
toChecksumAddress,
fromWei,
toWei,
BN,
isAddress,
RelayerError,
} }

View File

@ -1,8 +1,5 @@
const fs = require('fs') const fs = require('fs')
const Web3 = require('web3')
const { toBN, toWei, fromWei, toChecksumAddress } = require('web3-utils')
const MerkleTree = require('fixed-merkle-tree') const MerkleTree = require('fixed-merkle-tree')
const Redis = require('ioredis')
const { GasPriceOracle } = require('gas-price-oracle') const { GasPriceOracle } = require('gas-price-oracle')
const { Utils, Controller } = require('tornado-anonymity-mining') const { Utils, Controller } = require('tornado-anonymity-mining')
@ -10,14 +7,22 @@ const swapABI = require('../abis/swap.abi.json')
const miningABI = require('../abis/mining.abi.json') const miningABI = require('../abis/mining.abi.json')
const tornadoABI = require('../abis/tornadoABI.json') const tornadoABI = require('../abis/tornadoABI.json')
const tornadoProxyABI = require('../abis/tornadoProxyABI.json') const tornadoProxyABI = require('../abis/tornadoProxyABI.json')
const aggregatorAbi = require('../abis/Aggregator.abi.json')
const { queue } = require('./queue') const { queue } = require('./queue')
const { poseidonHash2, getInstance, fromDecimals, sleep } = require('./utils') const {
poseidonHash2,
getInstance,
fromDecimals,
sleep,
toBN,
toWei,
fromWei,
toChecksumAddress,
RelayerError,
} = require('./utils')
const { jobType, status } = require('./constants') const { jobType, status } = require('./constants')
const { const {
torn, torn,
netId, netId,
redisUrl,
gasLimits, gasLimits,
instances, instances,
privateKey, privateKey,
@ -28,9 +33,10 @@ const {
tornadoServiceFee, tornadoServiceFee,
tornadoGoerliProxy, tornadoGoerliProxy,
} = require('./config') } = require('./config')
const ENSResolver = require('./resolver') const resolver = require('./modules/resolver')
const resolver = new ENSResolver()
const { TxManager } = require('tx-manager') const { TxManager } = require('tx-manager')
const { redis, redisSubscribe } = require('./modules/redis')
const getWeb3 = require('./modules/web3')
let web3 let web3
let currentTx let currentTx
@ -40,8 +46,6 @@ let txManager
let controller let controller
let swap let swap
let minerContract let minerContract
const redis = new Redis(redisUrl)
const redisSubscribe = new Redis(redisUrl)
const gasPriceOracle = new GasPriceOracle({ defaultRpc: oracleRpcUrl }) const gasPriceOracle = new GasPriceOracle({ defaultRpc: oracleRpcUrl })
async function fetchTree() { async function fetchTree() {
@ -76,7 +80,8 @@ async function fetchTree() {
async function start() { async function start() {
try { try {
web3 = new Web3(httpRpcUrl) await clearErrors()
web3 = getWeb3()
const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env
txManager = new TxManager({ txManager = new TxManager({
privateKey, privateKey,
@ -101,6 +106,7 @@ async function start() {
queue.process(processJob) queue.process(processJob)
console.log('Worker started') console.log('Worker started')
} catch (e) { } catch (e) {
redis.zadd('errors', new Date().getTime(), e.message)
console.error('error on start worker', e.message) console.error('error on start worker', e.message)
} }
} }
@ -116,13 +122,11 @@ async function getGasPrice() {
const block = await web3.eth.getBlock('latest') const block = await web3.eth.getBlock('latest')
if (block && block.baseFeePerGas) { if (block && block.baseFeePerGas) {
const baseFeePerGas = toBN(block.baseFeePerGas) return toBN(block.baseFeePerGas)
return baseFeePerGas
} }
const { fast } = await gasPriceOracle.gasPrices() const { fast } = await gasPriceOracle.gasPrices()
const gasPrice = toBN(toWei(fast.toString(), 'gwei')) return toBN(toWei(fast.toString(), 'gwei'))
return gasPrice
} }
async function checkTornadoFee({ args, contract }) { async function checkTornadoFee({ args, contract }) {
@ -160,7 +164,10 @@ async function checkTornadoFee({ args, contract }) {
fromWei(feePercent.toString()), fromWei(feePercent.toString()),
) )
if (fee.lt(desiredFee)) { if (fee.lt(desiredFee)) {
throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.') throw new RelayerError(
'Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.',
0,
)
} }
} }
@ -196,7 +203,6 @@ async function checkMiningFee({ args }) {
} }
} }
async function getProxyContract() { async function getProxyContract() {
let proxyAddress let proxyAddress
if (netId === 5) { if (netId === 5) {
@ -262,7 +268,7 @@ async function isOutdatedTreeRevert(receipt, currentTx) {
async function processJob(job) { async function processJob(job) {
try { try {
if (!jobType[job.data.type]) { if (!jobType[job.data.type]) {
throw new Error(`Unknown job type: ${job.data.type}`) throw new RelayerError(`Unknown job type: ${job.data.type}`)
} }
currentJob = job currentJob = job
await updateStatus(status.ACCEPTED) await updateStatus(status.ACCEPTED)
@ -304,10 +310,10 @@ async function submitTx(job, retry = 0) {
await updateStatus(status.RESUBMITTED) await updateStatus(status.RESUBMITTED)
await submitTx(job, retry + 1) await submitTx(job, retry + 1)
} else { } else {
throw new Error('Tree update retry limit exceeded') throw new RelayerError('Tree update retry limit exceeded')
} }
} else { } else {
throw new Error('Submitted transaction failed') throw new RelayerError('Submitted transaction failed')
} }
} }
} catch (e) { } catch (e) {
@ -323,10 +329,10 @@ async function submitTx(job, retry = 0) {
console.log('Tree is still not up to date, resubmitting') console.log('Tree is still not up to date, resubmitting')
await submitTx(job, retry + 1) await submitTx(job, retry + 1)
} else { } else {
throw new Error('Tree update retry limit exceeded') throw new RelayerError('Tree update retry limit exceeded')
} }
} else { } else {
throw new Error(`Revert by smart contract ${e.message}`) throw new RelayerError(`Revert by smart contract ${e.message}`)
} }
} }
} }
@ -349,4 +355,9 @@ async function updateStatus(status) {
await currentJob.update(currentJob.data) await currentJob.update(currentJob.data)
} }
async function clearErrors() {
console.log('Errors list cleared')
await redis.del('errors')
}
start() start()

View File

@ -4,7 +4,7 @@ const {
getTornadoWithdrawInputError, getTornadoWithdrawInputError,
getMiningRewardInputError, getMiningRewardInputError,
getMiningWithdrawInputError, getMiningWithdrawInputError,
} = require('../src/validator') } = require('../src/modules/validator')
describe('Validator', () => { describe('Validator', () => {
describe('#getTornadoWithdrawInputError', () => { describe('#getTornadoWithdrawInputError', () => {