Search code examples
ethereumgo-ethereumgethevm

Extracting emitted events (logs) from geth transaction trace (debug_traceCall)


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)


Solution

  • 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
    }])