detect tx reverts

This commit is contained in:
poma 2020-11-25 21:41:10 +03:00
parent 2b0415c542
commit e8bf718143
No known key found for this signature in database
GPG Key ID: BA20CB01FE165657

View File

@ -36,6 +36,8 @@ let tree
let txManager let txManager
let controller let controller
let swap let swap
let minerContract
let proxyContract
const redis = new Redis(redisUrl) const redis = new Redis(redisUrl)
const redisSubscribe = new Redis(redisUrl) const redisSubscribe = new Redis(redisUrl)
const gasPriceOracle = new GasPriceOracle({ defaultRpc: httpRpcUrl }) const gasPriceOracle = new GasPriceOracle({ defaultRpc: httpRpcUrl })
@ -83,6 +85,8 @@ async function start() {
const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env
txManager = new TxManager({ privateKey, rpcUrl: httpRpcUrl, config: { CONFIRMATIONS, MAX_GAS_PRICE } }) txManager = new TxManager({ privateKey, rpcUrl: httpRpcUrl, config: { CONFIRMATIONS, MAX_GAS_PRICE } })
swap = new web3.eth.Contract(swapABI, await resolver.resolve(torn.rewardSwap.address)) swap = new web3.eth.Contract(swapABI, await resolver.resolve(torn.rewardSwap.address))
minerContract = new web3.eth.Contract(miningABI, await resolver.resolve(torn.miningV2.address))
proxyContract = new web3.eth.Contract(tornadoProxyABI, await resolver.resolve(torn.tornadoProxy.address))
redisSubscribe.subscribe('treeUpdate', fetchTree) redisSubscribe.subscribe('treeUpdate', fetchTree)
await fetchTree() await fetchTree()
const provingKeys = { const provingKeys = {
@ -175,8 +179,7 @@ async function getTxObject({ data }) {
if (data.type === jobType.TORNADO_WITHDRAW) { if (data.type === jobType.TORNADO_WITHDRAW) {
let contract, calldata let contract, calldata
if (getInstance(data.contract).currency === 'eth') { if (getInstance(data.contract).currency === 'eth') {
const tornadoProxyAddress = await resolver.resolve(torn.tornadoProxy.address) contract = proxyContract
contract = new web3.eth.Contract(tornadoProxyABI, tornadoProxyAddress)
calldata = contract.methods.withdraw(data.contract, data.proof, ...data.args).encodeABI() calldata = contract.methods.withdraw(data.contract, data.proof, ...data.args).encodeABI()
} else { } else {
contract = new web3.eth.Contract(tornadoABI, data.contract) contract = new web3.eth.Contract(tornadoABI, data.contract)
@ -188,17 +191,65 @@ async function getTxObject({ data }) {
data: calldata, data: calldata,
} }
} else { } else {
const minerAddress = await resolver.resolve(torn.miningV2.address)
const contract = new web3.eth.Contract(miningABI, minerAddress)
const method = data.type === jobType.MINING_REWARD ? 'reward' : 'withdraw' const method = data.type === jobType.MINING_REWARD ? 'reward' : 'withdraw'
const calldata = contract.methods[method](data.proof, data.args).encodeABI() const calldata = minerContract.methods[method](data.proof, data.args).encodeABI()
return { return {
to: minerAddress, to: minerContract._address,
data: calldata, data: calldata,
} }
} }
} }
function extractRevertReason(msg) {
console.log('RAW error message:', msg)
if (!msg.startsWith('Node error: ')) {
console.log('Failed to parse error message from Ethereum call: ' + msg)
return null
}
// Trim "Node error: "
const errorObjectStr = msg.slice(12)
// Parse the error object
const errorObject = JSON.parse(errorObjectStr)
if (!errorObject.data) {
console.log('Failed to parse data field error object:' + errorObjectStr)
return null
}
if (errorObject.data.startsWith('Reverted 0x')) {
// Trim "Reverted 0x" from the data field
msg = errorObject.data.slice(11)
} else if (errorObject.data.startsWith('0x')) {
// Trim "0x" from the data field
msg = errorObject.data.slice(2)
} else {
console.log('Failed to parse data field error object:' + errorObjectStr)
return null
}
// Get the length of the revert reason
const strLen = parseInt(msg.slice(8 + 64, 8 + 128), 16)
// Using the length and known offset, extract and convert the revert reason
const reasonCodeHex = msg.slice(8 + 128, 8 + 128 + (strLen * 2))
// Convert reason from hex to string
const reason = web3.utils.hexToAscii('0x' + reasonCodeHex)
return reason
}
async function isOutdatedTreeRevert(receipt, currentTx) {
try {
await web3.eth.call(currentTx.tx, receipt.blockNumber)
console.log('Simulated call successful')
return false
} catch(e) {
const reason = extractRevertReason(e.message)
console.log('Decoded revert reason:', reason)
return reason === 'Outdated account merkle root' || reason === 'Outdated tree update merkle root'
}
}
async function processJob(job) { async function processJob(job) {
try { try {
if (!jobType[job.data.type]) { if (!jobType[job.data.type]) {
@ -207,31 +258,7 @@ async function processJob(job) {
currentJob = job currentJob = job
await updateStatus(status.ACCEPTED) await updateStatus(status.ACCEPTED)
console.log(`Start processing a new ${job.data.type} job #${job.id}`) console.log(`Start processing a new ${job.data.type} job #${job.id}`)
await checkFee(job) await submitTx(job)
currentTx = await txManager.createTx(await getTxObject(job))
if (job.data.type !== jobType.TORNADO_WITHDRAW) {
await fetchTree()
}
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}`)
}
} catch (e) { } catch (e) {
console.error(e) console.error(e)
await updateStatus(status.FAILED) await updateStatus(status.FAILED)
@ -239,6 +266,48 @@ async function processJob(job) {
} }
} }
async function submitTx(job, retry = 0) {
await checkFee(job)
currentTx = await txManager.createTx(await getTxObject(job))
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)
if (receipt.status === 1) {
await updateStatus(status.CONFIRMED)
} else {
if (job.data.type !== jobType.TORNADO_WITHDRAW && await isOutdatedTreeRevert(receipt, currentTx)) {
if (retry < 3) {
await submitTx(job, retry + 1)
} else {
throw new Error('Tree update retry limit exceeded')
}
} else {
throw new Error('Submitted transaction failed')
}
}
} catch (e) {
// todo this could result in duplicated error logs
// todo handle a case where account tree is still not up to date (wait and retry)?
console.error('Revert', e)
throw new Error(`Revert by smart contract ${e.message}`)
}
}
async function updateTxHash(txHash) { async function updateTxHash(txHash) {
console.log(`A new successfully sent tx ${txHash}`) console.log(`A new successfully sent tx ${txHash}`)
currentJob.data.txHash = txHash currentJob.data.txHash = txHash