EIP-3155: Create EVM Trace Specification

Discussion for EIP-3155 which can be found here: https://github.com/ethereum/EIPs/pull/3155

It tries to formalize the de-facto standard for tracing during state tests and move it to a more visible place s.th. it will be picked up by more implementations

1 Like

I added some in-line comments in the EIP. Generally summarized as

  • The CUT should not be part of the EIP, the standard is useful outside of just fuzz testing (the rpc debug_standardTraceBadBlocks for example)
  • field types need to be specified (what’s a hex string and what’s a json number, and unit for time, for example)
  • de-facto standard -> common format. I think Parity’s trace is more of a de-facto standard as we have many users asking for it. Mostly because of the internal transaction and state handling.
1 Like

Thank you very much for the comments! They are very helpful, I will insert them tomorrow.
Sorry about the misnomer of the common format, didn’t want to step on anyones toes with that.

Some nitpicks:

  • Hex-String data type perhaps could be better described as “hex-encoded byte array”.

  • It’s not clear what exactly returnStack is, please provide an example where it’s not empty.

  • Please provide an example where error is not empty.

  • Clients SHOULD output the fields in the same order as listed in this EIP.

According to the JSON spec “an object is an unordered set of name/value pairs”, so strictly speaking this requires something not supported by JSON.

  • The CUT MUST NOT output a line for the STOP operation if an error occurred: Example:

The example following this does in fact output STOP operation, so it’s contradictory, or an example for something else.

{"stateRoot":"0xd4c577737f5d20207d338c360c42d3af78de54812720e3339f7b27293ef195b7","output":"","gasUsed":"0x3","successful":"true","time":141485}

This example of a summary contains successful field not mentioned in the spec.

My experience is limited to some knowledge about tracing implementations inside EVMs and using tracing for debugging convoluted state tests.

The most confusing part of the current tracing is that it reports a kind of “in progress” state of an instruction execution if you consider precondition-checking a part of the execution. I.e. it reports the gas cost of the instruction (hopefully total gas cost but that was not the case in Aleth; does it also report total CREATE and CALL costs?) but not the execution result.

In one of my prototypes I changed that. The tracing there was reporting the state after instruction execution. This was in my opinion much more DevEx friendly.

Moreover, I also focused on limiting the amount data transferred from EVM. Together these provided additional nice options:

  1. Instead of dumping whole EVM stack, you can always dump only the top item. It can be noticed that an instruction pushes at most one value to the stack so the “stack top dump” is also the instruction execution result.

    {"opName":"PUSH1", "stackTop":"0x02"}
    {"opName":"DUP1", "stackTop":"0x02"}
    {"opName":"ADD", "stackTop":"0x04"}
    
  2. Instead of full memory dump, you can only report the modifiedMemory: the memory area where the instruction has written to. It can be noticed that an instruction may at most modify single continuous memory area. This also can be seen as the instruction execution result. If you report the instruction before execution the “modified memory” has no meaning.

    {"opName":"MSTORE", "modifiedMemoryOffset":"0x20", "modifiedMemory":"0x000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
    
  3. It is enough to just report gasLeft after the execution. The instruction gas cost can be easily computed from the gasLeft of the previous instruction.

  4. An error code can be meaningfully added to an instruction trace. Is this the meaning of the error field? However, this only make sense for the last instruction in a call as other instructions must be successful.

    {"opName":"POP", "error":"stack underflow"}
    

Lastly, the “new” tracing should provide the same information as the “legacy” tracing. Therefore, the “legacy” tracing format can be emulated by a statefull wrapper. If that is not the case, consider this a bug.

Many of these options have variants and alternatives. At this moment I only want to present an overview. Let me know if this direction is something you would like to explore.

References

  1. EVMC tracing prototype (and introducing PR) — never fully utilized by any EVM and finally removed from EVMC.
  2. Aleth implementation of the EVMC tracing prototype.

ethereumjs-vm reports I think both, because different use cases required the different versions. Maybe I remember it wrongly, and it was only discussed as an issue and one of the options was not merged. In any case I think getting input from both ethereumjs and the Remix team would be very valuable.

Ping @yann300 and @jochem-brouwer (I could not find any other ethereumjs dev here).

I’d prefer the 5-6 topmost items. Then you don’t have to backtrack up to (potentially) infinity lines to see what the inputs to an op were.

Clever!

Not sure about that. In a call, the gasCost was the cost of the call. The gasLeft is what you have available in this new execution frame.

Although, in general, your comments makes a presumptive traceviewer (such as my traceview: https://github.com/holiman/goevmlab#traceview ) forced to be come more stateful. In order to provide a memory dump, it needs to iterate through all the ops leading up to the point in question, if we only ever provide snippets.
Same with stack, but I already mentioned that.
So yes, it’ll make the trace(s) smaller, but it’ll also increase the complexity at the parsing/analysis side a whole lot.

Assuming that we mean by “EVM traces” the step event which the VM fires, then we only report the state of the VM before execution of an operation. We do not report the state right after running an operation. I think that @chfast raises a very good point that there are essentially “two” events happening: the first is the state of the VM before the operation runs, and the other is the state after the operation runs.

A very notable situation where this is important is if you invoke any CALL operation. In the stepBefore, we have the gas available before we run the CALL. Then afterStep, we deduct the call gas. But, the beforeStep in our new environment (new address), not only will the operation be different which we evaluate, but also our gas could have changed (since we have the 63/64 max forwarded gas rule).

I think it would make sense to add both these events in cases where it makes sense (it does not make sense to use beforeStep and afterStep just for a PUSH* operation).