Skip to content

Instantly share code, notes, and snippets.

@KentonPrescott
Last active January 27, 2020 19:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KentonPrescott/e5e38da3b9c3c94ef7f233076e1ea64b to your computer and use it in GitHub Desktop.
Save KentonPrescott/e5e38da3b9c3c94ef7f233076e1ea64b to your computer and use it in GitHub Desktop.
Solidity revert message debugging w/ Python and Parity

Solidity Revert message debugging w/ Python and Parity

I followed the open-sourced error message decoder by etherscan. The code I modified is contained in this PR (which supports parity nodes).

The following steps are compatible with the testing workflow of the pymaker and MakerDao keepers. It will print out the RAW and Decoded error message to the CLI of a failed transaction on a local Parity testchain

  1. Modify lib/pymaker/init.py to mine a failing transaction
  2. Note the txhash on the failed test
  3. Add debuggingRevert.js to the directory that holds the python code with the failing
  4. Update debuggingRevert.js with the txhash

We are going to call debuggingRevert.js through the python code, right after the failed (but now successfully mined) transaction. To do this, I followed this stackoverflow answer.

After Naked is imported via from Naked.toolshed.shell import execute_js, muterun_js, here's an example of how I called debuggingRevert.js after spell.cast() was failing in Chief-Keeper:

spell.cast().transact(gas_price=self.gas_price())

response = muterun_js('debuggingRevert.js')
if response.exitcode == 0:
  print(response.stdout)
else:
  print(response.stderr)
  # sys.stderr.write(response.stderr)

Which yeilds the following console log within a pytest session:

b'RAW result: 0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001b64732d70617573652d64656c656761746563616c6c2d6572726f720000000000\n\nDecoded reason: "ds-pause-delegatecall-error"\n'
/* eslint-disable no-console */
const Web3 = require('web3')
const kovanPOA = 'https://kovan.poa.network'
const kovanInfura = 'https://kovan.infura.io/v3/c7463beadf2144e68646ff049917b716'
const mainnetInfura = 'https://mainnet.infura.io/v3/c7463beadf2144e68646ff049917b716'
const mainnetTrust = 'http://ethereum-rpc.trustwalletapp.com'
const mainnetMEW = 'https://api.mycryptoapi.com/eth'
const testnet = 'http://localhost:8545'
const web3 = new Web3(testnet)
// INSERT TXHASH THAT'S FAILING
const txHash = '0x205a25f2cf8100f33ef63a95b498d36d4f86ebe7ef6d51eeddab8edc6dad0163'
const parity = true
// INSERT TXHASH THAT's FAILING
if (parity) {
web3.eth.extend({
property: 'parity',
methods: [
{
name: 'traceReplayTransaction',
call: 'trace_replayTransaction',
params: 2,
},
],
})
}
async function getResult({ hash, to, input, from, value, gas, gasPrice, blockNumber }) {
// Parity call to get result
if (parity) {
return (await web3.eth.parity.traceReplayTransaction(hash, ['trace'])).output
}
// Geth call to get result
return web3.eth.call(
{
to,
data: input,
from,
value,
gas,
gasPrice,
},
blockNumber
)
}
async function main() {
if (!txHash.match(/^0x([A-Fa-f0-9]{64})$/)) {
console.error('Invalid transaction hash argument. Must be a 32 byte hex string with a 0x prefix which is 64 characters in total.')
process.exit(2)
}
const receipt = await web3.eth.getTransactionReceipt(txHash)
if (!receipt) {
console.error('Could not get transaction receipt. Are you sure it was mined?')
process.exit(3)
}
if (receipt.status) {
console.error('Transaction did not fail. Can only read the revert reason from failed transactions')
process.exit(3)
}
const transaction = await web3.eth.getTransaction(txHash)
if (receipt.gasUsed === transaction.gas) {
console.error('Transaction failed as it ran out of gas.')
process.exit(4)
}
let rawMessageData
try {
const result = await getResult(transaction)
console.log('RAW result:', result)
// Trim the 0x prefix
rawMessageData = result.slice(2)
} catch (e) {
console.log('RAW error message:', e.message)
if (e.message.startsWith('Node error: ')) {
// Trim "Node error: "
const errorObjectStr = e.message.slice(12)
// Parse the error object
const errorObject = JSON.parse(errorObjectStr)
if (!errorObject.data) {
throw Error('Failed to parse data field error object:' + errorObjectStr)
}
if (errorObject.data.startsWith('Reverted 0x')) {
// Trim "Reverted 0x" from the data field
rawMessageData = errorObject.data.slice(11)
}
else if (errorObject.data.startsWith('0x')) {
// Trim "0x" from the data field
rawMessageData = errorObject.data.slice(2)
}
else {
throw Error('Failed to parse data field error object:' + errorObjectStr)
}
}
else {
throw Error('Failed to parse error message from Ethereum call: ' + e.message)
}
}
// Get the length of the revert reason
const strLen = parseInt(rawMessageData.slice(8 + 64, 8 + 128), 16)
// Using the length and known offset, extract and convert the revert reason
const reasonCodeHex = rawMessageData.slice(8 + 128, 8 + 128 + (strLen * 2))
// Convert reason from hex to string
const reason = web3.utils.hexToAscii('0x' + reasonCodeHex)
console.log('\nDecoded reason: "' + reason + '"')
}
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment