Extracting emitted events (logs) from geth transaction trace (debug_traceCall)
Asked Answered
E

1

5

When using debug_traceCall, I get a low-level EVM trace of all opcodes and state changes during the execution. This is excessively detailed. When I use default callTracer, I can get a much nicer call tree. However, neither way I cannot seem to be able to extract the emitted events from the trace. I can see them in the trace (LOG* opcodes) however there is no easy way to actually parse them to something "readable" (along with values and originating address) There must be a way to get the logs - any ideas?

Eg. this is what Etherscan shows https://etherscan.io/tx-decoder?tx=0x3e3ad35fda1fddd9e154b3860b50371a1acd2fdb4f27f897e234846522bde732 (see Emitted Events section)

Eutherian answered 29/4, 2022 at 23:49 Comment(0)
E
11

So I figured this myself - I created a custom JavaScript tracer for geth that is passed to geth in 3rd param to debug_traceCall (see provided API reference by the link):

{
    data: [],
    fault: function (log) {
    },
    step: function (log) {
        var topicCount = (log.op.toString().match(/LOG(\d)/) || [])[1];
        if (topicCount) {
            var res = {
                address: log.contract.getAddress(),
                data: log.memory.slice(parseInt(log.stack.peek(0)), parseInt(log.stack.peek(0)) + parseInt(log.stack.peek(1))),
            };
            for (var i = 0; i < topicCount; i++)
                res['topic' + i.toString()] = log.stack.peek(i + 2);
            this.data.push(res);
        }
    },
    result: function () {
        return this.data;
    }
}

This tracer is executed by geth for each operation in the trace. Essentially what it does:

  • check if this is one of LOG0, LOG1, LOG2, LOG3 or LOG4 EVM opcodes
  • extract contract address from current contract
  • extract default topic0 and subsequent topics (if any)
  • extract additional event data from memory (note: stack[0] is offset, stack[1] is data size)

Passing the tracer to geth looks like this:

res = await ethersProvider.send('debug_traceCall', [{
    from: tx.from,
    to: tx.to,
    gas: BigNumber.from(tx.gas)._hex.replace('0x0', '0x'),
    gasPrice: BigNumber.from(tx.gasPrice)._hex.replace('0x0', '0x'),
    value: BigNumber.from(tx.value)._hex.replace('0x0', '0x'),
    data: tx.input
}, "latest", {
    tracer: "{\n" +
        "    data: [],\n" +
        "    fault: function (log) {\n" +
        "    },\n" +
        "    step: function (log) {\n" +
        "        var topicCount = (log.op.toString().match(/LOG(\\d)/) || [])[1];\n" +
        "        if (topicCount) {\n" +
        "            var res = {\n" +
        "                address: log.contract.getAddress(),\n" +
        "                data: log.memory.slice(parseInt(log.stack.peek(0)), parseInt(log.stack.peek(0)) + parseInt(log.stack.peek(1))),\n" +
        "            };\n" +
        "            for (var i = 0; i < topicCount; i++)\n" +
        "                res['topic' + i.toString()] = log.stack.peek(i + 2);\n" +
        "            this.data.push(res);\n" +
        "        }\n" +
        "    },\n" +
        "    result: function () {\n" +
        "        return this.data;\n" +
        "    }\n" +
        "}",
    enableMemory: true,
    enableReturnData: true,
    disableStorage: true
}])
Eutherian answered 30/4, 2022 at 7:39 Comment(1)
Is it okay that this transaction is causing a 'Regenerating historical state' process to run for 5 seconds on my fairly powerful Geth node?Denice

© 2022 - 2024 — McMap. All rights reserved.