tornado-relayer/src/worker.js

243 lines
7.2 KiB
JavaScript
Raw Normal View History

const fs = require('fs')
2020-09-28 05:28:34 +03:00
const Web3 = require('web3')
2020-10-05 17:22:52 +03:00
const { toBN, toWei, fromWei } = require('web3-utils')
2020-09-28 05:28:34 +03:00
const MerkleTree = require('fixed-merkle-tree')
const Redis = require('ioredis')
const { GasPriceOracle } = require('gas-price-oracle')
2020-10-06 21:55:03 +03:00
const { Utils } = require('tornado-cash-anonymity-mining')
const tornadoABI = require('../abis/tornadoABI.json')
2020-10-01 09:30:50 +03:00
const miningABI = require('../abis/mining.abi.json')
const swapABI = require('../abis/swap.abi.json')
const { queue } = require('./queue')
2020-10-05 17:22:52 +03:00
const { poseidonHash2, getInstance, fromDecimals } = require('./utils')
const jobType = require('./jobTypes')
const {
netId,
rpcUrl,
redisUrl,
privateKey,
swapAddress,
minerAddress,
gasLimits,
instances,
tornadoServiceFee,
2020-10-06 21:55:03 +03:00
miningServiceFee,
2020-10-06 14:20:26 +03:00
} = require('./config')
2020-10-02 13:16:43 +03:00
const { TxManager } = require('tx-manager')
const { Controller } = require('tornado-cash-anonymity-mining')
2020-09-28 05:28:34 +03:00
let web3
let currentTx
2020-09-29 06:17:42 +03:00
let currentJob
2020-09-28 05:28:34 +03:00
let tree
let txManager
let controller
2020-10-06 21:55:03 +03:00
let swap
const redis = new Redis(redisUrl)
const redisSubscribe = new Redis(redisUrl)
const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
2020-09-28 05:28:34 +03:00
2020-10-02 15:09:33 +03:00
const status = Object.freeze({
ACCEPTED: 'ACCEPTED',
SENT: 'SENT',
MINED: 'MINED',
CONFIRMED: 'CONFIRMED',
})
2020-09-28 05:28:34 +03:00
async function fetchTree() {
console.log('got tree update')
2020-09-28 05:28:34 +03:00
const elements = await redis.get('tree:elements')
const convert = (_, val) => (typeof val === 'string' ? toBN(val) : val)
2020-09-28 05:28:34 +03:00
tree = MerkleTree.deserialize(JSON.parse(elements, convert), poseidonHash2)
if (currentTx && currentJob && ['miningReward', 'miningWithdraw'].includes(currentJob.data.type)) {
2020-10-02 15:09:33 +03:00
const { proof, args } = currentJob.data
2020-10-02 13:16:43 +03:00
if (toBN(args.account.inputRoot).eq(toBN(tree.root()))) {
return
}
const update = await controller.treeUpdate(args.account.outputCommitment, tree)
2020-10-02 15:09:33 +03:00
const instance = new web3.eth.Contract(miningABI, minerAddress)
2020-10-02 13:16:43 +03:00
const data =
currentJob.data.type === 'miningReward'
? instance.methods.reward(proof, args, update.proof, update.args).encodeABI()
: instance.methods.withdraw(proof, args, update.proof, update.args).encodeABI()
currentTx = await currentTx.replace({
to: minerAddress,
data,
})
console.log('replaced pending tx')
2020-09-28 05:28:34 +03:00
}
}
async function start() {
2020-10-01 07:01:02 +03:00
web3 = new Web3(rpcUrl)
txManager = new TxManager({ privateKey, rpcUrl })
2020-10-06 21:55:03 +03:00
swap = new web3.eth.Contract(swapABI, swapAddress)
redisSubscribe.subscribe('treeUpdate', fetchTree)
2020-09-28 05:28:34 +03:00
await fetchTree()
const provingKeys = {
treeUpdateCircuit: require('../keys/TreeUpdate.json'),
treeUpdateProvingKey: fs.readFileSync('./keys/TreeUpdate_proving_key.bin').buffer,
}
controller = new Controller({ provingKeys })
await controller.init()
queue.process(process)
console.log('Worker started')
2020-09-28 05:28:34 +03:00
}
2020-10-05 17:22:52 +03:00
function checkFee({ data }) {
if (data.type === jobType.TORNADO_WITHDRAW) {
2020-10-02 15:09:33 +03:00
return checkTornadoFee(data)
}
return checkMiningFee(data)
}
async function checkTornadoFee({ args, contract }) {
2020-10-05 17:22:52 +03:00
const { currency, amount } = getInstance(contract)
const { decimals } = instances[`netId${netId}`][currency]
const [fee, refund] = [args[4], args[5]].map(toBN)
const { fast } = await gasPriceOracle.gasPrices()
2020-10-05 17:22:52 +03:00
const ethPrice = await redis.hget('prices', currency)
2020-10-06 14:20:26 +03:00
const expense = toBN(toWei(fast.toString(), 'gwei')).mul(toBN(gasLimits[jobType.TORNADO_WITHDRAW]))
2020-10-05 17:22:52 +03:00
const feePercent = toBN(fromDecimals(amount, decimals))
.mul(toBN(tornadoServiceFee * 1e10))
.div(toBN(1e10 * 100))
let desiredFee
switch (currency) {
case 'eth': {
desiredFee = expense.add(feePercent)
break
}
default: {
desiredFee = expense
.add(refund)
.mul(toBN(10 ** decimals))
.div(toBN(ethPrice))
desiredFee = desiredFee.add(feePercent)
break
}
}
console.log(
'sent fee, desired fee, feePercent',
fromWei(fee.toString()),
fromWei(desiredFee.toString()),
fromWei(feePercent.toString()),
)
if (fee.lt(desiredFee)) {
throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.')
}
2020-09-28 05:28:34 +03:00
}
2020-10-02 15:09:33 +03:00
async function checkMiningFee({ args }) {
2020-10-06 11:51:41 +03:00
const { fast } = await gasPriceOracle.gasPrices()
2020-10-06 14:20:26 +03:00
const ethPrice = await redis.hget('prices', 'torn')
2020-10-06 11:51:41 +03:00
const expense = toBN(toWei(fast.toString(), 'gwei')).mul(toBN(gasLimits[args.type]))
2020-10-06 21:55:03 +03:00
const expenseInTorn = expense.mul(toBN(1e18)).div(toBN(ethPrice))
// todo make aggregator for ethPrices and rewardSwap data
const balance = await swap.virtualTornBalance()
const poolWeight = await swap.poolWeight()
const expenseInPoints = Utils.reverseTornadoFormula({ balance, tokens: expenseInTorn, poolWeight })
2020-10-06 14:20:26 +03:00
/* eslint-disable */
2020-10-06 21:55:03 +03:00
const serviceFeePercent =
2020-10-06 11:51:41 +03:00
args.type === jobType.MINING_REWARD
? 0
2020-10-06 21:55:03 +03:00
: toBN(args.amount)
2020-10-06 14:20:26 +03:00
.mul(toBN(miningServiceFee * 1e10))
.div(toBN(1e10 * 100))
/* eslint-enable */
2020-10-06 21:55:03 +03:00
const desiredFee = expenseInPoints.add(serviceFeePercent) // in points
2020-10-06 11:51:41 +03:00
console.log(
2020-10-06 21:55:03 +03:00
'sent fee, desired fee, serviceFeePercent',
2020-10-06 11:51:41 +03:00
fromWei(args.fee.toString()),
fromWei(desiredFee.toString()),
2020-10-06 21:55:03 +03:00
fromWei(serviceFeePercent.toString()),
2020-10-06 11:51:41 +03:00
)
2020-10-06 21:55:03 +03:00
if (toBN(args.fee).lt(desiredFee)) {
2020-10-06 11:51:41 +03:00
throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.')
}
}
2020-10-05 17:22:52 +03:00
function getTxObject({ data }) {
let [ABI, contractAddress, value] =
data.type === jobType.TORNADO_WITHDRAW
? [tornadoABI, data.contract, data.args[5]]
: [miningABI, minerAddress, 0]
const method = data.type !== jobType.MINING_REWARD ? 'withdraw' : 'reward'
2020-10-02 15:09:33 +03:00
const contract = new web3.eth.Contract(ABI, contractAddress)
const calldata = contract.methods[method](data.proof, ...data.args).encodeABI()
return {
value,
2020-10-05 17:22:52 +03:00
to: contractAddress,
2020-10-02 15:09:33 +03:00
data: calldata,
2020-09-29 06:17:42 +03:00
}
}
2020-10-02 15:09:33 +03:00
async function process(job) {
2020-10-01 09:30:50 +03:00
try {
2020-10-05 17:22:52 +03:00
if (!jobType[job.data.type]) {
throw new Error(`Unknown job type: ${job.data.type}`)
}
currentJob = job
await updateStatus(status.ACCEPTED)
console.log(`Start processing a new ${job.data.type} job #${job.id}`)
await checkFee(job)
if (job.data.type !== jobType.TORNADO_WITHDRAW) {
// precheck if root is up to date
}
currentTx = await txManager.createTx(getTxObject(job))
try {
await currentTx
.send()
.on('transactionHash', txHash => {
updateTxHash(txHash)
updateStatus(status.SENT)
})
.on('mined', receipt => {
console.log('Mined in block', receipt.blockNumber)
updateStatus(status.MINED)
})
.on('confirmations', updateConfirmations)
await updateStatus(status.CONFIRMED)
} catch (e) {
console.error('Revert', e)
throw new Error(`Revert by smart contract ${e.message}`)
}
2020-10-01 09:30:50 +03:00
} catch (e) {
2020-10-05 17:22:52 +03:00
console.error(e)
throw e
2020-10-01 09:30:50 +03:00
}
}
2020-09-29 06:17:42 +03:00
async function updateTxHash(txHash) {
console.log(`A new successfully sent tx ${txHash}`)
currentJob.data.txHash = txHash
await currentJob.update(currentJob.data)
}
async function updateConfirmations(confirmations) {
console.log(`Confirmations count ${confirmations}`)
currentJob.data.confirmations = confirmations
await currentJob.update(currentJob.data)
2020-09-28 05:28:34 +03:00
}
2020-10-02 15:09:33 +03:00
async function updateStatus(status) {
console.log(`Job status updated ${status}`)
currentJob.data.status = status
await currentJob.update(currentJob.data)
}
2020-10-05 17:22:52 +03:00
start()
module.exports = { start, process }