8

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.

17

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.

  • Thanks @Remco, you conducted a great work, that posts explains a lot. In my opinion lack of solution to the problem in question is a huge deficiency of ethereum infrastructure. I hope a solution will come soon. – Zline Sep 12 '17 at 17:03
  • It's been more than 1 year since this was posted. Any updates on better tools? – Paul Razvan Berg Nov 23 '18 at 9:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.