11

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.

20

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.

5
  • 1
    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? Nov 23 '18 at 9:34
  • Great job @Remco. I have a contract example where there are around 100 opcodes, but sourceMap has only around 15 ";". I thought sourceMap entries should be equal to number of instructions in the opcode! Am I missing something, maybe compression? Thx in advance! Jun 30 at 11:38
  • 2021 update: the Remix Debugger handles stack traces pretty well now. Sep 4 at 16:41
  • Also see Hardhat's source-maps.ts. Sep 4 at 16:44
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.

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.