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

View File

@ -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

View File

@ -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 })

View File

@ -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,

View File

@ -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}`)

View File

@ -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 <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) {

View File

@ -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,
}

View File

@ -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 }