From e8bf718143728f3675b29a40d3e96fa288cf5c8b Mon Sep 17 00:00:00 2001 From: poma Date: Wed, 25 Nov 2020 21:41:10 +0300 Subject: [PATCH 1/4] detect tx reverts --- src/worker.js | 131 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 100 insertions(+), 31 deletions(-) diff --git a/src/worker.js b/src/worker.js index e6e60c5..32dd9f0 100644 --- a/src/worker.js +++ b/src/worker.js @@ -36,6 +36,8 @@ let tree let txManager let controller let swap +let minerContract +let proxyContract const redis = new Redis(redisUrl) const redisSubscribe = new Redis(redisUrl) const gasPriceOracle = new GasPriceOracle({ defaultRpc: httpRpcUrl }) @@ -83,6 +85,8 @@ async function start() { const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env txManager = new TxManager({ privateKey, rpcUrl: httpRpcUrl, config: { CONFIRMATIONS, MAX_GAS_PRICE } }) 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) await fetchTree() const provingKeys = { @@ -175,8 +179,7 @@ async function getTxObject({ data }) { if (data.type === jobType.TORNADO_WITHDRAW) { let contract, calldata if (getInstance(data.contract).currency === 'eth') { - const tornadoProxyAddress = await resolver.resolve(torn.tornadoProxy.address) - contract = new web3.eth.Contract(tornadoProxyABI, tornadoProxyAddress) + contract = proxyContract calldata = contract.methods.withdraw(data.contract, data.proof, ...data.args).encodeABI() } else { contract = new web3.eth.Contract(tornadoABI, data.contract) @@ -188,17 +191,65 @@ async function getTxObject({ data }) { data: calldata, } } 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 calldata = contract.methods[method](data.proof, data.args).encodeABI() + const calldata = minerContract.methods[method](data.proof, data.args).encodeABI() return { - to: minerAddress, + to: minerContract._address, 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) { try { if (!jobType[job.data.type]) { @@ -207,31 +258,7 @@ async function processJob(job) { currentJob = job await updateStatus(status.ACCEPTED) console.log(`Start processing a new ${job.data.type} job #${job.id}`) - await checkFee(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}`) - } + await submitTx(job) } catch (e) { console.error(e) 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) { console.log(`A new successfully sent tx ${txHash}`) currentJob.data.txHash = txHash From 008433adb4cdf9d6e96f0daed2eb8c758c15e31c Mon Sep 17 00:00:00 2001 From: Alexey Date: Wed, 25 Nov 2020 22:42:16 +0100 Subject: [PATCH 2/4] fix bugs for account root update --- package.json | 2 +- src/worker.js | 65 ++++++++++++++------------------------------------- yarn.lock | 8 +++---- 3 files changed, 22 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index fd8959e..7cd43f4 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "node-fetch": "^2.6.0", "torn-token": "git+ssh://git@github.com/tornadocash/torn-token.git#04c4df88d470ca7503ef5d97882c56cba4f3647d", "tornado-cash-anonymity-mining": "git+ssh://git@github.com/tornadocash/tornado-anonymity-mining.git#b13228c20126f212ebbcc5a8493ce2105210739e", - "tx-manager": "^0.2.6", + "tx-manager": "^0.2.8", "uuid": "^8.3.0", "web3": "^1.3.0", "web3-core-promievent": "^1.3.0", diff --git a/src/worker.js b/src/worker.js index 32dd9f0..350b50d 100644 --- a/src/worker.js +++ b/src/worker.js @@ -48,6 +48,7 @@ const status = Object.freeze({ MINED: 'MINED', CONFIRMED: 'CONFIRMED', FAILED: 'FAILED', + RESUBMITTED: 'RESUBMITTED', }) async function fetchTree() { @@ -83,7 +84,11 @@ async function fetchTree() { async function start() { web3 = new Web3(httpRpcUrl) 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, 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)) proxyContract = new web3.eth.Contract(tornadoProxyABI, await resolver.resolve(torn.tornadoProxy.address)) @@ -175,7 +180,7 @@ async function checkMiningFee({ args }) { } } -async function getTxObject({ data }) { +function getTxObject({ data }) { if (data.type === jobType.TORNADO_WITHDRAW) { let contract, calldata if (getInstance(data.contract).currency === 'eth') { @@ -200,53 +205,17 @@ async function getTxObject({ data }) { } } -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' + } 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 + ) } } @@ -260,7 +229,7 @@ async function processJob(job) { console.log(`Start processing a new ${job.data.type} job #${job.id}`) await submitTx(job) } catch (e) { - console.error(e) + console.error('processJob', e.message) await updateStatus(status.FAILED) throw e } @@ -268,7 +237,7 @@ async function processJob(job) { async function submitTx(job, retry = 0) { await checkFee(job) - currentTx = await txManager.createTx(await getTxObject(job)) + currentTx = await txManager.createTx(getTxObject(job)) if (job.data.type !== jobType.TORNADO_WITHDRAW) { await fetchTree() @@ -290,8 +259,9 @@ async function submitTx(job, retry = 0) { if (receipt.status === 1) { await updateStatus(status.CONFIRMED) } else { - if (job.data.type !== jobType.TORNADO_WITHDRAW && await isOutdatedTreeRevert(receipt, currentTx)) { + if (job.data.type !== jobType.TORNADO_WITHDRAW && (await isOutdatedTreeRevert(receipt, currentTx))) { if (retry < 3) { + await updateStatus(status.RESUBMITTED) await submitTx(job, retry + 1) } else { throw new Error('Tree update retry limit exceeded') @@ -303,7 +273,6 @@ async function submitTx(job, retry = 0) { } 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}`) } } diff --git a/yarn.lock b/yarn.lock index 7919d18..6202d98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4384,10 +4384,10 @@ tweetnacl@^1.0.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== -tx-manager@^0.2.6: - version "0.2.6" - resolved "https://registry.yarnpkg.com/tx-manager/-/tx-manager-0.2.6.tgz#05f49794695ebd71c58b794bb213927efdbf6d3e" - integrity sha512-19h+PacgoUuXbN2wKTpb4R+MxD9fOxil3ZsvnDRG/Z4pPboUzd/D2+VBdcH9+EEVGXovZ6bcVggIZ4RIO7LO1w== +tx-manager@^0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/tx-manager/-/tx-manager-0.2.8.tgz#9175782f16c1fc3de9d5bcdec8c408e8474bf45a" + integrity sha512-Uo3Jz9NfCZucwwVXuB6MYImIX6Yq8YRhRAJbMMGQMbwG+ghs4mG8p6+GNld7ZA2sH7qdEzY5c3Sk+BROznIAGw== dependencies: async-mutex "^0.2.4" ethers "^5.0.17" From f9773b1c2292b75ed57922d47ba314e3ddfca580 Mon Sep 17 00:00:00 2001 From: Alexey Date: Thu, 26 Nov 2020 20:43:04 +0100 Subject: [PATCH 3/4] tx-manager update --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7cd43f4..76b261b 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "node-fetch": "^2.6.0", "torn-token": "git+ssh://git@github.com/tornadocash/torn-token.git#04c4df88d470ca7503ef5d97882c56cba4f3647d", "tornado-cash-anonymity-mining": "git+ssh://git@github.com/tornadocash/tornado-anonymity-mining.git#b13228c20126f212ebbcc5a8493ce2105210739e", - "tx-manager": "^0.2.8", + "tx-manager": "^0.2.9", "uuid": "^8.3.0", "web3": "^1.3.0", "web3-core-promievent": "^1.3.0", diff --git a/yarn.lock b/yarn.lock index 6202d98..b601a8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4384,10 +4384,10 @@ tweetnacl@^1.0.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== -tx-manager@^0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/tx-manager/-/tx-manager-0.2.8.tgz#9175782f16c1fc3de9d5bcdec8c408e8474bf45a" - integrity sha512-Uo3Jz9NfCZucwwVXuB6MYImIX6Yq8YRhRAJbMMGQMbwG+ghs4mG8p6+GNld7ZA2sH7qdEzY5c3Sk+BROznIAGw== +tx-manager@^0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/tx-manager/-/tx-manager-0.2.9.tgz#dc949d7f1a3ed3a3517384f6ef89b10c0a140e02" + integrity sha512-IQvDo/j+K9JG1NjLOYUUegyCdfsn0D8sTjQU5w2/Mj30GB/MVPmMqWlPIuu2bwhIJY78WpOln25i60TaT5Sawg== dependencies: async-mutex "^0.2.4" ethers "^5.0.17" From eca47a1f261389c6f1ba781e9a2a6758e8c9c715 Mon Sep 17 00:00:00 2001 From: Alexey Date: Thu, 26 Nov 2020 21:42:56 +0100 Subject: [PATCH 4/4] new aggregator --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 6fc31af..b5bf004 100644 --- a/.env.example +++ b/.env.example @@ -18,4 +18,4 @@ REWARD_ACCOUNT= # in GWEI MAX_GAS_PRICE=1000 CONFIRMATIONS=8 -AGGREGATOR=0x9125921ab88429c81d50b6d7e97231cddd181542 +AGGREGATOR=0x466121060aD4dCE1E421027297d7e263236cbfc3