2020-10-02 08:38:31 +03:00
|
|
|
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')
|
2020-09-30 18:35:48 +03:00
|
|
|
|
2020-11-04 19:01:51 +03:00
|
|
|
const tornadoProxyABI = require('../abis/tornadoProxyABI.json')
|
2020-11-17 18:39:39 +03:00
|
|
|
const tornadoABI = require('../abis/tornadoABI.json')
|
2020-10-01 09:30:50 +03:00
|
|
|
const miningABI = require('../abis/mining.abi.json')
|
2020-10-02 08:38:31 +03:00
|
|
|
const swapABI = require('../abis/swap.abi.json')
|
2020-09-30 18:35:48 +03:00
|
|
|
const { queue } = require('./queue')
|
2020-12-19 01:43:38 +03:00
|
|
|
const { poseidonHash2, getInstance, fromDecimals, sleep } = require('./utils')
|
2020-11-27 19:21:56 +03:00
|
|
|
const { jobType, status } = require('./constants')
|
2020-10-05 17:22:52 +03:00
|
|
|
const {
|
|
|
|
netId,
|
2020-11-04 22:23:14 +03:00
|
|
|
torn,
|
2020-10-14 14:43:38 +03:00
|
|
|
httpRpcUrl,
|
2020-10-05 17:22:52 +03:00
|
|
|
redisUrl,
|
|
|
|
privateKey,
|
|
|
|
gasLimits,
|
|
|
|
instances,
|
2021-01-12 13:20:54 +03:00
|
|
|
oracleRpcUrl,
|
2021-03-24 12:22:55 +03:00
|
|
|
tornadoProxy,
|
2020-10-05 17:22:52 +03:00
|
|
|
tornadoServiceFee,
|
2020-10-06 21:55:03 +03:00
|
|
|
miningServiceFee,
|
2020-10-06 14:20:26 +03:00
|
|
|
} = require('./config')
|
2020-11-04 22:23:14 +03:00
|
|
|
const ENSResolver = require('./resolver')
|
|
|
|
const resolver = new ENSResolver()
|
2020-10-02 13:16:43 +03:00
|
|
|
const { TxManager } = require('tx-manager')
|
2020-10-02 08:38:31 +03:00
|
|
|
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
|
2020-09-30 18:35:48 +03:00
|
|
|
let txManager
|
2020-10-02 08:38:31 +03:00
|
|
|
let controller
|
2020-10-06 21:55:03 +03:00
|
|
|
let swap
|
2020-11-25 21:41:10 +03:00
|
|
|
let minerContract
|
|
|
|
let proxyContract
|
2020-09-30 18:35:48 +03:00
|
|
|
const redis = new Redis(redisUrl)
|
|
|
|
const redisSubscribe = new Redis(redisUrl)
|
2021-01-12 13:20:54 +03:00
|
|
|
const gasPriceOracle = new GasPriceOracle({ defaultRpc: oracleRpcUrl })
|
2020-09-28 05:28:34 +03:00
|
|
|
|
|
|
|
async function fetchTree() {
|
|
|
|
const elements = await redis.get('tree:elements')
|
2020-09-30 18:35:48 +03:00
|
|
|
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)
|
|
|
|
|
2020-10-14 14:43:38 +03:00
|
|
|
if (currentTx && currentJob && ['MINING_REWARD', 'MINING_WITHDRAW'].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()))) {
|
2020-10-14 14:43:38 +03:00
|
|
|
console.log('Account root is up to date. Skipping Root Update operation...')
|
2020-10-02 08:38:31 +03:00
|
|
|
return
|
2020-10-14 14:43:38 +03:00
|
|
|
} else {
|
|
|
|
console.log('Account root is outdated. Starting Root Update operation...')
|
2020-10-02 08:38:31 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
const update = await controller.treeUpdate(args.account.outputCommitment, tree)
|
|
|
|
|
2020-11-04 22:23:14 +03:00
|
|
|
const minerAddress = await resolver.resolve(torn.miningV2.address)
|
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 =
|
2020-10-14 14:43:38 +03:00
|
|
|
currentJob.data.type === 'MINING_REWARD'
|
2020-10-02 13:16:43 +03:00
|
|
|
? instance.methods.reward(proof, args, update.proof, update.args).encodeABI()
|
|
|
|
: instance.methods.withdraw(proof, args, update.proof, update.args).encodeABI()
|
2020-10-14 14:43:38 +03:00
|
|
|
await currentTx.replace({
|
2020-10-02 08:38:31 +03:00
|
|
|
to: minerAddress,
|
|
|
|
data,
|
|
|
|
})
|
|
|
|
console.log('replaced pending tx')
|
2020-09-28 05:28:34 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-30 18:35:48 +03:00
|
|
|
async function start() {
|
2021-03-02 07:38:16 +03:00
|
|
|
try {
|
|
|
|
web3 = new Web3(httpRpcUrl)
|
|
|
|
const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env
|
|
|
|
txManager = new TxManager({
|
|
|
|
privateKey,
|
|
|
|
rpcUrl: httpRpcUrl,
|
|
|
|
config: { CONFIRMATIONS, MAX_GAS_PRICE, THROW_ON_REVERT: false },
|
|
|
|
})
|
|
|
|
swap = new web3.eth.Contract(swapABI, await resolver.resolve(torn.rewardSwap.address))
|
|
|
|
minerContract = new web3.eth.Contract(miningABI, await resolver.resolve(torn.miningV2.address))
|
2021-03-24 12:22:55 +03:00
|
|
|
if (netId === 5) {
|
|
|
|
proxyContract = new web3.eth.Contract(tornadoProxyABI, tornadoProxy)
|
|
|
|
} else {
|
|
|
|
proxyContract = new web3.eth.Contract(
|
|
|
|
tornadoProxyABI,
|
|
|
|
await resolver.resolve(torn.tornadoProxy.address),
|
|
|
|
)
|
|
|
|
}
|
2021-03-02 07:38:16 +03:00
|
|
|
redisSubscribe.subscribe('treeUpdate', fetchTree)
|
|
|
|
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(processJob)
|
|
|
|
console.log('Worker started')
|
|
|
|
} catch (e) {
|
|
|
|
console.error('error on start worker', e.message)
|
2020-10-02 08:38:31 +03:00
|
|
|
}
|
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)
|
2020-09-30 18:35:48 +03:00
|
|
|
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-15 20:20:00 +03:00
|
|
|
const isMiningReward = currentJob.data.type === jobType.MINING_REWARD
|
|
|
|
const providedFee = isMiningReward ? toBN(args.fee) : toBN(args.extData.fee)
|
2020-10-06 11:51:41 +03:00
|
|
|
|
2020-10-08 17:12:36 +03:00
|
|
|
const expense = toBN(toWei(fast.toString(), 'gwei')).mul(toBN(gasLimits[currentJob.data.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
|
2020-10-08 17:12:36 +03:00
|
|
|
const balance = await swap.methods.tornVirtualBalance().call()
|
|
|
|
const poolWeight = await swap.methods.poolWeight().call()
|
2020-10-06 21:55:03 +03:00
|
|
|
const expenseInPoints = Utils.reverseTornadoFormula({ balance, tokens: expenseInTorn, poolWeight })
|
2020-10-06 14:20:26 +03:00
|
|
|
/* eslint-disable */
|
2020-10-15 20:20:00 +03:00
|
|
|
const serviceFeePercent = isMiningReward
|
|
|
|
? toBN(0)
|
|
|
|
: toBN(args.amount)
|
|
|
|
.sub(providedFee) // args.amount includes fee
|
|
|
|
.mul(toBN(miningServiceFee * 1e10))
|
|
|
|
.div(toBN(1e10 * 100))
|
2020-10-06 14:20:26 +03:00
|
|
|
/* 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-15 20:20:00 +03:00
|
|
|
'user provided fee, desired fee, serviceFeePercent',
|
|
|
|
providedFee.toString(),
|
2020-10-08 17:12:36 +03:00
|
|
|
desiredFee.toString(),
|
|
|
|
serviceFeePercent.toString(),
|
2020-10-06 11:51:41 +03:00
|
|
|
)
|
2020-10-15 02:46:01 +03:00
|
|
|
if (toBN(providedFee).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-02 08:38:31 +03:00
|
|
|
}
|
|
|
|
|
2020-11-26 00:42:16 +03:00
|
|
|
function getTxObject({ data }) {
|
2020-11-04 19:01:51 +03:00
|
|
|
if (data.type === jobType.TORNADO_WITHDRAW) {
|
2020-11-17 18:39:39 +03:00
|
|
|
let contract, calldata
|
|
|
|
if (getInstance(data.contract).currency === 'eth') {
|
2020-11-25 21:41:10 +03:00
|
|
|
contract = proxyContract
|
2020-11-17 18:39:39 +03:00
|
|
|
calldata = contract.methods.withdraw(data.contract, data.proof, ...data.args).encodeABI()
|
|
|
|
} else {
|
|
|
|
contract = new web3.eth.Contract(tornadoABI, data.contract)
|
|
|
|
calldata = contract.methods.withdraw(data.proof, ...data.args).encodeABI()
|
|
|
|
}
|
2020-11-04 19:01:51 +03:00
|
|
|
return {
|
|
|
|
value: data.args[5],
|
2020-11-17 18:39:39 +03:00
|
|
|
to: contract._address,
|
2020-11-04 22:23:14 +03:00
|
|
|
data: calldata,
|
2020-11-04 19:01:51 +03:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const method = data.type === jobType.MINING_REWARD ? 'reward' : 'withdraw'
|
2020-11-25 21:41:10 +03:00
|
|
|
const calldata = minerContract.methods[method](data.proof, data.args).encodeABI()
|
2020-11-04 19:01:51 +03:00
|
|
|
return {
|
2020-11-25 21:41:10 +03:00
|
|
|
to: minerContract._address,
|
2020-11-04 19:01:51 +03:00
|
|
|
data: calldata,
|
|
|
|
}
|
2020-09-29 06:17:42 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-25 21:41:10 +03:00
|
|
|
async function isOutdatedTreeRevert(receipt, currentTx) {
|
|
|
|
try {
|
|
|
|
await web3.eth.call(currentTx.tx, receipt.blockNumber)
|
|
|
|
console.log('Simulated call successful')
|
|
|
|
return false
|
2020-11-26 00:42:16 +03:00
|
|
|
} catch (e) {
|
|
|
|
console.log('Decoded revert reason:', e.message)
|
|
|
|
return (
|
|
|
|
e.message.indexOf('Outdated account merkle root') !== -1 ||
|
|
|
|
e.message.indexOf('Outdated tree update merkle root') !== -1
|
|
|
|
)
|
2020-11-25 21:41:10 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-10 18:20:02 +03:00
|
|
|
async function processJob(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}`)
|
2020-11-25 21:41:10 +03:00
|
|
|
await submitTx(job)
|
|
|
|
} catch (e) {
|
2020-11-26 00:42:16 +03:00
|
|
|
console.error('processJob', e.message)
|
2020-11-25 21:41:10 +03:00
|
|
|
await updateStatus(status.FAILED)
|
|
|
|
throw e
|
|
|
|
}
|
|
|
|
}
|
2020-10-14 14:43:38 +03:00
|
|
|
|
2020-11-25 21:41:10 +03:00
|
|
|
async function submitTx(job, retry = 0) {
|
|
|
|
await checkFee(job)
|
2020-11-26 00:42:16 +03:00
|
|
|
currentTx = await txManager.createTx(getTxObject(job))
|
2020-10-05 17:22:52 +03:00
|
|
|
|
2020-11-25 21:41:10 +03:00
|
|
|
if (job.data.type !== jobType.TORNADO_WITHDRAW) {
|
|
|
|
await fetchTree()
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const receipt = 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)
|
2020-10-05 17:22:52 +03:00
|
|
|
|
2020-11-25 21:41:10 +03:00
|
|
|
if (receipt.status === 1) {
|
2020-10-05 17:22:52 +03:00
|
|
|
await updateStatus(status.CONFIRMED)
|
2020-11-25 21:41:10 +03:00
|
|
|
} else {
|
2020-11-26 00:42:16 +03:00
|
|
|
if (job.data.type !== jobType.TORNADO_WITHDRAW && (await isOutdatedTreeRevert(receipt, currentTx))) {
|
2020-11-25 21:41:10 +03:00
|
|
|
if (retry < 3) {
|
2020-11-26 00:42:16 +03:00
|
|
|
await updateStatus(status.RESUBMITTED)
|
2020-11-25 21:41:10 +03:00
|
|
|
await submitTx(job, retry + 1)
|
|
|
|
} else {
|
|
|
|
throw new Error('Tree update retry limit exceeded')
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error('Submitted transaction failed')
|
|
|
|
}
|
2020-10-05 17:22:52 +03:00
|
|
|
}
|
2020-10-01 09:30:50 +03:00
|
|
|
} catch (e) {
|
2020-11-25 21:41:10 +03:00
|
|
|
// todo this could result in duplicated error logs
|
|
|
|
// todo handle a case where account tree is still not up to date (wait and retry)?
|
2020-12-19 01:43:38 +03:00
|
|
|
if (
|
|
|
|
job.data.type !== jobType.TORNADO_WITHDRAW &&
|
|
|
|
(e.message.indexOf('Outdated account merkle root') !== -1 ||
|
|
|
|
e.message.indexOf('Outdated tree update merkle root') !== -1)
|
|
|
|
) {
|
|
|
|
if (retry < 5) {
|
|
|
|
await sleep(3000)
|
|
|
|
console.log('Tree is still not up to date, resubmitting')
|
|
|
|
await submitTx(job, retry + 1)
|
|
|
|
} else {
|
|
|
|
throw new Error('Tree update retry limit exceeded')
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error(`Revert by smart contract ${e.message}`)
|
|
|
|
}
|
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)
|
|
|
|
}
|
|
|
|
|
2020-09-30 18:35:48 +03:00
|
|
|
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()
|