15

In other words, how to get "classic stacktrace" of failed transaction?

E.g. we have a trace (don't look at "Missing opcode 0xfd" - its the revert instruction) and the contract solidity source. How to find out at which line of the source the exception was thrown?

I managed to get assembly of the contract (using solc --asm), but there are no PC (program counter) hints, so I cant find a line which corresponds to PC=557. Also, I reckon that optimization performed during compilation, but despite that assembly is still readable to some degree.

I'm using solc 0.4.16+commit.d7661dd9.Linux.g++.

Thanks in advance.

Achala Dissanayake
  • 5,819
  • 15
  • 28
  • 38
Zline
  • 151
  • 1
  • 5

2 Answers2

26

I went down this rabbit hole and got a proof of concept to work at the end. I can not recommend the journey. There's impedance mismatches on many levels, requiring lots of format conversions. In the end, my implementation still does not handle cross-contract calls. (There seems to be no way to figure out which contract address a particular program counter belongs too, short of interpreting the call instructions).

My implementation is too dirty to share, but the main steps are:

1) You need solc to produce a runtime sourcemap. It can not directly output this, but it can output this as part of the 'combined json output'. For this, run solc --combined-json bin-runtime,srcmap-runtime.

const srcmaps = JSON.parse(fs.readFileSync("./Contract.json"));
const srcmap =
  srcmaps.contracts["./contracts/Contract.sol:Contract"]["srcmap-runtime"];

const source = fs.readFileSync("./contracts/Contract.sol").toString();

const bin = Buffer.from(
  srcmaps.contracts["./contracts/Contract.sol:Contract"]["bin-runtime"],
  "hex"
);

2) The sourcemap format is compressed and you need to write a decoder. Specification of the formati is in the solidity documentation. You now have a way to map instruction-indices to source offsets.

3) we don't want byte offsets in the source files, but line and column numbers. For this, you need to parse the source files and create a mapping from byte offset to line/column pairs. I decided to ignore this for now and used the get-line-from-pos npm package.

Step 2 and 3 together are:

const parsed = srcmap
  .split(";")
  .map(l => l.split(":"))
  .map(([s, l, f, j]) => ({ s: s === "" ? undefined : s, l, f, j }))
  .reduce(
    ([last, ...list], { s, l, f, j }) => [
      {
        s: parseInt(s || last.s, 10),
        l: parseInt(l || last.l, 10),
        f: parseInt(f || last.f, 10),
        j: j || last.j
      },
      last,
      ...list
    ],
    [{}]
  )
  .reverse()
  .slice(1)
  .map(
    ({ s, l, f, j }) => `${srcmaps.sourceList[f]}:${getLineFromPos(source, s)}`
  );

4) The source map is in instruction number, but we need bytecode addresses. To solve this we need to built a map from bytecode ofset to instruction number (or the other way). I found it easiest to just parse the runtime binary myself. All instructions are 1 byte long, except for PUSH_n which are n+1 long.

const isPush = inst => inst >= 0x60 && inst < 0x7f;

const pushDataLength = inst => inst - 0x5f;

const instructionLength = inst => (isPush(inst) ? 1 + pushDataLength(inst) : 1);

const byteToInstIndex = bin => {
  const result = [];
  let byteIndex = 0;
  let instIndex = 0;
  while (byteIndex < bin.length) {
    const length = instructionLength(bin[byteIndex]);
    for (let i = 0; i < length; i += 1) {
      result.push(instIndex);
    }
    byteIndex += length;
    instIndex += 1;
  }
  return result;
};

Then you need to get the backtrace for a given transaction:

const promisify = func => async (...args) =>
  new Promise((accept, reject) =>
    func(...args, (error, result) => (error ? reject(error) : accept(result)))
  );

const rpcCommand = method => async (...params) =>
  (await promisify(web3.currentProvider.sendAsync)({
    jsonrpc: "2.0",
    method,
    params,
    id: Date.now()
  })).result;

const traceTransaction = rpcCommand("debug_traceTransaction");

Once you have all that, you can get a something ressembling a classic stracktrace:

const trace = await traceTransaction(result.tx);
trace.structLogs.forEach(({op, pc, gasCost}) =>
  console.log(
    `${pc}\t${op}\t${gasCost}\t${byteToInstr[pc]}\t${parsed[
      byteToInstr[pc]
    ]}`
  )
);

I hope to turn clean this up and turn it into a library soon. The ablity to handle traces and map them back to solidity has many uses.

Remco
  • 413
  • 4
  • 8
0

If you can get hold of the contract's source code, you could use Hardhat to get the Solidity stack traces. Hardhat Network is a debugging-first EVM implementation, built for low-level development of smart contracts.

Shameless plug: start from my Solidity template, which is using Hardhat: https://github.com/paulrberg/solidity-template:

Side note: see Nomic Labs' announcement.

Paul Razvan Berg
  • 17,902
  • 6
  • 73
  • 143